这是一个关于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等。
非常感谢您对此问题的意见和想法,提前致谢。