【转】在WPF中自定义控件
周銀輝的開發(fā)博客(WPF)
在WPF中自定義控件(1)
一, 不一定需要自定義控件
在使用WPF以前,動(dòng)輒使用自定義控件幾乎成了慣性思維,比如需要一個(gè)帶圖片的按鈕,但在WPF中此類任務(wù)卻不需要如此大費(fèi)周章,因?yàn)榭丶梢郧短资褂靡约翱梢詾榭丶庥^打造一套新的樣式就可以了.是否需要我們來自定義控件,這需要你考慮目前已有控件的真正邏輯功能而不要局限于外觀,如果目前的控件都不能直覺地表達(dá)你的想法,那么你可以自己來打造一個(gè)控件,否則,也許我們僅僅改變一下目前控件的模板等就可以完成任務(wù).很多人在自定義控件上經(jīng)常犯的錯(cuò)誤是:重復(fù)撰寫已有的邏輯
二,UserControl還是CustomControl?
要在WPF中自定義一個(gè)控件,使用UserControl與CustomControl都是不錯(cuò)的選擇(除此之外,還有更多選擇,比如打造一個(gè)自定義的面板,但這不在本文的討論范圍),他們的區(qū)別在于:
UserControl,其更像WinForm中自定義控件的開發(fā)風(fēng)格,在開發(fā)上更簡(jiǎn)單快速,幾乎可以簡(jiǎn)單地理解為:利用設(shè)計(jì)器來將多個(gè)已有控件作為子元素來拼湊成一個(gè)UserControl并修改其外觀,然后后臺(tái)邏輯代碼直接訪問這些子元素.其最大的弊端在于:其對(duì)模板樣式等支持度不好,其重復(fù)使用的范圍有限.
CustomControl, 其開發(fā)出來的控件才真正具有WPF風(fēng)格,其對(duì)模板樣式有著很好的支持,這是因?yàn)榇蛟霤ustomControl時(shí)做到了邏輯代碼與外觀相分離,即使換上一套完全不同的視覺樹其同樣能很好的工作,就像WPF內(nèi)置的控件一樣.
在使用Visual Studio打造控件時(shí),UserControl與CustomControl的差別就更加明顯,在項(xiàng)目中添加一個(gè)UserControl時(shí),我們會(huì)發(fā)現(xiàn)設(shè)計(jì)器為我們添加了一個(gè)XAML文件以及一個(gè)對(duì)應(yīng)的.CS文件(或.VB等),然后你就可以像設(shè)計(jì)普通窗體一樣設(shè)計(jì)該UserControl; 如果我們是在項(xiàng)目中添加一個(gè)CustomControl,情況卻不是這樣,設(shè)計(jì)器會(huì)為我們生成一個(gè).CS文件(或.VB等),該文件用于編寫控件的后臺(tái)邏輯,而控件的外觀卻定義在了軟件的應(yīng)用主題(Theme)中了(如果你沒有為軟件定義通用主題,其會(huì)自動(dòng)生成一個(gè)通用主題themes\generic.xaml, 然后主題中會(huì)自動(dòng)為你的控件生成一個(gè)Style),并將通用主題與該控件關(guān)聯(lián)了起來.這也就是CustomControl對(duì)樣式的支持度比UserControl好的原因.
三,繼承于UserContorl,Control還是其它?
如果你準(zhǔn)備打造一個(gè)控件,并使用像Visual?Studio這樣的工具來開發(fā)的話,打造UserControl時(shí)其會(huì)自動(dòng)為你從System.Windows.Controls.UserControl繼承,打造CustomControl時(shí)其會(huì)為從System.Windows.Controls.Control繼承.但實(shí)際情況下,也許我們從他們的衍生類別開始繼承會(huì)得到更多的好處(更好的重用已有的邏輯),比如你的控件擁有更多的類似于Button的某些特性,那么從Button開始繼承就比從Control繼承少寫很多代碼.
在接下來的幾節(jié)中,我們會(huì)逐步討論如何打造UserControl與CustomControl以及讓它們更好支持WPF新特性.
?
在WPF中自定義控件(2) UserControl
?
在這里我們將將打造一個(gè)UserControl(用戶控件)來逐步講解如何在WPF中自定義控件,并將WPF的一些新特性引入到自定義控件中來.
我們制作了一個(gè)帶語音報(bào)時(shí)功能的鐘表控件, 效果如下:
在VS中右鍵單擊你的項(xiàng)目,點(diǎn)擊"添加新項(xiàng)目",在出現(xiàn)的選擇列表中選擇"UserControl",VS會(huì)自動(dòng)為你生成一個(gè)*.xaml文件以及其對(duì)應(yīng)的后臺(tái)代碼文件(*.cs或其它).
值得注意的是,自動(dòng)生成的代碼中,你的控件是繼承于System.Windows.Controls.UserControl類的,這對(duì)應(yīng)你的控件而言并不一定是最恰當(dāng)?shù)幕?你可以修改它,但注意你應(yīng)該同時(shí)修改*.cs文件和*.xaml文件中的基類,而不只是修改*.cs文件,否則當(dāng)生成項(xiàng)目時(shí)會(huì)報(bào)錯(cuò)"不是繼承于同一基類".修改*.xaml文件的方法是:將該文件的第一行和最后一行的"UserControl"改成與你認(rèn)為恰當(dāng)?shù)幕惷Q.
1,為控件添加屬性(依賴屬性,DependencyProperty)
正如下面的代碼所示:
?public???static???readonly??DependencyProperty?TimeProperty??=??
????????????DependencyProperty.Register(?"?Time?"?,??typeof?(DateTime),??typeof?(ClockUserCtrl),?
?????????????new??FrameworkPropertyMetadata(DateTime.Now,?new??PropertyChangedCallback(TimePropertyChangedCallback)));
我們?yōu)榭丶?或者任何一個(gè)WPF類)添加的依賴屬性都是"公開的","靜態(tài)的","只讀的",其命名方式是"屬性名+Property",這是依賴屬性一成不變的書寫方式.對(duì)于依賴屬性的注冊(cè)可以在聲明該屬性時(shí)就調(diào)用?DependencyProperty.Register()方法注冊(cè),也可以在其靜態(tài)構(gòu)造方法中注冊(cè).上面的?DependencyProperty.Register方法的幾個(gè)參數(shù)分別是:屬性名(該屬性名與聲明的依賴屬性名稱"XXXProperty"相比僅僅是少了"Property"后綴,其它完全一樣,否則在運(yùn)行時(shí)會(huì)報(bào)異常),屬性的數(shù)據(jù)類型,屬性的擁有者的類型,元數(shù)據(jù).
關(guān)于參數(shù)中傳遞的元數(shù)據(jù):如果是普通的類則應(yīng)該傳遞?PropertyMetadata,如果是FrameworkElement則可以傳遞?FrameworkPropertyMetadata,其中?FrameworkPropertyMetadata中可以制定一些標(biāo)記表明該屬性發(fā)生變化時(shí)控件應(yīng)該做出什么反應(yīng),比如某屬性的變化會(huì)影響到該控件的繪制,那么就應(yīng)該像這樣書寫該屬性的元數(shù)據(jù):??new FrameworkPropertyMetadata(defauleValue, FrameworkPropertyMetadataOptions.AffectsRender);這樣當(dāng)該屬性發(fā)生變化時(shí)系統(tǒng)會(huì)考慮重繪該控件.另外元數(shù)據(jù)中還保護(hù)很多內(nèi)容,比如默認(rèn)值,數(shù)據(jù)驗(yàn)證,數(shù)據(jù)變化時(shí)的回調(diào)函數(shù),是否參與屬性"繼承"等.
然后,我們將該依賴屬性包裝成普通屬性:
?????????[Description(?"?獲取或設(shè)置當(dāng)前日期和時(shí)間?"?)]
????????[Category(?"?Common?Properties?"?)]
?????????public??DateTime?Time
?????????{
????????????get
????????????{
????????????????return?(DateTime)this.GetValue(TimeProperty);
????????????}
????????????set
????????????{
????????????????this.SetValue(TimeProperty,?value);
????????????}
????????}
GetValue和SetValue方法來自于DependencyObject類,其用于獲取或設(shè)置類的某屬性值.
注意:在將依賴屬性包裝成普通屬性時(shí),在get和set塊中除了按部就班的調(diào)用GetValue和SetValue方法外,不要進(jìn)行任何其它的操作.下面的代碼是?不恰當(dāng)的:
?????????[Description(?"?獲取或設(shè)置當(dāng)前日期和時(shí)間?"?)]
????????[Category(?"?Common?Properties?"?)]
?????????public??DateTime?Time
?????????{
????????????get
????????????{
????????????????return?(DateTime)this.GetValue(TimeProperty);
????????????}
????????????set
????????????{
????????????????this.SetValue(TimeProperty,?value);
????????????????this.OnTimeUpdated(value);//Error
????????????}
????????}
在以前這或許是很多人的慣用寫法,但在WPF中,這樣的寫法存在潛在的錯(cuò)誤,原因如下:我們知道繼承于DependencyObject的類擁有GetValue和SetValue方法來獲取或設(shè)置屬性值,那為什么我們不直接使用該方法來獲取或設(shè)置屬性值,而要將其包裝成普通的.NET屬性呢,事實(shí)上在這里兩種方式都是可以的,只不過包裝成普通的.NET屬性更符合.NET開發(fā)人員的習(xí)慣,使用GetValue和SetValue更像JAVA開發(fā)人員的習(xí)慣,但XAML在執(zhí)行時(shí)似乎于JAVA開發(fā)人員一樣,其不會(huì)調(diào)用.NET屬性而是直接使用GetValue或SetValue方法,這樣一來,我們寫在get塊和set塊中的其它代碼根本不會(huì)被XAML執(zhí)行到.所以說,就上面的Time屬性而言,C#(或其它)對(duì)該屬性的調(diào)用不會(huì)出現(xiàn)任何問題,但該屬性被用在XAML中時(shí)(比如在XAML對(duì)該屬性進(jìn)行數(shù)據(jù)綁定等),其set塊中的?this.OnTimeUpdated(value);語句不會(huì)被執(zhí)行到.
那么,當(dāng)Time屬性發(fā)生變化時(shí)的確需要調(diào)用?this.OnTimeUpdated(value);語句(因?yàn)樵撜Z句會(huì)引發(fā)時(shí)間被更新了的事件),還是在傳遞的依賴屬性元數(shù)據(jù)做文章:
new FrameworkPropertyMetadata(DateTime.Now,new?PropertyChangedCallback(TimePropertyChangedCallback)),我們?yōu)閷傩缘淖兓付艘粋€(gè)回調(diào)函數(shù),當(dāng)該屬性變化時(shí)該回調(diào)函數(shù)就會(huì)被執(zhí)行:
??????????private???static???void??TimePropertyChangedCallback(DependencyObject?sender,?DependencyPropertyChangedEventArgs?arg)
?????????{
????????????if?(sender?!=?null?&&?sender?is?ClockUserCtrl)
????????????{
????????????????ClockUserCtrl?clock?=?sender?as?ClockUserCtrl;
????????????????clock.OnTimeUpdated((DateTime)arg.OldValue,?(DateTime)arg.NewValue);
????????????????
????????????}
????????}
2,為控件添加事件(傳閱事件,RoutedEvent)
添加傳閱事件的方法與添加依賴屬性的方法很類似:
??????????public???static???readonly??RoutedEvent?TimeUpdatedEvent??=??
????????????EventManager.RegisterRoutedEvent(?"?TimeUpdated?"?,
?????????????RoutingStrategy.Bubble,??typeof?(RoutedPropertyChangedEventHandler?<?DateTime?>?),??typeof?(ClockUserCtrl));
其支持方法?EventManager.RegisterRoutedEvent()對(duì)應(yīng)的幾個(gè)參數(shù)分別為:事件名稱,事件傳閱的方式(向上傳閱,向下傳閱或不傳閱),事件對(duì)應(yīng)的EventHandler的類型,事件擁有者的類型)
然后將事件包裝成普通的.NET事件:
?????????[Description(?"?日期或時(shí)間被更新后發(fā)生?"?)]
?????????public???event??RoutedPropertyChangedEventHandler?<?DateTime?>??TimeUpdated
?????????{
????????????add
????????????{
????????????????this.AddHandler(TimeUpdatedEvent,?value);
????????????}
????????????remove
????????????{
????????????????this.RemoveHandler(TimeUpdatedEvent,?value);
????????????}
????????}
注意,與依賴屬性一樣,不要在add與remove塊中添加除AddHandler與RemoveHandler以外的代碼.
題外話,事件參數(shù)中的e.Handled=true并不是終止事件的傳閱,這只是為事件做一個(gè)標(biāo)記而已,以便在默認(rèn)情況下的讓那些事件處理函數(shù)在該標(biāo)記為true的情況下不被調(diào)用,要為該標(biāo)記為true的事件注冊(cè)處理方法并讓該方法得到執(zhí)行,請(qǐng)使用AddHandler方法,并把最后一個(gè)參數(shù)handlerEventsToo設(shè)置為true,如下:
?this?.myInkCanvas.AddHandler(
??????InkCanvas.MouseLeftButtonDownEvent,
???????new??MouseButtonEventHandler(
??????????myInkCanvas_MouseLeftButtonDown),
???????true?);
?private???void??myInkCanvas_MouseLeftButtonDown(
????????object??sender,?MouseButtonEventArgs?e)
?{
???????//do?something
}
然后編寫慣用的OnXXX方法:
??????????protected???virtual???void??OnTimeUpdated(DateTime?oldValue,?DateTime?newValue)
?????????{
????????????RoutedPropertyChangedEventArgs<DateTime>?arg?=?
????????????????new?RoutedPropertyChangedEventArgs<DateTime>(oldValue,?newValue,TimeUpdatedEvent);
????????????this.RaiseEvent(arg);
????????????
????????}
?
3,為控件添加命令(Commands)
能為自定義控件添加如WPF內(nèi)置控件一樣的命令是一件很不錯(cuò)的事情(事實(shí)上這也是在CustomControl中降低界面和后臺(tái)邏輯耦合度的一種方法,本系列隨筆中的下一篇中將會(huì)具體談?wù)?.
WPF中內(nèi)置的命令有兩大類型:RoutedCommand以及RoutedUICommand,后者比前者多了一個(gè)Text屬性用于在界面上自動(dòng)本地化地顯示該命令對(duì)應(yīng)的文本,更多的可以參考WPF中的命令與命令綁定(一)以及WPF中的命令與命令綁定(二).
這里我們來定義一個(gè)命令,其功能是控件的語音報(bào)時(shí).首先我們定義一個(gè)命令:
??????????public???static???readonly??RoutedUICommand?SpeakCommand??=???new??RoutedUICommand(?"?Speak?"?,??"?Speak?"?,??typeof?(ClockUserCtrl));
參數(shù)分別為命名的顯示名稱,命令的名稱,命令的擁有者類型.
然后在控件的靜態(tài)函數(shù)中定義一個(gè)命令綁定,該命令綁定定義了命令的具體細(xì)節(jié):對(duì)應(yīng)的命令是什么?其完成什么樣的功能,當(dāng)前環(huán)境下其能執(zhí)行嗎?
?????????????CommandBinding?commandBinding??=
?????????????????new??CommandBinding(SpeakCommand,??new??ExecutedRoutedEventHandler(ExecuteSpeak),
?????????????????new??CanExecuteRoutedEventHandler(CanExecuteSpeak));
??????????private???static???void??ExecuteSpeak(?object??sender,?ExecutedRoutedEventArgs?arg)
?????????{
????????????ClockUserCtrl?clock?=?sender?as?ClockUserCtrl;
????????????if?(clock?!=?null)
????????????{
????????????????clock.SpeakTheTime();
????????????}
????????}
?????????private???static???void??CanExecuteSpeak(?object??sender,?CanExecuteRoutedEventArgs?arg)
?????????{
????????????ClockUserCtrl?clock?=?sender?as?ClockUserCtrl;
????????????arg.CanExecute?=?(clock?!=?null);
????????}
CanExecuteRoutedEventArgs的CanExecute屬性用于指示當(dāng)前命令是否可用,也就是說系統(tǒng)會(huì)不斷地檢視該命令與該命令的作用對(duì)象,并根據(jù)你所提供的條件來判斷當(dāng)前命令是否可用,比如文本框狀態(tài)變?yōu)?#34;只讀"后,其"粘貼"命令將不可用,作用于該文本框的粘貼按鈕會(huì)自動(dòng)被禁用,反之則啟用.
new ExecutedRoutedEventHandler(ExecuteSpeak)委托指定了當(dāng)該命令被執(zhí)行時(shí)所要完成的任務(wù),這通過回調(diào)ExcuteSpeak函數(shù)來實(shí)現(xiàn).
??????????private???static???void??ExecuteSpeak(?object??sender,?ExecutedRoutedEventArgs?arg)
?????????{
????????????ClockUserCtrl?clock?=?sender?as?ClockUserCtrl;
????????????if?(clock?!=?null)
????????????{
????????????????clock.SpeakTheTime();
????????????}
????????}
??????????private???void??SpeakTheTime()
?????????{
????????????DateTime?localTime?=?this.Time.ToLocalTime();
????????????string?textToSpeak?=?"現(xiàn)在時(shí)刻,"?+?
????????????????localTime.ToShortDateString()?+","+
????????????????localTime.ToShortTimeString()??+?
????????????????",星期"?+?(int)localTime.DayOfWeek;
????????????this.speecher.SpeakAsync(textToSpeak);
????????}
我們也可以為命令添加快捷鍵,這是通過InputBinding來實(shí)現(xiàn)的,其將命令與命令的快捷鍵關(guān)聯(lián)起來,比如:
?????????????InputBinding?inputBinding??=???new??InputBinding(SpeakCommand,??new??MouseGesture(MouseAction.LeftClick));
????????????CommandManager.RegisterClassInputBinding(?typeof?(ClockUserCtrl),?inputBinding);
這樣,當(dāng)我們鼠標(biāo)點(diǎn)擊控件時(shí)就會(huì)引發(fā)控件的Speak命令,從而調(diào)用SpeakTheTime函數(shù)進(jìn)行語音播報(bào).
快捷鍵可以通過MouseGesture或KeyGesture來定義.
4,優(yōu)點(diǎn)與缺點(diǎn):
正如在在WPF中自定義控件(1)?中談到的一樣,UserControl能比較快速的打造自定義控件,但其對(duì)模板樣式等缺乏很好的支持,打造出來的控件不如WPF內(nèi)置控件一樣靈活,在本系列隨筆的下一篇中,我們將介紹如何打造能對(duì)WPF新特性提供完全支持的CustomControl.
DEMO
?
WPF中的命令與命令綁定(一)
說到用戶輸入,可能我們更多地會(huì)聯(lián)想到鍵盤、鼠標(biāo)、手寫筆,其實(shí)還用一種高級(jí)別的輸入——命令(Commands),從WPF類庫角度講他們分別對(duì)于Keyboard,Mouse,Ink與ICommand。命令是一種語義級(jí)別的輸入而不是設(shè)備級(jí)別的,比如“復(fù)制”與“粘貼”,但實(shí)現(xiàn)一個(gè)命令可以有很多中方式,比如“粘貼”,我們可以使用CTRL-V,也可以使用主菜單或右鍵菜單(上下文菜單)等等。在以往的.net版本中,要在軟件界面上添加一個(gè)“粘貼”按鈕,是非常麻煩的事情,你得監(jiān)視剪切板中是否有可用的文本以及對(duì)應(yīng)的文本框是否獲得了焦點(diǎn)以便啟用或禁用該按鈕,當(dāng)粘貼時(shí)你還得從剪切板中取得相應(yīng)的文本并插入到文本框的合理位置,等等。
在WPF中提供的命令機(jī)制能非常簡(jiǎn)單地實(shí)現(xiàn)這些任務(wù),下面的Demo演示了如何簡(jiǎn)單到不用手動(dòng)編寫一行后臺(tái)邏輯代碼便解決上面的難題的,你可以粘貼下面的代碼到XamlPad:
?<?Window
?????xmlns?="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
????xmlns:x?="http://schemas.microsoft.com/winfx/2006/xaml"
????x:Name?="Window"
????Title?="Window1"
????Width?="640"??Height?="480"?>
?????<?DockPanel??LastChildFill?="True"?>
?????????<?Menu??Width?="Auto"??Height?="20"??DockPanel.Dock?="Top"?>
?????????????<?MenuItem??Command?="ApplicationCommands.Copy"??Header?="{Binding?Path=Command.Text,?RelativeSource={RelativeSource?Self}}"?/>
?????????????<?MenuItem??Command?="ApplicationCommands.Paste"??Header?="{Binding?Path=Command.Text,?RelativeSource={RelativeSource?Self}}"?/>
?????????????<?MenuItem??Command?="ApplicationCommands.Cut"??Header?="{Binding?Path=Command.Text,?RelativeSource={RelativeSource?Self}}"?/>
?????????????<?MenuItem??Command?="ApplicationCommands.Redo"??Header?="{Binding?Path=Command.Text,?RelativeSource={RelativeSource?Self}}"?/>
?????????????<?MenuItem??Command?="ApplicationCommands.Undo"??Header?="{Binding?Path=Command.Text,?RelativeSource={RelativeSource?Self}}"?/>
?????????</?Menu?>
?????????<?RichTextBox?>
?????????????<?FlowDocument?>
?????????????????<?Paragraph?/>
?????????????</?FlowDocument?>
?????????</?RichTextBox?>
?????</?DockPanel?>
?</?Window?>
Demo中菜單欄的菜單項(xiàng)不僅僅能完美地完成任務(wù)而且能根據(jù)文本框的狀態(tài)和剪切板自動(dòng)的啟用與禁用,而我們卻沒有為這些復(fù)雜的邏輯編寫任何的后臺(tái)代碼。這就是WPF中的命令機(jī)制為我們提供了方便。
注意這一行代碼:
?<?MenuItem??Command?="ApplicationCommands.Copy"??Header?="{Binding?Path=Command.Text,?RelativeSource={RelativeSource?Self}}"?/>
?
我們將“復(fù)制”命令(ApplicationCommands.Copy)賦值給了菜單項(xiàng)的Command屬性,實(shí)現(xiàn)了ICommandSource接口的元素都擁有該屬性,這表示該元素可以作為一個(gè)“命令源”來引發(fā)某個(gè)命令,其Command屬性就指示了其將引發(fā)的命令。
其實(shí)一個(gè)命令系統(tǒng)是被分為四個(gè)部分的:
Command(命令):一個(gè)語義級(jí)別的輸入,比如“復(fù)制”,“左對(duì)齊”,“播放”等
CommandSource(命令源):引發(fā)某命令的元素,比如按鈕,菜單項(xiàng),鍵盤(Ctrl-C,F1等),鼠標(biāo)等。
CommandTarget(命令目標(biāo)):命令被作用的目標(biāo),比如文本框,播放器等。
CommandBinding(命令綁定):用于將命令和命令的處理邏輯鏈接起來,比如同樣的"粘貼",但粘貼文本和粘貼圖片的處理邏輯是不一樣的,命令綁定負(fù)責(zé)將“粘貼”命令與合理的處理邏輯連接起來。
關(guān)于命令系統(tǒng)將在本文章的后續(xù)部分中講解,不過值得一提的是,在上面的Demo中我們只指定了命令和命令源,并未指定命令目標(biāo),但它會(huì)以獲取鍵盤焦點(diǎn)的元素(這里是我們的RichTextBox)作為默認(rèn)值,而命令綁定以及命令的后臺(tái)執(zhí)行邏輯被隱藏到了RichTextBox內(nèi)部,那些編寫RichTextBox控件的開發(fā)人員會(huì)為我們編寫該部分代碼。
另外,你可能已經(jīng)發(fā)現(xiàn),在Demo中我們并沒有為菜單項(xiàng)標(biāo)題直接設(shè)置“復(fù)制”“粘貼”這樣的文本,而是使用了如下的一個(gè)綁定:
?Header="{Binding?Path=Command.Text,?RelativeSource={RelativeSource?Self}}"/>
我們將菜單文本綁定到了命令的Text屬性,這是因?yàn)?#xff0c;如果一個(gè)命令為RoutedUICommand類型,那么該命令將有一個(gè)Text屬性來說明該命令對(duì)應(yīng)到的文本名稱,該Text屬性會(huì)自動(dòng)本地化的,也就是說如果你的計(jì)算機(jī)使用語言是簡(jiǎn)體中文的話該菜單項(xiàng)顯示的是“復(fù)制”,如果你的計(jì)算機(jī)使用的語言是英語的話該菜單項(xiàng)顯示的將是“Copy”。
WPF為我們提供了大量內(nèi)置命令,包括ApplicationCommands,NavigationCommands,,MediaCommands,EditingCommands與ComponentCommands,以及控件開發(fā)人員為它們的控件也提供了很多特有的命令(比如Slider.DecreaseLarge 與 Slider.DecreaseSmall),這些足以應(yīng)付平時(shí)的大多數(shù)應(yīng)用,如果還不夠的話,你可以為自己的應(yīng)用自定義更多的命令。
在本隨筆的后續(xù)部分我們將更加深入的探討WPF的命令系統(tǒng),敬請(qǐng)關(guān)注,謝謝。
?
WPF中的命令與命令綁定(二)
在WPF中,命令(Commanding)被分割成了四個(gè)部分,分別是ICommand,ICommandSource,CommandTarget和CommandBinding。下面我們來分別探討這四個(gè)部分。
1,ICommand
Command也就是我們的“命令”本身,比如“復(fù)制”“粘貼”。在WPF中,所有的命令都必須實(shí)現(xiàn)ICommand接口,它為所有的命令提供一個(gè)抽象,這個(gè)抽象對(duì)于我們實(shí)現(xiàn)Undo、Redo操作非常重要,如果你學(xué)習(xí)一下設(shè)計(jì)模式中的“命令”模式,你會(huì)更加深刻的理解。
ICommand接口中擁有Execute()方法,該方法用于命令的執(zhí)行(不過,注意:命令的執(zhí)行邏輯——比如將剪切板中的文本去出來放到文本框的合適位置——并沒有被編寫到該方法中,稍后我們會(huì)講到這其中的奧妙),另外的一個(gè)方法是CanExecute()用于指示當(dāng)前命令在目標(biāo)元素上是否可用,當(dāng)這種可用性發(fā)生改變時(shí)其便會(huì)引發(fā)該接口的尾頁一個(gè)事件CanExecuteChanged。
在目前的WPF類庫中,你能看到唯一一個(gè)實(shí)現(xiàn)了ICommand接口的類型RoutedCommand(其實(shí)還有一個(gè)名為SecureUICommand的類也實(shí)現(xiàn)了該接口,不過該類未被公開),“Routed”是一個(gè)不太容易被翻譯的修飾詞(有人將它翻譯為“路由”),但這意味著該類型的命令可以向WPF中的RoutedEvent一樣在元素樹中上下傳遞。
RoutedCommand的子類RoutedUICommand是我們經(jīng)常使用的類型,它與RoutedCommand的不同之處僅僅在與它多了一個(gè)Text屬性來描述該命令,不過大多數(shù)WPF內(nèi)置命令的Text屬性有一個(gè)很不錯(cuò)的特點(diǎn):其支持自動(dòng)本地化。這至少會(huì)為我們的軟件的本地化減少工作量。
在本系列隨筆的后續(xù)部分將介紹如何自定義一個(gè)命令。
?
2,ICommandSource與CommandTarget
命令源,用來觸發(fā)我們的命令,比如用一個(gè)菜單項(xiàng)來觸發(fā)“復(fù)制”命令,那么該菜單項(xiàng)就是命令源。要使一個(gè)元素成為命令源,其必須實(shí)現(xiàn)ICommandSource接口。命令源決定了它所要觸發(fā)的命令、該命令所作用的對(duì)象以及命令參數(shù)(如果需要的話),這分別對(duì)應(yīng)于它的三個(gè)屬性:Command、CommandTarget以及CommandParameter。其中需要注意的是CommandTarget,因?yàn)樵赪PF中如果你不為命令源指定其命令對(duì)象,那么其將會(huì)把界面上獲得鍵盤焦點(diǎn)的元素作為默認(rèn)的命令對(duì)象,這為我們提供了方便,比如界面上有兩個(gè)文本框,我們不必?fù)?dān)心主菜單項(xiàng)上的“粘貼”操作是針對(duì)哪個(gè)文本框的,誰獲得焦點(diǎn)便針對(duì)誰,這符合大家的習(xí)慣。但引入的問題是,如果命令目標(biāo)不具備獲取鍵盤焦點(diǎn)的能力(比如Label)或命令源會(huì)搶占焦點(diǎn)(比如用Button來代替菜單項(xiàng),點(diǎn)擊按鈕時(shí)焦點(diǎn)由文本框轉(zhuǎn)移到了按鈕上),你的命令將會(huì)無效,這時(shí)你就必須為命令源指定命令目標(biāo)。
在本系列隨筆的后續(xù)部分將介紹如何讓你的自定義控件成為命令源和命令目標(biāo)。
3,CommandBinding
前面已經(jīng)提到我們并沒有將命令的執(zhí)行邏輯編寫到其Excute()方法中,這是有道理的,比如"粘貼"命令(ApplicationCommands.Paste),粘貼一段文本到文本框和粘貼一個(gè)圖片到繪圖板的執(zhí)行邏輯肯定是不一樣的,負(fù)責(zé)開發(fā)該“粘貼”命令的開發(fā)人員不可能知道所有的粘貼操作的具體邏輯,使用“粘貼”命令的客戶也不應(yīng)該為該執(zhí)行邏輯負(fù)責(zé),編寫該執(zhí)行邏輯的任務(wù)應(yīng)該被分發(fā)給那些支持“粘貼”操作的控件的開發(fā)人員以及那些希望為自己的控件添加“粘貼”操作的客戶。也就是說我們需要將“行為的請(qǐng)求者(命令)”和“行為的執(zhí)行者(命令的執(zhí)行邏輯)”分開而實(shí)現(xiàn)一種松耦合,而CommandBinding(命令綁定)便是命令和命令執(zhí)行邏輯的橋接器。
我們使用CommandBinding將命令與其合適的執(zhí)行邏輯綁定在一起:
??CommandBinding?CloseCommandBinding??=???new??CommandBinding(
????ApplicationCommands.Close,?CloseCommandHandler,?CanExecuteHandler);
?
CommandBinding構(gòu)造方法的最后兩個(gè)參數(shù)分別是ExecutedRoutedEventHandler?與 CanExecuteRoutedEventHandler 類型的委托,用于指示如何執(zhí)行命令和如何判斷命令能否被執(zhí)行。
與CommandBinding一樣扮演著中間角色的還有CommandManager類,它為命令綁定(以及輸入綁定)提供了很多實(shí)用方法。
在本系列隨筆的后續(xù)部分將介紹WPF的命令系統(tǒng)與“命令模式”(設(shè)計(jì)模式之一)之間的關(guān)系。
總結(jié)
以上是生活随笔為你收集整理的【转】在WPF中自定义控件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 信用卡挂失后找到了可以解挂吗
- 下一篇: 【转】.Net中的异步编程总结