“Avalon”输入系统发布日期: 9/3/2004 | 更新日期: 9/3/2004
Nick Kramer 摘要:“Longhorn”中的表示子系统(代号为“Avalon”)提供了功能强大的新 API 以用于输入。本文将概述这些 API:为应用程序提供哪些服务、输入系统的体系结构以及如何支持新的输入设备。 ![]() 本页内容
简介“Longhorn”中的表示子系统(代号为“Avalon”)提供了新的 API 以用于输入。这些主要输入 API 都在 Element 类上。请注意,在本文中,我将“FrameworkElement”或“ContentFrameworkElement”统称为“element”。虽然它们是截然不同的类,但从输入的角度讲它们却是完全相同的。元素具有您期望从 Windows 操作系统中获得的全部鼠标和键盘功能:按键、鼠标按钮、鼠标移动、焦点管理以及鼠标捕获等等。元素具有下列与输入相关的属性、方法和事件: 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 还具有对笔针的集成支持。笔针是指笔输入,广泛用于 Tablet PC。Avalon 应用程序通过使用鼠标 API 可将笔针视为鼠标。但是,Avalon 还公开了与键盘和鼠标 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() 笔针还可充当鼠标,因此,仅识别鼠标的应用程序会自动获得某一级别的笔针支持。当以这种方式使用笔针时,应用程序首先获取适当的笔针事件,然后再获取相应的鼠标事件,我们称这个过程为笔针事件提升 到鼠标事件。(我在添加新型设备中简要讨论了提升的概念。) 此外,还可以使用更高级别的服务(例如,手写输入),虽然它们超出了本文的讨论范围。 在树中输入元素包含其他元素(它的子元素),从而形成了通常具有数层深的元素树。在 Avalon 中,父元素始终可以参与定向到其子元素(或孙元素等)的输入。这对于控件组合(使用较小的控件来构建控件)特别有用。 Avalon 使用事件路由向父元素发出通知。路由 是指将事件传递到多个元素,直至其中一个元素将事件标记为“handled”(已处理)的过程。事件使用以下三种路由机制之一:直接通知(也称为“不路由”)、隧道和冒泡。直接通知 意味着仅通知目标元素,这种机制由 Windows 窗体和其他 .NET 库使用。冒泡 沿元素树向上通知:先通知目标元素,然后依次通知目标的父元素以及父元素的父元素等等。隧道 的通知过程相反:先通知元素树的根,然后向下通知,最后通知目标元素。 Avalon 输入事件通常是成对出现的 — 隧道事件后面跟有冒泡事件。例如,PreviewMouseMove 隧道事件就与冒泡 MouseMove 事件一同出现。作为一个示例,假设在以下树中,“叶元素 #2”是 MouseDown/PreviewMouseDown 的目标: ![]() 事件处理的顺序将为:
以下为 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; 通常,在将事件标记为已处理之后,不会调用其他处理程序。但是,当您创建处理程序时,您可以要求它通过使用 AddHandler 方法(为 handledEventsToo 参数传递“true”)来接收已处理的事件以及未处理的事件。 由于隧道和冒泡,父元素将会接收最初以其子元素为目标的事件。通常,谁为目标并不重要,毕竟事件是未处理的事件。但是,当有必要知道目标(尤其是 MouseEnter/MouseLeave 和 GotFocus/LostFocus)时,InputEventArgs.Source 将会通知您。 另一个引人注意的问题是坐标空间。坐标 (0,0) 位于左上方,但这是什么事物的左上方?是作为输入目标的元素的左上方,还是您附加有事件处理程序的元素的左上方,或是其他事物的左上方?为了避免混淆,Avalon 输入 API 要求您在处理坐标时指定您的引用框架。例如,MouseEventArgs.GetPosition 方法将 Element 作为一个参数,而且由 GetPosition 返回的 (0,0) 坐标位于该元素的左上角。 文本输入TextInput 事件允许组件或应用程序以与设备无关的方式侦听文本输入。键盘是 TextInput 的主要方式,但是语音、手写以及其他输入设备也可生成 TextInput。 对于键盘输入,Avalon 将首先发送适当的 KeyDown/KeyUp 事件,但如果这些事件是未处理的且键为文本键,则会发送 TextInput 事件。通常,在 KeyDown/KeyUp 和 TextInput 事件之间并不是单个的一对一映射,多个击键可以生成单个字符的 TextInput,而单个击键可以生成多字符的字符串。对于中文、日文以及韩文尤其如此,这些语言使用输入法编辑器 (IME) 生成数以千计的以字母表示的不同字符。 当 Avalon 发送 KeyDown/KeyUp 事件时,如果击键成为 TextInput 事件的一部分,那么 KeyEventArgs.Key 将被设置为 Key.TextInput,因此应用程序不会意外地处理属于较大 TextInput 一部分的击键。在这些情况下,KeyEventArgs.TextInputKey 将显示实际击键。同样,如果 IME 处于活动状态,那么 KeyEventArgs.Key 将为 Key.ImeProcessed,而 KeyEventArgs.ImeProcessedKey 将提供实际击键。 键盘示例让我们看一个简单的示例,在该示例中,按 CTRL+O 会打开一个文件(无论什么控件具有焦点),而按 Open 按钮也可执行该操作: ![]() 在 Win32 中,应该定义一个快捷键对应表并处理 WM_COMMAND,这通常使用 switch 语句来完成。(您也可以尝试在窗口的 WndProc 内处理 WM_KEYDOWN,但是,如果焦点不在按钮或编辑框上,您只能获取击键,除非您还修改了按钮和编辑框的 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 窗体中,应该将窗体上的 KeyPreview 设置为 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 中,应该为 Button 的 Click 事件 (btn_Click) 定义一个处理程序,也要为 KeyDown (fp_KeyDown) 定义一个处理程序: <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> 请注意,KeyDown 处理程序附加到树根附近的 FlowPanel 中。(我们使用 FlowPanel 而不是 Window,这是因为 Window 类不能获取输入。)由于输入沿树向上冒泡,因此无论哪种元素具有焦点,FlowPanel 均将获取输入。 这些示例有一点略有不同,假设编辑控件要处理 CTRL+O,结果会如何呢?在 Win32 和 Windows 窗体示例中,编辑控件从未接收到 WM_KEYDOWN 或等效通知,这是因为事件是在消息循环中通过 TranslateAccelerator 的方式处理的。在 Avalon 示例中,首先通知 TextBox 控件,然后,仅当 TextBox 没有处理输入时才会调用我们的 fp_KeyDown 处理程序。或者,我们可以处理 PreviewKeyDown 而不是 KeyDown,在这种情况下,首先调用我们的 fp_KeyDown 处理程序。 在上面的 Avalon 示例中,我们两次结束编写处理逻辑,一次针对 CTRL+O,另一次针对按钮单击。我们可以使用 Avalon 命令 简化此操作。 命令注命令在 PDC 2003 Longhorn 预发布版本中仅部分实现。 与设备输入相比,命令使您能够在更富有语义的层次上处理输入。命令是简单的指令,例如,“cut”、“copy”、“paste”或“open”。Avalon 将提供通用命令库,但是您也可以定义自己的命令库。 命令对于集中处理逻辑十分有用。可以从菜单中、在工具栏上或者通过键盘快捷键来访问相同的命令,而且,您可以使用命令来编写适用于所有不同输入情况的单行代码。命令还提供了一种机制,当命令变得不可用时,使用此机制可以将菜单项和工具栏按钮变为灰色。 Avalon 提供的通用命令附带有一组内置的默认输入绑定,因此,当您指定应用程序处理 Copy 时,您会自动获得 CTRL+C = Copy 绑定。您还可获得用于其他输入设备的绑定,例如,Tablet 笔势输入和语音信息。最后,许多通用命令都附带有自己的图标,从而使工具栏看上去更加一致且专业。 许多控件都具有对某些命令的内置支持。例如,TextBox 理解 Cut、Copy 和 Paste。由于这些命令中的每一个都提供了默认的键绑定,因此,TextBox 会自动支持这些快捷键。 输入核心体系结构![]() 输入系统由内核模式组件和用户模式组件组成。输入在设备驱动程序中产生,然后,对于大多数输入设备而言,此输入会发送到 USER 和 GDI 的内核模式组件 win32k.sys 中。Win32k.sys 会对输入进行一些处理,并决定将输入发送到哪个应用程序进程。在 Longhorn 应用程序内,Avalon 会对输入执行进一步的处理,并向应用程序发送通知。 与 Win32 程序一样,Avalon 程序也具有可以轮询外部环境以获取新通知的消息循环。Avalon 可以与标准的 Win32 消息循环集成,该消息循环通过调度程序 与 Avalon 的其余部分连接。调度程序能提取特定循环的详细信息,从而也能提供服务以便处理嵌套消息循环。为了从 Win32 接收消息,Avalon 具有一个称为 HwndSource 的 hwnd。消息处理是同步的,即在 Avalon 完全处理完输入消息之前,HwndSource WndProc 不会返回。在期望 WndProc 返回一个值的情况下,这会启用与 Win32 的集成。 在 Longhorn 应用程序内,输入处理如下所示: ![]() 在核心输入系统(以灰色框表示)中,当 IInputProvider 向其相应的 InputProviderSite 通知有关可用输入报告时,输入便会开始。站点会通知 InputManager,后者将输入报告放在临时区域中。然后,会在临时区域上运行各种监视器和筛选器,从而将输入报告变成一系列事件。最后,通过元素树路由事件,并调用处理程序。 键盘和鼠标的输入提供程序通过 HwndSource(未用图表示)的方式从 Win32 USER 获取输入。其他设备可以选择此机制,也可以选择一种完全不同的机制。笔针就是并非来自 HwndSource 的输入的一个示例,笔针的输入提供程序从 wisptis.exe 获取输入,后者又通过 HID(人机接口设备 API)与设备驱动程序进行会话。InputManager 提供了用于注册新的输入提供程序的 API。 筛选器是指侦听 InputManager.PreProcessInput 或 InputManager.PostProcessInput 事件的任何代码。筛选器可以修改临时区域。取消 PreProcessInput 将会从临时区域中删除输入事件。PostProcessInput 将临时区域公开为一个堆栈,即可以将项从临时区域顶部弹出或推入。 监视器是侦听 InputManager.PreNotifyInput 或 InputManager.PostNotifyInput 的任何代码。监视器无法修改临时区域。 添加新型设备注我们处于本部分所讨论功能的早期设计阶段,非常感谢您的反馈。以下列出了能够启用设备扩展性的一些方案:
小结使用 Avalon 可以完全访问鼠标、键盘以及笔针,从而提供了更高级别的服务,以用于文本输入和命令。 |
|