【翻译】在WPF中创建可换肤(更改主题)的用户界面

原文链接: Creating a Skinned User Interface in WPF

译者:HectorInsanE


引言

本文主要介绍了在程序运行时实现皮肤系统的基础知识。我们会探索WPF对UI换肤的支持,同时使用例子实现这些特性。

 

背景知识

“皮肤” 这个术语,在应用到用户界面时,表示的是界面上所有元素都遵循的一种视觉上的风格。 一个“可换肤”的界面指,界面可以在编译过程中,或运行过程中改变其风格。WPF为这种功能提供了很好的支持。(为了不那么拗口,下文中的“主题”将会代替“皮肤”一词,但表示的是同一个意思)

 

UI换肤具有重要的意义。它使最终用户根据自己的审美自定义UI。另一个情况是当公司为多个客户订制软件的时候,客户希望使用自己公司的logo,字体或颜色等等与自己公司原有的软件吻合。假如UI系统可提供换肤(更换主题)功能,那么这些工作就可以很轻松地完成了。

 

三个重要知识

为了解决这个问题,有三个内容我们需要知道。该部分主要介绍这些内容, 外部链接中有这些问题的更多相关信息。假如你已经熟悉了分层式资源,合并资源字典以及动态资源引用,那就请放心地跳过这一部分。

 

分层式资源 Hierarchical Resource

您需要了解WPF的资源系统是如何工作的,这样才能为实现换肤功能。很多WPF中的类有一个公共属性叫Resources,类型是ResourceDictionary。这个字典包含了一组键值对,每个键值对中的key和value都可以使任意的object。但通常key是一个string,或者是一个Type对象。所有的资源都存储在这些字典里面,寻找资源时,就使用它们来找到程序想要的特定资源。

  Resource dictionaries 在程序中分层存放。当程序需要定位一个资源(例如Brush, Style, DataTemplate或其他对象)的时候,WPF会发起一个资源寻找过程,根据key值在分层的架构中寻找。

  首先 他检查发起资源寻找请求的元素的Resource属性。假如没有找到,它会检查该元素的父元素。假如还是没有找到该资源,那么它会一直向上,检查每个祖先节点。假如仍然没有找到,那么它最后会问Application,它是否有这个resource。在本文中,我们暂时不去考虑此后的事吧。

 

合并的资源字典 Merged Resource Dictionaries

ResourceDictionary 暴露了一个属性,让我们可以将其他的ResourceDictionary合并到当前的ResourceDictionary中。该属性是MergedDictionaries,类型是Collection<ResourceDictionary>。SDK中对此属性的解释是:
在MergedDictionary中的资源会在资源查找的过程中占用一段空间,这段空间位于合并它们的资源之后。虽然在任何的字典中,资源的key值不应该重名。但是一个MergedDictionary中,一个key可以存在多次。此时,资源寻找会返回MergedDictionary中最后一个与key对应的资源。若MergedDictionary在xaml中定义,那么MergedDictionary的顺序就是xaml中子元素的顺序。假如一个key在初级的Dictionary中被定义了,同时又出现在一个被合并的子典中,那么查找过程会返回初级的字典中的对应资源。这个规则对于静态资源引用和动态资源引用都适用。

“外部链接”中 有更多的内容

 

 Dynamic Resource Reference

最后我们要知道的,就是这些可视的资源是如何动态地与元素的属性联系到一起的。DynamicResource标记关键字起作用了。一个动态的资源引用与数据绑定类似,当资源在运行时被代替(译者注:更新?),使用它的属性会被给予一个新的值。

 

 例如,我们认为,TextBlock的Background应该设置成当前主题中设定的颜色。那么我们可以给TextBlock的背景设置一个动态资源引用。当这个主题(皮肤)在运行时被更换了,虽然旧的Brush赋给了TextBlock的Background属性,但是动态资源引用仍然会自动地更新TextBlock的Background。

 

<TextBlock Background="{DynamicResource myBrush}" Text="Whatever..." />

 

本文末尾的“外部链接” 有更多的关于编写此代码的信息。

 

使用这三个功能

各个主题的资源文件应该放在一个单独的ResourceDictionary中,每个主题有一个自己的XAML文件。在程序运行的过程中,我们可以通过加载包含所有主题元素的ResourceDictionary,并将它们加入到Application的ResourceDictionary的MergedDictionary属性中(好绕啊)。这样在应用程序中的所有元素都可以使用这些主题中包含的资源。

 

需要支持动态换肤的元素,应该使用动态资源引用与主题文件建立联系。这样我们就可以在运行时通过更改主题来让这些元素应用新的主题资源了。

 

 最简单的做法,就是将元素的Style属性,赋值给一个动态资源引用。使用元素的style属性,我们可以让主题字典&*&*&*&*&*&。从编写或者维护的角度上说,这样的做法比起在所有的属性中设置主题字典的元素的做法要简单得多。

 

示例的外观:

 当在Window的区域内右键单击时,一个ContextMenu会弹出,让用户选择使用哪种主题。


在真正的程序中,这样做简直就是可笑的,但是这只是一个示例,就让它这样吧!假如用户单击了叫David的员工,并且选择了绿色作为主题,那么就会变成这样。

 

 注意,这位叫Greene的员工和这个界面是绿色的没有哪怕一丁点的关系!

 

最后一个界面稍稍有点奇怪,但是我挺喜欢的。

 

你们可能已经看出来了,我的UI做的不太好。

 

工程布局:

 

 

让用户点击进行换肤的 ContextMenu ,在MainWindow中定义。

<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>

 

处理点击事件的后台代码 :

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 函数:

View Code
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标明了MainWindow左边的Agent区。包含了一个LIstbox放置了员工的名字,以及一个Header。

 

<UserControl 
    
x:Class="SkinnableApp.AgentSelectorControl"
    xmlns
="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x
="http://schemas.microsoft.com/winfx/2006/xaml"
    
>
    
<Border Style="{DynamicResource styleContentArea}">
        
<Grid>
            
<Grid.RowDefinitions>
                
<RowDefinition Height="Auto" />
                
<RowDefinition Height="*" />
            
</Grid.RowDefinitions>

            
<!-- AGENT SELECTOR HEADER -->
            
<Border Style="{DynamicResource styleContentAreaHeader}">
                
<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}"
                ItemTemplate
="{DynamicResource agentListItemTemplate}"
                ScrollViewer.HorizontalScrollBarVisibility
="Hidden" />
        
</Grid>
    
</Border>
</UserControl>

 下面就是默认的主题定义出来的AgentSelectorControl的外观了。

 

 在AgentSelectorControl控件中,有三个地方引用了DynamicResource,其中的每一个都对应了一个必须在资源字典中必须存在的资源。所有的资源字典都在示例项目内,我也不啰嗦这些没什么好玩的XAML了。

 

 外部链接:

 

 

 

 

posted @ 2011-03-10 14:08  Hector  阅读(1982)  评论(0编辑  收藏  举报