WPF 入门笔记 - 03 - 样式基础及控件模板
原学习路线是按照圣殿骑士的《WPF基础到企业应用系列》的路线走的,但是布局之后直接依赖属性学起来有些僵硬,不太好理解,尝试了文章的前部分内容后放弃,调整为本篇博文内容。笔记路线将按照痕迹g给出的《WPF基础入门总结》中提及的路线进行,实际以我自己的学习过程为主。
从按钮、文本框到下拉框、列表框,WPF提供了一系列常用控件,每个控件都有自己独特的特性和用途。通过灵活的布局容器,如网格、堆栈面板和换行面板,我们可以将这些控件组合在一起,实现复杂的界面布局。而通过样式和模板,我们可以轻松地定制控件的外观和行为,以符合我们的设计需求。
程序的本质 - 数据结构 + 算法
概述
在WPF中UI(User Interface)是通过数据来驱动的,数据是核心,UI从属于数据并表达数据,这和传统的windows图形界面开发(比如``Winform)有很大的区别。WPF中能够展示数据、响应用户操作的UI`元素称为控件(Control)- 数据和行为的载体,它们被设计成总是无外观的(lookless)。控件中展示的内容称之为“数据内容”,响应用户操作后执行的方法或事件(Event)称之为“行为”。
WPF革命性的概念就是把控件的特性和控件的显示方式分开。
控件在用户界面上的样子是由控件模板决定的,WPF为每个控件提供了默认的控件模板和相应的特性,但用户可以用自己的控件模板来替换WPF提供的控件模板,每个控件都可以成为开发者自己个性化的控件。
在WPF中,有两个类似的类继承树:一个与界面(UI)相关,一个与内容(Content)相关,这种分离设计使得WPF能够更好地处理UI元素和内容元素的不同需求。
UI元素的类继承树以UIElement为基础,它是所有可视化UI元素的基类,可以理解为控件。UIElement提供了处理输入事件、布局、渲染等UI相关功能的基本支持。从UIElement派生出了FrameworkElement,它进一步扩展了UI元素的功能,包括数据绑定、样式、模板等。而Control类则是FrameworkElement的子类,它提供了一些常见控件的默认外观和行为。内容元素的类继承树以
ContentElement为基础,它用于处理内容相关的功能,例如文本内容的显示和处理。与之相对应的是FrameworkContentElement,它从ContentElement派生出来,提供了更多的内容相关功能。
需要注意的是,ContentElement与内容控件(Content Controls)是不同的概念:ContentElement主要用于处理文本内容,而内容控件则是一种控件,用于展示和管理单个内容元素。
比着痕迹g的图画的:

WPF的类继承树包括UI元素和内容元素两个分支,它们分别满足了界面和内容的不同需求,并通过继承关系提供了相应的功能和特性。这种设计使得开发者能够更灵活地构建丰富的用户界面和内容展示。
相关属性
在WPF中,"相关属性(Related Properties)"是指与控件或元素的属性之间存在一定关联或依赖关系的属性。这些属性的值通常会相互影响,当一个属性的值发生变化时,其他相关属性的值也可能会跟着改变。
常见的相关属性包括:
Width和Height:这两个属性定义了控件或元素的宽度和高度。它们通常是相互关联的,当其中一个属性的值发生变化时,另一个属性的值也可能会受到影响。Margin和Padding:Margin属性定义了控件或元素与其容器之间的空白区域,而Padding属性定义了控件或元素内部内容与其边界之间的空白区域。它们的值也可能会相互影响。IsEnabled和Opacity:IsEnabled属性用于指示控件或元素是否处于启用状态,而Opacity属性用于定义控件或元素的不透明度。当IsEnabled属性的值为False时,通常会将控件或元素的Opacity属性设置为较低的值,以表示禁用状态。IsChecked、IsSelected和Visibility:这些属性常用于复选框、单选按钮、列表框等控件中。它们表示控件或元素的选中状态或可见性。当其中一个属性的值发生变化时,可能会触发其他相关属性的变化。
这些是一些常见的相关属性,具体的相关属性取决于控件或元素的类型和功能。了解这些相关属性之间的关系,可以帮助我们更好地使用和控制控件或元素的行为和外观。
样式
简单的说就是控件的外观、风格,在WPF中,样式就像是给控件穿衣服一样,让它们焕发出独特的魅力。就像是给一个调皮的按钮戴上一顶时尚的帽子,或者给一个文本框穿上一件华丽的礼服。样式可以让控件在人群中脱颖而出,吸引眼球。它们就像是控件的时尚顾问,为它们设计独特的外观,让它们在界面中大放异彩。所以,如果你想让你的应用程序充满时尚和个性,别忘了给控件找一个合适的样式。
样式(Style),负责控制控件元素的外观以及行为,是可用于元素的属性值集合,可以把样式(Style)看成一种将一组属性值应用到多个元素的便捷方法,使用资源的最常见原因之一就是保存样式,有点类似与Web中的css文件,但是WPF中的样式Style还支持触发器(Trigger),比如当元素属性发生变化时,可通过触发器改变控件样式。样式是组织和重用格式化选项的重要工具,不提倡在xaml中使用重复的属性填充控件,应该是创建一系列封装了这些细节二点样式,在需要的控件上应用样式。
样式可以应用于单个控件或整个应用程序范围内的所有控件。通过定义样式,我们可以设置控件的属性、视觉效果、动画、模板等内容,从而改变控件的外观和行为。样式通常由选择器和一组属性设置组成,选择器用于指定要应用样式的控件或一组控件。
WPF中的每个控件元素都具有Style属性:

F12转到Style定义可以发现,实现样式属性的Style类继承自FrameworkElement类,当然对于继承自文本内容的FrameworkContentElement类的元素也是类似,也可以使用样式。

在Style类里面,有几个重要的属性需要说明一下:
TargetType:设置样式所针对的控件类型,设置该属性后,在XAML中设置Setters或Triggers中元素的Property属性时,可以不用添加类作用域限定(这个后面部分会提到)Setters:属性设置器SetterBase对象集合 -SetterBase类主要用于控制控件的静态外观风格Triggers:条件触发器TriggerBase对象集合 -TriggerBase类主要用于控制控件的动态行为风格BaseOn:在已有样式的基础上继承另一个样式Resources:资源字典ResourceDictionary对象集合IsSealed:是否允许“派生”出其他样式
设置器 Setter
Setter(设置器)是Style类中的一个重要属性,类型是SetterBaseCollection,一个可以放入SetterBase类型对象的容器,在Style中Setter属性用于设置目标对象的属性值。Setter通常用于定义样式中的属性设置,以统一控件的外观和行为。
Setter具有两个主要属性:
Property(属性):指定要设置的属性名称。可以是任何依赖属性(DependencyProperty)或依赖对象(DependencyObject)的属性[超前警告]。Value(值):指定要为属性设置的值。
Setter的作用是在样式中定义属性设置规则,使得适用于该样式的目标对象会继承这些属性设置。当样式应用于目标对象时,Setter将设置指定属性的值为所定义的值。在实际应用中,我们很少对某一个控件使用样式,使用样式的目的是:当改变某个样式时,希望所有使用该样式的控件都会改变它们的表现形式,从而不必对某控件逐一进行修改。
例如,可以使用Setter在样式中设置Button控件的背景颜色、字体大小、边距等属性。当应用该样式于Button控件时,这些属性将自动应用,并使得所有的Button控件具有相同的外观和行为。
Setter可以在样式的<Style>标签中使用多个,以定义多个属性的设置。这样可以一次性为目标对象设置多个属性,提高代码的可读性和可维护性。
具体案例:比如现在有三个外观风格一模一样的红绿配色按钮:
<Window x:Class="HELLOWPF.ControlWindow"
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:HELLOWPF"
mc:Ignorable="d"
Title="ControlWindow" Height="450" Width="800">
<Grid>
<StackPanel>
<Button Background="Green" Margin="5" FontSize="16" Content="Bite Me!" Foreground="Red"/>
<Button Background="Green" Margin="5" FontSize="16" Content="Bite Me!" Foreground="Red"/>
<Button Background="Green" Margin="5" FontSize="16" Content="Bite Me!" Foreground="Red"/>
</StackPanel>
</Grid>
</Window>

有要求说三个按钮的风格必须是一样的,那么我们需要修改按钮样式的时候就很痛苦,需要挨个调整属性值,通过样式(Style)可以很好的解决这个问题:
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="Green" />
<Setter Property="Margin" Value="5"/>
<Setter Property="FontSize" Value="16" />
<Setter Property="Content" Value="Bite Me!"/>
<Setter Property="Foreground" Value="Red"/>
</Style>
</Window.Resources>
然后把三个Button里面的属性都删掉,可以发现Button的样式还在:
<Window x:Class="HELLOWPF.ControlWindow"
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:HELLOWPF"
mc:Ignorable="d"
Title="ControlWindow" Height="450" Width="800">
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="Green" />
<Setter Property="Margin" Value="5"/>
<Setter Property="FontSize" Value="16" />
<Setter Property="Content" Value="Bite Me!"/>
<Setter Property="Foreground" Value="Red"/>
</Style>
</Window.Resources>
<Grid>
<StackPanel>
<Button />
<Button />
<Button />
</StackPanel>
</Grid>
</Window>

Setter元素里只能指定一组属性值,可以通过多个Setter来设置多个属性值
TargetType也是Style类中的一个属性用来说明所定义的样式要施加的对象
在上述样式中,使用了多个Setter元素来设置按钮的属性:
<Window.Resources>表示在窗口的资源部分开始定义资源,其中包含样式。<Style TargetType="Button">表示定义一个针对Button控件的样式。<Setter Property="Background" Value="Green" />设置Button的背景颜色为绿色。<Setter Property="Margin" Value="5"/>设置Button的边距为5个单位。<Setter Property="FontSize" Value="16" />设置Button的字体大小为16。<Setter Property="Content" Value="Bite Me!"/>设置Button的内容为"Bite Me!"。<Setter Property="Foreground" Value="Red"/>设置Button的前景色(文本颜色)为红色。
为什么要在Resources中定义样式呢?显然我们不能只在某个特定的控件中使用样式,当然这在WPF中也是可行的:
<Button>
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="Green" />
<Setter Property="Margin" Value="5"/>
<Setter Property="FontSize" Value="16" />
<Setter Property="Content" Value="Bite Me!"/>
<Setter Property="Foreground" Value="Red"/>
</Style>
</Button.Style>
</Button>
这个样式定义在Window.Resources中,它将适用于该Window中所有的Button控件。这意味着所有的按钮都会有绿色的背景、5个单位的边距、字体大小为16、显示文本为"Bite Me!"以及红色的前景颜色。这个时候通过修改样式中的相应属性的Value就可以直接修改三个Button的样式了,不需要为每个Button控件都单独设置这些属性,可以大大简化界面设计和维护工作。
But表转折:就像上面说的,现在定义的样式效果意味着所有的按钮都会有绿色的背景、5个单位的边距、字体大小为16、显示文本为"Bite Me!"以及红色的前景颜色,那我们不想要这个样式怎么办呢?有三种解决方法:
- 你给每个
Button重新都设置需要的属性覆盖掉所设置的样式(属性的优先级),这样显然是有悖于我们使用样式的初衷的 - 使用
{x:Null}显示地清空Style - 给定义的样式取个名字
x:key,当需要的时候通过这个名字来找到它{StaticResource keyValue},这在为同一控件定义不同的样式时,非常方便。比如我们可以创建两种不同风格的Button:
<Window x:Class="HELLOWPF.ControlWindow"
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:HELLOWPF"
mc:Ignorable="d"
Title="ControlWindow" Height="450" Width="800">
<Window.Resources>
<Style x:Key="ButtonStyle1" TargetType="Button">
<Setter Property="Background" Value="Green" />
<Setter Property="Margin" Value="5"/>
<Setter Property="FontSize" Value="16" />
<Setter Property="Content" Value="Bite Me!"/>
<Setter Property="Foreground" Value="Red"/>
</Style>
<Style x:Key="ButtonStyle2" TargetType="Button">
<Setter Property="Background" Value="LightGreen" />
<Setter Property="Content" Value="Dude!"/>
</Style>
</Window.Resources>
<Grid>
<StackPanel>
<Button Style="{StaticResource ButtonStyle1}" />
<Button Style="{StaticResource ButtonStyle2}" />
<Button Style="{x:Null}" Content="No Way!"/>
</StackPanel>
</Grid>
</Window>

样式继承 BaseOn
BaseOn是样式中几个重要属性之一,用于指定当前样式基于哪个已存在的样式进行继承和扩展。通过设置BaseOn属性,可以创建一个新的样式,并在现有样式的基础上进行修改或添加新的设置。通过BaseOn属性的巧妙运用,我们能够建立起一座座视觉上的宫殿,让用户陶醉其中。无论是继承经典、扩展创新,还是重塑风貌BaseOn`属性都是我们的得力助手。
基本语法如下所示:
<Style x:Key="NewStyle" TargetType="Button" BasedOn="{StaticResource ExistingStyle}">
<!-- 新样式的设置 -->
</Style>
NewStyle会继承ExistingStyle中已定义的所有设置,然后可以在NewStyle中添加、修改或覆盖需要的属性设置。使用BasedOn属性可以提高样式的重用性和维护性。通过基于现有样式创建新的样式,可以在整个应用程序中一致地应用样式,并在需要时进行统一的更改。
比如上面例子中的样式ButtonStyle1,我们需要一个在此基础上显示内容为斜体加粗的按钮,创建一个全新的样式当然没问题,但是设置背景色、调整间距、字体大小、字体颜色又得写一遍,用最多的时间创造最低的价值,这个时候就可以通过继承ButtonStyle1样式,加上额外的样式:
<Window x:Class="HELLOWPF.ControlWindow"
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:HELLOWPF"
mc:Ignorable="d"
Title="ControlWindow" Height="450" Width="800">
<Window.Resources>
<Style x:Key="ButtonStyle1" TargetType="Button">
<Setter Property="Background" Value="Green" />
<Setter Property="Margin" Value="5"/>
<Setter Property="FontSize" Value="16" />
<Setter Property="Content" Value="Bite Me!"/>
<Setter Property="Foreground" Value="Red"/>
</Style>
<Style x:Key="ButtonStyle2" TargetType="Button">
<Setter Property="Background" Value="LightGreen" />
<Setter Property="Content" Value="Dude!"/>
</Style>
<Style x:Key="ButtonStyle3" TargetType="Button" BasedOn="{StaticResource ButtonStyle1}">
<Setter Property="FontStyle" Value="Italic" />
<Setter Property="FontWeight" Value="Bold"/>
</Style>
</Window.Resources>
<Grid>
<StackPanel>
<Button Style="{StaticResource ButtonStyle1}" />
<Button Style="{StaticResource ButtonStyle2}" />
<Button Style="{StaticResource ButtonStyle3}" Content="No Way!"/>
</StackPanel>
</Grid>
</Window>

红配绿,冒傻气
尽管乍一看通过BaseOn进行样式继承看起来非常方便,但它通常也存在一些缺点需要注意:
- 紧密的样式耦合:使用
BasedOn属性继承样式时,子样式会紧密地依赖于父样式。这意味着如果父样式发生了变化,子样式可能也会受到影响,导致意外的样式改变。这种紧密的样式耦合可能会增加代码维护的复杂性。 - 代码可读性下降:当样式继承层级变得很深时,代码的可读性可能会下降。阅读代码时需要跟踪样式的继承关系,理解每个样式的作用和效果可能会变得更加困难。
- 样式冗余和性能影响:使用
BasedOn属性继承样式时,子样式可能会继承了一些不必要的属性或样式,导致样式冗余。这可能会增加界面的渲染时间和内存消耗,对性能产生一定的影响。同时,样式继承层级的增加也可能会导致样式的解析和应用变慢。 - 难以调试和定位问题:当样式继承层级复杂时,如果出现样式的问题或者需要进行调试,可能需要在多个样式中进行追踪和定位,增加了调试的复杂性。
样式继承所产生的依赖性会使程序变得更脆弱,上面演示的实例倒还好说,但是,通常,根据不同的内容类型以及内容所扮演的角色会出现各类的样式,通过样式继承之后往往会得到一个更复杂的模型,并且真正重复使用的样式设置少之又少。除非有特殊原因要求一个样式继承自另一个样式(比如,第二个样式是第一个样式的特例,并且只改变了继承来的大量设置中的几个特征),否则不建议使用样式继承。
控件模板
WPF的每一个控件都有一个默认的模板,该模板描述了控件的外观以及外观对外界的变化所做出的反应。我们可以自定义一个模板来替换掉控件的默认模板以便打造个性化的控件。与样式Style不同,Style只能改变控件的已有属性值来定制控件,但控件模板可以改变控件的内部结构(VisualTree,视觉树)来完成更为复杂的定制,比如我们可以定制这样的按钮:在它的左办部分显示一个小图标而它的右半部分显示文本,一个带图标的Button就搞定了。
具体内容可参阅下一章节:控件模板
通过控件模板,我们可以定制控件的每一个细节,包括背景、边框、文本、图标等等。可以使用各种布局容器和可视化元素,将它们巧妙地组合在一起,创造出独特的界面风格和布局效果。控件模板还允许我们使用样式和触发器,根据不同的状态和条件改变控件的外观,使用户界面更加动态和生动。
触发器 Triggers
触发器(Triggers)用于在特定条件满足时改变控件的外观或行为。它们是一种强大的工具,用于响应用户交互、数据变化或其他事件,从而实现动态的控件效果。
触发器可以在控件模板的Style或ControlTemplate中定义。它们基于属性的值来触发特定的动作或设置。
WPF中有几种类型的触发器,包括:
Trigger:用于在属性值满足特定条件时触发动作或设置。例如,当按钮被点击时改变其背景色。MultiTrigger:与Trigger类似,但可以同时满足多个属性的条件。DataTrigger:根据数据绑定的值触发动作或设置。例如,当绑定的数据达到某个特定值时隐藏控件。MultiDataTrigger:与DataTrigger类似,但可以同时满足多个数据绑定的条件。EventTrigger:在特定事件发生时触发动作或设置。例如,当鼠标移入控件时改变其透明度。
这几种类型的触发器都是从TriggerBase类中派生出来的。DataTrigger和MultiDataTrigger是一对数据触发器,两者的区别是在DataTrigger中只能说明一个条件,而MultiDataTrigger中则可以说明多个条件。Trigger和MultiTrigger也是一对触发器,和DataTrigger相似,Trigger中只能说明一个条件,而MultiTrigger里可以说明多个条件。DataTrigger和Trigger的不同在于,DataTrigger中带有Banding属性,即DataTrigger支持数据绑定。
触发器通常与Setter一起使用,以在触发时改变控件的属性。我们可以在触发器中设置新的属性值,也可以应用动画效果或其他更复杂的操作。
浅尝一下触发器。
Trigger
最简单,也是最基础的触发器,我们用一个小例子来演示一下,也作为样式的一次小复习,比如实现一个鼠标移过文本字体变大的效果:
<Window
x:Class="HELLOWPF.ControlWindow"
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:local="clr-namespace:HELLOWPF"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="ControlWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Window.Resources>
<Style x:Key="smallTrigger" TargetType="TextBlock">
<Setter Property="FontSize" Value="14"/>
<Setter Property="Margin" Value="5"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="FontSize" Value="18"/>
<Setter Property="FontWeight" Value="Bold"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<StackPanel>
<TextBlock Text="有情武术届的麦克阿瑟 - 黑虎阿福:"/>
<TextBlock Style="{StaticResource smallTrigger}">大象踢腿</TextBlock>
<TextBlock Style="{StaticResource smallTrigger}">狮子拜天</TextBlock>
<TextBlock Style="{StaticResource smallTrigger}">二龙戏珠</TextBlock>
<TextBlock Style="{StaticResource smallTrigger}">龙卷风摧毁停车场</TextBlock>
<TextBlock Style="{StaticResource smallTrigger}">乌鸦坐飞机</TextBlock>
<TextBlock Style="{StaticResource smallTrigger}">佛朗明哥舞步</TextBlock>
</StackPanel>
</Grid>
</Window>

注意在使用触发器时要避免死循环,不要将触发器中设定的相关属性作为触发器的条件,即:改变相关属性A引起相关属性B发生改变,而相关属性B改变又引发相关属性A改变的情况。
MultiTrigger
实现一个输入效果,当输入的时候,边框变厚以提醒输入状态,同时背景颜色也发生变化:
<Window
x:Class="HELLOWPF.ControlWindow"
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:local="clr-namespace:HELLOWPF"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="ControlWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Window.Resources>
<Style x:Key="smallTrigger" TargetType="TextBox">
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsFocused" Value="True" />
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter Property="Background" Value="LightPink" />
<Setter Property="BorderThickness" Value="5" />
</MultiTrigger.Setters>
</MultiTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<StackPanel>
<TextBox
Width="150"
Height="50"
Style="{StaticResource smallTrigger}" />
</StackPanel>
</Grid>
</Window>

DataTrigger 和 MultiDataTrigger
DataTrigger和MultiDataTrigger这一对触发器和Trigger和MultiTrigger非常类似,但是它们多了一个Binding属性,需要用到数据绑定,这个后面再说。
EventTrigger
事件触发器类似winform中的小闪电,就是通过触发特定的事件执行相应的动作,我们借助动画做一个小演示:
<Window
x:Class="HELLOWPF.ControlWindow"
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:local="clr-namespace:HELLOWPF"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="ControlWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Window.Resources>
<Style x:Key="smallTrigger" TargetType="{x:Type CheckBox}">
<Setter Property="Foreground" Value="SaddleBrown" />
<Style.Triggers>
<EventTrigger RoutedEvent="Mouse.MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Width"
To="150"
Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="Mouse.MouseLeave">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Width"
To="70"
Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<StackPanel>
<CheckBox
Width="80"
FontSize="15"
Style="{StaticResource smallTrigger}">
佛朗明哥舞步
</CheckBox>
</StackPanel>
</Grid>
</Window>

格式化之后的代码有点长,不是很会用
XAML STYLE拓展,等我研究研究
控件模板
控件模板是WPF中用于定义控件外观和布局的重要概念,是WPF特有的技术。
如上面概述章节所述,WPF革命性的概念就是把控件的特性和控件的显示方式分开,也就是说WPF中的控件的显示和内部逻辑它的行为是分开的。使用过Winform的人应该知道,Winform的开发效率是非常高的,可以使用预先构建好的控件,这些控件可以工作的足够好,但是定制性也足够的有限。在开发过程中我们只能调整有限的参数来修改控件外观,比如位置(Location)、尺寸(Size)、背景颜色、鼠标悬停时的颜色等。如果想要实现一些特殊效果或者稍微好看的外观,就不得不从头重新绘制控件、实现控件的功能,这除了需要一些功底以外,重绘的自定义控件有时候可能并不会像我们预想中的样子工作,之后还会有一段漫长的调试过程。也就是说,在Winform中,实现一个略微美观的页面自定义控件是必需的,同时也是一项令人挠头的工作。
为此,WPF设计了一套很好的解决方案:把控件的外观和逻辑分离,控件的外观由模板提供,开发人员可以自由使用自己设计的控件模板,最终通过样式(Style)和模板(Template)很好的解决了上述的传统问题。这主要得益于WPF中控件的实现方式的重大改变:
传统的用户界面技术(比如Winform)中控件实际是通过窗体的控件类封装Win32 API后实现(通过gdi绘制)的,对传统windows界面元素的封装导致它们是不可更改的。而WPF是全新的dx渲染绘制的界面,脱离了对传统Win32 API的依赖,这同时也解决了Winform等传统图形界面在实现不同分辨率下的页面布局自适应的问题。
概述
所有模板类都是从FrameworkTemplate中派生出来的,FrameworkTemplate是个抽象类,负责管理模板的一些基本属性。我们知道,WPF中的所有控件都是从Control类派生而来,在Control类中就有一个类型为ControlTemplate的属性Template,修改Control类中的Template属性就可以改变控件在界面上的外观。我们创建个Button和ListBox F12 查看一下它们的继承关系:


从FrameworkTemplate中实际派生出三个类型的模板:ControlTemplate、ItemsPanelTemplate和DataTemplate:

分别是控件模板ControlTemplate、数据模板DataTemplate(由DataTemplate和HierarchicalDataTemplate类表示)和更特殊的用于ItemsControl的ItemsPanelTemplate:


本章节主要是关于
ControlTemplate控件模板的内容
控件模板定义了控件在界面上的呈现方式,包括控件的布局、样式、触发器和绑定等。通过修改控件模板,可以自定义控件的外观,使其符合特定的设计需求和用户体验要求。在控件模板中,可以使用各种控件和容器元素来构建控件的可视化结构。例如,可以使用Grid、StackPanel、Border等容器元素来布局控件的子元素,使用TextBlock、Button、TextBox等控件来显示文本和图标,还可以使用触发器(Trigger)来定义控件在不同状态下的样式变化。
感受控件模板
在xaml里面创建一个空按钮,我们会发现它实际上也是有外观的:灰色的背景色、黑色的边框、运行时候鼠标悬停会背景色会发生变化等等,但是我们之前说多控件是被设计成无外观的,这不是矛盾的吗?WPF中的每一个控件都有一个默认的模板,这个默认模板描述了在默认情况下控件的外观以及控件对外界变化所做出的反应。
选中这个Button右击编辑模板 => 编辑副本 => 创建Style资源 => 会在当前页面<Windows.Resources>键下面生成一个key为ButtonStyle1作用于Button的样式:

生成以后文档大纲也发生了变化:

这个时候Grid里的Buton变成了 <Button Style="{DynamicResource ButtonStyle1}"/>,DynamicResource是一种用于动态绑定资源的标记,当资源发生更改时,绑定会自动更新以反映最新的资源值,现在Button使用的是显式的ButtonStyle1样式。
ButtonStyle1样式中设置了我们上面所说的按钮的默认外观:灰色的背景色、边框(border)、字体颜色、边框大小、垂直水平位置······

观察当前 样式的xaml代码,样式的Key是ButtonStyle1,作用于Button类型控件,在Setter的子元素中除了我们常见的目标对象属性值集合,还有一个名为Template的属性,它的值是一个作用于Button的ControlTemplt,ControlTemplt里面包含了一个名为border的Border和的触发器:

这里的ControlTemplt就是标题中的主人公 - 控件模板,在模板里的Border里除了我们常见的属性还有个ContentPresenter,猜猜它是干啥的。
内容展示器 ContentPresenter
我们省略掉为嵌套元素设置属性的特性,以及当按钮获得焦点、被单击、禁用时按钮行为的触发器:
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="true">
<ContentPresenter x:Name="contentPresenter"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Focusable="False"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
当前样式中Border作为Button的主要容器,用于绘制按钮的背景和边框。在Border内部,有一个ContentPresenter。
ContentPresenter:用于呈现其他控件或对象的内容。它通常用作控件模板中的占位符,用于显示控件的内容部分。主要功能是将内容(通常是控件的内容)呈现到其布局中。它根据控件的模板定义来决定内容的呈现方式和位置。通过将ContentPresenter放置在控件模板中的适当位置,可以定义控件的外观和布局,并确保内容正确地显示在其中。
为什么
ContentPresenter既可以呈现对象的内容,又可以是其他控件呢从
ConteneControl派生而来的控件它内容是Content,而查看Content的定义可以发现,它是一个object类型的属性,这就使得Content的内容既可以是我们常用的文本,也可以是非常复杂的控件内容。
对于从ConteneControl派生而来的控件,比如Button、CheckBox等这些控件都是通过ContenrPresenter来呈现内容的。
ContentPresenter具有一些属性,例如Margin、HorizontalAlignment、VerticalAlignment等,可用于控制内容的布局和对齐方式。它还可以处理一些相关的特性,例如可访问性和焦点管理。通过TemplateBinding,将Button的Padding、HorizontalAlignment、VerticalAlignment等属性绑定到ContentPresenter的相应属性上,这也是为什么当给Button的Content加上内容之后它总是默认垂直居中的缘故。
在控件模板中,通常使用ContentPresenter来代表控件的内容部分,以便在模板被应用时将实际的内容呈现出来。这使得控件的外观和内容的呈现可以分离开来,并且可以根据需要进行自定义和修改。
我们可以动手修改模板内容让Button看起来更好看一点,比如尝试改造成图示效果:

调整
Button模板中的边框实现圆角:
调整
Button模板中内容呈现的位置:
在创建的
Button中将Content改为如下的布局控件:<Button Style="{DynamicResource ButtonStyle1}" Width="180" Background="#72ad86">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="8*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.ColumnSpan="2" Margin="10" FontSize="15" FontWeight="Medium"
Foreground="#246157" Text="Total balance" />
<TextBlock Grid.Row="1" Grid.Column="0" Margin="10" Foreground="#246157" Text="$" />
<TextBlock Grid.Row="1" Grid.Column="1" Text="439,177" Foreground="#246157" FontSize="25"/>
</Grid>
</Button>
简单的效果就实现了,最好把边框的颜色改下一,圆角再大一点,字体什么的微调一下就跟图片中的效果差不多了,样子不重要,大家理解
ContentPresenter和Content的关系以及作用就可以了:

ContentPresenter用于展示控件的内容(即Content属性的值)并决定其在控件模板中的呈现方式,而Content的类型object决定了ContentPresenter既可以呈现对象的内容也可以是其他控件。
那么ContentPresenter可以删掉嘛,当然可以,我们可以把它换成我们想要的任何内容,但它可能就无法正确的呈现我们在Content中设置的内容了。当然了,如果按钮足够简单,去点展示器换成其他类型能对上的也是可以的,需要调整下呈现的内容的绑定【️超前警告】,但是一旦按钮中的内容稍微复杂一点就没办法正常呈现内容了。Ps:一开始上面图片中的效果就是通过将ContentPresenter调整为上面内容中的Grid的嵌套,同时呢由于依赖关系需要把下面的触发器(因为触发器有ContentPresenter的引用)也去掉,但是我怕大家混淆了就调整成了上面部分。
此外,我们还可以借助VS的转到实时可视化树来验证我们上面的猜想:

模板绑定 TemplateBinding
TemplateBinding(模板绑定)是一种特殊的绑定方式,用于在控件模板中绑定模板内部元素的属性与外部控件的属性之间建立关联。
使用TemplateBinding,可以在控件模板内部直接引用外部控件的属性,而无需手动编写绑定表达式。它允许我们在控件模板中以一种简洁、直接的方式访问外部控件的属性,从而实现属性的传递和同步。
具体而言,我们可以通过在控件模板的属性设置中使用TemplateBinding来引用外部控件的属性。例如,我们可以在控件模板中的某个属性设置中使用TemplateBinding来设置该属性的值为外部控件的对应属性的值,从而实现属性的绑定和同步。
比如还用上面那个按钮的例子,图片里面的边框不是和背景色一样吗,我们就可以修改模板中Border的边框颜色(BorderBrush属性)绑定到设置的背景色,这样设置好了按钮的背景色以后,按钮的边框也就和背景色一样了:


TemplateBinding只能在控件模板中使用,用于绑定模板内部元素的属性与控件实例的属性。
需要注意的是,
TemplateBinding只能绑定到当前控件的相关属性。它用于在控件模板中绑定控件自身的属性,以便将模板中的元素与控件的属性保持一致。比如我们在Border里面多给他建一个Text绑定到Button中显示的Content是会报错的,因为Button本身没有Text这个属性。
在资源中使用模板
和Style一样,一般很少在控件中直接定义控件的模板,通常控件的模板要放在某个窗体(Window)、页面(Page)或应用程序(Application)的资源部分。这样同一类别的控件可以共享控件模板。我们查看Button的默认模板也能看到它是定义在Button所在窗体的资源Window.Resources里面的,仿照着默认模板,我们尝试定义在资源定义一个简单的按钮模板:
<Window
x:Class="HELLOWPF.ControlWindow"
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:local="clr-namespace:HELLOWPF"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="ControlWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Window.Resources>
<ControlTemplate x:Key="myButton" TargetType="Button">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Border
x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="5">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<TextBlock
Name="myTextBlock"
Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="ForestGreen"
Text="模板按钮" />
</Grid>
</ControlTemplate>
</Window.Resources>
<Grid>
<StackPanel>
<Button
Height="50"
Content="dwadad"
Template="{StaticResource myButton}" />
</StackPanel>
</Grid>
</Window>

WPF 入门笔记 - 03 - 样式基础及控件模板的更多相关文章
- 《Programming WPF》翻译 第5章 7.控件模板
原文:<Programming WPF>翻译 第5章 7.控件模板 如果仔细的看我们当前的TTT游戏,会发现Button对象并没有完全为我们工作.哪些TTT面板有内圆角? 图5-14 这里 ...
- 【WPF学习】第六十章 创建控件模板
经过数十天的忙碌,今天终于有时间写博客. 前面一章通过介绍有关模板工作方式相关的内容,同时介绍了FrameWorkElement下所有控件的模板.接下来将介绍如何构建一个简单的自定义按钮,并在该过程中 ...
- WPF学习笔记(1)——image控件图片不显示的问题
说明(2017-6-7 16:08:35): 1. 本来想用winform的,用winform就没这么多破事了. 2. 不过项目要用WPF,拉出一个mediaelement控件,视频正常显示. 3. ...
- WPF教程十一:简单了解并使用控件模板
WPF教程十一:简单了解并使用控件模板 这一章梳理控件模板,每个WPF控件都设计成无外观的,但是行为设计上是不允许改变的,比如使用Button的控件时,按钮提供了能被点击的内容,那么自由的改变控件外观 ...
- WPF笔记(1.9 样式和控件模板)——Hello,WPF!
原文:WPF笔记(1.9 样式和控件模板)--Hello,WPF! 资源的另一个用途是样式设置: <Window > <Window.Resources> <St ...
- WPF自定义控件与样式(7)-列表控件DataGrid与ListView自定义样式
一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: Dat ...
- 【转】WPF自定义控件与样式(7)-列表控件DataGrid与ListView自定义样式
一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等. 本文主要内容: DataGrid自定义样式: ListView自定义样式: 二.Dat ...
- [Java入门笔记] 面向对象编程基础(二):方法详解
什么是方法? 简介 在上一篇的blog中,我们知道了方法是类中的一个组成部分,是类或对象的行为特征的抽象. 无论是从语法和功能上来看,方法都有点类似与函数.但是,方法与传统的函数还是有着不同之处: 在 ...
- WPF自定义控件与样式(9)-树控件TreeView与菜单Menu-ContextMenu
一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: 菜单M ...
- WPF自定义控件与样式(10)-进度控件ProcessBar自定义样
一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: Pro ...
随机推荐
- springboot---多环境启动命令格式
一.多环境命令启动 maven插件中首先clean,再package打包,(修改字符集为UTF-8) 使用cmd命令java -jar s(Tab键自动补全) -spring.profiles.ac ...
- springboot 连接不上 redis 的三种解决方案!
针对于这种情况,首先,我们最简单直接的方法就是需要确认Redis是否已经正常启动(验证方法:如果安装在Linux下的话可以使用ps-ef|grep redis来进行确认是否开启) 如果未开启,我们可以 ...
- Collection单列集合总结
这篇文章记录了Collection集合,List集合,Set集合 在文章第七点总结了两大系列集合的五种实现类的区别,有需要的小伙伴可以直接去查看 一.什么是集合 集合是Java中存储对象数据的一种容器 ...
- 在进行程序国际化时(Locale)遇到的问题
Java程序国际化-Question 为了使程序能够个根据不同的国家/语言环境来输出不同的内容,通常需要把将需要输出的内容定义在资源文件中. 而在创建资源文件的过程中难免会出现问题,我遇到了在创建资源 ...
- git的Rebase和Merge之间的区别
有人会说Merge更好,因为它保留了最完整的工作历史.其他人则认为,Rebase变得更整洁,这使审阅者的生活更轻松,更高效.本文将解释合并和重新设置之间的区别是什么,使用它们之一有什么好处. 从根本上 ...
- JavaScript中计时器requestAnimationFrame、setTimeout、setInterval、setImmediate的使用和区别
在JavaScript中,我们经常使用requestAnimationFrame.setTimeout.setInterval和setImmediate来控制代码的执行时机.它们各有特点和适用场景: ...
- [GIT] 如何处理GIT分支合并(GIT MERGE)
1 概述 2 分支合并 如果你有两个分支main和dev,main存放稳定版本,dev是开发版本,一个阶段后,你需要把dev代码更新到main分支中. dev --(merge update cont ...
- Ubuntu20.04 Docker搭建远程xfce桌面以及ssh教程
简介:本文主要介绍ubuntu20.04容器中搭建xfce远程桌面.C++.Go环境.容器内docker操作配置. 一.创建容器1.创建容器 docker pull ubuntu:20.04docke ...
- Semantic Kernel 入门系列:📅 Planner 计划管理
Semantic Kernel 的一个核心能力就是实现"目标导向"的AI应用. 目标导向 "目标导向"听起来是一个比较高大的词,但是却是实际生活中我们处理问题的 ...
- 为什么 APISIX Ingress 是比 Emissary-ingress 更好的选择?
本文从可扩展性和服务发现集成等多个维度对比了 APISIX Ingress 与 Emissary-ingress 的性能. 作者:容鑫,API7.ai 云原生技术工程师,Apache APISIX C ...
