1. 為什么選擇Aero2
除了以外觀為賣點的控件庫,WPF的控件庫都默認使用“素顏”的外觀,然后再提供一些主題包。這樣做的最大好處是可以和原生控件或其它控件庫兼容,而且對於大部分人來說模仿原生的主題也比自己設計一套好看的UI容易得多。
WPF有以下幾種原生主題:
主題文件 | 桌面主題 |
---|---|
Classic.xaml | Windows XP 操作系統上的經典 Windows 外觀(Windows 95、Windows 98 和 Windows 2000)。 |
Luna.NormalColor.xaml | Windows XP 上的默認藍色主題。 |
Luna.Homestead.xaml | Windows XP 上的橄欖色主題。 |
Luna.Metallic.xaml | Windows XP 上的銀色主題。 |
Royale.NormalColor.xaml | Windows XP Media Center Edition 操作系統上的默認主題。 |
Aero.NormalColor.xaml | Windows Vista 操作系統上的默認主題。 |
Win8之后WPF更新了Aero2和AeroLite兩種主題,關於Aero、Aero2、AeroLite具體可見這個網頁。再之后微軟就沒有更新WPF主題了。
如果不在代碼中指定主題,WPF大概就是用這段代碼確定主題,也就是說默認是Aero,如果在Win8或以上自動轉為Aero2:
_themeName = themeName.ToString();
_themeName = Path.GetFileNameWithoutExtension(_themeName);
if(String.Compare(_themeName, "aero", StringComparison.OrdinalIgnoreCase) == 0 && Utilities.IsOSWindows8OrNewer)
{
_themeName = "Aero2";
}
由於我暫時不想兼容Win7,而且我又不討厭Win8的風格,所以Kino.Toolkit.Wpf直接選擇了Aero2作為控件庫的主題。
2. Aero2的設計
上面分別是Aero2(左)和Aero(右)的Button在幾種狀態下的外觀,從中可以看出Aero2的設計是扁平化的風格,移除圓角、漸變等裝飾性元素,以實用為目的。這樣一來控件模板的結構更加簡單(如Button只有Border和ContentPresenter 兩個元素),移除裝飾性元素更節省空間,而且漸變在質量較差或陽光下很影響閱讀,圓角則是占用更多空間而且在低分辨率下表現不好。
總的來說就是以實用為目的,盡量簡單,減少裝飾性元素。
3. 以Button為例,談談Aero2中的細節:尺寸、顏色、字體、動畫
<Style x:Key="FocusVisual">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle Margin="2" SnapsToDevicePixels="true" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" StrokeThickness="1" StrokeDashArray="1 2"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<SolidColorBrush x:Key="Button.Static.Background" Color="#FFDDDDDD"/>
<SolidColorBrush x:Key="Button.Static.Border" Color="#FF707070"/>
<SolidColorBrush x:Key="Button.MouseOver.Background" Color="#FFBEE6FD"/>
<SolidColorBrush x:Key="Button.MouseOver.Border" Color="#FF3C7FB1"/>
<SolidColorBrush x:Key="Button.Pressed.Background" Color="#FFC4E5F6"/>
<SolidColorBrush x:Key="Button.Pressed.Border" Color="#FF2C628B"/>
<SolidColorBrush x:Key="Button.Disabled.Background" Color="#FFF4F4F4"/>
<SolidColorBrush x:Key="Button.Disabled.Border" Color="#FFADB2B5"/>
<SolidColorBrush x:Key="Button.Disabled.Foreground" Color="#FF838383"/>
<Style TargetType="{x:Type Button}">
<Setter Property="FocusVisualStyle" Value="{StaticResource FocusVisual}"/>
<Setter Property="Background" Value="{StaticResource Button.Static.Background}"/>
<Setter Property="BorderBrush" Value="{StaticResource Button.Static.Border}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Padding" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="true">
<ContentPresenter x:Name="contentPresenter" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsDefaulted" Value="true">
<Setter Property="BorderBrush" TargetName="border" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Background" TargetName="border" Value="{StaticResource Button.MouseOver.Background}"/>
<Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.MouseOver.Border}"/>
</Trigger>
<Trigger Property="IsPressed" Value="true">
<Setter Property="Background" TargetName="border" Value="{StaticResource Button.Pressed.Background}"/>
<Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.Pressed.Border}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Background" TargetName="border" Value="{StaticResource Button.Disabled.Background}"/>
<Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.Disabled.Border}"/>
<Setter Property="TextElement.Foreground" TargetName="contentPresenter" Value="{StaticResource Button.Disabled.Foreground}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
這是Aero2使用Blend獲取的Button控件模板。因為Button是最基礎最常用最具代表性的控件,所以以它為例談談Aero2主題中的各種細節。
3.1 尺寸
首先考慮下控件是否有必要有統一的尺寸。
我記得很久很久以前微軟有份文檔要求桌面按鈕的高度是22像素(有可能是23,已經不記得了)。微軟自己有沒有遵守?真是太看得起微軟了。
就以IE來說,上圖從上到下幾組按鈕的高度分別是21,28,24像素。
這個頁面大部分按鈕都是28,只有中間那個“將所有區域重置為默認級別”是30像素。
可以看出,微軟一直以來開放、包容、擁抱多元化的策略,在IE上可以說是完美體現。作為對比我看了看Chrome的類似按鈕,統一為32像素,看來有很好地執行Material Design中"所有距離,尺寸都應該是8dp的整數倍"的要求(到處都是8,可以說深得中國人歡心)。
<Rectangle Height="1" Fill="Gray" />
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center">
<Button Content="Button" VerticalAlignment="Center" />
<TextBox Text="TextBox" VerticalAlignment="Center" />
<PasswordBox Password="password" VerticalAlignment="Center" />
<ComboBox VerticalAlignment="Center">
<ComboBoxItem Content="ComboBox" IsSelected="True"/>
</ComboBox>
<DatePicker VerticalAlignment="Center"/>
</StackPanel>
<Rectangle Height="1" Fill="Gray" />
順便拿Button與WPF的其它控件、及UWP的相同控件做橫向對比,使用相同的XAML產生的UI如上圖所示(上為UWP,下為WPF)。可以看出UWP的表單元素基本上完全統一高度,而WPF則根據內容自適應。
總結來說,WPF原生控件通常沒有設置具體的尺寸,所以模仿Aero2主題的自定義控件也不應該改變這個行為,只需控件要能夠清晰展示數據及容易操作就好(也就是符合基本的UI設計原則)。
我建議在實際項目中根據需要使用樣式將按鈕的高度統一為24、28、32像素(The sizes, margins, and positions of UI elements should always be in multiples of 4 epx in your UWP apps.,因為Windows系統的縮放比例總是5/4(125%)、6/4(150%)、7/4(175%)、8/4(200%),所以尺寸最好是4的倍數,真不吉利)。
3.2 顏色
從Button的控件模板可以看到Button的字體顏色使用了{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}
。WPF為系統環境封裝了三個類,用於訪問系統環境設置:
- SystemFonts,包含公開有關字體的系統資源的屬性。
- SystemColors,包含與系統顯示元素相對應的系統顏色、系統畫筆和系統資源鍵。
- SystemParameters,包含可用來查詢系統設置的屬性。
使用方式可以參考資源幫助主題。
這些設置只應用作參考,可以看到Button也只是主要使用了ControlTextBrushKey,Aero2主題有自己的顏色風格,不會跟隨系統而改變。
再次橫向比較一下,這次試用Disabled狀態作比較,可以看到每個控件的邊框無論在Enabled或Disabled的狀態下邊框顏色都不一樣(除了TextBox和PasswordBox,他們關系好)。
因為看不到Aero2在顏色上有什么要求,我的建議是,如果自定義的控件長得像TextBox就使用TextBox的顏色設置,長得像Button的就用Button,總之盡量模仿原生控件,顏色也盡量使用藍色或灰色就可以了。
3.3 字體
只有Menu、StatusBar、Toolbar等有限幾個控件會使用SystemFonts的值,其它都可以使用繼承值。這樣可以方便地通過在根元素設置字體來統一字體的使用。
3.4 動畫
幾乎、完全、沒有。也許是為了兼顧Windows的UI,或者照顧低端配置的電腦,Aero2里真的幾乎完全看不到動畫效果,一眼看過去所有Storyboard的Duration都是0。也好,以和Aero2統一風格作借口我也可以不做動畫啦。
最近我發現lindexi這樣介紹我:
其實我也並不是那么喜歡親自寫動畫,只是WPF和UWP里連最基本的都沒提供所以我才在這方面鼓起干勁努力了一把。
4. 提供VisualState
<ControlTemplate TargetType="local:KinoButton">
<Border x:Name="border"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
SnapsToDevicePixels="true">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="MouseOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Panel.Background)"
Storyboard.TargetName="border">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource Button.MouseOver.Background}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush)"
Storyboard.TargetName="border">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource Button.MouseOver.Border}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush)"
Storyboard.TargetName="border">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource Button.Pressed.Border}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Panel.Background)"
Storyboard.TargetName="border">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource Button.Pressed.Background}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(TextElement.Foreground)"
Storyboard.TargetName="contentPresenter">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource Button.Disabled.Foreground}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Panel.Background)"
Storyboard.TargetName="border">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource Button.Disabled.Background}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush)"
Storyboard.TargetName="border">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource Button.Disabled.Border}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="{TemplateBinding Padding}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<!--comecode-->
<ContentPresenter x:Name="contentPresenter"
Grid.Column="1"
Focusable="False"
RecognizesAccessKey="True"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsDefaulted"
Value="true">
<Setter Property="BorderBrush"
TargetName="border"
Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
出於好玩,我把KinoButton(主要是在Button的基礎上添加了Icon的功能)的控件模板從使用Trigger改為盡量使用VisualState,這樣做沒什么實際意義,真的只是好玩而已,而且XAML的行數還增加了不少。
不過在實現其它自定義控件的時候我也比較傾向提供VisualState,因為這樣可以明確指出控件外觀有幾種狀態,避免了混輪,而且提供了VisualState可以更方便擴展。這點WPF原生控件也是一樣的,它們很多都沒有聲明TemplateVisualState,而且ControlTemplate也沒有使用VisualState,但使用Blend編輯控件模板還是可以在“狀態”面板看到它的TemplateVisualState(其中FocusStates和ValidationStates可以不使用,如果修改了這兩組狀態也就是讓控件外觀更個性化而已)。對最終用戶來說多一個選擇並不是壞事。
5. 結語
通過這篇文章讀者應該對Aero2的風格有了一定程度的了解。更多Aero和Aero2的相關信息可以看這個Github項目。
很多控件庫都會提供額外的主題包,這點可以放到后面再考慮。