在 WPF 開發 custom theme 可在 XAML 使用 DynamicResource 的機制,動態更換定義好的 Theme ResourceDictionary。
這篇介紹在 UWP 要怎麽做到這樣的效果。
首先介紹基本的定義:
- Theme Resources 依賴系統 theme 設定或是從 App 修改來使用,分成:Light, Dark 與 HighContrast。
- Theme resources 與 Static resources 的差別:
- {ThemeResource} markup extension
- 當 App 啓動,從 App 調整主題或是從設定調整主題時,利用 {ThemeResource} 的對象都會被觸發更新;
- UWP SDK 定義了系統與 App 共用的 themeresources.xaml,檔案路徑: \(Program Files)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\
\Generic ,各種 theme resources 都可以在裏面找到; - 系統啓動時會載入系統的 ThemeResource 到記憶體(而不是用上述的檔案或是複製一份到 App 裏),因此,App 會自動參考記憶體的内容,不需要額外定義。除非是 App 自定義的則會 merge 進去;
- {StaticResource} markup extension
從已定義的 XAML 資源(ResourceDictionary)中,利用 key 取得值來給 XAML 屬性使用; StaticResource 跟 ThemeResource不同,它只有在 App 啓動時被載入 XAML 定義時才會觸發,之後則不會再更新;
- {ThemeResource} markup extension
- 自定義 themes resources 需要參考:Guidelines for custom theme resources 的説明:
- 定義 Light 與 Dark 是基本的,建議額外定義 HighContrast;雖然可定義一個 Default 的 ResourceDictionary,但建議使用明確的名字會比較好;
- 使用 Styles, Setters, Control templates, Property setters 與 Animation 時換成 {ThemeResource} markup extension;
- 切記在定義的 ThemeDictionaries 中不要使用 {ThemeResource} markup extension,要改用 {StaticResource} markup extension;
- 如果遇到 theme 的 exception 可以參考 Troubleshooting theme resources 排除問題;
- Resource 與 ResourceDictionary
- Resources 被定義在 ResourceDictionary 中,通常是在獨立的檔案或是 Page 的最前面,藉由 key 與 StaticResource markup extension 或 ThemeResource markup extension來取得;
- Resources 可能是 string 或其他可分享的物件,例如:styles, templates, brushes 與 colors;而 controls, shapes 與其他 FrameworkElemets 是不能被分享使用的,詳細的差別可參考 XAML resources must be shareable;範例如下:
<Page> <Page.Resources> <SolidColorBrush x:Key="myFavoriteColor" Color="green"/> <x:String x:Key="greeting">Hello world</x:String> </Page.Resources> <TextBlock Foreground="{StaticResource myFavoriteColor}" Text="{StaticResource greeting}" VerticalAlignment="Top"/> <Button Foreground="{StaticResource myFavoriteColor}" Content="{StaticResource greeting}" VerticalAlignment="Center"/> </Page>
- 所有的 resources 都要有一個 key,例如:x:Key="mystring",或是其他方式定義:
- Style 與 ControlTemplate 必須有 TargetType,代表適用的對象,如果沒有給 key,預設會套入所有指定的對象。
- DataTemplate 有 TargetType 可以套入設定的對象。
- x:Name 常被與 x:Key 比較,差別在 x:Name 會在 code behind 中被建立。因此 x:Name 的效率不容 x:Key 好,因爲每個項目需要在 Page Loaded 時被初始化。
- StaticResource 只能使用字串名稱(x:Name 或 x:Key)檢索資源。但當 control 沒有設定 Style/ContentTemplate/ItemTemplate 時 XAML framework 會查找隱式樣式資源(使用 TargetType)
- 也可以在程式碼中加入應用程式資源,需要注意:
- 定義在 Page 中的只有該 Page 内可以用,如果要共用建議放到 App.xaml 中定義;
- 要在程式完成執行前就把 resources 加入,避免有頁面要用到;
- 無法在 App.xaml.cs 的建構子加入 resources;
- 由於任何 FrameworkElement 都擁有一個 ResourceDictionary,因此,XAML 在處理 Resources 的定義時,以最靠近 Element 的定義為主,例如:Page 的 ResourceDictionary 定義了某個值,在 Grid 也定義了相同 Key,那再 Grid 中的 TextBox 使用的 Key 則是用 Grid 中定義的爲主。如果您是在程式裏面使用,則需注意來源者是誰,例如:(string)this.Resource["greeting"]; 拿到的可能是 control 或是 page 的 resources。
- 合并 resource dictionaries:
利用 ResourceDictionary.MergedDictionaries 將多份 resource dictionary xaml 合并起來,如下:
<Page.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Dictionary1.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Page.Resources>
- Theme resources 與 theme dictionaries:
ThemeResource 與 StaticResource 相似,差別在 Theme 改變時會重新做資源更新的查詢。 Theme dictionaries 是特殊的合并字典,根據目前裝置上使用的 theme 保留不同的資源。 可利用 ResourceDictionary.ThemeDictionaries 將定義好的 xaml resource file 加入,並設定對應的 key,例如:
<Page> <Page.Resources> <ResourceDictionary> <ResourceDictionary.ThemeDictionaries> <ResourceDictionary Source="Dictionary1.xaml" x:Key="Light"/> <ResourceDictionary Source="Dictionary2.xaml" x:Key="Dark"/> </ResourceDictionary.ThemeDictionaries> </ResourceDictionary> </Page.Resources> <TextBlock Foreground="{StaticResource brush}" Text="hello world" VerticalAlignment="Center"/> </Page>
- 如何使用 Theme resources 可以參考 guidelines for using theme resources。themeresources.xaml 定義多種 resources,例如:Style 可用在文字控制項目或其他。
更多詳細可以參考 XAML overview。
整理上述的重點:
參考 ResourceDictionary and XAML resource references 的介紹,自定義的 resources (style, template, container, etc ...) 設定唯一的 key 就可以加入到 ResourceDictionary,使用 {StaticResource} markup extension 或 ThemeResource 載入設定的内容。
藉由下面範例,介紹動態切換 themes 的處理:
利用 ThemeListener 抓取系統 theme 的改變,來調整為自定義的 theme resources:
private readonly ThemeListener themeListener;
public MainPage()
{
themeListener = new ThemeListener();
themeListener.ThemeChanged += ThemeListener_ThemeChanged;
}
private void ThemeListener_ThemeChanged(ThemeListener sender)
{
// 取得用戶變換的 theme 類型: Dark 或 Light
Debug.WriteLine(sender.CurrentThemeName);
}
要注意,App 啓動時 FrameworkElement.RequestTheme 預設使用 Default,而不是我們熟悉的 Dark 或 Light。
因此,定義 Resources 要特別注意,分別為兩個定義,如下:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<!-- 為 3 個模式定義 theme resource -->
<ResourceDictionary x:Key="Default">
<Color x:Key="ButtonBaseBorder">#FFFF0000</Color>
<!-- 要記得使用 ThemeResource extension 來指定對象 -->
<SolidColorBrush x:Key="ButtonColor1" Color="{ThemeResource ButtonBaseBorder}" />
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<Color x:Key="ButtonBaseBorder">#FFEFFF00</Color>
<SolidColorBrush x:Key="ButtonColor1" Color="{ThemeResource ButtonBaseBorder}" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<Color x:Key="ButtonBaseBorder">#FF0091FF</Color>
<SolidColorBrush x:Key="ButtonColor1" Color="{ThemeResource ButtonBaseBorder}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Application.Resources>
利用 {ThemeResource} markup extension 來指定自定義的 key:<Button Content="change theme" Background="{ThemeResource ButtonColor1}" />。
這樣一來就可以支援用戶從設定切換 Dark/Light 時跟著變換不同的顔色。
如果要設定 app 變成特定的 theme,可利用 Application.Current.RequestedTheme = ApplicationTheme.Dark;。
重點元素:
名稱 | 説明 |
FrameworkElemnt.RequestedTheme | 設定或取得該 UIElement 的 UI theme,設定之後會覆寫 app-level 的 RequestedTheme |
Application.Current.RequestedTheme | 由於 Applicaiton 管理 app-scoped 資源與生命周期。 因此,RequestTheme 負責管理 App 的 theme。 它與 ThemeListener 類似。 |
ThemeListener | 可設定目前 Application theme,並監控 system theme 被改變時會發出事件。 可安裝 Microsoft.Toolkit.Uwp.UI.Helpers 來使用它。 |
ElementTheme | 代表特定 UIElement 的 theme。與 ApplicationTheme 的層級不一樣,只會影響該 UIElement。 |
ApplicationTheme | 代表 app 的 theme。值由:Dark 與 Light。 |
[補充]
- 參考 System brushes 知道那些 resouces 會跟著 theme 改變時調整
- ResourceDictionary
- DependencyObject
- ContentTemplate
- VisualStateManager.VisualStateGroups
- DataTemplate
======
當然還有別的做法,例如把描述寫成 CSS 再程式裏載入,搭配 Convert 做刷新;或是自定義 DependencyObject 來實作。
以上是介紹如何自訂主題,並提供讓用戶選擇或是跟隨系統的顔色/主題來切換。謝謝。
References:
- ResourceDictionary and XAML resource references
- XAML theme resources
- Control templates
- How to: Use an Application-Scope Resource Dictionary
- XAML overview
- Tutorial: Create custom styles
- UWP 029 | XAML Themes
- {TemplateBinding} markup extension
- {StaticResource} markup extension
- {RelativeSource} markup extension
- {CustomResource} markup extension
- {ThemeResource} markup extension
- UWP how to use custom Theme
- Using a Dynamic System Accent Color in UWP