实现主题更换功能主要是三个知识点:
动态资源 (DynamicResource)
INotifyPropertyChanged 接口
界面元素与数据模型的绑定 (MVVM 中的 ViewModel)
Demo 代码地址: https://github.com/ArthurRen/WPF-ModernUI-Example
下面开门见山, 直奔主题
一, 准备主题资源
在项目 (怎么建项目就不说了, 百度上多得是) 下面新建一个文件夹 **Themes**, 主题资源都放在这里面, 这里我就简单实现了两个主题 **Light /Dark**, 主题只包含背景颜色一个属性.
- 1. Themes
- Theme.Dark.xaml
- <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:local="clr-namespace:ModernUI.Example.Theme.Themes">
- <Color x:Key="WindowBackgroundColor">#333</Color>
- </ResourceDictionary>
- Theme.Light.xaml
- <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:local="clr-namespace:ModernUI.Example.Theme.Themes">
- <Color x:Key="WindowBackgroundColor">#ffffff</Color>
- </ResourceDictionary>
然后在程序的 App.xaml 中添加一个默认的主题
不同意义的资源最好分开到单独的文件里面, 最后 Merge 到 App.xaml 里面, 这样方便管理和搜索.
- App.xaml
- <Application x:Class="ModernUI.Example.Theme.App"
- xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:local="clr-namespace:ModernUI.Example.Theme"
- StartupUri="MainWindow.xaml">
- <Application.Resources>
- <ResourceDictionary>
- <ResourceDictionary.MergedDictionaries>
- <ResourceDictionary Source="Themes/Theme.Light.xaml"/>
- </ResourceDictionary.MergedDictionaries>
- </ResourceDictionary>
- </Application.Resources>
- </Application>
二, 实现视图模型 (ViewModel)
界面上我模仿 **ModernUI** , 使用 **ComboBox** 控件来更换主题, 所以这边需要实现一个视图模型用来被 ComboBox 绑定.
新建一个文件夹 Prensentation , 存放所有的数据模型类文件
1. NotifyPropertyChanged 类
**NotifyPropertyChanged** 类实现 **INotifyPropertyChanged** 接口, 是所有视图模型的基类, 主要用于实现数据绑定功能.
- abstract class NotifyPropertyChanged : INotifyPropertyChanged
- {
- public event PropertyChangedEventHandler PropertyChanged;
- protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = "")
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
- }
这里面用到了一个 **[CallerMemberName] Attribute** , 这个是. net 4.5 里面的新特性, 可以实现形参的自动填充, 以后在属性中调用 **OnPropertyChanged** 方法就不用在输入形参了, 这样更利于重构, 不会因为更改属性名称后, 忘记更改 **OnPropertyChanged** 的输入参数而导致出现 BUG. 具体可以参考 **C# in depth (第五版) 16.2 节 ** 的内容
2. Displayable 类
**Displayable** 用来实现界面呈现的数据,**ComboBox Item** 上显示的字符串就是 **DisplayName** 这个属性
- class Displayable : NotifyPropertyChanged
- {
- private string _displayName { get; set; }
- /// <summary>
- /// name to display on ui
- /// </summary>
- public string DisplayName
- {
- get => _displayName;
- set
- {
- if (_displayName != value)
- {
- _displayName = value;
- OnPropertyChanged();
- }
- }
- }
- }
3. Link 类
**Link** 类继承自 **Displayable** , 主要用于保存界面上显示的主题名称 **(DisplayName)**, 以及主题资源的路径 **(Source)**
- class Link : Displayable
- {
- private Uri _source = null;
- /// <summary>
- /// resource uri
- /// </summary>
- public Uri Source
- {
- get => _source;
- set
- {
- _source = value;
- OnPropertyChanged();
- }
- }
- }
4. LinkCollection 类
**LinkCollection** 继承自 **ObservableCollection\<Link\>**, 被 **ComboBox** 的 **ItemsSource** 绑定, 当集合内的元素发生变化时,**ComboBox** 的 **Items** 也会一起变化.
- class LinkCollection : ObservableCollection<Link>
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="LinkCollection"/> class.
- /// </summary>
- public LinkCollection()
- {
- }
- /// <summary>
- /// Initializes a new instance of the <see cref="LinkCollection"/> class that contains specified links.
- /// </summary>
- /// <param name="links">The links that are copied to this collection.</param>
- public LinkCollection(IEnumerable<Link> links)
- {
- if (links == null)
- {
- throw new ArgumentNullException("links");
- }
- foreach (var link in links)
- {
- Add(link);
- }
- }
- }
5.ThemeManager 类
**ThemeManager** 类用于管理当前正在使用的主题资源, 使用单例模式 **(Singleton)** 实现.
- class ThemeManager : NotifyPropertyChanged
- {
- #region singletion
- private static ThemeManager _current = null;
- private static readonly object _lock = new object();
- public static ThemeManager Current
- {
- get
- {
- if (_current == null)
- {
- lock (_lock)
- {
- if (_current == null)
- {
- _current = new ThemeManager();
- }
- }
- }
- return _current;
- }
- }
- #endregion
- /// <summary>
- /// get current theme resource dictionary
- /// </summary>
- /// <returns></returns>
- private ResourceDictionary GetThemeResourceDictionary()
- {
- return (from dictionary in Application.Current.Resources.MergedDictionaries
- where dictionary.Contains("WindowBackgroundColor")
- select dictionary).FirstOrDefault();
- }
- /// <summary>
- /// get source uri of current theme resource
- /// </summary>
- /// <returns>resource uri</returns>
- private Uri GetThemeSource()
- {
- var theme = GetThemeResourceDictionary();
- if (theme == null)
- return null;
- return theme.Source;
- }
- /// <summary>
- /// set the current theme source
- /// </summary>
- /// <param name="source"></param>
- public void SetThemeSource(Uri source)
- {
- var oldTheme = GetThemeResourceDictionary();
- var dictionaries = Application.Current.Resources.MergedDictionaries;
- dictionaries.Add(new ResourceDictionary
- {
- Source = source
- });
- if (oldTheme != null)
- {
- dictionaries.Remove(oldTheme);
- }
- }
- /// <summary>
- /// current theme source
- /// </summary>
- public Uri ThemeSource
- {
- get => GetThemeSource();
- set
- {
- if (value != null)
- {
- SetThemeSource(value);
- OnPropertyChanged();
- }
- }
- }
- }
6. SettingsViewModel 类
**SettingsViewModel** 类用于绑定到 **ComboBox** 的 **DataContext** 属性, 构造器中会初始化 **Themes** 属性, 并将我们预先定义的主题资源添加进去.
- ComboBox.SelectedItem -> SettingsViewModel.SelectedTheme
- ComboBox.ItemsSource -> SettingsViewModel.Themes
- class SettingsViewModel : NotifyPropertyChanged
- {
- public LinkCollection Themes { get; private set; }
- private Link _selectedTheme = null;
- public Link SelectedTheme
- {
- get => _selectedTheme;
- set
- {
- if (value == null)
- return;
- if (_selectedTheme != value)
- _selectedTheme = value;
- ThemeManager.Current.ThemeSource = value.Source;
- OnPropertyChanged();
- }
- }
- public SettingsViewModel()
- {
- Themes = new LinkCollection()
- {
- new Link { DisplayName = "Light", Source = new Uri(@"Themes/Theme.Light.xaml" , UriKind.Relative) } ,
- new Link { DisplayName = "Dark", Source = new Uri(@"Themes/Theme.Dark.xaml" , UriKind.Relative) }
- };
- SelectedTheme = Themes.FirstOrDefault(dcts => dcts.Source.Equals(ThemeManager.Current.ThemeSource));
- }
- }
三, 实现视图 (View)
1.MainWindwo.xaml
主窗口使用 **Border** 控件来控制背景颜色,**Border** 的 **Background.Color ** 指向到动态资源 **WindowBackgroundColor** , 这个 **WindowBackgroundColor ** 就是我们在主题资源中定义好的 Color 的 Key, 因为需要动态更换主题, 所以需要用 **DynamicResource** 实现.
**Border** 背景动画比较简单, 就是更改 **SolidColorBrush** 的 **Color** 属性.
**ComboBox** 控件绑定了三个属性 :
- ItemsSource="{Binding Themes }" -> SettingsViewModel.Themes
- SelectedItem="{Binding SelectedTheme , Mode=TwoWay}" -> SettingsViewModel.SelectedTheme
- DisplayMemberPath="DisplayName" -> SettingsViewModel.SelectedTheme.DisplayName
- <Window ...>
- <Grid>
- <Border x:Name="Border">
- <Border.Background>
- <SolidColorBrush x:Name="WindowBackground" Color="{DynamicResource WindowBackgroundColor}"/>
- </Border.Background>
- <Border.Resources>
- <Storyboard x:Key="BorderBackcolorAnimation">
- <ColorAnimation
- Storyboard.TargetName="WindowBackground" Storyboard.TargetProperty="Color"
- To="{DynamicResource WindowBackgroundColor}"
- Duration="0:0:0.5" AutoReverse="False">
- </ColorAnimation>
- </Storyboard>
- </Border.Resources>
- <ComboBox x:Name="ThemeComboBox"
- VerticalAlignment="Top" HorizontalAlignment="Left" Margin="30,10,0,0" Width="150"
- DisplayMemberPath="DisplayName"
- ItemsSource="{Binding Themes }"
- SelectedItem="{Binding SelectedTheme , Mode=TwoWay}">
- </ComboBox>
- </Border>
- </Grid>
- </Window>
- 2. MainWindow.cs
后台代码将 **ComboBox.DataContext** 引用到 **SettingsViewModel** , 实现数据绑定, 同时监听 **ThemeManager.Current.PropertyChanged** 事件, 触发背景动画
- public partial class MainWindow : Window
- {
- private Storyboard _backcolorStopyboard = null;
- public MainWindow()
- {
- InitializeComponent();
- ThemeComboBox.DataContext = new Presentation.SettingsViewModel();
- Presentation.ThemeManager.Current.PropertyChanged += AppearanceManager_PropertyChanged;
- }
- private void AppearanceManager_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
- {
- if (_backcolorStopyboard != null)
- {
- _backcolorStopyboard.Begin();
- }
- }
- public override void OnApplyTemplate()
- {
- base.OnApplyTemplate();
- if (Border != null)
- {
- _backcolorStopyboard = Border.Resources["BorderBackcolorAnimation"] as Storyboard;
- }
- }
- }
四, 总结
关键点:
绑定 ComboBox(View 层) 的 ItemsSource 和 SelectedItem 两个属性到 SettingsViewModel (ViewModel 层)
ComboBox 的 SelectedItem 被更改后, 会触发 ThemeManager 替换当前正在使用的主题资源 (ThemeSource 属性)
视图模型需要实现 INotifyPropertyChanged 接口来通知 WPF 框架属性被更改
使用 DynamicResource 引用 会改变的 资源, 实现主题更换.
另外写的比较啰嗦, 主要是给自己回过头来复习看的... 这年头 WPF 也没什么市场了, 估计也没什么人看吧 o(﹏)o
来源: https://www.cnblogs.com/ArthurRen/p/9537645.html