在Windows應用程序中,又3種基本的用戶輸入形式:鼠標、鍵盤和手寫板。 同時,還有一種更高級輸入方式,其可能來自快捷鍵、工具欄的按鈕、菜單項。
盡管控件擔當著主要的輸入對象,用戶界面的所有元素都可以接受輸入。不 必吃驚,這是因為,為了提供外觀,控件完全依賴於底層元素的服務,如 Rectangle和TextBlock。因此,在用戶界面內的元素類型中,所有的輸入機制都 是有用的,我們將要在接下來的章節介紹這些機制。
3.2.1 Routed事件
.Net框架定義了一個標准的機制來暴露事件。一個類可能暴露了一些事件, 每個事件可能有任意數量的訂閱者。雖然WPF也使用了這一標准機制,聲稱其克 服了一個局限:如果一個正常.NET事件沒有注冊句柄,該事件將被視為無效並忽 略。
考慮一下這對於一個典型的WPF控件意味著什麼。大多數控件是由多個可視化 組件組成的。例如,即使你為一個按鈕添加了一個非常簡單的可視化樹,這棵樹 包括一個單獨的矩形框,以及一條簡單的文本,目前有兩個元素:文本和矩形框 。不管光標是否在文本或矩形框上,這個按鈕都要響應鼠標點擊事件。在標 准.NET事件處理模型中,這意味著要為所有元素注冊MouseLeftButtonUp事件。
更嚴重的是使用WPF內容模型。一個按鈕並不局限於只有簡單文本作為標題, 它可以包含任意標簽。示例3-2是一個相當普通的情況,但即使如此,其中仍然 有6個元素:黃色的邊框,代表眼睛的兩個點,代表嘴的曲線,文本,以及作為 背景的按鈕本身。為每一個單獨元素關聯事件句柄關聯,是煩冗而且效率低下的 。幸運的是,這些並不是必需的。
圖3-2
WPF使用routed事件,該事件比其他普通事件更為直接。原先的機制是,將委 托句柄關聯到激發該事件的元素,調用該句柄。如今,一個rounted事件會調用 所有的關聯到已知代碼的句柄,從初始元素向上直到用戶界面書的根元素。
示例3-1顯示了圖3-2中按鈕的標記。如果Canvas中的一個Elliipse元素接收 到輸入,事件路由可以支持Button、Grid、Canvas和Ellispse接收事件,如圖3 -3所示。
示例3-1
圖3-3
<Button MouseLeftButtonDown="MouseButtonDownButton"
PreviewMouseLeftButtonDown="PreviewMouseButtonDownButton">
<Grid MouseLeftButtonDown="MouseButtonDownGrid"
PreviewMouseLeftButtonDown="PreviewMouseButtonDownGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Canvas MouseLeftButtonDown="MouseButtonDownCanvas"
PreviewMouseLeftButtonDown="PreviewMouseButtonDownCanvas"
Width="20" Height="18" VerticalAlignment="Center">
<Ellipse MouseLeftButtonDown="MouseButtonDownEllipse"
PreviewMouseLeftButtonDown="PreviewMouseButtonDownEllipse"
Canvas.Left="1" Canvas.Top="1" Width="16" Height="16"
Fill="Yellow" Stroke="Black" />
<Ellipse Canvas.Left="4.5" Canvas.Top="5" Width="2.5" Height="3"
Fill="Black" />
<Ellipse Canvas.Left="11" Canvas.Top="5" Width="2.5" Height="3"
Fill="Black" />
<Path Data="M 5,10 A 3,3 0 0 0 13,10" Stroke="Black" />
</Canvas>
<TextBlock Grid.Column="1">Foo</TextBlock>
</Grid>
</Button>
一個路由事件可以是bubbling,tunneling或Direct的。Bubbling事件以尋找 附屬到激發事件的事件句柄開始,接著尋找它的父級別,再接著是它的父級別的 父級別,依次類推,直到達到這棵樹的根,這個順序是由圖3-3的數字表明的。 Tunneling事件以相反的方式工作。它先在樹根尋找句柄,接著向下開始工作, 以原始的元素作為結束。
Direct事件的路由方式與傳統的.NET事件處理相同,只有直接附屬到原始元 素的句柄會被通知到。這典型地用於僅在它們的原始元素的上下文中有意義的那 些事件。例如,如果鼠標的進入和移開是bubbled或tunneled的,這將是無用的 。父級元素未必會關心何時鼠標從一個元素移動到另一個元素。在父一級元素, 你可能希望“鼠標移開”意味著“鼠標已經離開了父一級元素”,因為使用了 Direct事件路由,這才是它正確地意味著什麼。一旦使用bubbiling,事件將有 效的意味著“鼠標已經離開了這個元素,可能在或不在其父一級內的另一個元素 中”。
除direct事件之外,WPF還定義了很多成對(bubbling和tunneling)的路由 事件。Tunneling事件的名稱通常以Preview開始,而且會首先被激發。這將給原 始元素的父級一個看一下事件的機會,在到達其子級別之前(因此以“Preview ”為前綴。)tunneling的Preview事件直接遵循bubbling事件。在大多數情形中 ,你將要處理bubbling事件,preview事件只用於你想要阻塞一個事件時,或者 你想要父一級在正常處理事件時預先做一些事情。
在示例3-1中,大多數元素擁有事件句柄,由MouseLeftButtonDown和 PreviewMouseLeftButtonDown事件指定相應的bubbling和tunneling事件。示例 3-2顯示了相應的後台代碼文件。
示例3-2
using System;
using System.Windows;
using System.Diagnostics;
namespace EventRouting …{
public partial class Window1 : Window …{
public Window1( ) …{
InitializeComponent( );
}
private void MouseButtonDownButton(object sender, RoutedEventArgs e)
…{ Debug.WriteLine("MouseButtonDownButton"); }
private void PreviewMouseButtonDownButton(object sender, RoutedEventArgs e)
…{ Debug.WriteLine("PreviewMouseButtonDownButton"); }
private void MouseButtonDownGrid(object sender, RoutedEventArgs e)
…{ Debug.WriteLine("MouseButtonDownGrid"); }
private void PreviewMouseButtonDownGrid(object sender, RoutedEventArgs e)
…{ Debug.WriteLine("PreviewMouseButtonDownGrid"); }
private void MouseButtonDownCanvas(object sender, RoutedEventArgs e)
…{ Debug.WriteLine("MouseButtonDownCanvas"); }
private void PreviewMouseButtonDownCanvas(object sender, RoutedEventArgs e)
…{ Debug.WriteLine("PreviewMouseButtonDownCanvas"); }
private void MouseButtonDownEllipse(object sender, RoutedEventArgs e)
…{ Debug.WriteLine("MouseButtonDownEllipse"); }
private void PreviewMouseButtonDownTextBlock (object sender,
RoutedEventArgs e)
…{ Debug.WriteLine("PreviewMouseButtonDownEllipse"); }
}
}
每一個句柄輸出了一條debug信息。這裡時我們獲得的debug輸出,當點擊 Canvas中的TextBlock時。
PreviewButtonDownButton
PreviewButtonDownGrid
PreviewButtonDownCanvas
PreviewButtonDownEllipse
ButtonDownEllipse
ButtonDownCanvas
ButtonDownGrid
ButtonDownButton
輸出結果證實了Preview事件是最先被激發的。還顯示了它是從Button元素開 始向下工作,正如我們對tunneling事件希望的那樣。bubbling事件則從 Ellispse開始向上工作。
Bubbling路由事件提供了很多事件,意味著你可以注冊一個單獨的事件處理 在一個控件上,而且它將為內嵌在控件中的任何元素接收事件。你不需要任何特 殊的處理以解決內嵌內容或自定義可視化內容,事件簡單的向上冒泡,並且在那 裡可以全部被處理。
3.2.1.1中止事件處理
有很多情形你可能不想讓事件冒泡。例如,你可能希望轉換事件為別的什麼 東西,Button元素有效的轉換了MouseLeftButtonDown和MouseLeftButtonUp事件 為Click事件。它抑止了底層事件,從而只有Click事件冒泡到控件之外。
任何句柄都能防止進一步的處理路由事件——通過設置RoutedEvebtArgs的 Handled屬性,如示例3-4所示。
示例3-3
private void ButtonDownCanvas(object sender, RoutedEventArgs e) …{
Debug.WriteLine("ButtonDownCanvas");
e.Handled = true;
}
另一個設置Handled標志的原因是,如果你想要防止正常的事件處理。一旦你 在Preview句柄中這麼做,不僅tunneling的Preview事件會停止,本應正常執行 的bubbling事件也不會被激活,因此看起來似乎事件沒有發生。
3.2.1.2確定目標
雖然在一個單獨的地方,能夠處理來自一組元素的事件,這是非常便利的, 你的句柄可能需要知道是哪個元素引起激活一個事件,你可能想這正是句柄中 sender參數的意圖。事實上,sender一直將對象歸諸於你附加到的事件句柄上。 在使用bubbling和tunneling事件的情形中,這並不總是引起事件被激活的元素 。在示例3-1中,ButtonDownWindow句柄的sneder是Window本身。
幸運的是,找到潛在的導致事件發生的元素,這是容易的。RouteEventArgs 對象作為第二個參數傳遞,提供了一個OriginalSource屬性。
3.2.1.3路由事件和正常的事件
正常的.NET事件(或者說,他們曾經稱為CLR事件),提供了一個優勢——相 對於路由事件語法:很多.NET語言對處理CLR事件提供內嵌的支持。這就提供了 最好的兩種世界:你可以使用你喜歡的語言的事件處理語法,而不是利用額外的 由路由事件提供的功能。
多虧了CLR事件機制的彈性設計。雖然這裡有一種標准的聯合了CLR事件的簡 單行為,CLR的設計者有遠見的意識到,一些應用程序需要更多的高級行為。這 些類因此可以自由的實現它們喜歡的事件。WPF獲益於這種有CLR事件定義的設計 ——這些事件內在的作為路由事件來實現。
示例3-1和示例3-2安排了事件句柄的連接,通過使用標記中的屬性。但是我 們可能已經替代地使用了正常的C#事件句柄語法來關聯構造函數中的句柄。例如 ,我們要在示例3-1中移除MouseLeftButtonDown和PreviewMouseLeftButtonDown 屬性,接著修改示例3-2的構造函數,如下面的示例3-4。
示例3-4
…
public Window1( ) …{
InitializeComponent( );
this.MouseLeftButtonDown += MouseButtonDownWindow;
this.PreviewMouseLeftButtonDown += PreviewMouseButtonDownWindow;
}
…
我們還能對來自內嵌元素的事件進行同樣的處理。我們不得不應用x:Name屬 性為了能夠訪問C#的元素。
後台代碼經常是最好的地方來附屬事件句柄。一旦你的用戶界面有不尋常和 有創意的可視化外觀,這是一個好的時機讓xaml文件有效地被圖形設計器擁有。 一個設計者不應該知道開發者需要處理哪些事件,或者調用那些句柄函數。因此 ,你將通常要設計者在xaml中給元素命名,同時開發者將要在後台代碼附屬句柄 。
3.2.2鼠標輸入
鼠標輸入關注於哪個元素直接位於鼠標下。所有的用戶界面元素派生於 UIElement基類,這個基類定義了大量的鼠標輸入事件。這些事件列於表3-1中。
表3-1
Tunnel, Bubble
UIElement還定義了一對屬性,表示鼠標當前是否在元素上:ISMouseOver和 ISDirectMouseOver。這兩個屬性的區別在於,當鼠標在正被討論的元素上或任 何它的子元素上時,前者為true;而後者僅當鼠標在正被討論的元素上的時候才 為true,不包括它的子元素這種情況。
注意到,上表中基本的鼠標事件設置不包括Click事件。這是因為Click一個 高級別的概念——相對於基本的輸入。一個按鈕可以被點擊——通過鼠標或鍵盤 。此外,Click並不是必要的直接符合一個單獨的鼠標事件。通常的,用戶不得 不點擊或按下或釋放鼠標,當鼠標在鼠標之上以注冊一個Click事件時。相應地 ,這些高級別的事件由更明確的元素類型提供。Control類添加了一對事件: MouseDoubleClick和PreviewMouseDoubleClick。ButtonBase——Button的基類 ,CheckBox,RadioButton,都有添加這個Click事件。
如果你使用了一個Fill屬性為透明的Shape,這個Shape將擔當輸入的目標, 一旦鼠標在Shape之上。這回有一點令人驚訝,如果你使用了一個完全透明的筆 刷。這個Shape將是不可見的,但是仍然作為輸入的目標,不管鼠標在其上看來 可能是什麼樣的。如果你想要一個填充為透明的Shape,而且不捕獲鼠標輸入, 簡單的根本不提供Fill屬性,如果Fill屬性為null值(而不是一個完全的透明筆 刷),,這個Shape將不會擔當輸入的模板。
記住,如果你考慮處理一個鼠標事件的原因是,簡單的為用戶提供某些可見 的反饋,寫一個事件句柄可能過度了。這通常是可能的,通過聲明性的屬性觸發 器和事件觸發器,可以在樣式的標簽中,完全達到你需要的可視化效果。
3.2.3鍵盤輸入
鍵盤輸入引入了focus的概念。不同於鼠標,沒法為用戶移動鍵盤在一個元素 上,從而指出輸入的目標。在Windows中,一個特定的元素被指定為擁有focus, 意味著它會擔當鍵盤輸入的目標。用戶通過點擊鼠標或Alt+Tap 在正在討論的控 件上設置focus,或者通過使用導航鍵如Tab和指針。
原則上,任何用戶元素可以獲得焦點。IsFocused屬性定義在UIElement—— FrameworkElement的基類。盡管如此,Focusable屬性決定了是否支持這個特征 在任意特定的元素上。默認的,這個值對於控件是true;對其他元素是false。
表3-2顯示了有用戶界面元素提供的盤輸入事件。所有的這些項使用tunnel和 bubble路由,分別為Preview和主要事件。
表3-2
PreviewKeyUp, KeyUp
注意到,TextInput並不是必要的鍵盤的輸入。它代表了文本的輸入在一個獨 立於設備的方式,因此這個事件也能被手動輸入的結果所激活。
3.2.4手動輸入
手寫板上的鐵筆以及其他支持手動輸入的系統,有一套自己的事件。表3-3顯 示了手動輸入事件——由用戶界面元素提供。
表3-3
3.2.5命令
很多應用程序提供了多於一種的方式來執行確定動作。例如,考慮創建一個 新文件的動作。你可以選擇Fiel——New menu item,或者你可以點擊相應的工 具欄按鈕。可選擇的,你可以使用快捷鍵如Ctrl+N。如果應用程序提供了一個腳 本系統,這個腳本還可以提供另一種執行這個動作的方式。結果是,無論你使用 什麼機制,都是一樣的,因為這裡有不同的方式調用同樣的底層命令。
WPF對這個想法提供了內嵌的支持。RoutedCommand類代表了一個可以在多種 方式調用的邏輯動作。在典型的WPF應用程序中,每個菜單項和工具欄按鈕都聯 合到一個底層的RoutedCommand對象。
RoutedCommand以一種與底層輸入表單非常相似的方式工作。當調用一個命令 的時候,它激活了兩個事件:PreviewExecuteEvent和ExecuteEvent。這些事件 在這棵元素樹中使用tunnel和bubble機制,和輸入事件的方式相同。命令的目標 是由命令的調用方式來決定。典型地,這個目標將會是當前有焦點的任何一個元 素,但是RoutedCommand還提供了一個Execute的重載方法,這會傳遞一個明確的 目標元素。
你可以從很多地方獲取一個RoutedCommand。一些控件提供了命令。例如, ScrollBar控件為它的每個動作定義了命令,使之在靜態字段有效,如 LineUpCommand和PageDownCommand。然而,大多數命令並不是唯一對應到特定的 控件。一些符合應用程序級別的動作如”新文件”或“打開”。其他動作會在控 件上被調用,但是可以被一些不同的控件實現。例如,TextBox和RichTextBox都 能處理剪切操作。
這裡有一組提供了標准命令的類。這些類顯示在表3-4中。這意味著你不需要 創建自己的RoutedCommand對象來代表最普遍的操作。此外,很多命令被內嵌控 件了解。例如TextBox和RichTextBox都支持很多標准的操作,包括clipboard, undo和redo命令。
表3-4
3.2.5.1命令句柄
作為一個有用的命令,必須有事物對其進行響應。這個工作些微不同於處理 正常的輸入事件,因為大多數不是由控件定義的命令將會處理它們。表3-4中的 類定義了95個命令,因此如果Control為每個截然不同的命令定義了CLR事件,那 將需要190個事件——一旦還要包括preview的話。這不僅會極度不廣泛,甚至還 不是一個完全的解決方案。大多數應用程序在使用標准命令的同時,還定義了他 們自身的自定義命令。明顯的可選擇性是為了RoutedCommand自身激活事件。然 而,每個命令都是一個單件。例如,只有一個ApplicationCommand.New對象。如 果你能直接添加一個句柄到命令對象,這個句柄會在任何時間運行。這個命令在 你的應用程序任何地方被調用。如果你正想處理一個命令,當此命令在一個特定 的窗口中執行的時候,會怎麼樣呢?
CommandBinding類解決了這些問題。一個CommandBinding對象映射了一個明 確的RoutedCommand到一個句柄函數上——在一個特定的用戶界面元素級別。正 是這個CommandBinding會激活PreviewExecute和Execute事件,而不是UI元素。 這些綁定保存在UI元素定義的CommandBinding屬性。示例3-5顯示了如何為一個 窗體在後台代碼文件中,處理ApplicationCommand.New命令。
示例3-5
public partial class Window1 : Window {
public Window1( ) {
InitializeComponent( );
CommandBinding cmdBindingNew = new CommandBinding (ApplicationCommands.New);
cmdBindingNew.Execute += NewCommandHandler;
CommandBindings.Add(cmdBindingNew);
}
private void NewCommandHandler(object sender, ExecuteEventArgs e) {
if (unsavedChanges) {
MessageBoxResult result = MessageBox.Show(this,
"Save changes to existing document?", "New",
MessageBoxButton.YesNoCancel);
if (result == MessageBoxResult.Cancel) {
return;
}
if (result == MessageBoxResult.Yes) {
SaveChanges( );
}
}
// Reset text box contents
inputBox.Clear( );
}
}
這段代碼依賴於命令路由的冒泡本質。頂級Window元素不同於成為命令目標 的元素,當焦點通常屬於某個窗體中的子元素時。然而,命令會向上冒泡到頂級 。這個路由對命令的處理只放在一個地方,從而變得容易。
示例3-5處理的命令是ApplicationCommand.New。如果這組標准命令並沒有滿 足你的應用程序的需要,你可以為明確的操作定義自定義命令。
3.2.5.2定義命令
示例3-6顯示了如何定義一個命令。WPF使用對象實例來確定命令的唯一性。 如果你要創建同名的第二個命令,這不會被當作同樣的命令。由於這個原因,命 令通常放置在靜態字段或屬性。
示例3-6
Example 3-6. Creating a custom command
public partial class Window1 : Window {
public static RoutedCommand FooCommand;
static Window1( ) {
InputGestureCollection fooInputs = new InputGestureCollection( );
fooInputs.Add(new KeyGesture
(Key.F,
ModifierKeys.Control|ModifierKeys.Shift));
FooCommand = new RoutedCommand("Foo", typeof(Window1), fooInputs);
}
...
}
在示例3-6中創建的Foo命令,通過一個CommandBinding被處理,正如任何其 它命令一樣。當然,用戶某種調用這個命令的方式。
3.2.5.3調用命令
不僅定義了一個自定義命令,示例3-6還顯示了一個將命令聯合到用戶輸入的 方法。配置這個特別的命令用來被一個特殊的輸入表示所調用。當前支持兩種輸 入表示類型:MouseGesture,是一個特別的由鼠標和觸筆選中的形狀; KeyGesture,正如在示例3-6中使用的,是一個特別的鍵盤快捷鍵。很多內嵌控 件聯合了標准的表示。例如,ApplicationCommand.Copy聯合了標准的鍵盤快捷 鍵,用來復制(大多數地方為Ctrl+C)。
雖然一個命令在創建的時候可以聯合一組表示, 在一個特別的窗體的上下文 中,你可能希望為這個命令分配另外的快捷鍵。為了允許這樣做,用戶界面元素 有一個InputBindings屬性。這個集合包含了InputBinding對象——聯合了輸入 表示和命令。這些增加了聯合了命令的默認表示。
輸入表示如快捷鍵,不是唯一調用命令的方式。你可以在命令上調用Execute 方法從而在代碼上調用它。正如示例3-7所示,Execute被重載了。如果你沒有傳 遞參數,這個命令目標將會是任何得到焦點的元素,正如通過一個輸入表示調用 一個命令。但是你可以傳遞任何你想要的目標元素。
示例3-7
ApplicationCommands.New.Execute( );
...or...
ApplicationCommands.New.Execute(targetElement);
你可能想,要在菜單項和工具欄按鈕的Click句柄中,編寫這樣的代碼。盡管 如此,由於命令經常聯合於菜單項和工具欄按鈕,Button和MenuItem都支持 Command屬性。這就唯一標志了要調用的命令,當元素被點擊的時候。這裡,為 命令本身,提供了一種聲明式的方式,而不是為每一個綁定到命令的UI元素提供 一個句柄。示例3-8顯示了一個聯合了標准Copy命令的Button。
示例3-8
<Button Command="Copy">Copy</Button>因為這個 示例使用了來自ApplicationCommands類的標准命令,我們可以使用這個語法的 簡寫形式,只需要指出命令名稱。因為命令不是定義表3-4中的類定義的,這就 需要一些更詳細的信息。完整的命令屬性xaml語法是:
[[xmlNamePrefix:]ClassName.]EventName
如果當前只有事件名,這個事 件假定為標准命令中的一個。例如,Undo是ApplicationCommands. Undo的簡寫 。否則,你必須提供一個類的名稱,以及可能一個命名空間前綴。如果你正在使 用自定義命令或者某個第三方組件定義的命令,這個命名空間前綴就是需要的。 與Mapping這個XML處理指令(使外部類型在xaml文件中有效)協力工作。(參見 附錄A獲取更多Mapping處理指令的信息。)
示例3-9顯示了命令名稱語法的使用——所有部分都在。M:MyCommand.Foo的 值意味著當前正在討論的命令是在mylib組件的MyLib.Commands.MyCommands類中 定義的,並且存儲在名為Foo的字段或屬性中。
示例3-9
<?Mapping ClrNamespace="MyLib.Commands" Assembly="mylib"
XmlNamespace="urn:mylib" ?>
<Window xmlns:m="urn:mylib" >
<Button Command="m:MyCommands.Foo">Custom Command</Button>
...
3.2.5.4支持命令
不僅可以被執行,命令還提供了一個QueryEnabled方法,返回了一個Boolean 值表明命令是否能被立刻調用;某些命令僅在特定的上下文中有效。這個特征可 以用來決定菜單或工具欄中的項是否應該變為灰色。調用QueryEnabled方法,會 被以Execute同樣的方式處理;CommandBinding對象用於處理這次查詢。這個綁 定激活一對PreviewQueryEnabled和QueryEnabled事件,這將以與 PreviewExecute和Execute同樣的方式進行tunnel和bubble。示例3-10顯示了如 何處理這個事件,為了系統定義的Redo命令。
示例3-10
public Window1( ) {
InitializeComponent( );
CommandBinding redoCommandBinding =
new CommandBinding(ApplicationCommands.Redo);
redoCommandBinding.QueryEnabled += RedoCommandQueryEnabled;
CommandBindings.Add(redoCommandBinding);
}
void RedoCommandQueryEnabled(object sender, QueryEnabledEventArgs e) {
if (!CanRedo( )) {
e.IsEnabled = false;
}
}
不幸的是,截止到寫作時間,當前WPF的版本並不會使菜單或工具欄中的項變 灰。它會激活QueryEnabled事件當一個菜單項被調用時,以及防止命令的執行, 如果被disabled了,但是當前不提供任何可視化的指示,來表明一個項被 disabled。我們希望這個問題會被解決在將來的版本中。
我們已經看到在WPF中控件是如何處理輸入的所有可能方式。現在讓我們開一 下一組內嵌在WPF中的控件。