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
<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
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类的模板:
<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
<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
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引用该资源。
<Button Template="{StaticResource ButtonTemplate}">ButtonTemplate</Button>
通过这种方法,不仅可以较容易地创建许多自定义按钮,在以后还可以很灵活地修改控件模板,而不会扰乱应用程序用户界面的其余部分。
要为基本按钮创建模板,需要绘制边框和背景,然后在按钮中放置内容。绘制边框的两种可选方法是使用Rectangle 和 Border 类。这里使用Border类,将具有圆角的桔色轮廓与红色背景和白色文本结合在一起。此外,还应该包含一个 ContentPresenter元素,所有内容控件都需要ContentPresenter元素——它是表示“在此插入内容”的标记器,告诉WPF在何处保存内容:
<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元素周围创建外边距:
<ContentPresenter RecognizesAccessKey="True" Margin="{TemplateBinding Padding}"></ContentPresenter>
这样就会得到所期望的效果,在边框和内容之间添加了一些空白。
模板绑定和普通的数据绑定类似,但它们的量级更轻,因为它们是专门针对在控件模板中使用而设计的。它们只支持单向数据绑定(换句话说,它们可从控件向模板传递信息,但不能从模板向控件传递信息),并且不能用于从Freezable类的派生类的属性中提取信息。如果遇到模板绑定不生效的情形,可改用具有完整功能的数据绑定。
模板绑定支持WPF的变化监测基础结构,所有依赖项属性都包含该基础结构。这意味着如果修改了控件的属性,模板会自动考虑该变化。当使用在一小段时间内重复改变属性值的动画时,该细节尤其有用。
预计需要哪些模板绑定的唯一方法是检查默认控件模板。如果查看Button类的控件模板,就会发现在模板绑定的使用方法上,与自定义模板是完全相同的——获取为按钮指定的内边距,并将它转换为ContentPresenter元素周围的外边距。还会发现标准按钮模板包含另外几个模板绑定,如HorizontalAlignment、VerticalAlignment以及Background,这个简单的自定义模板中没有使用这些模板绑定。这意味着如果为按钮设置了这些属性,对于这个简单的自定义模板来说,这些设置没有效果。
改变属性触发器
如果测试上面创建的按钮,就会发现它令人非常失望。本质上,它不过是一个红色的圆角矩形——当在它上面移动鼠标或单击鼠标时,其外观没有任何反应。
可通过为控件模板添加触发器来方便的解决这个问题。
<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元素,这两个元素位于同一个单元格中。
<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时,该触发器改变按钮的背景色。为确保该规则优于其它相冲突的触发器设置,应当在触发器列表的末尾定义它。
模板与样式
模板与样式有类似之处。通常,在整个应用程序中,这两个特性都可以改变元素的外观。然而,样式被限制在一个小得多的范围之内。它们可调整控件的属性,但不能使用全新的由不同元素组成的可视化树替代控件原来的外观。
在前面看到的简单按钮包含了一些仅凭样式无法实现的特性。尽管可使用样式设置按钮的背景色,但当按下按钮时调整按钮的背景色会遇到更多麻烦,因为按钮的内置模板已经针对该目的提供了一个触发器。另外,也不能很方便的添加焦点矩形。
还可以通过控件模板实现许多特殊类型的按钮,如果使用样式,是无法获得此类效果的。例如,不是使用矩形边框,而是创建类似椭圆形状的按钮,或使用路径绘制更复杂的形状。其余的标记——甚至是用于在不同状态之间切换背景色的触发器——基本上不需要加以修改。
使用动画触发器
触发器并非仅局限于设置属性,当特定属性发生变化时,还可以使用事件触发器运行动画。
<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
<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
<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
<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
<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
<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
<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事件处理程序更换皮肤:
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语法加载在另一个程序集中定义的资源字典:
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
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
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属性:
<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>
<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>
现在可使用该代码创建资源字典并将它应用于窗口:
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
<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
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;}
}
相关文章:
WPF控件模板
在过去,Windows开发人员必须在方便性和灵活性之间做出选择。为得到最大的方便性,他们可以使用预先构建好的控件。这些控件可以工作的足够好,但可定制性十分有限,并且几乎总是具有固定的可视化外观。偶尔,某些控件提供了…...
vue移动端页面适配
页面的适配,就是一个页面能在PC端正常访问,同时也可以在移动端正正常访问。 现在我们可以通过弹性布局【Flexible布局】、媒体查询和响应式布局。除此之外,还可以通过rem和vw针对性地解决页面适配问题。 响应式布局 响应式布局的核心&…...
Ei Scopus 双检索 |第三届信息与通信工程国际会议国际会议(JCICE 2024)
会议简介 Brief Introduction 2024年第三届信息与通信工程国际会议国际会议(JCICE 2024) 会议时间:2024年5月10日-12日 召开地点:中国福州 大会官网:JCICE 2024-2024 International Joint Conference on Information and Communication Engin…...
ChatGPT实战-Embeddings打造定制化AI智能客服
本文介绍Embeddings的基本概念,并使用最少但完整的代码讲解Embeddings是如何使用的,帮你打造专属AI聊天机器人(智能客服),你可以拿到该代码进行修改以满足实际需求。 ChatGPT的Embeddings解决了什么问题? …...
C语言指针,深度长文全面讲解
指针对于C来说太重要。然而,想要全面理解指针,除了要对C语言有熟练的掌握外,还要有计算机硬件以及操作系统等方方面面的基本知识。所以本文尽可能的通过一篇文章完全讲解指针。 为什么需要指针? 指针解决了一些编程中基本的问题。…...
云桌面打开部署在linux的服务特别卡 怎么解决
云桌面打开部署在 Linux 服务器上的服务卡顿可能是由多种因素引起的,包括服务器性能、网络连接、应用程序配置等。以下是一些可能的解决方法,可以帮助您缓解云桌面访问部署在 Linux 服务器上的服务时的卡顿问题: 优化服务器性能: …...
day5ARM
循环点亮三个led灯 方法1 ------------------led.h---------------- #ifndef __LED_H__ #define __LED_H__#define RCC (*(volatile unsigned int *)0x50000A28) #define GPIOE ((GPIO_t *)0x50006000) #define GPIOF ((GPIO_t *)0x50007000)//结构体封装 typedef struct {vo…...
旋转链表-双指针思想-LeetCode61
题目要求:给定链表的头结点,旋转链表,将链表每个节点向右移动K个位置。 示例: 输入:head [1,2,3,4,5], k2 输出:[4,5,1,2,3] 双指针思想: 先用双指针策略找到倒数K的位置,也就是(…...
使用自定义XML配置文件在.NET桌面程序中保存设置
本文将详细介绍如何在.NET桌面程序中使用自定义的XML配置文件来保存和读取设置。除了XML之外,我们还将探讨其他常见的配置文件格式,如JSON、INI和YAML,以及它们的优缺点和相关的NuGet类库。最后,我们将重点介绍我们为何选择XML作为…...
1787_函数指针的使用
全部学习汇总:GitHub - GreyZhang/c_basic: little bits of c. 前阵子似乎写了不少错代码,因为对函数指针的理解还不够。今天晚上似乎总算是梳理出了一点眉目,在先前自己写过的代码工程中做一下测试。 先前实现过一个归并排序算法,…...
解决nomachine扫描不出ip问题
IP扫描工具Advanced IP Scanner 快速的扫描局域网中存在ip地址以及pc机的活跃状态,还能列出局域网计算机的相关信息。并且ip扫描工具(Advanced IP Scanner)还能够单击访问更多有用的功能- 远程关机和唤醒 软件下载地址...
Web 3.0 发展到什么水平了?
最初,有互联网:电线和服务器的物理基础设施,让计算机和它们前面的人相互交谈。美国政府的阿帕网在1969年发出了第一条消息,但我们今天所知道的网络直到1991年才出现,当时HTML和URL使用户可以在静态页面之间导航。将此视…...
大模型:如何利用旧的tokenizer训练出一个新的来?
背景: 我们在用chatGPT或者SD的时候,发现如果使用英语写提示词得到的结果比我们使用中文得到的结果要好很多,为什么呢?这其中就有一个叫做tokenizer的东西在作怪。 训练一个合适的tokenizer是训练大模型的基础,我们既…...
【LeetCode-中等题】107. 二叉树的层序遍历 II
文章目录 题目方法一:队列层序迭代 题目 方法一:队列层序迭代 解题详情:【LeetCode-中等题】102. 二叉树的层序遍历 res.add(0,zres); //效果是将 zres 列表作为 res 的第一个子列表,并将其它原本在第一位置及之后的子列表向后移…...
斯坦福联合培养博士|专科生的逆袭之路
从山东医学高等专科学校到首都医科大学附属北京天坛医院神经外科博士,再到斯坦福医学院神经外科联合培养博士,知识人网小编带大家看看何世豪通往成功的逆袭之路。 上面照片中这位戴眼镜的主人公就是何志豪,他从山东医学高等专科学校考入泰山医…...
Verilog中parameter在仿真时的应用
parameter能够定义一个常量 例如 parameter [7:0]A 8d123; 在仿真时我们可以用它来改变模块的参数,而不会影响综合的结果。 考虑下面的模块,输入时钟是clk,频率为24MHz,输出一个1Hz的方波驱动小灯让其闪烁 module test1(in…...
v-model绑定导致的element UI文本框输入第一次值后被绑定,导致空文本框无法再输入文字
在工作岗位上,上边分配一个任务,创建一个页面,从0-1,全部自己搭建,也没有啥模版,就这么来,那就直接来吧,没办法,那就直接上手,开发过程中,我使用了…...
数据结构——KD树
KD树(K-Dimensional Tree)是一种用于多维空间的二叉树数据结构,旨在提供高效的数据检索。KD树在空间搜索和最近邻搜索等问题中特别有用,允许在高维空间中有效地搜索数据点。 重要性质 1.分割K维数据空间的数据结构 2.是一颗二叉树…...
python趣味编程-恐龙克隆游戏
Python 中使用 Turtle 的恐龙克隆游戏免费源代码 使用 Turtle 的恐龙克隆游戏是一个用Python编程语言编码的桌面游戏应用程序。该项目包含在 Chrome 浏览器中克隆实际恐龙游戏的多种功能。该项目可以使正在修读 IT 相关课程的学生受益。这个应用程序非常有趣,可以帮助您学习创…...
【漏洞复现】泛微e-office OfficeServer2.php 存在任意文件读取漏洞复现
文章目录 前言声明一、漏洞描述二、漏洞分析三、漏洞复现四、修复建议前言 泛微e-office OfficeServer2.php 存在任意文件读取漏洞,攻击者可通过构造特定Payload获取敏感数据信息。 声明 请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息或者工具而造…...
基于Yolov8的野外烟雾检测(4):通道优先卷积注意力(CPCA),效果秒杀CBAM和SE等 | 中科院2023最新发表
目录 1.Yolov8介绍 2.野外火灾烟雾数据集介绍 3.CPCA介绍 3.1 CPCA加入到yolov8 4.训练结果分析 5.系列篇 1.Yolov8介绍 Ultralytics YOLOv8是Ultralytics公司开发的YOLO目标检测和图像分割模型的最新版本。YOLOv8是一种尖端的、最先进的(SOTA)模型&a…...
程序员必掌握的核心算法:提升编程技能的关键路径
一:引言 作为程序员,算法是我们编程生涯中的灵魂。算法是解决问题的方法和步骤,它们在计算机科学中扮演着至关重要的角色。无论你是初学者还是经验丰富的专业人士,都需要掌握一些核心算法,因为它们在各种应用场景中频…...
面试算法10:和为k的子数组
题目 输入一个整数数组和一个整数k,请问数组中有多少个数字之和等于k的连续子数组?例如,输入数组[1,1,1],k的值为2,有2个连续子数组之和等于2。 分析 在从头到尾逐个扫描数组中的数字时求出前…...
王道考研操作系统
王道考研操作系统 计算机系统概述操作系统的概念操作系统的特征操作系统的发展历程操作系统内核中断和异常![在这里插入图片描述](https://img-blog.csdnimg.cn/162452b4c60144e0bd500e180127c447.png)系统调用操作系统结构虚拟机错题 进程与线程进程控制进程通信线程和多线程模…...
HEXO 基本使用
1 新建、编辑并预览文章 1. 新建文章 hexo new [layout] title # 或 hexo n [layout] title创建文章前要先选定模板,在hexo中也叫做布局。hexo支持三种布局(layout):post(默认)、draft、page。我们先介绍如何使用已有布局…...
Webpack Sourcemap文件泄露漏洞
Webpack Sourcemap文件泄露漏洞 前言一、Webpack和Sourcemap1.1 什么是Webpack1.2 什么是Sourcemap二、漏洞利用2.1 使用reverse-sourcemap工具2.1 直接看前端代码三、漏洞挖掘漏洞修复前言 Webpack主要是用于前端框架进行打包的工具,打包后形成.js.map文件,如果.js.map文件…...
WebGL层次模型——单节点模型
目录 多个简单模型组成的复杂模型 层次结构模型 单关节模型 JointModel程序中模型的层次结构 示例程序(JointMode.js) 代码详解 绘制层次模型(draw()) 程序效果 多个简单模型组成的复杂模型 绘制…...
【链表】反转链表 II-力扣 92 题
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kuan 的首页,持续学…...
【考研数学】高等数学第六模块 —— 空间解析几何(1,向量基本概念与运算)
文章目录 引言一、空间解析几何的理论1.1 基本概念1.2 向量的运算 写在最后 引言 我自认空间想象能力较差,所以当初学这个很吃力。希望现在再接触,能好点。 一、空间解析几何的理论 1.1 基本概念 1.向量 —— 既有大小,又有方向的量称为向…...
巨人互动|Facebook海外户Facebook客户反馈分数
Facebook客户反馈分数是一项用于衡量用户对Facebook产品和服务满意度的指标。该指标被广泛应用于各种调研和评估活动,帮助Facebook了解用户对其平台和功能的意见和建议,并从中识别出改进的机会。 巨人互动|Facebook海外户&Facebook新闻提要的算法&am…...
东营做网站优化的公司/网络营销课程
欢迎来到我的《从源码中学Vue》专题系列文章,更多精彩内容持续更新中,欢迎关注 :)上一章节我们通过源码分析了Vue中的methods对象下的方法是如何挂载到vm下,以及各方法内部的this为何是指向了vm对象。其实在Vue中,还有…...
网站top排行榜/网页推广怎么做的
基本配置: Configuration public class RestConfig {Beanpublic RestClient getClient() throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {// 如果有多个从节点可以持续在内部new多个HttpHost,参数1是ip,参数2是HTTP端口…...
全包家装原创装修网站/怎么做百度网页
美食节 题解:学习了动态加边,可以说是进一步理解了网络流。具体思路就是考虑每一道菜,如果这是该位厨师最后一次做,那么等待时间就是做这道菜的时间,如果是倒数第二次做,就要两倍时间(目前做了一…...
网站制作的文章/国内永久免费的云服务器
原标题:TVS管特性曲线、参数说明及应用TVS管的英文名是TRANSIENT VOLTAGE SUPPRESSOR,中文名叫瞬变抑制。它在承受瞬间高能量脉冲时,能在极短的时间内由原来的高阻抗状态变为低阻抗,并把电压箝制到特定的水平,从而有效…...
在广告公司上班都干嘛/青岛自动seo
内存分配:1. 栈区:栈可分为Java虚拟机和本地方法栈2. 堆区:堆被所有线程共享,在虚拟机启动时创建,是唯一的目的是存放对象实例,是gc的主要区域。通常可分为两个区块年轻代和年老代。更新一点年轻代可分为Ed…...
网站怎么做显得简洁美观/交换链接的方法
大数据实时计算工程师/Hadoop工程师/数据分析师职业路线图 描述本路线图是一个专门针对大数据实时处理、Hadoop工程师和数据分析师所设计的课程体系介绍,在实时计算方向主要包括了从数据收集框架、集群协调框架、数据缓存框架到实时计算框架都全面进行深度解析&…...