【问题标题】:WPF themeing best practicesWPF 主题化最佳实践
【发布时间】:2015-08-14 21:01:51
【问题描述】:

这是关于 wpf 主题化,更具体地说是换肤最佳实践问题。 这更像是一个基于意见的问题,因为我在进行这项工作时没有问题,但更多的是一般想知道我的结论是否涵盖所有场景,以及是否有人对这个问题有相同的想法,他们的想法是什么接近。

一些背景知识,我们的团队需要定义一种方法,让我们的系统能够主题化

我们将此能力分为 2 类:

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

2) 他们用于自定义外观的资源称为“Skin”,其中包括 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 ControlTemplate 中,有一些元素使用 TemplateBinding 绑定到 DependencyProperties,还有一些元素,如 CornerRadius 和 InnerCornerRadius、InnerBorderThickness 和 InnerBorderBrush,它们的值来自资源。

最好的方法是什么?

创建具有相关依赖属性的派生控件 这将引用相关资源,然后将控件模板中的元素绑定到它们。

或者

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

使用依赖属性方法:

优点:

1) 清晰,我们为控件提供了更清晰的 API,并且更好地理解了控件的外观和行为方式。

2) 模板无需更改即可自定义。一切都通过风格来控制。

3) 触发器也可以更改控件的外观和感觉,而无需覆盖控件模板,也不需要 ControlTemplate 触发器。

4) “Blendabilty”使用 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> 

想想也不错…… 我们有时会通过 Selector 实现来做到这一点,它在摆脱非活动选择和高亮颜色时在内部使用特定资源,如下所示:

   <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) 控制模板应该追求简约并使用现有的依赖属性。像背景、前景、BorderBrush 等。

非常感谢您对此事的意见和想法,在此先感谢。

【问题讨论】:

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

标签: wpf resources styles themes


【解决方案1】:

正如 Xavier 所说,这对于 Code Review 来说可能是一个更好的问题。但我会就你的问题传达一些关键的想法,尽管其中很多会涉及个人(或团队)的风格和要求。

在创建了几十个主题之后,我建议尽可能不要使用自定义控件。随着时间的推移,可维护性会下降很多。

如果您需要对样式进行细微修改,如果情况允许,最好使用 DataTemplates 和 Data Triggers。这样你就可以干净利落地改变风格。

此外,您可以利用 BasedOn 属性。创建您的“基本”样式并拥有多个具有属性 BasedOn="{myBaseStyle} 的样式。这将允许您有很多选择而不会弄乱您的代码。

根据经验,我始终建议使用更多画笔/颜色/资源,而不是更多样式或模板。我们通常为颜色->画笔->样式->模板设置层次结构。这有助于重复使用颜色,同时仍然通过画笔保持分离。

在动态加载资源的某些情况下,使用 DynamicResource 而不是 StaticResource 也很有用。

希望这会有所帮助。很想写更多,但写一个坚实的主题的一些参数是非常具体的上下文。如果您有更多示例,我很乐意添加更多信息。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-04-09
    • 1970-01-01
    • 2018-07-07
    • 1970-01-01
    • 2013-01-08
    • 2011-08-06
    • 1970-01-01
    • 2015-09-10
    相关资源
    最近更新 更多