[WPF自定义控件]从ContentControl开始入门自定义控件
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
[WPF ⾃定义控件]从ContentControl 开始⼊门⾃定义控件
1. 前⾔
我去年写过⼀个在UWP ⾃定义控件的,⼤部分的经验都可以⽤在WPF 中(只有⼀点⼩区别)。
这篇⽂章的⽬的是快速⼊门⾃定义控件的开发,所以尽量精简了篇幅,更深⼊的概念在以后介绍各控件的⽂章中实际运⽤到才介绍。
是WPF 中最基础的⼀种控件,Window 、Button 、ScrollViewer 、Label 、ListBoxItem 等都继承⾃ContentControl 。
⽽且ContentControl 的结构⼗分简单,很适合⽤来⼊门⾃定义控件。
这篇⽂章通过⾃定义⼀个ContentControl 来介绍⾃定义控件的⼀些基础概念,包括⾃定义控件的基本步骤及其组成。
2. 什么是⾃定义控件
在开始之前⾸先要了解什么是⾃定义控件以及为什么要⽤⾃定义控件。
在WPF 要创建⾃⼰的控件(Control ),通常可以使⽤⾃定义控件(CustomControl )或⽤户控件(UserControl ),两者最⼤的区别是前者可以通过对控件的外观灵活地进⾏定制。
如在下⾯的例⼦中,通过ControlTemplate 将Button
改成⼀个圆形按钮:
控件库中通常使⽤⾃定义控件⽽不是⽤户控件。
3. 创建⾃定义控件
ContentControl 最简单的派⽣类应该是HeaderedContentControl 了吧,这篇⽂章会创建⼀个模仿HeaderedContentControl 的MyHeaderedContentControl ,它继承⾃ContentControl 并添加了⼀些细节。
在“添加新项”对话框选择“⾃定义控件(WPF )”,名称改为"MyHeaderedContentControl.cs"(⽤My-做前缀是⼗分差劲的命名⽅式,但只要⼀看到这种命名就明⽩这是个测试⽤的东西,不会和正规代码搞错,所以我习惯了测试⽤代码就这样命名。
),点击“添加”后VisualStudio <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>
会⾃动创建两个⽂件:MyHeaderedContentControl.cs 和Themes/Generic.xaml。
编译通过后在XAML 上添加MyHeaderedContentControl 的命名空间即可使⽤这个控件:
在添加新项时,⼩⼼不要和“Windows Forms”⾥的“⾃定义控件”搞混。
4. ⾃定义控件的组成
⾃定义控件通常由代码和DefaultStyle 两部分组成,它们分别位于VisualStudio 创建的MyHeaderedContentControl.cs 和
Themes/Generic.xaml 两个⽂件中。
4.1 代码
控件代码负责定义控件的结构和⾏为。
MyHeaderedContentControl.cs 的代码如上所⽰,只包含⼀个静态构造函数及⼀句
DefaultStyleKeyProperty.OverrideMetadata 。
DefaultStyleKey 是⽤于查找控件样式的键,没有这句代码控件就找不到默认样式。
4.2 DefaultStyle
<Window x:Class="CustomControlDemo.MainWindow"
xmlns="/winfx/2006/xaml/presentation"
xmlns:x="/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControlDemo">
<Grid>
<local:MyHeaderedContentControl Content="I am a new control" />
</Grid>
</Window>
public class MyHeaderedContentControl: Control
{
static MyCustomControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyHeaderedContentControl), new FrameworkPropertyMetadata(typeof(MyHeaderedContentControl))); }
}
<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
了解的作⽤有助于处理好⾃定义控件的焦点。
<GroupBox>
在上⾯这个UI 中,在第⼀个TextBox 获得焦点时按下Tab 后第⼆个TextBox 将获得焦点,这很⾃然。
但如果换成下⾯这段
XAML:
结果就如上⾯截图显⽰,第⼆个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 ⽅法)。
</GroupBox>
<GroupBox>
<TextBox />
</GroupBox>
<ContentControl>
<TextBox />
</ContentControl>
<ContentControl>
<TextBox />
</ContentControl>
/// <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)
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中设置,不推荐在构造函数中设置。
依赖属性的定义代码⽐较复杂,我⼀直都是⽤代码段⽣成,可以参考我另⼀篇博客。
添加依赖属性后再更新控件模板,这个控件就基本完成了。
<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. 参考。