WPF 你真的会写 XAML 吗?浅谈 ControlTemplate 、DataTemplate 和其它 Template

news/2025/3/28 14:28:19/文章来源:https://www.cnblogs.com/fanbal/p/18784878

WPF 你真的会写 XAML 吗?浅谈 ControlTemplate 、DataTemplate 和其它 Template

本文希望从写死的代码慢慢引入 WPF 的一些机制。

一、Button 难题

我们想要修改 Button 的背景色但是效果非常不理想,默认的 Button 样式是完全无法给大家看的,改造 Button 的方法是借助 Style 在 Template 中自定义 ControlTemplate(Style 并不关键)。

<Style x:Key="Button_Test_Style" TargetType="Button"><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button"><Grid Width="100" Height="40"><Border Background="Aqua" /><TextBlock Text="Hello, WPF!" /></Grid></ControlTemplate></Setter.Value></Setter>
</Style>

Style 在自定义控件的部分并不关键,实际上你完全可以使用 Button.Template 属性展开然后实现同样的效果,但是缺少复用性。

二、猿之手/猴爪难题与依赖属性

虽然它现在变得比较好看了,但是这个 Button 样式完全是一个植物人,成了一个赛博手办,好看固然是好看,甚至是可以执行 Click 事件来进行交互的,但是我们本来可以设置 <Button Background="Red"/> 不能用了。

之所以不能用,原因是因为你在 ControlTemplate 的地方确实没有让 Button 的 Background 发挥作用。

这种行为就好像下面的代码一样:

private void SayHello(string name)
{Console.WriteLine("hello, world!");
}

请问,在这个代码片段,一个名为 SayHello 的函数中,参数 name 的意义是什么?

为此,WPF 引入了依赖属性 DependencyProperty 的机制来让它就像函数的参数意义,能够为 ControlTemplate 里面的东西带来意义。

在 WPF 的控件中,大部分属性都属于依赖属性,对于 Button 来说,Background 是依赖属性,Content 也是。

我们是必不可少要学习自己创建自定义控件和自定义的依赖属性的,但是在这一部分,我们来看一下如何使用自带的依赖属性为原生控件进行自定义。

<Style x:Key="Button_Test_Style" TargetType="Button"><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button"><Grid Width="100" Height="40"><Border Background="{TemplateBinding Background}" /><TextBlock Text="Hello, WPF!" /></Grid></ControlTemplate></Setter.Value></Setter>
</Style>

总而言之,使用 {TemplateBinding XXX} 对控件的依赖属性进行使用,有的时候直接使用 TemplateBinding 可能无法生效,所以你可以使用下面的平替,下面这种的泛用性会更强,但是没有 TemplateBinding 的写法那么方便,属于是比较 Hack 的写法,我们在后面的介绍中有一处只有它才能实现的效果。

<Style x:Key="Button_Test_Style" TargetType="Button"><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button"><Grid Width="100" Height="40"><Border Background="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Background}" /><TextBlock Text="Hello, WPF!" /></Grid></ControlTemplate></Setter.Value></Setter>
</Style>

三、更好看的样子

当你知道了 Background 和 Content 都是依赖属性之后,我们目前没有做 TextBlock 呈现内容的参数化模板绑定,但是我想你也应该知道怎么做了。

你在进行编写的时候,可能会遇到智能提示的问题,你会发现你在为 TextBlock 的 Text 进行绑定的时候,可能会发现你在 VS 的智能提示的小窗里并没有办法找到 Content,这并不是 VS 出现了 BUG,VS 的消极反应也并不是在否定你,我们打算在美化完 UI 后,再来细讲为什么 VS 会如此的不配合,为什么在 TextBlock 的 Text 中绑定 Content 是一个不算对也不算错的行为。

<Style x:Key="Button_Test_Style" TargetType="Button"><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button"><Grid><Border Background="{TemplateBinding Background}" CornerRadius="5" /><TextBlockHorizontalAlignment="Center"VerticalAlignment="Center"Text="{TemplateBinding Content}" /></Grid></ControlTemplate></Setter.Value></Setter>
</Style>

你可以这样使用:

<ButtonWidth="100"Height="40"Background="LightGreen"Content="Hello WPF!"Style="{StaticResource Button_Test_Style}" />

四、我们的 IDE 到底在抗拒什么,会不配合我们的 XAML 编写?

在 VS 的 WPF 编辑环境中对于 控件模板 ControlTemplate 和 控件绑定的智能提示来自于 TargetType="Button" 这边对控件的指定,如果没有指定类型,智能提示会完全没有办法给你补全什么有效代码。

我们在写 Background 的时候很顺利但是在 写 Text 的时候遇到了 VS 的阻挠,即便如此我们硬写还是写出来了。

效果还不错是吧?

你知道吗,WPF 的很多控件是支持嵌套的,就像下面这样:

代码就像这样:

<Button Width="100" Height="100"><TextBlock Text="hello, world!" />
</Button>

你会发现你无法为它再赋予 Content 了,编辑器会告诉你属性重复,也就是说,你在 xaml 里面写的 Text 属性为"hello, world!" 的文本块 TextBlock 控件,已经是 Button 的 Content 属性了。

所以,我想要说什么?

Content 这个依赖属性能描述的不仅是一个字符串,它实际上能描述一个对象,它的类型其实是 object,我们去看它的定义就可以知道:

public object Content { get; set; }

正是因为 Content 是 object 类型,而 Text 属性接收的要求是 string 字符串,所以 VS 在智能提示的时候并不认为它们俩合适,所以我们在智能提示的时候根本找不到它。

那,为什么我们直接写还是能够生效?

对于 Text 这种字符串类型来说,我们恰好传的是 string 字符串这个对象,瞎猫碰上死耗子,自然就没有发现问题。

所以,如果我们的自定义样式有内部嵌套的对象,它在使用 TemplateBinding 写法的我们的样式里,是完全没有反应的。

如果说你真的要让只能接收到 string 的 Text 依赖属性,被迫吃下那么一坨,不就是 object 吗,只要是个 object ,使用 ToString() 转成字符串不就好了么。于是,你可以使用上文提到的那种非常冗长的写法。

于是就会有这样的效果:

<Style x:Key="Button_Test_Style" TargetType="Button"><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button"><Grid><Border Background="{TemplateBinding Background}" CornerRadius="5" /><TextBlockHorizontalAlignment="Center"VerticalAlignment="Center"Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Content}" /></Grid></ControlTemplate></Setter.Value></Setter>
</Style>
<ButtonWidth="100"Height="100"Background="LightGreen"Style="{StaticResource Button_Test_Style}"><TextBlock Name="PART_TextBlock" Text="hello, world!" />
</Button>

五、真正的 Button

为了实现 Button 内部控件的嵌套和对象的嵌套呈现,用我们目前的 TextBlock 来呈现内容是完全不可取的。
实际上一个标准的 Button 实现会使用 ContentPresenter 来呈现,ContentPresenter 本身具备的 Content 依赖属性才是 Button 的 Content 的依赖属性最终的去处。

<Style x:Key="Button_Test_Style" TargetType="Button"><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button"><Grid><Border Background="{TemplateBinding Background}" CornerRadius="5" /><ContentPresenterHorizontalAlignment="Center"VerticalAlignment="Center"Content="{TemplateBinding Content}" /></Grid></ControlTemplate></Setter.Value></Setter>
</Style>

这个样式的效果是这样的:

代码:

<ButtonWidth="100"Height="100"Background="LightGreen"Style="{StaticResource Button_Test_Style}"><TextBlock Name="PART_TextBlock" Text="hello, world!" />
</Button>

六、Content object 的样子

1. 若我掏出自定义对象,你该如何应对?

我们现在能知道的是,对于字符串类型的 Content 会显示一串文字,如果填入的是控件内容,它会显示控件 UI 的样子,用来为按钮创建图标等相关需求的时候会非常有用。

可是,object 也就意味着是所有类型,我们自己的 class 实例对象给它会是什么样子?

让我们在 Button 初始化的时候在 C# Code-Behind 代码的部分创建一些内容吧!

这是我们自定义的类:

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

请注意 xaml 中关于 Loaded="Button_Loaded" 的部分。

<ButtonWidth="100"Height="100"Background="LightGreen"Loaded="Button_Loaded"Style="{StaticResource Button_Test_Style}" />

这是事件订阅后要执行的事情。

private void Button_Loaded(object sender, RoutedEventArgs e)
{var button = sender as Button;if (button is null) return;// 虽然可以写成 button!.Content 但是怕各位看不懂button.Content = new Person() { Name = "小明", Age = 18 };
}

这是效果:

因为我们的项目叫做 WPFPlayground,所以 Person 这个对象被 ToString()后得到的结果是 WPFPlayground.Person 因为尺寸有限所以目前呈现的是这样。

我们希望把信息显示出来,你可以这样完善 Person 的内容:

public class Person
{public string Name { get; set; }public int Age { get; set; }public override string ToString(){return $"{Name}: {Age} !!!!!";}
}

效果就会变成这样:

2. 若我想要给这个数据自定义外观,你又该如何应对?

但是有的时候,我们希望呈现的数据也是有布局和 UI 的。
因为自定义数据的属性并不是依赖属性,所以上面在控件模板中介绍的那些方法,TemplateBinding 之类的做法在这边就完全失效了。

面对这个数据的外观定义,你要使用 ContentTemplate 来做。

当然,ContentTemplate 内容模板和 ControlTemplate 控件模板长得很像,但是不一样的概念,ContentTemplate 和 Content 是一对内容,相互配合才能实现最好的效果,作为 Content 的好兄弟,ContentTemplate 自然也是依赖属性,在 ContentPresenter 中发挥作用。

我们说了这么多话,到底想要说什么?

我想说的是,光写 Button 中的 ContentTemplate 是没有用的,因为实际承担工作的是 你的 ControlTemplate 控件模板的 ContentPresenter,你需要把这个依赖属性作为参数传递进去,写成这个样子:

<Style x:Key="Button_Test_Style" TargetType="Button"><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button"><Grid><Border Background="{TemplateBinding Background}" CornerRadius="5" /><ContentPresenterHorizontalAlignment="Center"VerticalAlignment="Center"Content="{TemplateBinding Content}"ContentTemplate="{TemplateBinding ContentTemplate}" /></Grid></ControlTemplate></Setter.Value></Setter>
</Style>

然后,我们将开始面对 DataTemplate 了。

我们为 Button 在更新了 Style 后,编写了对应的 ContentTemplate。
ContentTemplate 的类型是 DataTemplate,集合容器所采用的 ItemTemplate 类型也是 DataTemplate 其实也是 DataTemplate,因为其中的原理其实就是每一项套了一个 ContentPresenter 内容呈现器。

<ButtonWidth="100"Height="100"Background="LightGreen"Loaded="Button_Loaded"Style="{StaticResource Button_Test_Style}"><Button.ContentTemplate><DataTemplate DataType="local:Person"><StackPanel><TextBlock Background="Yellow" Text="{Binding Name}" /><TextBlockBackground="Aqua"FontSize="20"Text="{Binding Age}" /></StackPanel></DataTemplate></Button.ContentTemplate></Button>

会变成这样的效果:

于是,在控件和主题上,你可以为 ControlTemplate 方面打一个坚实的底子用于项目风格的复用,而在自定义数据特别是业务数据的呈现上,以 ContentTemplate 为代表的 DataTemplate,会为你带来业务上的拓展性。

七、所谓集合面板 ItemsControl、ListBox

1. 每一项的外观样子使用 ItemTemplate 定义

我们来创建一个毫无通知能力,只是好看的 ViewModel,并不实现 INotifyPropertyChanged。

public class MainViewModel
{public List<Person> Persons { get; set; }public MainViewModel(){Persons = new List<Person>(){new Person(){ Name = "小明", Age = 18},new Person(){ Name = "小红", Age = 17},new Person(){ Name = "小黄", Age = 16},new Person(){ Name = "小亮", Age = 11},new Person(){ Name = "小军", Age = 19},new Person(){ Name = "小帅", Age = 30},new Person(){ Name = "小马", Age = 6},};}
}

我相信你知道怎么绑定上下文,所以这边只做了绑定的部分:

<ItemsControl ItemsSource="{Binding Persons}" />

我们来看一下效果:

你可以注意到的是,集合容器控件中呈现的样子,和我们刚才描述的关于 ContentPresenter 和 Content 的机制是完全一致的。

让我们把上面写的 ContentTemplate 中的 DataTemplate 交给 ItemsControl 的 ItemTemplate 中去。

<ItemsControl ItemsSource="{Binding Persons}"><ItemsControl.ItemTemplate><DataTemplate DataType="local:Person"><StackPanel><TextBlock Background="Yellow" Text="{Binding Name}" /><TextBlockBackground="Aqua"FontSize="20"Text="{Binding Age}" /></StackPanel></DataTemplate></ItemsControl.ItemTemplate>
</ItemsControl>

现在的效果就是这样的:

具体 ItemTemplate 生效的原因来自于 ContentPresenter,参看源码:https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/ItemsControl.cs,a32a4ab17d3998f0,references

2. 容器的外观

你可能会对 ItemsControl 和 ListBox 的默认的 StackPanel 纵向布局感到不满。
这个时候你可以使用 ItemsPanel 创建 ItemsPanelTemplate 对象,它是属于 ControlTemplate 相似的,和数据无关的 Template。

默认情况如果写出来是这样的:

<ItemsControl ItemsSource="{Binding Persons}"><ItemsControl.ItemTemplate><DataTemplate DataType="local:Person"><StackPanel><TextBlock Background="Yellow" Text="{Binding Name}" /><TextBlockBackground="Aqua"FontSize="20"Text="{Binding Age}" /></StackPanel></DataTemplate></ItemsControl.ItemTemplate><ItemsControl.ItemsPanel><ItemsPanelTemplate><StackPanel /></ItemsPanelTemplate></ItemsControl.ItemsPanel></ItemsControl>

我如果在使用它,一般我会使用它的 WrapPanel 和 Horizontal StackPanel。

2.1 WrapPanel 效果

效果:

WrapPanel 的可折叠性。

代码:

<ItemsControl.ItemsPanel><ItemsPanelTemplate><WrapPanel /></ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

2.2

效果:

代码:

<ItemsControl.ItemsPanel><ItemsPanelTemplate><StackPanel Orientation="Horizontal" /></ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

八、所谓 DataGrid 和 CellTemplate

这是 DataGrid 的默认样子:

<DataGrid ItemsSource="{Binding Persons}" />

除了显示一些栏位字段属性之外,你可能希望能本来的数据源中显示一些别的控件,比如我们上面定义的 DataTemplate,来帮助我们更加可视化的看到数据。

你可以实现这样的效果:

代码如下:

<DataGrid ItemsSource="{Binding Persons}"><DataGrid.Columns><DataGridTemplateColumn Header="可视化的样子"><DataGridTemplateColumn.CellTemplate><DataTemplate DataType="local:Person"><StackPanel><TextBlock Background="Yellow" Text="{Binding Name}" /><TextBlockBackground="Aqua"FontSize="20"Text="{Binding Age}" /></StackPanel></DataTemplate></DataGridTemplateColumn.CellTemplate></DataGridTemplateColumn></DataGrid.Columns>
</DataGrid>

推荐用法:可以用来显示重要程度比如红色绿色、进度百分比等等效果,具体想要可视化什么取决于业务需求。

九、总结

属性名 类型 用法
Template ControlTemplate 在 Control 控件中的 Template 属性,是 WPF 最为基础的内容,是所有控件的可复用性的保障,通常和 Style 搭配,编写 ControlTemplate 就好比在 xaml 中编写函数一样,具有非常重要的工程意义。
ContentTemplate DataTemplate 所有 ContentControl(如 Button)实现,会在控件模板 ControlTemplate 中的某些 ContentPresenter 将会承担解析和呈现它们的任务,前提是你需要传递过去。
ItemTemplate DataTemplate 集合容器之所以能够呈现内容就是因为它每一项都是一个 ContentPresenter,容器将会把定义的 ItemTemplate 信息交给每一个 ContentPresenter 的 ContentTemplate 中,把每一项的数据信息交给 ContentPresenter 的 Content 中,最后实现列表项的呈现,具体会有 ItemsPresenter 的参与
ItemsPanel ItemsPanelTemplate 属于 ItemsControl 和 ListBox 等数据呈呈现容器,用于描述每一项应该如何排布,默认是 StackPanel Vertical 布局,你完全可以改成 WrapPanel,Canvas,StackPanel Horizontal,Grid 等等的布局容器
CellTemplate DataTemplate WPF 出于 DataGrid 更好可视化的角度为你提供的办法

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/902397.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从《黑神话:悟空》看项目管理的关键策略与突破点

《黑神话:悟空》的火爆,凸显了游戏项目管理的重要性。当下游戏行业竞争激烈,面临着诸多挑战。而日事清凭借支持敏捷管理、需求与任务管理、BUG管理、多项目并行处理、知识沉淀,以及项目统计与考核等一系列功能,助力游戏研发团队实现高效协作,推动敏捷开发。8月20日上午10…

边缘检测及Canny算法

对边缘的直观理解边缘有助于我们对图像进行语义理解。直观上,边缘发生在图像强度值变化剧烈的地方 如何描述变化?自然是用导数/梯度如上图,我们对图中的信号在水平方向上求导,可以得到右侧的导数图像,可以看到,它在边缘处由于信号发生剧烈变化,导数产生了极值。因此,导…

​工贸一体企业全链路管理应用实例:业务流程优化+任务智能分配+项目进度追踪

河源集品科技主营小型家电出口,自主研发实力强。发展中项目管理遇难题,试用多款工具无果。经对比,选用日事清。其功能全面,匹配公司流程、绩效等管理需求,能一键发起流程、自动流转待办,性价比高。针对订单、生产流程、多部门协作挑战,日事清给出对应方案,试用效果出色…

如何使用FPGA开发

介绍 与传统的微控制器相比,FPGA(现场可编程门阵列)是独一无二的,因为它们不执行顺序指令。相反,它们由一组可配置的逻辑块组成,这些逻辑块可以被重新编程以执行自定义的数字逻辑功能。这使得FPGA可以并行执行多个操作,使其在信号处理、数据处理和实时控制等特定任务中非常…

3月10号-3月16号笔记三合一+两篇面经

2025.3.10 学习 八股 blocked和waiting有啥区别 触发条件:线程进入BLOCKED状态通常是因为试图获取一个对象的锁(monitor lock),但该锁已经被另一个线程持有。这通常发生在尝试进入synchronized块或方法时,如果锁已被占用,则线程将被阻塞直到锁可用。线程进入WAITING状态是…

Dicom纯js的三维重建影像浏览器

主要功能介绍 实现通过浏览器浏览Dicom影像阅片。主要功能:支持标准DIcom影像的2D浏览,预设窗位,伪彩,序列间,序列内多种布局方式。 影像处理,提供影像翻图、缩放、移动、透镜、反相、旋转、截图等操作 影像测量,提供箭头、直线、十字架、角度、Cobb、心胸比、椭圆、矩形…

电子签借贷真实吗?315报道引发的行业地震!电子签到底冤不冤?

看了315的相关报道后,曝光套路贷本来是正向的,却将电子签和高利贷混为一谈。这种无意识的混淆不仅会误导大众,也不利于新兴技术的健康发展。电子签作为一种替代纸质合同签署的新兴技术,有着独特的优势和重要的意义。在过去,不同主体之间签订合同往往需要面对面进行签署,这…

C#反编译

偶遇C#逆向题,IDA拼尽全力无法战胜,只给我一团乱码。 这时可以使用dnSpy来反编译: https://github.com/dnSpy/dnSpy/releases/tag/v6.1.8 1.dnSpy使用方法: 将你要反编译的程序的整个文件夹拖到那里,似乎只有.dll那里才能看到源码, 例题:https://www.nssctf.cn/problem/3…

Windows11跳过Microsoft账户登录方法

新版Win11默认必须联网并且使用Microsoft账户登录,不然不能进去系统,以下是不联网进入方法1、在开机出现登录界面时,按下Ctrl+Shift+F3快捷键,然后系统会自动重启,重启 后会出现一个不需要登录Microsoft账户的启动界面,直接进入Windows11系统,系统中会弹窗,点确定。 2、…

android studio 真机wifi调试经验汇总

--------------- 2025/03/11 真机连接连不上wifi调试可能的原因: 1.手机没有开启开发者模式 2.手机没有开启usb调试以及wifi调试 3.手机应该改为传输文件模式 4.手机和电脑应该连接同一个wifi 5.在电脑cmd窗口输入adb connect ip:port(前提电脑需要配好adb环境)adb连接手机教…

广度优先搜索(BFS)走迷宫:

广度优先搜索(BFS)走迷宫: 广度优先搜索(BFS)是一种经典的图遍历算法,在解决路径查找、迷宫问题、拓扑排序等问题 前置知识:队列: 队列与栈类似,但是先进先出,而不是栈的先进后出。 原理: 相当于爆破,将所有的位置都走一遍 通用迷宫代码实现: #include <iostre…

TEA与XTEA算法:

TEA算法使用64位的明文分组和128位的密钥,它使用Feistel分组加密框架,需要进行 64 轮迭代,尽管作者认为 32 轮已经足够了。该算法使用了一个神秘常数δ作为倍数,它来源于黄金比率,以保证每一轮加密都不相同。但δ的精确值似乎并不重要,这里 TEA 把它定义为 δ=「(√5 - 1…