[UWP]了解模板化控件(5):VisualState

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与代码分离原则及开放封闭原则。

时间: 2024-08-02 18:36:14

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

[UWP]了解模板化控件(4):TemplatePart

1. TemplatePart TemplatePart(部件)是指ControlTemplate中的命名元素.控件逻辑预期这些部分存在于ControlTemplate中,并且使用protected DependencyObject GetTemplateChild(String childName)获取它们后进行操作. 以AutoSuggestBox为例,它的ControlTemplate结构如下,可以看到AutoSuggestBox由四个TemplatePart组成,每个TemplatePa

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

1. TemplatePart vs. VisualState 在前面两篇文章中分别使用了TemplatePart及VisualState的方式实现了相同的功能,其中明显VisualState的方式更灵活一些.如果遇到这种情况通常我更倾向使用VisualState.不过在实际应用中这两种实现方式并不是互斥的,很多模板化控件都同时使用这两种方式, 使用VisualState有如下好处: 代码和UI分离. 可以更灵活地扩展控件. 可以使用Blend轻松实现动画. 并不是说VisualState好处这

[UWP]了解模板化控件(5.2):UserControl vs. TemplatedControl

1. UserControl vs. TemplatedControl 在UWP中自定义控件常常会遇到这个问题:使用UserControl还是TemplatedControl来自定义控件. 1.1 使用UserControl自定义控件 继承自UserControl. 由复数控件组合而成. 包含XAML及CodeBehind. 优点: 上手简单. 可以在CodeBehind直接访问UI元素. 开发速度很快. 缺点: 不能使用ControlTemplate进行定制. 通常很难继承及扩展. 使用场景:

[WPF自定义控件库] 自定义控件的代码如何与ControlTemplate交互

1. 前言 WPF有一个灵活的UI框架,用户可以轻松地使用代码控制控件的外观.例设我需要一个控件在鼠标进入的时候背景变成蓝色,我可以用下面这段代码实现: protected override void OnMouseEnter(MouseEventArgs e) { base.OnMouseEnter(e); Background = new SolidColorBrush(Colors.Blue); } 但一般没人会这么做,因为这样做代码和UI过于耦合,难以扩展.正确的做法应该是使用代码告诉C

在 UWP 中实现 Expander 控件

WPF 中的 Expander 控件在 Windows 10 SDK 中并不提供,本文主要说明,如何在 UWP 中创建这样一个控件.其效果如下图: 首先,分析该控件需要的一些特性,它应该至少包括如下三个属性: Content: 最重要的属性,设置该属性,可以使 Expander 控件显示其内容: Header: 控件的 Header: IsExpand: 当前是否展开. 接下来是定义其 UI,在这里使用 Grid,添加两行,一行显示 Header,一行显示 Content,当 IsExpand

UWP:可滚动的PivotHeader

原文:UWP:可滚动的PivotHeader UWP开发里,Pivot真是个令人又爱又恨的控件.为了实现某些可滚动Header的效果,有些大佬甚至去掉了原本的Header,使用一个ListView或者ListBox自己画Header,不过这样会让控件变得很复杂. 既然Pivot是一个模板化控件,那么应该有方法直接让Header可以滚动. 先贴效果图: 先自定义Pivot的Style,从generic.xaml找(位置应该在C:\Program Files (x86)\Windows Kits\1

[UWP]合体姿势不对的HeaderedContentControl

原文:[UWP]合体姿势不对的HeaderedContentControl 1. 前言 HeaderedContentControl是WPF中就存在的控件,这个控件的功能很简单:提供Header和Content两个属性,在UI上创建两个ContentPresenter并分别绑定到Header和Content,让这两个ContentPresenter合体组成HeaderedContentControl. 2. 以前的问题 在WPF中,HeaderedContentControl是Expander.

[UWP]为番茄钟应用设计一个平平无奇的状态按钮

1. 为什么需要设计一个状态按钮 OnePomodoro应用里有个按钮用来控制计时器的启动/停止,本来这应该是一个包含"已启动"和"已停止"两种状态的按钮,但我以前在WPF和UWP上做过太多StateButton.ProgressButton之类的东西,已经厌倦了这种控件,所以我在OnePomodoro应用里只是简单地使用两个按钮来实现这个功能: <Button Content="" Visibility="{x:Bind Vi

[UWP小白日记-3]记账项目-1

学了一段时间的UWP,来个项目试试手. 本来是想边做边学MVVMLight的结果感觉MVVM对于萌新来说太高难,以后再把这个项目改造成MVVMLight框架的项目. 下面进入正题. 中间那快空白打算放gridview,用来放标签.利用DataTemplate读取数据库里的标签表. 头部标题和底部标签没什么好说的,主要说下那个pivot的实现. 先来看看图,就知道有什么问题了,然后在解决这个问题. 鼠标点最左边尽然也能跳转页面,这什么鬼,改了他的默认Styel一样不行. 1 <Style x:Ke