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();
}

这个函数使用 LINQApp.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>

到这里整个主题更换的流程就很明朗了。

三 、总结

总结一下主题更换的简要流程:

  1. ComboBox 绑定 SettingsAppearanceViewModel 类中的 ThemesSelectedTheme 两个字段。
  2. SettingsAppearanceViewModel.SelectedThemeset 的时候更改 AppearanceManager.Current.ThemeSource的值。
  3. AppearanceManager.Current.ThemeSource 被更改时进行主题资源的置换以及其他一系列操作,最后触发 AppearanceManager.Current.PropertyChanged 事件
  4. ModernWindow 中绑定到 AppearanceManager.Current.PropertyChanged 事件的函数 OnAppearanceManagerPropertyChanged 被触发,打开 backgroundAnimation 动画。

四、下一篇参考本流程来自己实现一个简单的主题更换功能

WPF Modern UI 主题更换原理的更多相关文章

  1. (转)基于 WPF + Modern UI 的 公司OA小助手 开发总结

    原文地址:http://www.cnblogs.com/rainlam163/p/3365181.html 前言: 距离上一篇博客,整整一个月的时间了.人不能懒下来,必须有个阶段性的总结,算是对我这个 ...

  2. 基于 WPF + Modern UI 的 公司OA小助手 开发总结

    前言: 距离上一篇博客,整整一个月的时间了.人不能懒下来,必须有个阶段性的总结,算是对我这个阶段的一个反思.人只有在总结的过程中才会发现自己的不足. 公司每天都要在OA系统上上班点击签到,下班点击签退 ...

  3. WPF实现主题更换的简单DEMO

    WPF实现主题更换的简单DEMO 实现主题更换功能主要是三个知识点: 动态资源 ( DynamicResource ) INotifyPropertyChanged 接口 界面元素与数据模型的绑定 ( ...

  4. WPF动态改变主题颜色

    原文:WPF动态改变主题颜色 国内的WPF技术先行者周银辉曾介绍过如何动态改变应用程序的主题样式,今天我们来介绍一种轻量级的改变界面风格的方式--动态改变主题色. 程序允许用户根据自己的喜好来对界面进 ...

  5. WPF的UI虚拟化

    许多时候,我们的界面上会呈现大量的数据,如包含数千条记录的表格或包含数百张照片的相册.由于呈现UI是一件开销比较大的动作,一次性呈现数百张照片就目前的电脑性能来说是需要占用大量内存和时间的.因此需要对 ...

  6. WPF相关UI库

    免费控件库: 1.Extended WPF Toolkit 官方拓展控件 http://wpftoolkit.codeplex.com/ 2.avalondock 可停靠布局(wpf toolkit包 ...

  7. 使用AsyncTask异步更新UI界面及原理分析

    概述: AsyncTask是在Android SDK 1.5之后推出的一个方便编写后台线程与UI线程交互的辅助类.AsyncTask的内部实现是一个线程池,所有提交的异步任务都会在这个线程池中的工作线 ...

  8. WPF多线程UI更新——两种方法

    WPF多线程UI更新——两种方法 前言 在WPF中,在使用多线程在后台进行计算限制的异步操作的时候,如果在后台线程中对UI进行了修改,则会出现一个错误:(调用线程无法访问此对象,因为另一个线程拥有该对 ...

  9. 2D UI和3D UI的工作原理

    2D UI的工作原理 UI控件的位置在UI Root 的红框(视窗)上,也就是UI控件的z轴,相机的z轴,UI Root的z轴都是0,因为2D UI都是纯粹的2D图片按层次显示,不会不出现三维立体效果 ...

随机推荐

  1. BATJ等公司必问的8道Java经典面试题,你都会了吗?

    1.谈谈你对 Java 平台的理解?“Java 是解释执行”,这句话正确吗? 考点分析: 对于这类笼统的问题,你需要尽量表现出自己的思维深入并系统化,Java 知识理解得也比较全面,一定要避免让面试官 ...

  2. Oracle创建表空间、用户管理、角色管理

    内容:Oracle创建表空间.用户管理.角色管理 1.用系统用户登录Oracle 默认的系统用户: sys/system.sysman.scott sys:权限最大,超级用户,可以完成所有任务, 默认 ...

  3. 寒假小软件开发记录06--apk生成

    先在strings.xml中修改了软件名称,再修改软件图标. 在Android模式下,进入Image Asset,进行图标的修改: android studio中,build->generate ...

  4. 设置python环境变量

    原始环境变量 /> python Python |Anaconda (-bit)| (default, May , ::) [GCC (Red Hat -)] on linux Type &qu ...

  5. vsCode 设置vue 保存自动格式化代码

    setting { // vscode默认启用了根据文件类型自动设置tabsize的选项 "editor.detectIndentation": false, // 重新设定tab ...

  6. ionic hidden scroll bar

    ionic 项目中隐藏所有的ion-content滚动条 在variables.scss文件中添加以下代码 ::-webkit-scrollbar { display: none; }  

  7. while true 死循环判断端口按顺序启动应用

    需求:spring微服务应用启动较慢并且要求一个应用启完才能启第二个应用. 思路:加了个while true 死循环判断端口启动了才启下一个应用. 执行方式:/appupgrade/spring_cl ...

  8. centos 7 mariadb安装

    centos 7 mariadb安装 1.安装MariaDB 安装命令 yum -y install mariadb mariadb-server 安装完成MariaDB,首先启动MariaDB sy ...

  9. xcodebuild构建时报错unknown error -1=ffffffffffffffff Command /bin/sh failed with exit code 1

    CI今日构建时报出如下错误: /Users/xxx/Library/Developer/Xcode/DerivedData/Snowball-ebllohyukujrncbaldsfojfjxwep/ ...

  10. DotNetCore深入了解之一Startup类

    一个典型的ASP.NET Core应用程序会包含Program与Startup两个文件.Program类中有应用程序的入口方法Main,其中的处理逻辑通常是创建一个WebHostBuilder,再生成 ...