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