[WPF自定義控制項庫] 自定義控制項的代碼如何與ControlTemplate交互

来源:https://www.cnblogs.com/dino623/archive/2019/05/22/interact_with_ControlTemplate.html
-Advertisement-
Play Games

1. 前言 WPF有一個靈活的UI框架,用戶可以輕鬆地使用代碼控制控制項的外觀。例設我需要一個控制項在滑鼠進入的時候背景變成藍色,我可以用下麵這段代碼實現: 但一般沒人會這麼做,因為這樣做代碼和UI過於耦合,難以擴展。正確的做法應該是使用代碼告訴ControlTemplate去改變外觀,或者控制Cont ...


1. 前言

WPF有一個靈活的UI框架,用戶可以輕鬆地使用代碼控制控制項的外觀。例設我需要一個控制項在滑鼠進入的時候背景變成藍色,我可以用下麵這段代碼實現:

protected override void OnMouseEnter(MouseEventArgs e)
{
    base.OnMouseEnter(e);
    Background = new SolidColorBrush(Colors.Blue);
}

但一般沒人會這麼做,因為這樣做代碼和UI過於耦合,難以擴展。正確的做法應該是使用代碼告訴ControlTemplate去改變外觀,或者控制ControlTemplate中可用的元素進入某個狀態。

這篇文章介紹自定義控制項的代碼如何和ControlTemplate交互,涉及的知識包括RelativeSource、Trigger、TemplatePart和VisualState。

2. 簡單的Expander

本文使用一個簡單的Expander介紹UI和ControlTemplate交互的幾種技術,它的代碼如下:

public class MyExpander : HeaderedContentControl
{
    public MyExpander()
    {
        DefaultStyleKey = typeof(MyExpander);
    }

    public bool IsExpanded
    {
        get => (bool)GetValue(IsExpandedProperty);
        set => SetValue(IsExpandedProperty, value);
    }

    public static readonly DependencyProperty IsExpandedProperty =
        DependencyProperty.Register(nameof(IsExpanded), typeof(bool), typeof(MyExpander), new PropertyMetadata(default(bool), OnIsExpandedChanged));

    private static void OnIsExpandedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        var oldValue = (bool)args.OldValue;
        var newValue = (bool)args.NewValue;
        if (oldValue == newValue)
            return;

        var target = obj as MyExpander;
        target?.OnIsExpandedChanged(oldValue, newValue);
    }

    protected virtual void OnIsExpandedChanged(bool oldValue, bool newValue)
    {
        if (newValue)
            OnExpanded();
        else
            OnCollapsed();
    }

    protected virtual void OnCollapsed()
    {
    }

    protected virtual void OnExpanded()
    {
    }
}
<Style TargetType="{x:Type local:MyExpander}">
    <Setter Property="HorizontalContentAlignment"
            Value="Stretch" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:MyExpander}">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <StackPanel>
                        <ToggleButton x:Name="ExpanderToggleButton"
                                      Content="{TemplateBinding Header}"
                                      IsChecked="{Binding IsExpanded,RelativeSource={RelativeSource Mode=TemplatedParent},Mode=TwoWay}" />
                        <ContentPresenter Grid.Row="1"
                                          x:Name="ContentPresenter"
                                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                          Visibility="Collapsed" />
                    </StackPanel>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

MyExpander是一個HeaderedContentControl,它包含一個IsExpanded用於指示當前是展開還是摺疊。ControlTemplate中包含ExpanderToggleButton及ContentPresenter兩個元素。

3. 使用RelativeSource

之前已經介紹過TemplateBinding,通常ControlTemplate中元素都通過TemplateBinding獲取控制項的屬性值。但需要雙向綁定的話,就是RelativeSource出場的時候了。

RelativeSource有幾種模式,分別是:

  • FindAncestor,引用數據綁定元素的父鏈中的上級。 這可用於綁定到特定類型的上級或其子類。
  • PreviousData,允許在當前顯示的數據項列表中綁定上一個數據項(不是包含數據項的控制項)。
  • Self,引用正在其上設置綁定的元素,並允許你將該元素的一個屬性綁定到同一元素的其他屬性上。
  • TemplatedParent,引用應用了模板的元素,其中此模板中存在數據綁定元素。。

ControlTemplate中主要使用RelativeSource Mode=TemplatedParent的Binding,它相當於TemplateBinding的雙向綁定版本。,主要是為了可以和控制項本身進行雙向綁定。ExpanderToggleButton.IsChecked使用這種綁定與Expander的IsExpanded關聯,當Expander.IsChecked為True時ExpanderToggleButton處於選中的狀態。

IsChecked="{Binding IsExpanded,RelativeSource={RelativeSource Mode=TemplatedParent},Mode=TwoWay}" 

接下來分別用幾種技術實現Expander.IsChecked為True時顯示ContentPresenter。

4. 使用Trigger

<ControlTemplate TargetType="{x:Type local:ExpanderUsingTrigger}">
    <Border Background="{TemplateBinding Background}">
        ......
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="IsExpanded"
                 Value="True">
            <Setter Property="Visibility"
                    TargetName="ContentPresenter"
                    Value="Visible" />
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

可以為ControlTemplate添加Triggers,內容為TriggerEventTrigger的集合,Triggers通過響應屬性值變更或事件更改控制項的外觀。

大部分情況下Trigger簡單好用,但濫用或錯誤使用將使ControlTemplate的各個狀態之間變得很混亂。例如當可以影響外觀的屬性超過一定數量,並且這些屬性可以組成不同的組合,Trigger將要處理無數種情況。

5. 使用TemplatePart

TemplatePart(部件)是指ControlTemplate中的命名元素(如上面XAML中的“HeaderElement”)。控制項邏輯預期這些部分存在於ControlTemplate中,控制項在載入ControlTemplate後會調用OnApplyTemplate,可以在這個函數中調用protected DependencyObject GetTemplateChild(String childName)獲取模板中指定名字的部件。

[TemplatePart(Name =ContentPresenterName,Type =typeof(UIElement))]
public class ExpanderUsingPart : MyExpander
{
    private const string ContentPresenterName = "ContentPresenter";

    protected UIElement ContentPresenter { get; private set; }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        ContentPresenter = GetTemplateChild(ContentPresenterName) as UIElement;
        UpdateContentPresenter();
    }

    protected override void OnIsExpandedChanged(bool oldValue, bool newValue)
    {
        base.OnIsExpandedChanged(oldValue, newValue);
        UpdateContentPresenter();
    }

    private void UpdateContentPresenter()
    {
        if (ContentPresenter == null)
            return;

        ContentPresenter.Visibility = IsExpanded ? Visibility.Visible : Visibility.Collapsed;
    }
}

上面的代碼實現了獲取ContentPresenter並根據IsExpanded 的值將它顯示或隱藏。由於Template可能多次載入,或者不能正確獲取TemplatePart,所以使用TemplatePart前應該先判斷是否為空;如果要訂閱TemplatePart的事件,應該先取消訂閱。

註意:不要在Loaded事件中嘗試調用GetTemplateChild,因為Loaded的時候OnApplyTemplate不一定已經被調用,而且Loaded更容易被多次觸發。

TemplatePartAttribute協定

有時,為了表明控制項期待在ControlTemplate存在某個特定部件,防止編輯ControlTemplate的開發人員刪除它,控制項上會添加添加TemplatePartAttribute協定。上面代碼中即包含這個協定:

[TemplatePart(Name =ContentPresenterName,Type =typeof(UIElement))]

這段代碼的意思是期待在ControlTemplate中存在名稱為 "ContentPresenterName",類型為UIElement的部件。

TemplatePartAttribute在UWP中的作用好像被弱化了,不止在UWP原生控制項中見不到TemplatePartAttribute,甚至在Blend中“部件”視窗也消失了。可能UWP更加建議使用VisualState。

使用TemplatePart需要遵循以下原則:

  • 儘可能減少TemplarePartAttribute協定。
  • 在使用TemplatePart之前檢查其是否為Null。
  • 如果ControlTemplate沒有遵循TemplatePartAttribute協定也不應該拋出異常,缺少部分功能可以接受,但要確保程式不會報錯。

6. 使用VisualState

VisualState 指定控制項處於特定狀態時的外觀。控制項的代碼使用VisualStateManager.GoToState(Control control, string stateName,bool useTransitions)指定控制項處於何種VisualState,控制項的ControlTemplate中根節點使用VisualStateManager.VisualStateGroups附加屬性,併在其中確定各個VisualState的外觀。

[TemplateVisualState(Name = StateExpanded, GroupName = GroupExpansion)]
[TemplateVisualState(Name = StateCollapsed, GroupName = GroupExpansion)]
public class ExpanderUsingState : MyExpander
{
    public const string GroupExpansion = "ExpansionStates";

    public const string StateExpanded = "Expanded";

    public const string StateCollapsed = "Collapsed";

    public ExpanderUsingState()
    {
        DefaultStyleKey = typeof(ExpanderUsingState);
    }

    protected override void OnIsExpandedChanged(bool oldValue, bool newValue)
    {
        base.OnIsExpandedChanged(oldValue, newValue);
        UpdateVisualStates(true);
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        UpdateVisualStates(false);
    }

    protected virtual void UpdateVisualStates(bool useTransitions)
    {
        VisualStateManager.GoToState(this, IsExpanded ? StateExpanded : StateCollapsed, useTransitions);
    }

}
<ControlTemplate TargetType="{x:Type local:ExpanderUsingState}">
    <Border Background="{TemplateBinding Background}"
            BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="{TemplateBinding BorderThickness}">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="ExpansionStates">
                <VisualState x:Name="Expanded">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
                                                       Storyboard.TargetName="ContentPresenter">
                            <DiscreteObjectKeyFrame KeyTime="0"
                                                    Value="{x:Static Visibility.Visible}" />
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="Collapsed" />
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
      ......
    </Border>
</ControlTemplate>

上面的代碼演示瞭如何通過控制項的IsExpanded 屬性進入不同的VisualState。ExpansionStates是VisualStateGroup,它包含Expanded和Collapsed兩個互斥的狀態,控制項使用VisualStateManager.GoToState(Control control, string stateName,bool useTransitions)更新VisualState。useTransitions這個參數指示是否使用 VisualTransition 進行狀態過渡,簡單來說即是VisualState之間切換時用不用VisualTransition裡面定義的動畫。請註意我在OnApplyTemplate()中使用了 UpdateVisualStates(false),這是因為這時候控制項還沒在UI上呈現,這時候使用動畫毫無意義。

使用VisualState的最佳實踐

使用屬性控制狀態,並創建一個方法幫助狀態間的轉換。如上面的UpdateVisualStates(bool useTransitions)。當屬性值改變或其它有可能影響VisualState的事件發生都可以調用這個方法,由它統一管理控制項的VisualState。註意一個控制項應該最多只有幾種VisualStateGroup,有限的狀態才容易管理。

TemplateVisualStateAttribute協定

自定義控制項可以使用TemplateVisualStateAttribute協定聲明它的VisualState,用於通知控制項的使用者有這些VisualState可用。這很好用,尤其是對於複雜的控制項來說。上面代碼也包含了這個協定:

[TemplateVisualState(Name = StateExpanded, GroupName = GroupExpansion)]
[TemplateVisualState(Name = StateCollapsed, GroupName = GroupExpansion)]

TemplateVisualStateAttribute是可選的,而且就算控制項聲明瞭這些VisualState,ControlTemplate也可以不包含它們中的任何一個,並且不會引發異常。

7. Trigger、TemplatePart及VisualState之間的選擇

正如Expander所示,Trigger、TemplatePart及VisualState都可以實現類似的功能,像這種三種方式都可以實現同一個功能的情況很常見。

在過去版本的Blend中,編輯ControlTemplate可以看到“狀態(States)”、“觸發器(Triggers)”、“部件(Parts)”三個面板,現在“部件”面板已經消失了,而“觸發器”從Silverlight開始就不再支持,以後也應該不會回歸(xaml standard在github上有這方面的討論(Add Triggers, DataTrigger, EventTrigger,___) [and-or] VisualState · Issue #195 · Microsoft-xaml-standard · GitHub[https://github.com/Microsoft/xaml-standard/issues/195])。現在看起來是VisualState的勝利,其實在Silverlight和UWP中TemplatePart仍是個十分常用的技術,而在WPF中Trigger也工作得很出色。

如果某個功能三種方案都可以實現,我的選擇原則是這樣:

  • 需要向控制項發出命令的,如響應點擊事件,就用TemplatePart;
  • 簡單的UI,如隱藏/顯示某個元素就用Trigger;
  • 如果要有動畫,並且代碼量和使用Trigger的話,我會選擇用VisualState;

幾乎所有WPF的原生控制項都提供了VisualState支持,例如Button雖然使用ButtonChrome實現外觀,但同時也可以使用VisualState定義外觀。有時做自定義控制項的時候要考慮為常用的VisualState提供支持。

8. 結語

VisualState是個比較複雜的話題,可以通過我的另一篇文章理解ControlTemplate中的VisualTransition更深入地理解它的用法(雖然是UWP的內容,但對WPF也同樣適用)。

即使不自定義控制項,學會使用ControlTemplate也是一件好事,下麵給出一些有用的參考鏈接。

9. 參考

創建具有可自定義外觀的控制項 Microsoft Docs
通過創建 ControlTemplate 自定義現有控制項的外觀 Microsoft Docs
Control Customization Microsoft Docs
ControlTemplate Class (System_Windows_Controls) Microsoft Docs


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

-Advertisement-
Play Games
更多相關文章
  • 第一個爬蟲程式——豆瓣新書信息爬取。主要用到 soup 的 find 和 find_all 方法。 ...
  • 前言: 我們在操作資料庫時候一般都是通過sql代碼來操作mysql資料庫中相關數據,這就需要懂得sql語句,那麼怎麼樣才能在不懂sql語句的情況下通過我們所學的python代碼來實現對mysql資料庫的操作? 當然有這種神奇的操作,其中之一就是今天深入瞭解的ORM對象關係映射(Object Rela ...
  • 運行結果: ...
  • Jenkins 的前身是 Hudson 是一個可擴展的持續集成引擎。 1.安裝JDK環境: 1.1配置環境變數信息如下:切記一定要找對jdk的安裝目錄 2.安裝Maven環境 2.1環境變數配置如下:註意找到maven的安裝目錄 3.安裝Jenkins CenOS版本 3.2修改配置文件如下: 3. ...
  • selenium+phantomjs爬取京東商品信息 今天自己實戰寫了個爬取京東商品信息,和上一篇的思路一樣,附上鏈接:https://www.cnblogs.com/cany/p/10897618.html 打開 https://www.jd.com/ 首先不需要登陸就可搜索,淘寶不一樣,所以淘寶 ...
  • 簡述:QPropertyAnimation (動畫類,用來向QObject對象添加動畫) 該類的繼承框圖如下所示: 1.QAbstractAnimation(所有動畫的抽象基類) 該抽象類為QPropertyAnimation提供了動畫播放,暫停,停止,持續時間,迴圈周期等抽象函數. 其中常用的成員 ...
  • 樹的結構說得差不多了,現在我們來說說一種數據結構叫做哈希表(hash table),哈希表有是乾什麼用的呢?我們知道樹的操作的時間複雜度通常為O(logN),那有沒有更快的數據結構?當然有,那就是哈希表; 1.哈希表簡介 哈希表(hash table)是一種數據結構,提供很快速的插入和查找操作(有的 ...
  • 一、項目背景 YW公司是一家電池供應商,目前由於公司內部的需要,需要做一個CRM項目,需要每一個不同角色的員工登陸系統後處理自己的事情。其流程大致如下: 其項目包括三部分內容: 1、許可權分配組件(rbac組件) 2、各個表的curd功能組件(stark組件) 3、將業務與上述兩個組件進行融入 二、各 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...