65.9K
CodeProject 正在变化。 阅读更多。
Home

创建 WPF 中的蒙皮用户界面

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (82投票s)

2007年7月27日

CPOL

8分钟阅读

viewsIcon

380797

downloadIcon

12480

回顾使用各种视觉样式创建 WPF 用户界面的基础知识。

Screenshot - montage.png

目录

引言

本文讨论了如何在 WPF 中创建可以在运行时“换肤”的用户界面的基础知识。我们将探讨 WPF 对 UI 换肤提供的支持,并回顾一个简单的演示应用程序如何利用这些功能。

背景

“皮肤”一词用于用户界面时,指的是一种在 UI 的所有元素上一致应用的视觉样式。“可换肤”的 UI 可以在编译时或运行时进行重新样式化。Windows Presentation Foundation 为 UI 换肤提供了极大的支持。

在应用程序中,UI 换肤可能在各种情况下都很重要。它可以允许最终用户根据自己的个人审美偏好来自定义 UI。换肤的另一种情况是,当一家公司创建一个应用程序并将其部署给不同的客户时。每个客户可能都希望应用程序显示其公司徽标、颜色、字体等。如果 UI 在设计时考虑了换肤,那么这项任务可以轻松完成,只需最少的精力。

三个支柱

这个难题有三个基本组成部分。本节将简要概述这些主题。有关它们的更多信息,请参阅文章末尾的“外部链接”部分。如果您已经熟悉分层资源、合并资源字典和动态资源引用,请随意跳过本节。

分层资源

要实现换肤支持,您必须了解 WPF 资源系统的基本工作原理。WPF 中的许多类都有一个名为 `Resources` 的公共属性,类型为 `ResourceDictionary`。此字典包含键值对列表,其中键是任何对象,值是可以是任何对象的资源。最常见的是,`ResourceDictionary` 中的键是 `string`;有时它们是 `Type` 对象。所有资源都存储在这些字典中,资源查找过程使用它们来查找请求的资源。

应用程序中的资源字典以分层方式排列。当需要查找诸如 `Brush`、`Style`、`DataTemplate` 或任何其他类型的对象等资源时,平台将执行一个查找过程,该过程沿着资源层次结构向上导航,搜索具有特定键的资源。

它首先检查请求资源的元素所拥有的资源。如果在那里找不到资源,它会检查该元素的父元素,看看它是否拥有请求的资源。如果父元素没有该资源,它会继续沿着元素树向上查找,询问每个祖先元素是否拥有具有请求键的资源。如果仍然找不到资源,它最终会询问 `Application` 对象是否拥有该资源。为了本文的目的,我们可以忽略之后发生的情况。

合并资源字典

`ResourceDictionary` 公开了一个属性,允许您将其他 `ResourceDictionary` 实例中的资源合并进来,类似于集合论中的并集。该属性名为 `MergedDictionaries`,类型为 `Collection`。以下是 SDK 文档的说明,它解释了应用于合并字典中资源的范围规则:

合并字典中的资源在资源查找范围中占据一个位置,紧随它们被合并到的主资源字典的范围之后。尽管任何单个字典中的资源键都必须是唯一的,但一个键可以在一组合并的字典中出现多次。在这种情况下,返回的资源将来自 `MergedDictionaries` 集合中按顺序找到的最后一个字典。如果 `MergedDictionaries` 集合是在 XAML 中定义的,那么集合中合并字典的顺序就是标记中提供的元素的顺序。如果主字典中定义了一个键,并且在合并的字典中也定义了该键,那么返回的资源将来自主字典。这些范围规则同样适用于静态资源引用和动态资源引用。

有关合并资源字典的帮助页面的链接,请参阅本文底部的“外部链接”部分。

动态资源引用

这个难题的最后一个基本组成部分是将视觉资源动态关联到元素属性的机制。这就是 `DynamicResource` 标记扩展发挥作用的地方。动态资源引用类似于数据绑定,即当运行时替换资源时,使用它的属性将被赋予新资源。

例如,假设我们有一个 `TextBlock`,其 `Background` 属性必须设置为当前皮肤指定的任何 `Brush`。我们可以为 `TextBlock` 的 `Background` 属性设置一个动态资源引用。当运行时更改皮肤时,以及更改 `TextBlock` 要使用的画笔时,动态资源引用将自动更新 `TextBlock` 的 `Background` 以使用新画笔。以下是 XAML 中的样子:

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

有关如何在代码中编写它的信息,请参阅本文末尾的“外部链接”部分。

运用这三个支柱

每个皮肤的资源应放在一个单独的 `ResourceDictionary` 中,每个 `ResourceDictionary` 属于其自己的 XAML 文件。在运行时,我们可以加载一个包含某个皮肤所有资源的 `ResourceDictionary`(下称“皮肤字典”)并将其插入到 `Application` 的 `ResourceDictionary` 的 `MergedDictionaries` 中。通过将皮肤字典放入 `Application` 的资源中,应用程序中的所有元素都可以使用它包含的资源。

UI 中所有必须支持换肤的元素都应通过动态资源引用来引用皮肤资源。这使我们能够更改皮肤并在运行时让这些元素使用新的皮肤资源。

最简单的方法是将元素的 `Style` 属性分配给一个动态资源引用。通过使用元素的 `Style` 属性,我们允许皮肤字典包含 `Style`,这些 `Style` 可以设置被换肤元素的任意数量的属性。这比为每个从皮肤字典获取值的属性设置动态资源引用更容易编写和维护。

演示应用程序的样子

本文顶部的演示应用程序包含一个简单的 `Window`,它可以以三种方式进行换肤。也就是说,除非您决定创建更多皮肤。首次运行应用程序时,它使用默认皮肤,外观如下:

Screenshot - black_skin.png

如果您右键单击 `Window` 的任何位置,将弹出一个 `ContextMenu`,允许您更改皮肤。外观如下:

Screenshot - skin_selection.png

在实际应用程序中,这绝对是一种奇怪的用户选择皮肤的方式,但这只是一个演示应用程序!如果用户单击 `ListBox` 中的名为 David 的代理,然后选择 `ContextMenu` 中的绿色条,则会应用“绿色皮肤”,UI 将如下所示:

Screenshot - green_skin.png

注意:选定代理的姓氏是 Greene 与 UI 现在是绿色的事实无关!:)

我创建的最后一个皮肤有点奇怪,但我喜欢它。以下是应用“蓝色皮肤”时的 UI 外观:

Screenshot - blue_skin.png

正如您可能猜到的,我不是一个很好的视觉设计师。

演示应用程序的工作原理

这是演示项目结构,如 Visual Studio 的解决方案资源管理器所示:

Screenshot - project_structure.png

`ContextMenu` 允许用户更改活动皮肤,在 `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);
}

现在我们将举例说明 UI 中的元素如何使用皮肤资源。以下 XAML 表示 `MainWindow` 左侧的“代理”区域。它包含一个包含保险代理人姓名的 `ListBox` 和一个标题“代理”。

<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` 在应用默认皮肤时的样子:

Screenshot - agent_selector.png

上面看到的 `AgentSelectorControl` 中有三个 `DynamicResource` 标记扩展的用法。每个都引用一个必须存在于皮肤字典中的资源。所有皮肤字典都在演示项目中可用,因此我不会在这里包含大量不那么有趣 XAML 来充斥这篇文章。

外部链接

修订历史

  • 2007 年 7 月 27 日 - 创建了文章
  • 2007 年 8 月 1 日 - 文章已编辑并移至 CodeProject.com 的主文章库
© . All rights reserved.