[WPF自定義控制項]從ContentControl開始入門自定義控制項

来源:https://www.cnblogs.com/dino623/archive/2019/05/15/How-To-Create-CustomControl.html
-Advertisement-
Play Games

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中創建依賴屬性可以做到很複雜,而再簡單也要好幾行代碼。在自定義控制項中創建依賴屬性通常包含以下幾部分:

  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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 2.1 註意不同類型轉換 2.2 2.3 2.4 使用 迴圈 每次對 個位 取數相加 取數後 除以10 使前一位 變為 個位 繼續 判斷小於等於0時停止 2.5 註意 System.currentTimeMillis()方法 返回 long 形 需要轉換為 int 形 2.6 a 不能為 0 b2 ...
  • 5.14自我總結 一.python插件插件相關技巧彙總 安裝在cmd上運行 二.PyCharm的安裝註意事項 1.激活碼 可以網上找 2.對於當中的Python的設置 對於python的路徑不能選擇系統預設,要手動輸入python.exe的路徑 3.字體設置以及快捷設置 點擊File→setting ...
  • 介紹 jar命令用來對*.class文件進行壓縮,從而生成jar(archive)歸檔文件,避免文件過多. 定義一個文件: 編譯後,生成包.類,然後將這個common包進行壓縮為jar文件.輸入下麵命令: 列印如下: 當我們想往my.jar添加其它包的時候,輸入: 如果想解壓jar包,則輸入: 當我 ...
  • 1、封裝類似lower()的函數 2、封裝類似upper()的函數 3、封裝類似find()的函數 4、封裝類似rfind()的函數 5、封裝功能類似isdigit()的函數 6、封裝功能類似partition()的函數 ...
  • day23 01 類的命名空間 一、初識面向對象複習 定義類: class 函數:方法 動態屬性 變數:類屬性 靜態屬性 過程: (1)_init_方法:初始化:def _init_(self,參數) python幫我們創建了一個對象self 每當我們調用類的時候就會自動觸發這個方法 在_init_ ...
  • 隨著動態語言的流行(Ruby,Groovy,Scala,Node.js),Java的開發顯得格外的笨重;繁多的配置,低下的開發效率,複雜的部署流程以及第三方技術集成難度大. 在上述環境 下,Spring Boot應運而生.它使用"習慣優於配置"(項目中存在大量的配置,此外還內置一個習慣性的配置,讓你 ...
  • java中的同步器是指什麼? 哪些類是使用AQS實現的? 分散式環境中怎麼實現同步? ...
  • 零基礎學編程,用python入門是個不錯的選擇。下麵我們就介紹一下0基礎學習Python的技巧和方法。當然,也包括一些學習的心態指導。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...