分享

“Avalon”输入系统

 xuyizun 2006-10-12

“Avalon”输入系统

发布日期: 9/3/2004 | 更新日期: 9/3/2004

Nick Kramer
Microsoft Corporation

摘要:“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; }
}

此外,MouseKeyboard 类提供:

    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 的目标:

avaloninput01

事件处理的顺序将为:

1.

根元素上的 PreviewMouseDown(隧道)

2.

中间元素 #1 上的 PreviewMouseDown(隧道)

3.

叶元素 #2 上的 PreviewMouseDown(隧道)

4.

叶元素 #2 上的 MouseDown(冒泡)

5.

中间元素 #1 上的 MouseDown(冒泡)

6.

根元素上的 MouseDown(冒泡)

以下为 ElementPreview 输入事件的列表:

        // 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/MouseLeaveGotFocus/LostFocus)时,InputEventArgs.Source 将会通知您。

另一个引人注意的问题是坐标空间。坐标 (0,0) 位于左上方,但这是什么事物的左上方?是作为输入目标的元素的左上方,还是您附加有事件处理程序的元素的左上方,或是其他事物的左上方?为了避免混淆,Avalon 输入 API 要求您在处理坐标时指定您的引用框架。例如,MouseEventArgs.GetPosition 方法将 Element 作为一个参数,而且由 GetPosition 返回的 (0,0) 坐标位于该元素的左上角。

文本输入

TextInput 事件允许组件或应用程序以与设备无关的方式侦听文本输入。键盘是 TextInput 的主要方式,但是语音、手写以及其他输入设备也可生成 TextInput

对于键盘输入,Avalon 将首先发送适当的 KeyDown/KeyUp 事件,但如果这些事件是未处理的且键为文本键,则会发送 TextInput 事件。通常,在 KeyDown/KeyUpTextInput 事件之间并不是单个的一对一映射,多个击键可以生成单个字符的 TextInput,而单个击键可以生成多字符的字符串。对于中文、日文以及韩文尤其如此,这些语言使用输入法编辑器 (IME) 生成数以千计的以字母表示的不同字符。

当 Avalon 发送 KeyDown/KeyUp 事件时,如果击键成为 TextInput 事件的一部分,那么 KeyEventArgs.Key 将被设置为 Key.TextInput,因此应用程序不会意外地处理属于较大 TextInput 一部分的击键。在这些情况下,KeyEventArgs.TextInputKey 将显示实际击键。同样,如果 IME 处于活动状态,那么 KeyEventArgs.Key 将为 Key.ImeProcessed,而 KeyEventArgs.ImeProcessedKey 将提供实际击键。

键盘示例

让我们看一个简单的示例,在该示例中,按 CTRL+O 会打开一个文件(无论什么控件具有焦点),而按 Open 按钮也可执行该操作:

avaloninput02

在 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 理解 CutCopyPaste。由于这些命令中的每一个都提供了默认的键绑定,因此,TextBox 会自动支持这些快捷键。

输入核心体系结构

avaloninput03

输入系统由内核模式组件和用户模式组件组成。输入在设备驱动程序中产生,然后,对于大多数输入设备而言,此输入会发送到 USER 和 GDI 的内核模式组件 win32k.sys 中。Win32k.sys 会对输入进行一些处理,并决定将输入发送到哪个应用程序进程。在 Longhorn 应用程序内,Avalon 会对输入执行进一步的处理,并向应用程序发送通知。

与 Win32 程序一样,Avalon 程序也具有可以轮询外部环境以获取新通知的消息循环。Avalon 可以与标准的 Win32 消息循环集成,该消息循环通过调度程序 与 Avalon 的其余部分连接。调度程序能提取特定循环的详细信息,从而也能提供服务以便处理嵌套消息循环。为了从 Win32 接收消息,Avalon 具有一个称为 HwndSource 的 hwnd。消息处理是同步的,即在 Avalon 完全处理完输入消息之前,HwndSource WndProc 不会返回。在期望 WndProc 返回一个值的情况下,这会启用与 Win32 的集成。

在 Longhorn 应用程序内,输入处理如下所示:

avaloninput05

在核心输入系统(以灰色框表示)中,当 IInputProvider 向其相应的 InputProviderSite 通知有关可用输入报告时,输入便会开始。站点会通知 InputManager,后者将输入报告放在临时区域中。然后,会在临时区域上运行各种监视器和筛选器,从而将输入报告变成一系列事件。最后,通过元素树路由事件,并调用处理程序。

键盘和鼠标的输入提供程序通过 HwndSource(未用图表示)的方式从 Win32 USER 获取输入。其他设备可以选择此机制,也可以选择一种完全不同的机制。笔针就是并非来自 HwndSource 的输入的一个示例,笔针的输入提供程序从 wisptis.exe 获取输入,后者又通过 HID(人机接口设备 API)与设备驱动程序进行会话。InputManager 提供了用于注册新的输入提供程序的 API。

筛选器是指侦听 InputManager.PreProcessInputInputManager.PostProcessInput 事件的任何代码。筛选器可以修改临时区域。取消 PreProcessInput 将会从临时区域中删除输入事件。PostProcessInput 将临时区域公开为一个堆栈,即可以将项从临时区域顶部弹出或推入。

监视器是侦听 InputManager.PreNotifyInputInputManager.PostNotifyInput 的任何代码。监视器无法修改临时区域。

添加新型设备

我们处于本部分所讨论功能的早期设计阶段,非常感谢您的反馈。以下列出了能够启用设备扩展性的一些方案:

向键盘添加新键;向鼠标添加按钮

添加能够向应用程序公开 API 的新型设备

添加能够模拟现有设备(通常为鼠标和键盘)的新型设备

添加能够公开 API 并模拟现有设备的新型设备,以获得与不理解此类设备的应用程序的兼容性

使用 HID 添加新设备或设备功能

启用使用 HID 的应用程序以获取较低级别的“原始”输入

全局启用到操作的绑定输入序列;例如,“mail”键将会启动电子邮件程序(包括前台应用程序不理解 mail 键的情形)

小结

使用 Avalon 可以完全访问鼠标、键盘以及笔针,从而提供了更高级别的服务,以用于文本输入和命令。

转到原英文页面

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多