【WPF學習】第六十五章 建立無外觀控制元件
使用者控制元件的目標是提供增補控制元件模板的設計表面,提供一種定義控制元件的快速方法,代價是失去了將來的靈活性。如果喜歡使用者控制元件的功能,但需要修改使其視覺化外觀,使用這種方法就有問題了。例如,設想希望使用相同的顏色拾取器,但希望使用不同的“面板”,將其更好地融合到已有的應用程式視窗中。可以通過樣式來改變使用者控制元件的某些方面,但該控制元件的一些部分是在內部鎖定,並硬編碼到標記中。例如,無法將預覽矩形移動到滑動條的左邊。
解決方法是建立無外觀控制元件——繼承自控制元件基類,但沒有設計表面的控制元件。相反,這個控制元件將其標記放到預設模板中,可替換預設模板而不會影響控制元件邏輯。
一、修改顏色拾取器的程式碼
將顏色拾取器改成無外觀控制元件並不難。第一步很容易——只需要改變類的宣告,如下所示:
public class ColorPicker:System.Windows.Controls.Control { }
在這個示例中,ColorPicker類繼承自Control類。繼承自FrameworkElement類是不合適的,因為顏色拾取器允許與使用者進行互動,而且其他高階的類不能準確地描述顏色拾取器的行為。例如,顏色拾取器不允許在內部巢狀其他內容,所以繼承自ContentControl類也是不合適的。
ColorPicker類中的程式碼與用於使用者控制元件的程式碼是相同的(除了必須刪除建構函式中的InitializeComponent()方法呼叫)。可使用相同的方法定義依賴項屬性和路由事件。唯一的區別是需要通知WPF,將為控制元件類提供新樣式。該樣式將提供新的控制元件模板(如果不執行該步驟,將繼續使用在基類中定義的模板)。
為通知WPF正在提供新的樣式,需要在子彈女工藝控制元件類的靜態建構函式中呼叫OverrideMetadata()方法。需要在DefaultStyleKeyProperty屬性上呼叫該方法,該屬性是為自定義控制元件定義預設樣式的依賴性屬性。需要的程式碼如下所示:
DefaultStyleKeyProperty.OverrideMetadata(typeof(ColorPicker), new FrameworkPropertyMetadata(typeof(ColorPicker)));
如果希望使用其他控制元件類的模板,可提供不同的型別,但幾乎總是為每個自定義控制元件建立特定的樣式。
二、修改顏色拾取器的標記
新增對OverrideMetadata()方法的呼叫後,只需要插入正確的樣式。需要將樣式放在名為generic.xaml的資源字典中,該資源字典必須放在專案資料夾的Themes子資料夾中。這樣,該樣式就會被識別為自定義控制元件的預設樣式。下面列出新增generic.xaml檔案的具體步驟:
(1)在Solution Explorer中右鍵類庫專案,並選擇Add|New Folder選單項。
(2)將新建資料夾命名為Themes。
(3)右擊Themes資料夾,並選擇Add|New Item選單項。
(4)在Add New Item對話方塊中選擇資源字典,輸入名稱generic.xaml,並單擊Add按鈕。
下圖顯示了Themes資料夾中的generic.xaml檔案。
通常,自定義控制元件庫會包含幾個控制元件。為了保持它們的樣式相互獨立以便編輯,generic.xaml檔案通常使用資源字典合併功能。下面是標記顯示了generic.xaml檔案,該檔案從ColorPicker.xaml資源字典中提取資源,該資源字典位於CustomControls控制元件庫的Themes資料夾中:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="/CustomControls;component/Themes/ColorPicker.xaml"> </ResourceDictionary> </ResourceDictionary.MergedDictionaries> </ResourceDictionary>
自定義的控制元件樣式必須使用TargetType特性來將自身自動關聯到顏色拾取器。下面是ColorPicker.xaml檔案中標記的基本結構:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:CustomControls"> <Style TargetType="{x:Type local:ColorPicker}"> ... </Style> </ResourceDictionary>
可使用樣式設定控制元件類中的任意屬性(無論是繼承自基類的屬性還是新增屬性)。但在此,樣式最有用的任務是應用新目標,新目標定義了控制元件的預設視覺化外觀。
很容易就能將普通標記(如顏色拾取器使用的標記)轉換到控制元件目標中。但要注意以下幾點:
- 當建立連結到父控制元件類屬性的繫結表示式時,不能使用ElementName屬性。而需要使用RelativeSource屬性指示希望繫結到父控制元件。如果單向繫結完全能夠滿足需要,通常可以使用輕量級的TemplateBinding標記表示式,而不需要使用功能完備的資料繫結。
- 不能在控制元件模板中關聯事件處理程式。相反,需要為元素提供能夠識別的名稱,並在控制元件建構函式中通過程式碼為他們關聯處理程式。
- 除非希望關聯事件處理程式或通過程式碼與它進行互動,否則不要在控制元件模板中命名元素。當命名希望使用的元素時,使用“PART_元素名”的形式進行命名。
遵循上面幾點,可為顏色拾取器建立以下模板:
<Style TargetType="{x:Type local:ColorPicker}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:ColorPicker}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Slider Minimum="0" Maximum="255" Margin="{TemplateBinding Padding}" Value="{Binding Path=Red, RelativeSource={RelativeSource TemplatedParent}}"/> <Slider Grid.Row="1" Minimum="0" Maximum="255" Margin="{TemplateBinding Padding}" Value="{Binding Path=Green, RelativeSource={RelativeSource TemplatedParent}}"/> <Slider Grid.Row="2" Minimum="0" Maximum="255" Margin="{TemplateBinding Padding}" Value="{Binding Path=Blue, RelativeSource={RelativeSource TemplatedParent}}"/> <Rectangle Grid.Column="1" Grid.RowSpan="3" Margin="{TemplateBinding Padding}" Width="50" Stroke="Black" StrokeThickness="1"> <Rectangle.Fill> <SolidColorBrush Color="{Binding Path=Color,RelativeSource={RelativeSource TemplatedParent}}"></SolidColorBrush> </Rectangle.Fill> </Rectangle> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style>
正如上面看到的,本例已用TemplateBinding擴充套件提到一些繫結表示式。其他一些繫結表示式仍使用Binding擴充套件,但將RelativeSource設定為指向模板的父元素(自定義控制元件)。儘管TemplateBinding和將RelativeSource屬性設定為TemplatedParent值得Binding的作用相同——從自定義控制元件的屬性中提取資料——但是使用量級更輕的TemplateBinding總是合適的。如果需要雙向繫結(與滑動條一樣)或繫結到繼承自Freezable的類(如SolidColorBrush類)的屬性,TemplateBinding就不能工作了。
三、精簡控制元件模板
通過上面設計,顏色拾取器控制元件模板填充了需要的全部內容,可按與使用顏色拾取器相同的方式來使用。然而,仍可通過移除一些細節來簡化模板。
現在,所有希望提供自定義模板的控制元件使用這必須新增大量的繫結表示式,已確保控制元件能夠繼續工作。這並不難,但是很繁瑣。另一種選擇是,在控制元件自身的初始化程式碼中配置所有繫結表示式。這樣,模板就不需要指定這些細節了。
1、新增部件名稱
為了讓這一系統能夠工作,程式碼要能找到所需的元素。WPF控制元件通過名稱定為它們需要的元素。所以,元素的名稱變成自定義控制元件公有介面的一部分,而且需要恰當的描述性名稱。根據約定,這些名稱以PART_開頭,後跟元素名稱。元素名稱的首字母要大寫,就像數學名稱。對於需要的元素名稱,PART_RedSlider是合適的選擇,而PART_sldRed、PART_redSlider以及RedSlider等名稱都不合適。
例如,下面的標記演示瞭如何通過刪除三個滾動條的Value數學的繫結表示式,併為三個滑動條新增PART_名稱,從而為通過程式碼設定繫結做好準備。
<Slider Name="PART_RedSlider" Minimum="0" Maximum="255" Margin="{TemplateBinding Padding}" /> <Slider Name="PART_GreemSlider" Grid.Row="1" Minimum="0" Maximum="255" Margin="{TemplateBinding Padding}" /> <Slider Name="PART_BlueSlider" Grid.Row="2" Minimum="0" Maximum="255" Margin="{TemplateBinding Padding}" />
注意,Margin數學仍使用繫結表示式新增內邊距,但這是一個可選的細節,可以很容易地從自定義模板中去掉該細節(可選擇硬編碼內邊距或者使用不同的佈局),
為確保獲得更大的靈活性,這是沒有為Rectangle元素提供名稱,而是為其內部的SolidColorBrush指定了名稱。這樣,可根據模板為顏色預覽功能使用任何形狀或任意元素。
<Rectangle Grid.Column="1" Grid.RowSpan="3" Margin="{TemplateBinding Padding}" Width="50" Stroke="Black" StrokeThickness="1"> <Rectangle.Fill> <SolidColorBrush x:Name="PART_PreviewBrush"></SolidColorBrush> </Rectangle.Fill> </Rectangle>
2、操作模板部件
在初始化控制元件後,可連線繫結表示式,但有一種更好的方法。WPF有一個專用的OnApplyTemplate()方法,如果需要在模板中查詢元素並關聯事件處理程式或新增資料繫結表示式,應重寫該方法。在該方法中,可以通過GetTemplateChild()方法查詢所需的元素。
如果沒有找到希望處理的元素,推薦的模式就不起作用。也可新增程式碼來檢索該元素,如果元素存在,在檢查型別是否正確;如果型別不正確,就引發異常。
下面的程式碼演示了OnApplyTemplate()方法使用:
public override void OnApplyTemplate() { base.OnApplyTemplate(); RangeBase slider = GetTemplateChild("PART_RedSlider") as RangeBase; if (slider != null) { Binding binding = new Binding("Red"); binding.Source = this; binding.Mode = BindingMode.TwoWay; slider.SetBinding(RangeBase.ValueProperty, binding); } slider = GetTemplateChild("PART_GreenSlider") as RangeBase; if (slider != null) { Binding binding = new Binding("Green"); binding.Source = this; binding.Mode = BindingMode.TwoWay; slider.SetBinding(RangeBase.ValueProperty, binding); } slider = GetTemplateChild("PART_BlueSlider") as RangeBase; if (slider != null) { Binding binding = new Binding("Blue"); binding.Source = this; binding.Mode = BindingMode.TwoWay; slider.SetBinding(RangeBase.ValueProperty, binding); } SolidColorBrush brush = GetTemplateChild("PART_PreviewBrush") as SolidColorBrush; if (brush != null) { Binding binding = new Binding("Color"); binding.Source = brush; binding.Mode = BindingMode.OneWayToSource; this.SetBinding(ColorPicker.ColorProperty, binding); } }
注意,上面程式碼使用的是System.Windows.Controls.Primitives.RangeBase類(Slider類繼承自該類)而不是Slider類。因為RangeBase類提供了需要的最小功能——在本例中是中Value屬性。通過儘可能提高程式碼的通用性,控制元件使用者可獲得更大自由。例如,現在可提供自定義模板,使用不同的派生自RangeBase類的控制元件代替顏色滑動條。
繫結SolidColorBrush畫刷的程式碼稍有區別,因為SolidColorBrush畫刷美譽包含SetBinding()方法(該方法是在FrameworkElement類中定義的)。一個比較容易得變通方法是為ColorPicker.Color屬性建立繫結表示式,使用指向源方向的單向繫結。這樣,當顏色拾取器的顏色改變後,將自動更新畫刷。
為檢視這種設計變化的優點,需要建立一個使用顏色拾取器的控制元件,並提供一個新的控制元件模板。
3、記錄模板部件
對於上面的示例,還有最後一處應予改進。良好的設計指導原則建議為控制元件宣告新增TemplatePart特性,以記錄在控制元件模板中使用了哪些部件名稱,以及為每個部件使用了什麼型別的控制元件。從技術角度看,這一步不是必須的,但該文件可為其他使用自定義類的使用者提供幫助。
下面是應當為ColorPicker控制元件類新增的TemplatePart特性:
[TemplatePart(Name = "PART_RedSlider", Type = typeof(RangeBase))] [TemplatePart(Name = "PART_BlueSlider", Type = typeof(RangeBase))] [TemplatePart(Name = "PART_GreenSlider", Type = typeof(RangeBase))] [TemplatePart(Name = "PART_PreviewBrush", Type = typeof(SolidColorBrush))] public class ColorPicker:System.Windows.Controls.Control { }
本例項原始碼:CustomControlsV2.0