[WPF自定义控件]从ContentControl开始入门自定义控件

原文:[WPF自定义控件]从ContentControl开始入门自定义控件

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

原文地址:https://www.cnblogs.com/lonelyxmas/p/10868856.html

时间: 2024-09-27 08:13:43

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

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

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

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

转自:http://blog.sina.com.cn/s/blog_691051e10101a3by.html 很多时候没有我们需要使用的控件,或者控件并不美观.比如这个滑动开关,这是android之后的版本才提供的控件,新版本并不提供,这个时候就需要我们自定义控件了. 一个2个主要类,OnChangedListener,SlipButton SlipButton代码如下 package com.appipv6.android.slipbutton; import com.appipv.onof

WPF中关于自定义控件的滚动条鼠标停留在内容上鼠标滚轮滚动无效的问题

问题起因:在一个用户控件里放置了1个TreeView垂直顺序放置. 当用户控件中的内容超过面板大小时,滚动条会自动出现 ,但是只有当鼠标指示在右边滚动条的那一条位置时,才支持鼠标滚轴滚动. 点在控件内部时滚轴无效. 问题分析:由于设置了d:designheight,自定义控件的宽高都是随着父容器的变化而变化的,于是我将Height设为较小的固定高度时,发现鼠标停留在控件内容时,滚轮控制滚动条滚动是有效的.这就说明UI上显示的滚动条并非是这个自定义控件的,而是这个自定义控件所在的父容器的,这样也解

Android自定义控件(状态提示图表) (转)

源:Android自定义控件(状态提示图表) 源:Android应用开发 [工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果] 1  背景 前面分析那么多系统源码了,也该暂停下来休息一下,趁昨晚闲着看见一个有意思的需求就操练一下分析源码后的实例演练—-自定义控件. 这个实例很适合新手入门自定义控件.先看下效果图: 横屏模式如下: 竖屏模式如下: 看见没有,这个控件完全自定义的,连文字等都是自定义的,没有任何图片等资源,就仅仅是一个小的java文

Android自定义控件第一发(状态提示图表)

[工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果] 1 背景 前面分析那么多系统源码了,也该暂停下来休息一下,趁昨晚闲着看见一个有意思的需求就操练一下分析源码后的实例演练--自定义控件. 这个实例很适合新手入门自定义控件.先看下效果图: 横屏模式如下: 竖屏模式如下: 看见没有,这个控件完全自定义的,连文字等都是自定义的,这个界面只有一个控件.如下咱们看下实现代码. !!!!!!! 下载Demo工程源码点击我 [工匠若水 http://blo

自定义控件之--组合控件(titlebar)

自定义控件相关知识从郭霖等大神身上学习,这里只不过加上自己的理解和实践,绝非抄袭. 组合控件是自定义控件中最简单的方式,但是是入门自定义控件和进阶的过程: 那么常见的组合控件有那些? 比如titlebar和视图中常见的可重用界面布局的可用都可以通过组合控件的方式来进行自定义,并通过向其他类暴露方法和回调来实现对视图内容显示,隐藏,图片展示,动画活动,文字内容的控制. 废话这么多,写个组合控件来加深影响. 首先思考下,titleBar应该包含那些内容,对了左中右三组控件,分别是左右按钮和中间的标题

Qt编写自定义控件45-柱状标尺控件

一.前言 这个控件写了很久了,是最早期的一批控件中的一个,和温度计控件类似,都是垂直的进度条,可以设置不同的背景颜色,左侧的刻度也可以自由设定,还提供了动画效果,其实就是开启定时器慢慢的进度到设定的目标值,如果设定的值比当前值大,则递增,反之递减.由于当时的qpainter绘制功底还不够如火纯情,所以当时的刻度尺部分都是定死的字体大小,并不会随着控件变化而增大. 二.实现的功能 1:可设置精确度(小数点后几位)和间距 2:可设置背景色/柱状颜色/线条颜色 3:可设置长线条步长及短线条步长 4:可

Qt编写自定义控件47-面板区域控件

一.前言 在很多web网页上,经常可以看到一个设备对应一个面板,或者某种同等类型的信息全部放在一个面板上,该面板还可以拖来拖去的,这个控件首次用在智能访客管理平台中,比如身份证信息一个面板,访客信息一个面板,被访人信息一个面板,这样相当于分类展示了,还提供了对应的标题栏有文字显示,这个控件的使用场景也是非常多,还有个子标题可以设置,拓展了报警闪烁的接口. 二.实现的功能 1:支持所有widget子类对象,自动产生滚动条 2:支持自动拉伸自动填充 3:提供接口获取容器内的所有对象的指针 4:可设置

Android自定义控件之自定义组合控件(三)

前言: 前两篇介绍了自定义控件的基础原理Android自定义控件之基本原理(一).自定义属性Android自定义控件之自定义属性(二).今天重点介绍一下如何通过自定义组合控件来提高布局的复用,降低开发成本,以及维护成本. 使用自定义组合控件的好处? 我们在项目开发中经常会遇见很多相似或者相同的布局,比如APP的标题栏,我们从三种方式实现标题栏来对比自定义组件带来的好处,毕竟好的东西还是以提高开发效率,降低开发成本为导向的. 1.)第一种方式:直接在每个xml布局中写相同的标题栏布局代码 <?xm