[WPF自定义控件库] 自定义控件的代码如何与ControlTemplate交互
1. 前言
WPF有一个灵活的UI框架,用户可以轻松地使用代码控制控件的外观。例设我需要一个控件在鼠标进入的时候背景变成蓝色,我可以用下面这段代码实现:
protected override void OnMouseEnter(MouseEventArgs e)
{base.OnMouseEnter(e);Background = new SolidColorBrush(Colors.Blue);
}
但一般没人会这么做,因为这样做代码和UI过于耦合,难以扩展。正确的做法应该是使用代码告诉ControlTemplate去改变外观,或者控制ControlTemplate中可用的元素进入某个状态。
这篇文章介绍自定义控件的代码如何和ControlTemplate交互,涉及的知识包括RelativeSource、Trigger、TemplatePart和VisualState。
2. 简单的Expander
本文使用一个简单的Expander介绍UI和ControlTemplate交互的几种技术,它的代码如下:
public class MyExpander : HeaderedContentControl
{public MyExpander(){DefaultStyleKey = typeof(MyExpander);}public bool IsExpanded{get => (bool)GetValue(IsExpandedProperty);set => SetValue(IsExpandedProperty, value);}public static readonly DependencyProperty IsExpandedProperty =DependencyProperty.Register(nameof(IsExpanded), typeof(bool), typeof(MyExpander), new PropertyMetadata(default(bool), OnIsExpandedChanged));private static void OnIsExpandedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args){var oldValue = (bool)args.OldValue;var newValue = (bool)args.NewValue;if (oldValue == newValue)return;var target = obj as MyExpander;target?.OnIsExpandedChanged(oldValue, newValue);}protected virtual void OnIsExpandedChanged(bool oldValue, bool newValue){if (newValue)OnExpanded();elseOnCollapsed();}protected virtual void OnCollapsed(){}protected virtual void OnExpanded(){}
}
<Style TargetType="{x:Type local:MyExpander}"><Setter Property="HorizontalContentAlignment"Value="Stretch" /><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="{x:Type local:MyExpander}"><Border Background="{TemplateBinding Background}"BorderBrush="{TemplateBinding BorderBrush}"BorderThickness="{TemplateBinding BorderThickness}"><StackPanel><ToggleButton x:Name="ExpanderToggleButton"Content="{TemplateBinding Header}"IsChecked="{Binding IsExpanded,RelativeSource={RelativeSource Mode=TemplatedParent},Mode=TwoWay}" /><ContentPresenter Grid.Row="1"x:Name="ContentPresenter"HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"VerticalAlignment="{TemplateBinding VerticalContentAlignment}"Visibility="Collapsed" /></StackPanel></Border></ControlTemplate></Setter.Value></Setter>
</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处于选中的状态。
IsChecked="{Binding IsExpanded,RelativeSource={RelativeSource Mode=TemplatedParent},Mode=TwoWay}"
接下来分别用几种技术实现Expander.IsChecked为True时显示ContentPresenter。
4. 使用Trigger
<ControlTemplate TargetType="{x:Type local:ExpanderUsingTrigger}"><Border Background="{TemplateBinding Background}">......</Border><ControlTemplate.Triggers><Trigger Property="IsExpanded"Value="True"><Setter Property="Visibility"TargetName="ContentPresenter"Value="Visible" /></Trigger></ControlTemplate.Triggers>
</ControlTemplate>
可以为ControlTemplate添加Triggers,内容为Trigger或EventTrigger的集合,Triggers通过响应属性值变更或事件更改控件的外观。
大部分情况下Trigger简单好用,但滥用或错误使用将使ControlTemplate的各个状态之间变得很混乱。例如当可以影响外观的属性超过一定数量,并且这些属性可以组成不同的组合,Trigger将要处理无数种情况。
5. 使用TemplatePart
TemplatePart(部件)是指ControlTemplate中的命名元素(如上面XAML中的“HeaderElement”)。控件逻辑预期这些部分存在于ControlTemplate中,控件在加载ControlTemplate后会调用OnApplyTemplate,可以在这个函数中调用protected DependencyObject GetTemplateChild(String childName)
获取模板中指定名字的部件。
[TemplatePart(Name =ContentPresenterName,Type =typeof(UIElement))]
public class ExpanderUsingPart : MyExpander
{private const string ContentPresenterName = "ContentPresenter";protected UIElement ContentPresenter { get; private set; }public override void OnApplyTemplate(){base.OnApplyTemplate();ContentPresenter = GetTemplateChild(ContentPresenterName) as UIElement;UpdateContentPresenter();}protected override void OnIsExpandedChanged(bool oldValue, bool newValue){base.OnIsExpandedChanged(oldValue, newValue);UpdateContentPresenter();}private void UpdateContentPresenter(){if (ContentPresenter == null)return;ContentPresenter.Visibility = IsExpanded ? Visibility.Visible : Visibility.Collapsed;}
}
上面的代码实现了获取HeaderElement并为它订阅鼠标点击事件。由于Template可能多次加载,或者不能正确获取TemplatePart,所以使用TemplatePart前应该先判断是否为空;如果要订阅事件,应该先取消订阅。
注意:不要在Loaded事件中尝试调用GetTemplateChild,因为Loaded的时候OnApplyTemplate不一定已经被调用,而且Loaded更容易被多次触发。
TemplatePartAttribute协定
有时,为了表明控件期待在ControlTemplate存在某个特定部件,防止编辑ControlTemplate的开发人员删除它,控件上会添加添加TemplatePartAttribute协定。上面代码中即包含这个协定:
[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的外观。
[TemplateVisualState(Name = StateExpanded, GroupName = GroupExpansion)]
[TemplateVisualState(Name = StateCollapsed, GroupName = GroupExpansion)]
public class ExpanderUsingState : MyExpander
{public const string GroupExpansion = "ExpansionStates";public const string StateExpanded = "Expanded";public const string StateCollapsed = "Collapsed";public ExpanderUsingState(){DefaultStyleKey = typeof(ExpanderUsingState);}protected override void OnIsExpandedChanged(bool oldValue, bool newValue){base.OnIsExpandedChanged(oldValue, newValue);UpdateVisualStates(true);}public override void OnApplyTemplate(){base.OnApplyTemplate();UpdateVisualStates(false);}protected virtual void UpdateVisualStates(bool useTransitions){VisualStateManager.GoToState(this, IsExpanded ? StateExpanded : StateCollapsed, useTransitions);}}
<ControlTemplate TargetType="{x:Type local:ExpanderUsingState}"><Border Background="{TemplateBinding Background}"BorderBrush="{TemplateBinding BorderBrush}"BorderThickness="{TemplateBinding BorderThickness}"><VisualStateManager.VisualStateGroups><VisualStateGroup x:Name="ExpansionStates"><VisualState x:Name="Expanded"><Storyboard><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"Storyboard.TargetName="ContentPresenter"><DiscreteObjectKeyFrame KeyTime="0"Value="{x:Static Visibility.Visible}" /></ObjectAnimationUsingKeyFrames></Storyboard></VisualState><VisualState x:Name="Collapsed" /></VisualStateGroup></VisualStateManager.VisualStateGroups>......</Border>
</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可用。这很好用,尤其是对于复杂的控件来说。上面代码也包含了这个协定:
[TemplateVisualState(Name = StateExpanded, GroupName = GroupExpansion)]
[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
转载于:https://www.cnblogs.com/lonelyxmas/p/10903848.html
[WPF自定义控件库] 自定义控件的代码如何与ControlTemplate交互相关推荐
- [WPF自定义控件] 开始一个自定义控件库项目
1. 目标 我实现了一个自定义控件库,并且打算用这个控件库作例子写一些博客.这个控件库主要目标是用于教学,希望通过这些博客初学者可以学会为自己或公司创建自定义控件,并且对WPF有更深入的了解. 控件库 ...
- [WPF自定义控件库]自定义Expander
[WPF自定义控件库]自定义Expander 原文:[WPF自定义控件库]自定义Expander 1. 前言 上一篇文章介绍了使用Resizer实现Expander简单的动画效果,运行效果也还好,不过 ...
- [WPF自定义控件库]使用WindowChrome自定义RibbonWindow
[WPF自定义控件库]使用WindowChrome自定义RibbonWindow 原文:[WPF自定义控件库]使用WindowChrome自定义RibbonWindow 1. 为什么要自定义Ribbo ...
- 看书要仔细----自定义控件库摸索记
日前在Windows Phone上做了一个小练习,用模板定义了一个控件,觉得这控件使用效果还不错,想把它做成一个控件库,留着给其他项目用. 没想到一个小小的控件库居然花了几天的时间才搞定.下面请看这个 ...
- python turtle画彩虹-Python利用turtle库绘制彩虹代码示例_天津SEO
天津SEO RGB模型:光的三原色,共同决定色相 HSB/HSV模型:H色彩,S深浅,B饱和度,H决定色相 需要将HSB模型转换为RGB模型 代码示例: #-*- coding:utf-8 –*- f ...
- python画图代码彩虹-Python利用turtle库绘制彩虹代码示例
语言:Python IDE:Python.IDE 需求 做出彩虹效果 颜色空间 RGB模型:光的三原色,共同决定色相 HSB/HSV模型:H色彩,S深浅,B饱和度,H决定色相 需要将HSB模型转换为R ...
- Crawler:基于requests库+json库+40行代码实现爬取猫眼榜单TOP100榜电影名称主要信息
Crawler:基于requests库+json库+40行代码实现爬取猫眼榜单TOP100榜电影名称主要信息 目录 输出结果 实现代码 输出结果 实现代码 # -*- coding: utf-8 -* ...
- mysql relay bin 主库_MySQL主库binlog(master-log)与从库relay-log关系代码详解
主库binlog: # at 2420 #170809 17:16:20 server id 1882073306 end_log_pos 2451 CRC32 0x58f2db87 Xid = 32 ...
- mysql 从库relay_MySQL主库binlog(master-log)与从库relay-log关系代码详解
主库binlog: # at 2420 #170809 17:16:20 server id 1882073306 end_log_pos 2451 CRC32 0x58f2db87 Xid = 32 ...
最新文章
- Amy Mcdonald - This is the Life
- Android启动initlogo.rle制作
- SMO写的查看数据库信息的代码
- linux int64_t 头文件,这对int64_t的处理是GCC和Clang的错误吗?
- SyntaxError: Identifier ‘XXX‘ has already been declared
- 医药公司java,医药管理系统java版
- 计算机核心期刊、学报一览
- 9000多篇投稿,接收率只有15%,今年的AAAI你中了吗?
- 怎么单选_听力三个选项都出的单选怎么破?| 附今日听力S1S2及听力原文
- 自己搭建Maven项目下运行项目后Eclipse报错:org.springframework.web.context.ContextLoaderListener
- cvtColor()实际操作
- CAD梦想画图中的“缩放命令”
- 北邮信通复试题c语言,北邮计算机考研复试题的C语言解答精选.pdf
- python好学吗要有什么基础-Python好学吗难不难?0基础能学会吗?
- win10/11如何安装安卓app?带你使用华为移动应用引擎
- 语句摘抄——第26周
- Minecraft 从安装到入门
- vue之封装loading组件
- 开源之道给您拜年啦~~
- 怎样将PDF转成JPG?PDF转换图片其实很简单
热门文章
- get请求可以传body吗_面试必备:GET和POST的区别详细解说
- 96KB存储器的怎么算地址范围_STM32入门系列-存储器与寄存器介绍
- android菜单键选择图标,Android Design
- python制作日历并保存成excel_利用Python自动化生成逼格高的日历!简单又实用
- PHP中的方形按钮怎么敲,php 魔术方法使用说明
- 云服务器学习linux_云服务器怎么选linux系统
- 一种非常好用的图像处理软件
- [MATLAB]设置坐标轴标签
- html ssm写用户登陆验证,ssm 使用token校验登录的实现
- 强化学习组队学习task06——DDPG 算法