在 WPF 中创建可换肤的用户界面

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

在 WPF 中创建可换肤的用户界面 这篇文章讨论的是在 WPF 中如何创建可以在运行时换肤的用户界面 换肤的用户界面的一些基础知识, 我们将验证 换肤的用户界面 WPF 对用户界面皮肤的支持,并通过一个简单的示例程序来展示如何使用这些特性。

背景 当皮肤这个术语被应用到用户界面中来时,就是指被运用于用户界面上的所有界面元素的可视 化样式.一个可换肤的用户界面既可以是在编译时也可以是在运行时被定制(制定皮肤).WPF 为用 户界面的"换肤"提供了强大的支持. 对于一个软件来说在很多情形下"换肤"也许将变得非常重要.它可以被用来允许最终用户根据 个人审美观念来定制自己的软件界面.还有一种情形也许会用到"换肤",就是当一个公司开发的应用 程序被分发成多种客户端,也许每个客户端得拥有它自己的 Logo,颜色,字体等等,如果这些程序被 有意地设计成可换肤的话,那么只需要付出一点点的努力就可以很轻松的完成这项任务了. 三大基础 解决这一难题需要三大基础,在该部分我们只对它们做一个简要的介绍,可以参考本文的结尾处 的"外部链接"部分以获得更多相关信息.如果你对"层次型资源","合并的资源字典"以及"动态资源" 比较熟悉的话,你可以跳过该部分. 层次型资源 为了实现软件"换肤",你必须明白 WPF 的资源系统是如何运作的.在 WPF 中有很多类型都拥有 一个 ResourceDictionary 类型的公开属性 Resources,该字典包含了一个"键-值"对列表,其中" 键"可以是任意类型的对象,其"值"就是一个资源(“值"也可以是任意类型的对象).大多数时候我们 放入资源字典中的"键"都是 string 类型的对象,而有时也可能是其他类型.所有的资源都被存放到 这样的资源字典中,而资源查找程序正是使用它们来查找所需的资源. 在应用程序中,资源是按照一种层次关系被组织在一起的.当定位资源(比如画刷,样式,数据模 板或气体任意类型的对象)时,软件就会执行一个导航于这个层次组织间的查找程序来查找与指定" 键"相对应的资源. 它(资源查找程序)会首先检查需求该资源的元素自己所拥有的那些资源,如果没有找到,则它会 检查该元素的"父元素",看该"父元素"是否拥有所需的资源.如果"父元素"也没有所需的资源,则它 会继续沿着"元素树"向上检查该元素的每一个"祖先".如果仍然没有找到,则它最终会向 Application 对象询问该资源,在本文中我们可以忽略在那之后还会发生什么. 合并的资源字典


ResourceDictionary 类中有一个属性允许你从其它的 ResourceDictionary 实例来合并资 源字典,这就像集合的"并集".这个属性名叫 MergedDictionaries,其类型为 Collection.下面这段 话是 SDK 文档中用于解释资源合并时的域规则: 在合并字典中的资源仅仅当它们被合并到主资源字典域中之后才在资源查找域中占有一个位 置.尽管在独立的字典中其资源"键"必须是互不相同的,但在合并字典中一个"键"却可能出现多次. 因此,被返回的资源就来自于被合并的资源字典集合中的最后一个字典.如果这些被合并的资源字典 是用 XAML 定义的话,那么它们在合并字典中的顺序就于它们在 XAML 语言中被标记的顺序一致. 如果一个"键"既包含于主字典又包含于其它被合并的字典,那么在主字典中的资源将被返回.这些规 则既适合动态资源引用也适合于静态资源引用. 转到本文末尾处的"外部链接"部分你可以找到关于资源合并的帮助页链接. 动态资源引用 解决这一难题(软件换肤)的最后一个基础点是通过元素的属性动态地访问可视化资源的这一 机制,这也就是扩展标记 DynamicResource 所做的事情.动态资源引用就向数据绑定一样,当资源 在运行时被替换后那些使用该资源的属性将被赋予新的资源. 比如说我们有一个 TextBlock 对象,它的 Background 属性必须被设定为有当前皮肤决定的任 意的 Brush,我们可以为该 TextBlock 对象的 Background 属性建立一个动态资源引用,当在运行 是软件的皮肤被更换后,与之相应的画刷就将被应用于该 TextBlock.动态资源引用将会自动地用新 画刷来更新 TextBlock 对象的 Background 属性. 正如下面的 XAML 所描述的一样: 以下是引用片段: 以下是引用片段: <TextBlock Background="" Text="Whatever..." /> 转到本文末尾处的"外部链接"部分参考在代码中是如何做到的. 应用三大基础 每个皮肤的资源都被放到独立的 ResourceDictionary 中,它们都属于自己的 XAML 文件.在运 行时我们可以加载一个包含的所有皮肤所需资源的 ResourceDictionary(此后我们称之为"皮肤字 典"),并将它插入到 MergedDictionaries 中(其为 Application 对象的 ResourceDictionary),通 过将皮肤字典插入到应用程序资源中,应用程序的所有的元素都可以使用该皮肤字典中所包含的资 源了. 界面上所有支持换肤的元素都应该通过动态资源引用来引用皮肤资源,这就使得我们可以在运 行时进行换肤以及让这些元素拥有新的皮肤资源.


最简单的完成这项任务的方式是让元素的 Style 属性被指定为动态资源引用.通过使用元素的 Style 属性,我们可以让皮肤字典包含那些可以设置任意个属性的 Style,这就比从皮肤字典中为每 一个单独的属性设置动态资源引用更容易编写和维护代码. 示例程序是什么样子的 我们可以在本文的顶部位置下载到这个示例程序,其包含了一个可以设置三种皮肤的简单窗体. 当你使用默认皮肤启动程序时,其如图所示:
当你右击窗体的任意位置时,会弹出一个上下文菜单允许你更换皮肤,如下图所示:


作为一个实际的应用程序以这样的方式来允许用户选择皮肤似乎有一点奇怪了,但这仅仅是一个示 例.如果用户在下拉列表中选择代理商的名称为"David"并且在上下文菜单中选择绿色,那么"绿色 皮肤"将被应用,软件界面将如下图所示:
注意:选择最后一个代理商的名字与现在软件界面为绿色并没有任何联系. 我创建的最后一个皮肤有一点点怪异,但我喜欢,当应用蓝色皮肤时软件界面看起来是这样的:


示例程序是如何运作的 下面是在 Visual Studio 的解决方案浏览器中我们的示例程序的项目结构:


允许用户更改皮肤的上下文菜单被定义在 MainWindow 的 XAML 文件中,如下所示: 以下是引用片段: 以下是引用片段: <Grid.ContextMenu> <ContextMenu MenuItem.Click="OnMenuItemClick"> <MenuItem Tag=".ResourcesSkinsBlackSkin.xaml" IsChecked="True"> <MenuItem.Header> <Rectangle Width="120" Height="40" Fill="Black" /> </MenuItem.Header> </MenuItem> <MenuItem Tag=".ResourcesSkinsGreenSkin.xaml"> <MenuItem.Header> <Rectangle Width="120" Height="40" Fill="Green" /> </MenuItem.Header> </MenuItem> <MenuItem Tag=".ResourcesSkinsBlueSkin.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 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< p> 以下是引用片段: 以下是引用片段: <UserControl x:Class="SkinnableApp.AgentSelectorControl" xmlns="/winfx/2006/xaml/presentation" xmlns:x="/winfx/2006/xaml" > <Border <STRONG>Style=""</STRONG>> <Grid>


<Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <!-- AGENT SELECTOR HEADER --> <Border <STRONG>Style=""</STRONG>> <StackPanel Orientation="Horizontal"> <Image Margin="4,4,0,4" Source=".ResourcesIconsagents.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="" <STRONG>ItemTemplate="" </STRONG> ScrollViewer.HorizontalScrollBarVisibility="Hidden" /> </Grid> </Border> </UserControl> 下图是 AgentSelectorControl 使用默认皮肤时的样子:


如上所示,在 AgentSelectorControl 中, DynamicResource 扩展标记供使用了三处,它们每 次引用的资源都必须存在于皮肤字典中.



相关文档
最新文档