原文:《Programming WPF》翻译 第6章 2.资源与样式
WPF的样式机制以来于资源体系来定位样式。正如你在第5章看到的,样式在元素的资源片段中定义,而且样式通过其名字被引用,正如示例6-18所示:
示例6-18
<Window x:Class="ResourcePlay.Window1" Text="ResourcePlay"
xmlns="http://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005">
<Window.Resources>
<Style x:Key="myStyle">
<Setter Property="Button.FontSize" Value="36" />
</Style>
</Window.Resources>
<Grid>
<Button Style="{StaticResource myStyle}">Hello</Button>
</Grid>
</Window>
然而,如何定义一个样式,使之自动的应用到一个元素,而无需显示指定要引用的资源——这是可以实现的,而且非常有用——当你需要把一个样式应用到具有独特类型的所有元素上,而不是把资源引用添加到每个元素上。示例6-19对示例6-18做了一些修改,展示了隐式声明这一功能。
示例6-19
<Window x:Class="ResourcePlay.Window1" Text="ResourcePlay"
xmlns="http://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005">
<Window.Resources>
<Style TargetType="{x:Type Button}">
<Setter Property="Button.FontSize" Value="36" />
</Style>
</Window.Resources>
<Grid>
<Button>Hello</Button>
</Grid>
</Window>
注意到Button标签不再有其特定的Style属性。然而,这个样式仍然通过TargetType应用到Button上,而不是定义一个key,这个样式使用x:Type来设置TargetType,于是通知XAML为这个TargetType类提供一个System.Type对象。
如果FrameworkElement没有显示指定Style,它总是会寻找一个使用其自身类型的样式资源,作为其Target类型。
如果你建立了一些非样式的资源,例如SolidColorBrush,同时设置其x:Key为某个UI元素的类型,如果试着使用该元素的类型就会发生一个错误。这是因为你创建了一个带有TargetType的Style却没有指定x:Key,x:Key隐式地设置为同TargetType一样。这个Key用于定位style。因此,通常而言,你应该避免将x:Key设置为Type类型的对象。
因为元素会在资源中搜索它的样式,你可以利用系统级别的资源。你可以定义一个样式资源在局部范围内,如果你仅仅希望影响少量的元素;或者在一个广义范围上,例如Window.Resource;或者在应用程序的范围。而且样式可能延及到系统级别。这种样式和资源之间的联系是使用皮肤和主体的关键
6.2.1皮肤和主题
皮肤和主题都是控制UI外观的技术。主题,是一种系统级别的外观,例如Windows2000的经典外观,又如Windows XP的“Luna”主题。皮肤是一个特定于应用程序的外观,正如各种各样具有不同样式的媒体播放程序,例如WinApp和Windows Media Player
皮肤和主题都可以在WPF实现,作为一组资源应用于需要该样式的控件
既然皮肤的意图在于控制一个特定应用程序的外观,它将为标准控件提供更多的样式,可以在应用程序的指定部分定义各种各样有命名的资源。例如,音乐播放器可能使用一个ListBox用来显示歌曲列表。皮肤可以为之提供一个特定的外观而不用影响应用程序中其他的ListBox。因此应用程序可以为这个ListBox设置特定的命名的样式,同时要在这个样式中支持这个样式。对于这种特定情形,提供这样一个样式是可选择的,但是在其他情形中,应用程序需要皮肤提供提供指定的资源。例如,如果应用程序中有一个工具条,皮肤可能就需要提供资源并在其中为这个工具条定义图像。
同样,主体是用于所有应用程序,因此,其必须为所有类型的控件提供模板和样式。比较而言,一个皮肤是特定于应用程序的,所以它不必提供广泛全面的一组样式。如果应用程序并不使用每一个单独的控件类型,皮肤只需要为那些在应用程序中出现的控件提供样式。示例6-20和示例6-21为一个相当简单的皮肤,展示了xaml和相应的后台代码
示例6-20
<ResourceDictionary x:Class="SimpleSkin.BlueSkin"
xmlns="http://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005"
>
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="Blue" />
<Setter Property="Foreground" Value="White" />
</Style>
</ResourceDictionary>
示例6-21
using System;
using System.Windows;
namespace SimpleSkin {
public partial class BlueSkin : ResourceDictionary {
public BlueSkin( ) {
InitializeComponent( );
}
}
}
以上代码为一个按钮设置了前景色和背景色。一个更复杂的皮肤应该可以为更多的类型元素提供样式,并且提供更多的属性。更多的皮肤包括一些模板属性的设定,从而可以定义控件的外观。但是即使是在这个简单的例子中,底层的原理也都是一样的。示例6-22展示了一个UI,示例6-23则是这个UI的相应后台代码,允许皮肤的切换。(这个示例假设有2个皮肤类,BlueSkin和GreenSkin,都是使用示例6-20的技术定义的。)
示例6-22
<Window x:Class="SimpleSkin.Window1" Text="SimpleSkin"
xmlns="http://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005">
<Grid Margin="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<RadioButtonList x:Name="radioSkins">
<TextBlock>Green</TextBlock>
<TextBlock>Blue</TextBlock>
</RadioButtonList>
<Button Grid.Row="1">Hello</Button>
</Grid>
</Window>
示例6-23
using System;
using System.Windows;
using System.Windows.Controls;
namespace SimpleSkin {
public partial class Window1 : Window {
public Window1( ) {
InitializeComponent( );
EnsureSkins( );
radioSkins.SelectionChanged += SkinChanged;
}
static ResourceDictionary greenSkin;
static ResourceDictionary blueSkin;
static bool resourcesLoaded = false;
private static void EnsureSkins( ) {
if (!resourcesLoaded) {
greenSkin = new GreenSkin( );
blueSkin = new BlueSkin( );
resourcesLoaded = true;
}
}
private void SkinChanged(object o, SelectionChangedEventArgs e) {
switch (radioSkins.SelectedIndex) {
case 0:
Application.Current.Resources = greenSkin;
break;
case 1:
Application.Current.Resources = blueSkin;
break;
}
}
}
}
SimpleSkin类的代码确保了皮肤只创建一次,而且简单地更换皮肤——通过设置应用程序资源字典,使之成为选中的皮肤。(第二个皮肤的源GreenSkin,在这里没有显示出来,看上去和示例6-20相同,只是用绿色取代了蓝色。)样式和系统资源随着资源的更换自动反映出来:当更改皮肤的时候更新所有有效的控件,因此,这就是我们需要的代码。图6-5显示了代码效果。
图6-5
这种切换皮肤的方式有一个小障碍。除了使用皮肤资源,在应用程序级别也存储了一些资源,那么这些应用程序资源会在切换皮肤时丢失。现在,唯一的解决方案是保证每一个皮肤包含一份应用程序级别的资源副本。最好的办法是将这些副本保存在一个单独的类,并将副本和并到资源皮肤中。WPF当前的版本不支持自动和并资源字典,WPF团队的成员已经声明,他们正在考虑更容易的处理办法在未来的发布版本中。目前,只能手动处理,正如示例6-24所示。
示例6-24
ResourceDictionary skinResources = new FooSkinResources( );
ResourceDictionary nonSkinAppResources = new DrawingResources( );
foreach (DictionaryEntry de in nonSkinAppResources) {
skinResources.Add(de.Key, de.Value);
}
如上,你可以将代码添加到加载资源的方法中。在示例6-23中,你可以在EnsureSkins方法中和并资源,将blue和green皮肤都和并到nonSkinAppResources中。