1. 前言

我去年写过一个在UWP自定义控件的系列博客,大部分的经验都可以用在WPF中(只有一点小区别)。这篇文章的目的是快速入门自定义控件的开发,所以尽量精简了篇幅,更深入的概念在以后介绍各控件的文章中实际运用到才介绍。

ContentControl是WPF中最基础的一种控件,Window、Button、ScrollViewer、Label、ListBoxItem等都继承自ContentControl。而且ContentControl的结构十分简单,很适合用来入门自定义控件。

这篇文章通过自定义一个ContentControl来介绍自定义控件的一些基础概念,包括自定义控件的基本步骤及其组成。

2. 什么是自定义控件

在开始之前首先要了解什么是自定义控件以及为什么要用自定义控件。

在WPF要创建自己的控件(Control),通常可以使用自定义控件(CustomControl)或用户控件(UserControl),两者最大的区别是前者可以通过ControlTemplate对控件的外观灵活地进行定制。如在下面的例子中,通过ControlTemplate将Button改成一个圆形按钮:

<Button Content="Orginal" Margin="0,0,20,0"/>
<Button Content="Custom">
<Button.Template>
<ControlTemplate TargetType="Button">
<Grid>
<Ellipse Stroke="DarkOrange" StrokeThickness="3" Fill="LightPink"/>
<ContentPresenter Margin="10,20" Foreground="White"/>
</Grid>
</ControlTemplate>
</Button.Template>
</Button>

控件库中通常使用自定义控件而不是用户控件。

3. 创建自定义控件

ContentControl最简单的派生类应该是HeaderedContentControl了吧,这篇文章会创建一个模仿HeaderedContentControl的MyHeaderedContentControl,它继承自ContentControl并添加了一些细节。

在“添加新项”对话框选择“自定义控件(WPF)”,名称改为"MyHeaderedContentControl.cs"(用My-做前缀是十分差劲的命名方式,但只要一看到这种命名就明白这是个测试用的东西,不会和正规代码搞错,所以我习惯了测试用代码就这样命名。),点击“添加”后VisualStudio会自动创建两个文件:MyHeaderedContentControl.cs和Themes/Generic.xaml。

编译通过后在XAML上添加MyHeaderedContentControl的命名空间即可使用这个控件:

<Window x:Class="CustomControlDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControlDemo">
<Grid>
<local:MyHeaderedContentControl Content="I am a new control" />
</Grid>
</Window>

在添加新项时,小心不要和“Windows Forms”里的“自定义控件”搞混。

4. 自定义控件的组成

自定义控件通常由代码和DefaultStyle两部分组成,它们分别位于VisualStudio创建的MyHeaderedContentControl.cs和Themes/Generic.xaml两个文件中。

4.1 代码

public class MyHeaderedContentControl: Control
{
static MyCustomControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyHeaderedContentControl), new FrameworkPropertyMetadata(typeof(MyHeaderedContentControl)));
}
}

控件代码负责定义控件的结构和行为。MyHeaderedContentControl.cs的代码如上所示,只包含一个静态构造函数及一句 DefaultStyleKeyProperty.OverrideMetadata。DefaultStyleKey是用于查找控件样式的键,没有这句代码控件就找不到默认样式。

4.2 DefaultStyle

<Style TargetType="{x:Type local:MyHeaderedContentControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MyHeaderedContentControl}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

在第一次创建控件后VisualStudio会自动创建Themes/Generic.xaml,并且插入上面的XAML。这段XAML即MyCustomControl的DefaultStyle,它负责定义控件的外观及属性的默认值。注意其中两个TargetType="{x:Type local:MyHeaderedContentControl}",第一个用于匹配MyHeaderedContentControl.cs中的DefaultStyleKey,第二个确定ControlTemplete针对的控件类型,两个都不可以移除。Style的内容是一组Setter的集合,除了Template外,还可以添加其它的Setter指定控件的各属性默认值。

注意,不可以为这个Style设置x:Key。

5. 在DefaultStyle上实现ContentControl的基础部分

接下来将MyHeaderedContentControl的父类修改为ContentControl。

如果只看常用属性的话,ContentControl的定义可以简化为以下代码:

[ContentProperty("Content")]
public class ContentControl : Control
{
public static readonly DependencyProperty ContentProperty;
public static readonly DependencyProperty ContentTemplateProperty; public object Content { get; set; }
public DataTemplate ContentTemplate { get; set; } protected virtual void OnContentChanged(object oldContent, object newContent);
protected virtual void OnContentTemplateChanged(DataTemplate oldContentTemplate, DataTemplate newContentTemplate);
}

对应的DefaultStyle可以如下实现:

<Style TargetType="{x:Type local:MyHeaderedContentControl}">
<Setter Property="IsTabStop"
Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:MyHeaderedContentControl">
<ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

DefaultStyle的内容也不多,简单讲解一下。

ContentPresenter

ContentPresenter用于显示内容,默认绑定到ContentControl的Content属性。基本上所有ContentControl中都包含一个ContentPresenter。ContentPresenter直接从FrameworkElement派生。

TemplateBinding

用于单向绑定ControlTemplate所在控件的功能属性,例如Margin="{TemplateBinding Padding}"几乎等效于Margin="{Binding Margin,RelativeSource={RelativeSource Mode=TemplatedParent},Mode=OneWay}",相当于一种简化的写法。但它们之间有如下不同:

  • TemplateBinding只能用在ControlTemplate中。
  • TemplateBinding的源和目标属性都必须是依赖属性。
  • TemplateBinding不能使用TypeConverter,所以源属性和目标属性必须为相同的数据类型。

通常在ContentPresenter上使用TemplateBinding的属性不会太多,因为很大一部分Control的属性的值都可继承,即默认使用VisualTree上父节点所设置的属性值,譬如字体属性(如FontSize、FontFamily)、DataContext等。

除了可继承值的属性,需要适当地将ControlTemplate中的元素属性绑定到所属控件的属性,例如Margin="{TemplateBinding Padding}",这样可以方便控件的使用者通过属性调整UI。

IsTabStop

了解IsTabStop的作用有助于处理好自定义控件的焦点。

<GroupBox>
<TextBox />
</GroupBox>
<GroupBox>
<TextBox />
</GroupBox>

在上面这个UI中,在第一个TextBox获得焦点时按下Tab后第二个TextBox将获得焦点,这很自然。但如果换成下面这段XAML:

<ContentControl>
<TextBox />
</ContentControl>
<ContentControl>
<TextBox />
</ContentControl>

结果就如上面截图显示,第二个TextBox没有获得焦点,焦点被包含它的ContentControl获取了,要再按一次 Tab TextBox才能获得焦点。这是由于ContentControl的IsTabStop属性默认为True。IsTabStop指示是否将某个控件包含在 Tab 导航中,Tab的导航顺序是用深度优先算法搜索VisualTree上的Control,所以ContentControl优先获得了焦点。如果ContentControl作为一个容器的话(如GroupBox)IsTabStop属性都应该设置为False。

通过Setter改变默认值

通常从父控件继承而来的属性很少在构造函数中设置默认值,而是在DefaultStyle的Setter中设置默认值。MyHeaderedContentControl为了将IsTabStop改为False而在Style添加了Property="IsTabStop"的Setter。

6. 添加Header和HeaderTemplate依赖属性

现在模仿HeaderedContentControl为MyHeaderedContentControl添加Header和HeaderTemplate属性。

在自定义控件中添加属性时应尽量使用依赖属性(有些只读属性可以使用CLR属性),因为只有依赖属性才可以作为Binding的Target。WPF中创建依赖属性可以做到很复杂,而再简单也要好几行代码。在自定义控件中创建依赖属性通常包含以下几部分:

  1. 注册依赖属性并生成依赖属性标识符。依赖属性标识符为一个public static readonly DependencyProperty字段。依赖属性标识符的名称必须为“属性名+Property”。在PropertyMetadata中指定属性默认值。

  2. 实现属性包装器。为属性提供 CLR get 和 set 访问器,在Getter和Setter中分别调用GetValue和SetValue,除此之外Getter和Setter中不应该有其它任何自定义代码。

  3. 需要监视属性值变更。在PropertyMetadata中定义一个PropertyChangedCallback方法,因为这个方法是静态的,可以再实现一个同名的实例方法(可以参考ContentControl的OnContentChanged方法)。

/// <summary>
/// 获取或设置Header的值
/// </summary>
public object Header
{
get => (object)GetValue(HeaderProperty);
set => SetValue(HeaderProperty, value);
} /// <summary>
/// 标识 Header 依赖属性。
/// </summary>
public static readonly DependencyProperty HeaderProperty =
DependencyProperty.Register(nameof(Header), typeof(object), typeof(MyHeaderedContentControl), new PropertyMetadata(default(object), OnHeaderChanged)); private static void OnHeaderChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var oldValue = (object)args.OldValue;
var newValue = (object)args.NewValue;
if (oldValue == newValue)
return; var target = obj as MyHeaderedContentControl;
target?.OnHeaderChanged(oldValue, newValue);
} /// <summary>
/// Header 属性更改时调用此方法。
/// </summary>
/// <param name="oldValue">Header 属性的旧值。</param>
/// <param name="newValue">Header 属性的新值。</param>
protected virtual void OnHeaderChanged(object oldValue, object newValue)
{
}

上面代码为MyHeaderedContentControl添加了Header属性(HeaderTemplate的代码大同小异就不写出来了)。请注意我使用object类型,在WPF中Content、Header、Title这类属性最好是object类型,这样不仅可以使用文字,还可以是UIElement如图片或其他控件。protected virtual void OnHeaderChanged(object oldValue, object newValue)目前只是个空函数,但为了派生类着想不要吝啬这一行代码。

依赖属性的默认值可以在注册依赖属性时在PropertyMetadata中设置,通常为属性类型的默认值,也可以在DefaultStyle的Setter中设置,不推荐在构造函数中设置。

依赖属性的定义代码比较复杂,我一直都是用代码段生成,可以参考我另一篇博客为附加属性和依赖属性自定义代码段(兼容UWP和WPF)

添加依赖属性后再更新控件模板,这个控件就基本完成了。

<ControlTemplate TargetType="local:MyHeaderedContentControl">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ContentPresenter Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}" />
<ContentPresenter Grid.Row="1"
ContentTemplate="{TemplateBinding ContentTemplate}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Grid>
</Border>
</ControlTemplate>

7. 结语

虽然尽量精简,但结果这篇文章仍是太长,而且很多关键的技术仍未介绍到。

更深入的内容会在后续文章中逐渐介绍,敬请期待。

8. 参考

控件自定义

Silverlight 控件自定义

Customizing the Appearance of an Existing Control by Using a ControlTemplate

[WPF自定义控件]从ContentControl开始入门自定义控件的更多相关文章

  1. WPF教程十二:了解自定义控件的基础和自定义无外观控件

    这一篇本来想先写风格主题,主题切换.自定义配套的样式.但是最近加班.搬家.新租的房子打扫卫生,我家宝宝6月中旬要出生协调各种的事情,导致了最近精神状态不是很好,又没有看到我比较喜欢的主题风格去模仿的, ...

  2. WPF:理解ContentControl——动态添加控件和查找控件

    WPF:理解ContentControl--动态添加控件和查找控件 我认为WPF的核心改变之一就是控件模型发生了重要的变化,大的方面说,现在窗口中的控件(大部分)都没有独立的Hwnd了.而且控件可以通 ...

  3. WPF样式(Style)入门

    原文:WPF样式(Style)入门 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/qq_34802416/article/details/78231 ...

  4. WPF 自定义控件,在ViewModel里面获取自定义控件的值

    上图: 用户自定义CS里面代码如下: 自定义控件XAML里面的代码如下: 调用用户自定义控件的页面代码如下: CItySelected的属性值就是我们点击确定按钮以后得到的值,通过双向绑定在VIewM ...

  5. Wpf实现图片自动轮播自定义控件

    近来,公司项目需要,需要写一个自定义控件,然后就有下面的控件产生.样式没有定义好,基本功能已经实现.1.创建为自定义控件的XAML页面.下面为后台代码 using System; using Syst ...

  6. WPF中用户控件对比自定义控件(UserControl VS CustomControl)

    接着这篇文章(http://www.cnblogs.com/shiyue/archive/2013/02/02/2889907.html)写: 用户控件(组合) 用于在一个项目中使用多次 自定义控件( ...

  7. android自定义控件的一个思路-入门

    转自:http://blog.sina.com.cn/s/blog_691051e10101a3by.html   很多时候没有我们需要使用的控件,或者控件并不美观.比如这个滑动开关,这是androi ...

  8. 编写Qt Designer自定义控件(二)——编写自定义控件界面

    接上文:编写Qt Designer自定义控件(一)——如何创建并使用Qt自定义控件 既然是控件,就应该有界面,默认生成的控件类只是一个继承了QWidget的类,如下: #ifndef LOGLATED ...

  9. [WPF 自定义控件]自定义控件库系列文章

    Kino.Toolkit.Wpf Kino.Toolkit.Wpf是一组简单实用的WPF控件与工具,用于介绍自定义控件的入门.相关博客地址如下: 开始一个自定义控件库项目 介绍开始一个自定义控件库项目 ...

随机推荐

  1. C++的异常捕获

    听课笔记: #define _CRT_SECURE_NO_WARNINGS #include<iostream> using namespace std; void fun() { ;// ...

  2. 纯CSS3实现的动感菜单效果

    1. [代码] 纯CSS3实现的动感菜单效果 <!DOCTYPE html><head><meta http-equiv="Content-Type" ...

  3. java:stack栈: Stack 类表示后进先出(LIFO)的对象堆栈

    //Stack 类表示后进先出(LIFO)的对象堆栈 //它提供了通常的 push 和 pop 操作,以及取栈顶点的 peek 方法.测试堆栈是否为空的 empty 方法.在堆栈中查找项并确定到栈顶距 ...

  4. HashOperations

    存储格式:Key=>(Map<HK,HV>) 1.put(H key, HK hashKey, HV value) putAll(H key, java.util.Map<? ...

  5. string(未完待续)

    1.string字符串的长度     可以用   a.length()  来测,或者是a.size() 来测 不可以用strlen(a)来求其长度, sizeof(a)是固定值16, 求的是strin ...

  6. 关于phonegap的cookie

    angular搞了一半现在开始搞phonegap(确切的说应该叫cordova). 因为有很紧迫的需求,所以我也不能系统的学,只能遇到啥问题就解决啥.第一个问题就是cookie. 经过调研,cordo ...

  7. Java企业微信开发_07_JSSDK多图上传

    一.本节要点 1.1可信域名 所有的JS接口只能在企业微信应用的可信域名下调用(包括子域名),可在企业微信的管理后台“我的应用”里设置应用可信域名.这个域名必须要通过ICP备案,不然jssdk会配置失 ...

  8. linux命令学习笔记(6):rmdir 命令

    今天学习一下linux中命令: rmdir命令.rmdir是常用的命令,该命令的功能是删除空目录,一个目录 被删除之前必须是空的.(注意,rm - r dir命令可代替rmdir,但是有很大危险性.) ...

  9. stl_heap.h

    stl_heap.h // Filename: stl_heap.h // Comment By: 凝霜 // E-mail: mdl2009@vip.qq.com // Blog: http://b ...

  10. vc++ 访问php webService

    之前做了一个VC++访问c#制作的WebService,没有问题,接着我又做了一个VC++访问php制作的WebService ,结果老是出现Client错误.这个php WebService是用Ze ...