WPF 入门笔记 - 03 - 样式基础及模板

🍟 程序的本质 - 数据结构 + 算法 🍟 本篇为学习李应保老师所著的《WPF专业编程指南》并搭配WPF开发圣经《WPF编程宝

🍟 程序的本质 - 数据结构 + 算法 🍟

本篇为学习李应保老师所著的《WPF专业编程指南》并搭配WPF开发圣经《WPF编程宝典第4版》以及痕迹大佬《WPF入门基础教程系列》文章所作笔记,对应《WPF专业编程指南》第 9-10 章之间内容,主要概述WPF中关于样式及模板部分的梳理及应用,希望可以帮到大家💖

References:

WPF控件模板(6)

WPF 详解模板

WPF 自定义模板

概述

从按钮、文本框到下拉框、列表框,WPF提供了一系列常用控件,每个控件都有自己独特的特性和用途。通过灵活的布局容器,如网格、堆栈面板和换行面板,我们可以将这些控件组合在一起,实现复杂的界面布局。而通过样式和模板,我们可以轻松地定制控件的外观和行为,以符合我们的设计需求。

WPFUI(User Interface)是通过数据来驱动的,数据是核心,UI从属于数据并表达数据,这和传统的windows图形界面开发(比如``Winform)有很大的区别。WPF中能够展示数据、响应用户操作的UI`元素称为控件(Control),同时控件也是数据和行为的载体,它们被设计成总是无外观的(lookless)。控件中展现的内容称之为“数据内容”,响应用户操作后执行的方法(Method)或事件(Event)称之为“行为”。

WPF革命性的概念就是把UI元素(控件)的特性和显示方式分开。控件在用户界面上的样子是由控件模板决定的,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中的样式(Style)是一种用于定义控件外观和行为的重要机制。它允许开发人员将属性设置封装到可重用的命名容器中,以便在应用程序中多次应用。在概念上WPF的样式和HTML中的CSS类似,但由于WPF中依赖属性的强大功能,使得WPF可以通过样式完成CSS所不具备的功能。

WPF中的模板(Template)则是一种更高级别的样式,它可以完全重写控件的外观和行为,包括其布局和交互。通过使用模板,开发人员可以创建自定义控件,这些控件可能与基本控件有完全不同的外观和交互模型。

相关属性

  • 按类别分类的控件

WPF中,"相关属性(Related Properties)"是指与控件或元素的属性之间存在一定关联或依赖关系的属性。这些属性的值通常会相互影响,当一个属性的值发生变化时,其他相关属性的值也可能会跟着改变。

常见的相关属性包括:

  1. WidthHeight:这两个属性定义了控件或元素的宽度和高度。它们通常是相互关联的,当其中一个属性的值发生变化时,另一个属性的值也可能会受到影响。
  2. MarginPaddingMargin属性定义了控件或元素与其容器之间的空白区域,而Padding属性定义了控件或元素内部内容与其边界之间的空白区域。它们的值也可能会相互影响。
  3. IsEnabledOpacityIsEnabled属性用于指示控件或元素是否处于启用状态,而Opacity属性用于定义控件或元素的不透明度。当IsEnabled属性的值为False时,通常会将控件或元素的Opacity属性设置为较低的值,以表示禁用状态。
  4. IsCheckedIsSelectedVisibility:这些属性常用于复选框、单选按钮、列表框等控件中。它们表示控件或元素的选中状态或可见性。当其中一个属性的值发生变化时,可能会触发其他相关属性的变化。

这些是一些常见的相关属性,具体的相关属性取决于控件或元素的类型和功能。了解这些相关属性之间的关系,可以帮助我们更好地使用和控制控件或元素的行为和外观。

样式

样式(Style),负责控制控件元素的外观以及行为,是可用于元素的属性值集合,可以把样式(Style)看成一种将一组属性值应用到多个元素的便捷方法,使用资源的最常见原因之一就是保存样式,有点类似与Web中的css文件,但是WPF中的样式Style还支持触发器(Trigger),比如当元素属性发生变化时,可通过触发器改变控件样式,这使得WPF可以通过样式可以完成CSS所不具备的功能。

简单来说,样式就是控件的外观、风格,在WPF中,样式就像是控件身上的衣服。

样式可以应用于单个控件或整个应用程序范围内的所有控件。通过定义样式,可以设置控件的属性、视觉效果、动画、模板等内容,从而改变控件的外观和行为。此外,样式是组织和重用格式化选项的重要工具,我们都知道,重复的代码就是问题,不提倡在xaml中使用重复的属性填充控件,而应该创建一系列封装了这些细节要点的样式,在需要的控件上应用该样式。

WPF中每个控件都有Style属性用来设定样式:

转到Style定义可以知道,样式类Style位于System.Windows命名空间下:

Style类里面,定义了几个重要的属性:

  • TargetType:设置样式所针对的控件类型,设置该属性后,在XAML中设置SettersTriggers中元素的Property属性时,可以不用添加类作用域限定(这个后面部分会提到)

  • Setters:属性设置器SetterBase对象集合 - SetterBase类主要用于控制控件的静态外观风格

  • Triggers:条件触发器TriggerBase对象集合 - TriggerBase类主要用于控制控件的动态行为风格

  • BaseOn:在已有样式的基础上继承另一个样式

  • Resources:资源字典ResourceDictionary对象集合

  • IsSealed:是否允许“派生”出其他样式

接下来我们来具体看看每个属性都是用来做什么的以及如何使用这些属性,近距离感受样式的神奇力量。

设置器 Setter

Setter(设置器)是Style类中的一个重要属性,其类型是SetterBaseCollection,一个可以放入SetterBase类型对象的容器,在StyleSetter属性用于设置目标对象的属性值。Setter通常用于定义样式中的属性设置,以统一控件的外观和行为。

Setter具有两个主要属性:

  1. Property(属性):指定要设置的属性名称。可以是任何依赖属性(DependencyProperty)或依赖对象(DependencyObject)的属性[超前警告⚠]。
  2. Value(值):指定要为属性设置的值。

Setter的作用是在样式中定义属性设置规则,使得适用于该样式的目标对象会继承这些属性设置。当样式应用于目标对象时,Setter将设置指定属性的值为所定义的值。在实际应用中,我们很少对某一个控件使用样式,使用样式的目的是:当改变某个样式时,希望所有使用该样式的控件都会改变它们的表现形式,从而不必对某控件逐一进行修改。

例如,可以使用Setter在样式中设置Button控件的背景颜色、字体大小、边距等属性。当应用该样式于Button控件时,这些属性将自动应用,并使得所有的Button控件具有相同的外观和行为。

Setter可以在样式的

然后把三个Button里面的属性都删掉,可以发现Button的样子和之前一个一个属性设置是一样的,但是这种方式相较于之前就显得灵活了很多,当需要调整的时候我们只需要调整样式中的对应属性,就可以让三个按钮跟着变化了:

 
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">

🔜🔜 Setter元素里只能指定一组属性值,可以通过多个Setter来设置多个属性值

​ TargetTypeStyle类中的一个属性用来说明所定义的样式要施加的对象 🔙🔙

在上述样式中,使用了多个Setter元素来设置按钮的属性:

  •  表示在窗口的资源部分开始定义资源,其中包含样式。
  • 但是这种定义方式和一开始我们分别定义每个按钮外观的方法如出一辙,显然失去了使用样式的意义。把样式定义在Window.Resources中,它将适用于当前Window(窗体)中所有的Button控件。这意味着所有的按钮都会有绿色的背景、5个单位的边距、字体大小为16、显示文本为"Bite Me!"以及红色的前景颜色。这个时候通过修改样式中的相应属性的Value就可以直接修改Button的样式了,不需要为每个Button控件都单独设置这些属性,可以大大简化界面设计和维护工作。

    ❗❗❗表转折:就像上面说的,现在定义的样式效果意味着所有的按钮都会有绿色的背景、5个单位的边距、字体大小为16、显示文本为"Bite Me!"以及红色的前景颜色,那如果有一个按钮不需要设定成这种样式或者它是别的样子怎么办呢?别急,有三种解决方法:

    • 你给每个Button重新都设置需要的属性覆盖掉所设置的样式(属性的优先级),这样显然是有悖于我们使用样式的初衷的
    • 使用{x:Null}显式清空Style
    • 给定义的样式取个名字x:key,当需要的时候通过这个名字来找到它{StaticResource keyValue},这在为同一控件定义不同的样式时,非常方便。比如我们可以创建两种不同风格的Button
     
    
    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">

    以上内容就是Setter的用法以及样式的一般定义和使用方法。

    样式继承 BaseOn

    BaseOn也是样式中几个重要属性之一,用于指定当前样式基于哪个已存在的样式进行继承和扩展。通过设置BaseOn属性,可以创建一个新的样式,并在现有样式的基础上进行修改或添加新的设置。通过BaseOn属性的巧妙运用,我们能够建立起一座座视觉上的宫殿,让用户陶醉其中。无论是继承经典、扩展创新,还是重塑风貌BaseOn`属性都是我们的得力助手。

    基本语法如下所示:

     
    

    声明的NewStyle样式会继承ExistingStyle中已定义的所有设置,然后可以在NewStyle中添加、修改或覆盖需要的属性设置。使用BasedOn属性可以提高样式的重用性,通过基于现有样式创建新的样式,可以在整个应用程序中一致地应用样式,并在需要时进行统一的更改。

    比如上面例子中的样式ButtonStyle1,我们需要一个在此基础上显示内容为斜体加粗的按钮,创建一个全新的样式当然没问题,但是设置背景色、调整间距、字体大小、字体颜色又得写一遍,用最多的时间创造最低的价值🙋,这个时候就可以通过继承ButtonStyle1样式,加上额外的样式:

     
    
    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">

    红配绿,冒傻气

    尽管乍一看通过BaseOn进行样式继承看起来非常方便,但它也存在一些缺点需要注意:

    1. 紧密的样式耦合:使用BasedOn属性继承样式时,子样式会紧密地依赖于父样式。这意味着如果父样式发生了变化,子样式可能也会受到影响,导致意外的样式改变。这种紧密的样式耦合可能会增加代码维护的复杂性。
    2. 代码可读性下降:当样式继承层级变得很深时,代码的可读性可能会下降。阅读代码时需要跟踪样式的继承关系,理解每个样式的作用和效果可能会变得更加困难。
    3. 样式冗余和性能影响:使用BasedOn属性继承样式时,子样式可能会继承了一些不必要的属性或样式,导致样式冗余。这可能会增加界面的渲染时间和内存消耗,对性能产生一定的影响。同时,样式继承层级的增加也可能会导致样式的解析和应用变慢。
    4. 难以调试和定位问题:当样式继承层级复杂时,如果出现样式的问题或者需要进行调试,可能需要在多个样式中进行追踪和定位,增加了调试的复杂性。

    样式继承所产生的依赖性会使程序变得更脆弱,上面演示的实例倒还好说,但是,通常,根据不同的内容类型以及内容所扮演的角色会出现各类的样式,通过样式继承之后往往会得到一个更复杂的模型,并且真正重复使用的样式设置少之又少。除非有特殊原因要求一个样式继承自另一个样式(比如,第二个样式是第一个样式的特例,并且只改变了继承来的大量设置中的几个特征),否则不建议使用样式继承。

    触发器 Triggers

    触发器(Triggers)用于在特定条件满足时改变控件的外观或行为。它们是一种强大的工具,用于响应用户交互、数据变化或其他事件,从而实现动态的控件效果。

    触发器可以在控件模板的StyleControlTemplate中定义。它们基于属性的值来触发特定的动作或设置。

    WPF中有几种类型的触发器,包括:

    1. Trigger:用于在属性值满足特定条件时触发动作或设置。例如,当按钮被点击时改变其背景色。
    2. MultiTrigger:与Trigger类似,但可以同时满足多个属性的条件。
    3. DataTrigger:根据数据绑定的值触发动作或设置。例如,当绑定的数据达到某个特定值时隐藏控件。
    4. MultiDataTrigger:与DataTrigger类似,但可以同时满足多个数据绑定的条件。
    5. EventTrigger:在特定事件发生时触发动作或设置。例如,当鼠标移入控件时改变其透明度。

    这几种类型的触发器都是从TriggerBase类中派生出来的。DataTriggerMultiDataTrigger是一对数据触发器,两者的区别是在DataTrigger中只能说明一个条件,而MultiDataTrigger中则可以说明多个条件。TriggerMultiTrigger也是一对触发器,和DataTrigger相似,Trigger中只能说明一个条件,而MultiTrigger里可以说明多个条件。DataTriggerTrigger的不同在于,DataTrigger中带有Banding属性,即DataTrigger支持数据绑定。

    触发器通常与Setter一起使用,以在触发时改变控件的属性。我们可以在触发器中设置新的属性值,也可以应用动画效果或其他更复杂的操作。

    浅浅尝试一下触发器:

    Trigger

    最简单,也是最基础的触发器,我们用一个小例子来演示一下,也作为样式的一次小复习,比如实现一个鼠标移过文本字体变大的效果:

     
    
    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">
    大象踢腿
    狮子拜天
    二龙戏珠
    龙卷风摧毁停车场
    乌鸦坐飞机
    佛朗明哥舞步

    注意在使用触发器时要避免死循环,不要将触发器中设定的相关属性作为触发器的条件,即:改变相关属性A引起相关属性B发生改变,而相关属性B改变又引发相关属性A改变的情况。

    MultiTrigger

    实现一个输入效果,当输入的时候,边框变厚以提醒输入状态,同时背景颜色也发生变化:

     
    
    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">
    Width="150"
    Height="50"
    Style="{StaticResource smallTrigger}" />

    DataTrigger 和 MultiDataTrigger

    DataTriggerMultiDataTrigger这一对触发器和TriggerMultiTrigger非常类似,但是它们多了一个Binding属性,需要用到数据绑定,这个后面再说。

    EventTrigger

    事件触发器类似winform中的小闪电,就是通过触发特定的事件执行相应的动作,我们借助动画做一个小演示:

     
    
    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">
    Width="80"
    FontSize="15"
    Style="{StaticResource smallTrigger}">
    佛朗明哥舞步

    格式化之后的代码看起来有点长😇。

    定义样式到单独文件中

    上面所演示的例子中,样式都是定义在目标控件所在的页面的资源里的,只有当前页面上的控件可以应用样式,这样当出现跨页面的时候样式的定义方式就显得有点无力了。想象一下,你的软件有多个页面,每个页面里面又有多个按钮、文本框、标签等控件,如果每个控件都手动设置它们的属性值来达到一致的外观,这将非常繁琐和容易出错。在大型软件工程中,当你需要开发多个WPF应用程序或DLL时,确保这些应用程序具有一致的外观是非常重要的。这也是样式(Styles)的真正强大之处所在。

    现在的程序一般不都是会带有主题自定义的功能吗,可以通过调整给出的配置项自由修改页面效果,还有明暗主题切换。虽然WPF没有ThemeSkin之类的东西,但是我们可以通过样式很好的完成这项工作捏🍗

    尝试一下实现一个简单的明暗主题切换:

    来看看怎么实现的:

    构建主题字典

    比较无聊,提取Style切换颜色,也不知道标准否,文件结构大概如图所示:

    分别创建了两个资源字典:DictionaryDarkStyle.xamlDictionaryDefaultStyle.xaml用来存放默认主题以及深色主题,对应xaml代码如下:

    Default:

     
    
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    Dark:

     
    
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    加载切换主题资源字典

    App.xaml中添加程序的资源,也就是我们定义的资源字典:

     
    
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:ThemeSwitchDemo"
    StartupUri="MainWindow.xaml">
    应用样式及切换

    默认页面的xaml如下:

     
    
    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:ThemeSwitchDemo"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    WindowStartupLocation="CenterScreen"
    mc:Ignorable="d">
    Height="100"
    Content="你想和老爹斗吗?"
    Style="{StaticResource LabelDefaultStyle}" />
    Height="200"
    Content="还有一件事!永远不要怀疑老爹的话!"
    Style="{StaticResource LabelDefaultStyle}" />
    Height="100"
    Click="button_Click"
    Content="SwitchTheme"
    Style="{StaticResource ButtonDefaultStyle}" />

    通过按钮的点击事件来切换样式:

     
    
    using System.Windows;
    namespace ThemeSwitchDemo
    {
    ///
    /// MainWindow.xaml 的交互逻辑
    ///
    public partial class MainWindow : Window
    {
    public MainWindow()
    {
    InitializeComponent();
    }
    bool isDarkStyleApplied = false;
    private void button_Click(object sender, RoutedEventArgs e)
    {
    if (isDarkStyleApplied)
    {
    // 如果当前是黑色样式,则切换回默认样式
    border.Style = (Style)Application.Current.Resources["BorderDefaultStyle"];
    label1.Style = (Style)Application.Current.Resources["LabelDefaultStyle"];
    label2.Style = (Style)Application.Current.Resources["LabelDefaultStyle"];
    button.Style = (Style)Application.Current.Resources["ButtonDefaultStyle"];
    isDarkStyleApplied = false;
    }
    else
    {
    // 如果当前是默认样式,则切换到黑色样式
    border.Style = (Style)Application.Current.Resources["BorderDarkStyle"];
    label1.Style = (Style)Application.Current.Resources["LabelDarkStyle"];
    label2.Style = (Style)Application.Current.Resources["LabelDarkStyle"];
    button.Style = (Style)Application.Current.Resources["ButtonDarkStyle"];
    isDarkStyleApplied = true;
    }
    }
    }
    }

    That’s all.

    可能是初学的缘故,我觉得应该有比我这样更好的实现方式,欢迎大家留言讨论,毕竟我是初学。还有一件事,牛战士是从来不会摘下面具的🦬。

    模板

    正如上面概述章节所述,WPF革命性的概念就是把控件的特性和控件的显示方式分开,也就是说WPF中的控件的显示和内部逻辑它的行为是分开的。使用过Winform的人应该知道,Winform的开发效率是非常高的,可以使用预先构建好的控件,这些控件可以工作的足够好,但是同样控件的定制性也足够的有限。在开发过程中我们只能调整有限的参数来修改控件外观,比如位置(Location)、尺寸(Size)、背景颜色、鼠标悬停时的颜色等。如果想要实现一些特殊效果或者稍微好看的外观,就不得不从头重新绘制控件、实现控件的功能,这除了需要一些功底以外,重绘的自定义控件有时候可能并不会像我们预想中的样子工作,之后还会有一段漫长的调试过程。也就是说,在Winform中,实现一个略微美观的页面自定义控件是必需的,同时也是一项令人挠头的工作🙈。

    为此,WPF设计了一套很好的解决方案:把控件的外观和逻辑分离,控件的外观由模板提供,开发人员可以自由使用自己设计的控件模板,最终通过样式(Style)和模板(Template)很好的解决了上述的传统问题。这主要得益于WPF中控件的实现方式的重大改变:

    传统的用户界面技术(比如Winform)中控件实际是通过窗体的控件类封装Win32 API后实现(通过gdi绘制)的,对传统windows界面元素的封装导致它们是不可更改的。而WPF是全新的dx渲染绘制的界面,脱离了对传统Win32 API的依赖,这同时也解决了Winform等传统图形界面在实现不同分辨率下的页面布局自适应的问题。

    回到本节的标题:什么是模板?不知道大家有没有尝试过自己动手DIY雪糕或者月饼,做过的小伙伴应该知道,在制作之前需要准备好模具,把材料放进去,这样做出来才会达到理想的形状。这里的模具就起到了模板的作用,之前看过一篇文章是这样解释模板的:

    模板从字面意思理解是“具有一定规格的样板"。在现实生活中,砖块都是方方正正的,那是因为制作砖块的模板是方方正正的,如果我们使模板为圆形的话,则制作出来的砖块就是圆形的,此时我们并不能说圆形的”砖块“不是砖块吧。因为形状只是它们的外观,其制作材料还是一样的。

    一个网站的主题通过一个模板来定义。如果我们使用不同的模板,网站的外观和风格也会不同。但是,这并不改变网站的本质和功能,它仍然是一个网站,并具有相同的基础构成部分。因此,模板可以理解为表现形式,WPF中的模板同样是表现形式的意思。

    WPF中,模板用于定义控件的可视化结构和布局,它决定了控件的内部构成和展示方式。通过模板,我们可以完全重写控件的外观,使其具有全新的视觉效果。模板的主要作用是为控件提供自定义的外观和布局,从而实现个性化的用户界面,模板可以覆盖控件的默认视觉结构。

    乍一听好像模板和上面的样式作用差不多,可别混淆了。样式更关注于控件的属性和视觉效果,例如颜色、字体等,而模板则更关注于控件的结构和布局。样式可以用于多个控件之间的外观统一,而模板通常用于对单个控件进行个性化定制。

    WPF里有三中模板:控件模板、数据模板和Items模板,都是从FrameworkTemplate类中派生出来的:

    1. 控件模板(ControlTemplate):控件造型师,用于定义控件的外观和布局,可以完全重写控件的外观,让控件具有全新的视觉效果。
    2. 数据模板(DataTemplate):用于定义数据对象的可视化呈现方式。数据模板通常与数据绑定一起使用,将数据对象与界面元素关联起来。
    3. 面板模板(ItemsPanelTemplate):用于定义ItemsControl的布局方式的模板,它决定了集合项在布局面板中的排列和位置规则。

    我们都知道,所有的WPF控件都是从Control类中派生出来的,Control类中有一个属性Template,设置Template属性就可以改变控件在界面上的外观。这里Template的指的就是三类模板之一的控件模板,下面我们来看看控件模板是啥。

    控件模板

    控件模板定义了控件在界面上的呈现方式,包括控件的布局、样式、触发器和绑定等。通过修改控件模板,可以自定义控件的外观,使其符合特定的设计需求和用户体验要求。在控件模板中,可以使用各种控件和容器元素来构建控件的可视化结构。例如,可以使用GridStackPanelBorder等容器元素来布局控件的子元素,使用TextBlockButtonTextBox等控件来显示文本和图标,还可以使用触发器(Trigger)来定义控件在不同状态下的样式变化。

    xaml里面创建一个空按钮,我们会发现它通常是这个样子的:灰色的背景色、黑色的边框、鼠标悬停会背景色会发生变化等等,但是我们之前说多控件是被设计成无外观的,这不是矛盾的吗💢

    WPF中的每一个控件都有默认模板该模板描述了控件的外观以及外观对外界的变化所做出的反应。我们可以自定义一个模板来替换掉控件的默认模板以便打造个性化的控件。与样式Style不同,Style只能改变控件的已有属性值来定制控件,但控件模板可以改变控件的内部结构(VisualTree,视觉树)来完成更为复杂的定制,比如我们可以定制一个带图标的按钮:在它的左边部分显示一个图片,右半部分显示文本,一个带图标的Button就搞定了。

    通过控件模板,我们可以定制控件的每一个细节,包括背景、边框、文本、图标等等。可以使用各种布局容器和可视化元素,将它们巧妙地组合在一起,创造出独特的界面风格和布局效果。我们先来看一下控件模板ControlTemplate的组成:

    选中设计器页面上的Button右击 => 编辑模板 => 编辑副本 => 创建Style资源 => 会在当前页面下面生成一个keyButtonStyle1作用于Button的样式:

    样式中设置的Template属性的值就是一个ControlTemplate对象,里面包括对控件背景色、字体颜色、边框大小、内容垂直水平位置等属性的设置:

    ControlTemplate

    1. Border 元素: 用于包裹 Button 的内容,并提供边框和背景样式。
    2. ContentPresenter 用于呈现 Button 的内容。它负责将控件的内容显示在 Button 内部,并根据模板中的属性设置进行对齐和布局。
    3. ControlTemplate.Triggers 元素: 包含了一系列触发器(Trigger),用于根据控件的状态改变外观和行为。在这个样式中,定义了几个触发器来响应 IsDefaultedIsMouseOverIsPressed和 IsEnabled属性的变化,并根据状态改变相应的样式。

    Border

    Border 元素: 用于包裹 Button 的内容,并提供边框和背景样式,可以尝试修改里面的内容,观察一下按钮会发生什么变化。比如我们想要按钮变成下图的样子:

    先不说里面的内容,我们来看看外观,可以通过修改Border元素的属性来实现该效果:

    ContentPresenter

    🚧🚧🚧 ContentPresenter用于呈现其他控件或对象的内容。它通常用作控件模板中的占位符,用于呈现控件的内容,并根据模板中的属性设置进行对齐和布局。

    为什么ContentPresenter既可以呈现对象的内容,又可以是其他控件呢❓

    一般从ConteneControl派生而来的控件它内容是Content,而查看Content的定义可以发现,它是一个object类型的属性,这就使得Content的内容既可以是我们常用的文本,也可以是非常复杂的控件内容,而ContentPresenter绑定的正是控件的Content属性。

    对于从ConteneControl派生而来的控件,比如ButtonCheckBox等这些控件都是通过ContenrPresenter来呈现内容的。

    怎么理解这句话呢,拿Border中的例子,我们已经将按钮的外观调整成了图片效果,我们可以通过调整Content属性让按钮显示三个TextBlock对应图片中的内容:

    🔖回到正题,在ContentPresenter中可以看到有这些属性,例如MarginHorizontalAlignmentVerticalAlignment等,用于控制内容的布局和对齐方式。清空Content中的内容,改成固定的Hello WPF,可以发现它是居中显示的,调整模板中的水平、垂直属性:

    显然,ContentPresenter用于展示控件的内容(即Content属性的值)并决定其在控件模板中的呈现方式,而Content的类型object决定了ContentPresenter既可以呈现对象的内容也可以是其他控件。

    ContentPresenter可以删掉嘛,当然可以,我们可以把它换成我们想要的任何内容,但它可能就无法正确的呈现我们在Content中设置的内容了。当然了,如果足够简单,去掉展示器换成其他类型能对应上的也是可以的,需要调整下呈现的内容的绑定,但是一旦按钮中的内容稍微复杂一点就没办法正常呈现内容了。

    TemplateBinding

    TemplateBinding(模板绑定)用于在控件模板中绑定模板内部元素的属性与外部控件的属性之间建立关联。使用TemplateBinding,可以在控件模板内部直接引用外部控件的属性,而无需手动编写绑定表达式,以一种简洁、直接的方式访问外部控件的属性,从而实现属性的传递和同步。

    具体而言,可以通过在控件模板的属性设置中使用TemplateBinding来引用外部控件的属性,如果不进行配置,那相应的属性就会失效。例如,如果我们把模板中Border中的背景色、边框颜色删掉或者设置为默认值,那么我们在使用这个模板的按钮中给这些属性赋值就不会生效了:

    通俗的讲,TemplateBinding 的作用就是在控件模板中绑定控件自身的属性,以实现属性的传递和绑定。

    需要注意的是,TemplateBinding只能绑定到当前控件的相关属性。它用于在控件模板中绑定控件自身的属性,以便将模板中的元素与控件的属性保持一致。比如我们在Border里面多给他建一个Text绑定到Button中显示的Content是会报错的,因为Button本身没有Text这个属性。

    Triggers

    触发器就很好理解了,这里就不再赘述了,大家可以查看 [触发器 Triggers](#触发器 Triggers) 章节内容。

    自定义控件模板

    自定义控件模板有几种方式可以实现,取决于你的需求和使用场景:

    1. 使用 ControlTemplate 属性:对于大多数控件,你可以使用ControlTemplate属性来定义控件的模板。你可以在XAML中直接定义模板,也可以在资源字典中定义模板,并通过键值引用使用。通过修改控件的模板,你可以完全改变其外观和布局。
     
    
    BorderBrush="DarkBlue"
    BorderThickness="2"
    CornerRadius="5"
    Width="150"
    Height="50"
    >

    1. 使用样式 Style :就像默认生成的样式一样,在样式中定义 ControlTemplate,并在需要的地方应用它。这种方式适用于对单个控件的样式和模板进行定义和定制,且样式与模板不需要在多个控件之间共享或重用的情况下。

    2. 不使用样式,直接定义ControlTemplate,并在需要的地方应用它。

     
    
    Background="{TemplateBinding Background}"
    BorderBrush="{TemplateBinding BorderBrush}"
    BorderThickness="{TemplateBinding BorderThickness}"
    CornerRadius="6"
    SnapsToDevicePixels="true">
    Margin="{TemplateBinding Padding}"
    HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
    VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
    Focusable="False"
    RecognizesAccessKey="True"
    SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />

    吧啦吧啦吧啦,还有好多就不一一介绍了🔊

    数据模板

    说完了控件模板,接着来聊一下数据模板,它可能是在开发中比较常用的一种模板。控件模板是针对于控件来设置控件的外观以及行为的,而数据模板用于定义数据对象的可视化呈现方式,主要是针对某种类型的数据而定制的模板,该模板会自动根据绑定的数据类型组织显示的内容。

    DataTemplate用于为底层数据提供可视结构,而ControlTemplate与底层数据无关,仅为控件本身提供可视化布局

    数据模板通常与数据绑定一起使用,将数据对象与界面元素关联起来。通过数据绑定,数据对象的属性值可以自动地显示在界面上,实现数据的呈现和更新。在WPF中,有两种类型的控件支持数据模板:

    • 内容控件通过ContentTemplate属性支持数据模板。内容模板用于显示任何放置在Content属性中的内容。
    • 列表控件(继承自ItemsControl类的控件)通过ItemTemplate或者CellTemplate属性支持数据模板。这个模板
      用于显示作为ItemsSource提供的集合中的每个项(或者来自DataTable的每一行)。

    痕迹g的图:

    数据模板和控件模板差不多,我们先来定义一个数据模板,然后看看如何使用。

    假设需要一个页面呈现用户信息,包括用户的姓名及年龄,存储在Person类中,同时我们还有以下的用户信息:

     
    
    public class Person
    {
    public string Name { get; set; }
    public int Age { get; set; }
    }

    假设我们想要在ListBox中有通过两个TextBlock来分别显示用户的姓名和年龄,我们会怎么做?一般情况下我们会创建一个ListBox控件,然后给它每个节点添加一个用户信息:

     
    
    ...

    或者通过代码创建子节点,至少这样重复的工作会少一点:

     
    
    using System.Collections.Generic;
    using System.Windows;
    using System.Windows.Controls;
    namespace HELLOWPF
    {
    public partial class ControlWindow : Window
    {
    public ControlWindow()
    {
    InitializeComponent();
    List

    WPF 入门笔记 - 03 - 样式基础及模板

    on> personList = new List
    {
    new Person { Name = "Alice", Age = 25 },
    new Person { Name = "Bobby", Age = 30 },
    new Person { Name = "Charlie", Age = 35 },
    new Person { Name = "David", Age = 42 },
    new Person { Name = "Emily", Age = 28 },
    new Person { Name = "Frank", Age = 50 },
    new Person { Name = "Grace", Age = 33 }
    };
    for (int i = 0; i < personList.Count; i++)
    {
    listBitem.Items.Add(new ListBoxItem()
    {
    Content = new StackPanel()
    {
    Children =
    {
    new TextBlock{Text=personList[i].Name},
    new TextBlock{Text=personList[i].Age.ToString()}
    }
    }
    });
    }
    }
    }
    }

    上述两种做法的原理是一样的,都是通过添加子节点一项一项加进去的。虽说也显示出了用户信息,但两种方法都或多多少的有些笨重,同时将数据与UI元素混合在了一起,程序维护和修改起来无疑非常繁琐。

    ItemTemplate

    我们都知道列表类控件一般都会有数据源可以直接添加,ListBox可以通过ItemsSource来绑定数据源,前提是数据源和ListBox得有正确的对应关系对吧,需要确保数据源的类型与列表控件期望的类型兼容。回到上面的例子我们直接绑定,信息是没办法正确显式的,那么就需要调整ListBox中呈现的内容以配合数据的结构,这时就可以通过数据模板来完成对数据格式的适配:

     
    
    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:WPFDemo"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    ...>

    后台代码:

     
    
    using System.Collections.Generic;
    using System.Windows;
    using System.Windows.Controls;
    namespace HELLOWPF
    {
    public partial class ControlWindow : Window
    {
    public ControlWindow()
    {
    InitializeComponent();
    List personList = new List
    {
    new Person { Name = "Alice", Age = 25 },
    new Person { Name = "Bobby", Age = 30 },
    new Person { Name = "Charlie", Age = 35 },
    new Person { Name = "David", Age = 42 },
    new Person { Name = "Emily", Age = 28 },
    new Person { Name = "Frank", Age = 50 },
    new Person { Name = "Grace", Age = 33 }
    };
    listBitem.ItemsSource = personList;
    }
    }
    }

    以上就是一个简单的ItemTemplate数据模板,我们只需要关注数据就可以达到最开始通过很多行重复代码所实现的效果了,同时也把UI元素和数据区分开来。

    当然也可定义通用的数据模板,在需要的时候应用:

     
    
    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:WPFDemo"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    ...>

    通过使用数据模板,我们可以灵活地定义每个项在列表控件中的外观和布局。这为我们提供了一种可重用的方式来自定义列表控件的呈现方式,以满足特定的界面需求。无论是简单的文本显示还是复杂的布局,数据模板都为我们提供了自由创作的空间。

    ItemSource作为属性也可以通过绑定来完成:

     

    我们就只需要在代码中提供数据就行了

    CellTemplate

    DataGrid里通过CellTemplate属性支持数据模板,同样的内容在DataGridView里可以这样定义:

     
    
    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:WPFDemo"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">

    记得在后台代码绑定一下数据源或者用ItemSource绑定数据源♻️

    面板模板

    ItemsControl中,有一个属性ItemsPanel。这个属性的类型是ItemsPanelTemplate,设置这个属性是改变条目控件中的条目在控件里面的排版。

    ListBox中的ItemsPanelVisualizingStackPanelMenuItemsPanel设为WrapPanel;而StatusBar中的ItemsPanel则是使用StackPanel

    我们可以通过面板模板让上面显示信息的ListBox由从上到下排列编程从左到右的水平排列:

     
    
    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">
    Margin="5,0,0,0"
    BorderBrush="#bec5ff"
    BorderThickness="2"
    CornerRadius="6">

    演示比较简单,只是改变了排列方式。