UWP - 開發 Custom Theme

在 WPF 開發 custom theme 可在 XAML 使用 DynamicResource 的機制,動態更換定義好的 Theme ResourceDictionary

這篇介紹在 UWP 要怎麽做到這樣的效果。

首先介紹基本的定義:

  • Theme Resources 依賴系統 theme 設定或是從 App 修改來使用,分成:Light, Dark 與 HighContrast。
  • Theme resources 與 Static resources 的差別:
    • {ThemeResource} markup extension
      1. 當 App 啓動,從 App 調整主題或是從設定調整主題時,利用 {ThemeResource} 的對象都會被觸發更新;
      2. UWP SDK 定義了系統與 App 共用的 themeresources.xaml,檔案路徑: \(Program Files)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\\Generic,各種 theme resources 都可以在裏面找到;
      3. 系統啓動時會載入系統的 ThemeResource 到記憶體(而不是用上述的檔案或是複製一份到 App 裏),因此,App 會自動參考記憶體的内容,不需要額外定義。除非是 App 自定義的則會 merge 進去;
    • {StaticResource} markup extension
      從已定義的 XAML 資源(ResourceDictionary)中,利用 key 取得值來給 XAML 屬性使用; StaticResource 跟 ThemeResource不同,它只有在 App 啓動時被載入 XAML 定義時才會觸發,之後則不會再更新;
  • 自定義 themes resources 需要參考:Guidelines for custom theme resources 的説明:
    1. 定義 Light 與 Dark 是基本的,建議額外定義 HighContrast;雖然可定義一個 Default 的 ResourceDictionary,但建議使用明確的名字會比較好;
    2. 使用 Styles, Setters, Control templates, Property setters 與 Animation 時換成 {ThemeResource} markup extension;
    3. 切記在定義的 ThemeDictionaries 中不要使用 {ThemeResource} markup extension,要改用 {StaticResource} markup extension;
    4. 如果遇到 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",或是其他方式定義:
      • StyleControlTemplate 必須有 TargetType,代表適用的對象,如果沒有給 key,預設會套入所有指定的對象。
      • DataTemplateTargetType 可以套入設定的對象。
      • 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;
        如果在 Application.OnLaunched 時加入 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>
      同樣地,載入檔案的順序也影響使用 resources 的順序,相同的 key 最後被載入的 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 extensionThemeResource 載入設定的内容。

藉由下面範例,介紹動態切換 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。

[補充]

======

當然還有別的做法,例如把描述寫成 CSS 再程式裏載入,搭配 Convert 做刷新;或是自定義 DependencyObject 來實作。

以上是介紹如何自訂主題,並提供讓用戶選擇或是跟隨系統的顔色/主題來切換。謝謝。

References