WPF主题化最佳实践

15

这是一个关于WPF主题和特别是皮肤的最佳实践问题。此问题更多基于个人意见,因为我没有问题使其正常工作,但我想知道我的结论是否涵盖了所有情况,以及其他人是否遇到了相同的问题思路以及他们的解决方案。

背景:我们的团队需要定义一种方法来让我们的系统能够进行主题设置。

我们将此能力分为两类:

1)控件的样式,我们简称为“主题”。

2)自定义它们外观所使用的资源,称为“皮肤”,其中包括画刷(Brushes)以及各种大小结构,例如CornerRadius、BorderThickness等。

设置系统的皮肤的方式就是将皮肤字典合并到应用程序资源中。

  <Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Default.skin.xaml" />
            <ResourceDictionary Source="Theme.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

我们的应用程序最后合并了另一个皮肤。

  protected override void OnStartup(StartupEventArgs e)
  {
       base.OnStartup(e);

       string skin = e.Args[0];
       if (skin == "Blue")
       {         .
            ResourceDictionary blueSkin = new ResourceDictionary();
            blueSkin.Source = new Uri("Blue.skin.xaml", UriKind.Relative);

            Application.Current.Resources.MergedDictionaries.Add(blueSkin);
       }
  }

在 Theme.xaml 中:

   <!-- Region TextBox ControlTemplate -->

<ControlTemplate TargetType="{x:Type TextBox}" x:Key="TextBoxTemplate">
    <Border  Background="{TemplateBinding Background}"  
         BorderBrush="{TemplateBinding BorderBrush}" 
         BorderThickness="{TemplateBinding BorderThickness}"
         CornerRadius="{StaticResource TextBoxCornerRadius}" >
      <Border x:Name="shadowBorder" BorderBrush="{StaticResource TextBoxShadowBrush}"                                   
        CornerRadius="{StaticResource TextBoxInnerShadowCornerRadius}" 
        BorderThickness="{StaticResource TextBoxInnerShadowBorderThickness}" 
        Margin="{StaticResource TextBoxInnerShadowNegativeMarginForShadowOverlap}" >
            <ScrollViewer x:Name="PART_ContentHost"  Padding="{TemplateBinding Padding}" 
                    VerticalAlignment="{TemplateBinding VerticalContentAlignment}" 
                    HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" />
        </Border>
    </Border>

    <ControlTemplate.Triggers>
        <Trigger Property="BorderThickness" Value="0">
            <Setter TargetName="shadowBorder" Property="BorderThickness" Value="0" />
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

<!-- EndRegion -->

<!-- Region TextBox Style -->

<Style x:Key="{x:Type TextBox}" TargetType="{x:Type TextBox}">     
    <Setter Property="BorderBrush" Value="{StaticResource TextBoxBorderBrush}" />
    <Setter Property="Background" Value="{StaticResource TextBoxBackgroundBrush}" />
    <Setter Property="BorderThickness" Value="{StaticResource TextBoxBorderThickness}" />

    <Setter Property="Padding" Value="{StaticResource TextBoxPadding}" />
    <Setter Property="Template" Value="{StaticResource TextBoxTemplate}"/>
  <Style.Triggers>

        <Trigger Property="IsMouseOver" Value="True">
            <Setter Property="Background" Value="{StaticResource TextBoxIsMouseOverBackgroundBrush}" />
            <Setter Property="BorderBrush" Value="{StaticResource TextBoxIsMouseOverBorderBrush}" />
        </Trigger>
        <Trigger Property="IsFocused" Value="True">
            <Setter Property="Background" Value="{StaticResource TextBoxIsMouseWithinBackgroundBrush}" />
            <Setter Property="BorderBrush" Value="{StaticResource TextBoxIsMouseWithinBorderBrush}" />
        </Trigger>

    </Style.Triggers>
</Style>

<!-- EndRegion -->

在 TextBox 控件模板中,使用 TemplateBinding 绑定到 DependencyProperties 的元素,还有一些像 CornerRadius、InnerCornerRadius、InnerBorderThickness 和 InnerBorderBrush 这样的元素,它们从资源中获取其值。

最佳实践是什么?

创建一个继承控件,该控件具有相关的依赖属性,引用相应的资源,然后让控件模板中的元素绑定到它们。

或者

让模板内的元素自己引用这些资源。

使用依赖属性方法:

优点:

1)清晰度更高,我们的控件具有更清晰的 API,更好地了解控件看起来和行为方式背后的原因。

2)模板不必更改即可进行自定义。通过样式控制一切。

3)触发器也会改变控件的外观和感觉,无需覆盖控件模板,无需 ControlTemplate 触发器。

4)"Blendability" 使用 Blend 我可以更轻松地自定义我的控件。

5)样式本身是可继承的。因此,如果我想改变控件的某个方面,我只需要继承默认样式即可。

缺点:

1)需要实现另一个自定义控件。

2)实现众多依赖属性,其中有些与控件并没有太多关系,只是为了满足模板中的某些需求而存在。

  • 这意味着需要从 TextBox 继承一个类似 InnerShadowTextBox 的控件,并在其中实现上述所有依赖属性。

如果模板中有许多需要自定义的元素,则会加重此问题。

例如:

  <Style x:Key="{x:Type cc:ComplexControl}" TargetType="{x:Type cc:ComplexControl}">
    <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type cc:ComplexControl}">
                    <Grid>
                        <Ellipse Fill="Red" Margin="0" Stroke="Black" StrokeThickness="1"/>
                        <Ellipse Fill="Green"  Margin="6" Stroke="Red" StrokeThickness="1"/>
                        <Ellipse Fill="Blue" Margin="12"/>
                        <Ellipse Fill="Aqua" Margin="24" />
                        <Ellipse Fill="Beige" Margin="32"/>
                        <StackPanel Orientation="Horizontal" Width="25" Height="25"
                                    VerticalAlignment="Center" HorizontalAlignment="Center">
                            <Rectangle Fill="Black" Width="2" />
                            <Rectangle Fill="Black" Width="2" Margin="2,0,0,0"/>
                            <Rectangle Fill="Black" Width="2" Margin="2,0,0,0"/>
                            <Rectangle Fill="Black" Width="2" Margin="2,0,0,0"/>
                        </StackPanel>

                    </Grid>
                </ControlTemplate>
            </Setter.Value>
    </Setter>
</Style>

这将需要大量的资源:

 <SolidColorBrush x:Key="Ellipse1Fill">Red</SolidColorBrush>
 <SolidColorBrush x:Key="Ellipse2Fill">Green</SolidColorBrush>
 <SolidColorBrush x:Key="Ellipse3Fill">Blue</SolidColorBrush>
 <SolidColorBrush x:Key="Ellipse4Fill">Aqua</SolidColorBrush>
 <SolidColorBrush x:Key="Ellipse5Fill">Beige</SolidColorBrush>
 <SolidColorBrush x:Key="Ellipse1Stroke">Beige</SolidColorBrush>
 <sys:Double x:Key="Ellipse1StrokeThickness>1</sys:Double>
      ......... and many more 

无论如何,我都会有一个大的资源列表。但使用依赖属性的话,我还需要分配需要在每个小部件中找到意义的任务,有时候不过是因为"它看起来不错",与控件本身并没有多大关系,或者说如果我明天想要更改模板怎么办。

采用从控件模板内引用资源的方法。

优点:

1) 易于使用,避开了 Dp 方法中描述的缺点所述的丑陋,同时提供了一种"hack"来实现主题。

缺点:

1) 如果我想要进一步自定义我的控件,比如添加一个触发器来影响 TextBox 的内部边框,我只需要创建一个新的控件模板即可。

2) 没有清晰的 API,请说我想要在特定视图中更改内部边框的 BorderBrush。

         <TextBox>
             <TextBox.Resources>
                  <SolidColorBrush x:Key="InnerBorderBrush" Color="Red" />
             </TextBox.Resources>
          </TextBox> 

想想也不是那么糟糕......我们有时会对使用特定资源的选择器实现进行这样的操作,以便在摆脱非活动选择和突出显示颜色时内部使用这些资源:

   <ListBox>
       <ListBox.Resources>
          <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent"/>
          <SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Transparent"/>
          <SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="Transparent"/>
          <SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}" Color="Transparent"/>
       </ListBox.Resources>
 </ListBox>

结论:

如上所述的混合方法是前进的方向。

1) 仅为控件逻辑及其特定模板部分相关方面引入依赖属性。

2) 资源名称应由清晰的命名规范组成,并根据它们所关联的控件和在视图中常见的用法分别放置在文件中,例如在我们应用程序中常用的通用画刷。

3) 控件模板应力求简约,并使用现有的依赖属性,例如Background、Foreground、BorderBrush等。

非常感谢您对此问题的意见和想法,提前致谢。


由于这个问题的性质,您最好将其发布在Code Review网站上。 - Xavier
1个回答

4
正如Xavier所说,这可能更适合在代码审查中提出。但是我将传达一些关于您的问题的关键想法,尽管其中很多都涉及个人(或团队)风格和要求。
创建了几十个主题后,我建议尽可能避免使用自定义控件。随着时间的推移,可维护性会大大降低。
如果您需要对样式进行轻微修改,最好使用Data Templates和Data Triggers(如果情况允许)。这样,您可以以干净的方式更改样式。
此外,您可以利用BasedOn属性。创建您的“基础”样式,并具有多个样式,具有属性BasedOn =“ {myBaseStyle}。这将为您提供许多选项,而不会使您的代码混乱。
作为经验法则,我总是建议拥有更多的brushes/颜色/资源,而不是更多的样式或模板。我们通常将层次结构设置为颜色-> brushes-> styles-> templates。这有助于重用颜色,同时通过画笔仍然保持分离。
在某些情况下,使用DynamicResource而不是StaticResource也很有用,例如在动态加载资源时。
希望这有所帮助。我很想写更多,但编写稳健主题的一些参数非常具体。如果您有更多的例子,我很乐意添加更多信息。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接