首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

WPF 基礎:使用 ControlTemplate 徹底客製化控制項外觀

     編輯:關於.NET

隨著 Windows Vista 與 .NET Framework 3.0 釋出,大量新技術湧現。WPF 開發團隊深知客製化控制項的痛苦:傳統 Windows 需四步驟——靈感、研究探索、遭遇困難、最終發現需完全重寫。因難以存取將控制項視覺部分與功能關聯的代碼,通常無法客製化,只能完全接受或完全替換。

WPF 提出嶄新強大解決方案——模板。WPF 模板不僅簡單且功能強大。每個具視覺外觀的預定義控制項皆擁有完整定義其外觀的模板,類型為 ControlTemplate(設定為 Control 類別定義的 Template 屬性)。使用 WPF 控制項時,可用自行設計模板替換預設模板,保留控制項核心功能(含鍵盤/滑鼠處理),卻能賦予完全不同外觀。這就是所謂「換臉」。控制項有預設外觀,但此外觀非固定對應內部功能。

用程式碼撰寫模板不合適,用 XAML 更容易,因模板可完整用 XAML 表示,得以輔助視覺化設計工具設計。若想編寫功能如現有 WPF 控制項但外觀不同的自訂控制項,請停下!很可能僅用一個模板即可達成。

傾印預設模板:最佳學習素材

每個具視覺外觀的預定義控制項都有預設模板。有興趣撰寫自訂模板者,請研究這些預設值。我的書中 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 衍生類別實例且已呈現於螢幕(否則 Template 為 null)。strbuild.ToString() 提供完整模板 XAML 文件。產生的 XAML 可能比手寫冗長,如 <Trigger Property="IsEnabled" Value="False"> 會產生完整屬性元素語法。

視覺樹:模板的骨架

先看簡單實用的 CheckBox 模板。想要比標準 CheckBox 更生動的外觀?如用大綠勾或大紅叉顯示選項,覆蓋 CheckBox 全部內容。BigCheckCheckBox.xaml 展示此模板。

XAML 中模板為 ControlTemplate 元素。小型程式通常在根元素 Resources 定義;大型應用或共用模板時置於獨立 ResourceDictionary XAML 檔。ControlTemplate 通常含三部分:

  1. Resources(可選):定義模板使用的樣式或筆刷。
  2. 視覺樹:以元素或控制項版面形式描述所需外觀。
  3. Triggers:指明響應控制項屬性變更時,視覺樹元素如何變化。

視覺樹範例

<ControlTemplate x:Key="templateBigCheck" TargetType="{x:Type CheckBox}">
  <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}">
    <Grid>
      <!-- ContentPresenter 顯示 CheckBox 內容 -->
      <ContentPresenter Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}"
          Margin="{TemplateBinding Padding}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
          VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
      <!-- 此 Border 顯示半透明紅叉 -->
      <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>
</ControlTemplate>

圖 3:使用自訂模板的 CheckBox 控制項

頂層元素為 Border(可作單一子元素父元素),三屬性用 TemplateBinding 指定。TemplateBinding 將視覺樹元素屬性繫結至控制項屬性。ContentPresenter 的 TemplateBinding 更有趣,可格式化 Button 等 ContentControl 衍生控制項內容,允許在 Button/CheckBox 顯示幾乎任何內容。注意 Margin 繫結至 Padding:Margin 為元素外部額外空間,Padding 為控制項內容未佔用空間,此繫結隱含邏輯清晰。

ContentPresenter 與另一 Border 同置於單一儲存格 Grid 內。第二 Border 顯示於 ContentPresenter 頂層,背景含 50% 不透明筆刷(紅叉)。Path 元素指定 Name="path",供 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>

預設 IsChecked=false 顯示紅叉。第一個 Trigger:IsChecked 變 true 時,path 元素 Data 與 Stroke 設為綠勾。第二個 Trigger:IsChecked=null(三態操作時)顯示藍問號。第三個 Trigger:IsEnabled=false 時將 Foreground 改為灰色筆刷。另有一 Trigger:控制項獲得焦點時在內容四周顯示虛線。

引用模板:<CheckBox Template="{StaticResource templateBigCheck}" ...>

進階控制項模板:ProgressBar、ScrollBar、Slider

消除對模板應用於複雜控制項能力的疑慮。這三個控制項繼承自 RangeBase,具有可動部件與大量使用者互動。

必要命名元素:PART_Track 與 PART_Indicator

複雜控制項模板需包含特定名稱的特定類型元素。ProgressBar 視覺樹必須有兩個 FrameworkElement(或衍生類)元素,名為 PART_Track 與 PART_Indicator。這稱為模板的「已命名組件」,可在 SDK 文件 TemplatePart 屬性中找到。缺少這些元素控制項將無法正確運作。

水平 ProgressBar 時,內部邏輯將 PART_Indicator.Width 設為 PART_Track.ActualWidth 分數(依 Minimum/Maximum/Value)。垂直時用 Height/ActualHeight。實際上水平/垂直對應兩個預設模板(ScrollBar/Slider 同)。若需支援雙向,應分別撰寫兩模板,在 Style 的 Triggers 中選擇。

極簡 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>

StackPanel 垂直堆疊兩矩形。PART_Track 紅色固定寬 300,PART_Indicator 藍色寬度隨值變化。實際應避免明確尺寸。

溫度計 ProgressBar:豐富視覺樹與 Resources

<ControlTemplate x:Key="templateThermometer" TargetType="{x:Type ProgressBar}">
  <ControlTemplate.Resources>
    <LinearGradientBrush x:Key="brushStem" StartPoint="0 0" EndPoint="1 0">...</LinearGradientBrush>
    <RadialGradientBrush x:Key="brushBowl" GradientOrigin="0.3 0.3">...</RadialGradientBrush>
  </ControlTemplate.Resources>
  <Grid>
    <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions>
    <Grid Grid.Row="0"> ... <Border> <Decorator Name="PART_Track"> <Border Name="PART_Indicator" .../> </Decorator> </Border> ... </Grid>
    <Ellipse Grid.Row="1" Width="{TemplateBinding Width}" Height="{TemplateBinding Width}" Stroke="SteelBlue" StrokeThickness="3" />
    <Grid Grid.Row="1"> ... <Ellipse ... Fill="{StaticResource brushBowl}"/> </Grid>
  </Grid>
</ControlTemplate>

圖 6:溫度計 ProgressBar

使用此模板的 ProgressBar 必須設 Orientation="Vertical"、明確 Width/Height。可定義 Style 同時設定這些屬性與模板。SpeedometerProgressBar.xaml 產生速度計外觀,利用兩不可見矩形(PART_Track 寬度設為半圓度數,PART_Indicator 無高度/填充/筆畫),紅色指針 Polygon 透過 RotateTransform 綁定 PART_Indicator.ActualWidth 旋轉角度。

剖析 ScrollBar:Track 元素與五個子控制項

ScrollBar 比 ProgressBar 複雜,響應使用者輸入行為分布於五個子控制項:

  • 可移動縮圖:Thumb 控制項
  • 縮圖兩側:兩個 RepeatButton(Page Up/Down),持續滑鼠按壓觸發多次 Click。大小隨 Value 變化,有時縮為零。
  • 兩端點:兩個固定大小 RepeatButton(Line Up/Down),標示箭頭。

Thumb 與中間 RepeatButton 為 Track 元素子項,Track 負責其互動、移動、大小變更。ScrollBar 核心功能與複雜邏輯由 Track 處理。

定義 ScrollBar ControlTemplate 時,視覺樹需含名為 PART_Track 的 Track 元素——模板唯一需命名部分。Track 非控制項(衍生自 FrameworkElement),無 Template 屬性,但可為組成它的三個控制項提供模板。

Track 定義三屬性對應三子項:Thumb、DecreaseRepeatButton、IncreaseRepeatButton。預設為 null,視覺樹需明確定義三控制項。若需兩端按鈕,視覺樹應含對應元素。可為五子控制項指定各自模板或僅指派屬性。

按鈕命令:RoutedCommand 連接邏輯

標準 ScrollBar 模板中四個 RepeatButton 需與內部邏輯通訊。透過 ScrollBar 類別定義的靜態唯讀欄位 RoutedCommand 物件實現。ScrollBar 定義至少 17 個此類欄位。部分用於內容選單、ScrollViewer 一部分的 ScrollBar。以 Line、Page 開頭的命令用於模板。

無花俏 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>

圖 9:基於圖 8 的 ScrollBar

全功能 ScrollBar,但四 RepeatButton 外觀如普通按鈕。兩端按鈕原很小,用 Wingdings 手勢字元放大。除 Command 與 Grid.Column 外未設其他屬性。垂直方向需三列而非三行,引用 Up/Down 命令。

預設模板用 Microsoft.Windows.Themes.ScrollChrome 顯示控制項,屬性多在 Style 中定義而非特性,導致標記冗長。撰寫自訂模板時可直接在元素設定屬性。

預設模板將每個 RepeatButton 的 IsFocusable/IsTabStop 設 false。單擊最左按鈕後按 Tab,焦點會在按鈕間移動(不應發生)。解決:在 ControlTemplate.Resources 定義 Style 統一設定:

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

彈簧式 ScrollBar:引用外部模板與 Foreground 繫結

<Grid>
  <RepeatButton Grid.Column="0" Command="ScrollBar.LineLeftCommand" Foreground="{TemplateBinding Foreground}" Template="{StaticResource templateArrow}" />
  <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>
  <RepeatButton Grid.Column="2" Command="ScrollBar.LineRightCommand" Foreground="{TemplateBinding Foreground}" Template="{StaticResource templateArrow}" LayoutTransform="-1 0 0 1 0 0" />
</Grid>

圖 11:使用不同 Foreground 設定的 ScrollBar

四 RepeatButton 設 Foreground 繫結至 ScrollBar 自身 Foreground,並套用 Resources 定義模板。templateArrow、templateSpring 使用 RepeatButton.Foreground 為視覺元素著色。加入 DropShadowBitmapEffect 於四按鈕,並在 Triggers 中滑鼠經過時增加陰影。此模板水平最佳高度 50 裝置無關單位,建議放入 Style 定義。Thumb 大小取決 ViewportSize,無法不設 ViewportSize 改變大小,需較大 Thumb 時應撰寫 Slider 模板。

打造 3D Slider:Viewport3D 與幾何體

Slider 預設兩端無小按鈕,但可添加。定義 6 個 RoutedCommand(DecreaseSmall 等)。可選擇顯示刻度線(TickBar)。預設模板超過 1000 行(ScrollBar 3 倍),因不依賴 Microsoft.Windows.Themes,在模板內部生成所有物件。

自訂垂直 Slider 目標:Thumb 如混音台滑桿,具 3D 立體外觀。移動時形狀變化:推至頂部見底部,在底部見頂部。WPF 3D 功能最適合此任務。

圖 12:3D Slider 控制項——滑桿形狀隨位置變化

垂直 Slider 視覺樹通常為三列 Grid,外側兩列為 TickBar。中列含提供凹陷圖像的 Border 與包含 Thumb、兩 RepeatButton 的 Track。自訂模板用同樣方法,兩 RepeatButton 定義為透明背景 Border,不使凹陷變暗但響應滑鼠。

Thumb 高 50 像素(Grid ColumnDefinition 寬度已 50)。視覺樹含另一透明背景 Border,子項為 Viewport3D——精彩所在。

3D 程式設計含兩部分:單調枯燥(定義 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" />

8 頂點 Positions 為三維座標。底部(Z=0)跨 X -2~+2、Y -1~+1。頂部矩形(Z=4)跨 X -1~+1、Y -0.25~+0.25。TriangleIndices 每三元組定義一面,對應 Positions 編號。TextureCoordinates 含每頂點一 2D 點,對應基於 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=-25、Maximum=25,最大擺動 50 度(更大滑桿會卡住)。Slider3D.xaml 用 Style 設定這些屬性與 Orientation、Template。使用此模板的 Slider 實際引用此 Style 而非直接引用模板。

適度原則:別讓創新破壞一致性

少許 XAML 編碼即可極大改變標準控制項外觀。但請記住 Windows 重要價值之一是使用者介面一致性。介面外觀相似,使用者才能預測下一步操作。請節制創新慾望,適度使用模板。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved