事件路由 Event Router
HiEasyX 的传统消息处理是每个控件自己扫
Context.MessageQuery,看谁顺眼就处理谁。控件一多、层级一深,就容易出现“谁吃了我的消息”的悬案。事件路由就是要终结这种混乱:统一走Capture -> Target -> Bubble三阶段,让每条消息都有明确的去向。
核心设计
事件路由借鉴了浏览器 DOM 的事件模型:
Capture 阶段:从外向内 ┐
Target 阶段:命中目标 ┼→ 任一阶段都可调用 PreventDefault / StopPropagation
Bubble 阶段:从内向外 ┘
目前 HiEasyX 同时支持新旧两套系统。HXMessage::Processed 是旧系统的标记,HXEvent::CancelBubble 和 DefaultPrevented 是新系统的标记。控件可以逐步迁移,不必一次性重写。
数据结构
HXEventPhase
enum class HXEventPhase {
Capture, // 捕获阶段:从根向叶子传播
Target, // 目标阶段:事件到达命中测试的控件
Bubble // 冒泡阶段:从叶子向根回溯
};
HXEvent
struct HXEvent {
size_t MessageIndex; // 对应 Context.MessageQuery 中的索引
HXEventPhase Phase; // 当前阶段
HXRect TargetRect; // 目标控件的矩形(屏幕坐标)
bool CancelBubble; // 是否阻止继续冒泡
bool DefaultPrevented; // 是否阻止默认行为
};
CancelBubble = true:事件不再传给父级或兄弟,但同一控件的其他监听器仍可能收到(如果支持多监听器)。DefaultPrevented = true:保留传播,但跳过框架内置的默认处理(如TextInput的字符插入)。
事件路由器
HXEventRouter
class HXEventRouter {
public:
void Reset(); // 清空本帧事件
void Sync(); // 从 MessageQuery 同步生成事件队列
const std::vector<HXEvent> &Events() const;
};
API 详解
GetEventRouter
HXEventRouter &GetEventRouter();
获取全局事件路由器实例。
ResetEventRouter
void ResetEventRouter();
每帧 HXBegin() 内部会自动调用,清空上一帧的事件状态。
SyncEventRouter
void SyncEventRouter();
把 Context.MessageQuery 中的原始消息转换成 HXEvent 列表,并按控件层级分配 Phase。通常在 HXBegin() 之后、Window() 之前由框架自动调用。
GetEventMessage
HXMessage *GetEventMessage(HXEvent &event);
通过事件的 MessageIndex 反查原始 HXMessage 指针。用于在事件回调里读取鼠标坐标、按键状态等详细信息。
矩形区域消息消费
这些是事件路由提供的便捷工具函数,封装了命中测试和 Processed 标记:
FindMouseEventInRect
HXMessage *FindMouseEventInRect(const HXRect &rect, bool requireUnprocessed = true);
参数
| 参数 | 类型 | 说明 |
|---|---|---|
rect | HXRect | 要检测的矩形(屏幕坐标)。 |
requireUnprocessed | bool | 是否只查找未被其他控件处理过的消息。 |
返回值
HXMessage * — 命中的鼠标消息指针;无则返回 nullptr。
ConsumeMouseEventsInsideRect
bool ConsumeMouseEventsInsideRect(const HXRect &rect, bool pressOnly = false);
参数
| 参数 | 类型 | 说明 |
|---|---|---|
rect | HXRect | 消费该矩形内部的所有鼠标消息。 |
pressOnly | bool | 若为 true,只消费按下事件;false 则消费所有鼠标消息。 |
返回值
bool — 是否消费了至少一条消息。
用于 Modal 弹窗:把背景窗口的鼠标消息全部吃掉,防止点击穿透。
ConsumeMouseEventsOutsideRect
bool ConsumeMouseEventsOutsideRect(const HXRect &rect, bool detectLeftPressOutside);
参数
| 参数 | 类型 | 说明 |
|---|---|---|
rect | HXRect | 安全区矩形。 |
detectLeftPressOutside | bool | 是否仅检测左键按下(true),或任何鼠标事件(false)。 |
返回值
bool — 矩形外是否有未被消费的鼠标事件。
用于 PopupMenu、Dropdown:点击菜单外部时关闭菜单。
完整示例:手动路由一个自定义控件
#include <include/hex.h>
#include <include/impl/EasyX/hex_impl_easyx.h>
void CustomButton(const HXString &id, const HXRect &rect) {
auto &router = HX::GetEventRouter();
// 方法 1:直接用区域消费工具(推荐,兼容旧系统)
HXMessage *msg = HX::FindMouseEventInRect(rect);
bool hovered = (msg != nullptr);
if (hovered && msg->MouseLeftPressed) {
// 按钮被点击
OnClick();
msg->Processed = true; // 旧系统标记
}
// 绘制按钮
HX::CurrentPainter()->DrawFilledRectangle(
rect, hovered ? HX::GetTheme().ButtonOnHoverBackground : HX::GetTheme().ButtonBackground,
hovered ? HX::GetTheme().ButtonOnHoverBackground : HX::GetTheme().ButtonBackground);
}
void DemoEventRouter() {
HX::WindowProfile wp;
HX::Window(HXStr("Event Router Demo"), wp);
HXRect btnRect = {50, 50, 150, 90};
CustomButton(HXStr("my_btn"), btnRect);
HX::End();
}
迁移建议
| 旧写法 | 新写法 |
|---|---|
if (msg->MouseLeftPressed && !msg->Processed) | auto *msg = FindMouseEventInRect(rect); if (msg) |
msg->Processed = true; | msg->Processed = true; + 可选 event.CancelBubble = true; |
直接扫 Context.MessageQuery | 通过 GetEventRouter().Events() 遍历 |
目前两者共存,你可以按自己的节奏迁移。新控件建议直接用事件路由工具函数,少踩坑。
如果旧代码已经 msg->Processed = true,新代码又通过事件路由消费一次同一条消息,会导致下游控件收不到。迁移一个控件时,把它的旧消息扫描逻辑全部替换掉,不要留两套并行。
总结
| 概念 | 说明 |
|---|---|
Capture | 从外层容器向内传播,适合做全局快捷键、Modal 阻断 |
Target | 命中测试的控件本身,主要处理逻辑在这里 |
Bubble | 从内向外回溯,适合做父级联动(如 Panel 高亮子控件选中) |
FindMouseEventInRect | 查矩形内的未处理鼠标消息 |
ConsumeMouseEventsInsideRect | 吃掉矩形内的消息(Modal 背景阻断) |
ConsumeMouseEventsOutsideRect | 检测矩形外的点击(Popup 关闭) |
事件路由是 HiEasyX 从“控件各自为政”走向“确定性输入系统”的桥梁。把消息管清楚,你的 UI 才能稳如老狗 🎯