控制項撰寫概觀

更新:2007 年 11 月

Windows Presentation Foundation (WPF) 控制項模型由於具有擴充性,大幅減少了建立新控制項的需求。不過,在某些情況下,您可能還是需要建立自訂控制項。本主題將說明一些功能使建立自訂控制項的需求減至最少,以及 Windows Presentation Foundation (WPF) 中的不同控制項撰寫模型。本主題也將示範如何建立新的控制項。

這個主題包含下列章節。

  • 撰寫新控制項的替代方案
  • 控制項撰寫模型
  • 控制項撰寫基本概念
  • 從 UserControl 繼承和使用 ControlTemplate 的比較
  • 相關主題

撰寫新控制項的替代方案

在過去,如果要從現有控制項自訂控制項,只能變更控制項的標準屬性,例如背景色彩、框線寬度和字型大小。除了這些預先定義的參數外,如果還想擴充控制項的外觀或行為,就需要建立新的控制項,建立的方式通常是藉由繼承現有控制項並覆寫負責繪製控制項的方法。雖然您現在還是可以使用前述方式,但 WPF 可以讓您藉由使用其豐富的內容模型、樣式、樣板和觸發程序來自訂現有控制項。下列清單提供的範例示範如何在不建立新控制項的情況下,即可以使用這些功能來建立自訂且一致的控制項。

  • 豐富內容:許多標準 WPF 控制項都支援豐富內容。例如,Button 的內容屬性的型別是 Object,所以理論上可以在 Button 上顯示任何東西。若要讓按鈕顯示影像和文字,您可以在 StackPanel 上加入影像和 TextBlock,並指派 StackPanelContent 屬性。因為控制項可以顯示 WPF 視覺化項目和任意資料,就比較不需要建立新控制項或修改現有控制項來支援複雜的視覺效果。如需 Button 和其他控制項的內容模型的詳細資訊,請參閱Controls 內容模型概觀。如需 WPF 中其他內容模型的詳細資訊,請參閱內容模型

  • 樣式Style 是代表控制項屬性的值集合。藉由使用樣式,您可以針對所要的控制項外觀和行為,建立可重複使用的表示,而不需要撰寫新的控制項。例如,假設您想要所有 TextBlock 控制項都具有紅色新細明體且大小為 14 的字型,您可以建立樣式做為資源,並對應設定適當的屬性。那麼,加入至您應用程式的每個 TextBlock,都會具有相同的外觀。

  • 資料樣板DataTemplate 可以讓您自訂控制項上資料的顯示方式。例如,DataTemplate 可以用來指定資料在 ListBox 中的顯示方式。如需範例,請參閱資料範本化概觀。除了自訂資料外觀之外,DataTemplate 可以包含 UI 項目 (Element),帶給您在自訂 UI 方面許多的彈性。例如,藉由使用 DataTemplate,您可以建立其中每個項目 (Item) 都包含核取方塊的 ComboBox

  • 控制項樣板.:許多 WPF 控制項使用 ControlTemplate 定義控制項的結構和外觀,這樣會將控制項外觀和控制項功能加以分隔開來。藉由重新定義控制項的 ControlTemplate,即可以大幅變更控制項的外觀。例如,假設您希望控制項看起來像號誌燈。這個控制項具有簡單的使用者介面和功能。控制項是三個圓圈,每次只會亮其中一個。在一番對照之後,您會發現 RadioButton 提供的功能會讓每次只能選取一個選項按鈕,但 RadioButton 的預設外觀看起來就像號誌燈的亮燈。因為 RadioButton 使用控制項樣板定義其外觀,很容易可以重新定義 ControlTemplate 以滿足控制項需求,並使用選項按鈕製作號誌燈。

    注意事項:

    雖然 RadioButton 可以使用 DataTemplate,但一個 DataTemplate 對本範例來說並不足夠。DataTemplate 會定義控制項內容的外觀。在 RadioButton 的案例中,內容是出現在代表是否有選取的 RadioButton 的圓圈右邊的任何項目。在號誌燈的範例中,選項按鈕只需要是可以發亮的圓圈。因為號誌燈的外觀需求跟 RadioButton 的預設外觀是如此的不同,所以有必要重新定義 ControlTemplate。一般而言,DataTemplate 是用於定義控制項的內容 (或資料),而 ControlTemplate 則用於定義控制項的結構排列方式。

  • 觸發程序Trigger 可以讓您動態變更控制項的外觀和行為,而不需要建立新的控制項。例如,假設應用程式中具有多個 ListBox 控制項,並希望每個 ListBox 中的項目在選取時呈現紅色粗體。您的第一個想法可能是建立繼承自 ListBox 的類別,並覆寫 OnSelectionChanged 方法以變更選取項目的外觀,但有個較好的方式是為 ListBoxItem 樣式加入觸發程序,以變更選取項目的外觀。觸發程序可以讓您變更屬性值,或是依據屬性值採取動作。EventTrigger 則可以讓您在發生事件時採取動作。

如需樣式、樣板和觸發程序的詳細資訊,請參閱設定樣式和範本

一般而言,如果您的控制項可以對應到現有控制項的功能,但您希望控制項看起來不太一樣,則應該先考慮是否能使用本節所討論的任何方法來變更現有控制項的外觀。

控制項撰寫模型

豐富的內容模型、樣式、樣板和觸發程序,可以讓您建立新控制項的需求降到最低。然而,如果有必要建立新的控制項,最好能先了解 WPF 中的不同控制項撰寫模型。WPF 提供三種建立控制項的常用模型,每種模型都提供不同的功能集和不同程度的彈性。三種模型的基底類別為 UserControlControlFrameworkElement

衍生自 UserControl

在 WPF 中建立控制項最簡單的方式,是衍生自 UserControl。在建置繼承自 UserControl 的控制項時,您要加入現有的元件至 UserControl、為元件命名,並參考可延伸標記語言 (XAML) 中的事件處理常式。您接著可以在程式碼中參考該具名項目並定義事件處理常式。這個開發模型跟 WPF 中應用程式開發所使用的模型非常類似。

如果正確建置,UserControl 就可以運用豐富內容、樣式和觸發程序的優點。然而,如果控制項繼承自 UserControl,則使用您的控制項的人將無法使用 DataTemplateControlTemplate 來自訂其外觀。所以就有必要衍生自 Control 類別或其中一個衍生類別 (UserControl 以外的),來建立可以支援樣板的自訂控制項。

從 UserControl 衍生的好處

如果適用下列所有條件的話,請考慮衍生自 UserControl

  • 您想要以類似建置應用程式的方式來建置控制項。

  • 您的控制項只包含現有的元件。

  • 您不需要支援複雜的自訂作業。

衍生自 Control

大部分現有的 WPF 控制項是使用衍生自 Control 類別的模型。在建立繼承自 Control 類別的控制項時,您要藉由使用樣板定義其外觀。這樣做您便可以將作業邏輯與視覺化表示分開處理。您也可以使用命令和繫結來取代事件,並盡可能避免參考 ControlTemplate 中的項目,藉此確保降低 UI 和邏輯間的耦合。如果有適當地降低控制項之 UI 和邏輯間的耦合,則控制項的使用者便可以重新定義控制項的 ControlTemplate 以自訂其外觀。雖然建置自訂的 Control 不像建置 UserControl 那樣簡單,但自訂的 Control 會提供您最佳的彈性。

從 Control 衍生的好處

如果適用下列任一條件的話,請考慮衍生自 Control,而不要使用 UserControl 類別:

  • 您想要透過 ControlTemplate 自訂控制項的外觀。

  • 您想要控制項支援不同的主題。

衍生自 FrameworkElement

衍生自 UserControlControl 的控制項都需要撰寫現有的項目。對於許多案例而言,這是可接受的方案,因為繼承自 FrameworkElement 的任何物件都可以位於 ControlTemplate 中。然而,有時候控制項外觀需要的不僅止於簡單項目複合的功能。在這些情況下,就適合使用 FrameworkElement 做為元件的基礎。

建置以 FrameworkElement 為基礎的元件有兩種標準方法:直接轉譯和自訂項目組合。直接轉譯是覆寫FrameworkElementOnRender 方法,並提供明確定義元件視覺效果的 DrawingContext 作業。這是 ImageBorder 所使用的方法。自訂項目複合則是使用型別 Visual 的物件,撰寫元件的外觀。如需範例,請參閱使用 DrawingVisual 物件Track 是 WPF 中控制項使用自訂項目複合的範例。您也可以在同一個控制項中混合直接轉譯和自訂項目複合。

從 FrameworkElement 衍生的好處

如果適用下列任一條件的話,請考慮衍生自 FrameworkElement

  • 您想要準確控制控制項的外觀,而這超出了簡單項目複合所能提供的控制。

  • 您想要藉由定義自己的轉譯邏輯,以定義控制項的外觀。

  • 您想要以全新方式撰寫現有的項目,而這超出 UserControlControl 能力所及。

控制項撰寫基本概念

如稍早所討論,WPF 其中一個最強大的功能,在於除了設定控制項的基本屬性之外,尚有能力變更其外觀和行為,並且不需要建立自訂控制項。樣式、資料繫結和觸發程序功能是藉由 WPF 屬性系統和 WPF 事件系統而達成的。如果您在控制項中實作相依性屬性和路由事件,自訂控制項的使用者就可以如同使用 WPF 所隨附控制項功能一般地使用這些功能,不論您是使用哪種模型來建立自訂控制項。

使用相依性屬性

當屬性是相依性屬性時,有可能會進行下列作業:

  • 設定樣式中的屬性。

  • 繫結屬性至資料來源。

  • 使用動態資源做為屬性值。

  • 將屬性顯示為動畫。

如果希望控制項屬性可以支援這些任一功能,就應該將控制項實作為相依性屬性。下列範例會藉由執行下列動作,定義名為 Value 的相依性屬性:

  • 將名為 ValueProperty 的 DependencyProperty 識別項定義為 publicstaticreadonly 欄位。

  • 藉由呼叫 DependencyProperty.Register 向屬性系統註冊屬性名稱,以指定下列項目:

  • 藉由實作屬性的 get 和 set 存取子,定義名為 Value 的 CLR「包裝函式」屬性,這個名稱與註冊相依性屬性所使用的名稱相同。請注意,get 和 set 存取子只會分別呼叫 GetValueSetValue。建議您,相依性屬性的存取子不要包含其他的邏輯,因為用戶端和 WPF 可以略過存取子並直接呼叫 GetValueSetValue。例如,在屬性繫結到資料來源時,就不會呼叫屬性的 set 存取子。取代將其他邏輯加入到 get 和 set 存取子的方式,是使用 ValidateValueCallbackCoerceValueCallbackPropertyChangedCallback 委派在變更時回應或檢查值。如需這些回呼的詳細資訊,請參閱相依性屬性回呼和驗證

  • 針對名為 CoerceValue 的 CoerceValueCallback 定義方法。CoerceValue 可以確保 Value 大於或等於 MinValue,並小於或等於 MaxValue。

  • 定義 PropertyChangedCallback 的方法,名為 OnValueChanged。OnValueChanged 會建立 RoutedPropertyChangedEventArgs<T> 物件並準備引發 ValueChanged 路由事件。下一節中將討論路由事件。

/// <summary>
/// Identifies the Value dependency property.
/// </summary>
public static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register(
        "Value", typeof(decimal), typeof(NumericUpDown),
        new FrameworkPropertyMetadata(MinValue, new PropertyChangedCallback(OnValueChanged),
                                      new CoerceValueCallback(CoerceValue)));

/// <summary>
/// Gets or sets the value assigned to the control.
/// </summary>
public decimal Value
{          
    get { return (decimal)GetValue(ValueProperty); }
    set { SetValue(ValueProperty, value); }
}

private static object CoerceValue(DependencyObject element, object value)
{
    decimal newValue = (decimal)value;
    NumericUpDown control = (NumericUpDown)element;

    newValue = Math.Max(MinValue, Math.Min(MaxValue, newValue));

    return newValue;
}

private static void OnValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    NumericUpDown control = (NumericUpDown)obj;         

    RoutedPropertyChangedEventArgs<decimal> e = new RoutedPropertyChangedEventArgs<decimal>(
        (decimal)args.OldValue, (decimal)args.NewValue, ValueChangedEvent);
    control.OnValueChanged(e);
}

如需詳細資訊,請參閱自訂相依性屬性

使用路由事件

就如同相依性屬性會使用其他功能擴充 CLR 屬性的概念,路由事件也同樣擴充了標準 CLR 事件的概念。建立新的 WPF 控制項時,最好也能將事件實作為路由事件,因為路由事件支援下列行為:

  • 事件可以對多個控制項的父項目進行處理。如果事件是反昇 (Bubbling) 事件,項目樹狀結構中的單一父項目可以訂閱事件。然後應用程式作者可以使用一個處理常式回應多個控制項的事件。例如,如果控制項是 ListBox 中每個項目的一部分 (因為包含在 DataTemplate 中),應用程式開發人員就可以對 ListBox 定義控制項事件的事件處理常式。每當任一控制項上發生事件時,就會呼叫事件處理常式。

  • 路由事件可以在 EventSetter 中使用,這樣可以讓應用程式開發人員指定樣式內的事件處理常式。

  • 路由事件可以在 EventTrigger 中使用,這對於使用 XAML 顯示屬性的動畫非常有用。如需詳細資訊,請參閱動畫概觀

下列範例會藉由執行下列動作,定義路由事件:

  • 將名為 ValueChangedEvent 的 RoutedEvent 識別項定義為 publicstaticreadonly 欄位。

  • 藉由呼叫 EventManager.RegisterRoutedEvent 方法,註冊路由事件。範例會在呼叫 RegisterRoutedEvent 時指定下列資訊:

    • 事件名稱是 ValueChanged。

    • 路由策略是 Bubble,這代表會先呼叫來源 (引發事件的物件) 上的事件處理常式,然後再接續呼叫來源父項目上的事件處理常式,從最接近的父項目上的事件處理常式開始。

    • 事件處理常式的型別是 RoutedPropertyChangedEventHandler<T>,並以 Decimal 型別建構。

    • 事件的擁有者型別是 NumericUpDown。

  • 宣告名為 ValueChanged 的公用事件,並包含事件存取子宣告。範例會在 add 存取子宣告中呼叫 AddHandler,並在 remove 存取子宣告中呼叫 RemoveHandler,以使用 WPF 事件服務。

  • 建立名為 OnValueChanged 的保護虛擬方法,以引發 ValueChanged 事件。

/// <summary>
/// Identifies the ValueChanged routed event.
/// </summary>
public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent(
    "ValueChanged", RoutingStrategy.Bubble, 
    typeof(RoutedPropertyChangedEventHandler<decimal>), typeof(NumericUpDown));

/// <summary>
/// Occurs when the Value property changes.
/// </summary>
public event RoutedPropertyChangedEventHandler<decimal> ValueChanged
{
    add { AddHandler(ValueChangedEvent, value); }
    remove { RemoveHandler(ValueChangedEvent, value); }
}

/// <summary>
/// Raises the ValueChanged event.
/// </summary>
/// <param name="args">Arguments associated with the ValueChanged event.</param>
protected virtual void OnValueChanged(RoutedPropertyChangedEventArgs<decimal> args)
{
    RaiseEvent(args);
}

如需詳細資訊,請參閱 路由事件概觀HOW TO:建立自訂路由事件

使用繫結

若要降低控制項 UI 和邏輯間的耦合,請考慮使用資料繫結。這在藉由使用 ControlTemplate 定義控制項外觀時特別重要。在您使用資料繫結時,也許可以排除從程式碼參考特定部分之 UI 的需要。最好能避免參考 ControlTemplate 中的項目,因為當程式碼參考 ControlTemplate 中的項目,而 ControlTemplate 變更時,被參考的項目就必須包含在新的 ControlTemplate 中。

下列範例會更新 NumericUpDown 控制項的 TextBlock,在程式碼中為其指派名稱並依據名稱參考文字方塊。

<Border BorderThickness="1" BorderBrush="Gray" Margin="2" 
        Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">
  <TextBlock Name="valueText" Width="60" TextAlignment="Right" Padding="5"/>
</Border>
private void UpdateTextBlock()
{
    valueText.Text = Value.ToString();
}

下列範例使用繫結達成相同目的。

<Border BorderThickness="1" BorderBrush="Gray" Margin="2" 
        Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">

    <!--Bind the TextBlock to the Value property-->
    <TextBlock 
        Width="60" TextAlignment="Right" Padding="5"
        Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                       AncestorType={x:Type local:NumericUpDown}}, 
                       Path=Value}"/>

</Border>

如需資料繫結的詳細資訊,請參閱資料繫結概觀

定義和使用命令

請考慮定義和使用命令而不要處理事件,藉以提供功能。當您在控制項中使用事件處理常式時,應用程式無法存取事件處理常式中執行的動作。而在控制項中實作命令時,應用程式可以存取上述無法使用的功能。

下列範例是某個控制項的一部分,這個控制項會處理兩個按鈕的 Click 事件,以變更 NumericUpDown 控制項的值。不論這個控制項是 UserControl 或包含 ControlTemplateControl,UI 和邏輯都會因為控制項使用事件處理常式而緊密結合。

  <RepeatButton Name="upButton" Click="upButton_Click" 
                  Grid.Column="1" Grid.Row="0">Up</RepeatButton>
                  
    <RepeatButton Name="downButton" Click="downButton_Click" 
                  Grid.Column="1" Grid.Row="1">Down</RepeatButton>
private void upButton_Click(object sender, EventArgs e)
{
        Value++;
}

private void downButton_Click(object sender, EventArgs e)
{
        Value--;
}

下列範例會定義兩個命令來變更 NumericUpDown 控制項的值。

public static RoutedCommand IncreaseCommand
{
    get
    {
        return _increaseCommand;
    }
}
public static RoutedCommand DecreaseCommand
{
    get
    {
        return _decreaseCommand;
    }
}

private static void InitializeCommands()
{
    _increaseCommand = new RoutedCommand("IncreaseCommand", typeof(NumericUpDown));
    CommandManager.RegisterClassCommandBinding(typeof(NumericUpDown), 
                            new CommandBinding(_increaseCommand, OnIncreaseCommand));
    CommandManager.RegisterClassInputBinding(typeof(NumericUpDown), 
                            new InputBinding(_increaseCommand, new KeyGesture(Key.Up)));

    _decreaseCommand = new RoutedCommand("DecreaseCommand", typeof(NumericUpDown));
    CommandManager.RegisterClassCommandBinding(typeof(NumericUpDown), 
                            new CommandBinding(_decreaseCommand, OnDecreaseCommand));
    CommandManager.RegisterClassInputBinding(typeof(NumericUpDown), 
                            new InputBinding(_decreaseCommand, new KeyGesture(Key.Down)));
}

private static void OnIncreaseCommand(object sender, ExecutedRoutedEventArgs e)
{
    NumericUpDown control = sender as NumericUpDown;
    if (control != null)
    {
        control.OnIncrease();
    }
}
private static void OnDecreaseCommand(object sender, ExecutedRoutedEventArgs e)
{
    NumericUpDown control = sender as NumericUpDown;
    if (control != null)
    {
        control.OnDecrease();
    }
}

protected virtual void OnIncrease()
{
    Value++;
}
protected virtual void OnDecrease()
{
    Value--;
}

private static RoutedCommand _increaseCommand;
private static RoutedCommand _decreaseCommand;

樣板中的項目接著就可以參考命令,如下列範例所示。

<RepeatButton 
    Command="{x:Static local:NumericUpDown.IncreaseCommand}"  
    Grid.Column="1" Grid.Row="0">Up</RepeatButton>
<RepeatButton 
    Command="{x:Static local:NumericUpDown.DecreaseCommand}"  
    Grid.Column="1" Grid.Row="1">Down</RepeatButton>

現在應用程式可以參考繫結來存取控制項使用事件處理常式時所無法存取的功能。如需命令的詳細資訊,請參閱命令概觀

指定 ControlTemplate 中需要項目

前面幾節說明了如何使用資料繫結和命令,這樣控制項就不會從程式碼的 ControlTemplate 參考項目。不過,有時候參考項目可能難以避免。如果發生此情況,您應該將 TemplatePartAttribute 套用至控制項。此屬性會將 ControlTemplate 中項目的型別和名稱告知樣板作者。ControlTemplate 中的項目不一定都要在 TemplatePartAttribute 中具名。事實上,具名項目愈少愈好。但如果您在程式碼中參考項目,則應該使用 TemplatePartAttribute

如需設計使用 ControlTemplate 之控制項的詳細資訊,請參閱設計可設定樣式控制項的方針

設計工具的設計

若要獲得 Windows Presentation Foundation (WPF) Designer for Visual Studio 中的自訂 WPF 控制項支援 (例如使用 [屬性] 視窗編輯屬性),請遵循這些方針。如需開發 WPF 設計工具的詳細資訊,請參閱 WPF 設計工具

相依性屬性

請務必依照稍早「使用相依性屬性」中的討論來實作 CLRget 和 set 存取子。設計工具可以使用包裝函式來偵測相依性屬性的存在,但就跟 WPF 和控制項用戶端一樣,設計工具在取得和設定屬性時不一定要呼叫存取子。

附加屬性

請使用下列方針,實作自訂控制項上的附加屬性:

  • 將 publicstaticreadonlyDependencyProperty 設為格式 PropertyNameProperty,這是使用 RegisterAttached 方法建立的。傳遞至 RegisterAttached 的屬性名稱必須與 PropertyName 相符。

  • 實作一組 public static CLR 方法,分別名為 SetPropertyName 和 GetPropertyName。這兩個方法都應接受衍生自 DependencyProperty 的類別做為第一個引數。SetPropertyName 方法也接受其型別與屬性之已註冊資料型別相符的引數。GetPropertyName 方法應傳回相同型別的值。如果遺漏 SetPropertyName 方法,屬性就會標示為唯讀。

  • SetPropertyName 和 GetPropertyName 必須分別直接路由至目標相依性物件上的 GetValueSetValue 方法。藉由呼叫方法包裝函式,或直接呼叫目標相依性物件,設計工具可以存取附加屬性。

如需附加屬性的詳細資訊,請參閱附加屬性概觀

為控制項定義和使用共用資源

您可以將控制項加入到與應用程式相同的組件,或者將控制項封裝在不同的組件中,以用於多個應用程式。在大多數情況下,不論使用的方法為何,本主題所討論的資訊都適用。不過,有一項差異值得注意。當您將控制項放入與應用程式相同的組件中時,可以任意將全域資源加入到 app.xaml 檔案。但只包含控制項的組件沒有與其相關聯的 Application 物件,因此不會有 app.xaml 檔案。

當應用程式尋找資源時,會以下列順序在三個層級中尋找:

  1. 項目層級 - 系統會從參考資源的項目開始,然後搜尋邏輯父項目的資源,以此類推,直到達到根項目為止。

  2. 應用程式層級 - 由 Application 物件定義的資源。

  3. 佈景主題層級 - 佈景主題層級字典會儲存在名為 Themes 的子資料夾。Themes 資料夾中的檔案會與佈景主題對應。例如,您可能有 Aero.NormalColor.xaml、Luna.NormalColor.xaml、Royale.NormalColor.xaml 等。可能也有一個名為 generic.xaml 的檔案。當系統在佈景主題層級尋找資源時,會先在佈景主題特定檔案中尋找資源,再到 generic.xaml 中尋找資源。

當您的控制項位於與應用程式不同的組件中時,必須將全域資源置於項目層級或佈景主題層級。這兩種方法各有其優點。

在項目層級定義資源

您可以在項目層級定義共用資源,方式是建立自訂資源字典,然後將它與控制項的資源字典合併。使用這種方法時,可以隨意命名資源檔,而且資源檔可以與控制項位於相同的資料夾中。項目層級的資源也可以使用簡單的字串做為索引鍵。下列範例會建立名為 Dictionary1.XAML 的 LinearGradientBrush 資源檔。

<ResourceDictionary 
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <LinearGradientBrush 
    x:Key="myBrush"  
    StartPoint="0,0" EndPoint="1,1">
    <GradientStop Color="Red" Offset="0.25" />
    <GradientStop Color="Blue" Offset="0.75" />
  </LinearGradientBrush>

</ResourceDictionary>

定義了字典之後,您需要將它與控制項的資源字典合併。您可以使用 XAML 或程式碼完成這項動作。

下列範例會使用 XAML 合併資源字典。

<UserControl.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Dictionary1.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</UserControl.Resources>

這種方法的缺點是每次參考 ResourceDictionary 物件時,就會建立它。例如,如果您的程式庫中有 10 個自訂控制項,然後使用 XAML 合併每個控制項的共用資源字典,就會建立 10 個相同的 ResourceDictionary 物件。要避免這樣的問題,可以建立靜態類別,讓它在程式碼中合併資源,再傳回產生的 ResourceDictionary

下列範例會建立傳回共用 ResourceDictionary 的類別。

internal static class SharedDictionaryManager
{
    internal static ResourceDictionary SharedDictionary
    {
        get
        {
            if (_sharedDictionary == null)
            {
                System.Uri resourceLocater =
                    new System.Uri("/ElementResourcesCustomControlLibrary;component/Dictionary1.xaml", 
                                    System.UriKind.Relative);

                _sharedDictionary = 
                    (ResourceDictionary)Application.LoadComponent(resourceLocater);
            }

            return _sharedDictionary;
        }
    }

    private static ResourceDictionary _sharedDictionary;
}

下列範例會在呼叫 InitilizeComponent 之前,在自訂控制項的建構函式中將共用資源與該控制項的資源合併。由於 SharedDictionaryManager.SharedDictionary 是靜態屬性,因此 ResourceDictionary 只會建立一次。由於資源字典是在呼叫 InitializeComponent 之前合併,因此控制項可在其 XAML 檔案中使用資源。

public NumericUpDown()
{
    this.Resources.MergedDictionaries.Add(SharedDictionaryManager.SharedDictionary);
    InitializeComponent();

}

在佈景主題層級定義資源

WPF 可讓您為不同的 Windows 佈景主題建立資源。 身為控制項作者,您可以為特定佈景主題定義資源,以根據使用的佈景主題變更控制項的外觀。例如,Button 在 Windows 傳統配色佈景主題 (Windows 2000 的預設佈景主題) 中的外觀,就與 Windows Luna 佈景主題 (Windows XP 的預設佈景主題) 中的 Button 不同,因為 Button 針對每個佈景主題會使用不同的 ControlTemplate。  

某個佈景主題專屬的資源會存放在有特定檔名的資源字典中。這些檔案必須位於名為 Themes 的資料夾中,這是包含控制項之資料夾的子資料夾。下表列出資源字典檔以及與每個檔案相關聯的佈景主題:

資源字典檔名稱

Windows 佈景主題

Classic.xaml

Windows XP 上的「傳統」Windows 9x/2000 外觀

Luna.NormalColor.xaml

Windows XP 上的預設藍色佈景主題

Luna.Homestead.xaml

Windows XP 上的橄欖色佈景主題

Luna.Metallic.xaml

Windows XP 上的銀色佈景主題

Royale.NormalColor.xaml

Windows XP Media Center Edition 上的預設佈景主題

Aero.NormalColor.xaml

Windows Vista 上的預設佈景主題

您不需要為每個佈景主題定義資源。如果某個特定佈景主題未定義資源,控制項就會使用位於名為 generic.xaml 的資源字典檔中的泛用資源,這個檔案與佈景主題特定資源字典檔位於同一個資料夾中。雖然 generic.xaml 並未對應到特定 Windows 佈景主題,但它仍是佈景主題層級字典。

NumericUpDown 自訂控制項與佈景主題和 UI 自動化支援範例針對 NumericUpDown 控制項包含兩個資源字典:一個位於 generic.xaml,另一個位於 Luna.NormalColor.xaml。您可以執行應用程式,然後在 Windows XP 的銀色佈景主題和其他佈景主題之間切換,看看兩個控制項樣板之間的差異 (如果您執行 Windows Vista,可以將 Luna.NormalColor.xaml 重新命名為 Aero.NormalColor.xaml,然後在兩個佈景主題之間切換,例如 Windows 傳統配色和 Windows Vista 的預設佈景主題)。

當您將 ControlTemplate 放入任何佈景主題特定資源字典檔,就必須為控制項建立靜態建構函式,然後在 DefaultStyleKey 上呼叫 OverrideMetadata(Type, PropertyMetadata) 方法,如下列範例所示。

static NumericUpDown()
{
    DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericUpDown),
               new FrameworkPropertyMetadata(typeof(NumericUpDown)));
}

定義和參考佈景主題資源的索引鍵

當您在項目層級定義資源時,可以指派字串做為它的索引鍵,然後透過字串存取資源。當您在佈景主題層級定義資源時,則必須使用 ComponentResourceKey 做為索引鍵。下列範例會在 generic.xaml 中定義資源。

下列範例會藉由指定 ComponentResourceKey 做為索引鍵來參考資源。

指定佈景主題資源的位置

若要尋找控制項的資源,裝載的應用程式必須知道組件是否包含控制項特定的資源。為達此目的,您可以將 ThemeInfoAttribute 加入到包含控制項的組件。ThemeInfoAttributeGenericDictionaryLocation 屬性 (指定泛用資源的位置) 和 ThemeDictionaryLocation 屬性 (指定佈景主題特定資源的位置)。

下列範例將 GenericDictionaryLocationThemeDictionaryLocation 屬性設定為 SourceAssembly,指定泛用和佈景主題特定資源與控制項位於相同的組件中。

[assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly, 
           ResourceDictionaryLocation.SourceAssembly)]

從 UserControl 繼承和使用 ControlTemplate 的比較

有幾個範例示範不同方法來撰寫和封裝 NumericUpDown 控制項。在 NumericUpDown UserControl 與 DependencyProperty 和 RoutedEvent 範例中,NumericUpDown 繼承自 UserControl;在 NumericUpDown 自訂控制項與佈景主題和 UI 自動化支援範例中,NumericUpDown 繼承自 Control 並使用 ControlTemplate。本節將簡要描述兩者之間的一些差異,並說明為什麼使用 ControlTemplate 的控制項更容易擴充。

第一個主要差異在於繼承自 UserControl 的 NumericUpDown 不使用 ControlTemplate,但直接繼承自 Control 的控制項卻使用。下列範例顯示繼承自 UserControl 之控制項的 XAML。如您所見,XAML 與建立應用程式並以 WindowPage 開頭時看到的十分類似。

<!--XAML for NumericUpDown that inherits from UserControl.-->
<UserControl x:Class="MyUserControl.NumericUpDown"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MyUserControl">
    <Grid Margin="3">
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <Border BorderThickness="1" BorderBrush="Gray" Margin="2" 
                Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">

            <!--Bind the TextBlock to the Value property-->
            <TextBlock 
                Width="60" TextAlignment="Right" Padding="5"
                Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                               AncestorType={x:Type local:NumericUpDown}}, 
                               Path=Value}"/>

        </Border>

        <RepeatButton Name="upButton" Click="upButton_Click" 
                      Grid.Column="1" Grid.Row="0">Up</RepeatButton>

        <RepeatButton Name="downButton" Click="downButton_Click" 
                      Grid.Column="1" Grid.Row="1">Down</RepeatButton>

    </Grid>
</UserControl>

下列範例顯示繼承自 Control 之控制項的 ControlTemplate。請注意,ControlTemplate 看起來與 UserControl 中的 XAML 類似,只在語法方面稍有差異。

<!--ControlTemplate for NumericUpDown that inherits from
    Control.-->
<Style TargetType="{x:Type local:NumericUpDown}">
  <Setter Property="HorizontalAlignment" Value="Center"/>
  <Setter Property="VerticalAlignment" Value="Center"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type local:NumericUpDown}">
        <Grid Margin="3">
          <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
          </Grid.RowDefinitions>
          <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
          </Grid.ColumnDefinitions>

          <Border BorderThickness="1" BorderBrush="Gray" 
                  Margin="2" Grid.RowSpan="2" 
                  VerticalAlignment="Center" HorizontalAlignment="Stretch">

            <TextBlock Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Value}" 
                       Width="60" TextAlignment="Right" Padding="5"/>
          </Border>

          <RepeatButton Command="{x:Static local:NumericUpDown.IncreaseCommand}"  
                        Grid.Column="1" Grid.Row="0">Up</RepeatButton>

          <RepeatButton Command="{x:Static local:NumericUpDown.DecreaseCommand}"
                        Grid.Column="1" Grid.Row="1">Down</RepeatButton>

        </Grid>

      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

上述兩個範例的最大差異在於,使用 ControlTemplate 的範例有可自訂的外觀,而繼承自 UserControl 的範例則無。在 NumericUpDown 繼承自 UserControl 的情況中,應用程式開發人員完全無法變更控制項的外觀。事實上,即使 NumericUPDown 有 ControlTemplate 屬性 (因為 UserControl 繼承自 Control),如果有人嘗試設定它,會在執行階段發生例外狀況。另一方面,如果應用程式開發人員使用繼承自 Control 的 NumericUpDown,可以隨意為控制項建立新的 ControlTemplate。例如,您可以建立 ControlTemplate,將按鈕置於 TextBlock 的左邊和右邊,而非上方和下方。

兩種方法的差異在上面範例的語法可以明顯看出來。使用 ControlTemplate 的控制項會在 NumericUpDown 的 Style 中設定 Template 屬性。這是建立控制項樣板的常見方法。藉由在樣式中設定 Template 屬性,您可以指定控制項的所有執行個體都使用該 ControlTemplate。應用程式開發人員可以隨意變更 NumericcUpDown 的 Template 屬性來自訂其外觀。相反地,對於繼承自 UserControl 的控制項,其 XAML 會填入 NumericUpDown 的 Content 屬性 (<UserControl.Content> 會隱含在 XAML 中)。如果應用程式開發人員無法變更 Content 屬性,NumericUpDown 就無法使用。

這兩個範例之間的另外一個差異是控制項回應 [Up] 和 [Down] 按鈕的方式。繼承自 UserControl 的控制項會處理 Click 事件,而使用 ControlTemplate 的控制項會實作命令,並在其 ControlTemplate 中繫結至命令。因此,為 NumericUpDown 建立新 ControlTemplate 的應用程式開發人員也可以繫結至命令,並保留控制項的功能。如果 ControlTemplate 處理了 Click 事件而非繫結至命令,則應用程式開發人員在建立新 ControlTemplate 時就必須實作事件處理常式,因此會破壞 NumericUpDown 的封裝。

另一個差異是 TextBlockText 屬性與 Value 屬性之間的繫結語法。對於 UserControl,繫結會指定 RelativeSource 是父代 NumericUpDown 控制項並繫結至 Value 屬性。對於 ControlTemplateRelativeSource 是樣板所屬的控制項。兩者會達成相同目的,但兩個範例之間的繫結語法差異是值得注意的。

NumericUpDown 自訂控制項與佈景主題和 UI 自動化支援範例中,NumericUpDown 控制項與應用程式位於不同的組件中,而且會定義和使用佈景主題層級資源,但在 NumericUpDown UserControl 與 DependencyProperty 和 RoutedEvent 範例中,NumericUpDown 控制項與應用程式位於相同的組件中,而且不會定義或使用佈景主題層級資源。

最後,NumericUpDown 自訂控制項與佈景主題和 UI 自動化支援範例示範如何為 NumericUpDown 控制項建立 AutomationPeer。如需支援自訂控制項之 UI 自動化的詳細資訊,請參閱 WPF 自訂控制項的 UI 自動化

請參閱

概念

Windows Presentation Foundation 中的 Pack URI

其他資源

WPF 設計工具

控制項自訂

控制項自訂範例