跳到主要内容

事件路由 Event Router

HiEasyX 的传统消息处理是每个控件自己扫 Context.MessageQuery,看谁顺眼就处理谁。控件一多、层级一深,就容易出现“谁吃了我的消息”的悬案。事件路由就是要终结这种混乱:统一走 Capture -> Target -> Bubble 三阶段,让每条消息都有明确的去向。

核心设计

事件路由借鉴了浏览器 DOM 的事件模型:

Capture 阶段:从外向内   ┐
Target 阶段:命中目标 ┼→ 任一阶段都可调用 PreventDefault / StopPropagation
Bubble 阶段:从内向外 ┘
迁移桥接

目前 HiEasyX 同时支持新旧两套系统。HXMessage::Processed 是旧系统的标记,HXEvent::CancelBubbleDefaultPrevented 是新系统的标记。控件可以逐步迁移,不必一次性重写。


数据结构

HXEventPhase

enum class HXEventPhase {
Capture, // 捕获阶段:从根向叶子传播
Target, // 目标阶段:事件到达命中测试的控件
Bubble // 冒泡阶段:从叶子向根回溯
};

HXEvent

struct HXEvent {
size_t MessageIndex; // 对应 Context.MessageQuery 中的索引
HXEventPhase Phase; // 当前阶段
HXRect TargetRect; // 目标控件的矩形(屏幕坐标)
bool CancelBubble; // 是否阻止继续冒泡
bool DefaultPrevented; // 是否阻止默认行为
};
CancelBubble vs 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);

参数

参数类型说明
rectHXRect要检测的矩形(屏幕坐标)。
requireUnprocessedbool是否只查找未被其他控件处理过的消息。

返回值

HXMessage * — 命中的鼠标消息指针;无则返回 nullptr

ConsumeMouseEventsInsideRect

bool ConsumeMouseEventsInsideRect(const HXRect &rect, bool pressOnly = false);

参数

参数类型说明
rectHXRect消费该矩形内部的所有鼠标消息。
pressOnlybool若为 true,只消费按下事件;false 则消费所有鼠标消息。

返回值

bool — 是否消费了至少一条消息。

用途

用于 Modal 弹窗:把背景窗口的鼠标消息全部吃掉,防止点击穿透。

ConsumeMouseEventsOutsideRect

bool ConsumeMouseEventsOutsideRect(const HXRect &rect, bool detectLeftPressOutside);

参数

参数类型说明
rectHXRect安全区矩形。
detectLeftPressOutsidebool是否仅检测左键按下(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 才能稳如老狗 🎯