<0x00> 前言
WPF开发中,总是会有些创建自定义控件的需求
毕竟原生的控件总是不够用的
<0x01> 原理
WPF使用xaml作为设计语言
而xaml负责的可以简单理解成将控件实例化到对应的布局上,然后设定控件的一些属性
既然是实例化,那么本质上就是创建了一个新的对象
<!--随便定义一个按钮-->
<button Content="123"/>
其实在编译器看来,就相当于这样
Button b = new();
所以我们自定义控件,首先需要有一个类定义控件的属性,然后用xaml去描述控件行为
那么怎么用xaml描述控件行为呢
在修改控件的style的时候,我们可以把自己定义的style写到一个ResourceDictionary中
然后通过合并ResourceDictionary来将自己style应用到不同的地方
xaml描述控件行为其实也就是写一个style,所以可以采用一样的思路
<0x02> 示例:带标签的文本输入框
这个感觉还是很常见的需求,有些时候需要文本输入框前面带一个标签,表示输入的是什么
也就是说,我们要做的控件跟下面xaml代码效果是等价的
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
VerticalAlignment="Center" />
<TextBox Grid.Column="1" />
</Grid>
定义控件
首先需要定义一个类,表示我们的类与其他类的不同
// Controls/TextBoxWithLable/TextBoxWithLable.cs
using System.Windows.Controls;
namespace XamlTest.Controls;
public class TextBoxWithLable : Control
{
// 后面还有别的代码,但这里先不需要
}
这样就可以了
描述控件
然后创建一个资源字典,用来描述我们的控件
<!-- Controls/TextBoxWithLable/TextBoxWithLable.xaml -->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:XamlTest.Controls">
<Style x:Key="Default_TextBoxWithLableStyle" TargetType="{x:Type controls:TextBoxWithLable}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:TextBoxWithLable}">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
VerticalAlignment="Center" />
<TextBox Grid.Column="1" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style BasedOn="{StaticResource Default_TextBoxWithLableStyle}" TargetType="{x:Type controls:TextBoxWithLable}" />
</ResourceDictionary>
就像前面说的,通过style的模板来让xaml知道需要实例化什么控件
这里先是定义了一个Default_TextBoxWithLableStyle的style,然后再写全局应用
这样写的话万一用户想再基于默认样式自定义的话就比较方便了
合并资源字典
上面的写好之后,如果直接使用会发现还是显示不了的
这是因为我们还没有合并资源字典
这里我建议在控件的文件夹中加一个文件,其中去合并所有控件的资源字典
然后再在app.xaml中合并这个整合的资源字典文件
毕竟控件开发一般会开发很多,全写app.xaml会很乱
<!-- Controls/ControlsDictionary.xaml -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Controls/TextBoxWithLable/TextBoxWithLable.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
然后在app.xaml中合并
<!-- app.xaml -->
<Application
x:Class="XamlTest.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:XamlTest.Controls"
xmlns:local="clr-namespace:XamlTest"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Controls/ControlsDictionary.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
这里的source属性出现的很像网址的东西是资源url,我暂时没研究的这么全
为控件创建属性
现在控件有了,但是如果在控件中输入文字我们是很难获取到的
而且我们也修改不了文本框前面的lable部分
这是因为我们还没有为控件创建属性,这时候就需要修改TextBoxWithLable.cs文件了
我们需要为我们的控件添加依赖属性
// Controls/TextBoxWithLable/TextBoxWithLable.cs
using System.Windows;
using System.Windows.Controls;
namespace XamlTest.Controls;
public class TextBoxWithLable : Control
{
#region LableText_propdp
public string LableText
{
get { return (string)GetValue(LableTextProperty); }
set { SetValue(LableTextProperty, value); }
}
public static readonly DependencyProperty LableTextProperty =
DependencyProperty.Register("LableText", typeof(string), typeof(TextBoxWithLable), new PropertyMetadata(null));
#endregion LableText_propdp
#region InputText_propdp
public string InputText
{
get { return (string)GetValue(InputTextProperty); }
set { SetValue(InputTextProperty, value); }
}
public static readonly DependencyProperty InputTextProperty =
DependencyProperty.Register("InputText", typeof(string), typeof(TextBoxWithLable), new PropertyMetadata(null));
#endregion InputText_propdp
}
这里讲一个小技巧
创建依赖属性的代码确实多,但也挺公式的
所以vs提供了一个模板,只要输入propdp然后敲两下tab就可以插入模板
然后逐个替换即可
然后修改TextBoxWithLable.xaml,绑定我们的依赖属性
<!-- Controls/TextBoxWithLable/TextBoxWithLable.xaml -->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:XamlTest.Controls">
<Style x:Key="Default_TextBoxWithLableStyle" TargetType="{x:Type controls:TextBoxWithLable}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:TextBoxWithLable}">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="{TemplateBinding LableText}" />
<TextBox
Grid.Column="1"
Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=InputText, Mode=TwoWay}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style BasedOn="{StaticResource Default_TextBoxWithLableStyle}" TargetType="{x:Type controls:TextBoxWithLable}" />
</ResourceDictionary>
这里需要注意的是,TemplateBinding是单向绑定
也就是相当于只能绑定源->控件,这对TextBlock是没啥问题的
但对于TextBox,我们希望里面的内容也可以传回来,这时候就不能只靠TemplateBinding
所以TextBox这里的绑定就写得很长,主要就是要写一个Mode
(TemplateBinding本身就是一种简写)
然后这个控件就创建完成了