键盘类与事件处理
- WPF框架中内置了
System.Input.Keyboard
基础键盘类
,该类提供了丰富的键盘相关功能,包括描述键盘状态
的属性、处理键盘操作
的方法以及一系列事件。这些键盘事件不仅直接由Keyboard类
提供,还通过UIElement等XAML基元素类
向外传递。 - 在
处理键盘输入
时,常用的两个事件组
是:
- KeyDown和PreviewKeyDown事件:当
键盘键
被按下
时,这两个事件
会被触发
。KeyDown事件属于冒泡路由事件
,这意味着它会从底层元素向上传播
到包含它的元素
。而PreviewKeyDown事件是隧道路由事件
,它会将事件沿着逻辑树向上传递
,直到它被处理或路由到根元素
。 - KeyUp和PreviewKeyUp事件:当
键盘键
被释放
时,这两个事件
会被触发
。KeyUp事件同样是冒泡路由事件
,而PreviewKeyUp事件则是隧道路由事件
。
- 如果要使某个
UI元素
能够接收并响应
键盘输入,首要条件是该元素必须具有焦点
。大部分UIElement的派生类默认
都可获取焦点
,但是像StackPanel和Canvas这类Panel类,默认情况下其Focusable
属性被设置为false
,因此不能
直接获取焦点
。若需此类元素能响应键盘输入,则需要显式
将其Focusable
属性设为true
。 - 下面写个以
PreviewKeyDown
为例子:
<Window x:Class="WpfKeyboardEventsApp.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:WpfKeyboardEventsApp"mc:Ignorable="d"Title="MainWindow" Height="450" Width="800" Focusable="True" PreviewKeyDown="Window_PreviewKeyDown"><Grid><!--添加TextBlock用来显示键盘输入显示效果--><TextBlock x:Name="myTextBlock" Text="这里演示“静音”、“增大音量”、“减小音量”这三个快捷键" Foreground="White" Background="Green" FontSize="16" Height="33" Padding="10,6" HorizontalAlignment="Center"></TextBlock></Grid>
</Window>
using System.Windows;
using System.Windows.Input;namespace WpfKeyboardEventsApp
{/// <summary>/// Interaction logic for MainWindow.xaml/// </summary>public partial class MainWindow : Window{public MainWindow(){InitializeComponent();}private void Window_PreviewKeyDown(object sender, KeyEventArgs e){if (e.Key == Key.VolumeMute){// 按下“静音”键myTextBlock.Text = "按下“静音”键";e.Handled = true;}else if (e.Key == Key.VolumeUp){// 按下“增大音量”键myTextBlock.Text = "按下“增大音量”键";e.Handled = true;}else if (e.Key == Key.VolumeDown){// 按下“减小音量”键myTextBlock.Text = "按下“减小音量”键";e.Handled = true;}}}
}
上述代码,当用户在窗口中按下键盘按键时,将调用名为Window_PreviewKeyDown的事件处理程序来处理按键事件。效果如下:
鼠标类与鼠标事件处理
- WPF框架内建的
System.Input.Mouse
类为开发人员提供了丰富的鼠标相关功能,涵盖了关于鼠标状态的各种事件、方法及属性。类似于键盘事件的处理方式,Mouse类所包含的这些事件同样通过UIElement和其他XAML基元素类传递给应用程序。 - 鼠标事件主要分为以下几个类别,每个类别都有对应的冒泡路由事件和隧道路由事件对:
- MouseDown和PreviewMouseDown事件:当用户
按下鼠标
按钮时触发,用于响应鼠标键按下动作。 - MouseUp和PreviewMouseUp事件:在用户
释放鼠标
按钮时发生,用于处理鼠标键抬起的动作。 - MouseEnter和PreviewMouseEnter事件:当
鼠标光标进入
控件区域时通知,标志着鼠标从外部移入控件。 - MouseLeave和PreviewMouseLeave事件:当
鼠标离开
控件边界时激活,表明鼠标从控件内部移至外部区域。 - MouseMove和PreviewMouseMove事件:随着
鼠标在控件范围内移动
而持续触发,可用于实时追踪鼠标的移动轨迹。
要对于获取精确的鼠标
位置,可通过调用Mouse类的GetPosition
静态方法,该方法接受一个UIElement参数,返回的是相对于指定控件坐标系内的鼠标位置坐标
,从而使得开发者能够根据具体控件上下文来捕获并处理鼠标的位置变化。
- 下面写个例子:
<Grid><Grid.RowDefinitions><RowDefinition></RowDefinition><RowDefinition></RowDefinition><RowDefinition></RowDefinition></Grid.RowDefinitions><!--添加一个矩形来演示鼠标的相关操作--><Rectangle x:Name="myRectangle" Grid.Row="0" Canvas.Left="246" Canvas.Top="46" Height="118" Stroke="Black" Width="200" Fill="White"MouseEnter="myRectangle_MouseEnter" MouseLeave="myRectangle_MouseLeave" MouseMove="myRectangle_MouseMove" MouseDown="myRectangle_MouseDown"MouseWheel="myRectangle_MouseWheel"></Rectangle><!--添加TextBlock来显示相关提示--><TextBlock x:Name="myTextBlock" Grid.Row="1" HorizontalAlignment="Center" FontSize="16" Foreground="Green" Text="鼠标滚动会改变矩形的宽度"></TextBlock></Grid>
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;namespace WpfKeyboardEventsApp
{/// <summary>/// MouseWindow.xaml 的交互逻辑/// </summary>public partial class MouseWindow : Window{public MouseWindow(){InitializeComponent();}private void myRectangle_MouseEnter(object sender, MouseEventArgs e){// 鼠标进入控件时,控件的颜色为蓝色myRectangle.Fill = new SolidColorBrush(Colors.Blue);}private void myRectangle_MouseLeave(object sender, MouseEventArgs e){// 鼠标离开控件时,控件的颜色为白色myRectangle.Fill = new SolidColorBrush(Colors.White);}private void myRectangle_MouseMove(object sender, MouseEventArgs e){// 获取基于Rectangle的鼠标的坐标Point pointBaseRectangle = Mouse.GetPosition(myRectangle);myTextBlock.Text = $"鼠标位置矩形的底部为 ({pointBaseRectangle.X},{pointBaseRectangle.Y})";myTextBlock.Text += "\r\n";// 获取基于窗体的鼠标的坐标Point pointBaseWindow = Mouse.GetPosition(this);myTextBlock.Text += $"鼠标位置基于窗口为 ({pointBaseWindow.X},{pointBaseWindow.Y})";}private void myRectangle_MouseDown(object sender, MouseButtonEventArgs e){// 获取点出的鼠标的按钮,是左边还是右边MouseButton button = e.ChangedButton;myTextBlock.Text += "\r\n";myTextBlock.Text += $"鼠标按键是{button.ToString()}";}private void myRectangle_MouseWheel(object sender, MouseWheelEventArgs e){if (e.Delta > 0){// 如果向上推动滚轮,图形的宽度增加myRectangle.Width++;}if (e.Delta < 0){// 如果向下推动滚轮,图形的宽度减小myRectangle.Width--;}}}
}
上述代码,包含一个矩形和一个文本块。当用户在矩形上执行鼠标操作时,将调用相应的事件处理程序来处理这些操作。文本块用于显示相关提示信息,效果图如下:
WPF中的焦点处理机制
- 在WPF应用程序中,焦点处理涉及到两个关键概念:
键盘焦点和逻辑焦点
。其中,键盘焦点
特指当前接收键盘输入
的元素,而逻辑焦点
则是在特定焦点范围内的焦点
元素。
1.键盘焦点
- 是当前
接受键盘
输入的对象,整个桌面同一时间只能有一个元素拥有键盘焦点
。在WPF中,若一个元素具有键盘焦点,其IsKeyboardFocused
属性将被设置为true
。通过Keyboard
类的静态属性FocusedElement
可以获取当前拥有键盘焦点的元素。
若要使某个UI元素能够获取键盘焦点,需要确保该元素的Focusable
属性和IsVisible
属性均设为true
。例如,默认情况下,Panel等基类的Focusable属性为false,因此需手动设置为true以允许此类元素获取焦点。
用户可以通过交互操作(如按Tab键切换或点击元素
)来改变键盘焦点。此外,还可以使用Keyboard.Focus(element)
方法以编程方式尝试将键盘焦点赋予指定元素,但返回的结果可能是当前真正获得键盘焦点的元素,因为如果有其他因素阻止了请求,则结果可能与预期不符。 - 下面来写个例子:
<StackPanel HorizontalAlignment="Center"><!--设置Focusable为True 表示可以用键盘获取到焦点--><TextBox x:Name="myTextBox" Focusable="True" Text="我是TextBox1" Margin="0,10" GotKeyboardFocus="myTextBox_GotKeyboardFocus"/><TextBox x:Name="myTextBox2" Focusable="True" Text="我是TextBox2" Margin="0,10" GotKeyboardFocus="myTextBox_GotKeyboardFocus"/><TextBlock x:Name="myTextBlock" Text="我是一个提示,用来提示谁获取焦点获取焦点" Margin="0,10" Foreground="Red"/>
</StackPanel>
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;namespace WpfKeyboardEventsApp
{/// <summary>/// FocusWindow.xaml 的交互逻辑/// </summary>public partial class FocusWindow : Window{public FocusWindow(){InitializeComponent();// 初始化设置myTextBox为焦点Keyboard.Focus(myTextBox);}private void myTextBox_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e){myTextBlock.Text = $"{((TextBox)sender).Name} 获取了键盘焦点";}}
}
上述代码,包含两个输入框和一个文本宽,默认设置第一个输入框为焦点,利用Tab切换,文本显示对应的切换提示,效果图如下:
2.逻辑焦点
- 体现在
FocusManager.FocusedElement
上,它指的是在一个焦点范围内
的焦点元素。当键盘焦点离开焦点范围时,尽管该元素失去了键盘焦点,但仍保留逻辑焦点。当键盘焦点重新回到焦点范围内时,该元素会再次获得键盘焦点,这使得键盘焦点能在多个焦点范围间切换,同时保证焦点返回时,焦点范围内的焦点元素能重获键盘焦点。
-在一个应用程序中,可以存在多个拥有逻辑焦点的元素,但在单一焦点范围内仅有一个元素拥有逻辑焦点。可通过调用FocusManager.GetFocusScope(element)
得到指定元素所在的焦点范围,而FocusManager.GetFocusedElement(scope)
用于获取指定焦点范围内的焦点元素,FocusManager.SetFocusedElement(scope, element)
则用来设置焦点范围内的焦点元素,通常用于设定初始焦点。 - 下面来写个例子:
<StackPanel HorizontalAlignment="Center"><!--设置TabControl 下面的内容TextBox可以获取到键盘焦点--><TabControl><TabItem Header="Tab导航一"><TextBox x:Name="tab1TextBox" Text="我是Tab1的内容,我这里仅仅之作演示" Focusable="True" Foreground="Red" Height="200"/></TabItem><TabItem Header="Tab导航二"><TextBox x:Name="tab2TextBox" Focusable="True" Text="我是Tab2的内容,我这里仅仅之作演示" Foreground="Blue" Height="200"/></TabItem></TabControl>
</StackPanel>
上述代码,在一个具有多个TabItem的TabControl中,每个TabItem可以视为一个独立的焦点范围。当切换到另一个TabItem时,即使当前TabItem中的元素失去了键盘焦点,它仍保留逻辑焦点。若返回原来的TabItem,先前获得焦点的元素会重新获取键盘焦点,效果图如下:
3.键盘导航
- 当用户按下
导航键
(如Tab、Shift+Tab、Ctrl+Tab组合键以及方向键
)时,KeyboardNavigation
类负责实现默认的键盘焦点导航行为。通过设置附加属性KeyboardNavigation.TabNavigation、ControlTabNavigation和DirectionalNavigation
,可以自定义导航容器的导航策略,这些属性可取值包括Continue
(继续导航。当键盘导航到达元素的末尾时,导航将继续到下一个元素)、Local
(本地循环。当键盘导航到达元素的末尾时,导航将从头开始导航元素。)、Contained
(包含循环。当键盘导航到达元素的末尾时,导航将从头开始导航元素,直到所有元素都被导航。)、Cycle
(循环。当键盘导航到达元素的末尾时,导航将从头开始导航元素,直到所有元素都被导航。)、Once
(一次。当键盘导航到达元素的末尾时,导航将停止。)和None(
无。当键盘导航到达元素的末尾时,导航将不执行任何操作。),默认为Continue
。 - 下面来写个例子:
<StackPanel HorizontalAlignment="Center"><!--设置按下Tab键时仅在其内部循环移动焦点,直到所有的都被导航停止--><ListBox KeyboardNavigation.TabNavigation="Contained"><ListBoxItem Content="我是ListBox选项1"/><ListBoxItem Content="我是ListBox选项2"/><ListBoxItem Content="我是ListBox选项3"/><ListBoxItem Content="我是ListBox选项4"/><ListBoxItem Content="我是ListBox选项5"/><ListBoxItem Content="我是ListBox选项6"/><ListBoxItem Content="我是ListBox选项7"/><ListBoxItem Content="我是ListBox选项8"/></ListBox>
</StackPanel>
上述代码,ListBox在按下Tab键时仅在其内部循环移动焦点,不跳转到其他控件上,到最后一个元素导航,效果图如下:
4.焦点事件
- 关于键盘焦点的事件包括
PreviewGotKeyboardFocus、GotKeyboardFocus、PreviewLostKeyboardFocus以及LostKeyboardFocus
。这些事件作为Keyboard类
的附加事件,但在实际开发中更常作为基元素类上的路由事件使用。
具体来说,当元素获取键盘焦点时触发GotKeyboardFocus
事件;失去键盘焦点时引发LostKeyboardFocus
事件。如果在预处理阶段即PreviewGotKeyboardFocus或 PreviewLostKeyboardFocus
事件中设置了Handled为true
,则焦点状态将不会发生改变。 - 下面来写个例子:
<StackPanel HorizontalAlignment="Center"><!--设置TextBox添加三个事件--><TextBox x:Name="myTextBox" Focusable="True" PreviewGotKeyboardFocus="myTextBox_PreviewGotKeyboardFocus" GotKeyboardFocus="myTextBox_GotKeyboardFocus" LostKeyboardFocus="myTextBox_LostKeyboardFocus" Width="180" Margin="0,10"/><TextBox Focusable="True" Text="我是用来测试焦点转移" Width="180" Margin="0,10"/><TextBlock x:Name="myTextBlock" Text="我是一个提示,用来提示焦点相关信息" Foreground="Red" FontSize="16"/><Button Content="设置/取消阻止转移焦点" Click="Button_Click" Width="180" Margin="0,30" Background="Green" Foreground="White" FontSize="16"></Button></StackPanel>
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;namespace WpfKeyboardEventsApp
{/// <summary>/// FocusWindow.xaml 的交互逻辑/// </summary>public partial class FocusWindow : Window{private bool disableFocus = false; //禁止转移焦点public FocusWindow(){InitializeComponent();// 初始化设置myTextBox为焦点Keyboard.Focus(myTextBox);}private void myTextBox_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e){//获得焦点//myTextBlock.Text = $"{((TextBox)sender).Name} 获取了键盘焦点";myTextBlock.Text = $"我获得焦点了";myTextBox.Background = Brushes.Yellow;}private void myTextBox_PreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e){//焦点转移时触发if (disableFocus){myTextBlock.Text = $"我焦点转移阻止了";e.Handled = true; // 阻止焦点转移}}private void myTextBox_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e){//失去焦点myTextBlock.Text = $"我失去焦点了";myTextBox.Background = Brushes.White;}private void Button_Click(object sender, RoutedEventArgs e){disableFocus = !disableFocus;}}
}
上述代码,当TextBox获取或失去键盘焦点时,触发相应的事件,效果图如下:
公众号“点滴分享技术猿”