Handler: 处理器
Preview: 预览、这指隧道
Raise: 引发
Bubble: 冒泡
Handled: 已处理
理解路由事件
事件路由允许源自某个元素的事件由另一个元素引发。
定义、注册和封装路由事件
public partial class Window1 : Window
{// 定义路由事件,必须是 static readonly// 类型 RoutedEventpublic static readonly RoutedEvent TestEvent;public Window1(){InitializeComponent();}// 也可以写在外面吧static Window1(){// 在静态构造函数中注册路由事件// 使用 EventManager.RegisterRoutedEvent 注册Window1.TestEvent = EventManager.RegisterRoutedEvent("Click", // 事件名称RoutingStrategy.Bubble, // 路由类型 Bubble:冒泡typeof(RoutedEventHandler), // 定义事件处理程序语法的委托typeof(Window1) // 拥有事件的类);}// 路由事件通过普通的 .NET 事件进行封装// 两个封装器:AddHandler 和 RemoveHandler// 这两个方法都在 FrameworkElement 基类中定义,并被每个 WPF 元素继承public event RoutedEventHandler Click{add { base.AddHandler(Window1.TestEvent, value)}remove { base.RemoveHandler(Window1.TestEvent, value)}}
}
👀 RoutingStrategy.Bubble
是枚举值,指示是冒泡路由还是隧道路由
参考文章:如何创建自定义路由事件 - WPF .NET | Microsoft Learn
共享路由事件
UIElement.MouseUpEvent = Mouse.MouseUpEvent.AddOwer(typeof(UIElement));
UIElement 类是所有普通元素的起点,通过 AddOwer() 方法征用事件,可令路由事件在类之间共享。
引发路由事件
RaiseEvent() 方法负责为每个已经通过 AddHandler() 方法注册的调用程序引发事件。
protected void OnTestEvent()
{RoutedEventArgs e = new RoutedEventArgs(Window1.TestEvent, this);base.RaiseEvent(e);
}
RoutedEventArgs 是事件参数类的基类。
处理路由事件
即关联事件处理程序。引发事件以后需要做出相应的处理。
方式一:
XAM 标记添加事件特性:(以“元素名_事件名”命名事件处理方法,没有名字的话使用“元素类型_事件名”)
<Image Name="img" MouseUp="img_MouseUp"/>
方式二:
img.MouseUp += new MouseButtonEventHandloer(img_MouseUp);
// 等效于
img.MouseUp += img_MouseUp; // 隐藏委托
方式三:
img.AddHandler(Image.MouseUpEvent, new MouseButtonEventHandler(img_MouseUp));
// 等效于
img.AddHandler(UIElement.MouseUpEvent, new MouseButtonEventHandler(img_MouseUp));
断开路由事件的几种写法:
img.MouseUp -= img_MouseUp;
img.RemoveHandler(Image.MouseUpEvent, new MouseButtonEventHandler(img_MouseUp));
事件路由
路由事件分三种:
- 直接路由事件
- 向上传递的冒泡路由事件
- 向下传递的隧道路由事件
可以在路由路径中任意一层完成对事件的处理
每个冒泡路由事件都对应有一个以Privew开关的隧道路由事件。
private void img_MouseUp(object sender, MouseButtonEventArgs e)
{}
sender 参数提供了对整个链条上最后那个链接的引用。(就是最后一个对象)
⭐RoutedEventArgs 类属性说明:
- Source: 指示引发了事件的对象;
- OriginalSource: 指示最初是什么对象引发了事件。💡 OriginalSource 属性值通常与 Source 属性值相同。
- RoutedEvent: 通过事件处理程序为触发的事件提供 RoutedEvent 对象。
- Handled: 该属性允许终止事件的冒泡或隧道过程。
冒泡路由事件
protected int eventCounter = 0;private void SomethingClicked(object sender, MouseButtonEventArgs e)
{eventCounter++;string message = "#" + eventCounter.ToString() + "\r\n" +" Sender: " + sender.ToString() + "\r\n" +" Source: " + e.Source + "\r\n" +" Original Source: " + e.OriginalSource;lstMessages.Items.Add(message);e.Handled = (bool)chkHandle.IsChecked; // 当为 true 时,终止路由传递
}
💡 大多数 WPF 元素没有提供 Click 事件,而是提供了更直接的 MouseDown 和 MouseUp 事件, Click 事件专用于基于按钮的控件。
❓处理挂起的事件
当设置第三个参数为 true 时,即使设置了 Handled 标志,也将接收到事件。
❌ 这一般是一种错误的做法,系统默认会挂起 MouseUp 事件而优先去处理 Click 事件。
cmdClear.AddHandler(UIElement.MouseUpEvent, new MouseButtonEventHandler(cmdClear_MouseUp), true);
⭐附加事件
<Grid Width="300"Height="300"Background="AliceBlue" Button.Click="DoSomething"><Grid Width="200"Height="200"Background="Yellow"><StackPanel Width="100"Height="100"Background="LightGreen"><Button Content="Btn1" /><Button Content="Btn2" /><Button Content="Btn3" /></StackPanel></Grid></Grid>
Button.Click="DoSomething"
就是附加事件,可以在 Grid 层处理按钮的路由事件。
💡 Button.Click="DoSomething"
也可以写成 ButtonBase.Click="DoSomething"
,但是,因为 RadioButton、CheckBox 也继承自 ButtonBase,因此如果只想处理 Button 对象的事件,则需要明确使用第一种写法。
在代码中关联附加事件
在代码中不能使用 += 关联附加事件,需要使用AddHandler()
pnlButtons.AddHandler(Button.Click, new RoutedEventHandler(DoSomething));
隧道路由事件
隧道路由事件的工作方式和冒泡路由事件相同,但方向相反。
隧道路由事件以单词 Preview 开头。
WPF 通常成对地定义冒泡路由事件和隧道路由事件。
💡 隧道路由事件总在冒泡路由事件之前被触发。
💡 如果将隧道路由事件标记为已处理过,那就不会发生冒泡路由事件。这是因为两个事件共享 RoutedEventArges 类的同一个实例。
❓如果需要执行一些预处理(根据键盘上特定的键执行动作或过滤特定的鼠标动作),隧道路由事件是非常有用的。
有时也称预览事件。
WPF 事件
WPF 事件的分类:
- 生命周期事件
- 鼠标事件
- 键盘事件
- 手写笔事件
- 多点触控事件
鼠标、键盘、手写笔及多点触控事件都是输入事件。
生命周期事件
所有元素的生命周期事件:
- Initialized
- Loaded
- Unloaded
窗口初始化时会自下而上初始化每个元素,也就是说根元素总是最后才会被初始化。
Window 类的生命周期事件
- SourceInitialized: 当取得窗口句柄前发生;
- ContentRendered: 在窗口首次呈现后立即发生;
- Activated: 当用户切换到窗口时发生,相当于控件的 GotFocus 事件;
- Deactivated: 当用户从该窗口切换到其他窗口时发生,相当于控件的 LostFocus 事件;
- Closing: 当关闭窗口时发生;
- Closed: 当窗口已经关闭后发生,此时仍可访问元素对象。
输入事件
输入事件是当用户使用某些各类的外设硬件进行交互时发生的事件,例如鼠标、键盘、手写笔或多点触控屏。输入事件可通过继承自 InputEventArgs 的自定义事件参数类传递额外的信息。
InputEventArgs 增加了两个属性:
- Timestamp: 指示事件是何时发生的;
- Device: 指示事件发生的相关设备对象。
键盘输入
所有元素的键盘事件(按顺序)
- PreviewKeyDown
- KeyDown
- PreviewTextInput
- TextInput
- PreviewKeyUp
- KeyUp
private void KeyEvent(object sender, KeyEventArgs e)
{string message = "Event: " + e.RoutedEvent + " " +" Key: " + e.Key;
}
KeyEventArgs 枚举可获得按钮属性。
当连续按住一个键时会引发连接的事件,可以通过判断是否是连续按下避免重复触发事件:
if ((bool) chkIgnoreRepeat.IsChecked && e.IsRepeat) return;
private void TextInput(object sender, TextCompositionEventArgs e)
{string message = "Event: " + e.RoutedEvent + " " +" Text: " + e.Text;
}
🔥 ❓
TextInput 事件中,可通过 TextCompositionEventArgs 获取输入文本值。
关于键盘数字:
在主键区的数字和数字区的数字,返回的Key值是不一样的,分别是:Key.D9 和 Key.NumPad9
可以通过转换将得到相同的数字:(但这个仅限于数字才能转换成一样的)
KeyConverter converter = new KeyConverter();
string ke = converter.ConvedrToString(e.Key);
💡 如果在文本框中按下了空格,将直接绕过 PreviewTextInput 事件,这意味着还处理 PreviewKeyDown 事件。
💡 只有当字符可以“输入”到元素中 时,都会触发 TextInput 事件。比如空格键等。
💡 当发生 PreviewTextInput 事件时,一般会挂起 TextInput 事件。
焦点
WPF 中用户一次只能使用一个控件。即具有焦点的控件。
Focussable="true" 属性可设置控件是否具有焦点。
Tab 键可以切换控件焦点。
TabIndex 可以设置控件的焦点顺序。0是第一个。
IsTabStop = “fasle” 属性将阻止控件被包含进 Tab 焦点顺序。
Visibility 设置是否显示。
IsEnabled 设置是否可用。
获取键盘状态
使用 KeyboardDevice 属性获取键盘状态:(速度慢,有时候状态跟不上输入速度)
if((e.KeyboardDevice.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
{lblInfo.Text = "You held the Control key.";
}
KeyboardDevice 方法:
- IskeyDown()
- IskeyUp()
- IsKeyToggled()
- GetKeyState()
使用 KeyBoard 类
if(Keyboard.IsKeyDown(Key.LeftShift))
{lblInfo.Text = "The left Shift is held down.";
}
鼠标输入
MouseEnter: 当指针移动到元素上时发生事件
MouseLeave: 当指针离开元素时发生事件
💡 这两个事件是直接事件
PreviewMouseMove 和 MouseMove 是路由事件
MouseEventArgs 包含鼠标状态,其中该属性的 GetPosition() 方法可获得鼠标相对于元素的位置
Point pt = e.GetPosition(this);
IsMouseOver 或 IsMouseDirectlyOver 可确定当前鼠标是否位于某元素上
鼠标单击事件
PreviewMoseWheel 和 MoseWheel 鼠标滚动事件
所有元素的鼠标单击事件:
- PreviewMoseLeftButtonDown/PreviewMoseRightButtonDown
- MouseLeftButtonDown/MouseRightButtonDown
- PreviewMouseLeftButtonUp/PreviewMoseRightButtonUp
- MouseLeftButtonUp/MouseLeftButtonUp
MouseButtonEventArgs 继承自 MouseEventArgs 对象
- MouseButton: 指示哪个鼠标按键
- ButtonState: 指示鼠标按钮状态
- ClickCount: 指示是单击还是双击事件
捕获鼠标
💡 如果单击一个元素,保持按下鼠标键,然后移动鼠标指针离开该元素,这里该元素就不会接收到鼠标键释放事件。
Mouse.Capture() 方法可捕获鼠标事件
LostMouseCapture 事件可响应鼠标丢失问题
鼠标拖放
💡 需要指定拖放的源和目标,拖放操作可交换任意类型的对象
👉 拖动
private void lblSource_MouseDown(object sender, MouseButtonEventArgs e)
{Label lbl = (Label)sender;DragDrop.DoDragDrop(lbl, lbl.Content, DragDropEffects.Copy);// DragDrop.DoDragDrop 调用此方法进行拖动// 参数:源,被移动的内容,移到方式上复制
}
👉 接收
<Label AllowDrop="True">To Here</Label>
<--!AllowDrop="True" 允许接收被拖动的内容-->
👉 如果需要对接收的内容进行处理: DragEnter 事件
private void lblTarget_DragEnter(object sender, DragEventArgs e)
{if(e.Data.GetDataPresent(DataFormats.Text))e.Effects = DragDropEffects.Copy; // 将鼠标变成复制外形elsee.Effects = DragDropEffects.None; // 将鼠标变成一条线
}
👉 接收拖放数据事件
private void lblTarget_Drop(object sender, DrogEventArgs e)
{((Lable)sender).Content = e.Data.GetData(DataFormats.Text);
}
也可以实现两个应用程序间的数据拖放操作。需要使用 System.Windows.Clipboard类。👀 我的理解:就是实现复制、粘贴操作。
多点触控输入
💡 单击和文本输入属于高层次输入;鼠标事件及按键事件属于低层次输入。
触控输入的三个层次:
- 原始触控:例如绘图操作。
- 操作:移动、绽放、旋转以及轻按。
- 内置的元素支持:有些元素已经对多点触控事件提供了内置支持,如可滚动的支持触控移动:ListBox、ListView、DataGrid、TextBox以及ScrollViewer。
原始触控事件
- PreviewTouchDown
- TouchDown
- PreviewTouchMove
- TouchMove
- PreviewTouchUp
- TouchUp
- TouchEnter
- TouchLeave
TouchEventArgs 对象提供的两个重要成员:
- GetTouchPoint()
- TouchDevice: 指的是每个触点,并通过 TouchDeviceId 进行区分
可在按下事件、移动事件、抬起事件中,通过获取当前触控点,画一个圆点来显示用户的手指位置,也可以移动这些圆点,并且在抬起后将圆点元素删除掉。
同样支持类似于鼠标的事件丢失处理。略。
操作(移动、转运、缩小或放大)
💡 将元素的 IsManipulationEnabled 属性设置为 True,将元素配置为接受触控操作。
💡 这样,元素将响应4个事件:(它们是冒泡事件)
- ManipulationStarting:当触摸对象时触发此事件
- ManipulationStarted
- ManipulationDelta:在操作过程中持续触发此事件
- ManipulationCompleted
惯性
当用户结束手势并抬起手指释放元素时,会触发 ManipulationInertiaStarting 事件(冒泡事件),可使用 ManipulationInertiaStartingEventsArgs 对象确定当前速度,然后通过代码设置希望的减速度。
路由传播机制参考文章:
走进WPF之路由事件 - 老码识途呀 - 博客园