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>