1. 前言 我去年寫過一個在UWP自定義控制項的 "系列博客" ,大部分的經驗都可以用在WPF中(只有一點小區別)。這篇文章的目的是快速入門自定義控制項的開發,所以儘量精簡了篇幅,更深入的概念在以後介紹各控制項的文章中實際運用到才介紹。 "ContentControl" 是WPF中最基礎的一種控制項,Win ...
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中創建依賴屬性可以做到很複雜,而再簡單也要好幾行代碼。在自定義控制項中創建依賴屬性通常包含以下幾部分:
註冊依賴屬性並生成依賴屬性標識符。依賴屬性標識符為一個
public static readonly DependencyProperty
欄位。依賴屬性標識符的名稱必須為“屬性名+Property”。在PropertyMetadata
中指定屬性預設值。實現屬性包裝器。為屬性提供 CLR get 和 set 訪問器,在Getter和Setter中分別調用GetValue和SetValue,除此之外Getter和Setter中不應該有其它任何自定義代碼。
需要監視屬性值變更。在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