WPF实现主题更换的简单DEMO
程序员文章站
2022-06-10 13:38:31
WPF实现主题更换的简单DEMO 实现主题更换功能主要是三个知识点: 1. 动态资源 ( DynamicResource ) 2. INotifyPropertyChanged 接口 3. 界面元素与数据模型的绑定 ( MVVM 中的 ViewModel ) Demo 代码地址: "" 下面开门见山 ......
wpf实现主题更换的简单demo
实现主题更换功能主要是三个知识点:
- 动态资源 ( dynamicresource )
- inotifypropertychanged 接口
- 界面元素与数据模型的绑定 (mvvm中的viewmodel)
下面开门见山,直奔主题
一、准备主题资源
在项目 (怎么建项目就不说了,百度上多得是) 下面新建一个文件夹 **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
上一篇: ReentrantLock详解
下一篇: DDD峰会归来话DDD
推荐阅读
-
Cocos2d游戏开发学习记录——1.Surface、SurfaceView、SurfaceHolder实现简单的游戏demo
-
使用.Net Core + Vue + IdentityServer4 + Ocelot 实现一个简单的DEMO +源码
-
WPF实现简单的跑马灯效果
-
PHP实现简单的协程任务调度demo示例
-
vue-cli下的vuex的简单Demo图解(实现加1减1操作)
-
Android开发实现的简单计算器功能【附完整demo源码下载】
-
JS实现简单的选择题测评系统代码思路详解(demo)
-
超简单的scrapy实现ip动态代理与更换ip的方法实现
-
使用.Net Core + Vue + IdentityServer4 + Ocelot 实现一个简单的DEMO +源码
-
WPF简单的分页控件实现