WPF進階技巧和實戰09-事件(1-路由事件、滑鼠鍵盤輸入)
理解路由事件
當有意義的事情發生時,有對象(WPF的元素)發送的用於通知程式碼的消息,就是事件的核心思想。WPF通過事件路由的概念增強了.NET事件模型。事件由允許源自某個元素的事件由另一個元素引發。例如:使用路由事件,來自工具欄按鈕的單擊事件可在被程式碼處理前上傳到工具欄,然後上傳到包含工具欄的窗口。
事件路由為在最合適的位置編寫緊湊的、組織良好的用於處理事件的程式碼提供了靈活性。要使用WPF內容模型,事件路由是必須的,內容模型允許使用許多不同的元素構建簡單元素,並且這些元素都擁有自己獨立的事件集合。
定義、註冊和封裝路由事件
路由事件由只讀的靜態欄位表示,在靜態構造函數中註冊,並通過標準的.NET事件定義進行封裝。當註冊路由事件時,需要指定事件的名稱、路由類型、定義事件處理程式語法的委託以及擁有事件的類。通常,路由事件通過普通的.NET事件進行封裝,從而使左右的.NET語言都能夠訪問她們,事件封裝器可使用AddHandler、RemoveHandler添加和刪除已註冊的調用程式,這兩個方法定義在FrameworkElement基類中,可以被每個WPF元素繼承。例如:
// 聲明並註冊路由事件
public static readonly RoutedEvent PointPositionChangedEvent =
EventManager.RegisterRoutedEvent("PointPosition", RoutingStrategy.Bubble,
typeof(EventHandler<PointChangedEvnetArgs>), typeof(TopoGraphyAnalysisItemControl));
public static readonly RoutedEvent LinePositionChangedEvent =
EventManager.RegisterRoutedEvent("LinePosition", RoutingStrategy.Bubble,
typeof(EventHandler<PointChangedEvnetArgs>), typeof(TopoGraphyAnalysisItemControl));
// 為路由事件添加CLR事件包裝器
public event RoutedEventHandler PointPositionChanged
{
add { this.AddHandler(PointPositionChangedEvent, value); }
remove { this.RemoveHandler(PointPositionChangedEvent, value); }
}
public event RoutedEventHandler LinePositionChanged
{
add { this.AddHandler(LinePositionChangedEvent, value); }
remove { this.RemoveHandler(LinePositionChangedEvent, value); }
}
共享路由事件
和依賴屬性一樣,可在類之間共享路由事件的定義。例如,UIElement(所有普通WPF元素的起點)和ContentElement(所有內容元素的起點)這兩個基類都使用了MouseUp事件。MouseUp事件是由System.Window.Input.Mouse類定義的。這兩個類知識通過RoutedEvent.AddOwner方法重用了MouseUp事件。
// 聲明並註冊路由事件
public static readonly RoutedEvent PointPositionChangedEvent =
TopoGraphyAnalysisItemControl.PointPositionChangedEvent.AddOwner(typeof(TopoGraphyAnalysisDiffControl));
public static readonly RoutedEvent LinePositionChangedEvent =
TopoGraphyAnalysisItemControl.LinePositionChangedEvent.AddOwner(typeof(TopoGraphyAnalysisDiffControl));
引發路由事件
路由事件不是通過傳統的.NET事件封裝器引發的,而是使用RaiseEvent方法引發事件,所有元素都從UIElement類繼承了該方法。
PointChangedEvnetArgs evnetArgs =
new PointChangedEvnetArgs(PointPositionChangedEvent,
ctrl, ctrl.AnalysisResult?.Result?.Id, offset);
ctrl.RaiseEvent(evnetArgs);
RaiseEvent()方法負責每個已經通過Addhandler()方法註冊的調用程式引發事件。因為AddHandler()方法是公有的,所以調用程式可訪問該方法(直接調用AddHandler()方法註冊他們自己,也可以使用事件封裝器),當調用RaiseEvent()方法時都會通知他們。
所有WPF事件都為事件簽名使用熟悉的.NET約定。每個事件處理程式的第一個參數(sender)都提供引發該事件的對象的引用。第二個參數是EventArgs對象,該對象與其他所有可能很重要的附加細節綁定在一起。
//添加窗體接收事件,接收控制項發出的點變化消息,通過參數更新所有介面中的點數據
this.AddHandler(TopoGraphyAnalysisItemControl.PointPositionChangedEvent, new EventHandler<PointChangedEvnetArgs>(
(object sender, PointChangedEvnetArgs e) =>
{
}));
如果時間不需要傳遞任何額外的細節,可使用RoutedEventArgs類,如果需要傳遞額外的資訊,可以繼承自RoutedEventArgs類的對象。
處理路由事件
事件的處理方式有很多種,最常用的方法是XAML標記添加事件特性。也可以使用程式碼連接事件,效果等同於XAML標記
public CustomWindow()
{
this.SizeChanged += CustomWindow_SizeChanged;
}
事件路由
路由事件在實際中以3種方式出現:
- 與普通.NET事件類似的直接路由事件。他們源於一個元素,不傳遞給其他元素。例如,MouseEnter事件是直接路由事件(滑鼠移動到元素上時發生)
- 在包含層次中向上傳遞的冒泡路由事件。例如MouseDown事件,該事件首先由被單擊的元素引發,接下來該元素的父元素引發,然後沿著元素樹傳遞到頂部位置
- 在包含層次中向下傳遞的隧道路由事件。隧道路由事件在事件到達恰當的控制項之前為預覽事件/終止事件提供了機會。例如,通過PreviewKeyDown事件可截獲是否按下某個鍵。首先在窗口級別上,然後是更具體的容器,直至到達按下鍵時具有焦點的元素。
使用EventManager註冊路由事件時,需要傳遞一個枚舉值RoutingStrategy,該值用於指示希望應用於事件的事件行為。
RoutedEventArgs類
在處理冒泡路由事件是,sender參數提供了對整個鏈條上最後那個鏈接的引用。有些情況下,可能希望確定事件最初發生的位置。可從RoutedEventArgs類的屬性獲得這些資訊。
名稱 | 說明 |
---|---|
Source | 引發事件的對象。對於鍵盤事件,是當事件發生時(比如按下鍵盤上的鍵)具有焦點的控制項。對於滑鼠事件,是當事件發生時(如單擊滑鼠按鈕)滑鼠指針下面所有元素中最靠上的元素 |
OriginalSource | 指示最初是什麼對象引發了事件。通常和Source屬性值相同。在某些情況下,本屬性指向對象樹中更深的層次,以獲得作為更高一級元素一部分的後台元素。比如,如果單擊窗口邊框上的關閉按鈕,事件源是Window對象,但事件最原始的源是Border對象。這是因為Window對象是由多個單獨的更小的部分組成 |
RoutedEvent | 通過事件處理程式為觸發的事件提供RoutedEvent對象。如果用同一個事件處理程式處理不同的事件,這一資訊就非常有用了 |
Handled | 改屬性允許終止事件的冒泡或者隧道過程。如果控制項將Handle屬性設置為true,那麼事件就不會繼續傳遞,也不會再為其他任何元素所看到 |
冒泡路由事件
創建測試窗口,將元素層析結構中的影像以及它上面的每個元素都關聯到同一個事件處理程式中
<Window x:Class="RoutedEvents.BubbledLabelClick"
xmlns="//schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="//schemas.microsoft.com/winfx/2006/xaml"
Title="BubbledLabelClick" Height="359" Width="329"
MouseUp="SomethingClicked"
>
<Grid Margin="3" MouseUp="SomethingClicked">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Label Margin="5" Background="AliceBlue" BorderBrush="Black" BorderThickness="1" MouseUp="SomethingClicked" HorizontalAlignment="Left" >
<StackPanel MouseUp="SomethingClicked" >
<TextBlock Margin="3" MouseUp="SomethingClicked" >
Image and picture label</TextBlock>
<Image Source="happyface.jpg" Stretch="None"
MouseUp="SomethingClicked" />
<TextBlock Margin="3"
MouseUp="SomethingClicked" >
Courtesy of the StackPanel</TextBlock>
</StackPanel>
</Label>
<ListBox Margin="5" Name="lstMessages" Grid.Row="1"></ListBox>
<CheckBox Margin="5" Grid.Row="2" Name="chkHandle">Handle first event</CheckBox>
<Button Click="cmdClear_Click" Grid.Row="3" HorizontalAlignment="Right" Margin="5" Padding="3">Clear List</Button>
</Grid>
</Window>
處理事件:
private void SomethingClicked(object sender, RoutedEventArgs e)
{
eventCounter++;
string message = "#" + eventCounter.ToString() + ":\r\n" +
" Sender: " + sender.ToString() + "\r\n" +
" Source: " + e.Source + "\r\n" +
" Original Source: " + e.OriginalSource;
lstMessages.Items.Add(message);
e.Handled = (bool)chkHandle.IsChecked;
}
private void cmdClear_Click(object sender, RoutedEventArgs e)
{
eventCounter = 0;
lstMessages.Items.Clear();
}
因為SomethingClicked()方法處理由Window對象引發的MouseUp事件,所以也能截獲在列表框和窗口表面空白處的滑鼠單擊事件。但當單擊Clear按鈕時,不會引發MouseUp事件,這是因為按鈕包含了一些有趣的程式碼,這些程式碼會掛起MouseUp事件,並引發更高級的Click事件。同時,Handled標誌被設置為true,從而會阻止MouseUp事件繼續傳遞。大多數WPF元素沒有提供Click事件,而是提供MouseDown和MouseUp事件,Click事件專門用於按鈕的控制項。
處理掛起路由事件
有一種辦法可以接收被標記為處理過的事件。使用AddHandler()方法,使用其重載方法,第三個參數傳遞true,即使設置了Handled標誌,也將接收到事件。
cmd.AddHandler(Button.MouseUpEvent, new RoutedEventHandler(Backdoor), true);
附加事件
並不是所有的元素都支援MouseUp事件。按鈕就是一個例子,它添加了Click事件,而其他任何基類都沒有定義該事件。假設在StackPanel面板中封裝一堆按鈕,並希望在一個事件處理程式中處理所有這些按鈕的單擊事件。最簡單粗暴的形式是每個按鈕都添加事件,並關聯到同一個處理程式。但是Click事件支援冒泡,從而可以使用一個更加巧妙的辦法,在更高層次的元素中處理Click事件。又因為StackPanel沒有Click事件,所以採用附加事件的形式實現,這樣事件處理程式就可以被StackPanel面板接收了。
<Grid Margin="3" Button.Click="cmdClear_Click">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
Click事件實際是ButtonBase類中定義的,而Button類繼承了該事件。如果為ButtonBase.Click事件關聯處理程式,那麼當單擊任何繼承自ButtonBase的控制項時(Button類,RadioButton類以及CheckBox類)時,都會調用該事件處理程式。
隧道路由事件
隧道路由事件的工作方式和冒泡路由事件相同,但是方向相反。隧道路由事件易於識別,都是以Preview開頭。而且,WPF通常成對定義冒泡事件和隧道路由事件。隧道路由事件總是在冒泡路由事件之前被觸發。如果將隧道路由事件標記為已處理過,那麼就不發生冒泡路由事件(他們兩個共享同一個RoutedEventArgs類的實例)。
如果需要執行一些預處理(根據鍵盤上特定的鍵執行動作或者過濾掉特定的滑鼠動作),隧道路由事件是非常有用的。
WPF事件
生命周期事件
當首次創建以及釋放所有元素時都會引發事件,可使用這些事件初始化窗口。他們都是在FrameworkElement類中定義的。
名稱 | 說明 |
---|---|
Initialized | 當元素被實例化,並已根據XAML標記設置了元素的屬性之後發生。這時元素已經初始化,但窗口的其他部分可能尚未初始化。此外,尚未應用樣式和數據綁定。這時,IsInitialized屬性為true。不是路由事件 |
Loaded | 當整個窗口已經初始化並應用了樣式和數據綁定時,該事件發生。這是在元素被呈現之前的最後一站。這時,IsLoaded屬性為true |
Unloaded | 當元素被釋放時,該事件發生。原因是包含元素的窗口被關閉或者特定的元素被從窗口中刪除 |
為了弄清Initialized事件和Loaded事件之間的關係,分析呈現過程是有幫助的。FrameworkElement類實現了ISupportInitialized介面,改介面提供了兩個控制初始化過程的方法。第一個方法是BeginInit(),在實例化元素後立即調用該方法,然後XAML解析器設置所有元素的屬性(並添加內容)。第二個方法是EndInit(),完成初始化後,將調用該方法,此時引發Initialized事件。
當創建窗口時,會自下而上地初始化每個元素分支。這意味著,位於深層的嵌套元素在他的容器之前被初始化。當引發初始化事件時,可確保元素樹中當前元素以下的元素已經全部完成了初始化。但是,包含當前元素的元素可能還沒有初始化,並且不能假定窗口的任何其他部分已經初始化。(可以理解為,初始化只是代表自己和子元素全部初始化完成,其餘的都是不確定)。
在每個元素都初始化完成之後,還需要在他們的容器中進行布局、應用樣式。如果需要的話,還會綁定到數據源。當引發窗口的Initialized事件後,就可以進入下一個階段了。
一旦完成了初始化過程,就會引發Loaded事件。包含所有元素的窗口最先引發Loaded事件,然後才是更深層的嵌套元素。為所有元素都引發Loaded事件之後,窗口就可見了,並且元素都已經被呈現。
Window類的生命周期事件
名稱 | 說明 |
---|---|
SourceInitialized | 當獲取窗口的HwndSource屬性時(在窗口可見之前)發生。HwndSource是窗口句柄,調用Win32 API時會用到 |
ContentRendered | 在窗口首次呈現後立即發生。該事件表明窗口已經完全可見,並且已經準備好接收輸入 |
Activated | 當用戶切換到該窗口時發生(例如,從應用程式的其他窗口或者從其他應用程式切換到該窗口時),當窗口第一次載入時也會引發該事件。 |
Deactivated | 當用戶從該窗口切換到其他窗口時發生(例如,切換到應用程式的其他窗口或者切換到其他應用程式),當用戶關閉窗口時也會發生該事件,該事件在Closing事件之後,但在Closed事件之前發生。 |
Closing | 當關閉窗口時發生,不管是用戶關閉創建偶還是通過程式碼調用Window.Close()或者Application.Shutdown()方法關閉窗口,closing事件提供了取消操作並保持打開狀態的機會,具體通過將CancelEventArgs.Cancel屬性設置為true實現改目標。但是,如果是因為用戶關閉或者註銷電腦導致應用程式被關閉,就不能收到Closing事件。為應對這種情況,需要處理Application.SessionEnding事件 |
Closed | 當窗口已經關閉後發生。但是,此時認可訪問元素對象,當然是在Unloaded事件尚未發生前。在此處,可以執行一些清理工作,向永久存儲位置寫入設置資訊等。 |
輸入事件
輸入事件是用戶通過某些種類的外設硬體進行交互時發生的事件,例如滑鼠、鍵盤、手寫筆、多點觸控螢幕。輸入事件通過繼承自InputEventAegs的自定義事件參數傳遞額外的資訊。InputEventArgs類只增加了兩個屬性:Timestamp和Device。Timestamp屬性是一個整數,指事件發生時的毫秒數。Device屬性返回一個對象,該對象提供與觸發事件的設備相關的更多資訊。
鍵盤輸入
當用戶按下鍵盤上的一個鍵時,就會發生一系列事情。按照發生的順序是:
名稱 | 路由類型 | 說明 |
---|---|---|
PreviewKeyDown | 隧道 | 當按下一個鍵時發生 |
KeyDown | 冒泡 | 當按下一個鍵時發生 |
PreviewTextInput | 隧道 | 當按鍵完成並且元素正在接收文本輸入時發生。對於那些不會產生輸入的按鍵(Ctrl,Shift,Backspace,方向鍵,功能鍵等)不會引發該事件 |
TextInput | 冒泡 | 同上 |
PreviewKeyUp | 隧道 | 當釋放一個按鍵式發生 |
KeyUp | 冒泡 | 同上 |
鍵盤處理是一個複雜的過程。一些空間可能會掛起這些事件中的某些事件,從而可執行自己更特殊的鍵盤處理。最明顯的例子就是TextBox控制項,它掛起了TextInput事件。對於一些鍵,TextBox控制項還掛起了KeyDown事件(方向鍵)。對於這些情況,可以使用隧道路由事件。
TextBox控制項還添加了TextChanged事件,在按鍵導致文本框文本發生變化後立即引發該事件。這時,在文本框中已經可以看到新的文本了,所以阻止不需要的按鍵已經晚了,可以使用隧道事件進行處理。
處理按鍵事件
按鍵輸入例子:
<Window x:Class="RoutedEvents.KeyPressEvents"
xmlns="//schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="//schemas.microsoft.com/winfx/2006/xaml"
Title="KeyPressEvents" Height="387" Width="368"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<DockPanel Margin="5">
<TextBlock Margin="3" >
Type here:
</TextBlock>
<TextBox PreviewKeyDown="KeyEvent" KeyDown="KeyEvent"
PreviewKeyUp="KeyEvent" KeyUp="KeyEvent"
PreviewTextInput="TextInput"
TextChanged="TextChanged"></TextBox>
</DockPanel>
<ListBox Margin="5" Name="lstMessages" Grid.Row="1"></ListBox>
<CheckBox Margin="5" Name="chkIgnoreRepeat" Grid.Row="2">Ignore Repeated Keys</CheckBox>
<Button Click="cmdClear_Click" Grid.Row="3" HorizontalAlignment="Right" Margin="5" Padding="3">Clear List</Button>
</Grid>
</Window>
public partial class KeyPressEvents : System.Windows.Window
{
public KeyPressEvents()
{
InitializeComponent();
}
private void KeyEvent(object sender, KeyEventArgs e)
{
if ((bool)chkIgnoreRepeat.IsChecked && e.IsRepeat) return;
string message = //"At: " + e.Timestamp.ToString() +
"Event: " + e.RoutedEvent + " " +
" Key: " + e.Key;
lstMessages.Items.Add(message);
}
private void TextInput(object sender, TextCompositionEventArgs e)
{
string message = //"At: " + e.Timestamp.ToString() +
"Event: " + e.RoutedEvent + " " +
" Text: " + e.Text;
lstMessages.Items.Add(message);
}
private void TextChanged(object sender, TextChangedEventArgs e)
{
string message =
"Event: " + e.RoutedEvent;
lstMessages.Items.Add(message);
}
private void cmdClear_Click(object sender, RoutedEventArgs e)
{
lstMessages.Items.Clear();
}
}
通過上面的例子,每次按下一個鍵時,都會觸發PreviewKeyDown和PreviewKeyUp事件。但是只有當字元可以「輸入」到元素中時,才會觸發TextInput事件。當輸入大寫字母S時,需要按下兩個按鍵,先按下Shift鍵,再按下S鍵,因此可以看到兩個KeyDown和KeyUp事件,但是只有一個TextInput事件。
PreviewKeyDown、KeyDown、PreviewKeyUp、KeyUp事件都是通過KeyEventArgs對象提供了相同的資訊。最重要的資訊是Key屬性。該屬性返回一個System.Windows.Input.Key枚舉值,該枚舉值標誌了按下和釋放的鍵。
Key值沒有考慮任何其他鍵的狀態。例如當按下S鍵事不必關心當前是否按下Shift鍵,不管是否按下Shift鍵,都會得到相同的Key值(Key.S)
根據Windows鍵盤的設置,持續按下一個鍵一段時間,會重複引發按鍵事件。例如,保持按下S鍵,顯然會在文本框中輸入一系列S字元。同樣,按下Shift鍵一段時間也會的到多個按鍵和一系列KeDown事件。按下Shift+S鍵進行測試的真實情況是,文本框實際上會為Shift鍵引發一系列KeyDown事件,然後為S鍵引發KeyDown事件,隨後是TextInput時間(對於文本框,是TextChanged事件),最後是為Shift鍵和S鍵引發KeyUp事件。如果希望忽略這些重複的Shift鍵,可以通過檢查KeyEventArgs.IsRepeat屬性,確定按鍵是不是因為按住鍵導致的結果。
理想情況下,可在控制項(TextBox控制項中)使用PreviewTextInput事件執行驗證工作(比如智慧輸入數字的文本框),可確保當前按鍵不是字母,如果是就設置Handled標誌為true。有些鍵可能不會觸發PreviewTextInput事件,例如在文本框按下空格鍵,這時還需要處理PreviewKeyDown事件。最好的辦法是兩個事件都要進行處理,PreviewTextInput事件負責大多數驗證,PreviewKeyDown用於那些在文本框中不會引發PreviewTextInput事件的按鍵。可將這些事件管理到單個文本框,或者更高層次的容器。
焦點
在windows中,用戶每次只能使用一個控制項。當前接收用戶按鍵的是具有焦點的控制項。為讓控制項能夠接受焦點,必須將Focusable屬性設置為true,默認值就是true。可以通過Tab鍵來切換焦點的位置,如果使用了IsTabStop屬性並設置了false,則阻止控制項被包含在Tab鍵焦點範圍內。
獲取鍵盤狀態
當發生按鍵事件時,經常需要知道按下的哪個鍵,而且需要確定其他鍵是否被按下,也很重要。對於鍵盤事件(PreviewKeyDown、KeyDown,PreviewKeyUpKeyUp),獲取這些資訊比較容易。首先KeyEventArgs對象包含了KeyStatus屬性,該屬性反應觸發事件的鍵的屬性。更有用的是,keyboardDevice屬性為鍵盤上的所有鍵提供了相同的資訊(包含當前元素是否具有焦點,以及當前事件發生時按下了哪些修飾鍵,並且可以使用位邏輯來檢查他們的狀態。KeyboardDevice屬性的方法:
名稱 | 說明 |
---|---|
IsKeyDown() | 當事件發生時,通知是否按下該鍵 |
IsKeyUP() | 當事件發生時,通知是否釋放該鍵 |
IsKeyToggled() | 當事件發生時,通知該鍵是否處於打開狀態,該方法只對那些能夠打開、關閉的鍵有意義,CapsLock、ScroolLock,NumLock |
GetkeyStatues() | 返回一個或者多個KeySttues枚舉值,指明該鍵當前是否被釋放了,按下了,或者處於切換狀態。該方法本質上和為同一個鍵同時調用IsKeyDown()方法和IsKeyToggled()方法相同 |
滑鼠輸入
滑鼠事件執行幾個關聯任務。MouseEnter(當滑鼠指針移到元素上時引發該事件)、MouseLeave(當滑鼠離開元素時引發該事件)。這兩個事件是直接事件,不使用冒泡或者隧道過程,而是源自一個元素且只被該元素引發。
例如:在一個StackPanel面板上放置一個按鈕Button,並將滑鼠指針移到按鈕上,那麼首先會為這個StackPanel面板引發MouseEnter事件(當滑鼠指針進入StackPanel面板邊界時),然後為按鈕引發MouseEnter事件(當滑鼠指針移到按鈕上時),將滑鼠指針移開時,首先為按鈕引發MouseLeave事件,然後是按鈕。還可以響應PreviewMouseMove事件(隧道事件)和MouseMove事件(冒泡事件),只要移動滑鼠就會引發這兩個事件。所有這些事件都提供了MouseEventArgs對象,此對象包含事件引發時標識滑鼠鍵狀態的屬性,以及GetPosition()方法返回相對所選元素的滑鼠坐標。
滑鼠單擊
滑鼠單擊事件的引發方式和按鍵事件的引發方式有類似之處。區別是對於滑鼠左鍵和滑鼠右鍵引發不同的事件。根據引發順序列出事件,其餘還有滑鼠滾輪事件:PreviewMouseWheel和MouseWheel
名稱 | 路由類型 | 說明 |
---|---|---|
PreviewMouseLeftButtonDown PreviewMouseRightButtonDown |
隧道 | 當按下滑鼠鍵時發生 |
MouseLeftButtonDown MouseRightButtonDown |
冒泡 | 當按下滑鼠鍵時發生 |
PreviewMouseLeftButtonUp PreviewMouseRightButtonUp |
隧道 | 當釋放滑鼠鍵時發生 |
MouseLeftButtonUp MouseRightButtonUp |
冒泡 | 當釋放滑鼠鍵時發生 |
所有滑鼠事件都提供MouseButtonEventArgs對象,繼承自MouseEventArgs類(包含相同的坐標和按鈕狀態資訊),還包含MouseButton(用於通知是哪個滑鼠鍵引發的事件),ButtonState(通知當事件發生時,滑鼠鍵是處於按下狀態還是釋放狀態),ClickCount(用於通知滑鼠鍵被單擊了幾次,可以區分是單擊還是雙擊)
通常的做法是,單擊滑鼠時,Windows程式對滑鼠鍵的釋放事件進行響應(Up事件,而不響應down事件)。
某些元素添加了高級的滑鼠事件,Control類添加了PreviewMouseDoubleClick事件和MouseDoubleClick事件,這兩個事件代替了MouseLeftButtonUp事件。類似處理,對於Button類,通過滑鼠和鍵盤可觸發Click事件。
與鍵盤按鍵事件一樣,當發生滑鼠事件時,這些事件提供了有關滑鼠位置和哪個滑鼠鍵被按下的資訊。為獲得當前滑鼠位置和按鍵狀態,可使用Mouse類的靜態成員,他們和MouseButtonEventArgs類的成員類似。
捕獲滑鼠
有一種情況,如果單擊一個元素,保持按下滑鼠鍵,然後移動滑鼠指針離開該元素,這時這個元素就不會接收滑鼠鍵釋放事件。這種情況下如果需要通知滑鼠釋放事件,就需要調用Mouse.Capture()方法並傳遞恰當的元素以捕獲滑鼠。此後,就會接收到滑鼠按鍵按下事件和釋放事件,直到再次調用Mouse.Capture()方法並傳遞空引用位置。當滑鼠被一個元素捕獲後,其他元素就不會接收到滑鼠事件。這意味著永不能單擊窗口中的其他位置按鈕,不能單擊文本框的內部。滑鼠捕獲有時用於被拖放並可以改變尺寸的元素時。
在有些情況下,可能會丟失滑鼠捕獲,如需要顯示系統對話框,Windows可能會釋放滑鼠捕獲,就可以通過處理LostMouseCapture事件來響應滑鼠不會的丟失。
當滑鼠被一個元素捕獲時,就不能與其他元素進行交互。滑鼠捕獲通常用於短時間的操作(拖放,滑動等)。一般不使用Mouse.Capture()方法,而是使用UIElement類提供的兩個方法CaptureMouse()和ReleaseMouseCapture()。
滑鼠拖放
拖放操作一般是:拖動資訊使其離開窗口中的某個位置,然後將其放到其他位置的技術。本質上,需要3個步驟:
- 用戶單擊元素(或者選擇元素中的一塊特定區域),並保持滑鼠鍵為按下狀態。這時,某些資訊被擱置起來,並且拖放操作開始。
- 用戶將滑鼠移動到其他元素上。如果該元素可接受正在拖動的內容的類型,滑鼠指針會變成拖放圖標,否則滑鼠指針會變成內部有一條線的圓形
- 當用戶釋放滑鼠鍵時,元素接收資訊並決定如何處理接收到的資訊。在沒有釋放滑鼠鍵時,可按下Esc鍵取消該操作。
可在窗口中添加兩個文本框來嘗試拖放操作支援的工作方式,因為TextBox控制項提供了支援拖放的內部邏輯。如果選中文本框中的一些文本,就可以將這些文本拖動到兩一個文本框中。當釋放滑鼠鍵時,這些文本將移動位置。同一技術在兩個應用程式之間也可以操作(可以將在word文檔中拖動一些文本,並放入到WPF的TextBox對象中,也可以將文本從WPF的TextBox對象拖動到word文檔中。
有時,希望在兩個未提供內置拖放功能的元素之間進行拖放。例如,用戶將內容從文本框拖放到標籤中,或者從Lable對象或者TextBox對象拖動文本,放到另一個標籤中。用於拖放操作的方法和事件都在System.Windows.DragDrop類中。
<Window x:Class="RoutedEvents.DragAndDrop"
xmlns="//schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="//schemas.microsoft.com/winfx/2006/xaml"
Title="DragAndDrop" Height="257.6" Width="392.8"
>
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Padding="10" VerticalAlignment="Center" HorizontalAlignment="Center">Drag from this TextBox</TextBox>
<Label Grid.Column="1" Padding="20" Background="LightGoldenrodYellow"
VerticalAlignment="Center" HorizontalAlignment="Center"
MouseDown="lblSource_MouseDown">Or this Label</Label>
<Label Grid.Row="1" Grid.ColumnSpan="2" Background="LightGoldenrodYellow"
VerticalAlignment="Center" HorizontalAlignment="Center" Padding="20"
AllowDrop="True" Drop="lblTarget_Drop">To this Label</Label>
</Grid>
</Window>
public partial class DragAndDrop : System.Windows.Window
{
public DragAndDrop()
{
InitializeComponent();
}
private void lblSource_MouseDown(object sender, MouseButtonEventArgs e)
{
Label lbl = (Label)sender;
DragDrop.DoDragDrop(lbl, lbl.Content, DragDropEffects.Copy);
}
private void lblTarget_Drop(object sender, DragEventArgs e)
{
((Label)sender).Content = e.Data.GetData(DataFormats.Text);
}
private void lblTarget_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.Text))
e.Effects = DragDropEffects.Copy;
else
e.Effects = DragDropEffects.None;
}
}
操作拖放有源和目標兩方面。為了創建拖放源,需要在某個位置調用DragDrop.DoDragDrop()方法來初始化拖放操作。然後擱置希望拖放的內容,並指明允許什麼樣的拖放效果(複製、移動等)。通常在響應MouseDown或者PreviewDropDown事件時調用DragDrop.DoDragDrop方法,上面的例子是在單擊標籤時初始化拖放操作,標籤中的文本內容用於拖放動作。
接收數據的元素需要將他的AllowDrop屬性設置為true(允許任何類型的資訊),還需要處理Drop事件來處理數據。如果希望選擇性地接收內容,可以處理DrapEnter事件。可以在DrapEnter事件中過濾需要處理的類型。
最後,當完成操作後就可以檢索並處理數據了。將拖放的文本插入標籤中。
明確一點,拖放操作可以交換任意類型的對象。如果希望和其他應用程式通訊,應使用基本數據類型(字元串或者整形等),或者使用實現了Iserializable或者IdataObject介面的對象(這兩個介面將對象轉換成位元組流,並在另一個應用程式中重構對象)如果希望在兩個應用程式之間傳遞數據,那麼務必檢查System.Windows.Clipboard類,用於在Windows剪切板中放置數據,並以各種不同的格式檢索剪切板中的數據。