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

基礎: 使用模板自定義WPF控件

編輯:關於.NET

隨著 Windows Vista™ 和 Microsoft® .NET Framework 3.0 的發布,出現了許多可供開發 人員學習、討論和使用的新技術。新的工具、庫和范例將改變構建托管應用程序的方法,帶來了巨大的可 能性。 我們推出的這一新的每月專欄將介紹用於開發應用程序的基本技術。您所熟知的業內專家將輪番 與您探討 Windows® Presentation Foundation、Windows Communication Foundation 和 Windows Workflow Foundation。我們開始吧。

在 Windows 中自定義現有控件通常需要四個步驟。首先需要有靈感。然後需要進行研究和探索。這一 過程難免會有困難。而最終發現需要完全重寫。由於很難訪問到將控件的可視部分與其功能相關聯的代碼 ,因而通常無法自定義控件。此代碼對於控件至關重要,因此必須完全接受它,或者完全跳過並替換它。

Windows Presentation Foundation(作為 .NET Framework 3.0 的一部分提供)的開發人員已經 不可避免地感受到了自定義控件的艱辛。他們提出了一個令人耳目一新的強大解決方案,我們稱之為 “模板”。

Windows Presentation Foundation 模板不僅簡單,而且功能強大,讓我 能迅速理解其概念。我很快就理解了 Windows Presentation Foundation 樣式(通常容易與模板混淆) ,但是模板需要我們花費更多的時間去了解。

Windows Presentation Foundation 中的每個具有 可視外觀的預定義控件也都具有一個完全定義了其外觀的模板。此模板是類型 ControlTemplate(設置為 由 Control 類定義的 Template 屬性)的一個對象。

在應用程序中使用 Windows Presentation Foundation 控件時,您可以用自己設計的模板替換該默認模板。您可以保留控件的基本功能(包括所有 鍵盤和鼠標操作的處理),但您可以為其設置完全不同的外觀。這就是在提到 Windows Presentation Foundation 控件時所說的“變臉”(不太文雅)的含義。控件都有默認外觀,但是此外觀並 不是固定地對應控件的內部功能。

用代碼編寫模板不太合適。而使用可擴展應用程序標記語言 (XAML) 進行編寫會更容易,因為模板完全可以用 XAML 來表示,從而能夠借助於可視化設計工具進行設 計。 如果您要編寫像現有 Windows Presentation Foundation 控件一樣工作但外觀不同的自定義控件, 請立即停止!很可能僅使用一個模板即可得到這樣的控件。

可下載的源代碼包含七個獨立的 XAML 文件,我將在整個專欄中進行討論。在制作此專欄的過程中,未編譯任何 C# 代碼!如果您已安裝 .NET Framework 3.0 SDK,則可以使用我的《Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation》一書中的類似程序 XAMLPad 或 XAML Cruncher 編輯這些文件。

Windows Presentation Foundation 支持用於顯示控件內容的其他類型的模板,但本專欄將僅討 論類型 ControlTemplate 的對象。

轉儲默認值

Windows Presentation Foundation 中的 每個具有可視外觀的預定義控件都有一個默認模板。如果您對編寫自定義模板感興趣,請研究這些默認值 。

我的書中第 25 章的 DumpControlTemplate 顯示了控件的簡單 XAML 格式的默認 Template 屬 性。(您可以下載源代碼。)如果希望自己編寫模板轉儲程序,以下是執行關鍵步驟的代碼:

XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.IndentChars = new string(' ', 4);
settings.NewLineOnAttributes = true;
StringBuilder strbuild = new StringBuilder();
XmlWriter xmlwrite = XmlWriter.Create(strbuild, settings);
XamlWriter.Save(ctrl.Template, xmlwrite);

此代碼假設 ctrl 是 Control 派生類的實例, 並且已在屏幕上呈現 ctrl。(這是我的個人經驗,否則 Template 屬性將為 null。)如果 Template 屬 性為 null,XamlWriter.Save 調用將引發異常,因為該值將用於某些無可視外觀的控件。在此代碼的末 尾,對 strbuild 調用 ToString 可以提供包含模板的完整 XAML 文檔。

此默認模板中的 XAML 可能比您編寫的 XAML 冗長一些。例如,您可能會編寫:

<Trigger Property="IsEnabled" Value="False">

但是在由 XamlWriter.Save 生成的 XAML 文件中,您將看到如下標記:

<Trigger Property="UIElement.IsEnabled">
  <Trigger.Value>
    <s:Boolean>False</s:Boolean>
  </Trigger.Value>

s 前綴是用 .NET 系統命名空間的 xmlns 命名空間聲明定義的。

可視樹

讓我們首先來看一下一個非常簡單但是很有用的控件模板:CheckBox 模板。假設您希望得到外觀比標 准 CheckBox 更生動一些的控件。您可能希望用戶所選擇的選項顯示為大的綠色復選標記或大的紅色 X, 並顯示在 CheckBox 的全部內容上。文件 BigCheckCheckBox.xaml 顯示了一個這樣的 CheckBox 模板。

在 XAML 文件中,模板是類型 ControlTemplate 的元素。在小型的 Windows Presentation Foundation 程序中,通常在 XAML 文件根元素的 Resources 部分定義 ControlTemplate 元素。對於大 型應用程序或定義多個應用程序共用的模板時,ControlTemplate 元素位於自己的具有 ResourceDictionary 根元素的 XAML 文件中。

無論是哪種情況,ControlTemplate 元素通常均包含三個部分。首先是 Resources 部分(可選),定 義了模板所使用的樣式或畫筆。(BigCheckCheckBox.xaml 的模板沒有 Resources 部分。)然後,模板 定義了模板的可視樹。此樹以元素或其他控件的布局的形式描述所需要的控件外觀。模板包含一個 Triggers 部分,用於指明在響應控件屬性更改時,可視樹元素應如何變化。圖 1 和圖 2 顯示了自定義 CheckBox 的大部分 ControlTemplate 元素。圖 1 中的代碼顯示了可視樹,圖 2 接著顯示了 ControlTemplate 元素的 Triggers 部分。

Figure 2 Triggers 部分

<ControlTemplate.Triggers>
  <Trigger Property="IsChecked" Value="True">
    <Setter TargetName="path"
        Property="Data"
        Value="M 0 5 L 3 10 10 0" />
     <Setter TargetName="path"
        Property="Stroke"
        Value="Green" />
  </Trigger>
  <Trigger Property="IsChecked" Value="{x:Null}">
     <Setter TargetName="path"
        Property="Data"
        Value="M 0 2.5 A 5 2.5 0 1 1 5 5
            L 5 8 M 5 10 L 5 10" />
    <Setter TargetName="path"
        Property="Stroke"
        Value="Blue" />
  </Trigger>
  <Trigger Property="IsEnabled" Value="False">
    <Setter Property="Foreground"
        Value="{DynamicResource
          {x:Static SystemColors.GrayTextBrushKey}}" />
  </Trigger>
  ...
</ControlTemplate.Triggers>

Figure 1 描述 CheckBox 的可視樹

<ControlTemplate x:Key="templateBigCheck"
         TargetType="{x:Type CheckBox}">
  <Border BorderBrush="{TemplateBinding BorderBrush}"
      BorderThickness="{TemplateBinding BorderThickness}"
      Background="{TemplateBinding Background}">
    <Grid>
      <!-- ContentPresenter displays content of CheckBox -->
      <ContentPresenter
        Content="{TemplateBinding Content}"
        ContentTemplate="{TemplateBinding ContentTemplate}"
        Margin="{TemplateBinding Padding}"
        HorizontalAlignment="{TemplateBinding
                    HorizontalContentAlignment}"
        VerticalAlignment="{TemplateBinding
                    VerticalContentAlignment}" />
      <!-- This Border displays a semi-transparent red X -->
      <Border>
        <Border.Background>
          <VisualBrush Opacity="0.5">
            <VisualBrush.Visual>
              <Path Name="path"
                 Data="M 0 0 L 10 10 M 10 0 L 0 10"
                 Stroke="Red"
                 StrokeStartLineCap="Round"
                 StrokeEndLineCap="Round"
                 StrokeLineJoin="Round" />
            </VisualBrush.Visual>
          </VisualBrush>
        </Border.Background>
      </Border>
    </Grid>
  </Border>

圖 1 顯示了 BigCheckBox.xaml 的摘要,說明了 ControlTemplate 開始標記和可視樹的使用。由於 此模板是資源,因此它必須包含一個帶有其資源名稱的 x:Key 屬性。您會注意到其中使用了 TargetType 屬性,盡管不是必需使用,但這麼做便無需在每個屬性前面加上類名,從而簡化了模版的其余部分。

此可視樹的頂級元素為一個 Border(邊框),後者可以作為單個子元素的父元素。在本示例中,此 Border 的三個屬性均使用 TemplateBinding 標記擴展進行指定。TemplateBinding 用於將可視樹中的元 素屬性綁定到控件屬性。

ContentPresenter 元素中的 TemplateBinding 表達式更有趣,它可以將按鈕和 ContentControl 派 生而來的其他控件的內容格式化。正是 ContentPresenter 允許在 Button 或 CheckBox 內顯示幾乎任何 內容。請注意,TemplateBinding 用於將 ContentPresenter 的 Margin 屬性設置為正在模板化的 CheckBox 控件的 Padding 屬性。Margin(邊緣)是元素外部的額外空間;Padding(留白)是控件中未 被其內容占據的空間,據此您可以看出此綁定所隱含的原理。

ContentPresenter 與另一個邊框一起顯示在單個單元格的網格面板內。網格單元格通常用於容納必須 分層顯示的多個子項。因此,第二個邊框顯示在 ContentPresenter 頂部。為此邊框指定包含 50% 不透 明畫筆(其中包含紅色 X 標記)的背景。請注意,應為呈現紅色 X 標記的 Path 元素指定一個路徑名。 模板的 Triggers 部分會引用此名稱。

模板觸發器

ControlTemplate 的最後部分通常專用於 Trigger 元素。對可視樹的組成元素的這些屬性更改是基於 控件的屬性更改進行的。圖 2 顯示了自定義 CheckBox 模板的 Triggers 部分的很大部分。

默認情況下,CheckBox 的 IsChecked 屬性為 false 並且 CheckBox 顯示紅色 X 標記。第一個 Trigger 元素指示在 IsChecked 變為 true 時,名為 path 的元素的 Data 屬性和 Stroke 屬性應設置 為顯示綠色復選標記。下一個 Trigger 元素通過顯示一個藍色問號來反映 IsChecked 為 null 值時的情 況(在 CheckBox 設置為三態操作時會發生)。當 IsEnabled 屬性為 false 時,最後一個 Trigger 元 素將控件的 Foreground 屬性更改為灰色畫筆。(BigCheckCheckBox.xaml 文件具有一個額外的 Trigger 元素,此元素在控件獲得輸入焦點時將在控件內容周圍顯示虛線。)

模板的 Triggers 部分可以非常寬泛。通常還會在其中包含 IsMouseOver 屬性的 Trigger 元素,因 此當鼠標從控件上面經過時,控件會做出響應。

然後您可以在創建 CheckBox 的元素中引用模板,例如:

<CheckBox Template="{StaticResource templateBigCheck}" ...

圖 3 顯示了由 BigCheckCheckBox.xaml 文件顯示的三個 CheckBox 控件。兩個包含文本內容,第三 個(其 IsThreeState 屬性設置為 true,IsChecked 屬性設置為 null)包含位圖。

圖 3使用自定義模板的 CheckBox 控件

改進

您可能會懷疑模板概念應用於較復雜控件的能力。畢竟,某些控件具有可動部分並更多地涉及與用戶 的交互。為了消除您的此類疑慮,我們將在本專欄的其余部分中介紹一下從 RangeBase 派生而來的三個 控件的模板。它們是 ProgressBar(進度條)、ScrollBar(滾動條)和 Slider(滑塊)。

要正確運行,更復雜控件的模板需要具有特定名稱的某些類型的元素。例如,ProgressBar 可視樹必 須具有類型 FrameworkElement(或由 FrameworkElement 派生)的兩個元素,名為 PART_Track 和 PART_Indicator。這兩個元素被稱為模板的“已命名部件”。這些名稱可以在 ProgressBar 類的 SDK 文 檔中找到,它們是 TemplatePart 屬性。如果您的模板不包括具有這些名稱的元素,控件將無法正確運行 。

如果 ProgressBar 以其默認的水平方向顯示,則控件的內部邏輯將 PART_Indicator 元素的 Width 屬性設置為 PART_Track 元素的 ActualWidth 屬性的一個分數。該分數取決於 ProgressBar 的 Minimum 、Maximum 和 Value 屬性。如果 ProgressBar 為豎直方向,則使用這兩個元素的 Height 和 ActualHeight 屬性。

實際上,兩種不同方向的 ProgressBar 控件對應兩個默認模板。(對於 ScrollBar 和 Slider,也是 如此。)如果希望您的新 ProgressBar 對兩個方向均支持,則應單獨編寫兩個模板,並在同樣為 ProgressBar 定義的 Style 元素的 Triggers 部分中選擇這兩個模板。

圖 4 是 BareBonesProgressBar.xaml 文件的摘要,該文件顯示了一個完整的 ProgressBar 元素,其 中 ControlTemplate 對象作為該元素的屬性。該文件還包含一個 ScrollBar,用於通過綁定來測試 ProgressBar。在操作 ScrollBar 時,藍色矩形(PART_Indicator 元素)的寬度可以變化,范圍為零到 紅色矩形(PART_Track 元素)的寬度。請注意,盡管 BareBonesProgressBar.xaml 為 PART_Track 矩形 指定了明確的寬度,但實際上您應在任何可能的情況下避免使用明確的尺寸。

Figure 4 ProgressBar 模板骨架

<ProgressBar Margin="50" HorizontalAlignment="Center"
       Value="{Binding ElementName=scroll, Path=Value}">
  <ProgressBar.Template>
    <ControlTemplate>
      <StackPanel>
        <Rectangle Name="PART_Track"
              Height="20" Width="300" Fill="Red" />
        <Rectangle Name="PART_Indicator"
              Height="20" Fill="Blue" />
      </StackPanel>
    </ControlTemplate>
  </ProgressBar.Template>
</ProgressBar>

指示元素通常在跟蹤元素之內,而且您可能會考慮將 Border 元素用於跟蹤。但是請注意:如果為該 Border 指定的 BorderThickness 不為零,則其粗細度將屬於 Border 元素總寬度的一部分,邊框內的寬 度將會略微小一些。如果出於顯示目的而采用了非零邊框厚度的 Border,則應為跟蹤元素指定位於第一 個邊框內的粗細度為零的 Border,並將其他內容放入第二個 Border 內以用作指示器。(如果使用 Border 元素而不使用其邊框或背景屬性,請考慮使用 Decorator,Decorator 是 Border 的無邊框、無 背景的祖先類。)

圖 5 顯示了 ThermometerProgressBar.xaml 文件中的一個更廣泛的 ControlTemplate 對象。此模板 具有 Resources 部分但沒有 Triggers 部分。盡管可視樹將一些明確坐標用於邊框和角,但未定義 ProgressBar 的總尺寸。這一職責落到了任何使用如下模板定義 ProgressBar 的標記的身上,例如:

<ProgressBar
    Template="{StaticResource
    templateThermometer}"
    Orientation="Vertical" Minimum="0"
    Maximum="100"
    Width="50" Height="350" ...

Figure 5 溫度計進度條的 ControlTemplate

<ControlTemplate x:Key="templateThermometer"
         TargetType="{x:Type ProgressBar}">
  <!-- Define two brushes for the thermometer liquid -->
  <ControlTemplate.Resources>
    <LinearGradientBrush x:Key="brushStem"
               StartPoint="0 0" EndPoint="1 0">
      <GradientStop Offset="0" Color="Red" />
      <GradientStop Offset="0.3" Color="Pink" />
      <GradientStop Offset="1" Color="Red" />
    </LinearGradientBrush>
    <RadialGradientBrush x:Key="brushBowl"
               GradientOrigin="0.3 0.3">
      <GradientStop Offset="0" Color="Pink" />
      <GradientStop Offset="1" Color="Red" />            
    </RadialGradientBrush>
  </ControlTemplate.Resources>
  <!-- Two-row Grid divides thermometer into stem and bowl -->
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="*" />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <!-- Second grid divides stem area in three columns -->
    <Grid Grid.Row="0">
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="25*" />
        <ColumnDefinition Width="50*" />
        <ColumnDefinition Width="25*" />
      </Grid.ColumnDefinitions>
      <!-- This border displays the stem -->
      <Border Grid.Column="1" BorderBrush="SteelBlue"
          BorderThickness="3 3 3 0"
          CornerRadius="6 6 0 0" >
        <!-- Track and Indicator elements -->
        <Decorator Name="PART_Track">
          <Border Name="PART_Indicator"
              CornerRadius="6 6 0 0"
              VerticalAlignment="Bottom"
              Background="{StaticResource brushStem}" />
        </Decorator>
      </Border>
    </Grid>
    <!-- The bowl outline goes in the main Grid second row -->
    <Ellipse Grid.Row="1"
         Width="{TemplateBinding Width}"
         Height="{TemplateBinding Width}"
         Stroke="SteelBlue" StrokeThickness="3" />
    <!-- Another grid goes in the same cell -->
    <Grid Grid.Row="1" >
      <Grid.RowDefinitions>
        <RowDefinition Height="50*" />
        <RowDefinition Height="50*" />
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="25*" />
        <ColumnDefinition Width="50*" />
        <ColumnDefinition Width="25*" />
      </Grid.ColumnDefinitions>
      <!-- This is to close up the gap between bowl and stem -->
      <Border Grid.Row="0" Grid.Column="1"
          BorderBrush="SteelBlue"
          BorderThickness="3 0 3 0"
          Background="{StaticResource brushStem}" />
    </Grid>
    <!-- Another ellipse to fill up the bowl -->
    <Ellipse Grid.Row="1"
         Width="{TemplateBinding Width}"
         Height="{TemplateBinding Width}"
         Stroke="Transparent" StrokeThickness="6"
         Fill="{StaticResource brushBowl}" />
  </Grid>
</ControlTemplate>

得到的 ProgressBar 如圖 6 所示。請注意,此 ProgressBar 的 Orientation(方向)屬性必須設置 為 Vertical(豎直),否則將無法正常運行。您可以記住在使用此模板時再設置 Orientation 屬性,也 可以為 ProgressBar 定義一個既能設置 Orientation 又能引用模版的樣式。(稍後即向您介紹此方法的 示例。)

圖 6

本專欄的可下載代碼中還包括 SpeedometerProgressBar.xaml,用於產生更多如圖 7 所示的基本 ProgressBar。

圖 7速度計進度條

這需要一點技巧。此模板包括兩個不可見矩形,它們沒有高度、填充顏色或筆畫顏色,如下所示:

 <Rectangle Name="PART_Track" Width="180" />
 <Rectangle Name="PART_Indicator" />

PART_Track 元素的寬度設置為半圓的度數。紅色指針的 Polygon 元素取決於 RotateTransform, RotateTransform 的 Angle 屬性與 PART_Indicator 的 ActualWidth 綁定在一起,例如:

<RotateTransform
  Angle="{Binding ElementName=PART_Indicator,
         Path=ActualWidth}" />

剖析 ScrollBar

ProgressBar 和 ScrollBar 均派生自抽象的 RangeBase 類。RangeBase 的全部內容包括:Value 屬 性、ValueChanged 事件以及 Minimum、Maximum、SmallChange 和 LargeChange 的定義。ProgressBar 只采用了 Minimum、Maximum 和 Value 屬性。

ScrollBar 比 ProgressBar 更復雜,它以多種方式響應用戶輸入。此行為分布在標准 ScrollBar 的 五個子控件中。可移動的縮略圖是名為 Thumb 的控件。直接位於縮略圖兩側的是兩個通常用於執行 Page Up 和 Page Down 命令的 RepeatButton 控件。(RepeatButton 類似於常規的 Button,不同的是,它以 多次 Click 事件響應持續的鼠標按壓。)在 ScrollBar 的 Value 屬性更改時,這兩個 RepeatButton 控件的大小也會發生更改,有時,其中的一個會縮小為零。在 ScrollBar 的兩個端點還有兩個標有箭頭 、大小固定的 RepeatButton 控件,通常用於執行 Line Up 和 Line Down 命令。

Thumb 和中間的 RepeatButton 控件是 Track 元素的子項,Track 元素負責它們的交互、移動和大小 更改。ScrollBar 的大部分核心功能和復雜的邏輯都由 Track 處理。

在為 ScrollBar 定義 ControlTemplate 時,可視樹需要包含一個名為 PART_Track 的 Track 元素。 這是 ScrollBar 模板唯一需要命名的部分。

Track 不是控件,而是從 FrameworkElement 派生而來,並且由於 Template 屬性是由 Control 定義 的,因此 Track 沒有 Template 屬性。盡管不能為 Track 提供模板,但可以為組成 Track 元素的三個 控件提供模板。

Track 定義了與它的三個子項相對應的三個屬性:Thumb(類型為 Thumb)、DecreaseRepeatButton( 類型為 RepeatButton)和 IncreaseRepeatButton(類型也為 RepeatButton)。默認情況下,這三個屬 性為 null,這意味著模板的可視樹應包含這三個控件的明確定義。如果您希望在 ScrollBar 的兩端提供 兩個按鈕,可視樹應包括用於這兩個按鈕的元素。您可以為這五個子控件指定它們自己的模板,或者使用 現有模板而僅分配某些屬性。

按鈕命令

標准 ScrollBar 模板中的四個 RepeatButton 控件必須能夠與 ScrollBar 的內部邏輯進行通信。通 過使用被 ScrollBar 類定義為靜態只讀字段的預定義 RoutedCommand 對象可實現通信。ScrollBar 定義 了至少 17 個這樣的靜態只讀字段,這些字段對 ScrollBar 可以執行的各種操作做出響應。其中的某些 命令與 ScrollBar 上下文菜單一起使用;某些命令用於被用作 ScrollViewer 控件的一部分的 ScrollBar。還有一些命令,特別是以單詞 Line 和 Page 開頭的命令,用於模板。

圖 8 顯示了 NoFrillsScrollBar.xaml 文件中的 ScrollBar 模板。 Track 元素和位於端點處的兩個 RepeatButton 控件可在 Grid 面板中進行組織。Track 元素的三個屬性設置為兩個其他 RepeatButton 控件和一個 Thumb。

Figure 8 一個簡單的 ScrollBar 模板

<ControlTemplate x:Key="templateNoFrillsScroll"
         TargetType="{x:Type ScrollBar}">
  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto" />
      <ColumnDefinition Width="1*" />
      <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>
    <RepeatButton Grid.Column="0"
           Command="ScrollBar.LineLeftCommand"
           FontFamily="Wingdings" Content="E" />
    <Track Grid.Column="1" Name="PART_Track">
      <Track.DecreaseRepeatButton>
        <RepeatButton Command="ScrollBar.PageLeftCommand" />
      </Track.DecreaseRepeatButton>
      <Track.IncreaseRepeatButton>
        <RepeatButton Command="ScrollBar.PageRightCommand" />
      </Track.IncreaseRepeatButton>
      <Track.Thumb>
        <Thumb />
      </Track.Thumb>
    </Track>
    <RepeatButton Grid.Column="2"
           Command="ScrollBar.LineRightCommand"
           FontFamily="Wingdings" Content="F" />
  </Grid>
</ControlTemplate>

這是全功能的 ScrollBar,但是正如您在圖 9 所見的那樣,這四個 RepeatButton 控件看起來像常規 按鈕。兩端的兩個按鈕原來很小,所以我增大了它們的尺寸,方法是將它們的內容設置為 Wingdings 字 體中的指示方向的手形字符。此外,五個控件的所有屬性中,除了 Command 屬性和 Grid.Column 附加屬 性之外,其他屬性均未設置。

圖 9 基於圖 8 的 ScrollBar

圖 8 中的模板用於水平 ScrollBar。豎直方向將需要網格中的三個行而不是三個列,並將引用包括字 Up 和 Down 而非 Left 和 Right 的 ScrollBar 類中的 RoutedCommand 字段。

默認的 ScrollBar 模板使用 Microsoft.Windows.Themes 命名空間的 ScrollChrome 類來顯示它的某 些控件。您還會注意到,模板看起來好像比需要的長度要長出許多,這是因為可視樹中控件的很多屬性是 通過樣式而非特性定義的。例如,在可以使用如下代碼的地方:

<RepeatButton IsFocusable="False" ...

您會看到以下代碼:

<RepeatButton ... >
  <RepeatButton.Style>
    <Style TargetType="RepeatButton">
      ...
      <Setter Property="UIElement.IsFocusable">
        <Setter.Value>
          <s:Boolean>False</s:Boolean>
        </Setter.Value>
      </Setter>
      ...

我想默認的模板是包含此類標記的,因為這些屬性最初是在模板的 Resources 部分的 Style 元素中 定義的。但是,在編寫自己的模板時,您可以直接在元素中設置屬性,知道這一點很重要。

默認模板將每個 RepeatButton 的 IsFocusable 和 IsTabStop 屬性設置為 false,在嘗試使用 NoFrillsScrollBar.xaml 時,您將明白為什麼。單擊最左側的按鈕。現在按幾次 Tab 鍵。您將發現輸入 焦點從 RepeatButton 轉移到了 RepeatButton,這當然是不應該發生的。

由於模板中有四個 RepeatButton 控件,最簡單的方法可能是使用一個 Style 元素為它們設置統一屬 性。您可以將以下標記添加到 NoFrillsScrollBar.xaml 中緊跟 ControlTemplate 開始標記的位置,以 解決焦點問題:

<ControlTemplate.Resources>
  <Style TargetType="{x:Type RepeatButton}">
    <Setter Property="Focusable" Value="False" />
    <Setter Property="IsTabStop" Value="False" />
  </Style>
</ControlTemplate.Resources>

您還可以使用 ControlTemplate 的 Resources 部分,來為組成 ScrollBar 可視樹的子控件定義屬性 。我在 SpringLoadedScrollBar.xaml 文件中進行了此操作。圖 10 顯示了該模板的可視樹部分。

Figure 10 對其他模板進行引用的可視樹

<Grid>
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="50" />
    <ColumnDefinition Width="1*" />
    <ColumnDefinition Width="50" />
  </Grid.ColumnDefinitions>
  <!-- Line-left button on left side -->
  <RepeatButton Grid.Column="0"
         Command="ScrollBar.LineLeftCommand"
         Foreground="{TemplateBinding Foreground}"
         Template="{StaticResource templateArrow}" />
  <!-- Named track occupies most of the ScrollBar -->
  <Track Grid.Column="1" Name="PART_Track" >
    <Track.DecreaseRepeatButton>
      <RepeatButton Command="ScrollBar.PageLeftCommand"
             Foreground="{TemplateBinding Foreground}"
             Template="{StaticResource templateSpring}" />
    </Track.DecreaseRepeatButton>
    <Track.IncreaseRepeatButton>
      <RepeatButton Command="ScrollBar.PageRightCommand"
             Foreground="{TemplateBinding Foreground}"
             Template="{StaticResource templateSpring}" />
    </Track.IncreaseRepeatButton>
    <Track.Thumb>
      <Thumb Background="{TemplateBinding Foreground}" />
    </Track.Thumb>
  </Track>
  <!-- Line-right button on right side -->
  <RepeatButton Grid.Column="2"
         Command="ScrollBar.LineRightCommand"
         Foreground="{TemplateBinding Foreground}"
         Template="{StaticResource templateArrow}"
         LayoutTransform="-1 0 0 1 0 0" />
</Grid>

請注意,模板中的四個 RepeatButton 元素將 Foreground 屬性設置為 ScrollBar 本身的 Foreground 屬性,並設置之前在 Resources 部分定義的 Template。其他模板(圖 10 中未顯示)使用 RepeatButton 的 Foreground 屬性為其可視化元素著色。圖 11 顯示了兩個使用此模板但具有不同 Foreground 設置的 ScrollBar 控件。

圖 11 使用不同的 Foreground 設置 (單擊該圖像獲得較大視圖)我將 DropShadowBitmapEffect 元素 添加到了這四個按鈕。實際上,我非常喜歡這個效果,因此決定添加一個 Triggers 部分,以便在鼠標經 過按鈕時出現一個額外的陰影。

假設這個像是安裝了彈簧的 ScrollBar 的模板為水平方向,當高度為與設備無關的 50 個單位時,效 果最佳。這些設置並不是模板自身的一部分,模板中實在沒有它們的位置。您可能會考慮將它們放在 Style 定義中,我稍後會對這種做法進行說明。

ScrollBar 中 Thumb 的大小取決於 ScrollBar ViewportSize 屬性,通常用於顯示當前查看的文檔的 比例。我沒有找到不通過設置 ViewportSize 而更改 Thumb 大小的方法。如果需要較大的 Thumb,您應 為 Slider 編寫模板,實際上您可以編寫模板,使 Slider 看起來更像 ScrollBar。

創建三維滑塊

與 ScrollBar 不同,默認的 Slider 控件的兩端並沒有兩個小的更改按鈕。但是您可以將此類控件添 加至 Slider 模板。Slider 將六個 RoutedCommand 對象定義為只能獲取的靜態屬性,包括 DecreaseSmall、IncreaseSmall、DecreaseLarge 和 IncreaseLarge。Slider 也可以在一側或兩側顯示 刻度線(可選)。

查看默認 Slider 模板時首先需會注意到的可能是,模板長度超過一千行,這是默認 ScrollBar 模板 長度的三倍還多。部分原因是由於 Slider 模板並不依賴於 Microsoft.Windows.Themes 中的類。而是在 模板內部生成所有對象。

此模板定義了不同的縮略圖形狀,以用於刻度線只顯示在 Slider 的一側時的情況,此模板包含很多 用於為此縮略圖著色的漸變畫筆。如果要將可選的 TickBar 元素包括在 Slider 模板中,應將其 Visibility 屬性設置為 Visibility.Collapsed。ControlTemplate 的 Triggers 部分應包含根據 Slider 的 TickPlacement 屬性將 Visibility 屬性設置為 Visibility.Visible 的 Trigger 元素。因 此,有必要為 TickBar 元素指定名稱,但不必是特殊名稱。

對於我自定義的豎直 Slider,我希望 Thumb 像混音板上的滑桿。目標是使滑桿像一個具有三維外觀 的突出的塑料塊。要模擬三維透視,則需要在上下移動時改變形狀。推至 Slider 頂部時,您應可以看到 操縱桿的底部;而在位於 Slider 底部時,應可以看到其頂部。

Windows Presentation Foundation 具有某些三維圖形功能,是處理此類任務的理想選擇。 Slider3D.xaml 文件包含該模板。圖 12 顯示了其中五個設置在不同位置的三維 Slider 控件。請注意, 滑桿的形狀變化取決於其位置。

圖 12 三維 Slider 控件 (單擊該圖像獲得較大視圖)豎直 Slider 開始處通常是一個三列的 Grid 面 板;外側的兩個列用於兩個 TickBar 元素。在默認的豎直 Slider 模板中,中間的 Grid 包含一個提供 凹陷圖像的 Border 元素以及一個包含 Thumb 和兩個 RepeatButton 控件的 Track 元素。我在自己的模 板中使用同樣的方法,並將兩個 RepeatButton 控件定義為具有 Transparent(透明)背景的 Border 元 素。它們不會使凹陷變暗,但仍會響應鼠標。

我確定 Thumb 應為 50 個像素高(由於 Grid 中的 ColumnDefinition,其寬度已為 50 個像素)。 可視樹包含另一個具有透明背景的 Border。此時,盡管您無法看到縮略圖,但 Slider 運行完全正常。 此透明 Border 的子項是一個 Viewport3D 元素,這正是其有趣之處。

我認為,三維圖形編程基本上包含兩個部分:單調枯燥和輕松自在。單調枯燥的工作主要涉及定義 MeshGeometry3D 元素,這些元素是在三維坐標空間中完整定義為一組互相連接的三角形的可視對象。對 於 Thumb,我定義了一個對象,是切掉頂部的矩形稜錐:

<MeshGeometry3D
Positions="-2 -1 0, -1 -0.25 4, -2 1 0, -1 0.25 4,
2 -1 0, 1 -0.25 4, 2 1 0, 1 0.25 4"
TriangleIndices="0 1 2, 1 3 2, 0 2 4, 2 6 4,
0 4 1, 1 4 5, 1 5 7, 1 7 3,
4 6 5, 7 5 6, 2 3 6, 3 7 6"
TextureCoordinates="0 1, 0.2 0.6, 0 0, 0.2 0.4,
1 1, 0.8 0.6, 1 0, 0.8 0.4" />

八個頂點中每個頂點的 Positions 屬性都由各自的三維坐標組成。圖形底部(Z 等於零)的跨度為從 X 值 -2 到 +2 和從 Y 值 -1 到 +1。頂部矩形(Z 等於 4)的跨度為從 X 值 -1 到 +1 和從 Y 值 - 0.25 到 +0.25。

TriangleIndices 屬性中的每個三元組確定了圖的一個面,並對應儲存在 Positions 數組中的編號。 例如,第一個 TriangleIndices 的三元組為 0、1 和 2,是指 Positions 集合中的第一、第二和第三個 頂點。TextureCoordinates 集合包含圖中每個三維頂點的一個二維點。這些點對應於基於 GeometryDrawing(用於包括三維對象外側)的畫筆中的點:

<GeometryDrawing Brush="LightGray"
Geometry="F 1 M 0 0 L 1 0 L 1 1 L 0 1 Z
M 0.2 0.4
L 0.8 0.4 0.8 0.6 0.2 0.6 Z
M 0 0 L 0.2 0.4
M 1 0 L 0.8 0.4
M 1 1 L 0.8 0.6
M 0 1 L 0.2 0.6">
<GeometryDrawing.Pen>
<Pen Brush="DarkGray"
Thickness=".05" />
</GeometryDrawing.Pen>
</GeometryDrawing>

此畫筆只是用淡灰色覆蓋整個對象,並以深灰色使對象的邊緣顏色加重。

我所說的三維圖形編程中輕松自在的部分是指使用照明和照相機。通常,使用照明都希望獲得 DirectionalLight 和 AmbientLight 結合的效果。前者本身太生硬;而後者會沖淡所有對象。模板中的 照相機最初設置是在滑桿從 Z 軸向下看,但也可以進行旋轉變換從上面和下面查看滑桿。我無法通過對 Slider 的 Value 屬性執行 TemplateBinding 而達到上述目的,因此改用常規綁定:

<AxisAngleRotation3D Axis="1 0 0"
Angle="{Binding RelativeSource={RelativeSource
AncestorType={x:Type Slider}}, Path=Value}" />

由於 Value 屬性決定了照相機的旋轉角度,我需要分別硬性地將 Minimum 和 Maximum 屬性設置為 -25 和 25。這使得照相機的最大擺動角度為 50 度,如果更大,滑桿將卡住。小於 50 度也可以,但是 三維效果會變弱。Slider3D.xaml 文件使用 Style 設置這兩個屬性以及 Orientation 屬性和 Template 屬性,如下所示:

<Style x:Key="styleSlider3D"
TargetType="Slider">
<Setter Property="Orientation" Value="Vertical" />
<Setter Property="Minimum" Value="-25" />
<Setter Property="Maximum" Value="25" />
<Setter Property="Template"
Value="{StaticResource templateSlider3D}" />
</Style>

使用此模板的 Slider 實際上引用的是此樣式而非此模板。

記住要適度

正如您所見,只需要對 XAML 進行少許編碼就可以極大地改變標准控件的外觀。如果您像我認識的多 數編程人員一樣,您可能會琢磨著編寫打破所有邏輯和風格的模板。

在您對模板著迷之前,請記住 Windows 的重要價值之一就是用戶界面的一致性。如果界面看起來相似,則用戶便能猜測到下一步的操作 。因此,要完全控制好您的創新欲望並適度使用模板。

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

本文配套源碼:http://www.bianceng.net/dotnet/201212/750.htm

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