[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>

38937-20170319165247166-684838189.png

在上面這段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"/>

38937-20170506101752961-1539195313.png

其中 FocusVisualPrimary指外邊框,FocusVisualSecondary指內邊框。
38937-20170506101340726-602581627.png

使用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>

38937-20170506101814523-4673821.png

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>

38937-20170506101425539-1185655087.png

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”,甚至同一個程序中同時存在兩種狀況。

38937-20170319165558323-467976505.png

例如這個對話框,一不小心就點擊左邊的“取消”按鈕了。

9. 符合典型的GUI設計原則

在控件層級就應該將UI設計成符合設計原則,例如對齊,使用字體和顏色突出主要內容,易於操作等。
38937-20170506101158617-815378178.png


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章