1. 程式人生 > 其它 >WPF進階技巧和實戰09-事件(1-路由事件、滑鼠鍵盤輸入)

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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://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個步驟:

  1. 使用者單擊元素(或者選擇元素中的一塊特定區域),並保持滑鼠鍵為按下狀態。這時,某些資訊被擱置起來,並且拖放操作開始。
  2. 使用者將滑鼠移動到其他元素上。如果該元素可接受正在拖動的內容的型別,滑鼠指標會變成拖放圖示,否則滑鼠指標會變成內部有一條線的圓形
  3. 當用戶釋放滑鼠鍵時,元素接收資訊並決定如何處理接收到的資訊。在沒有釋放滑鼠鍵時,可按下Esc鍵取消該操作。

可在視窗中新增兩個文字框來嘗試拖放操作支援的工作方式,因為TextBox控制元件提供了支援拖放的內部邏輯。如果選中文字框中的一些文字,就可以將這些文字拖動到兩一個文字框中。當釋放滑鼠鍵時,這些文字將移動位置。同一技術在兩個應用程式之間也可以操作(可以將在word文件中拖動一些文字,並放入到WPF的TextBox物件中,也可以將文字從WPF的TextBox物件拖動到word文件中。

有時,希望在兩個未提供內建拖放功能的元素之間進行拖放。例如,使用者將內容從文字框拖放到標籤中,或者從Lable物件或者TextBox物件拖動文字,放到另一個標籤中。用於拖放操作的方法和事件都在System.Windows.DragDrop類中。

<Window x:Class="RoutedEvents.DragAndDrop"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://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剪下板中放置資料,並以各種不同的格式檢索剪下板中的資料。