1. 功能需求

使用TemplatePart实现上篇文章的两个需求(Header为空时隐藏HeaderContentPresenter,鼠标没有放在控件上时HeaderContentPresent半透明),虽然功能已经实现,但这样实现的话基本上也就别想扩展了。譬如开发者做不到通过继承或修改ControlTemplate实现如下功能:

  • 半透明时的Opacity不是0.7,而是0.5。
  • 半透明和不透明之前切换时有渐变动画。

当然也并不是不可以用代码实现这些需求,只是会复杂很多。大部分的开发者都是对C#熟悉,对XAML陌生,很容易就选择尽量使用C#实现全部功能,将所有功能集中在同一个地方并用熟悉的语言处理,当然也有这样做的优点,不过既然在用XAML平台,就应该尽可能利用XAML平台UI和代码分离的优点。

这篇文章用ContentView2示例讲解VisualState如何实现上述的需求,ContentView2和上篇文章的ContentView一样继承自HeaderedContentControl。

2. VisualState

在实现需求前首先解释VisualState的概念。

VisualState 指定控件处于特定状态时的外观。控件的代码指定控件处于何种状态,控件的ControlTemplate中根节点包含VisualStateManager.VisualStateGroups附加属性,并在其中确定各个VisualState的外观。

以CheckBox为例,CheckBox基本上包含Unchecked、Checked、Indeterminate三种状态,它通过IsChecked的值在这三种状态中转换。

这三种状态的外观如下所示:

实际上Checkbox的VisualState复杂很多,这里是简化的模型。

3. 确定VisualState

要使用VisualState,首先要明确控件中包含哪些VisualState。在ContentView2中有两组VisualState:

  • CommonStates: 默认是“Normal”,当鼠标进入控件时是“PointerOver”。
  • HeaderStates: 默认是“NoHeader”,当Header属性的值不为空时是“HasHeader”。

其中“CommonStates”、“HeaderStates”称为VisualStateGroup,“Normal”、“PointerOver”等称为VisualState。在同一个VisualStateGroup中的VisualState是互斥的,控件始终只能处于每组状态中的一种。例如,控件只能处于NoHeader状态,或者HasHeader状态。

模板化控件可以使用TemplateVisualStateAttribute协定声明它的VisualState,用于通知控件的使用者有这些VisualState可用。TemplateVisualStateAttribute是可选的,而且就算控件声明了这些VisualState,ControlTemplate也可以不包含它们中的任何一个,并且不会引发异常。

ContentView2的TemplateVisualStateAttribute如下:

[TemplateVisualState(Name = NormalState, GroupName = CommonStates)]
[TemplateVisualState(Name = PointerOverState,GroupName =CommonStates)]
[TemplateVisualState(Name = NoHeaderState, GroupName = HeaderStates)]
[TemplateVisualState(Name = HasHeaderState, GroupName = HeaderStates)]
public class ContentView2 : HeaderedContentControl
{
public const string CommonStates = "CommonStates";
public const string NormalState = "Normal";
public const string PointerOverState = "PointerOver"; public const string HeaderStates = "HeaderStates";
public const string NoHeaderState = "NoHeader";
public const string HasHeaderState = "HasHeader"; }

4. VisualStateManager

VisualStateManager用于管理VisualState并操作它们之间的转换。

public ContentView2()
{
this.DefaultStyleKey = typeof(ContentView2);
} private bool _isPointerEntered; protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
UpdateVisualState(false);
} protected override void OnPointerEntered(PointerRoutedEventArgs e)
{
base.OnPointerEntered(e);
_isPointerEntered = true;
UpdateVisualState();
} protected override void OnPointerExited(PointerRoutedEventArgs e)
{
base.OnPointerExited(e);
_isPointerEntered = false;
UpdateVisualState();
} protected override void OnHeaderChanged(object oldValue, object newValue)
{
base.OnHeaderChanged(oldValue, newValue);
UpdateVisualState();
} internal virtual void UpdateVisualState(bool useTransitions = true)
{
if (_isPointerEntered)
VisualStateManager.GoToState(this, PointerOverState, useTransitions);
else
VisualStateManager.GoToState(this, NormalState, useTransitions); if (Header == null)
VisualStateManager.GoToState(this, NoHeaderState, useTransitions);
else
VisualStateManager.GoToState(this, HasHeaderState, useTransitions);
}

ContentView2的其它代码如上所示,在OnApplyTemplate、OnHeaderChanged及鼠标进入离开时使用VisualStateManager.GoToState(Control control, string stateName,bool useTransitions)更新VisualState。useTransitions这个参数指示是否使用 VisualTransition 进行状态过渡,简单来说即是VisualState之间切换时用不用VisualTransition里面定义的动画。

注意OnApplyTemplate中的这句代码:UpdateVisualState(false)。控件在加载ControlTemplate时就需要确定它的状态,一般这时候都不会使用过渡动画。

VisualStateManager.GoToState不会使控件重复进入某个状态,譬如如果控件已处于PointerOverState,再次调用VisualStateManager.GoToState(this, PointerOverState, useTransitions)不会触发任何操作,也不会打断正在执行的过渡动画或重复触发动画。

到这里为止ContentView2.cs的工作已经完成,接下来就是XAML的责任了。

5. 使用Blend编辑ControlTemplate

使用Blend编辑ContentView2的空白ControlTemplate时,由于已经声明了TemplateVisualStateAttribute,可以看到在“状态”窗口已经默认就有定义好的状态。

编辑后结果如下:

<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0:0:0.5">
<VisualTransition.GeneratedEasingFunction>
<CubicEase EasingMode="EaseInOut" />
</VisualTransition.GeneratedEasingFunction>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Target="HeaderContentPresenter.(UIElement.Opacity)"
Value="0.5" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="HeaderContentPresenter.(UIElement.Opacity)"
Value="1" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="HeaderStates">
<VisualState x:Name="NoHeader">
<VisualState.Setters>
<Setter Target="HeaderContentPresenter.(UIElement.Visibility)"
Value="Collapsed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="HasHeader" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>

从XAML中可以看出VisualState子节点的Setter是关键所在,如PointerOver的VisualState通过Setter将HeaderContentPresenter的Opacity更改为1,满足了“当鼠标移动到控件控件上时,设置Header的Opacity=1”这个需求。

另外,VisualStateGroup.Transitions 节点定义了CommonStates在各个状态之间切换时的过渡动画。VisualStateManager.GoToState(this, PointerOverState, useTransitions) 中的参数useTransitions即是控制是否使用过渡动画。示例中使用的过渡动画为CubicEase,过渡时间为0.5秒。

需要注意的是不同VisualStateGroup之间尽量不要对同一个UI元素的同一个属性进行操作,否则会引起冲突。

这个主题不会详细讲解使用Blend修改VisualState,因为那会占用很多篇幅。幸好Blend在这方面做得很容易上手,而且多年来基本操作都没有变过,可以在网上找到很多这方面的文章。

6. 结论

很多时候VisualState方式并不会比TemplatePart方式少写代码,譬如ContentView2的代码量就基本和ContentView一致,而XAML行数还更多。但VisualState的实现方式更灵活,更加符合UI与代码分离原则及开放封闭原则。

[UWP 自定义控件]了解模板化控件(5):VisualState的更多相关文章

  1. UWP 自定义控件:了解模板化控件 系列文章

    UWP自定义控件的入门文章 [UWP 自定义控件]了解模板化控件(1):基础知识 [UWP 自定义控件]了解模板化控件(2):模仿ContentControl [UWP 自定义控件]了解模板化控件(2 ...

  2. [UWP 自定义控件]了解模板化控件(5.1):TemplatePart vs. VisualState

    1. TemplatePart vs. VisualState 在前面两篇文章中分别使用了TemplatePart及VisualState的方式实现了相同的功能,其中明显VisualState的方式更 ...

  3. [UWP 自定义控件]了解模板化控件(10):原则与技巧

    1. 原则 推荐以符合以下原则的方式编写模板化控件: 选择合适的父类:选择合适的父类可以节省大量的工作,从UWP自带的控件中选择父类是最安全的做法,通常的选择是Control.ContentContr ...

  4. [UWP 自定义控件]了解模板化控件(4):TemplatePart

    1. TemplatePart TemplatePart(部件)是指ControlTemplate中的命名元素.控件逻辑预期这些部分存在于ControlTemplate中,并且使用protected ...

  5. [UWP 自定义控件]了解模板化控件(8):ItemsControl

    1. 模仿ItemsControl 顾名思义,ItemsControl是展示一组数据的控件,它是UWP UI系统中最重要的控件之一,和展示单一数据的ContentControl构成了UWP UI的绝大 ...

  6. [UWP 自定义控件]了解模板化控件(9):UI指南

    1. 使用TemplateSettings统一外观 TemplateSettings提供一组只读属性,用于在新建ControlTemplate时使用这些约定的属性. 譬如,修改HeaderedCont ...

  7. [UWP 自定义控件]了解模板化控件(1):基础知识

    1.概述 UWP允许开发者通过两种方式创建自定义的控件:UserControl和TemplatedControl(模板化控件).这个主题主要讲述如何创建和理解模板化控件,目标是能理解模板化控件常见的知 ...

  8. [UWP 自定义控件]了解模板化控件(2):模仿ContentControl

    ContentControl是最简单的TemplatedControl,而且它在UWP出场频率很高.ContentControl和Panel是VisualTree的基础,可以说几乎所有VisualTr ...

  9. [UWP 自定义控件]了解模板化控件(3):实现HeaderedContentControl

    1. 概述 来看看这段XMAL: <StackPanel Width="300"> <TextBox Header="TextBox" /&g ...

随机推荐

  1. WebSocket实现简单的在线聊天

    SuperWebSocket在WebService中的应用 最开始使用是寄托在IIS中,发布之后测试时半个小时就会断开,所以改为WindowsService 1. 新建Windows服务项目[Test ...

  2. 解决wordpress上传文件出现http错误问题

    解决wordpress上传文件出现http错误问题 问题现象 今天上传约1.4m大小的gif文件到wordpress的媒体库时失败,提示http错误. 原因 由于之前一直上传图片都是可以的,所以推测最 ...

  3. logstash过滤器插件filter详解及实例

    1.logstash过滤器插件filter 1.1.grok正则捕获 grok是一个十分强大的logstash filter插件,他可以通过正则解析任意文本,将非结构化日志数据弄成结构化和方便查询的结 ...

  4. 6.3Pytyhon文件的操作(三)

    目录 目录 前言 (一)文件的创建 (二)文件的删除 (三)文件的重命名 (四)文件的查看 (五)文件的复制 ==1.小文件的复制== ==2.大文件的复制== (六)文件的实战案例 ==1.文件的分 ...

  5. 个人技术博客(a)

  6. 拓普微小尺寸TFT液晶屏-高性价比

    智能模块(Smart LCD)是专为工业显示应用而设计的TFT液晶显示模块. 模块自带主控IC.Flash存储器.实时嵌入式操作系统,客户主机可把要存储的数据(如背景图.图标等)存储到屏的flash中 ...

  7. mysql 拒绝访问的解决办法

    java.sql.SQLException: null,  message from server: "Host 'xxx' is not allowed to connect to thi ...

  8. Django templates 模板的语法

    MVC 以及 MTV MVC: M : model -->> 存取数据(模型) V: view -->> 信息的展示(视图) C: controller -->> ...

  9. Scrapy运行流程

    接下来的图表展现了Scrapy的架构,包括组件及在系统中发生的数据流的概览(绿色箭头所示). 下面对每个组件都做了简单介绍,并给出了详细内容的链接.数据流如下所描述. 来源于https://scrap ...

  10. centos增加环境变量

    在Linux CentOS系统上安装软件后,如果需要运行软件,一般是需要进入到软件的bin目录下进行才可以运行相应命令,否则会提示未找到命令.为了使用方便,我们一般把相应的bin目录增加到环境变量中( ...