WPF中怎么自定义控件

<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本身就是一种简写)

然后这个控件就创建完成了

Licensed under CC BY-NC-SA 4.0