在WPF应用程序中,主题的更换通常需要重新编译应用程序。但是,通过采用一种可插拔的架构,可以在不重新编译应用程序的情况下更换或添加样式。本文将介绍如何实现这一功能。
为了实现这一功能,选择了一种可插拔的架构。这意味着应用程序中有一个专门的文件夹,用于存放所有的主题。如果用户想要一个新的主题,他们只需要向该文件夹添加一个新的程序集。应用程序启动时,会使用反射加载这些程序集。
主题通过资源字典来表示,这些资源字典还附有代码后端类,以便在代码中访问它们。程序集只需要包含代表主题的资源字典即可。文件数量和资源键名称都不重要,重要的是使用默认应用于控件的样式(没有资源键的样式)来应用模板。
应用程序解决方案包含三个项目。一个是使用主题的主项目,另外两个项目展示了如何构建主题以便正确加载。首先,将介绍这两个项目,最后将展示加载这些程序集的反射代码。
主题程序集 主题将由一个程序集表示。这个程序集将包含一个或多个资源字典,这些资源字典将包含主题的资源(画笔、动画、控件模板、样式等)。这可以在下面的图片中看到:
如上图所示,每个资源字典都有一个关联的代码后端文件。这个代码后端文件在加载资源时非常有用。为了将资源字典文件链接到代码后端文件,需要做几件事情。
首先,需要在资源字典中设置x:Class属性为想要的类名。这必须是一个完全限定的名称。代码如下所示。代码是为ListBoxStyle.xaml字典。
<ResourceDictionary x:Class="Theme1.ListBoxDictionary" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
下一步是添加一个新的部分类来链接这个文件。类的代码非常少,如下所示:
namespace Theme1
{
public partial class ListBoxDictionary : ResourceDictionary
{
public ListBoxDictionary()
{
InitializeComponent();
}
}
}
如所见,需要一个从ResourceDictionary派生的部分类。因为这个类与XAML文件链接,所以它将有一个由设计器自动生成的InitializeComponent()方法。所有提供的字典都需要这样的类。
第二个文件的代码类似。XAML类名如下所示:
<ResourceDictionary x:Class="Theme1.ScrollBarDictionary" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
代码后端文件同样简单:
namespace Theme1
{
public partial class ScrollBarDictionary : ResourceDictionary
{
public ScrollBarDictionary()
{
InitializeComponent();
}
}
}
在为所有主题设置了样式之后,需要编译程序集,然后就完成了。现在可以将程序集添加到主应用程序中预留的文件夹中。
主应用程序 主应用程序非常小。它有两个窗口。主窗口将包含将被样式化的列表框控件,第二个窗口将允许选择要应用的主题。当应用程序启动时,没有应用主题,但可以轻松更改此设置。两个窗口如下所示:
在左侧,可以看到第二个主题,在右侧,可以看到选项窗口。鉴于打开选项窗口的代码(当选择工具->选项菜单时调用ShowDialog())是微不足道的,将只讨论选项窗口中的代码。
当选项窗口加载时,加载MyThemes目录中的每个程序集,并将引用存储在泛型列表中。代码如下所示:
DirectoryInfo di = new DirectoryInfo("../../MyThemes");
foreach (FileInfo fi in di.GetFiles())
{
try
{
themes.Add(Assembly.LoadFile(fi.FullName));
}
catch { }
}
cbThemes.ItemsSource = themes.Select(p => p.GetName().Name).ToList();
用户可以从组合框中选择一个主题,然后按下应用按钮。当他们这样做时,根据名称检索所选的程序集:
Assembly a = themes.Where(p => p.GetName().Name.Equals(cbThemes.SelectedValue)).SingleOrDefault();
如果程序集存在于程序集列表中,首先清除应用程序的资源字典:
if (a != null)
{
((App)Application.Current).Resources.MergedDictionaries.Clear();
}
foreach (Type t in a.GetTypes())
{
Trace.WriteLine(t.FullName);
if (t.IsSubclassOf(typeof(ResourceDictionary)))
{
ConstructorInfo ci = t.GetConstructor(Type.EmptyTypes);
ResourceDictionary rd = (ResourceDictionary)ci.Invoke(new object[] { });
((App)Application.Current).Resources.MergedDictionaries.Add(rd);
}
}