输入概述
更新:2007 年 11 月
Windows Presentation Foundation (WPF) 子系统提供了一个功能强大的 API,用于获取来自各种设备(包括鼠标、键盘和手写笔)的输入。
本主题介绍由 WPF 提供的服务并说明输入系统的体系结构。
本主题包括下列各节。
- 输入 API
- 事件路由
- 处理输入事件
- 文本输入
- 焦点
- 鼠标位置
- 鼠标捕获
- 命令
- 输入系统和基元素
- 接下来的内容
- 相关主题
输入 API
可在基元素类上找到公开的主输入 API:UIElement、ContentElement、FrameworkElement 和 FrameworkContentElement。有关基元素的更多信息,请参见基元素概述。这些类为相应输入事件提供功能,例如与以下项目相关的输入事件:按键、鼠标按钮、鼠标滚轮、鼠标移动、焦点管理和鼠标捕获。通过在基元素上放置输入 API,而不是将所有输入事件都视为服务,该输入体系结构使输入事件可通过 UI 中的特定对象来指明其来源,并使输入事件支持事件路由方案,从而使得多个元素均有机会处理输入事件。许多输入事件都有一对与其关联的事件。例如,键按下事件与 KeyDown 和 PreviewKeyDown 事件关联。这些事件之间的差别是它们路由到目标元素的方式。预览事件在元素树中从根元素到目标元素向下进行隧道操作。冒泡事件从目标元素到根元素向上进行冒泡操作。本概述后面和路由事件概述中将更详细地讨论 WPF 中的路由事件。
键盘和鼠标类
除了基元素类上的输入 API,Keyboard 类和 Mouse 类还提供了用于处理键盘和鼠标输入的其他 API。
Keyboard 类上的输入 API 的示例有 Modifiers 属性(用于返回当前按下的 ModifierKeys)和 IsKeyDown 方法(用于确定是否按下了指定的键)。
下面的示例使用 GetKeyStates 方法确定 Key 是否处于按下状态。
// Uses the Keyboard.GetKeyStates to determine if a key is down.
// A bitwise AND operation is used in the comparison.
// e is an instance of KeyEventArgs.
if ((Keyboard.GetKeyStates(Key.Return) & KeyStates.Down) > 0)
{
btnNone.Background = Brushes.Red;
}
Mouse 类上的输入 API 的示例有 MiddleButton(用于获取鼠标中键的状态)和 DirectlyOver(用于获取鼠标指针当前位于其上的元素)。
下面的示例确定鼠标上的 LeftButton 是否处于 Pressed 状态。
if (Mouse.LeftButton == MouseButtonState.Pressed)
{
UpdateSampleResults("Left Button Pressed");
}
本概述中将更详细地介绍 Mouse 和 Keyboard 类。
手写笔输入
WPF 已集成了对 Stylus 的支持。Stylus 是一种因 Tablet PC 带动而流行的笔输入。WPF 应用程序可以使用鼠标 API 将手写笔视为鼠标,但 WPF 也公开了使用与键盘和鼠标类似的模型的手写笔设备抽象。所有与手写笔相关的 API 都包含单词“Stylus”。
由于手写笔可充当鼠标,因此仅支持鼠标输入的应用程序仍可自动获取一定程度的手写笔支持。通过这样的方式使用手写笔时,将使应用程序有机会处理相应的手写笔事件,然后处理对应的鼠标事件。此外,通过手写笔设备抽象也可以使用较高级别的服务,例如墨迹输入。有关墨迹作为输入的更多信息,请参见墨迹入门。
事件路由
FrameworkElement 可以在其内容模型中包含其他元素作为子元素,从而形成一个元素树。在 WPF 中,父元素可通过处理事件来参与定向到其子元素或其他子代的输入。这对于生成进行较少控制的控件(一个称为“控件组合”或“合成”的过程)尤其有用。有关元素树和元素树与事件路由相关的方式的更多信息,请参见 WPF 中的树。
事件路由是将事件转发到多个元素的过程,以便路由中的特定对象或元素可选择为已通过其他元素指明来源的事件提供重要响应(通过处理)。路由事件使用三种路由机制之一:直接、冒泡和隧道。在直接路由中,源元素是唯一得到通知的元素,该事件不会路由到任何其他元素。但是,相对于标准 CLR 事件,直接路由事件仍提供了一些其他仅对于路由事件才存在的功能。冒泡操作在元素树中向上进行,首先通知指明了事件来源的第一个元素,然后是父元素,等等。隧道操作从元素树的根开始,然后向下进行,以原始的源元素结束。有关路由事件的更多信息,请参见路由事件概述。
WPF 输入事件通常成对出现,由一个隧道事件和一个冒泡事件组成。隧道事件与冒泡事件在“Preview”前缀上存在区别。例如,PreviewMouseMove 是隧道版本的鼠标移动事件,MouseMove 是冒泡版本的鼠标移动事件。此事件配对是在元素级别实现的一种约定,不是 WPF 事件系统的固有功能。有关详细信息,请参见路由事件概述中的“WPF 输入事件”一节。
处理输入事件
若要在元素上接收输入,必须将事件处理程序与该特定事件关联。在 XAML 中,此操作非常简单:将事件的名称作为将侦听此事件的元素的属性来引用。然后,基于委托,将属性的值设置为所定义的事件处理程序的名称。事件处理程序必须使用代码(如 C#)编写,并可包括在代码隐藏文件中。
当操作系统报告发生键操作时,如果键盘焦点正处在元素上,则将发生键盘事件。鼠标和手写笔事件每一种都分为两类:报告指针位置相对于元素的变化的事件,和报告设备按钮状态的变化的事件。
键盘输入事件示例
下面的示例侦听对向左键的按下操作。创建了具有 Button 的 StackPanel。用于侦听按下的向左键的事件处理程序已附加到 Button 实例。
示例的第一部分创建 StackPanel 和 Button,并附加 KeyDown 的事件处理程序。
<StackPanel>
<Button Background="AliceBlue"
KeyDown="OnButtonKeyDown"
Content="Button1"/>
</StackPanel>
// Create the UI elements.
StackPanel keyboardStackPanel = new StackPanel();
Button keyboardButton1 = new Button();
// Set properties on Buttons.
keyboardButton1.Background = Brushes.AliceBlue;
keyboardButton1.Content = "Button 1";
// Attach Buttons to StackPanel.
keyboardStackPanel.Children.Add(keyboardButton1);
// Attach event handler.
keyboardButton1.KeyDown += new KeyEventHandler(OnButtonKeyDown);
第二部分使用代码编写并定义事件处理程序。当按下向左键并且 Button 具有键盘焦点时,处理程序将运行并且 Button 的 Background 颜色将发生改变。如果按下了键,但该键不是向左键,则 Button 的 Background 颜色将变回其开始颜色。
private void OnButtonKeyDown(object sender, KeyEventArgs e)
{
Button source = e.Source as Button;
if (source != null)
{
if (e.Key == Key.Left)
{
source.Background = Brushes.LemonChiffon;
}
else
{
source.Background = Brushes.AliceBlue;
}
}
}
鼠标输入事件示例
在下面的示例中,当鼠标指针进入 Button 时,Button 的 Background 颜色将发生改变。当鼠标离开 Button 时,Background 颜色将还原。
示例的第一部分创建 StackPanel 和 Button 控件,并将 MouseEnter 和 MouseLeave 事件的事件处理程序附加到 Button。
<StackPanel>
<Button Background="AliceBlue"
MouseEnter="OnMouseExampleMouseEnter"
MouseLeave="OnMosueExampleMouseLeave">Button
</Button>
</StackPanel>
// Create the UI elements.
StackPanel mouseMoveStackPanel = new StackPanel();
Button mouseMoveButton = new Button();
// Set properties on Button.
mouseMoveButton.Background = Brushes.AliceBlue;
mouseMoveButton.Content = "Button";
// Attach Buttons to StackPanel.
mouseMoveStackPanel.Children.Add(mouseMoveButton);
// Attach event handler.
mouseMoveButton.MouseEnter += new MouseEventHandler(OnMouseExampleMouseEnter);
mouseMoveButton.MouseLeave += new MouseEventHandler(OnMosueExampleMouseLeave);
该示例的第二部分是使用代码编写的,用来定义事件处理程序。当鼠标进入 Button 时,Button 的 Background 颜色将改变为 SlateGray。当鼠标离开 Button 时,Button 的 Background 颜色将变回为 AliceBlue。
private void OnMouseExampleMouseEnter(object sender, MouseEventArgs e)
{
// Cast the source of the event to a Button.
Button source = e.Source as Button;
// If source is a Button.
if (source != null)
{
source.Background = Brushes.SlateGray;
}
}
private void OnMosueExampleMouseLeave(object sender, MouseEventArgs e)
{
// Cast the source of the event to a Button.
Button source = e.Source as Button;
// If source is a Button.
if (source != null)
{
source.Background = Brushes.AliceBlue;
}
}
文本输入
通过 TextInput 事件,您可以借助与设备无关的方式侦听文本输入。键盘是文本输入的主要方式,但也可以通过语音、手写和其他输入设备输入文本。
对于键盘输入,WPF 首先发送相应的 KeyDown/KeyUp 事件。如果未处理这些事件并且按键为文本键(而不是控制键,例如方向键或功能键),则将引发 TextInput 事件。KeyDown/KeyUp 与 TextInput 事件之间不会始终存在简单的一对一映射,因为多次击键可以产生一个字符的文本输入,一次击键也可以产生多字符的字符串。对于中文、日语和朝鲜语这样的语言尤其如此,这些语言使用输入法编辑器 (IME) 生成由其对应的字母组成的成千上万个可能的字符。
当 WPF 发送 KeyUp/KeyDown 事件时,如果击键可以成为 TextInput 事件的一部分(例如按下了 Alt+S),则 Key 将设置为 Key.System。这使得 KeyDown 事件处理程序中的代码可以检查 Key.System,如果找到,则将保留以供随后引发的 TextInput 事件的处理程序进行处理。在这些情况中,可以使用 TextCompositionEventArgs 参数的各种属性来确定原始击键。同样,如果 IME 处于活动状态,则 Key 具有值 Key.ImeProcessed,并且 ImeProcessedKey 将指出原始击键。
下面的示例定义 Click 事件的处理程序和 KeyDown 事件的处理程序。
第一段代码或标记创建用户界面。
<StackPanel KeyDown="OnTextInputKeyDown">
<Button Click="OnTextInputButtonClick"
Content="Open" />
<TextBox> . . . </TextBox>
</StackPanel>
// Create the UI elements.
StackPanel textInputStackPanel = new StackPanel();
Button textInputeButton = new Button();
TextBox textInputTextBox = new TextBox();
textInputeButton.Content = "Open";
// Attach elements to StackPanel.
textInputStackPanel.Children.Add(textInputeButton);
textInputStackPanel.Children.Add(textInputTextBox);
// Attach event handlers.
textInputStackPanel.KeyDown += new KeyEventHandler(OnTextInputKeyDown);
textInputeButton.Click += new RoutedEventHandler(OnTextInputButtonClick);
第二段代码包含事件处理程序
private void OnTextInputKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.O && Keyboard.Modifiers == ModifierKeys.Control)
{
handle();
e.Handled = true;
}
}
private void OnTextInputButtonClick(object sender, RoutedEventArgs e)
{
handle();
e.Handled = true;
}
public void handle()
{
MessageBox.Show("Pretend this opens a file");
}
由于输入事件在事件路由中向上冒泡,因此不管哪个元素具有键盘焦点,StackPanel 都将接收输入。TextBox 控件首先得到通知,而只有在 TextBox 未处理输入时才会调用 OnTextInputKeyDown 处理程序。如果使用 PreviewKeyDown 事件而不是 KeyDown 事件,则将首先调用 OnTextInputKeyDown 处理程序。
在此示例中,处理逻辑写入了两次,一次针对 Ctrl+O,另一次针对按钮的单击事件。使用命令,而不是直接处理输入事件,可简化此过程。本概述和命令概述中将讨论这些命令。
焦点
在 WPF 中,有两个与焦点有关的主要概念:键盘焦点和逻辑焦点。
键盘焦点
键盘焦点指正在接收键盘输入的元素。在整个桌面上,只能有一个具有键盘焦点的元素。在 WPF 中,具有键盘焦点的元素会将 IsKeyboardFocused 设置为 true。静态 Keyboard 方法 FocusedElement 返回当前具有键盘焦点的元素。
可以通过按 Tab 键定位到某个元素或者在某些元素(如 TextBox)上单击鼠标来获取键盘焦点。也可以通过编程方式使用 Keyboard 类的 Focus 方法获取键盘焦点。Focus 将尝试对指定的元素给予键盘焦点。Focus 返回的元素是当前具有键盘焦点的元素。
为了使元素能够获取键盘焦点,Focusable 属性和 IsVisible 属性必须设置为 true。某些类(如 Panel)默认情况下将 Focusable 设置为 false;因此,如果您希望该元素能够获取焦点,则必须将此属性设置为 true。
下面的示例使用 Focus 在 Button 上设置键盘焦点。建议在应用程序中的 Loaded 事件处理程序中设置初始焦点。
private void OnLoaded(object sender, RoutedEventArgs e)
{
// Sets keyboard focus on the first Button in the sample.
Keyboard.Focus(firstButton);
}
有关键盘焦点的更多信息,请参见焦点概述。
逻辑焦点
逻辑焦点指焦点范围中的 FocusManager.FocusedElement。一个应用程序中可以有多个具有逻辑焦点的元素,但在一个特定的焦点范围中只能有一个具有逻辑焦点的元素。
焦点范围是一个容器元素,该元素跟踪其焦点范围内的 FocusedElement。当焦点离开焦点范围时,焦点元素会失去键盘焦点,但保留逻辑焦点。当焦点返回到焦点范围时,焦点元素会再次获得键盘焦点。这使得键盘焦点可以在多个焦点范围之间切换,但将确保在焦点返回时,焦点范围中的焦点元素可再次获得焦点。
在可扩展应用程序标记语言 (XAML) 中,通过将 FocusManager 的附加属性 IsFocusScope 设置为 true,或者在代码中使用 SetIsFocusScope 方法设置附加属性,可以将元素转变为焦点范围。
下面的示例通过设置 IsFocusScope 附加属性将 StackPanel 转变为焦点范围。
<StackPanel Name="focusScope1"
FocusManager.IsFocusScope="True"
Height="200" Width="200">
<Button Name="button1" Height="50" Width="50"/>
<Button Name="button2" Height="50" Width="50"/>
</StackPanel>
StackPanel focuseScope2 = new StackPanel();
FocusManager.SetIsFocusScope(focuseScope2, true);
WPF 中默认情况下即为焦点范围的类有 Window、Menu、ToolBar 和 ContextMenu。
具有键盘焦点的元素还具有它所属的焦点范围的逻辑焦点;因此,在具有 Keyboard 类或基元素类的 Focus 方法的元素上设置焦点将尝试对该元素给予键盘焦点和逻辑焦点。
若要确定焦点范围中的焦点元素,请使用 GetFocusedElement。若要更改焦点范围中的焦点元素,请使用 SetFocusedElement。
有关逻辑焦点更多信息,请参见焦点概述。
鼠标位置
WPF 输入 API 提供了与坐标空间有关的有用信息。例如,坐标 (0,0) 是左上角坐标,但该坐标是树中那一个元素的左上角坐标?是属于输入目标的元素?是在其上附加事件处理程序的元素,还是其他内容?为了避免混淆,WPF 输入 API 要求,在处理通过鼠标获取的坐标时,应指定参考框架。GetPosition 方法返回与指定元素相关的鼠标指针的坐标。
鼠标捕获
鼠标设备拥有称为鼠标捕获的特定模式特征。鼠标捕获用于在启动拖放操作时,保持转换的输入状态,以便涉及鼠标指针的名义屏幕上位置的其他操作无需发生。在拖动过程中,未终止拖放操作时用户无法单击,这将使得大多数 mouseover 提示在拖动来源拥有鼠标捕获时是不合适的。输入系统公开了可确定鼠标捕获状态的 API,以及可强制在特定元素上捕获鼠标或清除鼠标捕获状态的 API。有关拖放操作的更多信息,请参见拖放概述。
命令
使用命令,输入处理可以更多地在语义级别(而不是在设备输入级别)进行。命令是简单的指令,如 Cut、Copy、Paste 或 Open。命令对于集中命令逻辑很有用。同一命令可通过 Menu、在 ToolBar 上或者通过键盘快捷方式来访问。命令还提供了在命令不可用时禁用控件的机制。
RoutedCommand 是 ICommand 的 WPF 实现。执行 RoutedCommand 时,将在命令目标上引发 PreviewExecuted 和 Executed 事件,这两个事件与其他输入一样,都通过元素树进行隧道和冒泡操作。如果未设置命令目标,则具有键盘焦点的元素将成为命令目标。执行该命令的逻辑将附加到 CommandBinding。当 Executed 事件访问该特定命令的 CommandBinding 时,将调用 CommandBinding 上的 ExecutedRoutedEventHandler。此处理程序执行命令的操作。
有关发出命令的更多信息,请参见命令概述。
WPF 提供了一个由 ApplicationCommands、MediaCommands、ComponentCommands、NavigationCommands 和 EditingCommands 组成的常见命令库,您也可以定义自己的命令库。
下面的示例演示如何设置 MenuItem,以便单击该菜单项时,将对 TextBox 调用 Paste 命令(假定 TextBox 具有键盘焦点)。
<StackPanel>
<Menu>
<MenuItem Command="ApplicationCommands.Paste" />
</Menu>
<TextBox />
</StackPanel>
// Creating the UI objects
StackPanel mainStackPanel = new StackPanel();
TextBox pasteTextBox = new TextBox();
Menu stackPanelMenu = new Menu();
MenuItem pasteMenuItem = new MenuItem();
// Adding objects to the panel and the menu
stackPanelMenu.Items.Add(pasteMenuItem);
mainStackPanel.Children.Add(stackPanelMenu);
mainStackPanel.Children.Add(pasteTextBox);
// Setting the command to the Paste command
pasteMenuItem.Command = ApplicationCommands.Paste;
有关 WPF 中的命令的更多信息,请参见命令概述。
输入系统和基元素
输入事件(如由 Mouse、Keyboard 和 Stylus 类定义的附加事件)由输入系统引发,并基于运行时针对可视化树的命中测试而注入到对象模型中的特定位置。
Mouse、Keyboard 和 Stylus 定义为附加事件的每个事件也由基元素类 UIElement 和 ContentElement 再次公开为新的路由事件。基元素路由事件由处理原始附加事件和重用事件数据的类生成。
当输入事件通过其基元素输入事件实现与特定源元素关联时,该事件可通过基于逻辑树对象和可视化树对象组合的事件路由的其余部分进行路由,并可通过应用程序代码进行处理。通常,在 UIElement 和 ContentElement 上使用路由事件处理这些与设备相关的输入事件要更加方便,因为您可以在 XAML 和代码中使用更直观的事件处理程序语法。您可以选择处理启动了该进程的附加事件,但将会面临多个问题:该附加事件可能标记为由基元素类处理来进行处理,并且您需要使用访问器方法而不是实际事件语法,才能对附加事件附加处理程序。
接下来的内容
现在有多种方法可处理 WPF 中的输入。您也应对各种类型的输入事件和 WPF 使用的路由事件机制有进一步的了解。
也可以获取更详细说明了 WPF 框架元素和事件路由的其他资源。有关更多信息,请参见以下概述:命令概述、焦点概述、基元素概述、WPF 中的树和路由事件概述。