在WPF中创建可换肤的用户界面
在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所描述的一样:
<TextBlock Background="{DynamicResource myBrush}" Text="Whatever
" />转到本文末尾处的”外部链接”部分参考在代码中是如何做到的.
应用三大基础
每个”皮肤”的资源都被放到独立的ResourceDictionary中,它们都属于自己的XAML文件.在运行时我们可以加载一个包含的所有”皮肤”所需资源的ResourceDictionary(此后我们称之为”皮肤字典”),并将它插入到MergedDictionaries 中(其为Application对象的ResourceDictionary),通过将皮肤字典插入到应用程序资源中,应用程序的所有的元素都可以使用该皮肤字典中所包含的资源了.
界面上所有支持”换肤”的元素都应该通过动态资源引用来引用皮肤资源,这就使得我们可以在运行时进行”换肤”以及让这些元素拥有新的皮肤资源.
最简单的完成这项任务的方式是让元素的Style属性被指定为动态资源引用.通过使用元素的Style属性,我们可以让皮肤字典包含那些可以设置任意个属性的Style,这就比从皮肤字典中为每一个单独的属性设置动态资源引用更容易编写和维护代码.
示例程序是什么样子的
我们可以在本文的顶部位置下载到这个示例程序,其包含了一个可以设置三种皮肤的简单窗体.当你使用默认皮肤启动程序时,其如图所示:
当你右击窗体的任意位置时,会弹出一个上下文菜单允许你更换皮肤,如下图所示:
作为一个实际的应用程序以这样的方式来允许用户选择皮肤似乎有一点奇怪了,但这仅仅是一个示例.如果用户在下拉列表中选择代理商的名称为”David”并且在上下文菜单中选择绿色,那么”绿色皮肤”将被应用,软件界面将如下图所示:
注意:选择最后一个代理商的名字与现在软件界面为绿色并没有任何联系.
我创建的最后一个皮肤有一点点怪异,但我喜欢,当应用”蓝色皮肤”时软件界面看起来是这样的:
示例程序是如何运作的
下面是在Visual Studio的解决方案浏览器中我们的示例程序的项目结构:
允许用户更改皮肤的上下文菜单被定义在MainWindow的XAML文件中,如下所示:
<Grid.ContextMenu>
<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>

当用户在菜单中选择一个新的”皮肤”时,在MainWindow的后台代码文件中以下代码将被执行:
void OnMenuItemClick(object sender, RoutedEventArgs e)

{
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);
}
DemoApp对象的ApplySkin方法的调用将导致下面的代码被执行:
public void ApplySkin(Uri skinDictionaryUri)

{
// 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);
}

现在我们来看一个界面元素如何使用皮肤资源的例子,下面的XAML代码诚信了窗口左边的"Agents"区域,其包含了一个包含保险业代理商名字的下拉列表控件,其标题为"Agents".
<UserControl
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
在WPF中创建可换肤的用户界面的更多相关文章
- angular2中一种换肤实现方案
思路:整体思路是准备多套不同主题的css样式.在anguar项目启动时,首先加载的index.html中先引入一套默认的样式.当我们页面有动作时再切换css. 可以通过url传参触发,也可以通过bu ...
- 使用OxyPlot在WPF中创建图表
目录(?)[+] Using Nuget 包括OxyPlot在你的应用程序的最简单方法是使用NuGet包管理器在Visual Studio 运行 Visual Studio并开始创建一个新的WPF项目 ...
- 简单实现WPF界面控件换肤效果
效果如下如图:选择皮肤颜色 1.首先新建一个如图界面: 选择匹夫下拉框Xaml代码如下:三种颜色选项,并触发SelectionChanged事件 <ComboBox Height="2 ...
- VC6.0中MFC界面换肤简例
利用VC中的MFC进行界面设计时,发现界面上的各控件无法简易地进行调整,比如字体大小.颜色.格式等. 为了改变外观,小小地美化一下,今天决定动手一试. 网上提供的库和方法不计其数,我选择了SkinMa ...
- Windows Phone 资源管理与换肤思考
新入手一台Windows 8的笔记本,安装了VS2013后,终于又可以开发WP了.公司暂时不愿意开发WP,那么咱就自行研究吧! 在没有WP开发环境的时候,曾经在WPF尝试了一下换肤功能的实现.最简单的 ...
- WPF中的三维空间(1)
原文:WPF中的三维空间(1) WPF中可以创建三维几何图形,支持3D对象的应用,支持从3D Max等软件将3D文件obj导入设计中,但是目前还不支持将材质同时导入,这样需要在WPF中对3D对象重新设 ...
- WPF中DPI的问题
先搞清楚一下几个概念: DPI:dots per inch ,每英寸的点数.我们常说的鼠标DPI,是指鼠标移动一英寸的距离滑过的点数:打印DPI,每英寸的长度打印的点数:扫描DPI,每英寸扫描了多 ...
- Qt编写安防视频监控系统11-动态换肤
一.前言 Qt中的动态换肤技术是非常一流的,直接调用qApp->setStyleSheet(qss);就可以对整个应用程序进行换肤,如果样式表内容不多,或者对应的贴图不对,效率还是蛮好的,不过据 ...
- WPF换肤之八:创建3D浏览效果
原文:WPF换肤之八:创建3D浏览效果 上节中,我们展示了WPF中的异步以及界面线程交互的方式,使得应用程序的显示更加的流畅.这节我们主要讲解如何设计一个具有3D浏览效果的天气信息浏览器. 效果显示 ...
随机推荐
- Qt socket制作一个简单的文件传输系统
服务器 用qt designer设计出服务器界面: 上代码: Server.pro #------------------------------------------------- # # Pro ...
- ubuntu 14.04 64位使用google官方android开发集成工具adt-64位无法使用adb
在使用ubuntu64位(14.04)时,下载来一个adt-bundle-linux-x86_64-20131030.zip,但是运行时报错: Android: Gradle: Execution f ...
- Android(java)学习笔记10:同步中的死锁问题以及线程通信问题
1. 同步弊端: (1)效率低 (2)如果出现了同步嵌套,就容易产生死锁问题 死锁问题及其代码 : (1)是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象 (2)同步代码块的 ...
- bzoj2004 [Hnoi2010]公交线路
Description 小Z所在的城市有N个公交车站,排列在一条长(N-1)km的直线上,从左到右依次编号为1到N,相邻公交车站间的距 离均为1km. 作为公交车线路的规划者,小Z调查了市民的需求,决 ...
- react中虚拟dom的diff算法
.state 数据 .jsx模板 .生成虚拟dom(虚拟DOM就是一个js对象,用它来描述真实DOM) ['div', {id:'abc'}, ['span', {}, 'hello world']] ...
- POJ 3067 Japan 【树状数组经典】
题目链接:POJ 3067 Japan Japan Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 32076 Accep ...
- ASP.NET SignalR 与LayIM配合,轻松实现网站客服聊天室(六)之 好友申请、同意、拒绝
不知道距离上一篇多久没有写了,可能是因为忙(lan)的关系吧.废话不多说,今天要介绍的不算什么新知识,主要是逻辑上的一些东西.什么逻辑呢,加好友,发送好友申请,对方审批通过,拒绝.(很遗憾,对方审批通 ...
- 中小学信息学奥林匹克竞赛-理论知识考点--IP地址
IP地址同身份证号一样,具有唯一性! 每个人都有一个唯一的标识:身份证号. 互联网中的计算机也一样,具有一个唯一的标识:IP地址. IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”(也 ...
- html加载顺序以及影响页面二次渲染额的因素
浏览器请求发往服务器以后,返回HTML页面,页面内容开始渲染,具体的执行顺序为: 1. 浏览器开始载入html代码,发现<head>标签内有一个<link>标签引用外部CSS文 ...
- 零基础Python知识点回顾(三)
元组 元组是用圆括号括起来的,其中的元素之间用逗号隔开.(都是英文半角)tuple(元组)跟列表类似是一种序列类型的数据,特点就是其中的元素不能更改 既然是有序的,那么,嘿嘿,不错,它也可以有索引,能 ...