WPF: WPF 中的 Triggers 和 VisualStateManager
在之前写的这篇文章 WPF: 只读依赖属性的介绍与实践 中,我们介绍了在 WPF 自定义控件中如何添加只读依赖属性,并且使其结合属性触发器 (Trigger) 来实现对控件样式的改变。事实上,关于触发器,在 WPF 中除了属性触发器,还有事件触发器 (EventTrigger) 和数据触发器 (DataTrigger)。此外,为了控制控件外观的切换,除了可以使用触发器外,我们还可以使用 VisualStates 和 VisualStateManager 来完成。
本文接下来会分别简单地介绍 Triggers 和 VisualStateManager,并且介绍一下它们的区别;了解这些,将会有助于我们在开发自定义控件时,能够明白何时选择属发器,何时选择 VisualStateManager。
一、触发器 (Triggers)
前面说过,触发器有三类,它们分别是:
- 属性触发器 (Trigger/MultiTrigger)
- 事件触发器 (EventTrigger)
- 数据触发器 (DataTrigger/MultiDataTrigger)
这些触发器的主要作用是:当指定属性的值发生改变或者事件触发时来更改控件的外观或行为;而且被触发器监测的属性必须是依赖属性,事件必须是路由事件;它们能够用在这些地方:DataTemplate, ControlTemplate, Style, 以及控件的 Inline 属性中。
1、属性触发器
特点:
- UIElement 属性改变时,执行 Trigger;
- 必须设置 Property 和 Value;必须包括 Setter 对象(不支持 EventSetter);
- 可以设置 EnterActions 和 ExitActions;
- 当条件不符合时,会将属性值还原到原来的值 ;
例如:
<Style x:Key="ButtonStyle" TargetType="{x:Type Button}">
<Style.Triggers>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Opacity" Value="0.5" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
MultiTrigger
特点:元素 (UIElement) 或控件的多个属性改变时,执行 Trigger,如:
<Style x:Key="MulitTriggerButtonStyle" TargetType="Button">
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsPressed" Value="True" />
<Condition Property="Background" Value="BlanchedAlmond" />
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter Property="Foreground" Value="Blue" />
<Setter Property="BorderThickness" Value="5" />
<Setter Property="BorderBrush" Value="Blue" />
</MultiTrigger.Setters>
</MultiTrigger>
</Style.Triggers>
</Style>
2. 事件触发器
特点:
- 当 FrameworkElement 的路由事件(RouteEvent)触发时,执行 EventTrigger;
- 通常用来执行一些动画;
- 不会还原成原来的值(不像 Property Trigger 一样);
例如:
<Border …>
<Border.Triggers>
<EventTrigger RoutedEvent="Mouse.MouseEnter">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Duration="0:0:1" Storyboard.TargetName="MyBorder"
Storyboard.TargetProperty="Color" To="Gray" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
3. 数据触发器
特点:数据绑定源的值符合 Trigger 指定的条件时,执行 Trigger;例如:
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=Picture}" Value="{x:Null}">
<Setter TargetName="viewImage" Property="Source" Value="/Images/noImage.png" />
……
</DataTrigger>
</DataTemplate.Triggers>
MultiDataTrigger
特点:当多个绑定值符合 Trigger 的 Conditions 指定的条件时,执行 Trigger;例如:
<DataTemplate.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=Picture}" Value="{x:Null}" />
<Condition Binding="{Binding Path=Title}" Value="Waterfall" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter TargetName="viewImage" Property="Source" Value="/Images/noImage.png"/>
<Setter TargetName="viewImage" Property="Opacity" Value="0.5" />
<Setter TargetName="viewText" Property="Background" Value="Brown" />
</MultiDataTrigger.Setters>
</MultiDataTrigger>
</DataTemplate.Triggers>
最后,要说明的是,对于控件的内联 (Inline) 属性,仅能设置 EventTrigger,如:
<Button>
<Button.Triggers>
<EventTrigger …>
</EventTrigger>
</Button.Triggers>
</Button>
所以,总结来看,以上三类触发器的使用场景就是当控件的属性值被更改或者事件被触发时,更修改控件的外观或行为;并且属性触发器有独特的特点,就是在属性值还原时,样式也会随即恢复。
理解了这些,在开发自定义控件时,我们就可以通过为自定义控件合理地定义依赖属性、只读依赖属性以及路由事件,来控制控件的显示。接下来,我们来看 VisualStateManager,它也可以解决同样的问题。
二、VisualStateManager
要使用 VisualStateManager,需要定义 VisualState;在 VisualState 中定义控件的不同的状态以及每种状态下的样式,然后,在代码中合适的地方,我们可以使用 VisusalStateManager 类的 GoToState 来切换到对应的状态,从而实现样式的切换。
所以,总括地说,这里涉及了以下四个方面:
- VisualState: 视图状态(Visual States)表示控件在一个特殊的逻辑状态下的样式、外观;
- VisualStateGroup: 状态组由相互排斥的状态组成,状态组与状态组并不互斥;
- VisualTransition: 视图转变 (Visual Transitions) 代表控件从一个视图状态向另一个状态转换时的过渡;
- VisualStateManager: 由它负责在代码中来切换到不同的状态;
每个 VisualState 都属于一个状态组 (VisualStateGroup),也即一个 VisualStateGroup 中可以定义多个 VisualState;并且,我们也可以定义多个 VisualStateGroup;需要再次强调的是:同一个 VisualStateGroup 中 VisualState 是互斥的,而不同的 VisualStateGroup 中的 VisualState 是在同一时刻是可以共存的。以 Button 为例:

我们看到,在它里面,定义了三个 VisualStateGroup,分别是 CommonStates(正常状态)、FocusStates(焦点状态)、ValidationStates(验证状态),而每个 VisualStateGroup 下又有若干个 VisualState。在 CommonStates 中,按钮可以是 Normal 、MouseOver 或 Pressed(只能是三者之一),但它却可以结合其它 VisualStateGroup 中的 VisualState 来显示,如按钮具有焦点时且鼠标移动到其上,这就结合了 MouseOver 与 Focused 两种状态。以下它的部分代码:
<ControlTemplate TargetType="Button">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0:0:1" To="MouseOver" />
<VisualTransition GeneratedDuration="0:0:1" To="Pressed" />
<VisualTransition GeneratedDuration="0:0:1" To="Normal" />
</VisualStateGroup.Transitions>
<VisualState x:Name="Normal" />
<VisualState x:Name="MouseOver">
<Storyboard>
<ColorAnimation
Storyboard.TargetName="BackgroundBorder"
Storyboard.TargetProperty="Background.(SolidColorBrush.Color)"
To="#A1D6FC"
Duration="0" />
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ColorAnimation
Storyboard.TargetName="BackgroundBorder"
Storyboard.TargetProperty="Background.(SolidColorBrush.Color)"
To="#FCA1A1"
Duration="0" />
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border
x:Name="BackgroundBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="true" />
<ContentPresenter
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Focusable="False"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Grid>
</ControlTemplate>
在自定义控件的开发过程中,我们也可以采用同样的原则,即在 XAML 中定义 VisualStateGroup、VisualState 以及 VisualTransition(可选),由借助于 VisualStateManager 来实现切换。以下是一个带水印功能的 TextBox 中 VisualStates 的定义:
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="WatermarkGroup">
<VisualStateGroup.Transitions>
<VisualTransition From="ShowWatermarkState" To="HideWatermarkState">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="PART_Watermark"
Storyboard.TargetProperty="Opacity" From="1"
To="0" Duration="0:0:2" />
</Storyboard>
</VisualTransition>
<VisualTransition From="HideWatermarkState" To="ShowWatermarkState">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="PART_Watermark"
Storyboard.TargetProperty="Opacity" From="0"
To="1" Duration="0:0:2" />
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="ShowWatermarkState">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0:0:0"
Storyboard.TargetName="PART_Watermark"
Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="0:0:0"
Value="{x:Static Visibility.Visible}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="HideWatermarkState">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0:0:0"
Storyboard.TargetName="PART_Watermark"
Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="0:0:0"
Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
可以看出,它定义了 WatermarkGroup 组,并在其中定义了 ShowWatermarkState 和 HideWatermarkState 两个 VisualState,并且,通过定义的 VisualTransition 能够实现在这两种状态下切换时,水印文本会有淡入、淡出的效果。
最后,在代码中,调用 VisualStateManager.GoToState 方法来切换到合适的状态即可:
private void UpdateState()
{
bool textExists = Text.Length > ;
var watermark = GetTemplateChild("PART_Watermark") as FrameworkElement;
var state = textExists || IsFocused ? "HideWatermarkState" : "ShowWatermarkState"; VisualStateManager.GoToState(this, state, true);
} protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
UpdateState();
} protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
UpdateState();
}
最后,何时切换状态呢?一般情况下,包括但不限于以下几个场合或地方:
- 在 OnApplyTemplate 方法(即应用模板时);
- 当某个属性改变时(可以在 PropertyChangedCallback 中调用);
- 当某个事件发生后;
三、Triggers 与 VisualStateManager 的区别
以上我们看到 Trigger 和 VisualStateManager 都可以完成相同的事,不过它们也是有区别的,主要有以下几点:
- Triggers 仅仅在 XAML 中使用(尽管它可能要用到我们的自定义属性或事件,但对于更改状态这件事来说,我们只要在 XAML 中操作即可),而 VisualStateManager 则 XAML 和 C# 代码都需要;
- 对于 Trigger,定义模板的人可以自由地指定当条件符合时时该有何种的变化;而对于 VisualStateManager,则需要控件开发人员定义不同的 VisualState,然后定义模板的人根据约定(根据 TemplateVisualStateAttribute 特性)来定义在控件不同状态下的样式;
- 对于 Trigger,它是对事件、属性或者所绑定的数据发生变化时,作出对应的改变;而 VSM 则可以自由控制状态的切换,并定在切换时,还可以指定 VisualTransition;
- 最后, VisualStateManager 不仅支持 WPF,而且也支持 UWP,我们可以说它是“跨平台”的,而 Trigger 在 UWP 上不被支持。
总结
本文主要讨论了 WPF 中的触发器 Triggers 和 VisualStateManager,在设计自定义控件时,它们都能够辅以完成控件样式切换的功能;我们分别介绍了它们二者的使用方法以及使用要点;最后我们也总结了它们的区别。当我们了解了它们各自的特点以及它们的区别后,我们就可以有选择地、更方便地来设计我们的自定义控件,并在 WPF 开发过程中自由地运用它们。
参考资料:
VisualStateManager and alternative to Triggers
WPF: WPF 中的 Triggers 和 VisualStateManager的更多相关文章
- wpf自定义控件中使用自定义事件
wpf自定义控件中使用自定义事件 1 创建自定义控件及自定义事件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 2 ...
- wpf XMAL中隐藏控件
原文:wpf XMAL中隐藏控件 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/a771948524/article/details/9264569 ...
- 在WPF程序中使用摄像头兼谈如何使用AForge.NET控件(转)
前言: AForge.NET 是用C#写的一个关于计算机视觉和人工智能领域的框架,它包括图像处理.神经网络.遗传算法和机器学习等.在C#程序中使用摄像头,我习惯性使用AForge.NET提供的类库.本 ...
- WPF 程序中启动和关闭外部.exe程序
当需要在WPF程序启动时,启动另一外部程序(.exe程序)时,可以按照下面的例子来: C#后台代码如下: using System; using System.Collections.Generic; ...
- 如何在WPF程序中使用ArcGIS Engine的控件
原文 http://www.gisall.com/html/47/122747-4038.html WPF(Windows Presentation Foundation)是美国微软公司推出.NET ...
- WPF/Silverlight中图形的平移,缩放,旋转,倾斜变换演示
原文:WPF/Silverlight中图形的平移,缩放,旋转,倾斜变换演示 为方便描述, 这里仅以正方形来做演示, 其他图形从略. 运行时效果图:XAML代码:// Transform.XAML< ...
- WPF/Silverlight中的RichTextBox总结
WPF/Silverlight中的RichTextBox总结 在WPF或者是在Silverlight中有个非常强大的可以编辑的容器控件RichTextBox,有的时间会采取该控件来作为编辑控件.鉴 ...
- WPF程序中App.Config文件的读与写
WPF程序中的App.Config文件是我们应用程序中经常使用的一种配置文件,System.Configuration.dll文件中提供了大量的读写的配置,所以它是一种高效的程序配置方式,那么今天我就 ...
- WPF项目中解决ConfigurationManager不能用(转)
https://blog.csdn.net/MOESECSDN/article/details/78107888 在WPF项目中遇到这样的问题,做一下笔记.希望对自己和读者都有帮助. 在aap.con ...
随机推荐
- css3渐变之径向渐变
径向渐变由它的中心定义.可以指定渐变的中心.形状(原型或椭圆形).大小.默认情况下,渐变的中心是 center(表示在中心点),渐变的形状是 ellipse(表示椭圆形),渐变的大小是 farthes ...
- k8s 创建资源的两种方式 - 每天5分钟玩转 Docker 容器技术(124)
命令 vs 配置文件 Kubernetes 支持两种方式创建资源: 1. 用 kubectl 命令直接创建,比如: kubectl run nginx-deployment --image=nginx ...
- 把织梦安装到子目录,不读取CSS 没有样式?
我在A5上找的一个模板,照着说明安装到根目录就正常,我想安装到子目录下面,结果很乱 应该是不读取CSS. {dede:global.cfg_templets_skin/}/style/about.cs ...
- 关于MacOS升级10.13系统eclipse菜单灰色无法使用解决方案
最近,苹果发布了macOS High Sierra,版本为10.13,专门针对mac pro的用户来着,至于好处大家到苹果官网看便是,我就是一个升级新版本系统的受益者,同时也变成了一个受害者:打开ec ...
- HTML <select>标签
1.简单的下拉列表 <html> <body> <form> 名: <select name="firstname"> <op ...
- requests关于Exceeded 30 redirects问题得出的结论
昨天一个朋友在爬网页时出现的一个问题,以及后续我对这个问题进行了简单的测试. 先说出现的问题的简单描述. 首先是使用urllib请求网页: #urllib.request发起的请求 import ur ...
- wamp版本升级小问题记录
在升级wamp版本时遇到的一些小问题,特此记录 在安装完成之后,修改了Apache根目录,可以正常访问.但是发现 httpd-vhosts.conf追加配置的无法访问,逐步检查,有以下问题 1.Inc ...
- python_如何进行反向迭代和实现反向迭代?
案例: 实现一个连续的浮点数发生器,FloatRange,根据给定范围(start, end) 和步进值,产生一些列的浮点数,例如:FloatRange(3,4,0.2),将产生下列序列: 正向:3. ...
- js 数组与对象的区别
学习javascript的时候,我曾经一度搞不清楚”数组”(array)和”对象”(object)的根本区别在哪里,两者都可以用来表示数据的集合. 比如有一个数组a=[1,2,3,4],还有一个对 ...
- Android开发模板代码(一)——简单打开图库选择照片
首先,先贴上样本代码 //检查权限 public void checkPermission() { if (ContextCompat.checkSelfPermission(this, Manife ...