【WPF学习】第六十一章 组织模板资源
为表达全国各族人民对抗击新冠肺炎疫情斗争牺牲烈士和逝世同胞的深切哀悼,国务院今天发布公告,决定2020年4月4日举行全国性哀悼活动。
当使用控件模板时,需要决定如何更广泛地共享模板,以及是否希望自动地或明确地位用模板。
第一个问题是关于希望在何处使用模板的问题。例如,是将它们限制在特定窗口中吗?大多数情况下,控件模板应用于多个窗口,甚至可能应用于整个应用程序。为避免多次定义模板,可在Application类的Resources集合中定义模板资源。
然而,为此需要考虑另一个事项。通常,控件模板在多个应用程序之间共享。单个应用程序很有可能使用单独开发的模板。然而,一个应用程序只能有一个App.xaml文件和一个Application.Resources集合。因此,在单独资源字典中定义资源是一个更好的主意。这样,可灵活地再特定窗口或在整个应用程序中使用资源。而且还可以结合使用样式,因为任何应用程序都可以包含多个资源字典。为在Visual Studio中添加资源字典,在Solution Explorer窗口中右击项目,选择Add|New Item菜单项,然后选择Resources Dictionary(WPF)模板。
在前面章节中介绍了资源字典,使用它们很容易,只需要为应用程序添加一个新的具有如下内容的XAML文件即可:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" > <ControlTemplate x:Key="GradientButtonTemplate" TargetType="{x:Type Button}"> ... </ControlTemplate> </ResourceDictionary>
虽然可将所有模板组合到单个资源字典文件中,但富有经验的开发人员更愿意为每个控件模板创建单独的资源字典。这是因为控件模板可能很快地变得过于复杂,并可能需要使用其他相关资源。将它们保存在一个单独的地方,并与其他控件相隔离,是一种很好的组织方式。
为使用资源字典,只需要将他们添加到特定窗口或应用程序(这种情况更常见)的Resources集合中。可使用MergedDictionaries集合完成该工作。例如,如果按钮模板在项目文件夹的Resources子文件夹下的Button.xaml文件中,就可以在App.xaml文件中使用以下标记:
<Application x:Class="ControlTemplates.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="Menu.xaml" > <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Resources\Button.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </Application>
一、分解按钮控件模板
当完善或扩展控件模板时,可发现其中封装了大量的不同细节,包括特定的形状、几何图形和画刷。从控件模板中提取这些细节并将他们定义为单独的资源是一个好主意。一个原因是通过该步骤,可以更方便地再一组相关的控件中重用这些画刷。例如,可能会决定创建使用相同的自定义Button、CheckBox和RadioButton控件。为使该工作更加容易,可为画刷(名为Brushes.xaml)创建一个单独的资源字典,并将该资源字典合并到每个控件(如Button.xaml、CheckBox.xaml和RadioButton.xaml)的资源字典中。
为查看这种技术的工作情况,分析下吗的标记。这些标记代表了一个按钮的完整资源字典,包括控件模板使用的资源、控件模板,以及为应用程序中每个按钮应用控件模板的样式规则。始终需要遵循这一顺序,因为资源需要在使用之前先定义(如果在模板之后定义画刷,将收到错误信息,因为模板找不到所需的画刷)。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <!-- Resources used by the template. --> <RadialGradientBrush RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3" x:Key="HighlightBackground"> <GradientStop Color="White" Offset="0"/> <GradientStop Color="Blue" Offset="0.4"/> </RadialGradientBrush> <RadialGradientBrush RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3" x:Key="PressedBackground"> <GradientStop Color="White" Offset="0"/> <GradientStop Color="Blue" Offset="1"/> </RadialGradientBrush> <SolidColorBrush Color="Blue" x:Key="DefaultBackground"></SolidColorBrush> <SolidColorBrush Color="Gray" x:Key="DisabledBackground"></SolidColorBrush> <RadialGradientBrush RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3" x:Key="Border"> <GradientStop Color="White" Offset="0"/> <GradientStop Color="Blue" Offset="1"/> </RadialGradientBrush> <!-- The button control template. --> <ControlTemplate x:Key="GradientButtonTemplate" TargetType="{x:Type Button}"> <Border Name="Border" BorderBrush="{StaticResource Border}" BorderThickness="2" CornerRadius="2" Background="{StaticResource DefaultBackground}" TextBlock.Foreground="White"> <Grid> <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black" StrokeThickness="1" StrokeDashArray="1 2" SnapsToDevicePixels="True"> </Rectangle> <ContentPresenter Margin="{TemplateBinding Padding}" RecognizesAccessKey="True"></ContentPresenter> </Grid> </Border> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="Border" Property="Background" Value="{StaticResource HighlightBackground}" /> </Trigger> <Trigger Property="IsPressed" Value="True"> <Setter TargetName="Border" Property="Background" Value="{StaticResource PressedBackground}" /> </Trigger> <Trigger Property="IsKeyboardFocused" Value="True"> <Setter TargetName="FocusCue" Property="Visibility" Value="Visible"></Setter> </Trigger> <Trigger Property="IsEnabled" Value="False"> <Setter TargetName="Border" Property="Background" Value="{StaticResource DisabledBackground}"></Setter> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </ResourceDictionary>
下图显示了该模板定义的按钮。在该例中,当用户鼠标移到按钮上时,使用渐变填充。然而,渐变中心位于按钮中央。如果希望创建更新颖的效果,例如跟随鼠标位置的渐变,就需要使用动画或者编写代码。
二、通过样式应用模板
这种设计存在局限性,控件模板本质上硬编码了一些细节,如颜色方案。这意味着如果希望在按钮中使用相同的元素组合(Border、Grid、Rectangle和ContentPresenter)并采用相同的方式安排它们,但希望提供不同的颜色方案,就必须创建应用不同画刷资源的新模板副本。
这未必是个问题(毕竟,布局和格式化细节可能紧密相关,以至于不希望以任何方式隔离它们)。但这确实限制了重用控件模板的能力。如果模板使用了元素的复合排列方式,并且希望重用这些具有各种不同格式化细节(通常是颜色和字体)的元素,可从模板中将这些细节提取出来,并将它们放到样式中。
为此,需要重新编写模板。这次不能使用硬编码的颜色,而需要使用模板绑定从控件属性中提取出信息。下面的示例为前面看到的特殊按钮定义了一个精简模板。控件模板将一些细节作为基础的固定要素——焦点框和两个单位宽的圆角边框。背景和边框画刷是可配置的。唯一需要保留的触发器是显示焦点框的那个触发器:
<ControlTemplate x:Key="GradientButtonTemplate" TargetType="{x:Type Button}"> <Border Name="Border" BorderThickness="2" CornerRadius="2" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"> <Grid> <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black" StrokeThickness="1" StrokeDashArray="1 2" SnapsToDevicePixels="True"> </Rectangle> <ContentPresenter Margin="{TemplateBinding Padding}" RecognizesAccessKey="True"></ContentPresenter> </Grid> </Border> <ControlTemplate.Triggers> <Trigger Property="IsKeyboardFocused" Value="True"> <Setter TargetName="FocusCue" Property="Visibility" Value="Visible"></Setter> </Trigger> </ControlTemplate.Triggers> </ControlTemplate>
关联的样式应用这个控件模板,设置边框和背景颜色,并添加触发器以便根据按钮的状态改变背景色:
<!-- The style that applies the button control template. --> <Style TargetType="{x:Type Button}"> <Setter Property="Control.Template" Value="{StaticResource GradientButtonTemplate}"></Setter> <Setter Property="BorderBrush" Value="{StaticResource Border}"></Setter> <Setter Property="Background" Value="{StaticResource DefaultBackground}"></Setter> <Setter Property="TextBlock.Foreground" Value="White"></Setter> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="{StaticResource HighlightBackground}" /> </Trigger> <Trigger Property="IsPressed" Value="True"> <Setter Property="Background" Value="{StaticResource PressedBackground}" /> </Trigger> <Trigger Property="IsEnabled" Value="False"> <Setter Property="Background" Value="{StaticResource DisabledBackground}"></Setter> </Trigger> </Style.Triggers> </Style>
理想情况下,应能在控件模板中保留所有触发器,因为它们代表控件的行为,并使用样式简单设置基本属性。但在此如果希望样式能够设置颜色方案,是不可能实现的。
为使用这个新模板,需要设置按钮的Style属性而不是Template属性:
<Button Margin="10" Padding="5" Style="{StaticResource CustomButtonStyle}" >A Simple Button with a Custom Template</Button>
现在可创建一些新样式,这些样式使用相同的模板,但为了应用新的颜色方案,应将模板绑定到不同的画刷。
使用这种方法存在的重要限制。在该样式中不能使用Setter.TargetName属性,因为样式不包含控件模板(只是引用模板而已)。因此,样式和触发器有一定的限制。它们不能深入到可视化树中来改变嵌套的元素的这个方面。反而,样式需要设置控件的属性,而且控件中的元素需要使用模板来绑定来绑定 。
三、自动应用模板
在当前示例中,每个按钮负责使用Template或Style属性将自身关联到适当模板。如果使用控件模板,在应用程序中的特定位置创建特殊效果,这是合理的。但如果希望在具有自定义外观的整个应用程序中改变每个按钮的皮肤,这就不是很方便了。对于这种情况,可能会更希望应用程序中的所有按钮自动请求新的模板。为实现该功能,需要通过样式应用控件模板。
技巧是使用类型样式,这种样式会自动影响响应的元素类型并设置Template属性。下面是一个样式示例,应将该样式放到资源字典的资源集合中,从而为按钮提供新外观:
<Style TargetType="{x:Type Button}"> <Setter Property="Control.Template" Value="{StaticResource GradientButtonTemplate}"></Setter> </Style>
上面的代码可以工作,原因是样式没有指定键名,这意味着改用元素类型(Button)。
请记住,仍可通过创建一个按钮并将其Style属性明确设置为null值,退出该样式:
<Button Style="{x:Null}" ...></Button>
包含基于类型的样式的组合的资源字典通常(非正式地)被称为主题(theme)。主题能够实现非凡的效果。通过主题可为已有应用程序的所有控件重新应用皮肤,而根本不需要改用户界面标记。需要做的全部工作就是为项目添加资源字典,并将其合并到App.xaml文件的Application.Resources集合中。
如果在Web上搜索,可找到许多能用于为WPF应用程序换肤的主题,例如,可下载WPF Futures版本中的几个示例主题。
为使用主题,为项目添加包含资源字典的.xaml文件。例如,WPF Futuers提供了一个名为ExpressionDark.xaml的主题文件。然后,需要在应用程序中激活样式。可逐个窗口地完成该工作,但更快捷的方法是通过添加如下所示的标记在应用程序级别导入他们:
<Application ...> <Application.Resources> <ResourceDictionary Source="ExpressionDark.xaml"/> </Application.Resources> </Application>
现在将全面实施资源字典中基于类型的样式,并将自动改变应用程序每个窗口的每个通用控件的外观。如果是一位正在搜索热门用户界面的应用程序开发人员,但不具备自己构建这类用户界面的设计技能,那么使用该技巧几乎不需要付出努力就能很容易地插入第三方的精彩界面。
四、由用户选择的皮肤
在一些应用程序中,可能希望动态改变模板,通常是根据用户的个人爱好加以改变。这很容易发现,但文档没有对比进行详细说明。基本技术是在运行时加载新的资源字典,并使用新加载的资源字典代替当前的资源字典(不需要替换所有资源,字需要替换那些勇于皮肤的资源)。
诀窍在于检索ResourceDictionary对象,该对象经过编译并作为资源嵌入到应用程序中。最简单的方法是使用ResourceManager类来加载所需资源。
例如,假定已创建用于定义同一个按钮控件模板的替代版本的两个资源。其中一个保存在GradientButton.xaml文件中,而另一个保存在GradientButtonVariant.xaml文件中。为了更好地组织资源,这两个文件都位于当前项目的Resources子文件夹中。
现在可创建一个简单的窗口,通过Resources集合使用这些资源中的一个,如下所示:
<Window.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Resources/GradientButton.xaml"></ResourceDictionary> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Window.Resources>
现在可通过如下代码使用不同的资源字典:
ResourceDictionary resourceDictionary = new ResourceDictionary(); resourceDictionary.Source = new Uri( "Resources/GradientButtonVariant.xaml", UriKind.Relative); this.Resources.MergedDictionaries[0] = resourceDictionary;
上面的代码加载GradientButtonVariant资源字典,并将它放置到MergedDictionaries集合的第一个位置。在此没有清空MergedDictionaries集合或其他任何窗口的资源,因为可能链接到了其他席位继续使用的资源字典。也没有为MergedDictionaries集合添加新条目,因为这可能与位于不同集合中的同名资源发生冲突。
如果正在为整个应用程序改变皮肤,可使用相同的方法,但应使用应用程序的资源字典,可使用如下代码更新这个资源字典:
Application.Current.Resources.MergedDictionaries[0]=newDictionary;
还可使用pack URI语法加载在另一个程序集合中的资源字典:
ResourceDictionary resourceDictionary = new ResourceDictionary(); resourceDictionary.Source = new Uri( "ControlTemplates;component/GradientButtonVariant.xaml", UriKind.Relative); this.Resources.MergedDictionaries[0] = resourceDictionary;
当加载新的资源字典时,会自动使用新模板更新所有按钮。如果当修改控件时不需要完全改变皮肤,还可以为皮肤提供基本样式。
该例假定GradientButton.xaml和GradientButtonVariant.xaml资源使用元素类型样式自动改变按钮。还有一种方法——通过手动设置Button对象的Template或Style属性来选用新的模板。如果使用这种方法,务必使用DynamicResource应用,而不能使用StaticResource。如果使用Resource,当切换皮肤时不会更新按钮模板。
还有一种通过编写代码加载资源字典的方法。可使用与为窗口创建带阿妈隐藏类几乎相同的方法,为资源字典创建代码隐藏类。然后就可以直接实例化这个类,而不是使用ResourceDictionary.Source属性。这种方法有一个优点,他是强类型的(没有机会为Source属性输入无效的URI),并且可为资源类添加属性、方法以及其他功能。例如,可以使用这种方法,为自定义窗口模板创建具有事件处理代码的资源。
尽管为资源字典创建代码隐藏类很容易,但Visual Studio不能自动完成该工作,需要为继承自ResourceDictionary的部分类添加代码文件,并为构造函数中调用InitializeComponent()方法:
public partial class GradientButtonVariant : ResourceDictionary { public GradientButtonVariant() { InitializeComponent(); } }
这里使用的类名为GradientButtonVariant,而且该类存储在GradientButtonVariant.xaml.cs文件中。包含资源的XAML文件被命名为GradientButtonVariant.xaml。不是必须使用一致的名称,但这是一个好主意,并且在创建窗口以及创建页面时也遵循了Visual Studio使用的这一约定。
接下来将类链接到资源字典。通过为资源字典的根元素添加Class特性完成该工作,就想为窗口应用Class特性一样,并且可为任何XAML类应用Class特性。然后提供完全限定的类名。在这个示例中,项目名称是ControlTemplates,因此这是默认名称空间,最后的标签可能如下所示:
<ResourceDictionary x:Class="ControlTemplates.GradientButtonVariant">
现在可使用该代码创建资源字典并将它应用于窗口:
GradientButtonVariant newDict = new GradientButtonVariant(); this.Resources.MergedDictionaries[0] = newDict;
在Solution Explorer中,如果希望GradientButtonVariant.xaml.cs文件嵌套在GradientButtonVariant.xaml文件的下面,需要在文本编辑器中修改.csproj项目文件。在<ItemGroup>部分,找到代码因此文件,并将下面的代码:
<Compile Include="Resources\GradientButtonVariant.xaml.cs"/>
修改为:
<Compile Include="Resources\GradientButtonVariant.xaml.cs"> <DependentUpon>Resources\GradientButtonVariant.xaml</DependentUpon> </Compile>