WPF Modern UI 主题更换原理
一 . 如何更换主题?
二 . 代码分析
代码路径 : FirstFloor.ModernUI.App / Content / SettingsAppearance.xaml
1.关键 XAML 代码
<ComboBox Grid.Row="1" Grid.Column="1" ItemsSource="{Binding Themes}" SelectedItem="{Binding SelectedTheme, Mode=TwoWay}" DisplayMemberPath="DisplayName" VerticalAlignment="Center" Margin="0,0,0,4" />
形如 Property = "{ Binding fieldname }" 这种格式的绑定,都是将控件属性绑定到当前 用户控件 DataContext 属性的对象中的。
那我们再打开 SettingsAppearance.cs 文件看一看后台代码:
public partial class SettingsAppearance : UserControl
{
public SettingsAppearance()
{
InitializeComponent();
// a simple view model for appearance configuration
this.DataContext = new SettingsAppearanceViewModel();
}
}
很显然,控件的 DataContext 属性引用到了 new SettingsAppearanceViewModel();
那再F12进去分析一下 SettingsAppearanceViewModel 这个类有哪些相关的东西:
2. Themes
首先看一下 Themes 这个属性 ,它是一个 LinkCollection 对象,并返回了 themes 字段:
public LinkCollection Themes
{
get { return this.themes; }
}
LinkCollection 继承自 ObservableCollection<Link> ,百度一下 ObservableCollection这个类,就知道它其实也是用来实现数据绑定的,当集合内的元素发生变化时,集合就会通知外部调用者,此处不多赘述。
此外,SettingsAppearance 类的构造函数中向 themes 添加了一些主题,这里就不贴代码了。
3. SelectedTheme
public Link SelectedTheme
{
get { return this.selectedTheme; }
set
{
if (this.selectedTheme != value) {
this.selectedTheme = value;
OnPropertyChanged("SelectedTheme");
// and update the actual theme
AppearanceManager.Current.ThemeSource = value.Source;
}
}
}
当 SelectedTheme 更改时,set 方法会更改当前的主题源。
对 ThemeSource 这个属性一直F12下去,会找到一个方法
SetThemeSource:
private void SetThemeSource(Uri source, bool useThemeAccentColor)
{
if (source == null) {
throw new ArgumentNullException("source");
}
var oldThemeDict = GetThemeDictionary();
var dictionaries = Application.Current.Resources.MergedDictionaries;
var themeDict = new ResourceDictionary { Source = source };
// if theme defines an accent color, use it
var accentColor = themeDict[KeyAccentColor] as Color?;
if (accentColor.HasValue) {
// remove from the theme dictionary and apply globally if useThemeAccentColor is true
themeDict.Remove(KeyAccentColor);
if (useThemeAccentColor) {
ApplyAccentColor(accentColor.Value);
}
}
// add new before removing old theme to avoid dynamicresource not found warnings
dictionaries.Add(themeDict);
// remove old theme
if (oldThemeDict != null) {
dictionaries.Remove(oldThemeDict);
}
OnPropertyChanged("ThemeSource");
}
看一下第一个函数 GetThemeDictionary:
private ResourceDictionary GetThemeDictionary()
{
// determine the current theme by looking at the app resources and return the first dictionary having the resource key ‘WindowBackground‘ defined.
return (from dict in Application.Current.Resources.MergedDictionaries
where dict.Contains("WindowBackground")
select dict).FirstOrDefault();
}
这个函数使用 LINQ 从 App.xaml 定义的 MergedDictionaries 中搜索主题资源
看一下 App.xaml 里面的代码:
<Application x:Class="FirstFloor.ModernUI.App.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/FirstFloor.ModernUI;component/Assets/ModernUI.xaml" />
<ResourceDictionary Source="/FirstFloor.ModernUI;component/Assets/ModernUI.Light.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
这里面预定义了两个资源,去相应的地方找到这两个资源文件,其中 ModernUI.Light.xaml 内有大量包含 "WindowBackground" 字符串的 Key ,那这个显然就是主题资源文件了。
接下来的逻辑就比较好理解了:调整 AccentColor ,加入新的主题,移除旧的主题,最后通知属性更改。
4. 动画
那主题更改的渐变动画效果是在哪里触发的呢?
看一下 MainWindon 的基类 MorderWindow , 这里面监听了 AppearanceManager.Current.PropertyChanged 事件:
/// <summary>
/// Initializes a new instance of the <see cref="ModernWindow"/> class.
/// </summary>
public ModernWindow()
{
// 其他代码
...
// listen for theme changes
AppearanceManager.Current.PropertyChanged += OnAppearanceManagerPropertyChanged;
}
...
private void OnAppearanceManagerPropertyChanged(object sender, PropertyChangedEventArgs e)
{
// start background animation if theme has changed
if (e.PropertyName == "ThemeSource" && this.backgroundAnimation != null) {
this.backgroundAnimation.Begin();
}
}
再找一下 backgroundAnimation 赋值的地方
/// <summary>
/// When overridden in a derived class, is invoked whenever application code or internal processes call System.Windows.FrameworkElement.ApplyTemplate().
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
// retrieve BackgroundAnimation storyboard
var border = GetTemplateChild("WindowBorder") as Border;
if (border != null) {
this.backgroundAnimation = border.Resources["BackgroundAnimation"] as Storyboard;
if (this.backgroundAnimation != null) {
this.backgroundAnimation.Begin();
}
}
}
backgroundAnimation 其实是从资源文件中加载的,找到 ModernWindow.xaml 文件,相关代码:
<Border.Resources>
<Storyboard x:Key="BackgroundAnimation">
<ColorAnimation Storyboard.TargetName="WindowBorderBackground" Storyboard.TargetProperty="Color" To="{DynamicResource WindowBackgroundColor}" Duration="0:0:.6" />
</Storyboard>
</Border.Resources>
到这里整个主题更换的流程就很明朗了。
三 、总结
总结一下主题更换的简要流程:
- ComboBox 绑定 SettingsAppearanceViewModel 类中的 Themes 和 SelectedTheme 两个字段。
- SettingsAppearanceViewModel.SelectedTheme在 set 的时候更改 AppearanceManager.Current.ThemeSource的值。
- AppearanceManager.Current.ThemeSource 被更改时进行主题资源的置换以及其他一系列操作,最后触发 AppearanceManager.Current.PropertyChanged 事件
- ModernWindow 中绑定到 AppearanceManager.Current.PropertyChanged 事件的函数 OnAppearanceManagerPropertyChanged 被触发,打开 backgroundAnimation 动画。
四、下一篇参考本流程来自己实现一个简单的主题更换功能
原文地址:https://www.cnblogs.com/ArthurRen/p/WPF_ModernUI.html