周銀輝的開發(fā)博客(WPF)
在WPF中自定義控件(1)
在WPF中自定義控件(2) UserControl
在這里我們將將打造一個UserControl(用戶控件)來逐步講解如何在WPF中自定義控件,并將WPF的一些新特性引入到自定義控件中來. public static readonly DependencyProperty TimeProperty = 我們?yōu)榭丶?或者任何一個WPF類)添加的依賴屬性都是"公開的","靜態(tài)的","只讀的",其命名方式是"屬性名+Property",這是依賴屬性一成不變的書寫方式.對于依賴屬性的注冊可以在聲明該屬性時就調用DependencyProperty.Register()方法注冊,也可以在其靜態(tài)構造方法中注冊.上面的DependencyProperty.Register方法的幾個參數(shù)分別是:屬性名(該屬性名與聲明的依賴屬性名稱"XXXProperty"相比僅僅是少了"Property"后綴,其它完全一樣,否則在運行時會報異常),屬性的數(shù)據類型,屬性的擁有者的類型,元數(shù)據.DependencyProperty.Register("Time", typeof(DateTime), typeof(ClockUserCtrl), new FrameworkPropertyMetadata(DateTime.Now,new PropertyChangedCallback(TimePropertyChangedCallback))); 關于參數(shù)中傳遞的元數(shù)據:如果是普通的類則應該傳遞PropertyMetadata,如果是FrameworkElement則可以傳遞FrameworkPropertyMetadata,其中FrameworkPropertyMetadata中可以制定一些標記表明該屬性發(fā)生變化時控件應該做出什么反應,比如某屬性的變化會影響到該控件的繪制,那么就應該像這樣書寫該屬性的元數(shù)據: new FrameworkPropertyMetadata(defauleValue, FrameworkPropertyMetadataOptions.AffectsRender);這樣當該屬性發(fā)生變化時系統(tǒng)會考慮重繪該控件.另外元數(shù)據中還保護很多內容,比如默認值,數(shù)據驗證,數(shù)據變化時的回調函數(shù),是否參與屬性"繼承"等. 然后,我們將該依賴屬性包裝成普通屬性: [Description("獲取或設置當前日期和時間")] GetValue和SetValue方法來自于DependencyObject類,其用于獲取或設置類的某屬性值.[Category("Common Properties")] public DateTime Time { get { return (DateTime)this.GetValue(TimeProperty); } set { this.SetValue(TimeProperty, value); } } 注意:在將依賴屬性包裝成普通屬性時,在get和set塊中除了按部就班的調用GetValue和SetValue方法外,不要進行任何其它的操作.下面的代碼是不恰當的: [Description("獲取或設置當前日期和時間")] 在以前這或許是很多人的慣用寫法,但在WPF中,這樣的寫法存在潛在的錯誤,原因如下:我們知道繼承于DependencyObject的類擁有GetValue和SetValue方法來獲取或設置屬性值,那為什么我們不直接使用該方法來獲取或設置屬性值,而要將其包裝成普通的.NET屬性呢,事實上在這里兩種方式都是可以的,只不過包裝成普通的.NET屬性更符合.NET開發(fā)人員的習慣,使用GetValue和SetValue更像JAVA開發(fā)人員的習慣,但XAML在執(zhí)行時似乎于JAVA開發(fā)人員一樣,其不會調用.NET屬性而是直接使用GetValue或SetValue方法,這樣一來,我們寫在get塊和set塊中的其它代碼根本不會被XAML執(zhí)行到.所以說,就上面的Time屬性而言,C#(或其它)對該屬性的調用不會出現(xiàn)任何問題,但該屬性被用在XAML中時(比如在XAML對該屬性進行數(shù)據綁定等),其set塊中的this.OnTimeUpdated(value);語句不會被執(zhí)行到.[Category("Common Properties")] public DateTime Time { get { return (DateTime)this.GetValue(TimeProperty); } set { this.SetValue(TimeProperty, value); this.OnTimeUpdated(value);//Error } } 那么,當Time屬性發(fā)生變化時的確需要調用this.OnTimeUpdated(value);語句(因為該語句會引發(fā)時間被更新了的事件),還是在傳遞的依賴屬性元數(shù)據做文章: new FrameworkPropertyMetadata(DateTime.Now,newPropertyChangedCallback(TimePropertyChangedCallback)),我們?yōu)閷傩缘淖兓付艘粋€回調函數(shù),當該屬性變化時該回調函數(shù)就會被執(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()對應的幾個參數(shù)分別為:事件名稱,事件傳閱的方式(向上傳閱,向下傳閱或不傳閱),事件對應的EventHandler的類型,事件擁有者的類型) 然后將事件包裝成普通的.NET事件: [Description("日期或時間被更新后發(fā)生")] 注意,與依賴屬性一樣,不要在add與remove塊中添加除AddHandler與RemoveHandler以外的代碼.public event RoutedPropertyChangedEventHandler<DateTime> TimeUpdated { add { this.AddHandler(TimeUpdatedEvent, value); } remove { this.RemoveHandler(TimeUpdatedEvent, value); } } 題外話,事件參數(shù)中的e.Handled=true并不是終止事件的傳閱,這只是為事件做一個標記而已,以便在默認情況下的讓那些事件處理函數(shù)在該標記為true的情況下不被調用,要為該標記為true的事件注冊處理方法并讓該方法得到執(zhí)行,請使用AddHandler方法,并把最后一個參數(shù)handlerEventsToo設置為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); }
public static readonly RoutedUICommand SpeakCommand = new RoutedUICommand("Speak", "Speak", typeof(ClockUserCtrl)); 參數(shù)分別為命名的顯示名稱,命令的名稱,命令的擁有者類型.然后在控件的靜態(tài)函數(shù)中定義一個命令綁定,該命令綁定定義了命令的具體細節(jié):對應的命令是什么?其完成什么樣的功能,當前環(huán)境下其能執(zhí)行嗎? CommandBinding commandBinding =
new CommandBinding(SpeakCommand, new ExecutedRoutedEventHandler(ExecuteSpeak), new CanExecuteRoutedEventHandler(CanExecuteSpeak)); private static void ExecuteSpeak(object sender, ExecutedRoutedEventArgs arg) CanExecuteRoutedEventArgs的CanExecute屬性用于指示當前命令是否可用,也就是說系統(tǒng)會不斷地檢視該命令與該命令的作用對象,并根據你所提供的條件來判斷當前命令是否可用,比如文本框狀態(tài)變?yōu)?只讀"后,其"粘貼"命令將不可用,作用于該文本框的粘貼按鈕會自動被禁用,反之則啟用.{ 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); } new ExecutedRoutedEventHandler(ExecuteSpeak)委托指定了當該命令被執(zhí)行時所要完成的任務,這通過回調ExcuteSpeak函數(shù)來實現(xiàn). private static void ExecuteSpeak(object sender, ExecutedRoutedEventArgs arg)
{ ClockUserCtrl clock = sender as ClockUserCtrl; if (clock != null) { clock.SpeakTheTime(); } } private void SpeakTheTime() 我們也可以為命令添加快捷鍵,這是通過InputBinding來實現(xiàn)的,其將命令與命令的快捷鍵關聯(lián)起來,比如:{ DateTime localTime = this.Time.ToLocalTime(); string textToSpeak = "現(xiàn)在時刻," + localTime.ToShortDateString() +","+ localTime.ToShortTimeString() + ",星期" + (int)localTime.DayOfWeek; this.speecher.SpeakAsync(textToSpeak); } InputBinding inputBinding = new InputBinding(SpeakCommand, new MouseGesture(MouseAction.LeftClick)); 這樣,當我們鼠標點擊控件時就會引發(fā)控件的Speak命令,從而調用SpeakTheTime函數(shù)進行語音播報.CommandManager.RegisterClassInputBinding(typeof(ClockUserCtrl), inputBinding); 快捷鍵可以通過MouseGesture或KeyGesture來定義. 4,優(yōu)點與缺點: WPF中的命令與命令綁定(一)說到用戶輸入,,可能我們更多地會聯(lián)想到鍵盤,、鼠標、手寫筆,,其實還用一種高級別的輸入——命令(Commands),,從WPF類庫角度講他們分別對于Keyboard,Mouse,,Ink與ICommand,。命令是一種語義級別的輸入而不是設備級別的,比如“復制”與“粘貼”,,但實現(xiàn)一個命令可以有很多中方式,,比如“粘貼”,我們可以使用CTRL-V,,也可以使用主菜單或右鍵菜單(上下文菜單)等等,。在以往的.net版本中,要在軟件界面上添加一個“粘貼”按鈕,,是非常麻煩的事情,,你得監(jiān)視剪切板中是否有可用的文本以及對應的文本框是否獲得了焦點以便啟用或禁用該按鈕,,當粘貼時你還得從剪切板中取得相應的文本并插入到文本框的合理位置,等等,。在WPF中提供的命令機制能非常簡單地實現(xiàn)這些任務,,下面的Demo演示了如何簡單到不用手動編寫一行后臺邏輯代碼便解決上面的難題的,你可以粘貼下面的代碼到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>
<MenuItem Command="ApplicationCommands.Copy" Header="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}"/> 我們將“復制”命令(ApplicationCommands.Copy)賦值給了菜單項的Command屬性,,實現(xiàn)了ICommandSource接口的元素都擁有該屬性,這表示該元素可以作為一個“命令源”來引發(fā)某個命令,,其Command屬性就指示了其將引發(fā)的命令,。 Header="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}"/>
我們將菜單文本綁定到了命令的Text屬性,,這是因為,,如果一個命令為RoutedUICommand類型,那么該命令將有一個Text屬性來說明該命令對應到的文本名稱,,該Text屬性會自動本地化的,,也就是說如果你的計算機使用語言是簡體中文的話該菜單項顯示的是“復制”,如果你的計算機使用的語言是英語的話該菜單項顯示的將是“Copy”,。 在本隨筆的后續(xù)部分我們將更加深入的探討WPF的命令系統(tǒng),,敬請關注,謝謝,。
WPF中的命令與命令綁定(二)在WPF中,,命令(Commanding)被分割成了四個部分,分別是ICommand,,ICommandSource,,CommandTarget和CommandBinding。下面我們來分別探討這四個部分,。1,,ICommand Command也就是我們的“命令”本身,比如“復制”“粘貼”,。在WPF中,,所有的命令都必須實現(xiàn)ICommand接口,它為所有的命令提供一個抽象,,這個抽象對于我們實現(xiàn)Undo,、Redo操作非常重要,如果你學習一下設計模式中的“命令”模式,,你會更加深刻的理解,。 ICommand接口中擁有Execute()方法,該方法用于命令的執(zhí)行(不過,,注意:命令的執(zhí)行邏輯——比如將剪切板中的文本去出來放到文本框的合適位置——并沒有被編寫到該方法中,,稍后我們會講到這其中的奧妙),另外的一個方法是CanExecute()用于指示當前命令在目標元素上是否可用,,當這種可用性發(fā)生改變時其便會引發(fā)該接口的尾頁一個事件CanExecuteChanged,。 在目前的WPF類庫中,,你能看到唯一一個實現(xiàn)了ICommand接口的類型RoutedCommand(其實還有一個名為SecureUICommand的類也實現(xiàn)了該接口,不過該類未被公開),,“Routed”是一個不太容易被翻譯的修飾詞(有人將它翻譯為“路由”),,但這意味著該類型的命令可以向WPF中的RoutedEvent一樣在元素樹中上下傳遞。 RoutedCommand的子類RoutedUICommand是我們經常使用的類型,,它與RoutedCommand的不同之處僅僅在與它多了一個Text屬性來描述該命令,,不過大多數(shù)WPF內置命令的Text屬性有一個很不錯的特點:其支持自動本地化。這至少會為我們的軟件的本地化減少工作量,。 在本系列隨筆的后續(xù)部分將介紹如何自定義一個命令,。
2,ICommandSource與CommandTarget CommandBinding CloseCommandBinding = new CommandBinding( ApplicationCommands.Close, CloseCommandHandler, CanExecuteHandler); CommandBinding構造方法的最后兩個參數(shù)分別是ExecutedRoutedEventHandler 與 CanExecuteRoutedEventHandler 類型的委托,,用于指示如何執(zhí)行命令和如何判斷命令能否被執(zhí)行。 |
|