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. MySQL优化--INSERT ON DUPLICATE UPDATE死锁

    INSERT ON DUPLICATE UPDATE与死锁 在MySQL中提供两种插入更新的方式:REPLACE INTO和INSERT ON DUPLICATE UPDATE,简化了“存在则更新,不 ...

  2. Python selenium webdriver设置js操作页面滚动条

    js2 = "window.scrollTo(0,0);" #括号中为坐标 当不知道需要的滚动的坐标大小时: weizhi2 = driver.find_element_by_id ...

  3. JSP中的作用域

    application用于全局变量,可以获取全局的数据.作用范围比session大. JSP常用内置对象总结:out对象:用于客户端输出数据.request对象:用于处理客户端发送的请求的数据信息.r ...

  4. [原创] 详解云计算网络底层技术——虚拟网络设备 tap/tun 原理解析

    本文首发于我的公众号 Linux云计算网络(id: cloud_dev),专注于干货分享,号内有 10T 书籍和视频资源,后台回复「1024」即可领取,欢迎大家关注,二维码文末可以扫. 在云计算时代, ...

  5. 签名时出错: 未在路径 C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\bin

    在运行winform程序时,由于清理解决方案等缘故,出现了下面的情况 解决办法:项目-属性-签名-取消勾选“为ClickOne清单签名” 问题完美解决

  6. Api 文档管理系统 RAP2 环境搭建

    Api 文档管理系统 RAP2 环境搭建  发表于 2018-03-27 |  分类于 Api |  评论数: 4|  阅读次数: 4704  本文字数: 4.8k |  阅读时长 ≍ 9 分钟 RA ...

  7. Github page搭建博客使用自定义插件的方法

    Github page的后台程序是由Jekyll搭建,但由于Github的保护措施,非认证的plugin一般不会被Github page支持,但可以使用一些小技巧来绕过屏蔽. 以个人page为例,个人 ...

  8. linux定时任务执行没结果,手动执行有结果问题总结

    今天写了个脚本手动执行有结果,但是放到系统定时任务跑却没结果,之前也遇到这种问题解决了没记录后面又懵逼了一次~~~ 如下图: 手动执行有结果 放到定时任务中每五分钟执行一次 解决方法: 脚本中加载系统 ...

  9. [Jenkins][git]构建时提示Caused by: hudson.plugins.git.GitException: Command "/usr/bin/git reset --hard" returned status code 128:

    --------------------- 如需转载,转载请注明出处. --------------------- 今日发现所有IOS构建相关的job全部失败,并提示如下错误: ERROR: Erro ...

  10. ES6 系列之我们来聊聊装饰器

    Decorator 装饰器主要用于: 装饰类 装饰方法或属性 装饰类 @annotation class MyClass { } function annotation(target) { targe ...