自定义控件与主题化:WPF中的文本框控件

在WPF应用程序开发中,创建可重用的自定义控件并为其添加主题化支持是一种常见的需求。本文将介绍如何创建一个无样式的带水印的文本框控件,并展示如何为主题化应用程序添加主题。将从之前的部分开始,回顾一下水印文本框控件的创建过程。

编译代码

为了保持代码的一致性和可读性,在项目中使用了StyleCop工具。如果在编译项目时遇到错误,可以下载并安装StyleCop,或者编辑/移除每个.csproj文件中的相应条目。

主题化演示应用程序

通常,会使用一个单独的DLL项目来为其他应用程序添加主题。但为了简化,直接在应用程序中添加了以下内容:

  • 包含3个主题的Themes文件夹
  • MainCommands命令类,用于绑定和执行viewTheme命令
  • 菜单栏,允许用户选择主题
  • TypeOfTheme枚举和一些基于它的代码,用于管理、选择和更改特定主题

在TestWindow中的核心主题化函数是ChangeThemeCommand_Executed函数。它接受一个字符串参数,表示要更改的主题名称,该名称由MenuBar项调用提供。主题名称映射到一个枚举,该枚举又映射到一个静态常量字符串数组,包含主主题XAML文件的资源地址。

创建无样式的自定义控件

在DLL项目中为主题化的WPF控件需要以下四件事:

  • 包含Generic.xaml文件的Themes文件夹
  • AssemblyInfo文件中的ThemeInfo条目
  • 从用户控件中移除XAML
  • 使辅助类公开

为了使WPF控件完全可换肤,需要在Themes文件夹中包含一个Generic.xaml文件。Generic.xaml文件应该包含自定义控件的默认声明。当没有其他声明时,WPF框架将应用这个默认声明。

在测试应用程序中,Generic主题不包含TextBoxWithWatermark控件的定义。因此,当用户在演示应用程序中选择Generic主题时,将应用DLL项目中的Themes/Generic.xaml文件中的定义。其他两个主题包含TextBoxWithWatermark控件的单独定义,这使能够使用特定主题的颜色来着色水印。

在Themes文件夹的Generic.xaml文件开头的两个颜色语句:

定义了在文本框中显示的水印的背景和前景颜色。这些颜色资源在XAML中进一步使用。

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, // 主题特定资源字典的位置 ResourceDictionaryLocation.SourceAssembly // 通用资源字典的位置 )]

自定义无样式控件不定义自己的GUI。相反,GUI要么在源程序集的Generic.xaml文件中定义,要么在应用程序的主题中定义。因此,从第一部分的TextBoxWithWatermark.xaml.cs用户控件中移除了TextBoxWithWatermark.xaml部分,并在TextBoxWithWatermark.cs中创建了一个新的代码后台文件。

比较第一部分的TextBoxWithWatermark.xaml.cs与第二部分的TextBoxWithWatermark.cs,会发现只添加了一个静态构造函数并移除了标准构造函数(因为this.InitializeComponent();在自定义控件中不再使用)。

static TextBoxWithWatermark() { DefaultStyleKeyProperty.OverrideMetadata( typeof(TextBoxWithWatermark), new FrameworkPropertyMetadata( typeof(TextBoxWithWatermark) )); }

DefaultStyleKeyProperty属性告诉WPF在Themes/Generic.xaml文件中找到TextBoxWithWatermark控件类的默认样式。这是通过静态构造函数完成的,因为它适用于每个TextBoxWithWatermark控件对象,这些对象没有通过上述讨论的其他方式获得其样式。

值得一提的是,可以使用OnApplyTemplate方法在每次新主题应用于控件时初始化无样式控件。这里没有必要,所以没有实现该方法。

创建自定义无样式控件的这一部分对于每个自定义控件来说并不是严格必要的,但在这里包括它,因为它可能会引起一些关注。

水印文本框的“特殊”之处在于它使用了一个转换器(参见WPF中的ValueConverter和MultiValueConverter的使用)来决定是否显示水印。

比较第一部分的WatermarkHelper.cs转换器与第二部分的转换器,会发现将类的可见性从内部更改为公开。这种更改是必要的,因为否则ExpressionDark和WhistlerBlue主题中的ControlTemplates将无法工作,而且没有看到严格隐藏在自定义控件实现中的理由。

将转换器公开并作为ControlTemplate的一部分,为这个实现增加了灵活性,因为它使每个人都能够根据他们自己的逻辑实现他们自己的转换器并显示水印,而无需更改DLL项目中的原始实现。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485