1. 前言

WPF有一个灵活的UI框架,用户可以轻松地使用代码控制控件的外观。例设我需要一个控件在鼠标进入的时候背景变成蓝色,我可以用下面这段代码实现:

  1. protected override void OnMouseEnter(MouseEventArgs e)
  2. {
  3. base.OnMouseEnter(e);
  4. Background = new SolidColorBrush(Colors.Blue);
  5. }

但一般没人会这么做,因为这样做代码和UI过于耦合,难以扩展。正确的做法应该是使用代码告诉ControlTemplate去改变外观,或者控制ControlTemplate中可用的元素进入某个状态。

这篇文章介绍自定义控件的代码如何和ControlTemplate交互,涉及的知识包括RelativeSource、Trigger、TemplatePart和VisualState。

2. 简单的Expander

本文使用一个简单的Expander介绍UI和ControlTemplate交互的几种技术,它的代码如下:

  1. public class MyExpander : HeaderedContentControl
  2. {
  3. public MyExpander()
  4. {
  5. DefaultStyleKey = typeof(MyExpander);
  6. }
  7. public bool IsExpanded
  8. {
  9. get => (bool)GetValue(IsExpandedProperty);
  10. set => SetValue(IsExpandedProperty, value);
  11. }
  12. public static readonly DependencyProperty IsExpandedProperty =
  13. DependencyProperty.Register(nameof(IsExpanded), typeof(bool), typeof(MyExpander), new PropertyMetadata(default(bool), OnIsExpandedChanged));
  14. private static void OnIsExpandedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
  15. {
  16. var oldValue = (bool)args.OldValue;
  17. var newValue = (bool)args.NewValue;
  18. if (oldValue == newValue)
  19. return;
  20. var target = obj as MyExpander;
  21. target?.OnIsExpandedChanged(oldValue, newValue);
  22. }
  23. protected virtual void OnIsExpandedChanged(bool oldValue, bool newValue)
  24. {
  25. if (newValue)
  26. OnExpanded();
  27. else
  28. OnCollapsed();
  29. }
  30. protected virtual void OnCollapsed()
  31. {
  32. }
  33. protected virtual void OnExpanded()
  34. {
  35. }
  36. }
  1. <Style TargetType="{x:Type local:MyExpander}">
  2. <Setter Property="HorizontalContentAlignment"
  3. Value="Stretch" />
  4. <Setter Property="Template">
  5. <Setter.Value>
  6. <ControlTemplate TargetType="{x:Type local:MyExpander}">
  7. <Border Background="{TemplateBinding Background}"
  8. BorderBrush="{TemplateBinding BorderBrush}"
  9. BorderThickness="{TemplateBinding BorderThickness}">
  10. <StackPanel>
  11. <ToggleButton x:Name="ExpanderToggleButton"
  12. Content="{TemplateBinding Header}"
  13. IsChecked="{Binding IsExpanded,RelativeSource={RelativeSource Mode=TemplatedParent},Mode=TwoWay}" />
  14. <ContentPresenter Grid.Row="1"
  15. x:Name="ContentPresenter"
  16. HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
  17. VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
  18. Visibility="Collapsed" />
  19. </StackPanel>
  20. </Border>
  21. </ControlTemplate>
  22. </Setter.Value>
  23. </Setter>
  24. </Style>

MyExpander是一个HeaderedContentControl,它包含一个IsExpanded用于指示当前是展开还是折叠。ControlTemplate中包含ExpanderToggleButton及ContentPresenter两个元素。

3. 使用RelativeSource

之前已经介绍过TemplateBinding,通常ControlTemplate中元素都通过TemplateBinding获取控件的属性值。但需要双向绑定的话,就是RelativeSource出场的时候了。

RelativeSource有几种模式,分别是:

  • FindAncestor,引用数据绑定元素的父链中的上级。 这可用于绑定到特定类型的上级或其子类。
  • PreviousData,允许在当前显示的数据项列表中绑定上一个数据项(不是包含数据项的控件)。
  • Self,引用正在其上设置绑定的元素,并允许你将该元素的一个属性绑定到同一元素的其他属性上。
  • TemplatedParent,引用应用了模板的元素,其中此模板中存在数据绑定元素。。

ControlTemplate中主要使用RelativeSource Mode=TemplatedParent的Binding,它相当于TemplateBinding的双向绑定版本。,主要是为了可以和控件本身进行双向绑定。ExpanderToggleButton.IsChecked使用这种绑定与Expander的IsExpanded关联,当Expander.IsChecked为True时ExpanderToggleButton处于选中的状态。

  1. IsChecked="{Binding IsExpanded,RelativeSource={RelativeSource Mode=TemplatedParent},Mode=TwoWay}"

接下来分别用几种技术实现Expander.IsChecked为True时显示ContentPresenter。

4. 使用Trigger

  1. <ControlTemplate TargetType="{x:Type local:ExpanderUsingTrigger}">
  2. <Border Background="{TemplateBinding Background}">
  3. ......
  4. </Border>
  5. <ControlTemplate.Triggers>
  6. <Trigger Property="IsExpanded"
  7. Value="True">
  8. <Setter Property="Visibility"
  9. TargetName="ContentPresenter"
  10. Value="Visible" />
  11. </Trigger>
  12. </ControlTemplate.Triggers>
  13. </ControlTemplate>

可以为ControlTemplate添加Triggers,内容为TriggerEventTrigger的集合,Triggers通过响应属性值变更或事件更改控件的外观。

大部分情况下Trigger简单好用,但滥用或错误使用将使ControlTemplate的各个状态之间变得很混乱。例如当可以影响外观的属性超过一定数量,并且这些属性可以组成不同的组合,Trigger将要处理无数种情况。

5. 使用TemplatePart

TemplatePart(部件)是指ControlTemplate中的命名元素(如上面XAML中的“HeaderElement”)。控件逻辑预期这些部分存在于ControlTemplate中,控件在加载ControlTemplate后会调用OnApplyTemplate,可以在这个函数中调用protected DependencyObject GetTemplateChild(String childName)获取模板中指定名字的部件。

  1. [TemplatePart(Name =ContentPresenterName,Type =typeof(UIElement))]
  2. public class ExpanderUsingPart : MyExpander
  3. {
  4. private const string ContentPresenterName = "ContentPresenter";
  5. protected UIElement ContentPresenter { get; private set; }
  6. public override void OnApplyTemplate()
  7. {
  8. base.OnApplyTemplate();
  9. ContentPresenter = GetTemplateChild(ContentPresenterName) as UIElement;
  10. UpdateContentPresenter();
  11. }
  12. protected override void OnIsExpandedChanged(bool oldValue, bool newValue)
  13. {
  14. base.OnIsExpandedChanged(oldValue, newValue);
  15. UpdateContentPresenter();
  16. }
  17. private void UpdateContentPresenter()
  18. {
  19. if (ContentPresenter == null)
  20. return;
  21. ContentPresenter.Visibility = IsExpanded ? Visibility.Visible : Visibility.Collapsed;
  22. }
  23. }

上面的代码实现了获取ContentPresenter并根据IsExpanded 的值将它显示或隐藏。由于Template可能多次加载,或者不能正确获取TemplatePart,所以使用TemplatePart前应该先判断是否为空;如果要订阅TemplatePart的事件,应该先取消订阅。

注意:不要在Loaded事件中尝试调用GetTemplateChild,因为Loaded的时候OnApplyTemplate不一定已经被调用,而且Loaded更容易被多次触发。

TemplatePartAttribute协定

有时,为了表明控件期待在ControlTemplate存在某个特定部件,防止编辑ControlTemplate的开发人员删除它,控件上会添加添加TemplatePartAttribute协定。上面代码中即包含这个协定:

  1. [TemplatePart(Name =ContentPresenterName,Type =typeof(UIElement))]

这段代码的意思是期待在ControlTemplate中存在名称为 "ContentPresenterName",类型为UIElement的部件。

TemplatePartAttribute在UWP中的作用好像被弱化了,不止在UWP原生控件中见不到TemplatePartAttribute,甚至在Blend中“部件”窗口也消失了。可能UWP更加建议使用VisualState。

使用TemplatePart需要遵循以下原则:

  • 尽可能减少TemplarePartAttribute协定。
  • 在使用TemplatePart之前检查其是否为Null。
  • 如果ControlTemplate没有遵循TemplatePartAttribute协定也不应该抛出异常,缺少部分功能可以接受,但要确保程序不会报错。

6. 使用VisualState

VisualState 指定控件处于特定状态时的外观。控件的代码使用VisualStateManager.GoToState(Control control, string stateName,bool useTransitions)指定控件处于何种VisualState,控件的ControlTemplate中根节点使用VisualStateManager.VisualStateGroups附加属性,并在其中确定各个VisualState的外观。

  1. [TemplateVisualState(Name = StateExpanded, GroupName = GroupExpansion)]
  2. [TemplateVisualState(Name = StateCollapsed, GroupName = GroupExpansion)]
  3. public class ExpanderUsingState : MyExpander
  4. {
  5. public const string GroupExpansion = "ExpansionStates";
  6. public const string StateExpanded = "Expanded";
  7. public const string StateCollapsed = "Collapsed";
  8. public ExpanderUsingState()
  9. {
  10. DefaultStyleKey = typeof(ExpanderUsingState);
  11. }
  12. protected override void OnIsExpandedChanged(bool oldValue, bool newValue)
  13. {
  14. base.OnIsExpandedChanged(oldValue, newValue);
  15. UpdateVisualStates(true);
  16. }
  17. public override void OnApplyTemplate()
  18. {
  19. base.OnApplyTemplate();
  20. UpdateVisualStates(false);
  21. }
  22. protected virtual void UpdateVisualStates(bool useTransitions)
  23. {
  24. VisualStateManager.GoToState(this, IsExpanded ? StateExpanded : StateCollapsed, useTransitions);
  25. }
  26. }
  1. <ControlTemplate TargetType="{x:Type local:ExpanderUsingState}">
  2. <Border Background="{TemplateBinding Background}"
  3. BorderBrush="{TemplateBinding BorderBrush}"
  4. BorderThickness="{TemplateBinding BorderThickness}">
  5. <VisualStateManager.VisualStateGroups>
  6. <VisualStateGroup x:Name="ExpansionStates">
  7. <VisualState x:Name="Expanded">
  8. <Storyboard>
  9. <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
  10. Storyboard.TargetName="ContentPresenter">
  11. <DiscreteObjectKeyFrame KeyTime="0"
  12. Value="{x:Static Visibility.Visible}" />
  13. </ObjectAnimationUsingKeyFrames>
  14. </Storyboard>
  15. </VisualState>
  16. <VisualState x:Name="Collapsed" />
  17. </VisualStateGroup>
  18. </VisualStateManager.VisualStateGroups>
  19. ......
  20. </Border>
  21. </ControlTemplate>

上面的代码演示了如何通过控件的IsExpanded 属性进入不同的VisualState。ExpansionStates是VisualStateGroup,它包含Expanded和Collapsed两个互斥的状态,控件使用VisualStateManager.GoToState(Control control, string stateName,bool useTransitions)更新VisualState。useTransitions这个参数指示是否使用 VisualTransition 进行状态过渡,简单来说即是VisualState之间切换时用不用VisualTransition里面定义的动画。请注意我在OnApplyTemplate()中使用了 UpdateVisualStates(false),这是因为这时候控件还没在UI上呈现,这时候使用动画毫无意义。

使用VisualState的最佳实践

使用属性控制状态,并创建一个方法帮助状态间的转换。如上面的UpdateVisualStates(bool useTransitions)。当属性值改变或其它有可能影响VisualState的事件发生都可以调用这个方法,由它统一管理控件的VisualState。注意一个控件应该最多只有几种VisualStateGroup,有限的状态才容易管理。

TemplateVisualStateAttribute协定

自定义控件可以使用TemplateVisualStateAttribute协定声明它的VisualState,用于通知控件的使用者有这些VisualState可用。这很好用,尤其是对于复杂的控件来说。上面代码也包含了这个协定:

  1. [TemplateVisualState(Name = StateExpanded, GroupName = GroupExpansion)]
  2. [TemplateVisualState(Name = StateCollapsed, GroupName = GroupExpansion)]

TemplateVisualStateAttribute是可选的,而且就算控件声明了这些VisualState,ControlTemplate也可以不包含它们中的任何一个,并且不会引发异常。

7. Trigger、TemplatePart及VisualState之间的选择

正如Expander所示,Trigger、TemplatePart及VisualState都可以实现类似的功能,像这种三种方式都可以实现同一个功能的情况很常见。

在过去版本的Blend中,编辑ControlTemplate可以看到“状态(States)”、“触发器(Triggers)”、“部件(Parts)”三个面板,现在“部件”面板已经消失了,而“触发器”从Silverlight开始就不再支持,以后也应该不会回归(xaml standard在github上有这方面的讨论(Add Triggers, DataTrigger, EventTrigger,___) [and-or] VisualState · Issue #195 · Microsoft-xaml-standard · GitHub[https://github.com/Microsoft/xaml-standard/issues/195])。现在看起来是VisualState的胜利,其实在Silverlight和UWP中TemplatePart仍是个十分常用的技术,而在WPF中Trigger也工作得很出色。

如果某个功能三种方案都可以实现,我的选择原则是这样:

  • 需要向控件发出命令的,如响应点击事件,就用TemplatePart;
  • 简单的UI,如隐藏/显示某个元素就用Trigger;
  • 如果要有动画,并且代码量和使用Trigger的话,我会选择用VisualState;

几乎所有WPF的原生控件都提供了VisualState支持,例如Button虽然使用ButtonChrome实现外观,但同时也可以使用VisualState定义外观。有时做自定义控件的时候要考虑为常用的VisualState提供支持。

8. 结语

VisualState是个比较复杂的话题,可以通过我的另一篇文章理解ControlTemplate中的VisualTransition更深入地理解它的用法(虽然是UWP的内容,但对WPF也同样适用)。

即使不自定义控件,学会使用ControlTemplate也是一件好事,下面给出一些有用的参考链接。

9. 参考

创建具有可自定义外观的控件 Microsoft Docs

通过创建 ControlTemplate 自定义现有控件的外观 Microsoft Docs

Control Customization Microsoft Docs

ControlTemplate Class (System_Windows_Controls) Microsoft Docs

[WPF自定义控件库] 自定义控件的代码如何与ControlTemplate交互的更多相关文章

  1. [WPF 自定义控件]自定义控件库系列文章

    Kino.Toolkit.Wpf Kino.Toolkit.Wpf是一组简单实用的WPF控件与工具,用于介绍自定义控件的入门.相关博客地址如下: 开始一个自定义控件库项目 介绍开始一个自定义控件库项目 ...

  2. [WPF 自定义控件]开始一个自定义控件库项目

    1. 目标 我实现了一个自定义控件库,并且打算用这个控件库作例子写一些博客.这个控件库主要目标是用于教学,希望通过这些博客初学者可以学会为自己或公司创建自定义控件,并且对WPF有更深入的了解. 控件库 ...

  3. [WPF自定义控件库] 关于ScrollViewer和滚动轮劫持(scroll-wheel-hijack)

    原文:[WPF自定义控件库] 关于ScrollViewer和滚动轮劫持(scroll-wheel-hijack) 1. 什么是滚动轮劫持# 这篇文章介绍一个很简单的继承自ScrollViewer的控件 ...

  4. [WPF自定义控件库]使用WindowChrome自定义RibbonWindow

    原文:[WPF自定义控件库]使用WindowChrome自定义RibbonWindow 1. 为什么要自定义RibbonWindow 自定义Window有可能是设计或功能上的要求,可以是非必要的,而自 ...

  5. [WPF自定义控件库] 让Form在加载后自动获得焦点

    原文:[WPF自定义控件库] 让Form在加载后自动获得焦点 1. 需求 加载后让第一个输入框或者焦点是个很基本的功能,典型的如"登录"对话框.一般来说"登录" ...

  6. WPF 如何创建自己的WPF自定义控件库

    在我们平时的项目中,我们经常需要一套自己的自定义控件库,这个特别是在Prism这种框架下面进行开发的时候,每个人都使用一套统一的控件,这样才不会每个人由于界面不统一而造成的整个软件系统千差万别,所以我 ...

  7. wpf 两个自定义控件

    wpf 两个自定义控件 一个是IP控件,一个滑动条.先看下效果图 IPControl 1.实际工作中有时需要设置IP信息,就想着做一个ip控件.效果没有window自带的好,需要通过tab切换.但也能 ...

  8. 二十五、【开源】EFW框架Winform前端开发之强大的自定义控件库

    回<[开源]EFW框架系列文章索引>        EFW框架源代码下载V1.2:http://pan.baidu.com/s/1hcnuA EFW框架实例源代码下载:http://pan ...

  9. WPF 精修篇 自定义控件

    原文:WPF 精修篇 自定义控件 自定义控件 因为没有办法对界面可视化编辑 所以用来很少 现在实现的是 自定义控件的 自定义属性 和自定义方法 用VS 创建自定义控件后 会自动创建 Themes 文件 ...

随机推荐

  1. BUPT复试专题—List(2015)

    题目描述 在该LIST上实现3种操作 1.append x在该LIST末尾添加x,x是32位整数 2.pop删除该LIST末尾的数 3.find i寻找第i个数,若i为负数表示寻找倒数第i个数,例如i ...

  2. SpringCloud中Redis的使用

    1.引入redis相关jar包 <dependency> <groupId>org.springframework.boot</groupId> <artif ...

  3. MapReduce编程实战之“调试”和&quot;调优&quot;

    本篇内容 在上一篇的"初识"环节,我们已经在本地和Hadoop集群中,成功的执行了几个MapReduce程序,对MapReduce编程,已经有了最初的理解. 在本篇文章中,我们对M ...

  4. MongoDB---出现no write has been done on this connection解决方式

    no write has been done on this connection 这个问题出现了好几天.日志里面一天出现几十次no write has been done on this conne ...

  5. HDOJ How many ways?? 2157【矩阵高速幂】

    How many ways? ? Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) ...

  6. openwrt: patch-dtb

    dts的概念是linux kernel中的,跟openwrt的关系不大.只是恰好在学习openwrt的时候碰到了这个东西,所以记录在openwrt名下. patch-dtb openwrt对arch/ ...

  7. 初识glib(1)

    最近搞DLNA,发现download的源码有许多glib库的使用.于是在Ubuntu中安装了glib库,以及简单测试了一些glib库函数,以此增加对glib的了解. 概述:glib库是Linux平台下 ...

  8. Arcgis Engine(ae)接口详解(4):featureClass的feature插入

    //由于测试数据不完善,featureClass在此要只设null值,真实功能要设实际的值 IFeatureClass featureClass = null; //获取某个字段的索引,后面取字段值用 ...

  9. solr单机多实例部署文件锁冲突解决的方法

    给出一个有问题的单机多tomcat实例引用同一个solr实例部署图. 这样的部署必定造成一个问题.启动第二个tomcat实例时,一定会报索引目录文件锁已经被占用. 最初的解决的方法是.有多少个tomc ...

  10. python day- 16 面向对象

    1.类的相关知识 类:是指具有相同属性和技能的一类事物. 比如:人类 ,植物类,动物类,狗类. 对象:是类中的某一个实例,是类的具体表现. 比如:具体到某个人,某一个植物,某一条狗. class 是p ...