作過.net開發的朋友對於事件應該都不陌生。追溯歷史,事件(Event)首先應用在Com和VB上,它是對在MFC中使用的煩瑣的消息機制的一個封裝,而後.net又繼承了這種事件驅動機制,這種事件也叫.net事件。正如WPF在簡單的.net屬性概念上添加了許多基礎的東西同樣,它也爲.net事件添加了許多基礎的東西。路由事件(RoutedEvent)是專門設計用於在元素樹中使用的事件。當路由事件觸發後,它能夠向上或向下遍歷邏輯樹和可視樹,用一種簡單並且持久的方式在每一個元素上觸發,而不須要使用任何定製代碼。下面咱們就來學習下路由事件,本節主要包括如下幾個方面內容:路由事件和WPF事件(包括鍵盤輸入、鼠標輸入和多點觸控輸入等)。windows
路由事件的實現和行爲與依賴屬性有不少相同的地方。咱們仍是先舉個例子來講明下路由事件的實現方式,而後再來說下路由事件的一些特性。拿最經常使用的Button的Click事件(繼承自ButtonBase抽象類)來講明:app
class MyButton:Button { //Add CLR Event wrapper for Routed Event public event RoutedEventHandler MyClick { add { this.AddHandler(MyClickEvent, value); } remove { this.RemoveHandler(MyClickEvent, value); } } //State and Register Routed Event public static readonly RoutedEvent MyClickEvent = EventManager.RegisterRoutedEvent("MyClick", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MyButton)); //Trigger Method of Routed Event protected override void OnClick() { base.OnClick(); RoutedEventArgs newEvent = new RoutedEventArgs(MyClickEvent, this); this.RaiseEvent(newEvent); } }
我照着依賴屬性的Code Snippet的樣子也寫個了路由事件的,名字無所謂,只要不衝突就好,代碼以下:框架
<?xml version="1.0" encoding="utf-8"?> <CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet"> <CodeSnippet Format="1.0.0"> <Header> <Title>定義一個 Routed Event</Title> <Shortcut>propre</Shortcut> <Description>將 RoutedEvent 用做後備存儲的路由事件的代碼段</Description> <Author>Jello Chen</Author> <SnippetTypes> <SnippetType>Expansion</SnippetType> </SnippetTypes> </Header> <Snippet> <Declarations> <Literal> <ID>Click</ID> <ToolTip>事件類型</ToolTip> <Default>Click</Default> </Literal> <Literal> <ID>newEvent</ID> <ToolTip>路由事件參數</ToolTip> <Default>newEvent</Default> </Literal> </Declarations> <Code Language="csharp"> <![CDATA[ //Add CLR Event wrapper for Routed Event public event RoutedEventHandler $Click$ { add {this.AddHandler($Click$Event,value);} remove {this.RemoveHandler($Click$Event,value);} } //State and Register Routed Event public static readonly RoutedEvent $Click$Event = EventManager.RegisterRoutedEvent("$Click$",RoutingStrategy.Bubble,typeof(RoutedEventHandler),typeof(ButtonBase)); //Trigger Method of Routed Event protected virtual void On$Click$ () { RoutedEventArgs $newEvent$ = new RoutedEventArgs($Click$Event,this); this.RaiseEvent($newEvent$); } $end$]]> </Code> </Snippet> </CodeSnippet> </CodeSnippets>
看起來是否是和依賴屬性的結構很像?首先是聲明註冊了一個RoutedEvent類型的MyClickEvent,也由static readonly修飾,也是經過一個靜態方法來得到實例,方法多了第二個事件路由策略的參數;而後是爲其添加CLR事件包裝器MyClick,分別經過AddHandler和RemoveHandler來向路由事件添加和移除一個委託,這兩個方法不是在DependencyObject中定義的,而是在更高層的UIElement類中定義的;最後定義了一個路由事件的觸發方法,實例化一個該路由事件相關的路由事件參數,將其做爲參數傳入RaiseEvent激發事件方法中,這個方法也是定義在UIElement類中的。ide
咱們先來使用上面的代碼:佈局
Xaml代碼:學習
<local:MyButton x:Name="btnTest" Content="Press me" Width="80" Height="30"/>
C#代碼:ui
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.btnTest.MyClick +=new RoutedEventHandler(btnTest_MyClick); } private void btnTest_MyClick(object sender, RoutedEventArgs e) { MessageBox.Show("I am Pressed!"); } }
這裏,咱們在構造器中使用過程式代碼來進行路由事件的訂閱,固然也能夠在Xaml中使用,由VS來自動生成這個訂閱關係。看起來和咱們在Webform和Winform中使用的沒什麼差異,這應該歸功於事件包裝器(Event Wrapper)。下面來詳細說下路由事件的註冊方法EventManager.RegisterRoutedEvent(string name, RoutingStrategy routingStrategy, Type handlerType, Type ownerType):this
再來看下路由事件處理程序的簽名,它與.net事件處理程序簽名相匹配。第一個參數是Object類型,指該處理程序被添加到的元素;第二個參數是RoutedEventArgs類型,它是EventArgs的子類,它提供了四個屬性:spa
Source屬性:邏輯樹中一開始觸發該事件的元素。.net
OriginalSource屬性:可視樹中一開四觸發該事件的元素。
Handled屬性:將事件標記爲是否已處理,true時,Tunnel隧道方式和Bubble冒泡方式將再也不繼續,不然繼續。
RoutedEvent屬性:指真正的路由事件對象,主要用於當一個事件處理程序同時被多個路由事件。
來看個冒泡事件的例子:
Xaml代碼:
<Window x:Class="RoutedEventDemo.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300" MouseDown="Image_MouseDown" x:Name="window1"> <Grid MouseDown="Image_MouseDown" x:Name="grid1"> <StackPanel x:Name="sp1" MouseDown="Image_MouseDown"> <TextBlock x:Name="tb1" MouseDown="Image_MouseDown" Width="60" Height="60"> <Image x:Name="img1" Source="Images/photo.png" MouseDown="Image_MouseDown"/> </TextBlock> <CheckBox Content="Handler Setting" x:Name="cb"/> </StackPanel> </Grid> </Window>
這裏的CheckBox是用來設置Handler屬性。
過程式代碼:
private int count = 0; private void Image_MouseDown(object sender, MouseButtonEventArgs e) { count++; string msg = "#" + count.ToString() + ":\r\n" + "sender:" + sender.ToString() + "\r\n" + "Source:" + e.Source + "\r\n" + "OriginalSource:" + e.OriginalSource + "\r\n"; e.Handled = (bool)this.cb.IsChecked; MessageBox.Show(msg); }
CheckBox未選中時,會觸發5次,依次爲Image--TextBlock--StackPanel--Grid--Window;選中時只會冒泡一次到Image。須要注意的是:Button繼承自UIElement的MouseDown事件(真正定義在Mouse類)在類內部已經處理(事件被掛起),通常不會觸發附加到Mouse類的其它實例的事件處理,瞭解詳細點擊MSDN,裏面提到了兩種解決辦法,更推薦使用相對應的Preview隧道事件來處理。
附加事件的思想,其實和附加屬性是同樣,某一元素沒有某一種事件,須要將該事件附加在該元素上以實現相應的事件監聽處理功能。先來看這樣的場景:
Xaml代碼:
<Window x:Class="RoutedEventDemo.Window2" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window2" Height="300" Width="300"> <Grid> <StackPanel> <Button Content="1" /> <Button Content="2" /> <Button Content="3" /> </StackPanel> </Grid> </Window>
如今須要用一個事件處理程序來處理StackPanel中的全部按鈕的單擊事件。天然而然,咱們會想到監聽StackPanel的Click事件。然而,Click事件是Button特有的事件(繼承自ButtonBase),這時候就須要使用附加事件來解決了。寫成這樣<StackPanel ButtonBase.Click="StackPanel_Click">......</StackPanel>。這裏沒寫成Button.Click是爲了智能提示的方便,固然在寫出Button.Click後Xaml已經能識別出這是Button的Click事件。固然,這兩種是由區別的,Button.Click只能監聽Button類型的單擊事件,而ButttonBase.Click能監聽ButtonBase類型的單擊事件,例如Button、RadioButton和CheckBox等。
過程式代碼:
this.sp.AddHandler(Button.ClickEvent, new RoutedEventHandler(StackPanel_Click));
UIElement.AddHandler還有一個重載版本:public void AddHandler(RoutedEvent routedEvent, Delegate handler, bool handledEventsToo);須要注意的是第三個參數,若是爲 true,則將按如下方式註冊處理程序:即便路由事件在其事件數據中標記爲已處理,也會調用該處理程序;若是爲 false,則使用默認條件註冊處理程序,即當路由事件被標記爲已處理時,將不調用處理程序。默認值爲false。前面說過這不是一種好的方式,由於事件應在第一時間被處理,而應該儘量地避免處理已經處理過的事件。由這也能看出來,當將事件標記爲已處理時,隧道傳遞和冒泡仍然會繼續,只不過在默認狀況下事件處理程序只處理未處理的事件。
在WPF框架中,已經爲咱們封裝了許多的事件,主要分爲這麼幾類:
當首次建立或釋放元素時都會觸發一些事件,這些事件就是生命週期事件。
來看下這些事件的執行時機。在FrameworkElement中實現了ISupportInitialize接口,該接口中有兩個方法BeginInit()和EndInit(),前者是用信號通知對象初始化即將開始,當元素被實例化後調用該方法,而後開始屬性設置;後者是用信號通知對象初始化已完成,當初始化完成後調用該方法,而後觸發Initialized事件。實現這個接口的目的是保證初始化工做的原子性。當初始化窗口時,是從下到上進行的,也就是從葉子節點開始的,這保證了當某一元素須要內容時其內容都已初始化。當全部的元素都已初始化完成後,而後開始佈局、應用樣式及可能的數據綁定等。Initialized事件後,開始觸發Loaded事件,Load的順序和Initialized的順序相反,是從上到下進行的,當全部的元素都Load完成後,窗口顯示出來。
對於Window窗口還有一些特有事件:
當但願在窗口首次加載時作一些額外的初始化工做時就能夠經過在其Loaded事件的處理程序中完成。正常狀況下,也能夠在窗口構造器中的InitializeComponent()方法後來處理。
用戶經過一些外設如鼠標、鍵盤、手寫筆和多點觸控屏等來進行輸入操做時觸發的事件,就是輸入事件。輸入事件能夠經過繼承自InputEventArgs自定義事件參數類來附加額外的信息,看下繼承關係圖:
InputEventArgs事件參數類在RoutedEventArgs類基礎上只增長了Timestamp和Device兩個屬性,Timestamp屬性表示事件什麼時候發生的毫秒數,用於用於比較各事件之間的發生順序,值越大越是最近發生;Device屬性表示觸發事件的設備的對象,設備對象是繼承自抽象類System.Windows.Input.InputDevice的子類的實例。
當用戶按下一個鍵,就會觸發一系列的事件,這裏按順序依次列出公共的事件:
上面是一些公共的事件,不一樣控件可能還有一些本身特有的事件,爲了避免衝突,還會將上面的致使衝突的事件掛起。例如TextBox控件擁有TextChanged事件,而掛起了TextInpute事件。
PreviewKeyDown事件、KeyDown事件、PreviewKeyUp事件和KeyUp事件都是經過KeyEventArgs對象來提供相應的信息。該對象有一個Key屬性,是一個System.Windows.Input.Key的枚舉類型。當咱們在檢查文本框的輸入的內容時,一般須要監聽PreviewTextInput事件(能夠接受文本輸入元素)和PreviewKeyDown事件(不能夠接受文本輸入元素)。看下面的例子:
Xaml代碼:
<StackPanel UIElement.PreviewTextInput="StackPanel_PreviewTextInput" UIElement.PreviewKeyDown="StackPanel_PreviewKeyDown"> <TextBox /> <TextBox /> <TextBox /> </StackPanel>
cs代碼:
//PreviewTextInput事件處理 private void StackPanel_PreviewTextInput(object sender, TextCompositionEventArgs e) { long num = 0; if (!long.TryParse(e.Text, out num)) { MessageBox.Show(e.Text + "鍵不是數字"); e.Handled = true; } } //PreviewKeyDown事件處理 private void StackPanel_PreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Space) { KeyConverter cvt = new KeyConverter();//這裏只是爲了演示KeyConverter的用法,此處並不合適 MessageBox.Show(cvt.ConvertToString(e.Key) + "鍵不是數字"); e.Handled = true; } /* //獲取鍵盤對象來判斷事件發生時是否打開的大小寫鍵 if (e.KeyboardDevice.IsKeyToggled(Key.CapsLock)) MessageBox.Show("CapsLock is opened"); else MessageBox.Show("CapsLock is closed"); */ /* //事件觸發時鍵盤狀態和實際的鍵盤狀態可能會不一致 //經過Keyboard.IsKeyToggled()靜態方法來實時獲取鍵盤狀態 if (Keyboard.IsKeyToggled(key: Key.CapsLock)) { MessageBox.Show("CapsLock is opened"); } else { MessageBox.Show("CapsLock is closed"); } */ }
運行效果:
在上面例子中,經過PreviewTextInput事件處理哪些能夠觸發PreviewTextInput事件的按鍵,經過PreviewKeyDown事件處理剩下的哪些不能觸發PreviewTextInput事件的按鍵(上面的空格鍵),從而讓輸入只能爲數字。
鼠標操做會觸發一系列相關的事件觸發。主要有移入移出事件、單擊雙擊事件、捕獲鼠標事件和鼠標拖放事件。
最基本的移入移出事件是MouseEnter和MouseLeave事件,這兩個事件是直接事件(Direct Event),也就是說不會冒泡會隧道傳輸。還有幾個冒泡和隧道事件爲PreviewMouseMove/MouseMove。
private void Window_MouseMove(object sender, MouseEventArgs e) { Point p = e.GetPosition(this);//獲取鼠標相對於窗口的座標 MessageBox.Show("Relative to Window,x is " + p.X + ",y is " + p.Y); Point p1 = PointToScreen(p);//獲取鼠標相對於屏幕的座標 MessageBox.Show("Relative to Screen,x is " + p1.X + ",y is " + p1.Y); }
能夠經過MouseEventArgs的GetPosition(IInputElement element)方法來獲取觸發事件時鼠標相對於控件的座標,而後能夠經過Visual的PointToScreen(Point p)方法將相對座標轉化爲相對於屏幕的座標,這個在一些交互中常常用到,固然也能夠經過P/Invoke方法調用Win32 API來獲取相對於屏幕座標。
鼠標單擊事件和鍵盤按鍵事件相似,區別是鼠標單擊分左右鍵。下面按順序列出事件:
這些事件都提供了MouseButtonEventArgs對象,它繼承自MouseEventArgs(有判斷鼠標是按下仍是釋放的MouseButtonState屬性,有獲取相對位置的GetPosition方法),MouseButtonEventArgs對象自身又增長了兩個屬性,一個是MouseButton屬性,用於判斷事件是由鼠標那個鍵觸發的;另外一個是ClickCount,用於判斷點擊次數,能夠用來判斷單擊雙擊。
某些元素又添加了更高級的鼠標事件,例如ButtonBase添加了Click事件,Control類添加了PreviewMouseDoubleClick事件和MouseDoubleClick事件。
另外,還提供了PreviewMouseWheel和MouseWheel鼠標滾輪滾動事件,它們提供了MouseWheelEventArgs對象,這個對象有個Delta的屬性,用於獲取指示鼠標滾輪變動量的值,若是鼠標滾輪朝上旋轉(背離用戶的方向),則該值爲正;若是鼠標滾輪朝下旋轉(朝着用戶的方向),則該值爲負。
能夠經過Mouse類來實時地獲取鼠標的相關信息。
一般狀況下,按下事件和釋放事件是成對被觸發的,可是也有另外,如單擊某個元素,保持按下狀態,而後移動鼠標指針離開該元素,這種狀況就不會觸發釋放的事件。當咱們想要在鼠標離開了元素後觸發仍然觸發釋放事件執行其它操做,就要先判斷該元素是否可用(IsEnabled="true",禁用的元素是沒法獲取捕獲鼠標的),而後經過UIElement.CaptureMouse()方法來捕獲鼠標,成功捕獲返回true,不然返回false,若是返回true,就會觸發GotMouseCapture和IsMouseCaptureChanged事件,並將事件數據中的RoutedEventArgs.Source報告爲調用 CaptureMouse 方法的元素。 若是強制執行捕獲,則可能會干擾現有捕獲,特別是與鼠標拖放有關的捕獲。若要從全部元素中清除鼠標捕獲,請用值爲 null 的 element 參數調用Mouse.Capture()方法。在UIElment和Mouse類中都有GotMouseCapture事件,它們存在這樣的關係:當UIElement做爲基元素繼承時,此事件會爲該類的Mouse.GotMouseCapture附加事件建立一個別名,以便 GotMouseCapture 包含在該類的成員列表中。 附加到 GotMouseCapture 事件的事件處理程序將附加到基礎Mouse.GotMouseCapture附加事件上,並接收同一事件數據實例。UIElement.LostMouseCapture事件也相似。
鼠標拖放是做爲一種快捷方便的方式來使用的,例如將垃圾文件拖放到回收站,將一個doc文檔拖放到打開的Word窗口來打開該doc文檔等。
通常,拖放操做分爲三個步驟:
1)用戶單擊某個元素,並保持鼠標鍵按下狀態,這時某些信息被擱置,拖放操做開始。
2)用戶將鼠標將鼠標移動到其它元素上,若是該元素能夠接受拖放的內容,則鼠標指針變爲拖放圖標,不然變爲拒絕操做圖標。
3)用戶釋放鼠標鍵時,該元素接受信息。按ESC可取消操做。
某些控件內部已經內置了拖放邏輯,例如TextBox,你能夠選中TextBox中的文本,將其拖放到另外一個TextBox中。
對於那些沒有內置拖放邏輯的控件來講,想要實現拖放,實現咱們來處理控件的拖放事件。下面來舉個例子:
Xaml代碼:
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="138*" /> <ColumnDefinition Width="140*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="99*" /> <RowDefinition Height="162*" /> </Grid.RowDefinitions> <TextBox Height="30" /> <TextBox Grid.Column="1" Height="30" /> <StackPanel x:Name="sp" Grid.Row="1" Button.PreviewMouseDown="StackPanel_PreviewMouseDown"> <Button Content="1" Background="Green"/> <Button Content="2" Background="Red" /> <Button Content="3" Background="Orange" /> <TextBlock Text="4" Background="AliceBlue" /> </StackPanel> <StackPanel x:Name="sp1" Grid.Row="1" Grid.Column="1" Background="Gray" AllowDrop="True" Drop="StackPanel_Drop"> </StackPanel> </Grid>
cs代碼:
//在PreviewMouseDown事件中調用DragDrop.DoDragDrop方法初始化拖放操做(建立源) private void StackPanel_PreviewMouseDown(object sender, MouseButtonEventArgs e) { Button button = e.Source as Button; if (button != null) DragDrop.DoDragDrop(button, button, DragDropEffects.Copy); } //將目標的AllowDrop設爲true,監聽其Drop事件 private void StackPanel_Drop(object sender, DragEventArgs e) { Button button = e.Data.GetData(typeof(Button)) as Button; this.sp.Children.Remove(button);//因爲拖放的Button已是sp的Child,故須要斷開與原容器的鏈接 this.sp1.Children.Add(button); }
效果以下:
重點是DragEventArgs對象的Data屬性,它是IDataObject接口類型,用於封裝拖放對象相關的信息,裏面有一些常常要的方法,如GetData/SetData方法,GetDataPresent方法。
若是想要達到過濾拖放內容的目標,應該使用DragEnter事件來判斷,最後交給Drop事件處理。
若是拖放操做時是在應用程序間進行的,並且源是複雜的對象,一種作法是經過序列化反序列化方式,另外一種是經過使用XamlWriter方法將WPF對象轉化爲Xaml,而後經過XamlReader方法再轉化爲WPF對象,實質也是第一種。
手寫筆是一種在平板電腦或其它觸屏設備上使用的相似筆的設備,它的行爲很像鼠標,能夠觸發MosueMove、MouseDown和MouseUp等事件,同時它還具備對應的事件StylusMove、StylusDown和StylusUp事件及它們的Preview事件,另外它還有一些特有的事件,例如:StylusInAirMove、StylusInRange、StylusOutOfRange和StylusSystemGesture事件,這些事件是手寫筆特有的事件。在InkCanvas中應用手寫筆事件,常常能夠實現不錯的效果。關於這些事件的詳細信息,可查看MSDN。
多點觸控和手寫筆不一樣的是,多點觸控支持同時多個手指操做甚至手勢(Gesture),win7上標準的手勢可查看MSDN,當前支持觸控的硬件列表可查看MSDN。
正如鼠標事件有高低層次同樣(Click及MouseDoubleClick等事件屬於高層次事件,而MouseDown及MouseUp等事件屬於低層次事件),多點觸控也有高低層次事件的區分。
1)原始觸控(raw touch):這是觸控的低級支持,都是一些單獨的觸控事件,不支持手勢。
2)操做(manipulation):這是一個對原始觸控的抽象層,支持手勢。WPF支持的通用的手勢包括移動(pan)、縮放(zoom)、旋轉(rotate)和輕按(tap)。
3)內置的元素支持(built-in element support):有些控件已經對多點觸控提供了內置支持,例如可滾動的控件支持觸控移動,如ListBox、ListView、DataGrid、TextBox和ScrollViewer。
原始觸控事件也像低級的鼠標鍵盤事件同樣,被封裝在UIElement和ContentElement之中。以下圖所示:
上面這些事件都提供了TouchEventArgs對象,這個對象有兩個重要的成員,一個GetTouchPoint方法(獲取觸控事件發生時觸控點的座標);一個是TouchDevice屬性。內部是將每一個觸點都當作是單獨的設備,會爲每一個觸點分配惟一的設備ID,根據TouchDevice.Id來區分觸點(手指)。
對於那些直接簡明的觸控,使用原始觸控就足夠了。可是,若是要方便地支持觸控手勢,例如旋轉,則須要觸控操做。經過將元素的IsManipulationEnabled屬性(定義在UIElement中)設爲true,則該元素可使用觸控操做,而後能夠響應四個事件:ManipulationStarting、ManipulationStarted、ManipulationDelta和ManipulationCompleted。它們都提供了不一樣的事件參數對象,每一個對象都有本身獨特的屬性和方法。具體能夠查看MSDN。
WPF中的慣性(Inertia)也是構建在低級事件之上的,它使得用戶體驗更好。例如,當用手指在划動一張圖片的時候,正常狀況下,當手指離開的時候圖片會當即中止。而當啓用了慣性屬性後,手指離開時圖片還會減速划動一段距離,並且,當圖片到邊界時還能夠產生一個反彈的效果。這須要監聽ManipulationInertiaStarting事件,這個事件提供了ManipulationInertiaStartingEventArgs對象,這個對象提供了延伸慣性行爲ExpansionBehavior、線性慣性行爲TranslationBehavior和旋轉慣性行爲RotationBehavior,經過設置相應Behavior的InitialVelocity初始速率和DesiredDeceleration預期減速度來實現相應慣性效果。另外,爲了使其接觸邊界時,可以天然彈回,須要在ManipulationDelta事件中調用ReportBoundaryFeedback()方法,將會觸發UIElement.ManipulationBoundaryFeedback事件,在該事件中,使應用程序或組件可以在對象到達邊界時提供可視反饋。還能夠調用事件參數的Cancel方法來取消操做,將再也不引起操做事件而且對於觸控將會觸發鼠標事件。
本節主要講了WPF的路由事件及附加事件,而後描述了WPF框架中的生命週期事件、鼠標事件、鍵盤事件、手寫筆事件和多點觸控事件,細節不少,但都有跡可循。