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






4.94/5 (9投票s)
在用户控件中组合控件可以提高应用程序的整体一致性,同时不会增加太多额外的工作或复杂性。
引言
Windows Presentation Foundation (WPF) 提供了至少两种实现控件的方式。有用户控件 (User Controls) 和自定义控件 (Custom Controls)。用户控件由一个或多个标准控件组成(且不能被样式化),而自定义控件则扩展现有控件或从头开始创建(且可以被样式化)。本系列文章共两篇,介绍了一种特别简单但有效的方法来实现自定义控件,并在第二篇文章中将其扩展为一个可主题化的(自定义控件)。
本文档假定您具备 WPF 的工作知识。特别是,需要了解依赖属性 (Dependency Properties) 和命令 (Commanding)。
在设计输入表单时,我有时会发现自己要做重复的事情:在这里放一个标签,在那里放一个文本框,再放另一个标签和另一个文本框……您懂的。当需要给所有标签设置 3 的外边距,而给所有文本框设置 0 的外边距时,问题有时会变得更糟……
本文介绍了一种技术,允许我们声明一个新控件,例如,它由一个标签和一个文本框组成,并增加一个新的 TextBoxLabel
属性,该属性可以在 XAML 中用于设置标签的文本,但由控件决定标签相对于文本框的位置。
这样,XAML 中的元素数量就减少了,包含少量控件的输入表单也易于维护,因为内容看起来可能更一致。让我们通过一个
- 带水印的文本框和标签
来理解它是如何工作的,稍后我们将通过一个
- 带自定义工具提示和上下文菜单的超链接
- 组合框和标签,以及一个
- 带标签的组合框和文本框
来扩展这个概念。稍后,本系列的第二部分将介绍如何将本文讨论的控件之一 变成一个无外观的自定义控件。
编译代码
StyleCop
我在项目中使用 StyleCop 来统一代码的可读性。因此,如果编译项目时遇到错误,您可以下载并安装 StyleCop,或者编辑/删除每个 .csproj 文件中的相应条目。
<Import Project="$(ProgramFiles)\MSBuild\StyleCop\v4.7\StyleCop.Targets" />
带水印的文本框和标签组合
带水印的文本框控件的行为与普通文本框相同,只是它带有标签并在文本区域显示水印。一旦用户开始输入,水印就会消失。
该控件在 TestWindow.xaml 中声明如下:
<textbox:TextBoxWithWatermark
Text="" Watermark="First and second given name" LabelTextBox="Name:" />
Text
属性可用于读取和写入用户输入的文本。您可以像使用标准文本框控件上的 Text
属性一样使用它。
Watermark
属性可用于设置当用户尚未输入任何内容时显示在文本框中的文本。
LabelTextBox
属性设置显示在文本框上方标签的文本。
通常,以上属性都不会如此容易获得,因为控件组合导致隐藏了内部元素。解决方案在于 TextBoxWithWatermark
用户控件的 XAML。在这里,内部属性(标签的内容属性)和外部属性(LabelTextBox
)被绑定,使得两者看起来像一个整体。
<Label Content="{Binding Path=LabelTextBox, RelativeSource={RelativeSource FindAncestor, AncestorType=local:TextBoxWithWatermark, AncestorLevel=1}}" HorizontalAlignment="Left" VerticalAlignment="Bottom" Grid.Column="0" Grid.Row="0"/>
上面的 XAML 代码包含在 TextBoxWithWatermark.xaml 文件中。标签控件内容中的绑定语句绑定到 TextBoxWithWatermark
控件的 LabelTextBox
依赖属性。这由以下标准的依赖属性代码模式支持:
private static readonly DependencyProperty LabelTextBoxProperty = DependencyProperty.Register("LabelTextBox", typeof(string), typeof(TextBoxWithWatermark)); public string LabelTextBox { get { return (string)GetValue(TextBoxWithWatermark.LabelTextBoxProperty); } set { SetValue(TextBoxWithWatermark.LabelTextBoxProperty, value); } }
简而言之:我们在演示窗口中声明一个 TextBoxWithWatermark
控件,并绑定到其依赖属性。TextBoxWithWatermark
控件的 DP 绑定到组合中相应控件的每个属性。
搞定。除此之外,不需要其他任何东西就能将 XAML 值通过用户控件(TextBoxWithWatermark
)传递到(标签)原始控件。
我从其他地方借鉴了这种实现水印的特定方法:在 WPF 应用程序中创建带水印的文本框。最初的方法建议使用带有透明背景的文本框,然后让文本框透过透明背景显示。这在 ExpressionDark 中导致了一个问题,因为背景是黑色的,而 WPF 在透明背景上使用黑色光标。我通过以下方式解决了这个问题:
- 移除文本框的透明背景
- 将文本块放在文本框之上(在文本框之后声明文本块),以及
- 将文本块设置为
IsHitTestVisible=false
(以将所有输入路由到文本框)。
带自定义工具提示和上下文菜单的超链接
超链接控件的行为与普通超链接相同,除了这个控件针对 Web 超链接进行了优化。它在工具提示中显示 URL,用户可以使用上下文菜单来
- 复制和粘贴 URL
- 导航到 URL(而不是单击带下划线的部分)
该控件在 TestWindow.xaml 中声明如下:
<hyperlink:WebHyperlink Text="Exposing inner Control properties for binding in WPF"
NavigateUri="http://stackoverflow.com/questions/4169090/exposing-inner-control-properties-for-binding-in-wpf"
Grid.Column="1" Grid.Row="1" Margin="6, 3" VerticalAlignment="Center"/>
我们可以查看 WebHyperlink
控件的 XAML 部分,发现它与 TextBoxWithWatermark
控件非常相似。
<TextBlock Text="{Binding Path=Text, RelativeSource={RelativeSource FindAncestor, AncestorType=hyperlink:WebHyperlink, AncestorLevel=1}}" />
我们有一个 Text
属性,它绑定到放置在标准超链接控件内容部分中的文本块的 Text
属性。对于超链接控件上的 NavigateUri
属性,情况似乎也是如此。
超链接控件还包含一个 RequestNavigate
事件,当用户单击超链接时会触发该事件。这会执行以下代码:
private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
{
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri));
}
如果执行 NavigateToUri
命令,则执行类似的命令。请注意,导航目标作为命令参数提供。
private static void Hyperlink_CommandNavigateTo(object sender, ExecutedRoutedEventArgs e)
{
if (sender == null || e == null) return;
e.Handled = true;
WebHyperlink whLink = sender as WebHyperlink;
if (whLink == null) return;
Process.Start(new ProcessStartInfo(whLink.NavigateUri.AbsoluteUri));
}
上下文菜单通过命令(加上参数)绑定到超链接控件。您可以将此作为使用命令参数使命令更灵活的示例。CommandTarget
位也很有趣。
<ContextMenu>
<MenuItem Header="Copy Url to Clipboard"
Command="{x:Static hyperlink:WebHyperlink.CopyUri}"
CommandParameter="{Binding ElementName=PART_Hyperlink, Path=NavigateUri}"
CommandTarget="{Binding PlacementTarget,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}}"/>
<MenuItem Header="Open Target in Browser"
Command="{x:Static hyperlink:WebHyperlink.NavigateToUri}"
CommandTarget="{Binding PlacementTarget,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}}"/>
</ContextMenu>
CommandTarget
位是必需的,否则命令将不会执行。命令不会被执行,因为上下文菜单拥有键盘焦点,并且不在与超链接相同的视觉和逻辑树中。
参考文献
附加功能是否有用是另一回事。但试想一下,如果我们每次想使用超链接时都要声明这些,那将是多么大的工作量。
组合框和标签组合
ComboBoxWithLabel
控件的行为与普通 ComboBox
相同,但它上面增加了一个标签。
将控件与组合框(或任何其他以 ItemsControl
为核心的控件)组合意味着需要多一点工作,因为有一些有用的依赖属性。尽管如此,该控件仍能显示我们之前看到的相同的 XAML 绑定机制。
SelectedItem="{Binding RelativeSource={RelativeSource FindAncestor, " +
"AncestorType=local:ComboBoxWithLabel, AncestorLevel=1}, Path=SelectedItem}"
private static readonly DependencyProperty SelectedItemProperty =
ComboBox.SelectedItemProperty.AddOwner(typeof(ComboBoxWithLabel));
public object SelectedValue
{
get { return (object)GetValue(ComboBoxWithLabel.SelectedValueProperty); }
set { SetValue(ComboBoxWithLabel.SelectedValueProperty, value); }
}
组合框中显示的数据是从 TestWindow
类资源部分定义的 ObjectDataProvider
加载的。ObjectDataProvider
本身从集合类中声明的静态方法获取数据。该静态方法基于枚举生成一个字典。
组合框和带标签的文本框组合
这些示例实际上只是上面讨论的带标签组合框和带水印带标签文本框示例的组合。因此,假设您已经阅读并理解了以上内容,那么这里就没有太多新东西了,除了我们还有一种可能被用作一个整体组合的控件组合。您可以使用它来再次回顾绑定模式。如果您觉得这太复杂难以理解,请回顾上面讨论的示例。
结论
简单的控件组合是一种可重复使用 UI 代码的有效方法。它确保相似的控件在大型应用程序中即使出现数百次也能看起来和行为相似。为现有控件添加特殊行为或属性从未如此简单。
请花点时间对本文进行评分并给我反馈。
进一步开发
本文中的控件无法以无外观的方式进行主题化。尽管如此,如果它们由可主题化的控件组成,它们就可以进行主题化。例如,带标签的组合框是可主题化的,只要每个主题都包含一个组合框控件的 ControlTemplate
和一个标签控件的 ControlTemplate
。但该控件不是无外观的,因为标签始终显示在组合框的顶部。
更糟糕的是,上面讨论的文本框控件的水印甚至可能不可见,因为它的颜色无法在样式或 ControlTemplate
中定义。
本文系列的第二部分展示了如何通过将带水印的文本框控件开发成一个可皮肤化的控件,并为演示应用程序添加三个演示主题来对简单控件进行主题化。
历史
- 2012 年 2 月 20 日:首次创建。
- 2012 年 3 月 16 日:源代码小型修补(现在可以绑定到
WebHyperlink
的Uri
和Text
依赖属性)