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