“Avalon”輸入系統(tǒng)發(fā)布日期: 9/3/2004 | 更新日期: 9/3/2004
Nick Kramer 摘要:“Longhorn”中的表示子系統(tǒng)(代號(hào)為“Avalon”)提供了功能強(qiáng)大的新 API 以用于輸入。本文將概述這些 API:為應(yīng)用程序提供哪些服務(wù)、輸入系統(tǒng)的體系結(jié)構(gòu)以及如何支持新的輸入設(shè)備。 ![]() 本頁內(nèi)容
簡(jiǎn)介“Longhorn”中的表示子系統(tǒng)(代號(hào)為“Avalon”)提供了新的 API 以用于輸入。這些主要輸入 API 都在 Element 類上。請(qǐng)注意,在本文中,我將“FrameworkElement”或“ContentFrameworkElement”統(tǒng)稱為“element”。雖然它們是截然不同的類,但從輸入的角度講它們卻是完全相同的。元素具有您期望從 Windows 操作系統(tǒng)中獲得的全部鼠標(biāo)和鍵盤功能:按鍵、鼠標(biāo)按鈕、鼠標(biāo)移動(dòng)、焦點(diǎn)管理以及鼠標(biāo)捕獲等等。元素具有下列與輸入相關(guān)的屬性、方法和事件: class Element { // non-input APIs omitted // Mouse event MouseButtonEventHandler MouseLeftButtonDown; event MouseButtonEventHandler MouseLeftButtonUp; event MouseButtonEventHandler MouseRightButtonDown; event MouseButtonEventHandler MouseRightButtonUp; event MouseEventHandler MouseMove; bool IsMouseOver { get; } bool IsMouseDirectlyOver { get; } event MouseEventHandler MouseEnter; event MouseEventHandler MouseLeave; event MouseEventHandler GotMouseCapture; event MouseEventHandler LostMouseCapture; bool IsMouseCaptured { get; } bool CaptureMouse(); void ReleaseMouseCapture(); event MouseEventHandler MouseHover; event MouseWheelEventHandler MouseWheel; // Keyboard event KeyEventHandler KeyDown; event KeyEventHandler KeyUp; event TextInputEventHandler TextInput; bool IsFocused { get; } bool Focus(); event FocusChangedEventHandler GotFocus; event FocusChangedEventHandler LostFocus; bool Focusable { get; set; } bool IsFocusWithin { get; } bool KeyboardActive { get; set; } bool IsEnabled { get; } } 此外,Mouse 和 Keyboard 類提供: class Keyboard { static Element Focused { get; } static bool Focus(Element elt) static ModifierKeys Modifiers { get; } static bool IsKeyDown(Key key) static bool IsKeyUp(Key key) static bool IsKeyToggled(Key key) static KeyState GetKeyState(Key key) static KeyboardDevice PrimaryDevice { get; } } class Mouse { static Element DirectlyOver { get; } static Element Captured { get; } static bool Capture(Element elt); static Cursor OverrideCursor { get; set; } static bool SetCursor(Cursor cursor); static MouseButtonState LeftButton { get; } static MouseButtonState RightButton { get; } static MouseButtonState MiddleButton { get; } static MouseButtonState XButton1 { get; } static MouseButtonState XButton2 { get; } static Point GetPosition(Element relativeTo); static void Synchronize(bool force); static MouseDevice PrimaryDevice { get; } static void AddAnyButtonDown(Element element, MouseButtonEventHandler handler); static void RemoveAnyButtonDown(Element element, MouseButtonEventHandler handler); } Avalon 還具有對(duì)筆針的集成支持。筆針是指筆輸入,廣泛用于 Tablet PC。Avalon 應(yīng)用程序通過使用鼠標(biāo) API 可將筆針視為鼠標(biāo)。但是,Avalon 還公開了與鍵盤和鼠標(biāo) API 同等的筆針 API: // stylus APIs on Element event StylusEventHandler StylusDown; event StylusEventHandler StylusUp; event StylusEventHandler StylusMove; event StylusEventHandler StylusInAirMove; bool IsStylusOver { get; } bool IsStylusDirectlyOver { get; } event StylusEventHandler StylusEnter; event StylusEventHandler StylusLeave; event StylusEventHandler StylusInRange; event StylusEventHandler StylusOutOfRange; event StylusSystemGestureEventHandler StylusSystemGesture; event StylusEventHandler GotStylusCapture; event StylusEventHandler LostStylusCapture; bool IsStylusCaptured { get; } bool CaptureStylus() void ReleaseStylusCapture() 筆針還可充當(dāng)鼠標(biāo),因此,僅識(shí)別鼠標(biāo)的應(yīng)用程序會(huì)自動(dòng)獲得某一級(jí)別的筆針支持。當(dāng)以這種方式使用筆針時(shí),應(yīng)用程序首先獲取適當(dāng)?shù)墓P針事件,然后再獲取相應(yīng)的鼠標(biāo)事件,我們稱這個(gè)過程為筆針事件提升 到鼠標(biāo)事件。(我在添加新型設(shè)備中簡(jiǎn)要討論了提升的概念。) 此外,還可以使用更高級(jí)別的服務(wù)(例如,手寫輸入),雖然它們超出了本文的討論范圍。 在樹中輸入元素包含其他元素(它的子元素),從而形成了通常具有數(shù)層深的元素樹。在 Avalon 中,父元素始終可以參與定向到其子元素(或?qū)O元素等)的輸入。這對(duì)于控件組合(使用較小的控件來構(gòu)建控件)特別有用。 Avalon 使用事件路由向父元素發(fā)出通知。路由 是指將事件傳遞到多個(gè)元素,直至其中一個(gè)元素將事件標(biāo)記為“handled”(已處理)的過程。事件使用以下三種路由機(jī)制之一:直接通知(也稱為“不路由”)、隧道和冒泡。直接通知 意味著僅通知目標(biāo)元素,這種機(jī)制由 Windows 窗體和其他 .NET 庫使用。冒泡 沿元素樹向上通知:先通知目標(biāo)元素,然后依次通知目標(biāo)的父元素以及父元素的父元素等等。隧道 的通知過程相反:先通知元素樹的根,然后向下通知,最后通知目標(biāo)元素。 Avalon 輸入事件通常是成對(duì)出現(xiàn)的 — 隧道事件后面跟有冒泡事件。例如,PreviewMouseMove 隧道事件就與冒泡 MouseMove 事件一同出現(xiàn)。作為一個(gè)示例,假設(shè)在以下樹中,“葉元素 #2”是 MouseDown/PreviewMouseDown 的目標(biāo): ![]() 事件處理的順序?qū)椋?
以下為 Element 上 Preview 輸入事件的列表: // Preview events on Element event MouseButtonEventHandler PreviewMouseLeftButtonDown; event MouseButtonEventHandler PreviewMouseLeftButtonUp; event MouseButtonEventHandler PreviewMouseRightButtonDown; event MouseButtonEventHandler PreviewMouseRightButtonUp; event MouseEventHandler PreviewMouseMove; event MouseWheelEventHandler PreviewMouseWheel; event MouseEventHandler PreviewMouseHover; event MouseEventHandler PreviewMouseEnter; event MouseEventHandler PreviewMouseLeave; event KeyEventHandler PreviewKeyDown; event KeyEventHandler PreviewKeyUp; event FocusChangedEventHandler PreviewGotFocus; event FocusChangedEventHandler PreviewLostFocus; event TextInputEventHandler PreviewTextInput; event StylusEventHandler PreviewStylusDown; event StylusEventHandler PreviewStylusUp; event StylusEventHandler PreviewStylusMove; event StylusEventHandler PreviewStylusInAirMove; event StylusEventHandler PreviewStylusEnter; event StylusEventHandler PreviewStylusLeave; event StylusEventHandler PreviewStylusInRange; event StylusEventHandler PreviewStylusOutOfRange; event StylusSystemGestureEventHandler PreviewStylusSystemGesture; 通常,在將事件標(biāo)記為已處理之后,不會(huì)調(diào)用其他處理程序。但是,當(dāng)您創(chuàng)建處理程序時(shí),您可以要求它通過使用 AddHandler 方法(為 handledEventsToo 參數(shù)傳遞“true”)來接收已處理的事件以及未處理的事件。 由于隧道和冒泡,父元素將會(huì)接收最初以其子元素為目標(biāo)的事件。通常,誰為目標(biāo)并不重要,畢竟事件是未處理的事件。但是,當(dāng)有必要知道目標(biāo)(尤其是 MouseEnter/MouseLeave 和 GotFocus/LostFocus)時(shí),InputEventArgs.Source 將會(huì)通知您。 另一個(gè)引人注意的問題是坐標(biāo)空間。坐標(biāo) (0,0) 位于左上方,但這是什么事物的左上方?是作為輸入目標(biāo)的元素的左上方,還是您附加有事件處理程序的元素的左上方,或是其他事物的左上方?為了避免混淆,Avalon 輸入 API 要求您在處理坐標(biāo)時(shí)指定您的引用框架。例如,MouseEventArgs.GetPosition 方法將 Element 作為一個(gè)參數(shù),而且由 GetPosition 返回的 (0,0) 坐標(biāo)位于該元素的左上角。 文本輸入TextInput 事件允許組件或應(yīng)用程序以與設(shè)備無關(guān)的方式偵聽文本輸入。鍵盤是 TextInput 的主要方式,但是語音、手寫以及其他輸入設(shè)備也可生成 TextInput。 對(duì)于鍵盤輸入,Avalon 將首先發(fā)送適當(dāng)?shù)?KeyDown/KeyUp 事件,但如果這些事件是未處理的且鍵為文本鍵,則會(huì)發(fā)送 TextInput 事件。通常,在 KeyDown/KeyUp 和 TextInput 事件之間并不是單個(gè)的一對(duì)一映射,多個(gè)擊鍵可以生成單個(gè)字符的 TextInput,而單個(gè)擊鍵可以生成多字符的字符串。對(duì)于中文、日文以及韓文尤其如此,這些語言使用輸入法編輯器 (IME) 生成數(shù)以千計(jì)的以字母表示的不同字符。 當(dāng) Avalon 發(fā)送 KeyDown/KeyUp 事件時(shí),如果擊鍵成為 TextInput 事件的一部分,那么 KeyEventArgs.Key 將被設(shè)置為 Key.TextInput,因此應(yīng)用程序不會(huì)意外地處理屬于較大 TextInput 一部分的擊鍵。在這些情況下,KeyEventArgs.TextInputKey 將顯示實(shí)際擊鍵。同樣,如果 IME 處于活動(dòng)狀態(tài),那么 KeyEventArgs.Key 將為 Key.ImeProcessed,而 KeyEventArgs.ImeProcessedKey 將提供實(shí)際擊鍵。 鍵盤示例讓我們看一個(gè)簡(jiǎn)單的示例,在該示例中,按 CTRL+O 會(huì)打開一個(gè)文件(無論什么控件具有焦點(diǎn)),而按 Open 按鈕也可執(zhí)行該操作: ![]() 在 Win32 中,應(yīng)該定義一個(gè)快捷鍵對(duì)應(yīng)表并處理 WM_COMMAND,這通常使用 switch 語句來完成。(您也可以嘗試在窗口的 WndProc 內(nèi)處理 WM_KEYDOWN,但是,如果焦點(diǎn)不在按鈕或編輯框上,您只能獲取擊鍵,除非您還修改了按鈕和編輯框的 WndProc。) // sample.rc IDC_INPUTSAMPLE2 ACCELERATORS BEGIN "O", ID_ACCELERATOR_O, VIRTKEY, CONTROL, NOINVERT END // sample.cpp int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { . . . MyRegisterClass(hInstance); InitInstance(hInstance, nCmdShow); HACCEL hAccelTable = LoadAccelerators(hInstance, (LPCTSTR)IDC_INPUTSAMPLE2); MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { if (!TranslateAccelerator(window, hAccelTable, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } return (int) msg.wParam; } ATODWM MyRegisterClass(HINSTANCE hInstance) { . . . } BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) { window = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); if (!window) { return FALSE; } button = CreateWindow("BUTTON", "Open", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 40, 40, 90, 30, window, (HMENU) ID_BUTTON, hInstance, NULL); if (!button) { return FALSE; } DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_BORDER | ES_LEFT | ES_NOHIDESEL | ES_AUTOHSCROLL | ES_AUTOVSCROLL; edit = CreateWindow("EDIT", "...", dwStyle, 40, 80, 150, 40, window, (HMENU) 6, hInstance, NULL); ShowWindow(window, nCmdShow); UpdateWindow(window); return TRUE; } LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_COMMAND: { switch (LOWORD(wParam)) { case ID_ACCELERATOR_O: case ID_BUTTON: MessageBox(NULL, "Pretend this opens a file", "", 0); return 0; } break; } case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hWnd, message, wParam, lParam); } 在 Windows 窗體中,應(yīng)該將窗體上的 KeyPreview 設(shè)置為 true,并處理窗體上的 KeyDown 事件: using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using System.Data; public class Form1 : Form { static void Main() { Application.Run(new Form1()); } private Button button1; private TextBox textBox1; public Form1() { this.button1 = new Button(); this.textBox1 = new TextBox(); this.SuspendLayout(); // // button1 // this.button1.Location = new Point(8, 40); this.button1.Name = "button1"; this.button1.TabIndex = 0; this.button1.Text = "Open"; this.button1.Click += new EventHandler(this.button1_Click); // // textBox1 // this.textBox1.Location = new Point(8, 88); this.textBox1.Name = "textBox1"; this.textBox1.TabIndex = 1; this.textBox1.Text = "..."; // // Form1 // this.AutoScaleBaseSize = new Size(6, 15); this.ClientSize = new System.Drawing.Size(292, 260); this.Controls.AddRange(new Control[] { this.textBox1, this.button1}); this.Name = "Form1"; this.Text = "Input Sample"; this.KeyPreview = true; this.KeyDown += new KeyEventHandler(this.Form1_KeyDown); this.ResumeLayout(false); } private void button1_Click(object sender, EventArgs e) { handle(); } private void Form1_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.O && e.Modifiers == Keys.Control) { handle(); e.Handled = true; } } void handle() { MessageBox.Show("Pretend this opens a file"); } } 在 Avalon 中,應(yīng)該為 Button 的 Click 事件 (btn_Click) 定義一個(gè)處理程序,也要為 KeyDown (fp_KeyDown) 定義一個(gè)處理程序: <Window xmlns="http://schemas.microsoft.com/2003/xaml" xmlns:def="Definition" Text="Application1" Visible="True" > <FlowPanel KeyDown="fp_KeyDown"> <Button Click="btn_Click"> Open </Button> <TextBox> ... </TextBox> <def:Code> <![CDATA[ void fp_KeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.O && Keyboard.Modifiers == ModifierKeys.Control) { handle(); e.Handled = true; } } void btn_Click(object sender, ClickEventArgs e) { handle(); e.Handled = true; } void handle() { MessageBox.Show("Pretend this opens a file"); } ]]> </def:Code> </FlowPanel> </Window> 請(qǐng)注意,KeyDown 處理程序附加到樹根附近的 FlowPanel 中。(我們使用 FlowPanel 而不是 Window,這是因?yàn)?Window 類不能獲取輸入。)由于輸入沿樹向上冒泡,因此無論哪種元素具有焦點(diǎn),FlowPanel 均將獲取輸入。 這些示例有一點(diǎn)略有不同,假設(shè)編輯控件要處理 CTRL+O,結(jié)果會(huì)如何呢?在 Win32 和 Windows 窗體示例中,編輯控件從未接收到 WM_KEYDOWN 或等效通知,這是因?yàn)槭录窃谙⒀h(huán)中通過 TranslateAccelerator 的方式處理的。在 Avalon 示例中,首先通知 TextBox 控件,然后,僅當(dāng) TextBox 沒有處理輸入時(shí)才會(huì)調(diào)用我們的 fp_KeyDown 處理程序?;蛘撸覀兛梢蕴幚?PreviewKeyDown 而不是 KeyDown,在這種情況下,首先調(diào)用我們的 fp_KeyDown 處理程序。 在上面的 Avalon 示例中,我們兩次結(jié)束編寫處理邏輯,一次針對(duì) CTRL+O,另一次針對(duì)按鈕單擊。我們可以使用 Avalon 命令 簡(jiǎn)化此操作。 命令注命令在 PDC 2003 Longhorn 預(yù)發(fā)布版本中僅部分實(shí)現(xiàn)。 與設(shè)備輸入相比,命令使您能夠在更富有語義的層次上處理輸入。命令是簡(jiǎn)單的指令,例如,“cut”、“copy”、“paste”或“open”。Avalon 將提供通用命令庫,但是您也可以定義自己的命令庫。 命令對(duì)于集中處理邏輯十分有用??梢詮牟藛沃?、在工具欄上或者通過鍵盤快捷鍵來訪問相同的命令,而且,您可以使用命令來編寫適用于所有不同輸入情況的單行代碼。命令還提供了一種機(jī)制,當(dāng)命令變得不可用時(shí),使用此機(jī)制可以將菜單項(xiàng)和工具欄按鈕變?yōu)榛疑?/p> Avalon 提供的通用命令附帶有一組內(nèi)置的默認(rèn)輸入綁定,因此,當(dāng)您指定應(yīng)用程序處理 Copy 時(shí),您會(huì)自動(dòng)獲得 CTRL+C = Copy 綁定。您還可獲得用于其他輸入設(shè)備的綁定,例如,Tablet 筆勢(shì)輸入和語音信息。最后,許多通用命令都附帶有自己的圖標(biāo),從而使工具欄看上去更加一致且專業(yè)。 許多控件都具有對(duì)某些命令的內(nèi)置支持。例如,TextBox 理解 Cut、Copy 和 Paste。由于這些命令中的每一個(gè)都提供了默認(rèn)的鍵綁定,因此,TextBox 會(huì)自動(dòng)支持這些快捷鍵。 輸入核心體系結(jié)構(gòu)![]() 輸入系統(tǒng)由內(nèi)核模式組件和用戶模式組件組成。輸入在設(shè)備驅(qū)動(dòng)程序中產(chǎn)生,然后,對(duì)于大多數(shù)輸入設(shè)備而言,此輸入會(huì)發(fā)送到 USER 和 GDI 的內(nèi)核模式組件 win32k.sys 中。Win32k.sys 會(huì)對(duì)輸入進(jìn)行一些處理,并決定將輸入發(fā)送到哪個(gè)應(yīng)用程序進(jìn)程。在 Longhorn 應(yīng)用程序內(nèi),Avalon 會(huì)對(duì)輸入執(zhí)行進(jìn)一步的處理,并向應(yīng)用程序發(fā)送通知。 與 Win32 程序一樣,Avalon 程序也具有可以輪詢外部環(huán)境以獲取新通知的消息循環(huán)。Avalon 可以與標(biāo)準(zhǔn)的 Win32 消息循環(huán)集成,該消息循環(huán)通過調(diào)度程序 與 Avalon 的其余部分連接。調(diào)度程序能提取特定循環(huán)的詳細(xì)信息,從而也能提供服務(wù)以便處理嵌套消息循環(huán)。為了從 Win32 接收消息,Avalon 具有一個(gè)稱為 HwndSource 的 hwnd。消息處理是同步的,即在 Avalon 完全處理完輸入消息之前,HwndSource WndProc 不會(huì)返回。在期望 WndProc 返回一個(gè)值的情況下,這會(huì)啟用與 Win32 的集成。 在 Longhorn 應(yīng)用程序內(nèi),輸入處理如下所示: ![]() 在核心輸入系統(tǒng)(以灰色框表示)中,當(dāng) IInputProvider 向其相應(yīng)的 InputProviderSite 通知有關(guān)可用輸入報(bào)告時(shí),輸入便會(huì)開始。站點(diǎn)會(huì)通知 InputManager,后者將輸入報(bào)告放在臨時(shí)區(qū)域中。然后,會(huì)在臨時(shí)區(qū)域上運(yùn)行各種監(jiān)視器和篩選器,從而將輸入報(bào)告變成一系列事件。最后,通過元素樹路由事件,并調(diào)用處理程序。 鍵盤和鼠標(biāo)的輸入提供程序通過 HwndSource(未用圖表示)的方式從 Win32 USER 獲取輸入。其他設(shè)備可以選擇此機(jī)制,也可以選擇一種完全不同的機(jī)制。筆針就是并非來自 HwndSource 的輸入的一個(gè)示例,筆針的輸入提供程序從 wisptis.exe 獲取輸入,后者又通過 HID(人機(jī)接口設(shè)備 API)與設(shè)備驅(qū)動(dòng)程序進(jìn)行會(huì)話。InputManager 提供了用于注冊(cè)新的輸入提供程序的 API。 篩選器是指?jìng)陕?InputManager.PreProcessInput 或 InputManager.PostProcessInput 事件的任何代碼。篩選器可以修改臨時(shí)區(qū)域。取消 PreProcessInput 將會(huì)從臨時(shí)區(qū)域中刪除輸入事件。PostProcessInput 將臨時(shí)區(qū)域公開為一個(gè)堆棧,即可以將項(xiàng)從臨時(shí)區(qū)域頂部彈出或推入。 監(jiān)視器是偵聽 InputManager.PreNotifyInput 或 InputManager.PostNotifyInput 的任何代碼。監(jiān)視器無法修改臨時(shí)區(qū)域。 添加新型設(shè)備注我們處于本部分所討論功能的早期設(shè)計(jì)階段,非常感謝您的反饋。以下列出了能夠啟用設(shè)備擴(kuò)展性的一些方案:
小結(jié)使用 Avalon 可以完全訪問鼠標(biāo)、鍵盤以及筆針,從而提供了更高級(jí)別的服務(wù),以用于文本輸入和命令。 |
|