当前位置: 移动技术网 > IT编程>开发语言>.net > WPF实现主题更换的简单DEMO

WPF实现主题更换的简单DEMO

2018年08月26日  | 移动技术网IT编程  | 我要评论

旅居,first love歌词,牧场之国ppt

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

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网