在资源字典中重用控件模板






4.98/5 (24投票s)
在 ResourceDictionary 中定义控件模板,
引言
本文及配套的演示应用程序将展示在ResourceDictionary
对象中定义的控件模板在整个应用程序中的重用优势。
背景
最近我写了一个需要Apple Mac OSX外观和感觉的Windows应用程序。多亏了WPF和控件模板,这不成问题。

在此过程中,我发现Resource Dictionaries(资源字典)使我无需编写重复的代码和标记。使用ResourceDictionary
类,我可以在一个地方(一个单独的程序集)定义控件模板、事件处理程序和其他相关资源,然后只需将应用程序的众多窗口的Style
属性指向Apple Mac风格的控件模板。
定义控件模板
根据我的经验,我认识到可重用的内容应该被定义为一个单独的组件(嗯,废话!)。正如本演示应用程序一样,可重用的Window控件模板已在单独的程序集中定义,并附带资源字典。这使得其他开发人员可以轻松地获取和使用模板,而无需费力。
我需要在这里提到,我将外部程序集编写成一个普通的、最基础的.NET类库。如果你打算走这条路,需要注意的是,你必须为项目添加几个引用才能使用资源字典和其他窗口类型的资源。所需的引用是:
PresentationCore
PresentationFramework
System.Xaml
WindowsBase
好的,继续。Window模板已在名为MacStyledWindow.xaml的ResourceDictionary
中定义,如下所示:
<!-- Window Template -->
<ControlTemplate x:Key="MacWindowTemplate" TargetType="{x:Type Window}">
<Grid>
..
<!-- The window content. -->
<Border Grid.Row="1">
<AdornerDecorator>
<ContentPresenter />
</AdornerDecorator>
</Border>
..
</Grid>
</ControlTemplate>
模板的绝大部分标记已省略。我想让你注意到AdornerDecorator
和ContentPresenter
标签。当你指定应用程序中的某个窗口使用此窗口控件模板时,这两个标签可以让你将任何自定义窗口内容(文本块、按钮、菜单、布局控件等)插入到你的窗口中。
在同一个资源字典中,我创建了一个Style
块,用于设置一些通用的Window属性,以便用Mac外观来模板化窗口。你会注意到我设置的一个属性是Template
属性。它指向前面提到的Mac窗口模板。
<!-- Mac Window Style -->
<Style x:Key="MacWindowStyle" TargetType="Window">
<Setter Property="Background" Value="Transparent" />
<Setter Property="WindowStyle" Value="None" />
<Setter Property="AllowsTransparency" Value="True" />
<Setter Property="Opacity" Value="0.95" />
<Setter Property="Template" Value="{StaticResource MacWindowTemplate}" />
</Style>
除了MacStyledWindow
资源字典,Demo.Common
项目还包含另外两个资源字典:一个用于按钮样式(MacStyledTitleBarButtons.xaml),一个用于渐变画笔(MacStyledButtonBrushes.xaml)。我不会深入介绍它们。我想指出的是如何从同一项目中的另一个资源字典引用这些资源字典。
在MacStyledWindow.xaml文件的顶部附近,你会看到以下标记:
<ResourceDictionary.MergedDictionaries>
<!-- Resource Dictionary containing Buttons used for the Mac Window titlebar buttons-->
<ResourceDictionary Source="MacStyledTitleBarButtons.xaml" />
</ResourceDictionary.MergedDictionaries>
你使用MergedDictionaries
集合来添加一个资源字典引用。上面的标记是如何引用同一项目中的资源字典的示例。稍后,我将展示如何从外部项目引用资源字典。
为ResourceDictionary创建代码隐藏
因此,我们定义了一个控件模板和一个包含一些默认属性的样式,这些属性使我们能够用Mac外观来皮肤化我们的窗口。这在一定程度上是很好的;它让我们能够给窗口提供Mac外观,但同时,在应用模板时,我们移除了_一些_基本的窗口功能;用户将无法再拖动窗口在屏幕上移动,并且标题栏中漂亮的关闭、最小化和最大化按钮在点击时也_什么_都不做!

默认情况下,当你通过Visual Studio创建一个ResourceDictionary
时,向导只会为你创建一个XAML文件来包含你的资源定义。通常这已经足够了,但在这个例子中,我们需要附加几个事件处理程序来使模板化的窗口更具交互性。
事件处理程序最常见的地方是代码隐藏文件(我想是这样);这些类将UI控件的事件与某种事件处理程序连接起来。因此,遵循同样的方法来解决我们的事件处理问题是有意义的。
我所做的是向包含定义控件模板的资源字典的项目添加了一个新类,将该类设置为partial
,并将类定义改为继承自ResourceDictionary
,并将其命名为与资源字典相同,但带有.cs
扩展名。根据我读到的内容,命名约定不是必需的,但似乎是微软的标准做法,所以,如果它没坏,就别修,对吧?
所以我们有一个名为MacStyledWindow.xaml的资源字典,还有一个名为MacStyledWindow.xaml.cs的代码隐藏文件。解决了?还没完全解决。新的代码隐藏文件需要链接到资源字典。这是通过在资源字典中使用x:Class="..."
语法指定代码隐藏类来完成的。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Demo_Common.Resource_Dictionaries.MacStyledWindow">
为了完成代码隐藏类的设置,最后要做的是确保在实例化时调用InitializeComponent
。我为新类创建了一个构造函数,并在其中放置了一个对InitializeComponent
的调用。
public MacStyledWindow()
{
InitializeComponent();
}
添加事件处理程序
现在我们的代码隐藏设置正确了,我们可以添加所需的事件处理程序。正如我前面提到的,我们的模板化窗口不支持在屏幕上拖动,标题栏按钮也_不_响应点击。这很容易在我们的新代码隐藏类中得到解决。
我定义的窗口控件模板有一个标题栏,它由一个Border
和一个TextBlock
组成。
<!-- The title bar. -->
<Border MouseLeftButtonDown="titleBar_MouseLeftButtonDown"
Padding="15" CornerRadius="10, 10, 0, 0">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFABABAB"/>
<GradientStop Color="#FF202020" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
</Border>
<TextBlock Foreground="White" Text="{TemplateBinding Title}"
MouseLeftButtonDown="titleBar_MouseLeftButtonDown"
HorizontalAlignment="Center" VerticalAlignment="Center" FontWeight="Normal" />
为了实现所需的拖动行为,我在代码隐藏中创建了一个事件处理程序,它调用边框和textblock
的DragMove()
方法。
private void titleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var window = (Window)((FrameworkElement)sender).TemplatedParent;
window.DragMove();
}
我之前在一个不相关的项目中遇到过DragMove()
方法,所以这对我来说并不新鲜。然而,我遇到的困难是如何确定哪个模板化窗口触发了MouseLeftButtonDown
事件。结果也同样简单。我所要做的就是获取FrameworkElement
的TemplatedParent
的引用,将该引用转换为Window
对象,然后在此引用上调用DragMove()
方法。搞定!我只需要在资源字典中连接边框和textblock
的事件处理程序。
<Border MouseLeftButtonDown="titleBar_MouseLeftButtonDown"...
我使用相同的方法来处理标题栏按钮上的点击事件。
// Close button
private void closeButton_Click(object sender, RoutedEventArgs e)
{
var window = (Window)((FrameworkElement)sender).TemplatedParent;
window.Close();
}
// Minimize button
private void minimizeButton_Click(object sender, RoutedEventArgs e)
{
var window = (Window)((FrameworkElement)sender).TemplatedParent;
window.WindowState = WindowState.Minimized;
}
// Maximize button
private void maximizeButton_Click(object sender, RoutedEventArgs e)
{
var window = (Window)((FrameworkElement)sender).TemplatedParent;
// Check the current state of the window.
// If the window is currently maximized, return the
// window to it's normal state when the maximize button is clicked,
// otherwise maximize the window.
if (window.WindowState == WindowState.Maximized)
window.WindowState = WindowState.Normal;
else window.WindowState = WindowState.Maximized;
}
从外部项目消耗模板
现在我们已经完成了模板上的大部分工作,它已经准备好被使用了。显然,附加的模板中还有许多其他项目和功能未包含在内,但这里的想法是展示如何在_一个_位置定义模板可以_减少_重复代码和XAML的数量。
话不多说,现在是时候向你展示我们的演示应用程序如何在其所有窗口中实现该模板了。首先,我们添加对包含模板的程序集的引用。在本例中,它是在同一解决方案中的一个项目,所以我们只需添加对Demo.Common
项目的引用。
接下来,我们在应用程序的App.xaml中全局添加对包含窗口模板的资源字典的引用。我们在这里添加引用,以便资源字典在整个应用程序中可用。如果你还记得,我之前展示过如何添加对同一项目中的资源字典的引用。在应用程序的App.xaml(位于单独的项目中)中添加引用略有不同。
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/
Demo.Common;component/Resource Dictionaries/MacStyledWindow.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
第一个区别是我们需要添加一个根ResourceDictionary
标签。第二个区别是我们用于设置ResourceDictionary
的Source
属性的URI格式。以前,资源字典位于同一个项目中,所以我们只需要指定包含资源字典的文件名。但是,在这里,资源字典定义在一个外部程序集中,所以我们必须使用pack:
URI。
<ResourceDictionary Source="pack://application:,,,/Demo.Common;
component/Resource Dictionaries/MacStyledWindow.xaml" />
pack:
URI允许我们告诉ResourceDictionary
在哪里_外部_找到XAML文件。不深入解释,我将简要说明URI的工作原理。
pack:
URI的第一部分指定了authority
。在我们的演示应用程序中,资源文件被编译到引用的程序集中,因此authority
被设置为application:,,,
。
pack:
URI的第二部分指定了资源文件的路径。在这里,我们指定AssemblyShortName
为Demo.Common
,我们使用;component
关键字指定被引用的程序集是从本地程序集引用的,最后,我们指定被引用程序集中资源文件的路径(包括子文件夹名称)。
有了这些,我们就可以将模板应用于演示应用程序中的窗口了。这非常直接。我们只需要将窗口的Style
属性设置为指向MacWindowStyle static
资源。
<Window x:Class="FortySixApplesResourceDictionaryDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Style="{StaticResource MacWindowStyle}"
>
结论
我发现,通过在单独的程序集中定义控件模板和样式,可以减少我不得不编写的重复代码和XAML。这种方法还有其他好处,当然,比如允许应用程序的并行开发;外观和感觉与业务逻辑分离,_一个_不影响_另一个_。
此外,还可以通过这种方式定义多个“主题”,并在运行时动态加载它们。如果需要添加或更改主题,只会影响包含控件模板的程序集,以及应用程序config文件中的一两个键...
Using the Code
演示应用程序是一个Visual Studio 2010解决方案,包含两个项目:
- 46ApplesResourceDictionaryDemo.csproj
- 这是解决方案中的主项目。它是一个WPF Windows应用程序。
- Demo.Common.csproj
- 这个项目是一个标准的.NET类库,并被WPF Window应用程序引用。
将zip存档提取到本地硬盘。打开46ApplesResourceDictionaryDemo.sln解决方案文件。如果尚未设置,请确保46ApplesResourceDictionaryDemo
项目是启动项目。
历史
- 2010年4月8日:初始发布