WPF 控件组合(第 2 部分,共 2 部分)






4.86/5 (12投票s)
为现有用户控件添加主题可在应用程序端增加灵活性,而无需更改原始实现。本文通过为之前未完全支持主题化的用户控件添加主题来提供一个示例。
引言
WPF 中的主题化技术是一种有效的编码方式,可以一次编写代码,并通过各个用户界面增强用户体验
体验(可复用的 WPF 自动完成文本框(第 2 部分))。这系列两篇文章的这一部分着重于为前一部分中讨论的水印文本框控件添加主题。具体来说,我们将创建一个无外观的自定义水印文本框控件。

如果您对主题化之外的任何问题有疑问,请回顾上一篇文章。
编译代码
StyleCop
我在我的项目中使用 StyleCop,以统一的方式使代码可读。因此,如果您在编译项目时遇到错误,您可以下载并安装 StyleCop,或者编辑/删除每个 .csproj 文件中相应的条目
<Import Project="$(ProgramFiles)\MSBuild\StyleCop\v4.7\StyleCop.Targets" />
为演示应用程序添加主题
通常,我使用一个单独的 DLL 项目来为任何其他应用程序添加主题。但为了简化事情,我只是添加了
- 一个包含 3 个主题的 Themes 文件夹
- 一个 MainCommands 命令类,用于绑定和执行 viewTheme 命令
- 一个菜单栏,让用户选择主题
- 一个 TypeOfTheme 枚举和一些基于它的代码,用于在请求时管理、选择和更改为特定主题
ChangeThemeCommand_Executed
函数。它接受要更改的主题名称作为字符串参数,该参数由
菜单栏项调用提供。主题名称映射到一个枚举,该枚举又映射到一个包含主主题 XAML 文件资源地址的静态常量字符串数组。
我知道这都是非常手动化的,并不是您会在生产场景中编写的代码。
但演示应用程序真的只是我们来这里所必需的。
我们来这里是为了学习更多关于编程无外观控件的知识。那么,接下来我们讨论这个。
创建无外观自定义控件
为 DLL 项目中包含的 WPF 控件添加主题需要 4 件事- 一个包含 Generic.xaml 文件的 Themes 文件夹
- AssemblyInfo 文件中的 ThemeInfo 条目
- 从用户控件中删除 XAML
- 使辅助类公开
Generic.xaml
使 WPF 控件完全可换肤需要一个包含 Generic.xaml 文件的 Themes 文件夹。Generic.xaml 文件应包含自定义控件的默认声明。当没有替代声明时,WPF 框架会应用该默认声明。在我们的例子中,测试应用程序中的通用主题不包含
TextBoxWithWatermark
控件的定义。因此,当用户在演示应用程序中选择通用主题时,将应用 DLL 项目中 Themes/Generic.xaml
文件中的定义。其他两个主题包含 TextBoxWithWatermark
控件的单独定义,这使我们能够以主题特定的颜色为水印着色。解释
Generic.xaml
文件和演示应用程序中的主题文件中的所有内容对本文来说太多了。但您应该注意以下几点DLL 项目 Themes 文件夹中
Generic.xaml
文件开头的两个颜色语句<SolidColorBrush x:Key="brushWatermarkForeground" Color="#AA000033" />
<SolidColorBrush x:Key="brushWatermarkBackground" Color="Transparent" />
...定义文本框中显示的水印的背景色和前景色。这些颜色资源在下面的 XAML 中使用。
<Style TargetType="{x:Type local:TextBoxWithWatermark}"> <Setter Property="SnapsToDevicePixels" Value="True"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:TextBoxWithWatermark}"> <ControlTemplate.Resources> <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" /> <local:WatermarkHelper x:Key="WatermarkHelper" /> </ControlTemplate.Resources> <Grid> <Grid.RowDefinitions>
根部的 Style
标签告诉 WPF,我们将为每个
没有替代样式的 TextBoxWithWatermark
控件定义样式。替代样式可以在以下位置定义:
- 主题特定的 XAML 文件(参见
DarkExpresssion/SimpleStyles/SimpleControls.xaml
)或 - 通过分配给
Style
属性的控件特定样式资源
(此处未涵盖,请参阅如何:定义和引用资源)。
最基本的样式应用包括一个 Setter
属性标签,其中 Property
属性选择属性名称,Value
属性选择我们希望为特定样式定义的实际值。Template
setter 更为复杂。它可用于为我们的自定义控件定义一个 ControlTemplate。控件模板包含的代码与本文第一部分中的原始用户控件几乎相同。比较第一部分中 TextBoxWithWatermark.xaml
文件的内容与第二部分中 SimpleControls/Themes/Generic.xaml
文件的内容,您会注意到我将 UserControl.Resources
部分移动到了 ControlTemplate.Resources
中,并保留了下面的 Grid 中的布局。
主题信息
在 Themes/Generic.xaml
文件中为控件定义默认外观只是完成了一半的工作,因为我们还必须告诉 WPF 在没有定义样式时去那里查找。我们通过在 SimpleControls/Properties/AssemblyInfo.cs
文件中声明 ThemeInfo
属性来做到这一点
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, // where theme specific resource dictionaries are located
// (used if a resource is not found in the page, or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly // where the generic resource dictionary is located
// (used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
从用户控件中删除 XAML 部分
自定义的无外观控件不定义自己的 GUI。相反,GUI 要么在源程序集的 Generic.xaml
文件中定义,要么在应用程序的主题化中定义。因此,我从第一部分的 TextBoxWithWatermark.xaml.cs
用户控件中删除了 TextBoxWithWatermark.xaml
部分,并有效地在 TextBoxWithWatermark.cs
中创建了一个新的代码隐藏文件。
比较第一部分的 TextBoxWithWatermark.xaml.cs
与第二部分的 TextBoxWithWatermark.cs
可以发现,我只添加了一个静态构造函数并删除了标准构造函数(因为 this.InitializeComponent();
在自定义控件中不再使用)。
static TextBoxWithWatermark()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(<b>TextBoxWithWatermark</b>),
new FrameworkPropertyMetadata(typeof(<b>TextBoxWithWatermark</b>)));
}
DefaultStyleKeyProperty
属性告诉 WPF 查找默认样式
用于 TextBoxWithWatermark
控件类,在 Themes/Generic.xaml
文件中。
这是通过静态构造函数完成的,因为它适用于每个没有通过上述替代途径获取样式的 TextBoxWithWatermark 控件对象。
值得指出的是,您可以使用 OnApplyTemplate
方法在每次应用新主题时初始化无外观控件。在这里没有必要,所以我没有实现该方法。
使辅助类公开
创建自定义无外观控件的这部分对于每个自定义控件来说并非严格必要,但我将其包含在此处,因为它可能会引起一些争议。
带水印文本框的“特殊”之处在于它使用了一个转换器(在 WPF 中使用 ValueConverter 和 MultiValueConverter)来决定是否显示水印。
比较第一部分的 WatermarkHelper.cs
转换器与第二部分的转换器可以发现,我将类的可见性从内部更改为公共。此更改是必要的
因为 ExpressionDark 和 WhistlerBlue 主题中的 ControlTemplates 否则将无法工作,而且我没有看到严格将其隐藏在自定义控件实现中的理由。
将转换器公开并作为 ControlTemplate 的一部分增加了此实现的灵活性,因为它使每个人都可以实现自己的转换器,并根据自己的逻辑显示水印,而无需更改 DLL 项目中的原始实现。
结论
这一系列两篇文章教会我,如果在正确的层次进行控件组合,可以节省我大量的工作。绑定依赖属性的简单性是如此美妙,我几乎可以为此编写一个代码生成程序(也许有一天我会这样做)。希望您喜欢这个系列。如果您发现任何错误或值得注意的地方,请为本文投票并给我您的反馈。
历史
- 2012 年 2 月 20 日 初始创建
- 2012 年 3 月 16 日 源代码中的小错误修复(WebHyperlink 控件中的依赖属性已正确指定)