[UWP]了解模板化控件(9):UI指南
2017-05-06 11:39
447 查看
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]了解模板化控件(8):ItemsControl
- [UWP 自定义控件]了解模板化控件(2):模仿ContentControl
- [UWP]了解模板化控件(1):基础知识
- [UWP]了解模板化控件(6):使用附加属性
- [UWP]了解模板化控件(2):模仿ContentControl
- [UWP]了解模板化控件(5.2):UserControl vs. TemplatedControl
- [UWP]了解模板化控件(6):使用附加属性
- [UWP]了解模板化控件(1):基础知识
- [UWP]了解模板化控件(2):模仿ContentControl
- [UWP]了解模板化控件(7):支持Command
- [UWP]了解模板化控件(5.1):TemplatePart vs. VisualState
- [UWP]了解模板化控件(2.1):理解ContentControl
- [UWP]了解模板化控件(8):ItemsControl
- [UWP]了解模板化控件(3):实现HeaderedContentControl
- [UWP]了解模板化控件(5):VisualState
- [UWP]了解模板化控件(5):VisualState
- [UWP]了解模板化控件(7):支持Command
- [UWP]了解模板化控件(3):实现HeaderedContentControl
- [UWP]了解模板化控件(5.1):TemplatePart vs. VisualState