简体   繁体   中英

WPF Derive Control but Keep Style

I'm working on an application in which I have a lot of sliders with labels above them like so:

带标签的滑块

In the interest of re-usability, I'd like to create a custom LabeledSlider control with extra properties for the left and right label strings. A simple UserControl seemed like a good fit at first, until I realized I would have to re-implement all of the Slider properties and methods I planned to use (there are a lot of them).

So inheriting from Slider seems like the better choice, but it looks like I have to create a custom style to do so. Is there a way to derive from Slider and add these labels while preserving the existing style?

For what it's worth, I'm using MahApps.Metro .

If you want to avoid all the plumbing that comes with realizing your custom slider with a UserControl, you can instead use a Slider control directly with a custom control template.


The control template

The custom control template would define all the UI elements necessary to present your custom slider; like the two labels, an "inner" Slider control (the actual slider shown in the UI), and any other desired/required UI elements.

An (incomplete) illustration of how a simple form of such a control template could look like:

    <Slider
        TickPlacement="Both"
        TickFrequency="0.5"
        BorderBrush="Red"
        BorderThickness="3" >
        <Slider.Template>

            <ControlTemplate TargetType="Slider">
                <Border
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition />
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition />
                            <ColumnDefinition />
                        </Grid.ColumnDefinitions>

                        <TextBlock Text="Left label" Grid.Column="0" Grid.Row="0" />
                        <TextBlock Text="Right label" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Right" />
                        <Slider Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1"
                            BorderThickness="0"
                            Value="{Binding Value, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
                            TickFrequency="{TemplateBinding TickFrequency}
                            TickPlacement="{TemplateBinding TickPlacement}"
                            ...
                        />
                    </Grid>
                </Border>
            </ControlTemplate>

        </Slider.Template>
    </Slider>

Note that all the properties of the "outer" slider that should be passed through to the "inner" slider should have corresponding TemplateBindings for the "inner" slider (as illustrated with the Value , TickFrequency and TickPlacement properties).

Pay attention to the template binding of the Value property. For any template binding that needs to be two-way, the shorthand form {TemplateName SourceProperty} will not do as it is one-way. For two-way template bindings, the binding should be declared like {Binding SourceProperty, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay} .

Some properties of the "outer" slider you might not wish to pass through the "inner" slider. Perhaps you would like the border (a slider control supports borders) to surround the slider together with the text labels. For this case, my example control template above features a Border element surrounding the "inner" slider and the text labels. Note that border-related properties from the "outer" slider control using the control template are being template-bound to this Border element and not the "inner" slider.

However, there would still be an issue with how the border is handled. If you (or someone else) at some time defines a default style for sliders that incorporates a border, then the respective border parameters of this default style would also apply to the "inner" slider -- something that would be undesired. To prevent this from happening, my example control template explicitly sets the value of BorderThickness for the "inner" slider.

Do so in a similar manner for any other properties of the UI elements in the control template you do not wish to be affected by their respective default styles.


Attached properties for the label text

If you wish to be able to change the text of the labels through bindings, you need to introduce some properties for them. One way of achieving this is to realize them as attached properties. Their implementation can be rather simple:

public static class SliderExtensions
{
    public static string GetLeftLabel(DependencyObject obj)
    {
        return (string)obj.GetValue(LeftLabelProperty);
    }

    public static void SetLeftLabel(DependencyObject obj, string value)
    {
        obj.SetValue(LeftLabelProperty, value);
    }

    public static readonly DependencyProperty LeftLabelProperty = DependencyProperty.RegisterAttached(
        "LeftLabel",
        typeof(string),
        typeof(SliderExtensions)
    );


    public static string GetRightLabel(DependencyObject obj)
    {
        return (string)obj.GetValue(RightLabelProperty);
    }

    public static void SetRightLabel(DependencyObject obj, string value)
    {
        obj.SetValue(RightLabelProperty, value);
    }

    public static readonly DependencyProperty RightLabelProperty = DependencyProperty.RegisterAttached(
        "RightLabel",
        typeof(string),
        typeof(SliderExtensions)
    );
}

The code above provides two attached properties (of type string ) called "SliderExtensions.LeftLabel" and "SliderExtensions.RightLabel". You could then employ them as follows:

    <Slider
        local:SliderExtensions.LeftLabel="Port"
        local:SliderExtensions.RightLabel="Starboard"
        TickPlacement="Both"
        TickFrequency="0.5"
        BorderBrush="Red"
        BorderThickness="3" >
        <Slider.Template>
            <ControlTemplate TargetType="Slider">
                ...
                <TextBlock Text="{TemplateBinding local:SliderExtensions.LeftLabel}" Grid.Column="0" Grid.Row="0" />
                <TextBlock Text="{TemplateBinding local:SliderExtensions.RightLabel}" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Right" />
                 ...
             ...


A custom slider class with dependency properties for the label text

If you don't want to use attached properties, you could also derive your own custom slider control from the standard Slider class and implement LeftLabel and RightLabel as dependency properties of this custom slider class.

public class MyCustomSlider : Slider
{
    public string LeftLabel
    {
        get { return (string)GetValue(LeftLabelProperty); }
        set { SetValue(LeftLabelProperty, value); }
    }

    public static readonly DependencyProperty LeftLabelProperty = DependencyProperty.Register(
        nameof(LeftLabel),
        typeof(string),
        typeof(MyCustomSlider)
    );


    public string RightLabel
    {
        get { return (string)GetValue(RightLabelProperty); }
        set { SetValue(RightLabelProperty, value); }
    }

    public static readonly DependencyProperty RightLabelProperty = DependencyProperty.Register(
        nameof(RightLabel),
        typeof(string),
        typeof(MyCustomSlider)
    );
}

This approach has the added benefit that you are able to define a default style specifically for your MyCustomSlider type. See this answer for more information related to that topic.

If you prefer to do this, then don't forget to adjust the target type of the control template:

    <local:MyCustomSlider
        LeftLabel="Port"
        RightLabel="Starboard"
        TickPlacement="Both"
        TickFrequency="0.5"
        BorderBrush="Red"
        BorderThickness="3" >
        <local:MyCustomSlider.Template>
            <ControlTemplate TargetType="TargetType="local:MyCustomSlider"">
                ...
                <TextBlock Text="{TemplateBinding LeftLabel}" Grid.Column="0" Grid.Row="0" />
                <TextBlock Text="{TemplateBinding RightLabel}" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Right" />
                 ...
             ...

You are not forced to use string properties and TextBlocks for the labels. You could use ContentPresenter instead of the TextBlock and make the LeftLabel and RightLabel properties of type object . This way you can still use text labels but - if desired - you could also use any other content for them (such as images, for example).


Side note: My answer is based on the standard WPF slider control. If you are using a slider control provided by MahApps.Metro (which i do know only very little about), certain details -- such as the naming, type, presence or absence of properties -- might perhaps differ from what my answer shows.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM