Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

A Custom WPF Slider Button

4.95/5 (7 votes)
24 Apr 2019CPOL4 min read 38.7K   2.3K  
This article describes a simple slider button using dependency properties and a template.

Introduction

This article describes a simple WPF two state slider button. The text displayed for each state, and the size of the slider button are configurable from XAML.

The slider button is configured using a combination of dependency properties and template resources. The sample code includes three template resources which allow you to create the following:

  1. A slider control with round ends, a round button, and a label on the left
  2. A slider control with round ends, a round button, and a label on the slider track
  3. A slider control with square ends, a square button, and a label on the slider track

Background

We needed a simple slider button for our product, and since Microsoft does not provide one as standard, I was forced to write a custom control. There are many examples online but most if not all tend to hard code the appearance. Thus the colours are fixed, or the text is fixed, or the size is fixed. The more flexible ones tend to be rather complex. We wanted something that was more flexible, with the appearance including the size (width and height) controlled by attributes in the XAML code. A key requirement was for the code to be as simple as possible, which ensures that it is easy to understand, and easy to extend to suit your own needs if required. I could have made it even more flexible, but the current version does everything I require.

Ideally, you should be familiar with the basics of dependency properties, resource dictionaries, and control templates. However, even if your knowledge is limited, you should be able to figure out how to use the code, and be able to make simple changes to suit your own requirements.

Screenshots

The demonstration application includes examples of all three styles:

Using the Code

The SliderControl is derived from the standard WPF ToggleButton class. It adds three dependency properties:

ButtonWidth The width of the slider button
OnLabel The text to display when the button is set to the on position
OffLabel The text to display when the button is set to the off position

The overall width of the button is controlled by the Width property of the base control. The height is controlled by the ButtonWidth custom property.

There are three styles which define three forms of slider button:

styleSliderButtonSideLabel A slider button with a label on the left hand side
styleSliderButton A slider button with a circular button
styleSliderButtonRectangular A slider button with a rectangular button

Each slider button style defines a template that overrides the appearance of the control. Each slider button is constructed from a combination of ellipse, border and label controls aligned using grid controls. Triggers are used to show and hide component controls as and when the button is checked and unchecked.

The SliderButton Class

The SliderButton class is derived from the standard ToggleButton class and adds three dependency properties:

C++
namespace WpfSliderButtonDemo.View
{
    public class SliderButton : System.Windows.Controls.Primitives.ToggleButton
    {
        public double ButtonWidth
        {
            get
            {
                return (double)GetValue(ButtonWidthProperty);
            }
            set
            {
                SetValue(ButtonWidthProperty, value);
            }
        }

        public static readonly System.Windows.DependencyProperty ButtonWidthProperty = 
               System.Windows.DependencyProperty.Register("ButtonWidth", typeof(double), 
               typeof(SliderButton), new System.Windows.PropertyMetadata(0.0));

        public string OnLabel
        {
            get
            {
                return (string)GetValue(OnLabelProperty);
            }
            set
            {
                SetValue(OnLabelProperty, value);
            }
        }

        public static readonly System.Windows.DependencyProperty 
               OnLabelProperty = System.Windows.DependencyProperty.Register
               ("OnLabel", typeof(string), typeof(SliderButton), 
               new System.Windows.PropertyMetadata(""));

        public string OffLabel
        {
            get
            {
                return (string)GetValue(OffLabelProperty);
            }
            set
            {
                SetValue(OffLabelProperty, value);
            }
        }

        public static readonly System.Windows.DependencyProperty OffLabelProperty = 
               System.Windows.DependencyProperty.Register("OffLabel", typeof(string), 
               typeof(SliderButton), new System.Windows.PropertyMetadata(""));
    }
}

The styleSliderButton Style

The styleSliderButton style defines a slider button with a circular button and a label on the slider track:

XML
<Style x:Key="styleSliderButton" TargetType="{x:Type local:SliderButton}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:SliderButton}">
                <Grid x:Name="mainGrid">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
                    <Border Grid.Column="0" Name="_borderOn"
                     Background="Transparent" Width="{TemplateBinding Width}">
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"/>
                            </Grid.RowDefinitions>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="*" />
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="Auto"/>
                            </Grid.ColumnDefinitions>
                            <Ellipse Grid.Row="0" Grid.RowSpan="1"
                             Grid.Column="0" Grid.ColumnSpan="2"
                             Width="{TemplateBinding ButtonWidth}"
                             Height="{TemplateBinding ButtonWidth}"
                             Style="{StaticResource styleEllipseGreyButton}"
                             Panel.ZIndex="3" />
                            <Border Grid.Row="0" Grid.RowSpan="1" Grid.Column="1"
                             Grid.ColumnSpan="3" Background="ForestGreen"
                             BorderBrush="Gray" BorderThickness="0,1,0,1" Panel.ZIndex="1"/>
                            <Label Grid.Row="0" Grid.RowSpan="1" Grid.Column="2"
                             Grid.ColumnSpan="3" Name="_labelOn"
                             Content="{TemplateBinding OnLabel}"
                             SPanel.ZIndextyle=
                                 "{StaticResource styleSliderButtonLabel}" ="2"/>
                            <Ellipse Grid.Row="0" Grid.RowSpan="1" Grid.Column="3"
                             Grid.ColumnSpan="2" Width="{TemplateBinding ButtonWidth}"
                             Height="{TemplateBinding ButtonWidth}"
                             Style="{StaticResource styleEllipseButton}"
                             Fill="ForestGreen" Panel.ZIndex="0"/>
                        </Grid>
                    </Border>

                    <Border Grid.Column="0" Name="_borderOff"
                     Background="Transparent" Width="{TemplateBinding Width}">
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"/>
                            </Grid.RowDefinitions>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="*" />
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="Auto"/>
                            </Grid.ColumnDefinitions>
                            <Ellipse Grid.Row="0" Grid.RowSpan="1" Grid.Column="0"
                             Grid.ColumnSpan="2" Width="{TemplateBinding ButtonWidth}"
                             Height="{TemplateBinding ButtonWidth}"
                             VerticalAlignment="Stretch" Fill="Crimson"
                             Stroke="Gray" Panel.ZIndex="0"/>
                            <Label Grid.Row="0" Grid.RowSpan="1" Grid.Column="0"
                             Grid.ColumnSpan="3" Name="_labelOff"
                             Content="{TemplateBinding OffLabel}"
                             Style="{StaticResource styleSliderButtonLabel}"
                             Panel.ZIndex="2"/>
                            <Border Grid.Row="0" Grid.RowSpan="1" Grid.Column="1"
                             Grid.ColumnSpan="3" Background="Crimson" BorderBrush="Gray"
                             BorderThickness="0,1,0,1" Panel.ZIndex="1"/>
                            <Ellipse Grid.Row="0" Grid.RowSpan="1"
                             Grid.Column="3" Grid.ColumnSpan="2"
                             Width="{TemplateBinding ButtonWidth}"
                             Height="{TemplateBinding ButtonWidth}"
                             Style="{StaticResource styleEllipseGreyButton}"
                             Panel.ZIndex="3"/>
                        </Grid>
                    </Border>
                </Grid>

                <!-- triggers toggle visual appearance -->
                <ControlTemplate.Triggers>
                    <Trigger Property="IsChecked" Value="True">
                        <Setter TargetName="_borderOff" Property="Visibility"
                         Value="Collapsed" />
                        <Setter TargetName="_borderOn" Property="Visibility"
                         Value="Visible" />
                    </Trigger>
                    <Trigger Property="IsChecked" Value="False">
                        <Setter TargetName="_borderOff" Property="Visibility"
                         Value="Visible" />
                        <Setter TargetName="_borderOn" Property="Visibility"
                         Value="Collapsed" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

The style overrides the Template property which defines the appearance and behaviour of the control. The control is constructed from a grid with two compound elements, one which is shown when the control is checked, and the other when it is unchecked. Each compound element consists of a grid with ellipse, border and label child elements which together create the appearance of a slider control.

Note the use of the Panel.ZIndex attached property to allow the slider button ellipse to appear in front of the track, and the track to appear in front of the ellipse that represents the track end.

Note also the use of the TemplateBinding markup extension, which allows an element in the template to use a property defined on the templated control, i.e., the SliderButton, including properties defined on the ToggleButton control class.

The slider button is created using an ellipse element with the styleEllipseGreyButton style:

XML
<Style x:Key="styleEllipseGreyButton" TargetType="{x:Type Ellipse}"
 BasedOn="{StaticResource styleEllipseButton}">
    <Setter Property="Fill">
        <Setter.Value>
            <LinearGradientBrush EndPoint="0.504,1.5" StartPoint="0.504,0.03">
                <GradientStop Color="#FFFFFF" Offset="0"/>
                <GradientStop Color="#BBBBBB" Offset="0.567"/>
            </LinearGradientBrush>
        </Setter.Value>
    </Setter>
</Style>

The appearance of the label text is defined using the styleSliderButtonLabel style:

XML
<Style x:Key="styleSliderButtonLabel" TargetType="{x:Type Label}">
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="Foreground" Value="White"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="VerticalAlignment" Value="Center"/>
    <Setter Property="HorizontalAlignment" Value="Center"/>
    <Setter Property="HorizontalContentAlignment" Value="Center"/>
    <Setter Property="Padding" Value="0"/>
</Style>

Using the Control

Adding the custom slider control to a view is extremely easy:

XML
<view:SliderButton Grid.Row="7" Grid.Column="1" OnLabel="On" OffLabel="Off" 
 Width="110" ButtonWidth="50" Style="{StaticResource styleSliderButton}" 
 HorizontalAlignment="Center" IsChecked="{Binding IsEnabled}" FontSize="30"/>

Sample Application

I have provided a simple demonstration application that you can download. It includes the code for the slider button, and a main window containing numerous examples of the slider button using the provided styles.

Further Enhancements

Future enhancements include the creation of a vertical slider button, and the addition of dependency properties to control the background colours for the two states. These tasks are left as an exercise for the reader.

Bugs

I was unable to get the label text to be vertically centred. This is not obvious, but ideally it should be fixed.

Points of Interest

WPF is extremely flexible, and there are many ways to create the illusion of a toggle button using standard controls.

History

  • 24th April, 2019: First version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)