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設計成符合設計原則,例如對齊,使用字體和顏色突出主要內容,易於操作等。