欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

WPF实现主题更换的简单DEMO

程序员文章站 2022-06-10 13:38:31
WPF实现主题更换的简单DEMO 实现主题更换功能主要是三个知识点: 1. 动态资源 ( DynamicResource ) 2. INotifyPropertyChanged 接口 3. 界面元素与数据模型的绑定 ( MVVM 中的 ViewModel ) Demo 代码地址: "" 下面开门见山 ......

wpf实现主题更换的简单demo

实现主题更换功能主要是三个知识点:

  1. 动态资源 ( dynamicresource )
  2. inotifypropertychanged 接口
  3. 界面元素与数据模型的绑定 (mvvm中的viewmodel)

demo 代码地址:

下面开门见山,直奔主题

一、准备主题资源

在项目 (怎么建项目就不说了,百度上多得是) 下面新建一个文件夹 **themes**,主题资源都放在这里面,这里我就简单实现了两个主题 **light /dark**,主题只包含背景颜色一个属性。

1. themes

  1. 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>
  1. 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里面,这样方便管理和搜索。
  1. 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();
            }
        }
    }
}
**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** 控件绑定了三个属性 :
  1. itemssource="{binding themes }" -> settingsviewmodel.themes
  2. selecteditem="{binding selectedtheme , mode=twoway}" -> settingsviewmodel.selectedtheme
  3. 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;
        }
    }
}

四、总结

关键点:

  1. 绑定 combobox(view层)itemssourceselecteditem 两个属性到 settingsviewmodel (viewmodel层)
  2. comboboxselecteditem 被更改后,会触发 thememanager 替换当前正在使用的主题资源(themesource属性)
  3. 视图模型需要实现 inotifypropertychanged 接口来通知 wpf 框架属性被更改
  4. 使用 dynamicresource 引用 会改变的 资源,实现主题更换。

另外写的比较啰嗦,主要是给自己回过头来复习看的。。。这年头wpf也没什么市场了,估计也没什么人看吧 o(╥﹏╥)o