[UWP 自定义控件]了解模板化控件(9):UI指南
1. 使用TemplateSettings统一外观
TemplateSettings提供一组只读属性,用于在新建ControlTemplate时使用这些约定的属性。
譬如,修改HeaderedContentControl的ControlTemplate以呈现不同的外观,但各个ControlTemplate之间的HeaderedContentControl中的Margin和FontWeight想要保持统一。为了实现这个目的可以创建一个提供默认Margin和FontWeight值的HeaderedContentControlTemplateSettings类。实现如下:
HeaderedContentControlTemplateSettings.cs
public class HeaderedContentControlTemplateSettings: DependencyObject
{
public Thickness HeaderMargin
{
get
{
return new Thickness(0, 0, 0, 8);
}
}
public FontWeight HeaderFontWeight
{
get
{
return FontWeights.Normal;
}
}
}
HeaderedContentControl.cs
public HeaderedContentControl()
{
this.DefaultStyleKey = typeof(HeaderedContentControl);
TemplateSettings = new HeaderedContentControlTemplateSettings();
}
public HeaderedContentControlTemplateSettings TemplateSettings { get; }
Generic.xaml
<ContentPresenter x:Name="HeaderContentPresenter"
Visibility="Collapsed"
Foreground="{ThemeResource TextControlHeaderForeground}"
Margin="{Binding RelativeSource={RelativeSource TemplatedParent},Path=TemplateSettings.HeaderMargin}"
FontWeight="{Binding RelativeSource={RelativeSource TemplatedParent},Path=TemplateSettings.HeaderFontWeight}"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"/>
TemplateSettings类有约定的命名规则,默认以使用它的控件的名称作为前缀,以“-TemplateSettings”作为后缀。
UWP中有多个 TemplateSettings 类。 它们全部都在 Windows.UI.Xaml.Controls.Primitives 命名空间中,如ComboBox.TemplateSettings和ProgressBar.TemplateSettings。
2. 借用附加属性
以TextBox为例,TextBox中包含一个ScrollViewer部件,想要通过属性控制这个ScrollViewer,其中一种做法是在TextBox中添加各项属性,然后在ControlTemplate中通过TemplateBinding设置到ScrollViewer的对应属性。使用方式如下:
<TextBox HorizontalScrollMode="Auto"
HorizontalScrollBarVisibility="Auto"
VerticalScrollMode="Auto"
VerticalScrollBarVisibility="Auto"
IsHorizontalRailEnabled="True"
IsVerticalRailEnabled="True"
IsDeferredScrollingEnabled="True" />
假设真的这么做,TextBox就会多了很多个属性,而其它包含ScrollViewer的控件也很可能参考TextBox添加这一大批属性。
幸运的是ScrollViewer将这些属性做成了附加属性,其它控件可以借这些属性来用。实际的使用方式如下:
<TextBox ScrollViewer.HorizontalScrollMode="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollMode="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.IsHorizontalRailEnabled="True"
ScrollViewer.IsVerticalRailEnabled="True"
ScrollViewer.IsDeferredScrollingEnabled="True" />
在TextBox的ControlTemplate中,ScrollViewer是这样绑定到附加属性的:
<ScrollViewer x:Name="ContentElement"
Grid.Row="1"
HorizontalScrollMode="{TemplateBinding ScrollViewer.HorizontalScrollMode}"
HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollMode="{TemplateBinding ScrollViewer.VerticalScrollMode}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
IsHorizontalRailEnabled="{TemplateBinding ScrollViewer.IsHorizontalRailEnabled}"
IsVerticalRailEnabled="{TemplateBinding ScrollViewer.IsVerticalRailEnabled}"
IsDeferredScrollingEnabled="{TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}"
Margin="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
IsTabStop="False"
AutomationProperties.AccessibilityView="Raw"
ZoomMode="Disabled" />
如果控件像ScrollViewer那样被频繁地使用,可以考虑定义这样的附加属性,这样既方便通过属性定制外观,又可以少定义很多属性。唯一的坏处,就是用户根本不知道原来有这些属性可用。
以下是ScrollViewer定义的全部附加属性:
- ScrollViewer.BringIntoViewOnFocusChange
- ScrollViewer.HorizontalScrollBarVisibility
- ScrollViewer.HorizontalScrollMode
- ScrollViewer.IsDeferredScrollingEnabled
- ScrollViewer.IsHorizontalRailEnabled
- ScrollViewer.IsHorizontalScrollChainingEnabled
- ScrollViewer.IsScrollInertiaEnabled
- ScrollViewer.IsVerticalRailEnabled
- ScrollViewer.IsVerticalScrollChainingEnabled
- ScrollViewer.IsZoomChainingEnabled
- ScrollViewer.IsZoomInertiaEnabled
- ScrollViewer.VerticalScrollBarVisibility
- ScrollViewer.VerticalScrollMode
- ScrollViewer.ZoomMode
3. StyleTypedPropertyAttribute
想进一步开放对部件外观的控制,可以考虑添加一个Style属性。例如,前述例子中的DateTimeSelector中包含一个TimePicker部件,可以公开一个TimePickerStyle属性让TimePicker绑定到这个属性。
/// <summary>
/// 获取或设置TimePickerStyle的值
/// </summary>
public Style TimePickerStyle
{
get { return (Style)GetValue(TimePickerStyleProperty); }
set { SetValue(TimePickerStyleProperty, value); }
}
<TimePicker x:Name="TimeElement" Style="{TemplateBinding TimePickerStyle}"/>
为了让其他人清楚这个Style的TargetType,可以在DateTimeSelector类上添加StyleTypedPropertyAttribute:
[StyleTypedProperty(Property = "TimePickerStyle", StyleTargetType = typeof(TimePicker))]
4. IsTabStop
要在UI上使用“Tab”键导航到某个控件,需要将这个控件的IsTabStop设置为True(默认值就是True)。如果设置成False,不止不能导航到,而且还不能获得焦点。
IsTabStop是Control的属性,FrameworkElement并没有这个属性。
对于复合型控件(即ControlTemplate中包含其它控件的控件,譬如DateTimeSelector,它本身是一个控件,又包含CalendarDatePicker和TimePicker),很多时候需要将IsTabStop默认设置成False。
<StackPanel>
<TextBox Width="300"
HorizontalAlignment="Left" />
<local:DateTimeSelector HorizontalAlignment="Left"
Margin="0,10" />
<ComboBox Width="300"
HorizontalAlignment="Left" />
</StackPanel>
在上面这段XAML中,如果DateTimeSelector.IsTabStop=True,在TextBox上需要输入两次“Tab”DateTimeSelector内的CalendarDatePicker才能获得焦点,但用户通常期望的是按一次Tab就能导航到CalendarDatePicker。这是因为Tab的导航顺序是用深度优先算法搜索VisualTree上的Control。DateTimeSelector和CalendarDatePicker都是Control,Tab会让DateTimeSelector先获得焦点,然后才让CalendarDatePicker获得焦点。解决办法是将DateTimeSelector的IsTabStop设置为False,这样Tab会忽略DateTimeSelector,由于Tab的导航顺序是深度优先,所以先是CalendarDatePicker获得焦点,然后是TimePicker,然后才是ComboBox。
再重申一次,模板化控件的属性默认值要在DefaultStyle中设置,尽量不要在构造函数中设置。
5. 处理焦点外观
5.1 FocusVisual
FocusVisual指控件获得焦点时的视觉指示器,默认是一个围绕控件边界的矩形边框。通常只用Tab键导航并获得焦点FocusVisual才会显示。UWP提供了一组FucosVisual属性用于控制这个矩形边框的外观。
<RadioButton FocusVisualMargin="-10"
FocusVisualPrimaryBrush="Red"
FocusVisualPrimaryThickness="2"
FocusVisualSecondaryBrush="Green"
FocusVisualSecondaryThickness="3"
Content="RadioButton"/>
其中 FocusVisualPrimary指外边框,FocusVisualSecondary指内边框。
使用UseSystemFocusVisuals="False"
可以禁用默认的FocusVisual。
FocusVisual属性属于FrameworkElement,这意味着派生自FrameworkElement的元素理论上都可以由FocusVisual。
5.2 IsTemplateFocusTarget
IsTemplateFocusTarget
附加属性是Control类提供的唯一一个附加属性。控件在获得焦点时会尝试从已加载的ControlTemplate中查找Control.IsTemplateFocusTarget="True"
的UI元素,如果找到,就将FocusVisual绘制到这个元素的边界。
<ControlTemplate TargetType="RadioButton">
<Grid x:Name="RootGrid"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
...
<Grid Height="32" Control.IsTemplateFocusTarget="True"
VerticalAlignment="Top">
...
</Grid>
<ContentPresenter x:Name="ContentPresenter"
AutomationProperties.AccessibilityView="Raw"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
Content="{TemplateBinding Content}"
Grid.Column="1"
Foreground="{TemplateBinding Foreground}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"
TextWrapping="Wrap"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Grid>
</ControlTemplate>
5.3 自定义FocusVisual
如果确实需要完全自定义FocusVisual的外观,可以重写ControlTemplate,在VisualStateManager.VisualStateGroups
中加入名称为FocusStates的VisualSateGroup,其中包含三个VisualState:
- Focused: 使用Tab导航并获得焦点的状态;
- Unfocused: 没获得任何焦点的状态;
- PointerFocused: 点击控件并获得焦点的状态;
Control自身已处理好在这三个状态中转换的逻辑,不需要额外写代码来转换状态。在ControlTemplate使用如下:
<Grid x:Name="RootGrid"
Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<!--other visual state groups here-->
<VisualStateGroup x:Name="FocusStates">
<VisualState x:Name="Focused">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="FocusVisual"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0" />
</Storyboard>
</VisualState>
<VisualState x:Name="Unfocused" />
<VisualState x:Name="PointerFocused" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentPresenter x:Name="ContentPresenter"
AutomationProperties.AccessibilityView="Raw"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
Content="{TemplateBinding Content}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
Padding="{TemplateBinding Padding}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
<Rectangle x:Name="FocusVisual" StrokeThickness="1" Stroke="BlueViolet" StrokeDashArray="4 2" Opacity="0"/>
</Grid>
6. 简化ControlTemplate
通过简化ControlTemplate可以有效提交UI的性能。先看一个反例:
<Border x:Name="Background"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="White"
CornerRadius="3">
<Grid Background="{TemplateBinding Background}"
Margin="1">
<Border x:Name="BackgroundAnimation"
Background="#FF448DCA"
Opacity="0" />
<Rectangle x:Name="BackgroundGradient">
<Rectangle.Fill>
<LinearGradientBrush EndPoint=".7,1"
StartPoint=".7,0">
<GradientStop Color="#FFFFFFFF"
Offset="0" />
<GradientStop Color="#F9FFFFFF"
Offset="0.375" />
<GradientStop Color="#E5FFFFFF"
Offset="0.625" />
<GradientStop Color="#C6FFFFFF"
Offset="1" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</Grid>
</Border>
<ContentPresenter x:Name="contentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
<Rectangle x:Name="DisabledVisualElement"
Fill="#FFFFFFFF"
IsHitTestVisible="false"
Opacity="0"
RadiusY="3"
RadiusX="3" />
<Rectangle x:Name="FocusVisualElement"
IsHitTestVisible="false"
Margin="1"
Opacity="0"
RadiusY="2"
RadiusX="2"
Stroke="#FF6DBDD1"
StrokeThickness="1" />
这是Silverlight中Button的ControlTemplate(不包含VisualState)。复杂的XAML结构不止影响了性能,还做了错误的示范。
简化XAML结构对CPU使用率及性能开销都有好处。幸好现在的主流是扁平化的简单的设计,在UWP中按钮的模板被大大简化:
<ContentPresenter x:Name="ContentPresenter"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
ContentTransitions="{TemplateBinding ContentTransitions}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
AutomationProperties.AccessibilityView="Raw" />
以我的经验来说,控件层级UI尽量保持简洁,或者与系统保持一致,后期维护起来也更简单,出错几率更少,性能也会更好(通常自己设计的ControlTemplate性能都不会比系统自带的好)。
7. 缩短过渡动画时间
为了给人系统流畅的感觉,过渡动画通常限制在1秒以内。曾经看过一个说法:把设计动画时觉得合理的时间,再缩短一半才是合适的。
另外,操作后0.5秒内要给出反应,否则用户会以为系统没有反应,甚至有可能重复操作。
8. 符合操作系统的操作习惯
以Windows平台来说,典型的错误是将约定俗成的“OK、Cancel”顺序改成“Cancel、OK”,甚至同一个程序中同时存在两种状况。
例如这个对话框,一不小心就点击左边的“取消”按钮了。
9. 符合典型的GUI设计原则
在控件层级就应该将UI设计成符合设计原则,例如对齐,使用字体和颜色突出主要内容,易于操作等。
[UWP 自定义控件]了解模板化控件(9):UI指南的更多相关文章
- UWP 自定义控件:了解模板化控件 系列文章
UWP自定义控件的入门文章 [UWP 自定义控件]了解模板化控件(1):基础知识 [UWP 自定义控件]了解模板化控件(2):模仿ContentControl [UWP 自定义控件]了解模板化控件(2 ...
- [UWP 自定义控件]了解模板化控件(8):ItemsControl
1. 模仿ItemsControl 顾名思义,ItemsControl是展示一组数据的控件,它是UWP UI系统中最重要的控件之一,和展示单一数据的ContentControl构成了UWP UI的绝大 ...
- [UWP 自定义控件]了解模板化控件(10):原则与技巧
1. 原则 推荐以符合以下原则的方式编写模板化控件: 选择合适的父类:选择合适的父类可以节省大量的工作,从UWP自带的控件中选择父类是最安全的做法,通常的选择是Control.ContentContr ...
- [UWP 自定义控件]了解模板化控件(1):基础知识
1.概述 UWP允许开发者通过两种方式创建自定义的控件:UserControl和TemplatedControl(模板化控件).这个主题主要讲述如何创建和理解模板化控件,目标是能理解模板化控件常见的知 ...
- [UWP 自定义控件]了解模板化控件(2):模仿ContentControl
ContentControl是最简单的TemplatedControl,而且它在UWP出场频率很高.ContentControl和Panel是VisualTree的基础,可以说几乎所有VisualTr ...
- [UWP 自定义控件]了解模板化控件(5.2):UserControl vs. TemplatedControl
1. UserControl vs. TemplatedControl 在UWP中自定义控件常常会遇到这个问题:使用UserControl还是TemplatedControl来自定义控件. 1.1 使 ...
- [UWP 自定义控件]了解模板化控件(2.1):理解ContentControl
UWP的UI主要由布局容器和内容控件(ContentControl)组成.布局容器是指Grid.StackPanel等继承自Panel,可以拥有多个子元素的类.与此相对,ContentControl则 ...
- [UWP 自定义控件]了解模板化控件(3):实现HeaderedContentControl
1. 概述 来看看这段XMAL: <StackPanel Width="300"> <TextBox Header="TextBox" /&g ...
- [UWP 自定义控件]了解模板化控件(4):TemplatePart
1. TemplatePart TemplatePart(部件)是指ControlTemplate中的命名元素.控件逻辑预期这些部分存在于ControlTemplate中,并且使用protected ...
随机推荐
- C#语言————选择结构
int[] num = new int[] {23,76,54,87,51,12 }; //冒泡排序 for (int i = 0; i < num.Length - 1; i++) { for ...
- January 13th, 2018 Week 02nd Saturday
Anyone who has no spiritual aspirations is an idiot. 任何没有精神追求的人都是愚昧无知的人. Today I went to a bookshop ...
- iOS-省市区选择的实现
一.简介 该文主要实现的是省市区的选择,可在个人修改地址的地方使用. 二.需要的东西 制作这个首先需要一个area.plist文件,该文件中保存这所有的关于省市区的信息,下载地址:http://pan ...
- Java设计模式之七 ----- 享元模式和代理模式
前言 在上一篇中我们学习了结构型模式的组合模式和过滤器模式.本篇则来学习下结构型模式最后的两个模式, 享元模式和代理模式. 享元模式 简介 享元模式主要用于减少创建对象的数量,以减少内存占用和提高性能 ...
- ES5-ES6-ES7_let关键字声明变量
let命令的介绍 let是ECMAScript6中新增的关键字,用于声明变量.它的用法类似于var var a = 3 let b = 4 let变量的声明 let 命令的特点不允许在同一作用域下声明 ...
- Djando 的 cmd命令
1.黑窗口命令 安装Django pip install django==1.11.16 -i https://pypi.doubanio.com/simple/ 创建项目 django-ad ...
- WINS服务器与DNS服务器有什么区别?
1.WINS实现的是IP地址和计算机名称的映射,DNS实现的是IP地址和域名的映射.2.WINS作用的范围是某个内部网络,DNS的范围是整个互联网.简单说明一下:WINS实现的是IP地址和计算机名称的 ...
- Ubuntu 14.04服务器配置 (1) 安装和配置
http://jingyan.baidu.com/article/9c69d48fb9fd7b13c8024e6b.html ssh是一种安全协议,主要用于给远程登录会话数据进行加密,保证数据传输的安 ...
- MATLAB卷积运算(conv、conv2、convn)解释
1 conv(向量卷积运算) 所谓两个向量卷积,说白了就是多项式乘法.比如:p=[1 2 3],q=[1 1]是两个向量,p和q的卷积如下:把p的元素作为一个多项式的系数,多项式按升幂(或降幂)排列, ...
- Python基础(8)——常见模块
模块介绍 time &datetime模块 random os sys shutil json & picle shelve xml处理 yaml处理 configparser has ...