• [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设计成符合设计原则,例如对齐,使用字体和颜色突出主要内容,易于操作等。

  • 相关阅读:
    POJ 3667 & HDU 3308 & HDU 3397 线段树的区间合并
    HDU 5992 kd-tree
    BZOJ 4154 kd-tree dfs序 + 二维空间的区间(矩阵)更新单点查找
    BZOJ 4066 kd-tree 矩形询问求和
    BZOJ 1941 kd-tree
    BZOJ 2648 kd-tree模板
    HDU 5925 离散化
    php trait
    php命名空间
    js
  • 原文地址:https://www.cnblogs.com/dino623/p/TemplatedControlUI.html
Copyright © 2020-2023  润新知