Use TemplateSettings to tweak your ProgressBar

Tags: Windows 10, UWP, StackOverflow

I check StackOverflow now and then for UWP questions that aren’t sniped by people trying to get quick and easy reputation (read: tougher questions or those that need quite some effort). I first thought that this one would be another simple ‘point to the default styles’ question, until I looked deeper into it. It turned out to be a fun and challenging hour of coding.

Task at hand

The questioner on StackOverflow wants to increase the dot size of the ProgressBar.

So why isn’t it one of those default easy style questions? Let’s dig into the default ProgressBar style and zoom in on the actual ellipse parts in question.

<Border x:Name="B1"
        Grid.Column="8"
        RenderTransformOrigin="0.5,0.5">
    <Border.RenderTransform>
        <TranslateTransform />
    </Border.RenderTransform>
    <Ellipse x:Name="E1"
             Fill="{TemplateBinding Foreground}"
             Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
             Height="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
             RenderTransformOrigin="0.5,0.5">
        <Ellipse.RenderTransform>
            <TranslateTransform />
        </Ellipse.RenderTransform>
    </Ellipse>
</Border>
<Rectangle Grid.Column="7"
           Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseOffset}" />

My initial idea: let’s just increase the EllipseDiameter value and we’re done. Both the TemplateSettings and the EllipseDiameter are read-only properties, bad luck there.

So what are the alternatives to increase the size?

Yes, you could make a new style being a copy from the default style and edit those properties. But 297 lines of XAML is quite a bit and you’re now introducing a fixed version of that template. If the product team happens to improve the visual tree or change some of the used styles or dependency properties in a future Windows SDK version, you’re in for extra work. I did in my sample app to make a small rollercoaster bar, which looks funny … but I’d like to prevent this approach in a large sized app with several customizations to maintain.

The easiest one by far is using a ViewBox control, which scales whatever is in the container from its original size to the available size. Works perfect, but isn’t the cleanest fix and could even introduce blurry graphics if you size up too much.

Templated control

So back to step one: what about TemplateSettings? Template setting classes are part of the Windows.UI.Xaml.Controls.Primitives namespace and contain read-only properties to be used in XAML when creating control templates. If you want to know more, this MSDN page is the entry point.

So we want to mess with control templates? Let’s create a templated control (also known as custom control) in our project so we can start customizing the looks of our progress bar. The TemplateSettings property is not only read-only, but the ProgressBarTemplateSettings type is also sealed. So far for using inheritance. Digging into the metadata, you’ll notice this type inherits from DependencyObject and IProgressBarTemplateSettings (which is internal) and has a limited number of properties. So I simply created my own type with the same properties.

public class MyProgressBarTemplateSettings : DependencyObject
{
    public double ContainerAnimationEndPosition { get; set; }
    public double ContainerAnimationStartPosition { get; set; }
    public double EllipseAnimationEndPosition { get; set; }
    public double EllipseAnimationWellPosition { get; set; }
    public double EllipseDiameter { get; set; }
    public double EllipseOffset { get; set; }
    public double IndicatorLengthDelta { get; set; }
}

Next step is creating my own control derived from the default ProgressBar control (as I’m not planning to rewrite the control from scratch). I intentionally hide the TemplateSettings property of the base control with the new keyword.

public sealed class MyProgressBar : Windows.UI.Xaml.Controls.ProgressBar
{
    public MyProgressBar()
    {
        this.DefaultStyleKey = typeof (MyProgressBar);
        this.Loaded += (sender, args) => TemplateSettings = new MyProgressBarTemplateSettings(this);
    }

    public static readonly DependencyProperty TemplateSettingsProperty = DependencyProperty.Register(
        "TemplateSettings", typeof (MyProgressBarTemplateSettings), typeof (MyProgressBar),
        new PropertyMetadata(default(MyProgressBarTemplateSettings)));

    public new MyProgressBarTemplateSettings TemplateSettings
    {
        get { return (MyProgressBarTemplateSettings) GetValue(TemplateSettingsProperty); }
        set { SetValue(TemplateSettingsProperty, value); }
    }
}

Now I simply have to fill in some values for these properties. I don’t know the algorithms behind the properties, but looking at what came out of the ProgressBar control properties, I somewhat aimed for the same numbers based on the control’s width (which I have once the control is loaded).

public MyProgressBarTemplateSettings(MyProgressBar progressBar)
{
    ContainerAnimationStartPosition = -1*progressBar.ActualWidth / 10;
    ContainerAnimationEndPosition = progressBar.ActualWidth * 3/8;
    EllipseAnimationEndPosition = progressBar.ActualWidth * 2/3;
    EllipseAnimationWellPosition = progressBar.ActualWidth / 3;
    EllipseOffset = 4;
    EllipseDiameter = 12;
}

There are probably better ways to calculate these values, but this works for me.

Now there’s one thing left: a templated control has a template defined in the Themes/Generic.xaml file. As I derive from ProgressBar, I can copy the default template and simply change the TargetType properties.

<Style TargetType="local:MyProgressBar" >
    <Setter Property="Foreground" Value="{ThemeResource SystemControlHighlightAccentBrush}" />
    <Setter Property="Background" Value="{ThemeResource SystemControlBackgroundBaseLowBrush}" />
    <Setter Property="BorderThickness" Value="{ThemeResource ProgressBarBorderThemeThickness}" />
    <Setter Property="Maximum" Value="100" />
    <Setter Property="MinHeight" Value="{ThemeResource ProgressBarThemeMinHeight}" />
    <Setter Property="IsTabStop" Value="False" />
    <Setter Property="VerticalAlignment" Value="Center" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:MyProgressBar">
     ...

Wait what? We did all this work to yet again copy all 297 lines of XAML and simply change the TargetType properties? Now that would be silly right? Since I’m intentionally hiding the TemplateSettings property with the new keyword, this property exists and my derived type’s values are used. So we can simply use the style of ProgressBar instead.

public MyProgressBar()
{
    this.DefaultStyleKey = typeof (Windows.UI.Xaml.Controls.ProgressBar);
    this.Loaded += (sender, args) => TemplateSettings = new MyProgressBarTemplateSettings(this);
}

Sample

In my sample on GitHub, I’m showing all the alternatives mentioned above. I’ve added the EllipseOffset in code, so the bottom progress bar has some spacing between the dots as well instead of the gif shown below.

Comments

comments powered by Disqus