程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 基礎: 非常用控件的模板

基礎: 非常用控件的模板

編輯:關於.NET

對於喜歡將常用控件轉變為非常用可視對象的程序員而言,Windows® Presentation Foundation (WPF) 提供了一種令人興奮不已的功能,即模板。控件的功能及其可視外觀一向是由復雜的控件代碼控制 。在 WPF 中,控件的功能仍通過代碼實現,但視覺效果與該代碼分離開來,並以 XAML 中定義的模板形 式存在。通過創建一個新模板(通常在 XAML 中,不用編寫任何代碼),程序員和設計師無需更改控件代 碼就能徹底修改控件的可視外觀。

在一年前的開篇專欄中,我講述了如何為 ScrollBar、ProgressBar 和 Slider 控件設計模板。但模 板化功能有利有弊:在設計新的自定義控件時,您要為控件的可視外觀提供一個默認模板,並允許該模板 由使用控件的程序員替換。您並不完全一定要這樣構造控件——事實上,在拙作 “Applications = Code + Markup”(應用程序 = 代碼 + 標記)(Microsoft Press®, 2006) 中,沒有任何自定義控件定義了可替換模板——但如果這麼做的話,需要使用該控件的 人(包括您)會省事得多。

本專欄的目的不是為了創建功能完備、外觀漂亮的控件,而是為了建 立一種機制,為分布在動態鏈接庫中的控件定義默認可替換模板。我在此討論的許多模板化技術都是通過 研究現有 WPF 控件上的模板學到的。如果您也想這麼做,“Applications = Code + Markup”(應用程序 = 代碼 + 標記)第 25 章中的 DumpControlTemplate 程序能讓您以方便的 XAML 格式從所有標准 WPF 控件中提取默認模板。

元素和控件

體驗過以前的 Windows 客 戶端編程環境的程序員很快就會在 WPF 類層次結構中發現一個有趣的現象。例如,在本機 Windows API 中,任何具有屏幕上可視外觀的東西都被歸類為“窗口”,而在 Windows 窗體中,所有東西 都是“控件”。但在 WPF 中,Control 類和許多其他可視對象(尤其是 TextBlock、Image、 Decorator 和 Panel),都從 FrameworkElement 派生。那麼,元素與控件到底有何區別呢?

首 先,Control 類將一組非常簡單的屬性添加到 FrameworkElement 類,包括 Foreground、Background 和 五個與字體相關的屬性。Control 並不直接使用這些屬性,它們只是為了方便從 Control 派生的類。

其次,Control 類添加了 IsTabStop 屬性和 TabIndex 屬性,這意味著控件在 tab 鍵導航鏈中 一般是停留點,而元素則不是。總而言之,元素用於觀看,而控件則用於交互(但元素仍能獲取焦點並對 鍵盤、鼠標和筆針輸入作出響應)。

第三,Control 類定義 ControlTemplate 類型的 Template 屬性。此模板一般是元素的可視樹和構成控件可視外觀的其他控件,通常還包含根據屬性變化和事件而更 改此可視外觀的觸發器。

第三個特征意味著從 Control 派生的類有一個可自定義的可視外觀,而 從 FrameworkElement 派生的其他類則沒有。TextBlock 和 Image 當然都有可視外觀,但自定義這些視 覺效果沒有任何意義,因為這些元素不會給它們顯示的格式化文本或位圖增添任何東西。在另一方面, ScrollBar 可有多種外觀,而功能則仍然相同。這就是模板的用途。

對於程序員來說,以下可能 是元素和控件之間最大的差別:如果從 FrameworkElement 派生,為了在屏幕上呈現元素的可視元素及其 子項,您很可能需要覆蓋 MeasureOverride、ArrangeOverride 和 OnRender。如果從 Control 派生,通 常情況下並不需要覆蓋這些方法,因為控件的視覺效果由 Template 屬性的 ControlTemplate 對象中的 可視樹定義。

WPF 包括一個名為 UserControl 的類,它通過 ContentControl 從 Control 派生 。通常推薦將此 UserControl 作為簡單自定義控件的基類,其用途廣泛。例如,拙作第 25 章中的 DatePicker 控件即從 UserControl 派生。但請記住 Control 與 UserControl 之間的如下顯著區別:當 從 UserControl 派生時,您可以在 XAML 中定義可視樹,但此可視樹是 UserControl 的 Content 屬性 的子項。UserControl 自有其簡單的默認模板,您可能不會替換該模板,因為它將 ContentPresenter 嵌 套在 Border 內部。

從 UserControl 所派生類的可視樹並不是用來被替換的,因此該類的代碼及其可視樹可以更緊密地耦 合。相反,如果您打算從 Control 派生並提供一個可替換的默認模板,代碼和可視樹之間的交互則應該 既簡單又記錄完備。

默認模板和 DLL

我決定在此專欄中再研究一下日歷控件。本專欄的源 代碼是一個名為 CalendarTemplateDemo 的單一 Visual Studio® 解決方案。它包含一個庫項目,創 建名為 CalendarControls 的 DLL 以及使用這些控件的四個演示程序。

CalendarControls 庫包 含名為 CalendarMonth、CalendarDay 和 CalendarDayNotes 的 CalendarControls 命名空間中的三個控 件的代碼和默認模板。這三個類都在各自的 C# 文件中定義。前兩個類從 Control 派生, CalendarDayNotes 則從 CalendarDay 派生。

為使默認模板能被使用控件的應用程序所替換,在 DLL 中定義默認模板的規則非常嚴格。如果不遵循這些規則,創建出的控件可能沒有默認模板或模板不能 由應用程序替換。DLL 項目必須有一個名為 Themes 的目錄,其中包含一個名為 generic.xaml 的文件, 根元素為 ResourceDictionary。資源為指向 DLL 中控件的 Style 元素。我的 CalendarControls 庫中 的 generic.xaml 文件類似圖 1 中所示。

Figure 1 Generic.xaml for CalendarControls

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:cc="clr-namespace:CalendarControls">
  <Style TargetType="cc:CalendarMonth" />
    ...
  </Style>
  <Style TargetType="cc:CalendarDay" />
    ...
  </Style>
  <Style TargetType="cc:CalendarDayNotes" />
    ...
  </Style>
</ResourceDictionary>

此文件中的每個 Style 元素至少有一個 Setter 元素,用於設 置控件的默認模板:

<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="cc:CalendarMonth">
      ...
    </ControlTemplate>
  </Setter.Value>
</Setter>

ControlTemplate 包含控件的可視樹。它還包含一個可選的 Resources 部分 ,用於定義模板中使用的一些資源。通常會有一個 Triggers 部分,用於定義模板如何對其他屬性或事件 中的變化作出響應。

或者,您可以將這些默認模板分成單獨的文件,我就是這麼做的。 CalendarControls 項目的 Themes 目錄包含三個文件,分別名為 CalendarMonthStyle.xaml、 CalendarDayStyle.xaml 和 CalendarDayNotesStyle.xaml。每個文件都有一個 ResourceDictionary 根 元素和 Style 類型的單一子項(指向特定控件)。generic.xaml 文件引用這三個資源文件,如圖 2 所 示。

Figure 2 Referencing Themes from Generic.xaml

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:cc="clr-namespace:CalendarControls">
  <ResourceDictionary.MergedDictionaries>
    <ResourceDictionary
      Source="CalendarControls;Component/themes/
        CalendarMonthStyle.xaml" />
    <ResourceDictionary
      Source="CalendarControls;Component/themes/
         CalendarDayStyle.xaml" />
    <ResourceDictionary
      Source="CalendarControls;Component/themes/CalendarDayNotesStyle.xaml" 

/>
  </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

為確保這些樣式能與 CalendarMonth、CalendarDay 和 CalendarDayNotes 類型的對象成功結合,這些類必須從其 DefaultStyleKey 屬性返回一個正確的值。由 於該原因,這些類的靜態構造函數改變了該屬性的默認值,從而返回類的類型。此過程通過覆蓋 DefaultStyleKey 依賴屬性的元數據完成:

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

OverrideMetadata 的第一個參數是修改元數據的類的類型;給 FrameworkPropertyMetadata 構造函數的參數表示 DefaultStyleKey 屬性的新默認值,它也是類的類型。

DLL 的程序集信息文件(通常命名為 AssemblyInfo.cs)是存放以下程序集屬性的好地方:

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

這意味著沒有主題特 定的模板集,但控件的通用模板所在的程序集 (DLL) 與控件本身的相同。

控件派生基礎知識

從 Control 派生時,您可能需要定義一些新的公共屬性。在大多數情況下,您應該使用依賴屬性 支持這些屬性,以使它們成為數據綁定和動畫的目標。即使它們是只讀屬性,不能成為綁定目標,但是使 用依賴屬性能提供一種通知機制,這樣當這些屬性改變時,其他對象就會收到通知。

如果您需要 定義一個已經由其他類定義但並未從 Control 繼承的屬性,不要完全重新定義該屬性。而是對現有的依 賴屬性調用 AddOwner。將 AddOwner 返回的值保存為公共靜態只讀字段,並在 CLR 屬性中使用該值。

如果需要更改繼承屬性的默認值,有多種方法。直接在類的構造函數中設置屬性值的做法並不可 取,因為這種所謂的本地設置有非常高的優先權,不能被樣式或屬性覆蓋。低優先權方法是調用依賴屬性 上的 OverrideMetadata,正如我在上文中使用 DefaultStyleKey 屬性演示的一樣。Control 類本身以這 種方式設置 IsTabStop 的默認 true 值。

另一種低優先權方法是使用與控件的默認模板在同一個 Style 元素中的 Setter 設置屬性。許多 WPF 控件使用該技術設置與控件的可視外觀關聯的屬性,如 SnapsToDevicePixels、MinWidth 或 MinHeight。如果類需要在每次繼承屬性的值改變時都收到通知,則 可以使用 OverrideMetadata 安裝一種附加回調方法。

從 Control 派生的最大挑戰是以一種方式 定義代碼和模板之間的交互,能夠滿足您的所有需要而又不阻止他人編寫替代模板。一般而言,控制代碼 能以幾種方式適應模板:

代碼定義模板可通過 TemplateBinding 標記擴展訪問的屬性。

代碼定義模板可將之用作觸發器的屬性和事件。

代碼定義模板中的按鈕可以觸發的 RoutedCommand 屬性或字段。

代碼假設某些幫助器元素存在於模板中;它有時通過預定義的名稱 引用這些元素。

我將為這幾項技術一一舉例。

在某些情況下,替換模板時需要注意從 Control 派生的類,使它能訪問新模板並建立指向模板的鏈接。Control 類定義一個虛擬 OnTemplateChanged 方法,您可以覆蓋該方法,以便在 Template 屬性改變時收到通知。然而,從我的經 驗來看,OnTemplateChanged 方法實際上並不是執行任何必需鏈接的好地方,因為模板尚未得到應用(以 使用 WPF 語言)。一種高明得多的策略是覆蓋由 FrameworkElement 定義但沒有默認實現(非常奇怪) 的 OnApplyTemplate 方法。

元素和控件的層次結構

CalendarControls 庫中的 MonthCalendar 控件顯示一年中的單個月份。TwoCalendars 程序顯示兩個 MonthCalendar 實例,一個用 英語,一個用法語,如圖 3 所示。

Figure 3 Two Instances of MonthCalendar

正是因為遵循了 WPF 設計原理,默認模板才與 Win32® 的對應體如此相似——它們通 常都有些單調,您現在看到的日歷確實如此。但從項目伊始,我的目標之一就是正確實現不一定要從星期 日(法語為 dimanche)開始的日歷。然而,我並沒有大膽地打算超出公歷。

為了使這些示例相對 簡單,我決定不實現選擇日期的概念。不過,正如您所見,名為 IsToday 的屬性導致某個特殊日期被突 出顯示。

我從 WPF 學到的非常重要的經驗之一就是,復雜的控件可通過較簡單的控件和元素構建 。您在圖 3 中看到的都不是由 CalendarMonth 的代碼部件所定義。它完全是默認的 XAML 模板的一部分 :一個邊框環繞四周,並提供背景。頂部的按鈕是 RepeatButton 控件,他們以一個月或一年向前或向後 導航。按鈕之間是 TextBlock。星期幾是 StatusBar 上的 StatusBarItem 對象,使用 UniformGrid 作 為 ItemsPanel。日期也在 UniformGrid 中顯示。

由於一些原因(在下文將很明確),我決定將 單個日期設為獨立的控件,並命名為 CalendarDay。CalendarDay 也從 Control 派生,默認模板是一個 包含 TextBlock 的 Border。當 CalendarMonth 控件顯示月份時,會生成 28、29、30 或 31 個 CalendarDay 對象。顯示這兩個日歷的 TwoCalendars.xaml 文件如圖 4 所示。

Figure 4 TwoCalendars.xaml

<!-- TwoCalendars.xaml by Charles Petzold, Sept. 2007 -->
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:cc="clr-namespace:CalendarControls;assembly=CalendarControls"
 x:Class="Petzold.TwoCalendars.TwoCalendars"
 Title="Two Calendars Demonstration">
 <Window.Resources></Window.Resources>
  
 <Grid>
  <Grid.ColumnDefinitions>
   <ColumnDefinition Width="Auto" />
   <ColumnDefinition Width="Auto" />
  </Grid.ColumnDefinitions>
  <Grid.RowDefinitions>
   <RowDefinition Height="Auto" />
  </Grid.RowDefinitions>
  <cc:CalendarMonth Grid.Column="0" Margin="24" />
    
  <cc:CalendarMonth Grid.Column="1" Margin="24"
   Culture="fr-FR" />
 </Grid>
</Window>

代碼和 XAML

由於 CalendarDay 比 CalendarMonth 簡單得多,而且足 夠短(能完整顯示),因此這是我希望在此項目中首先重點關注的部分。圖 5 顯示了 CalendarDay.cs。 注意這並不是一個部分類。默認模板並不是 CalendarDay 類的一部分;相反,它自動應用於 CalendarDay 類型的對象。

Figure 5 CalendarDay.cs

// CalendarDay.cs by Charles Petzold, Sept. 2007
using System;
using System.Windows;
using System.Windows.Controls;
namespace CalendarControls
{
  public class CalendarDay : Control
  {
    // Static constructor: Change defaults.
    static CalendarDay()
    {
      DefaultStyleKeyProperty.OverrideMetadata(typeof(CalendarDay),
        new FrameworkPropertyMetadata(typeof(CalendarDay)));
      IsTabStopProperty.OverrideMetadata(typeof(CalendarDay),
        new FrameworkPropertyMetadata(false));
    }
    // Date dependency property and property.
    public static readonly DependencyProperty DateProperty =
      DependencyProperty.Register("Date",
        typeof(DateTime),
        typeof(CalendarDay),
        new PropertyMetadata(DateChangedCallback));
    public DateTime Date
    {
      set { SetValue(DateProperty, value); }
      get { return (DateTime)GetValue(DateProperty); }
    }
    // IsToday dependency property and property.
    static readonly DependencyProperty IsTodayProperty =
      DependencyProperty.Register("IsToday",
        typeof(bool),
        typeof(CalendarDay),
        new PropertyMetadata(false));
    public bool IsToday
    {
      set { SetValue(IsTodayProperty, value); }
      get { return (bool)GetValue(IsTodayProperty); }
    }
    // Day read-only dependency property and property.
    static readonly DependencyPropertyKey DayKey =
      DependencyProperty.RegisterReadOnly("Day",
        typeof(string),
        typeof(CalendarDay),
        new PropertyMetadata());
    public static readonly DependencyProperty DayProperty =
      DayKey.DependencyProperty;
    public string Day
    {
      protected set { SetValue(DayKey, value); }
      get { return (string)GetValue(DayProperty); }
    }
    // DateChangedCallback method
    static void DateChangedCallback(DependencyObject obj,
      DependencyPropertyChangedEventArgs args)
    {
      CalendarDay calday = obj as CalendarDay;
      calday.Day = calday.Date.Day.ToString();
    }
  }
}

類定義三個新屬性,所有屬性都由依賴屬性支持:DateTime 類型的日期,bool 類型的 IsToday 以及字符串類型、名為 Day 的只讀屬性。Day 屬性完全為了方便模板。每當 Date 屬性改變時 ,類就將 DateTime 對象的 Day 屬性轉換為字符串,並將它設置為自己的 Day 屬性。

CalendarDayStyle.xaml 文件包含 CalendarDay 控件的模板。模板通常以 Border 元素開頭,該元素 的屬性通過 TemplateBinding 擴展設置為由 Control 類定義的同名屬性。如果不在模板中顯式引用這些 屬性,它們將不會對控件的可視外觀產生任何影響!

通過可視樹繼承的 Control 屬性無需在模板 中引用。這些繼承屬性為 Foreground 和五個與字體相關的屬性。對這些屬性的任何更改都會由 TextBlock 自動繼承;TextBlock 顯示由 CalendarDay 類定義的 Day 屬性。 HorizontalContentAlignment 和 VerticalContentAlignment 屬性決定文本在單元格中的對齊方式。模 板以一個 Triggers 部分結束,如下文所示:

<Trigger Property="IsToday" 

Value="True">
 <Setter Property="Background"
  Value="{DynamicResource
  {x:Static SystemColors.HighlightBrushKey}}" />
 <Setter Property="Foreground"
  Value="{DynamicResource
  {x:Static SystemColors.HighlightTextBrushKey}}" />
</Trigger>

當 CalendarDay 定義的 IsToday 屬性為 true 時,Background 和 Foreground 屬性將設置為突出顯示項目的系統畫筆。此 Triggers 部分也可以包含 MultiTrigger 元素 和 EventTrigger 元素,後者能觸發動畫。

雖然 CalendarDay 包含一個 Border 元素,但卻沒有 可見的邊框。CalendarMonth 模板的定義類似,整個月份周圍也沒有可見邊框。邊框不可見是因為 Control 將 BorderBrush 的默認值定義為空值,將 BorderThickness 的默認值定義為零,就像 Border 元素本身一樣。您可能會強烈地認為,應將每一天都圈在一個可見的方框內。為什麼不將模板中的屬性設 置為更合理的值呢?

<Border BorderThickness="1"
    BorderBrush="Black" ...

此方法的問題是,將來使用該控件的任何程序員 都必須更改默認模板才能更改這些值。模板的硬編碼屬性值應盡可能地少。如果您是控件及其默認模板的 設計者,您可以使用資源文件中的 Style 元素將這些屬性設置為更合理的默認值。如果您是控件的使用 者,則可以在使用該控件時設置這些屬性。

例如,在 TwoCalendars.xaml 文件中,您可以按如下 方法更改第一個日歷:

<cc:CalendarMonth BorderThickness="1"
         BorderBrush="Black" ...

這麼做會給整個日歷加上一個可見的 邊框。但如何設置 CalendarDay 邊框呢?CalendarDay 元素甚至不在 TwoCalendars.xaml 文件中顯示, 因為 CalendarMonth 在內部生成所有 CalendarDay 對象!

解決方案是使用以 CalendarDay 為目 標的樣式。嘗試將它插入 TwoCalendars.xaml 的 Resources 部分:

<Style 

TargetType="cc:CalendarDay">
  <Setter Property="BorderThickness" Value="1" />
  <Setter Property="BorderBrush"
      Value="{DynamicResource
        {x:Static SystemColors.ControlTextBrushKey}}" />
  <Setter Property="HorizontalContentAlignment"
      Value="Center" />
  <Setter Property="VerticalContentAlignment"
      Value="Center" />
  <Setter Property="FontStyle" Value="Italic" />
</Style>

此樣式不僅會設置邊框,也會設置內容對齊方式屬性,使單元格中的日期居中 ,並使數字斜體。

默認的 CalendarMonth 模板包含在 CalendarMonthStyle.xaml 文件中。它以 Border 開頭,隨後是包含三個垂直單元格的 Grid,用於按鈕和月份名稱、星期幾以及日歷網格本身。 Grid 的頂部單元格是另一個 Grid,包含五個水平單元格,用於按鈕和月份名稱。

CalendarMonth 類准備了幾種屬性,模板可用這些屬性顯示文字信息,如 MonthName、AbbreviatedMonthName 和格式化 的 AbbreviatedYearMonth。(CalendarMonth 包含許多來自 DateTimeFormatInfo 的這類信息。) CalendarMonth 也在兩個數組中存儲星期幾的名稱,即 DayNames 屬性和 AbbreviatedDayNames 屬性。 它們與 DateTimeFormatInfo 類中的同名屬性有些差別,因為 DateTimeFormatInfo 中的數組通常以星期 日開頭。

由 CalendarMonth 定義的 FirstDayInWeek 屬性基於當月第一天為星期幾, FirstDayOfWeek 屬性來自 DateTimeFormatInfo。

CalendarMonthStyle.xaml 中的模板擁有自己的 Resources 部分。這對於在模板內設置樣式非常方便 ,對標記中未明確顯示的元素類型尤為如此。例如,注意星期幾如何顯示。將 AbbreviatedDayNames 屬 性分配給用 UniformGrid 作為其 ItemsPanel 的 StatusBar。AbbreviatedDaysNames 數組的字符串在內 部成為 StatusBarItem 控件的 Content 屬性。StatusBarItem 實際上不會在此標記中顯示,但 Style 能指向 StatusBarItem 控件,將內容居中,並在名稱之間插入一些空格。

訪問命名元素

到目前為止,我們已經介紹了類如何定義 XAML 文件通過 TemplateBinding 擴展和觸發器引用的屬性 的示例。但有些時候,類訪問模板中的特定元素更為方便。在這個示例中,CalendarMonth 類需要為一個 特定月份生成所有 CalendarDay 對象,且這些對象最終必須在某種面板中。看起來最簡便的方法是為此 面板指定一個代碼引用的名稱。

您會發現,CalendarMonth 模板中的第二個 UniformGrid 被命名為 PART_Panel。此名稱與幾個 WPF 控件的默認模板中定義的名稱類似。CalendarMonth 類定義之前是一個屬性,該屬性指明此名稱以及代碼 期望它標識的元素類型:

[TemplatePart(Name = "PART_Panel", Type = typeof(Panel))]

此屬性是為了方便可視化設計器,不是必需的。但請注意,我已暗示模板中的此元素可以是 Panel 的 任何派生物。

作為對調用 ApplyTemplate 的響應,CalendarMonth.cs 中的此語句獲取此面板並將其存儲為字段:

pnl = Template.FindName("PART_Panel", this) as Panel;

注意這並不是普通的 FindName 方法。這是由派生 ControlTemplate(以及 ItemsPanelTemplate、 DataTemplate 和 HierarchicalDataTemplate)的 FrameworkTemplate 定義的 FindName 方法,因此可 通過 CalendarMonth 對象的 Template 屬性訪問它。

如果此 pnl 對象為空會發生什麼情況?這意味著模板不包括任何名為 PART_Panel 的內容,或者可能 是名為 PART_Panel 的元素不是從 Panel 派生。如果某個類在模板中找不到一個命名元素,它會根據現 有情況默認繼續。

在將名稱和類型與模板中的幫助器元素相關聯時,盡可能使之通用。在這種情況下,MonthCalendar 需要一個帶 Children 屬性的元素,因此,Panel 似乎比較合適。雖然 UniformGrid 用起來顯然不錯, 但 MonthCalendar 不需要特殊類型的面板。

生成命令

CalendarMonth 模板必須包含向前導航和向後導航的按鈕。如何編寫對這些按鈕按下作出響應的代碼 ?最簡便的方法是由類定義公共靜態只讀字段或 RoutedCommand 類型的只讀屬性。現有的 WPF 控件在將 這些 RoutedCommand 對象定義為字段或屬性時存在不一致。ScrollBar 類將它們定義為只讀字段,並將 它們命名為 LineDownCommand 和 LineLeftCommand 等。Slider 類將它們定義為只讀屬性,命名為 DecreaseLarge、DecreaseSmall、IncreaseLarge 和 IncreaseSmall。我選擇了 ScrollBar 方法,並將 我的 RoutedCommand 對象定義為字段,如下所示:

public static readonly RoutedCommand NextMonthCommand =
new RoutedCommand("NextMonth",
typeof(CalendarMonth));

注意這些都是靜態的!模板以指定給控件的 Command 屬性的完全限定名稱引用它們:

<ToggleButton Command="
CalendarMonth.NextMonthCommand" ...

並不是許多控件都可以如此定義 Command 屬性,即您可以將其設置為 RoutedCommand 類型的對象。 只有 ButtonBase、MenuItem 和 Hyperlink 可以。

通過將對象添加到其 CommandBindings 集合,CalendarMonth 構造函數將這些 RoutedCommand 對象 與一個執行方法鏈接:

CommandBindings.Add(new
CommandBinding(NextMonthCommand,
NextMonthExecuted));

此外,您也可以指定一個可執行的方法,它可讓代碼設置一個 Boolean,表明控件是否有效。如果無 效,它將自動被禁用。

CalendarMonth.cs 中的 NextMonthExecuted 方法如下所示:

void NextMonthExecuted(object sender,
ExecutedRoutedEventArgs args)
{
Date = Date.AddMonths(1);
}

使用 RoutedCommand 對象的一個優勢是您可以通過調用 Execute 方法相當輕松地從代碼觸發它們。 例如,您可以使用某些鍵盤輸入觸發命令。但在許多情況下,通過將 InputGesture 類型的對象(派生 KeyGesture 和 MouseGesture 的抽象類)與 RoutedCommand 相關聯,您無需顯式的鍵盤處理便能完成這 一工作。CalendarMonth 的靜態構造函數將 KeyGesture 對象添加到所有四個 RoutedCommand 字段中, 如下所示:

NextMonthCommand.InputGestures.Add(
new KeyGesture(Key.PageDown));

這些筆勢本應添加到 RoutedCommand 對象的原始字段定義中,但這個方法有點笨,因為您需要在構造 函數中提供整個 InputGestureCollection。

替換模板

當您編寫一個應用程序來試圖替換默認模板並使日歷完全改觀時,真正考驗這整個練習的時候來臨了 。NewCalendar 程序為垂直顯示日期的 CalendarMonth 定義一個新模板。該程序創建 12 個月歷,將它 們並排顯示(請參見圖 6)。

Figure 6 The NewCalendar Display

也可以從 CalendarDay 派生新類,以增強日歷的功能,但最初這似乎不可行:CalendarMonth 負責生 成 28、29、30 或 31 個 CalendarDay 實例。似乎如果您希望從 CalendarDay 派生,則必須同時從 CalendarMonth 派生才能更改該邏輯。為避免這種情況,我為 Type 類型定義了另一個名為 DayType 的 CalendarMonth 屬性。默認情況下,此屬性等於 typeof(CalendarDay),但它也可以設置為派生自 CalendarDay 的類的類型。

CalendarDayNotes 從 CalendarDay 派生,並假定其模板包含一個名為 PART_TextBox 的 TextBox 類 型控件。您向這些 TextBox 控件鍵入的任何內容都由 CalendarDayNotes 保存在小文件中,由派生自日 期的文件名標識。下次您再運行該程序時,它會加載所有內容。CalendarDayNotes 的默認模板非常單調 ,而 ReminderCalendar 程序的模板則稍微漂亮一些,它將日期顯示為狹長的形狀,並用顏色表示漸變, 如圖 7 所示。

Figure 7 The ReminderCalendar

為了使日期更漂亮一些,甚至都不需要從 CalendarDay 派生。CalendarControls 庫還有一個從 Viewport3D 派生的名為 MoonDisk 的類,以及一個 DateTime 屬性,以模擬引導月亮球面上的光線。 MoonPhaseCalendar 創建日歷的速度並不是世界上最快的——它需要為每個月生成多達 31 個 Viewport3D 對象——但它確實與普通日歷不一樣,如圖 8 所示。

Figure 8 The MoonPhaseCalendar Display

請將您想詢問的問題和提出的意見發送至 [email protected].

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved