在WPF中自定義控件(3) CustomControl (下)
周銀輝
1, 控件UI部分與邏輯部分的耦合.
這是一個容易被忽略但卻非常重要的問題, 我們之所以使用CustomControl而不是UserControl,是因為我們希望自己的控件能向WPF內置控件一樣,其UI能輕易地被其他用戶定制或我們將來所改變.也就是說其視覺樹不能與后臺邏輯糾纏在一起,因為其視覺樹中的元素完全可能被你的控件用戶改變.比如,如果你的控件的視覺樹中有一個Button,而你在該Button的Click事件中做了一些控件的邏輯處理,那么很可能你的控件打造失敗了,因為該Button可能會在用戶重新定義控件Template時被刪除.
在討論解決方案之前,需要提醒的是:一定要注意控件的邏輯與UI表現(Style,Template)各自職責的區分.不屬于后臺邏輯管的事情后臺邏輯就不要管,不屬于界面管的事情界面基本上也管不了或者說做起來很麻煩.一個簡單的例子是:比如說你想鼠標移動到你的控件上的事情,控件稍稍變大一點,鼠標離開控件時控件大小又還原(或其他比較絢麗的效果),那么你在控件上的后臺邏輯中添加的MouseEnter與MouseLeave事件的處理來達到這一效果.這時你的后臺邏輯就管得過寬了,因為這種效果是Style的事情,你可以把它放在控件的默認Style中(在Generic.xaml中,你可以參考在WPF中自定義控件(3) CustomControl (上) )來提供給控件用戶而不應該加在后臺邏輯中而費力不討好.這不但增加了耦合,而且在用戶看來這也有些"強奸民意",因為他沒有辦法通過自定義的Style來覆蓋掉你認為比較漂亮的控件效果.
雖然WPF將UI與后臺邏輯的隔離已經做得很不錯了,以便UI設計師能和我們更好的溝通和分工協助,但這并不意味著,WPF可以將UI與后臺完全的隔離而互不影響.事實上,我們在編寫后臺邏輯的時候常常需要用到控件UI樹中的某些元素才能完成,比如在編寫ProgressBar時我們需要知道視覺樹中的某個表示"總量"的元素的長度或高度,以便根據ProgressBar的當前Value來確定視覺樹中另外一個表示"當前量"的元素的長度或高度.還有一種情況是,我們后臺寫好了一個不錯的邏輯,但需要視覺樹中的某個UI元素來明確調用,比如說,我們在ScrollBar控件中寫好了LineDown()方法,但該方法需要用戶點擊控件視覺樹中某個表示"向下滾動一行"的元素(比如一個向下的箭頭)時來調用.
WPF提供了兩種方案,一是利用TemplatePartAttribute,二是使用Command.
1.1 TemplatePartAttribute
TemplatePart適用于上面所說的第一種情況,其用于告知用戶,在目前的情況下必須在控件的視覺樹中存在指定類型和名稱的元素才能是控件發揮完整的功能,否則可能導致功能喪失或需要用戶自行處理刪除視覺樹中的該元素而帶來的后遺癥.如果我們是某個控件的使用者,且其注明了該屬性,那么我們在修改控件的Template時就應該保證控件中是指存在其指明的特定類型和名稱的元素,除非了了解自己的確不需要其關聯的相關功能或你已另有處理.
在WPF內置控件中,這種類型的控件很多,比如ComboBox,PasswordBox,ProgressBar等等.
我們看看ComboxBox:
[TemplatePartAttribute(Name = "PART_EditableTextBox", Type = typeof(TextBox))]
[TemplatePartAttribute(Name = "PART_Popup", Type = typeof(Popup))]
[LocalizabilityAttribute(LocalizationCategory.ComboBox)]
[StyleTypedPropertyAttribute(Property = "ItemContainerStyle", StyleTargetType = typeof(ComboBoxItem))]
public class ComboBox : Selector
我們的控件也可以仿照ComboBox來規定必須的部件,并Override一些OnApplyTemplate()方法來取得相應元素:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
Button mybtn = base.GetTemplateChild("PART_BTN");
if (mybtn != null)
{
mybtn.Click += new RoutedEventHandler(mybtn_Click);
}
}
1.2 Command
這適合上面提到的第二種情況,即是我們后臺寫好了一個不錯的邏輯,但需要視覺樹中的某個UI元素來明確調用.比如ScrollBar的上端和下端的兩個小箭頭用來上下翻行,我們明顯不能在這兩個小箭頭的鼠標點擊事件中調用LineDown方法.那么正確的做法是,將后臺邏輯中的LineDown和LineUp方法包裝成LineDownCommand和LineUpCommand,然后將視覺樹中的元素的Command屬性綁定到相應的Command上.這樣一來,即便用戶修改視覺樹中的上下小箭頭為其他類型的元素,用戶也可以通過命令綁定來與相應的功能聯系起來.比如WPF內置的ScroolBar控件的向下小箭頭的XAML代碼便是如下書寫的:
<RepeatButton IsEnabled="{TemplateBinding IsMouseOver}" Style="{StaticResource ScrollBarButton}" Grid.Row="2" Command="{x:Static ScrollBar.LineDownCommand}" Microsoft_Windows_Themes:ScrollChrome.ScrollGlyph="DownArrow"/>
2,"鶴立雞群"并不總是好事
如果某天藝術細胞大爆發,打造了一個非常漂亮的控件,這自然是好事情,但我擔心這與用戶當前操作系統下的大多數界面顯得過于鶴立雞群而格格不入,畢竟在還是又不少人在Vista下使用"Windows經典"主題而非"Aero".為了打造與用戶操作系統當前主題相容的控件UI,你可能需要為控件提供幾套Style,比如一個比較相當較華麗的用于Aero主題,另一個較樸實用于Windows classical.為了實現效果隨著用戶操作系統主題改變而動態改變,你至少有兩種方法來實現:(1)監聽系統消息WM_THEMECHANGE,然后切控件界面.(2)將系統主題對應的Style放置在控件解決方案的themes文件夾下,比如與Vista Aero向對應的放在themes\Aero.NormalColor.xaml,與藍色的Windows XP主題對應的放在themes\Luna.NormalColor.xaml,與Window經典主題相對應的放在themes\Classic.xmal,相信大家已經看出規律:themes\主題名.顏色名.xaml,其中經典主題沒有顏色名.這樣當用戶切換主題時我們的控件就會切換到對應的Style,如果我們沒有提供用戶當前的主題所對應的樣式則調用themes\Generic.xaml(這也就是為什么我在在WPF中自定義控件(3) CustomControl (上) 中說"Generic.xaml這個名稱并非偶然"的原因)
3,關于控件資源的存儲位置
一般說來,為了不破壞控件的封裝性,我們不會把控件的資源放到控件以外的位置,比如,有一些資源在我們的應用中被頻繁的使用,我們共享這些資源,我們可能會將這些資源移動到APP的資源字典中,但我們控件中的資源也被移出去,會破壞封裝,并且這不利于控件被重用到其他APP. 但我們常常又會面臨這樣的問題:如果我把控件的資源完全放在該控件的資源字典中,但我們的應用很多地方使用了該控件,這就造成資源的頻繁復制.一個典型的例子是,我們制作一個撲克牌游戲,我們的美工為我制作了一套漂亮的撲克牌圖片,共54張圖片,然后我建立一個撲克牌控件,控件實例將根據其當前點數和花色來選擇其中一張圖片并呈現出來,最后生成54個撲克牌控件實例來構成一套完整的撲克牌.如果我將美工提供的54張圖片放置在控件的資源字典中,事實上對于一個撲克牌控件實例來說只使用了其中的一張圖片,其余53張完全是多余的.而生成54張撲克牌控件實例時則相當于保存了54*54=2916張美工提供的圖片.解決的辦法是將資源轉移到控件的themes\Generic.xaml中,這樣既沒有破壞封裝又然資源得到了共享.
最后非常感謝大家關注我的博客,能在這里和大家分享工作與學習經驗是件很美妙的事情.另外表示歉意的是:"在WPF中自定義控件"這個系列拖的時間太長了,差不多快一個半月了才算完成,因為這段時間我的確有太多的事情需要去完成.非常感謝大家.

浙公網安備 33010602011771號