原文:在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扩展标记供使用了三处,它们每次引用的资源都必须存在于皮肤字典中.

外部链接

Resources Overview

Merged Resource Dictionaries

DynamicResource Markup Extension

How to set a property to a DynamicResource reference in code

在WPF中创建可换肤的用户界面的更多相关文章

  1. angular2中一种换肤实现方案

    思路:整体思路是准备多套不同主题的css样式.在anguar项目启动时,首先加载的index.html中先引入一套默认的样式.当我们页面有动作时再切换css.  可以通过url传参触发,也可以通过bu ...

  2. 使用OxyPlot在WPF中创建图表

    目录(?)[+] Using Nuget 包括OxyPlot在你的应用程序的最简单方法是使用NuGet包管理器在Visual Studio 运行 Visual Studio并开始创建一个新的WPF项目 ...

  3. 简单实现WPF界面控件换肤效果

    效果如下如图:选择皮肤颜色 1.首先新建一个如图界面: 选择匹夫下拉框Xaml代码如下:三种颜色选项,并触发SelectionChanged事件 <ComboBox Height="2 ...

  4. VC6.0中MFC界面换肤简例

    利用VC中的MFC进行界面设计时,发现界面上的各控件无法简易地进行调整,比如字体大小.颜色.格式等. 为了改变外观,小小地美化一下,今天决定动手一试. 网上提供的库和方法不计其数,我选择了SkinMa ...

  5. Windows Phone 资源管理与换肤思考

    新入手一台Windows 8的笔记本,安装了VS2013后,终于又可以开发WP了.公司暂时不愿意开发WP,那么咱就自行研究吧! 在没有WP开发环境的时候,曾经在WPF尝试了一下换肤功能的实现.最简单的 ...

  6. WPF中的三维空间(1)

    原文:WPF中的三维空间(1) WPF中可以创建三维几何图形,支持3D对象的应用,支持从3D Max等软件将3D文件obj导入设计中,但是目前还不支持将材质同时导入,这样需要在WPF中对3D对象重新设 ...

  7. WPF中DPI的问题

    先搞清楚一下几个概念: DPI:dots  per  inch ,每英寸的点数.我们常说的鼠标DPI,是指鼠标移动一英寸的距离滑过的点数:打印DPI,每英寸的长度打印的点数:扫描DPI,每英寸扫描了多 ...

  8. Qt编写安防视频监控系统11-动态换肤

    一.前言 Qt中的动态换肤技术是非常一流的,直接调用qApp->setStyleSheet(qss);就可以对整个应用程序进行换肤,如果样式表内容不多,或者对应的贴图不对,效率还是蛮好的,不过据 ...

  9. WPF换肤之八:创建3D浏览效果

    原文:WPF换肤之八:创建3D浏览效果 上节中,我们展示了WPF中的异步以及界面线程交互的方式,使得应用程序的显示更加的流畅.这节我们主要讲解如何设计一个具有3D浏览效果的天气信息浏览器. 效果显示 ...

随机推荐

  1. Git warning push.default is unset

    warning: push.default is unset; its implicit value is changing in Git 2.0 from 'matching' to 'simple ...

  2. PIL 图像字符画绘制

    from PIL import Image ascii_char = list('"$%_&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]? ...

  3. 2018.10.31 Mac下的Mysql修改字符编码修改的问题总结

    今天在弄数据库的时候发现存入中文汉字变成了问号,Mac跟windows处理方式不一样. show variables like '%char%'; 查看当前mysql的编码格式 也就是默认编码格式 + ...

  4. ThreadLocal 例子

    /** * 一个ThreadLocal代表一个变量,故其中里只能放一个数据,有两个变量都要线程内共享,则要定义两个ThreadLocal. */ public class ThreadLocalTes ...

  5. Joker

    人生的第一位老师当然是我的爸妈,他们生我下来,教我学会走路讲话,教会我做人的道理,当然,他们还给我名字.人生的第二位老师就是我的初中老师,初中班主任对我非常好,在之后的考上市一中也是其中的缘由,初中班 ...

  6. Android学习笔记_62_手机安全卫士知识点归纳(2)ListView重要属性 PopupWindow应用

    1.缓存颜色: 为什么ListView在拖动的时间是黑色,而静止时间是自己的颜色是因为 ListView的缓存.只需一个配置即可.在这个ListView里面加上它即可. android:cacheCo ...

  7. 百度webAPI配合微信JSDK获取用户当前位子

    逻辑: ①通过微信JS-SDK 获取地理位置接口 获取经纬度 ②调用百度地图转换经纬度的API,得到百度地图的经纬度 ③调用百度地图 正/逆地址编码服务-->国际化逆地理编码 得到JSON数据, ...

  8. Entity Framework 五

    连接情景中的CRUD操作: 连接场景中的CRUD操作是一项相当简单的任务,因为默认情况下,上下文会自动跟踪实体在其生命周期中发生的更改,前提是AutoDetectChangesEnabled为true ...

  9. html基础用法(上)

    html的定义: html超文本标记语言,标准通用标记语言下的一个应用. “超文本”就是指页面内可以包含图片,链接,甚至音乐,程序等非文字语言. 超文本标记语言的结构包括“头”部分(head),和“主 ...

  10. BionicApi 学习笔记

    1.内存管理 malloc, realloc, free new, delete2.文件输入操作 fopen, fwrite, fputs, fputc, fprintf, fflush fread, ...