这是关于wpf themeing 的 best practices 问题,更具体地说是 skinning . 这更像是一个基于意见的问题,因为我没有问题使这项工作,但更多的是一般想知道我的结论是否涵盖所有情景,以及是否有其他人在这个问题上遇到同样的想法,他们是什么方法 .
一些背景,我们的团队需要定义一种方法,使我们的系统能够成为 themeable .
我们将此功能分为两类:
1)我们的控件的样式我们简称为' Theme ' .
2)用于定制其外观的资源称为“ Skin ”,这包括画笔,以及各种大小调整结构,如CornerRadius,BorderThickness等 .
为系统设置Skin的方法是将皮肤字典最后合并到应用程序资源中的简单情况 .
<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);
}
}
Inside 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,它们从资源中获得它们的值 .
什么是最好的方法?
使用相关的依赖项属性创建派生控件,该属性将引用相关资源,然后让控件模板中的元素绑定到它们 .
Or
让模板中的元素自己引用这些资源 .
Using the Dependency Property approach :
Advantages :
1)清晰度,我们有一个更清晰的API用于控制和更好地理解我们的控件的外观和行为方式 .
2)模板不必更改以便可自定义 . 一切都是通过风格控制的 .
3)触发器也可以更改控件的外观,而无需覆盖控件模板,无需ControlTemplate触发器 .
4)使用混合物的“Blendabilty”可以很容易地定制我的控制 .
5)样式本身是可继承的 . 因此,如果我只想改变控件的一个方面,我需要做的就是从默认样式继承 .
Disadvantages :
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
无论哪种方式,我都会有大量的资源 . 但是依赖属性 . 我还需要指定需要在每一个小部分中找到意义,有时候并不是“它看起来很好”并且与控件没有太大关系,或者如果明天我想要更改模板怎么办 .
Using the approach where the resources are referenced from within the control template.
Advantages :
1)易于使用,边缘步骤丑陋描述了上述Dp方法中描述的缺点,同时提供了能够实现主题的“黑客” .
Disadvantages :
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>
Conclusions :
上面的TextBox Style中描述的混合是可行的方法 .
1)依赖性属性仅用于与控件逻辑相关的控件方面,包括特定模板部分 .
2)资源名称将由一个明确的命名约定组成,并根据它们所涉及的控件和视图中的常见用法分隔在文件中,就像在我们的应用程序中的视图中使用的Common Brush一样 .
3)控件模板应该尽量简约,并使用现有的依赖属性 . 像背景,前景,BorderBrush等 .
我非常感谢您对此事的意见和建议,并提前感谢 .
1 回答
正如Xavier所说,对于Code Review来说,这可能是一个更好的问题 . 但我会就你的问题传达一些关键的想法,尽管其中很多都会涉及到个人(或团队)的风格和要求 .
在创建了几十个主题之后,我建议尽可能不使用自定义控件 . 随着时间的推移,可维护性会下降很多 .
如果您需要对样式进行少量修改,最好在情境允许的情况下使用DataTemplates和Data Triggers . 这样你就可以以干净的方式改变风格 .
此外,您可以利用基于 properties . 创建“基础”样式并具有多个样式,这些样式具有属性BasedOn =“ . 这将允许您使用许多选项而不会使代码混乱 .
根据经验,我总是建议使用更多画笔/颜色/资源而不是更多样式或模板 . 我们通常为colors-> brushes-> styles-> templates设置层次结构 . 这有助于重复使用颜色,同时仍然通过刷子保持分离 .
在动态加载资源的某些情况下,使用DynamicResource而不是StaticResource也很有用 .
希望这可以帮助 . 愿意写更多,但编写一个可靠主题的一些参数是非常特定于上下文的 . 如果您有更多示例,我很乐意添加更多信息 .