在WPF中创建可换肤的用户界面.
周银辉 译
原文参见: http://www.codeproject.com/WPF/SkinningInWPF.asp
下载示例代码
介绍
这篇文章讨论的是在WPF中如何创建可以在运行时”换肤”的用户界面的一些基础知识,我们将验证WPF对用户界面”皮肤”的支持,并通过一个简单的示例程序来展示如何使用这些特性.
背景
当”皮肤”这个术语被应用到用户界面中来时,就是指被运用于用户界面上的所有界面元素的可视化样式.一个可”换肤”的用户界面既可以是在编译时也可以是在运行时被定制(制定皮肤).WPF为用户界面的”换肤”提供了强大的支持.
对于一个软件来说在很多情形下”换肤”也许将变得非常重要.它可以被用来允许最终用户根据个人审美观念来定制自己的软件界面.还有一种情形也许会用到”换肤”,就是当一个公司开发的应用程序被分发成多种客户端,也许每个客户端得拥有它自己的Logo,颜色,字体等等,如果这些程序被有意地设计成可换肤的话,那么只需要付出一点点的努力就可以很轻松的完成这项任务了.
三大基础
解决这一难题需要三大基础,在该部分我们只对它们做一个简要的介绍,可以参考本文的结尾处的”外部链接”部分以获得更多相关信息.如果你对”层次型资源”,”合并的资源字典”以及”动态资源”比较熟悉的话,你可以跳过该部分.
层次型资源
为了实现软件”换肤”,你必须明白WPF的资源系统是如何运作的.在WPF中有很多类型都拥有一个ResourceDictionary类型的公开属性Resources,该字典包含了一个”键-值”对列表,其中”键”可以是任意类型的对象,其”值”就是一个资源(“值”也可以是任意类型的对象).大多数时候我们放入资源字典中的”键”都是string类型的对象,而有时也可能是其他类型.所有的资源都被存放到这样的资源字典中,而资源查找程序正是使用它们来查找所需的资源.
在应用程序中,资源是按照一种层次关系被组织在一起的.当定位资源(比如画刷,样式,数据模板或气体任意类型的对象)时,软件就会执行一个导航于这个层次组织间的查找程序来查找与指定”键”相对应的资源.
它(资源查找程序)会首先检查需求该资源的元素自己所拥有的那些资源,如果没有找到,则它会检查该元素的”父元素”,看该”父元素”是否拥有所需的资源.如果”父元素”也没有所需的资源,则它会继续沿着”元素树”向上检查该元素的每一个”祖先”.如果仍然没有找到,则它最终会向Application对象询问该资源,在本文中我们可以忽略在那之后还会发生什么.
合并的资源字典
ResourceDictionary类中有一个属性允许你从其它的ResourceDictionary实例来合并资源字典,这就像集合的”并集”.这个属性名叫MergedDictionaries,其类型为Collection<ResourceDictionary>.下面这段话是SDK文档中用于解释资源合并时的域规则:
在合并字典中的资源仅仅当它们被合并到主资源字典域中之后才在资源查找域中占有一个位置.尽管在独立的字典中其资源”键”必须是互不相同的,但在合并字典中一个”键”却可能出现多次.因此,被返回的资源就来自于被合并的资源字典集合中的最后一个字典.如果这些被合并的资源字典是用XAML定义的话,那么它们在合并字典中的顺序就于它们在XAML语言中被标记的顺序一致.如果一个”键”既包含于主字典又包含于其它被合并的字典,那么在主字典中的资源将被返回.这些规则既适合动态资源引用也适合于静态资源引用.
转到本文末尾处的”外部链接”部分你可以找到关于资源合并的帮助页链接.
动态资源引用
解决这一难题(软件换肤)的最后一个基础点是通过元素的属性动态地访问可视化资源的这一机制,这也就是扩展标记DynamicResource所做的事情.动态资源引用就向数据绑定一样,当资源在运行时被替换后那些使用该资源的属性将被赋予新的资源.
比如说我们有一个TextBlock对象,它的Background属性必须被设定为有当前皮肤决定的任意的Brush,我们可以为该TextBlock对象的Background属性建立一个动态资源引用,当在运行是软件的皮肤被更换后,与之相应的画刷就将被应用于该TextBlock.动态资源引用将会自动地用新画刷来更新TextBlock对象的Background属性.
正如下面的XAML所描述的一样:
转到本文末尾处的”外部链接”部分参考在代码中是如何做到的.
应用三大基础
每个”皮肤”的资源都被放到独立的ResourceDictionary中,它们都属于自己的XAML文件.在运行时我们可以加载一个包含的所有”皮肤”所需资源的ResourceDictionary(此后我们称之为”皮肤字典”),并将它插入到MergedDictionaries 中(其为Application对象的ResourceDictionary),通过将皮肤字典插入到应用程序资源中,应用程序的所有的元素都可以使用该皮肤字典中所包含的资源了.
界面上所有支持”换肤”的元素都应该通过动态资源引用来引用皮肤资源,这就使得我们可以在运行时进行”换肤”以及让这些元素拥有新的皮肤资源.
最简单的完成这项任务的方式是让元素的Style属性被指定为动态资源引用.通过使用元素的Style属性,我们可以让皮肤字典包含那些可以设置任意个属性的Style,这就比从皮肤字典中为每一个单独的属性设置动态资源引用更容易编写和维护代码.
示例程序是什么样子的
我们可以在本文的顶部位置下载到这个示例程序,其包含了一个可以设置三种皮肤的简单窗体.当你使用默认皮肤启动程序时,其如图所示:
当你右击窗体的任意位置时,会弹出一个上下文菜单允许你更换皮肤,如下图所示:
作为一个实际的应用程序以这样的方式来允许用户选择皮肤似乎有一点奇怪了,但这仅仅是一个示例.如果用户在下拉列表中选择代理商的名称为”David”并且在上下文菜单中选择绿色,那么”绿色皮肤”将被应用,软件界面将如下图所示:
注意:选择最后一个代理商的名字与现在软件界面为绿色并没有任何联系.
我创建的最后一个皮肤有一点点怪异,但我喜欢,当应用”蓝色皮肤”时软件界面看起来是这样的:
示例程序是如何运作的
下面是在Visual Studio的解决方案浏览器中我们的示例程序的项目结构:
允许用户更改皮肤的上下文菜单被定义在MainWindow的XAML文件中,如下所示:
< ContextMenu MenuItem.Click ="OnMenuItemClick" >
< MenuItem Tag =".\Resources\Skins\BlackSkin.xaml" IsChecked ="True" >
< MenuItem .Header >
< Rectangle Width ="120" Height ="40" Fill ="Black" />
</ MenuItem.Header >
</ MenuItem >
< MenuItem Tag =".\Resources\Skins\GreenSkin.xaml" >
< MenuItem .Header >
< Rectangle Width ="120" Height ="40" Fill ="Green" />
</ MenuItem.Header >
</ MenuItem >
< MenuItem Tag =".\Resources\Skins\BlueSkin.xaml" >
< MenuItem .Header >
< Rectangle Width ="120" Height ="40" Fill ="Blue" />
</ MenuItem.Header >
</ MenuItem >
</ ContextMenu >
</ Grid.ContextMenu >
{
MenuItem item = e.OriginalSource as MenuItem;
// Update the checked state of the menu items.
Grid mainGrid = this.Content as Grid;
foreach (MenuItem mi in mainGrid.ContextMenu.Items)
mi.IsChecked = mi == item;
// Load the selected skin.
this.ApplySkinFromMenuItem(item);
}
void ApplySkinFromMenuItem(MenuItem item)
{
// Get a relative path to the ResourceDictionary which
// contains the selected skin.
string skinDictPath = item.Tag as string;
Uri skinDictUri = new Uri(skinDictPath, UriKind.Relative);
// Tell the Application to load the skin resources.
DemoApp app = Application.Current as DemoApp;
app.ApplySkin(skinDictUri);
}
{
// Load the ResourceDictionary into memory.
ResourceDictionary skinDict =
Application.LoadComponent(skinDictionaryUri) as ResourceDictionary;
Collection<ResourceDictionary> mergedDicts =
base.Resources.MergedDictionaries;
// Remove the existing skin dictionary, if one exists.
// NOTE: In a real application, this logic might need
// to be more complex, because there might be dictionaries
// which should not be removed.
if (mergedDicts.Count > 0)
mergedDicts.Clear();
// Apply the selected skin so that all elements in the
// application will honor the new look and feel.
mergedDicts.Add(skinDict);
}
x:Class ="SkinnableApp.AgentSelectorControl"
xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
>
< Border <STRONG > Style="{DynamicResource styleContentArea}" </ STRONG > >
< Grid >
< Grid .RowDefinitions >
< RowDefinition Height ="Auto" />
< RowDefinition Height ="*" />
</ Grid.RowDefinitions >
<!-- AGENT SELECTOR HEADER -->
< Border <STRONG > Style="{DynamicResource styleContentAreaHeader}" </ STRONG > >
< StackPanel Orientation ="Horizontal" >
< Image
Margin ="4,4,0,4"
Source =".\Resources\Icons\agents.ico"
/>
< TextBlock
FontSize ="20"
Padding ="8"
Text ="Agents"
VerticalAlignment ="Center"
/>
</ StackPanel >
</ Border >
<!-- AGENT SELECTION LIST -->
< ListBox
Background ="Transparent"
BorderThickness ="0"
Grid.Row ="1"
IsSynchronizedWithCurrentItem ="True"
ItemsSource ="{Binding}"
<STRONG > ItemTemplate="{DynamicResource agentListItemTemplate}"
</ STRONG > ScrollViewer.HorizontalScrollBarVisibility="Hidden"
/>
</ Grid >
</ Border >
</ UserControl >
下图是AgentSelectorControl使用默认皮肤时的样子:
如上所示,在 AgentSelectorControl中, DynamicResource扩展标记供使用了三处,它们每次引用的资源都必须存在于皮肤字典中.
DynamicResource Markup Extension
How to set a property to a DynamicResource reference in code