WPF控件模板

在过去,Windows开发人员必须在方便性和灵活性之间做出选择。为得到最大的方便性,他们可以使用预先构建好的控件。这些控件可以工作的足够好,但可定制性十分有限,并且几乎总是具有固定的可视化外观。偶尔,某些控件提供了不很直观的"自主绘图"模式,允许开发人员通过响应回调来绘制控件的一部分。但基本控件------按钮、文本框、复选框和列表框等------被完全锁定了。因此,希望实现一些特殊效果的开发人员不得不从头构建自定义控件。这确实是一个问题------手工编写绘图逻辑不但非常费时而且很困难,但自定义控件开发人员还需要从头实现基本功能(例如,在文本框中选择文本以及在按钮中处理按键)。并且,即使自定义控件是完美的,将它们插入到已有应用程序中也需要进行一些重要的修改,通常需要修改代码(并且还需要进行更多的测试)。简单的说,自定义控件是必须的内容------它们是实现新颖时髦的用户界面的唯一方法,但支持它们并将它们集成到应用程序中也是一件棘手的事情。

WPF最终通过样式以及模板解决了控件的自定义问题。这些特性能够很好地工作的原因是,在WPF中控件的实现方式发生了重大变化。在以前的用户界面技术(如Windows窗体)中常用的控件实际上不是由.NET代码实现的。相反,Windows窗体控件封装了来自Win32 API的核心要素,它们是不能改变的,而WPF中的每个控件是由纯粹的.NET 代码构成的,其背后没有使用任何Win32 API。因此,WPF能够提供一种机制(样式和模板),运行您进入这些元素的内部并"扭曲"它们。实际上,"扭曲"是一种错误的说法,因为正如控件模板所做到的,可采用所能想到的方式对WPF控件进行最彻底的重新设计。

逻辑树与可视化树

在一个窗口中,添加的元素分类称为逻辑树,WPF编程人员需要耗费大部分时间构建逻辑树,然后使用事件处理代码支持它们。实际上,WPF的特性(如属性值继承、事件路由以及样式)都是通过逻辑树进行工作的。

然而,如果希望自定义元素,逻辑树起不到多大帮助作用。显然,可使用另一个元素替换整个元素(例如,可使用自定义的 FancyButton类 替换当前的 Button类),但这需要做更多工作,并且可能扰乱应用程序的用户界面代码。因此,WPF通过可视化树进入更深层次。

可视化树是逻辑树的扩展版本。它将元素分成更小的部分。它并不查看被精心封装到一起的黑色方框,如按钮,而是查看按钮的可视化元素------使按钮具有阴影背景特性的边框(Border)、内部的容器(ContentPresenter)以及存储按钮文本的块(TextBlock)。所有这些细节本身都是元素------换句话说,控件中的每个单独的细节都是由FrameworkElement 类的派生类表示的。

通过可视化树可以完成以下两项非常有用的工作:

1、可使用样式改变可视化树中的元素。可使用 Style.TargetType 属性选择希望修改的特定元素。甚至当控件属性发生变化时,可使用触发器自动完成更改。不过,某些特定的细节很难甚至无法修改。

2、可为控件创建新模板。对于这种情况,控件模板将被用于按期望的方式构建可视化树。

WPF提供了用于浏览逻辑树和可视化树的两个类:System.Windows.LogicalTreeHelper 和 System.Windows.Media.VisualTreeHelper。 LogicalTreeHelper 类允许通过动态加载XAML文档在WPF应用程序中关联事件处理程序。LogicalTreeHelper 类提供了较少的方法,尽管这些方法偶尔很有用,但大多数情况下会改用特定的FrameworkElement 类中的方法。

FindLogicalNode() 根据名称查找特定元素,从指定的元素开始并向下查找逻辑树

BringIntoView() 如果元素在可滚动的容器中,并且当前不可见,就将元素滚动到视图中。FrameworkElement.BringIntoView() 方法执行相同的工作。

GetParent() 获取指定元素的父元素。

GetChildren() 获取指定元素的子元素。不同的元素支持不同的内容模型。例如,面板支持多个子元素,而内容控件只支持一个子元素。然而GetChildren() 方法抽象了这一区别,并且可以使用任何类型的元素进行工作。

除了专门用来执行低级绘图操作的一些方法外(例如,命中测试和边界检查的方法),VisualTreeHelper 类提供的方法与LogicalTreeHelper类提供的方法类似,也提供了 GetChildrenCount()、GetChild()以及GetParent()方法。

VisualTreeHelper类还提供了一种研究应用程序中可视化树的有趣方法。使用GetChild()方法,可以遍历任意窗口的可视化树,并且为了进行分析可以将它们显示出来。这是一个非常好的学习工具,只需要使用一些递归的代码就可以实现。

VisualTreeDisplay.xaml

cs 复制代码
<Window x:Class="TestControlTemplate.VisualTreeDisplay"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestControlTemplate"
        mc:Ignorable="d"
        Title="VisualTreeDisplay" Height="450" Width="800">
    <TreeView Name="treeElements" Margin="10"/>
</Window>

VisualTreeDisplay.xaml.cs

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace TestControlTemplate;


public partial class VisualTreeDisplay : Window
{
    public VisualTreeDisplay()
    {
        InitializeComponent();
    }
    public void ShowVisualTree(DependencyObject element)
    {
        // Clear the tree.
        treeElements.Items.Clear();
        // Start processing elements, begin at the root.
        ProcessElement(element, null);
    }
    private void ProcessElement(DependencyObject element, TreeViewItem previousItem)
    {
        // Create a TreeViewItem for the current element.
        TreeViewItem item = new TreeViewItem();
        item.Header = element.GetType().Name;
        item.IsExpanded = true;
        // Check whether this item should be added to the root of the tree
        //(if it's the first item), or nested under another item.
        if (previousItem == null)
        {
            treeElements.Items.Add(item);
        }
        else
        {
            previousItem.Items.Add(item);
        }
        // Check if this element contains other elements.
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
        {
            // Process each contained element recursively.
            ProcessElement(VisualTreeHelper.GetChild(element, i), item);
        }
    }
}

理解模板

对于可视化树的分析引出了几个有趣的问题。例如,控件如何从逻辑树扩展成可视化树表示?

每个控件都有一个内置的方法,用于确定如何渲染控件(作为一组更基础的元素)。该方法称为控件模板,使用XAML标记块定义的。

每个WPF控件都设计成无外观的(lookless),这意味着完全可以重定义其可视化元素(外观)。但不能改变控件的行为,控件的行为被固化到控件类中(尽管经常可使用各种属性微调控件的行为)。当选择使用类似Button的控件时,是希望得到类似按钮的行为(换句话说,选择的是一个元素,该元素提供了能被单击的内容,通过单击来触发动作,并且可用做窗口上的默认按钮或取消按钮)。然而,可自由的改变控件的外观,以及当鼠标移动到元素上或按下鼠标时的响应方式。另外,也可自由改变控件外观的其他方面和可视化行为。

下面是普通Button类的模板:

cs 复制代码
    <Window.Resources>
        <SolidColorBrush x:Key="Button.MouseOver.Background" Color="#FFBEE6FD"/>
        <SolidColorBrush x:Key="Button.MouseOver.Border" Color="#FF3C7FB1"/>
        <SolidColorBrush x:Key="Button.Pressed.Background" Color="#FFC4E5F6"/>
        <SolidColorBrush x:Key="Button.Pressed.Border" Color="#FF2C628B"/>
        <SolidColorBrush x:Key="Button.Disabled.Background" Color="#FFF4F4F4"/>
        <SolidColorBrush x:Key="Button.Disabled.Border" Color="#FFADB2B5"/>
        <SolidColorBrush x:Key="Button.Disabled.Foreground" Color="#FF838383"/>
        <ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            <Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="true">
                <ContentPresenter x:Name="contentPresenter" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}"
                                  RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsDefaulted" Value="true">
                    <Setter Property="BorderBrush" TargetName="border" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
                </Trigger>
                <Trigger Property="IsMouseOver" Value="true">
                    <Setter Property="Background" TargetName="border" Value="{StaticResource Button.MouseOver.Background}"/>
                    <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.MouseOver.Border}"/>
                </Trigger>
                <Trigger Property="IsPressed" Value="true">
                    <Setter Property="Background" TargetName="border" Value="{StaticResource Button.Pressed.Background}"/>
                    <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.Pressed.Border}"/>
                </Trigger>
                <Trigger Property="IsEnabled" Value="false">
                    <Setter Property="Background" TargetName="border" Value="{StaticResource Button.Disabled.Background}"/>
                    <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.Disabled.Border}"/>
                    <Setter Property="TextElement.Foreground" TargetName="contentPresenter" Value="{StaticResource Button.Disabled.Foreground}"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </Window.Resources>

除去触发器部分,就剩下了Border标签,其内就是在可视化树中看到的扩展内容。Border定义了按钮的标准可视化外观,而ContentPresenter类 存储了提供的所有内容。如果希望构建全新按钮,只需要创建新的控件模板。

当按钮获得焦点、被单击以及被禁用时,触发器控制按钮如何进行变换。对于这些触发器,实际上没什么特别需要介绍的内容。针对获取焦点和单击的触发器并不会修改按钮本身,只是修改为按钮提供可视化外观的 Border 类的属性。

当构建自己的控件模板时将看到同样的职责分离。如果足够幸运,可直接使用触发器完成所有工作,可能不需要创建自定义类并添加代码。另一方面,如果需要提供更复杂的可视化设计,可能需要继承自定义的修饰类。

剖析控件

当创建控件模板是,新建的控件模板完全代替了原来的模板。这样可以得到更大的灵活性,但更复杂些。大多数情况下,在创建满足自己需求的模板之前,需要查看控件使用的标准模板。某些情况下,自定义的控件模板可镜像标准模板,并只进行很少的修改。

WPF文档没有列出标准控件模板的XAML。然而,可通过编程获取所需的信息。基本思想是从Template属性(该属性在Control类中定义)获取控件的模板,然后使用XamlWriter类,将该模板串行化到XAML文件中。

ControlTemplateDisplay.xaml

cs 复制代码
<Window x:Class="TestControlTemplate.ControlTemplateDisplay"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestControlTemplate"
        mc:Ignorable="d"
        Title="ControlTemplateDisplay" Height="450" Width="800" Loaded="Window_Loaded">
    <Grid Name="grid">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="3*"/>
        </Grid.ColumnDefinitions>
        <ListBox Grid.Column="0" Name="ListTypes" SelectionChanged="ListTypes_SelectionChanged" DisplayMemberPath="Name"></ListBox>
        <TextBox Grid.Column="1" Name="TextTemplate" TextWrapping="Wrap" VerticalScrollBarVisibility="Visible" FontFamily="Consolas"></TextBox>
    </Grid>
</Window>

ControlTemplateDisplay.xaml.cs

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Xml;

namespace TestControlTemplate;

public class TypeComparer : IComparer<Type>
{
    public int Compare(Type x, Type y)
    {
        return x.Name.CompareTo(y.Name);
    }
}

public partial class ControlTemplateDisplay : Window
{
    public ControlTemplateDisplay()
    {
        InitializeComponent();
    }
    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        Type controlType = typeof(Control);
        List<Type> derivedTypes = new List<Type>();
        Assembly? assembly = Assembly.GetAssembly(typeof(Control));
        if (assembly != null)
        {
            foreach (Type type in assembly.GetTypes())
            {
                if (type.IsSubclassOf(controlType) && !type.IsAbstract && type.IsPublic)
                {
                    derivedTypes.Add(type);
                }
            }
        }
        derivedTypes.Sort(new TypeComparer());
        ListTypes.ItemsSource = derivedTypes;
    }
    private void ListTypes_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        try
        {
            Type type = (Type)ListTypes.SelectedItem;
            ConstructorInfo? info = type.GetConstructor(System.Type.EmptyTypes);
            if (info == null)
            {
                return;
            }
            Control control = (Control)info.Invoke(null);
            Window? win = control as Window;
            if (win != null)
            {
                win.WindowState = System.Windows.WindowState.Minimized;
                win.ShowInTaskbar = false;
                win.Show();
            }
            else
            {
                control.Visibility = Visibility.Collapsed;
                grid.Children.Add(control);
            }
            ControlTemplate template = control.Template;
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            StringBuilder sb = new StringBuilder();
            XmlWriter writer = XmlWriter.Create(sb, settings);
            XamlWriter.Save(template, writer);
            TextTemplate.Text = sb.ToString();
            if (win != null)
            {
                win.Close();
            }
            else
            {
                grid.Children.Remove(control);
            }
        }
        catch (Exception err)
        {
            TextTemplate.Text = "<< Error generating template: " + err.Message + ">>";
        }
    }
}

构建该应用的诀窍是使用反射(reflection),反射是用于检查类型的 .NET API。当第一次加载应用程序的主窗口时,扫描 PresentationFramework.dll核心程序集(在该程序集中定义了控件类)中的所有类型。然后将这些类型添加到一个集合中,根据类型名称进行排序,然后将该集合绑定到一个列表。

创建控件模板

当创建自定义控件时,可以不用担心标准化和主题集成(实际上,WPF不像以前的用户界面技术那样强调用户界面标准化)。反而更需要关注如何创建富有吸引力的新颖控件,并将它们混合到用户界面的其他部分。因此,可能不需要创建诸如ButtonChrome的类,而可使用已有的元素设计自给自足的不使用代码的控件模板。

简单按钮

为应用自定义控件模板,只需要设置控件的Template属性。尽管可定义内联模板(通过在控件标签内部嵌入控件模板标签),但这种方法基本没有意义。这是因为几乎总是希望为同一控件的多个皮肤实例重用模板。为适应这种设计,需要将控件模板定义为资源,并使用StaticResource引用该资源。

cs 复制代码
<Button Template="{StaticResource ButtonTemplate}">ButtonTemplate</Button>

通过这种方法,不仅可以较容易地创建许多自定义按钮,在以后还可以很灵活地修改控件模板,而不会扰乱应用程序用户界面的其余部分。

要为基本按钮创建模板,需要绘制边框和背景,然后在按钮中放置内容。绘制边框的两种可选方法是使用Rectangle 和 Border 类。这里使用Border类,将具有圆角的桔色轮廓与红色背景和白色文本结合在一起。此外,还应该包含一个 ContentPresenter元素,所有内容控件都需要ContentPresenter元素------它是表示"在此插入内容"的标记器,告诉WPF在何处保存内容:

cs 复制代码
        <ControlTemplate x:Key="ButtonTemplate2" TargetType="{x:Type Button}">
            <Border Name="border" Background="Red" BorderBrush="Orange" BorderThickness="3" CornerRadius="2" TextBlock.Foreground="White">
                <ContentPresenter RecognizesAccessKey="True"></ContentPresenter>
            </Border>
        </ControlTemplate>

该ContentPresenter元素将RecognizesAccessKey 属性设置为true。尽管这不是必需的,但可确保按钮支持访问键------具有下划线的字母,可使用该字母触发按钮。

如果控件继承自ContentControl类,其模板将包含一个ContentPresenter元素,指示将在何处放置内容。如果控件继承自ItemsControl 类,其模板将包含一个 ItemsPresenter 元素,指示在何处放置包含列表项的面板。在极少数情况下,控件可能使用这些类的派生版本------例如,ScrollViewer的控件模板使用继承自ContentPresenter类的ScrollContentPresenter类。

模板绑定

现在这个按钮模板还存在一个小问题。现在为按钮添加的标签将Margin 属性的值指定为10,并将Padding属性的值指定为5。父容器关注的是Margin属性,但忽略了Padding属性,是按钮的内容和侧边挤压在一起。此处的问题是Padding 属性不起作用,除非在模板中特别注意它。换句话说,模板负责检索内边距值并使用该值在内容周围插入额外的空白。

辛运的是,WPF专门针对该目的设计了一个工具:模板绑定。通过使用模板绑定,模板可从应用模板的控件中提取一个值。在这里,可使用模板绑定检索Padding属性的值,并使用该属性值在ContentPresenter元素周围创建外边距:

cs 复制代码
<ContentPresenter RecognizesAccessKey="True" Margin="{TemplateBinding Padding}"></ContentPresenter>

这样就会得到所期望的效果,在边框和内容之间添加了一些空白。

模板绑定和普通的数据绑定类似,但它们的量级更轻,因为它们是专门针对在控件模板中使用而设计的。它们只支持单向数据绑定(换句话说,它们可从控件向模板传递信息,但不能从模板向控件传递信息),并且不能用于从Freezable类的派生类的属性中提取信息。如果遇到模板绑定不生效的情形,可改用具有完整功能的数据绑定。

模板绑定支持WPF的变化监测基础结构,所有依赖项属性都包含该基础结构。这意味着如果修改了控件的属性,模板会自动考虑该变化。当使用在一小段时间内重复改变属性值的动画时,该细节尤其有用。

预计需要哪些模板绑定的唯一方法是检查默认控件模板。如果查看Button类的控件模板,就会发现在模板绑定的使用方法上,与自定义模板是完全相同的------获取为按钮指定的内边距,并将它转换为ContentPresenter元素周围的外边距。还会发现标准按钮模板包含另外几个模板绑定,如HorizontalAlignment、VerticalAlignment以及Background,这个简单的自定义模板中没有使用这些模板绑定。这意味着如果为按钮设置了这些属性,对于这个简单的自定义模板来说,这些设置没有效果。

改变属性触发器

如果测试上面创建的按钮,就会发现它令人非常失望。本质上,它不过是一个红色的圆角矩形------当在它上面移动鼠标或单击鼠标时,其外观没有任何反应。

可通过为控件模板添加触发器来方便的解决这个问题。

cs 复制代码
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter TargetName="border" Property="Background" Value="DarkRed"></Setter>
                </Trigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="border" Property="Background" Value="IndianRed"></Setter>
                    <Setter TargetName="border" Property="BorderBrush" Value="DarkKhaki"></Setter>
                </Trigger>
            </ControlTemplate.Triggers>

在所有按钮中还需要另一个元素------焦点指示器。虽然无法改变现有的边框以添加焦点效果,但是还可以很容易的添加另一个元素以显示是否具有焦点,并且可以简单地使用触发器根据Button.IsKeyboardFocused属性显示或隐藏该元素。尽管可使用许多方法创建焦点效果,但下面的示例值添加了一个具有虚线边框的透明的Rectangle元素。Rectangle元素不能包含子内容,从而需要确保Rectangle元素和其余内容相互重叠。完成该操作最容易的方法是,使用只有一个单元格的Grid控件来封装Rectangle元素和ContentPresenter元素,这两个元素位于同一个单元格中。

cs 复制代码
        <ControlTemplate x:Key="ButtonTemplate3" TargetType="{x:Type Button}">
            <Border Name="border" Background="Red" BorderBrush="Orange" BorderThickness="3" CornerRadius="2" TextBlock.Foreground="White">
                <Grid>
                    <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black" StrokeThickness="1" StrokeDashArray="1 2" SnapsToDevicePixels="True" ></Rectangle>
                    <ContentPresenter RecognizesAccessKey="True" Margin="{TemplateBinding Padding}"></ContentPresenter>
                </Grid>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter TargetName="border" Property="Background" Value="DarkRed"></Setter>
                </Trigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="border" Property="Background" Value="IndianRed"></Setter>
                    <Setter TargetName="border" Property="BorderBrush" Value="DarkKhaki"></Setter>
                </Trigger>
                <Trigger Property="IsKeyboardFocused" Value="True">
                    <Setter TargetName="FocusCue" Property="Visibility" Value="Visible" />
                </Trigger>
                <Trigger Property="IsEnabled" Value="False">
                    <Setter TargetName="border" Property="TextBlock.Foreground" Value="Gray" />
                    <Setter TargetName="border" Property="Background" Value="MistyRose" />
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>

设置器再次使用TargetName属性查找需要改变的元素。另外还添加了一个触发器,当按钮的IsEnable属性变为false时,该触发器改变按钮的背景色。为确保该规则优于其它相冲突的触发器设置,应当在触发器列表的末尾定义它。

模板与样式

模板与样式有类似之处。通常,在整个应用程序中,这两个特性都可以改变元素的外观。然而,样式被限制在一个小得多的范围之内。它们可调整控件的属性,但不能使用全新的由不同元素组成的可视化树替代控件原来的外观。

在前面看到的简单按钮包含了一些仅凭样式无法实现的特性。尽管可使用样式设置按钮的背景色,但当按下按钮时调整按钮的背景色会遇到更多麻烦,因为按钮的内置模板已经针对该目的提供了一个触发器。另外,也不能很方便的添加焦点矩形。

还可以通过控件模板实现许多特殊类型的按钮,如果使用样式,是无法获得此类效果的。例如,不是使用矩形边框,而是创建类似椭圆形状的按钮,或使用路径绘制更复杂的形状。其余的标记------甚至是用于在不同状态之间切换背景色的触发器------基本上不需要加以修改。

使用动画触发器

触发器并非仅局限于设置属性,当特定属性发生变化时,还可以使用事件触发器运行动画。

cs 复制代码
        <ControlTemplate x:Key="ButtonTemplate4" TargetType="{x:Type Button}">
            <Border Name="border" Background="Red" BorderBrush="Orange" BorderThickness="3" CornerRadius="2" TextBlock.Foreground="White">
                <Grid>
                    <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Blue" StrokeThickness="1" StrokeDashArray="2 2" SnapsToDevicePixels="True" ></Rectangle>
                    <ContentPresenter RecognizesAccessKey="True" Margin="{TemplateBinding Padding}"></ContentPresenter>
                </Grid>
            </Border>
            <ControlTemplate.Triggers>
                <EventTrigger RoutedEvent="MouseEnter">
                    <BeginStoryboard>
                        <Storyboard>
                            <ColorAnimation Storyboard.TargetName="border" Storyboard.TargetProperty="Background.Color"
                                            To="Blue" Duration="0:0:1" AutoReverse="True" RepeatBehavior="Forever"></ColorAnimation>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
                <EventTrigger RoutedEvent="MouseLeave">
                    <BeginStoryboard>
                        <Storyboard>
                            <ColorAnimation Storyboard.TargetName="border" Storyboard.TargetProperty="Background.Color" Duration="0:0:0.5"></ColorAnimation>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="border" Property="Background" Value="IndianRed" />
                    <Setter TargetName="border" Property="BorderBrush" Value="DarkKhaki" />
                </Trigger>
                <Trigger Property="IsKeyboardFocused" Value="True">
                    <Setter TargetName="FocusCue" Property="Visibility" Value="Visible" />
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>

这里使用ColorAnimation对象来改变按钮。下面是可能希望使用EventTrigger驱动的动画执行的其他一些任务:

显示或隐藏元素为此,需要改变控件模板中的Opacity属性

改变形状或位置 可使用TranslateTransform 对象调整元素的位置(例如,稍偏移元素是按钮具有已被按下的感觉)。当用户将鼠标移到元素上时,可使用ScaleTransform 或 RotateTransform 对象稍微旋转元素的外观。

改变光照或着色 为此,需使用改变绘制背景的画刷的动画。可使用ColorAnimation 动画改变SolidBrush 画刷中的颜色,也可动态显示更复杂的画刷以得到更高级的效果。例如,可改变LinearGradientBrush画刷中的一种颜色(这是默认按钮控件模板执行的操作),也可改变RadialGradientBrush 画刷的中心点。

有些高级光照效果使用多层透明元素。对于这种情况,可使用动画修改其中一层的透明度,从而让其他层能够透过该层显示。

组织模板资源

当使用控件模板时,需要决定如何更广泛地共享模板,以及是否希望自动地或明确地应用模板。

第一个问题是关于希望在何处使用模板的问题。例如,是将它们限制在特定的窗口中吗?大多数情况下,控件模板应用于多个窗口,甚至可能应用于整个应用程序。为避免多次定义模板,可在Application类的Resources集合中定义模板资源。

然而,为此需要考虑领一个事项。通常,控件模板在多个应用程序之间共享。单个应用程序很可能使用单独开发的模板。然而,一个应用程序只有一个App.xaml文件和一个Application.Resources集合。因此,在单独资源字典中定义资源是一个更好的主意。这样,可灵活地在特定窗口或在整个应用程序中使用资源。而且还可以结合使用样式,因为任何应用程序都可以包含多个资源字典。

虽然可将所有模板组合到单个资源字典文件中,但富有经验的开发人员更愿意为每个控件模板创建单独的资源字典。这是因为控件模板可能很快会变得过于复杂,并可能需要使用其他相关资源。将它们保存在一个单独的地方,并与其它控件相隔离,是一种很好的组织方式。

为使用资源字典,只需要将它们添加到特定窗口或应用程序(这种情况更常见)的Resources集合中。

分解按钮控件模板

当完善或扩展控件模板时,可发现其中封装了大量的不同细节,包括特定的形状、几何图形和画刷。从您的控件模板中提取这些细节并将它们定义为单独的资源是一个好主意。一个原因是通过该步骤,可以更方便的在一组相关的控件中重用这些画刷。为使该工作更加容易,可为画刷创建一个单独资源字典(Brush.xaml),并将该资源字典合并到每个控件(如Button.Xaml、CheckBox.xaml、RadioButton.xaml)的资源字典中。

Resources/Brush.xaml

cs 复制代码
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <RadialGradientBrush RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3" x:Key="HighlightBackground">
        <GradientStop Color="White" Offset="0" />
        <GradientStop Color="Blue" Offset=".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>
</ResourceDictionary>

为查看这种技术的工作情况,分析下面的标记。这些标记代表了一个按钮的完整资源字典,包括控件模板使用的资源、控件模板,以及为应用程序中每个按钮应用控件模板的样式规则。始终需要遵循这一顺序,因为资源需要在使用之前先定义(如果在模板之后定义画刷,将收到错误消息,因为模板找不到所需的画刷)。

Resources/GradientButton.xaml

cs 复制代码
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="TestControlTemplate.Resources.GradientButton">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="Brush.xaml"></ResourceDictionary>
    </ResourceDictionary.MergedDictionaries>
    <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>

    <Style TargetType="{x:Type Button}">
        <Setter Property="Control.Template" Value="{StaticResource GradientButtonTemplate}"></Setter>
    </Style>
</ResourceDictionary>

通过样式应用模板

这种设计存在局限性,控件模板本质上硬编码了一些细节,如颜色方案。这意味着如果希望在按钮中使用相同的元素组合(Border、Grid、Rectangle和ContentPresenter)并采用相同的方式安排他们,但希望提供不同的颜色方案,就必须创建引用不同画刷资源的新模板副本。

这未必是个问题(毕竟,布局和格式化细节可能紧密相关,以至于不希望以任何方式隔断它们)。但这确实限制可重用控件模板的能力。如果模板使用了元素的复合排列方式,并且希望重用这些具有各种不同格式化细节(通常是颜色和字体)的元素,可从模板中将这些细节提取出来,并将它们放到样式中。

为此,需要重新编写模板。这次不能使用硬编码的颜色,而需要使用模板绑定从控件属性中提取出信息。下面示例为前面特殊按钮定义了一个精简模板。控件模板将一些细节作为基础的固定要素------焦点框和两个单位宽的圆角边框。唯一需要保留的触发器是显示焦点框的那个触发器。

Resources/CustomButton.xaml

cs 复制代码
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="Brush.xaml"></ResourceDictionary>
    </ResourceDictionary.MergedDictionaries>
    <ControlTemplate x:Key="CustomButtonTemplate" TargetType="{x:Type Button}">
        <Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="true">
            <Grid>
                <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="AliceBlue" StrokeThickness="2" StrokeDashArray="1 2" SnapsToDevicePixels="true"></Rectangle>
                <ContentPresenter x:Name="contentPresenter" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}"
                                  RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
            </Grid>
        </Border>
        <ControlTemplate.Triggers>
            <Trigger Property="IsKeyboardFocused" Value="true">
                <Setter Property="Visibility" TargetName="FocusCue" Value="Visible"/>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
    <Style TargetType="{x:Type Button}">
        <Setter Property="Control.Template" Value="{StaticResource CustomButtonTemplate}"></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}"></Setter>
            </Trigger>
            <Trigger Property="IsPressed" Value="True">
                <Setter Property="Background" Value="{StaticResource PressedBackground}"></Setter>
            </Trigger>
            <Trigger Property="IsEnabled" Value="False">
                <Setter Property="Background" Value="{StaticResource DisabledBackground}"></Setter>
            </Trigger>
        </Style.Triggers>
    </Style>
</ResourceDictionary>

理想情况下,应能在控件模板中保留所有触发器,因为它们代表控件的行为,并使用样式简单设置基本属性。但在此如果希望样式能够设置颜色方案,是不可能实现的。如果在控件模板和样式中都设置了触发器,那么样式触发器具有优先权。

由用户选择的皮肤

在一些应用程序中,可能希望动态改变模板,通常是根据用户的个人爱好加以改变。这很容易实现,但文档中没有对此进行详细说明。基本技术是在运行时加载新的资源字典,并使用新加载的资源字典代替当前的资源字典(不需要替换所有资源,只需要替换那些用于皮肤的资源)。

诀窍在于检索ResourceDictionary对象,该对象经过编译并作为资源嵌入到应用程序中。最简单的方法是使用ResourceManager类来加载所需资源。

例如,前面已经创建了一个按钮控件模板,保存在GradientButton.xaml文件中,现在创建另一个按钮控件模板保存在 GradientButton.Variant.xaml中,这两个文件都位于Resources文件夹中。

Resources/Brush.Variant.xmal

cs 复制代码
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <RadialGradientBrush RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3" x:Key="HighlightBackground">
        <GradientStop Color="White" Offset="0" />
        <GradientStop Color="Green" Offset=".4" />
    </RadialGradientBrush>
    <RadialGradientBrush RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3" x:Key="PressedBackground">
        <GradientStop Color="White" Offset="0" />
        <GradientStop Color="Green" Offset="1" />
    </RadialGradientBrush>
    <SolidColorBrush Color="DarkGreen" 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="Green" Offset="1" />
    </RadialGradientBrush>
</ResourceDictionary>

Resources/GradientButton.Variant.xaml

cs 复制代码
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="TestControlTemplate.Resources.GradientButtonVariant">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="Brush.Variant.xaml"></ResourceDictionary>
    </ResourceDictionary.MergedDictionaries>
    <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>
    <Style TargetType="{x:Type Button}">
        <Setter Property="Control.Template" Value="{StaticResource GradientButtonTemplate}"></Setter>
    </Style>
</ResourceDictionary>

现在在一个容器中使用两个按钮控件模板中的一个,比如 GradientButton.xaml

cs 复制代码
        <StackPanel Name="skinStackPanel">
            <StackPanel.Resources>
                <ResourceDictionary>
                    <ResourceDictionary.MergedDictionaries>
                        <ResourceDictionary Source="Resources/GradientButton.xaml"></ResourceDictionary>
                    </ResourceDictionary.MergedDictionaries>
                </ResourceDictionary>
            </StackPanel.Resources>
            <Button Margin="10" Padding="5">A Simple Button with a Custom Template</Button>
            <Button Margin="10" Padding="5">Another Button with a Custom Template</Button>
            <Button Margin="10" Padding="5">A _Third Button with a Custom Template</Button>
            <Button Margin="10" Padding="5" IsEnabled="False" >A Disabled Button</Button>
            <CheckBox Margin="10" Checked="chkGreen_Checked" Unchecked="chkGreen_Unchecked">Use Alternate Theme</CheckBox>
        </StackPanel>

通过CheckBox的Checked和Unchecked事件处理程序更换皮肤:

cs 复制代码
    private void chkGreen_Checked(object sender, RoutedEventArgs e)
    {
        ResourceDictionary resourceDictionary = new ResourceDictionary();
        resourceDictionary.Source = new Uri("Resources/GradientButton.Variant.xaml", UriKind.Relative);
        skinStackPanel.Resources.MergedDictionaries[0] = resourceDictionary;
    }
    private void chkGreen_Unchecked(object sender, RoutedEventArgs e)
    {
        ResourceDictionary resourceDictionary = new ResourceDictionary();
        resourceDictionary.Source = new Uri("Resources/GradientButton.xaml", UriKind.Relative);
        skinStackPanel.Resources.MergedDictionaries[0] = resourceDictionary;
    }

上面代码加载GradientButton.Variant.xaml资源字典,并将它放置到MergedDictionaries集合的第一个位置。在此没有清空MergedDictionaries集合或其他任何窗口资源,因为您可能连接到了其他希望继续使用的资源字典。也没有为MergedDictionaries集合添加新条目,因为这可能与位于不同集合中的同名资源发生冲突。

如果正在为整个应用程序改变皮肤,可使用相同的方法,但应使用应用程序资源字典。还可以使用pack URI语法加载在另一个程序集中定义的资源字典:

cs 复制代码
        ResourceDictionary resourceDictionary = new ResourceDictionary();
        resourceDictionary.Source = new Uri("ControlTemplateLibrary;component/GradientButton.Variant.xaml", UriKind.Relative);
        skinStackPanel.Resources.MergedDictionaries[0] = resourceDictionary;

当加载新的资源字典时,会自动使用新模板更新所有按钮。如果当修改控件时不需要完全改变皮肤,还可以为皮肤提供基本样式。

这里GradientButton.xaml 和 GradientButton.Variant.xaml 资源使用元素类型样式自动改变按钮。还有一种方法------可通过手动设置Button对象的Template 或 Style 属性来选用新的模板。如果使用这种方法,务必使用Dynamic Resource引用,而不能使用StaticResource。如果使用StaticResource,当切换皮肤时不会更新按钮模板。

当使用DynamicResource引用时,首先要保证所需要的资源位于资源层次结构中。如果资源并不位于资源层次结构中,就会忽略资源。而且按钮会恢复为它们的标准外观,而不会生成错误。

还有一种通过编写代码加载资源字典的方法。可使用与为窗口创建代码隐藏类几乎相同的方法,为资源字典创建代码隐藏类。然后就可以直接实例化这个类,而不是使用ResourceDictionary.Source 属性。这种方法有一个优点,它是强类型的(没有机会为Source属性输入无效的URI),并且可为资源类添加属性、方法以及其他功能。例如,可以使用这种方法为自定义窗口模板创建具有事件处理代码的资源。

尽管为资源字典创建代码隐藏类很容易,但是VisualStudio并不能自动完成该工作。需要为继承自ResourceDictionary的部分类添加代码文件,并在构造函数中调用InitializeComponent() 方法:

Resources/GradientButton.xaml.cs

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;

namespace TestControlTemplate.Resources;

public partial class GradientButton : ResourceDictionary
{
    public GradientButton()
    {
        InitializeComponent();
    }
}

Resources/GradientButton.Variant.xaml.cs

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;

namespace TestControlTemplate.Resources;

public partial class GradientButtonVariant : ResourceDictionary
{
    public GradientButtonVariant()
    {
        InitializeComponent();
    }
}

需要在对应的资源字典中添加Class属性:

cs 复制代码
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="TestControlTemplate.Resources.GradientButton">
    ...
</ResourceDictionary>
cs 复制代码
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="TestControlTemplate.Resources.GradientButtonVariant">
    ...
</ResourceDictionary>

现在可使用该代码创建资源字典并将它应用于窗口:

cs 复制代码
    private void chkGreen2_Checked(object sender, RoutedEventArgs e)
    {
        GradientButtonVariant gradientButtonVariant = new GradientButtonVariant();
        skinStackPanel2.Resources.MergedDictionaries[0] = gradientButtonVariant;
    }
    private void chkGreen2_Unchecked(object sender, RoutedEventArgs e)
    {
        GradientButton gradientButton = new GradientButton();
        skinStackPanel2.Resources.MergedDictionaries[0] = gradientButton;
    }

测试代码文件清单:

除了创建工程自动生成的:App.xaml、App.xaml.cs、AssemblyInfo.cs外,

在前面已经列出了: VisualTreeDisplay.xaml、VisualTreeDisplay.xaml.cs、ControlTemplateDisplay.xaml、ControlTemplateDisplay.xaml.cs、Resource/Brush.xaml、Resource/Brush.Variant.xaml、Resource/CustomButton.xaml、Resource/GradientButton.xaml、Resource/GradientButton.xaml.cs、Resource/GradientButton.Variant.xaml、Resource/GradientButton.Variant.xaml.cs

还剩下主窗口:

MainWindow.xaml

cs 复制代码
<Window x:Class="TestControlTemplate.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestControlTemplate"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <SolidColorBrush x:Key="Button.MouseOver.Background" Color="#FFBEE6FD"/>
        <SolidColorBrush x:Key="Button.MouseOver.Border" Color="#FF3C7FB1"/>
        <SolidColorBrush x:Key="Button.Pressed.Background" Color="#FFC4E5F6"/>
        <SolidColorBrush x:Key="Button.Pressed.Border" Color="#FF2C628B"/>
        <SolidColorBrush x:Key="Button.Disabled.Background" Color="#FFF4F4F4"/>
        <SolidColorBrush x:Key="Button.Disabled.Border" Color="#FFADB2B5"/>
        <SolidColorBrush x:Key="Button.Disabled.Foreground" Color="#FF838383"/>
        <ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            <Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="true">
                <ContentPresenter x:Name="contentPresenter" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}"
                                  RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsDefaulted" Value="true">
                    <Setter Property="BorderBrush" TargetName="border" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
                </Trigger>
                <Trigger Property="IsMouseOver" Value="true">
                    <Setter Property="Background" TargetName="border" Value="{StaticResource Button.MouseOver.Background}"/>
                    <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.MouseOver.Border}"/>
                </Trigger>
                <Trigger Property="IsPressed" Value="true">
                    <Setter Property="Background" TargetName="border" Value="{StaticResource Button.Pressed.Background}"/>
                    <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.Pressed.Border}"/>
                </Trigger>
                <Trigger Property="IsEnabled" Value="false">
                    <Setter Property="Background" TargetName="border" Value="{StaticResource Button.Disabled.Background}"/>
                    <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.Disabled.Border}"/>
                    <Setter Property="TextElement.Foreground" TargetName="contentPresenter" Value="{StaticResource Button.Disabled.Foreground}"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>

        <ControlTemplate x:Key="ButtonTemplate2" TargetType="{x:Type Button}">
            <Border Name="border" Background="Red" BorderBrush="Orange" BorderThickness="3" CornerRadius="2" TextBlock.Foreground="White">
                <ContentPresenter RecognizesAccessKey="True" Margin="{TemplateBinding Padding}"></ContentPresenter>
            </Border>
        </ControlTemplate>
        <ControlTemplate x:Key="ButtonTemplate3" TargetType="{x:Type Button}">
            <Border Name="border" Background="Red" BorderBrush="Orange" BorderThickness="3" CornerRadius="2" TextBlock.Foreground="White">
                <Grid>
                    <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Blue" StrokeThickness="1" StrokeDashArray="2 2" SnapsToDevicePixels="True" ></Rectangle>
                    <ContentPresenter RecognizesAccessKey="True" Margin="{TemplateBinding Padding}"></ContentPresenter>
                </Grid>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter TargetName="border" Property="Background" Value="DarkRed"></Setter>
                </Trigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="border" Property="Background" Value="IndianRed"></Setter>
                    <Setter TargetName="border" Property="BorderBrush" Value="DarkKhaki"></Setter>
                </Trigger>
                <Trigger Property="IsKeyboardFocused" Value="True">
                    <Setter TargetName="FocusCue" Property="Visibility" Value="Visible" />
                </Trigger>
                <Trigger Property="IsEnabled" Value="False">
                    <Setter TargetName="border" Property="TextBlock.Foreground" Value="Gray" />
                    <Setter TargetName="border" Property="Background" Value="MistyRose" />
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>

        <ControlTemplate x:Key="ButtonTemplate4" TargetType="{x:Type Button}">
            <Border Name="border" Background="Red" BorderBrush="Orange" BorderThickness="3" CornerRadius="2" TextBlock.Foreground="White">
                <Grid>
                    <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Blue" StrokeThickness="1" StrokeDashArray="2 2" SnapsToDevicePixels="True" ></Rectangle>
                    <ContentPresenter RecognizesAccessKey="True" Margin="{TemplateBinding Padding}"></ContentPresenter>
                </Grid>
            </Border>
            <ControlTemplate.Triggers>
                <EventTrigger RoutedEvent="MouseEnter">
                    <BeginStoryboard>
                        <Storyboard>
                            <ColorAnimation Storyboard.TargetName="border" Storyboard.TargetProperty="Background.Color"
                                            To="Blue" Duration="0:0:1" AutoReverse="True" RepeatBehavior="Forever"></ColorAnimation>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
                <EventTrigger RoutedEvent="MouseLeave">
                    <BeginStoryboard>
                        <Storyboard>
                            <ColorAnimation Storyboard.TargetName="border" Storyboard.TargetProperty="Background.Color" Duration="0:0:0.5"></ColorAnimation>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="border" Property="Background" Value="IndianRed" />
                    <Setter TargetName="border" Property="BorderBrush" Value="DarkKhaki" />
                </Trigger>
                <Trigger Property="IsKeyboardFocused" Value="True">
                    <Setter TargetName="FocusCue" Property="Visibility" Value="Visible" />
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </Window.Resources>
    <StackPanel>
        <Button Click="ShowVisualTree_Click">ShowVisualTree</Button>
        <Button Click="ShowControlTemplate_Click">ShowControlTemplate</Button>
        <Button Template="{StaticResource ButtonTemplate}">ButtonTemplate</Button>
        <Button Style="{x:Null}" Template="{StaticResource ButtonTemplate2}" Padding="5" HorizontalContentAlignment="Center">ButtonTemplate2</Button>
        <Button Template="{StaticResource ButtonTemplate3}" Padding="5" HorizontalContentAlignment="Center">ButtonTemplate3</Button>

        <Button Template="{StaticResource ButtonTemplate4}" Padding="5" HorizontalContentAlignment="Center">ButtonTemplate4</Button>
        <Button Content="GradientButtonTemplate">
            <Button.Resources>
                <ResourceDictionary>
                    <ResourceDictionary.MergedDictionaries>
                        <ResourceDictionary Source="Resources/GradientButton.xaml"></ResourceDictionary>
                    </ResourceDictionary.MergedDictionaries>
                </ResourceDictionary>
            </Button.Resources>
        </Button>
        <Button Content="CustomButtonTemplate">
            <Button.Resources>
                <ResourceDictionary>
                    <ResourceDictionary.MergedDictionaries>
                        <ResourceDictionary Source="Resources/CustomButton.xaml"></ResourceDictionary>
                    </ResourceDictionary.MergedDictionaries>
                </ResourceDictionary>
            </Button.Resources>
        </Button>
        <StackPanel Name="skinStackPanel">
            <StackPanel.Resources>
                <ResourceDictionary>
                    <ResourceDictionary.MergedDictionaries>
                        <ResourceDictionary Source="Resources/GradientButton.xaml"></ResourceDictionary>
                    </ResourceDictionary.MergedDictionaries>
                </ResourceDictionary>
            </StackPanel.Resources>
            <Button Margin="10" Padding="5">A Simple Button with a Custom Template</Button>
            <Button Margin="10" Padding="5">Another Button with a Custom Template</Button>
            <Button Margin="10" Padding="5">A _Third Button with a Custom Template</Button>
            <Button Margin="10" Padding="5" IsEnabled="False" >A Disabled Button</Button>
            <CheckBox Margin="10" Checked="chkGreen_Checked" Unchecked="chkGreen_Unchecked">Use Alternate Theme</CheckBox>
        </StackPanel>

        <StackPanel Name="skinStackPanel2">
            <StackPanel.Resources>
                <ResourceDictionary>
                    <ResourceDictionary.MergedDictionaries>
                        <ResourceDictionary Source="Resources/GradientButton.xaml"></ResourceDictionary>
                    </ResourceDictionary.MergedDictionaries>
                </ResourceDictionary>
            </StackPanel.Resources>
            <Button Margin="10" Padding="5">A Simple Button with a Custom Template</Button>
            <Button Margin="10" Padding="5">Another Button with a Custom Template</Button>
            <Button Margin="10" Padding="5">A _Third Button with a Custom Template</Button>
            <Button Margin="10" Padding="5" IsEnabled="False" >A Disabled Button</Button>
            <CheckBox Margin="10" Checked="chkGreen2_Checked" Unchecked="chkGreen2_Unchecked">Use Alternate Theme</CheckBox>
        </StackPanel>
    </StackPanel>
</Window>

MainWindow.xaml.cs

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using TestControlTemplate.Resources;

namespace TestControlTemplate;

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void ShowVisualTree_Click(object sender, RoutedEventArgs e)
    {
        VisualTreeDisplay treeDisplay = new VisualTreeDisplay();
        treeDisplay.ShowVisualTree(this);
        treeDisplay.Show();
    }
    private void ShowControlTemplate_Click(object sender, RoutedEventArgs e)
    {
        ControlTemplateDisplay controlTemplate = new ControlTemplateDisplay();
        controlTemplate.Show();
    }

    private void chkGreen_Checked(object sender, RoutedEventArgs e)
    {
        ResourceDictionary resourceDictionary = new ResourceDictionary();
        resourceDictionary.Source = new Uri("Resources/GradientButton.Variant.xaml", UriKind.Relative);
        skinStackPanel.Resources.MergedDictionaries[0] = resourceDictionary;
    }
    private void chkGreen_Unchecked(object sender, RoutedEventArgs e)
    {
        ResourceDictionary resourceDictionary = new ResourceDictionary();
        resourceDictionary.Source = new Uri("Resources/GradientButton.xaml", UriKind.Relative);
        skinStackPanel.Resources.MergedDictionaries[0] = resourceDictionary;
    }
    private void chkGreen2_Checked(object sender, RoutedEventArgs e)
    {
        GradientButtonVariant gradientButtonVariant = new GradientButtonVariant();
        skinStackPanel2.Resources.MergedDictionaries[0] = gradientButtonVariant;
    }
    private void chkGreen2_Unchecked(object sender, RoutedEventArgs e)
    {
        GradientButton gradientButton = new GradientButton();
        skinStackPanel2.Resources.MergedDictionaries[0] = gradientButton;
    }
}
相关推荐
当下就是最好1 小时前
WPF应用程序的生命周期-笔记
wpf
九鼎科技-Leo15 小时前
什么是 WPF 中的依赖属性?有什么作用?
windows·c#·.net·wpf
麻花20131 天前
C#之WPF的C1FlexGrid空间的行加载事件和列事件变更处理动态加载的枚举值
开发语言·c#·wpf
lcintj1 天前
【WPF】Prism学习(九)
学习·wpf·prism
界面开发小八哥1 天前
界面控件DevExpress WPF中文教程:网格视图数据布局的列和卡片字段
wpf·界面控件·devexpress·ui开发·用户界面
△曉風殘月〆1 天前
如何在WPF中嵌入其它程序
wpf
Crazy Struggle1 天前
功能齐全的 WPF 自定义控件资源库(收藏版)
.net·wpf·ui控件库
shepherd枸杞泡茶2 天前
WPF动画
c#·.net·wpf
lcintj2 天前
【WPF】Prism学习(十)
学习·wpf·prism
wyh要好好学习2 天前
WPF数据加载时添加进度条
ui·wpf