核心概念
要真正用好 HiEasyX,我们需要理解几个关键的设计思想。别担心,这些概念都不难,我会尽量用大白话讲清楚。
IMGUI 范式:每帧重建界面
HiEasyX 采用的是 IMGUI(Immediate Mode GUI) 设计。这和传统的"保留模式 GUI"(比如 Windows 的 Win32 API、Qt)有本质区别。
保留模式 GUI(传统方式)
在传统 GUI 中,你需要:
- 创建一个按钮对象 → 它一直"存在"在内存里
- 绑定一个回调函数 → 点击时自动调用
- 手动销毁对象 → 否则内存泄漏
// 传统方式的伪代码
Button* btn = new Button("点击我");
btn->OnClick = []() { printf("Clicked!\n"); };
// ... 很久以后 ...
delete btn;
IMGUI 方式(HiEasyX 的做法)
在 IMGUI 中,没有持久的控件对象。你每帧都重新"声明"一次界面:
while (running) {
HX::HXBegin();
// ...
if (HX::Window(HXStr("面板"), wp)) {
if (HX::Button(HXStr("点击我"), bp)) {
printf("Clicked!\n");
}
}
HX::End();
HX::Render();
}
每一帧,Button() 都会被调用一次。如果这一帧鼠标刚好点击了按钮的位置,Button() 就返回 true,你立即处理逻辑。不存在"创建按钮"这个步骤,也不需要回调。
- 代码即布局:界面的结构和代码结构完全一致,读起来像看 HTML 一样直观。
- 无内存管理负担:不用
new/delete控件,框架内部帮你处理一切。 - 状态天然简单:需要记住的东西(比如输入框的文字)交给框架的 StatePool,你不用自己写类来封装。
因为界面是每帧重建的,所以你不能把局部变量的地址作为状态保存下来。比如,你不能在 if 块里定义一个 std::string 然后期望下一帧还能读到它的内容。持久化状态必须使用 static、全局变量,或者 HiEasyX 的 HXStatePool。
Profile:控件资料的容器
几乎每个 HiEasyX 控件都有一个对应的 Profile(资料/配置)结构体。它用来描述控件的外观和行为参数。
HX::ButtonProfile bp;
bp.Size = {120, 32};
if (HX::Button(HXStr("确定"), bp)) {
// 被点击了
}
Profile 的生命周期
这是最关键的一点:
Profile 的生命周期必须跨越至少两帧。
为什么?因为控件内部有很多状态是跨帧持久的,比如:
- 按钮是否正在被按下
- 输入框的光标位置
- 滑动条的当前值
- 窗口的滚动偏移
这些状态都保存在 Profile 结构体里。如果你每帧都创建一个全新的局部 Profile,那这些状态就会全部丢失。
✅ 正确的做法
使用 static 局部变量:
if (HX::Window(HXStr("面板"), wp)) {
static HX::TextInputProfile tip;
HX::TextInput(tip); // 文字内容跨帧保留
}
使用全局变量:
HX::WindowProfile g_MainWindowProfile;
HX::SliderProfile1f g_VolumeSlider;
void DrawUI() {
if (HX::Window(HXStr("主窗口"), g_MainWindowProfile)) {
HX::Slider1f(HXStr("音量"), g_Volume, g_VolumeSlider);
}
}
❌ 错误的做法
// 错误!每帧都是新的 Profile,状态全部丢失
while (running) {
HX::WindowProfile wp; // ❌ 每次循环都重新构造
HX::Window(HXStr("面板"), wp);
}
| 控件 | 跨帧状态举例 |
|---|---|
Window | 位置、大小、滚动偏移、是否折叠 |
TextInput | 当前文字、光标位置、选区 |
Slider | 当前数值、是否正在被拖拽 |
Checkbox | 勾选状态(放在 Profile.Checked 里) |
Dropdown | 当前选中项索引、是否展开 |
像 Button 这种瞬间状态的控件,用局部变量也可以,但为了一致性,建议统一用 static。
帧生命周期详解
一个标准的主循环有固定的顺序,不能打乱:
while (running) {
HX::HXBegin(); // ① 帧开始
ExMessage msg;
while (peekmessage(&msg)) { // ② 获取消息
HX::PushMessage(HX::GetHXMessage(&msg)); // ③ 推送消息
}
HX::WindowProfile wp; // ④ 准备 Profile
if (HX::Window(HXStr("窗口"), wp)) { // ⑤ 创建窗口
HX::Button(HXStr("按钮"), bp); // ⑥ 放置控件
}
HX::End(); // ⑦ 帧结束
HX::Render(); // ⑧ 渲染到屏幕
}
各阶段说明
| 阶段 | 函数 | 作用 |
|---|---|---|
| ① 帧开始 | HXBegin() | 重置临时状态(消息查询、布局栈、DrawList),保留池化对象的容量 |
| ②~③ 消息处理 | peekmessage + PushMessage | 把 EasyX 的原始消息翻译成 HiEasyX 格式并送入队列 |
| ④~⑥ UI 描述 | Window() / Button() 等 | 用代码"描述"这一帧的界面长什么样 |
| ⑦ 帧结束 | End() | Tab 导航处理、布局收尾、解锁绘图状态 |
| ⑧ 渲染 | Render() | 合成所有子缓冲区,执行 DrawList,最终输出到屏幕 |
HXBegin()必须在任何控件调用之前Render()必须在所有控件和End()之后- 消息推送必须在
HXBegin()之后、第一个控件 之前 - 如果把
Render()放在Window()前面,你会看到黑屏或上一帧的内容
消息处理机制
HiEasyX 不直接读取系统消息,而是采用"推送式"设计:
while (peekmessage(&msg)) {
HX::PushMessage(HX::GetHXMessage(&msg));
}
为什么不自动读取?
因为 EasyX 的 peekmessage 有多种过滤模式,而且你可能想在消息进入 HiEasyX 之前做一些预处理(比如截获某些快捷键、做输入映射)。把消息泵的控制权交给你,灵活性更高。
完整的消息处理流程
- EasyX 从 Windows 获取原始消息(
WM_MOUSEMOVE、WM_LBUTTONDOWN等) GetHXMessage(&msg)把它转换成 HiEasyX 的HXMessage结构体PushMessage()把它放入内部消息队列- 控件内部通过查询
HXMessage::Processed标志来判断消息是否已被消费
如果你只想让 HiEasyX 处理鼠标消息,可以这样做:
while (peekmessage(&msg, EM_MOUSE)) {
HX::PushMessage(HX::GetHXMessage(&msg));
}
键盘消息仍然由你自己的逻辑处理,互不干扰。
多个 Window 的创建方式
HiEasyX 支持同时创建多个窗口面板,它们会自动管理层级关系。
简单多窗口
HX::WindowProfile wp1, wp2;
wp1.Where = {50, 50};
wp1.Size = {300, 200};
wp2.Where = {400, 100};
wp2.Size = {300, 200};
if (HX::Window(HXStr("窗口 A"), wp1)) {
HX::Text(HXStr("这是 A"));
}
if (HX::Window(HXStr("窗口 B"), wp2)) {
HX::Text(HXStr("这是 B"));
}
窗口层级
后创建的窗口会叠在前面(画家算法)。如果你点击了后面的窗口,它会自动提升到最前面。这个行为由 HiEasyX 内部的聚焦管理系统自动处理,你不需要手动干预。
带关闭按钮的窗口
HX::WindowProfile wp;
wp.CloseButton = true; // 显示右上角 × 按钮
if (HX::Window(HXStr("可关闭"), wp)) {
HX::Text(HXStr("点 × 可以关闭我"));
}
当用户点击关闭按钮后,下一帧 Window() 会返回 false,你可以据此决定要不要继续绘制它。
Window() 创建的是一个独立的浮动面板,有自己的标题栏和边框。如果你想在窗口内部做更复杂的布局(比如左右分栏、滚动区域),可以配合 BeginPanel()、BeginScroller()、BeginDockSpace() 等容器控件使用。我们会在后续文档中详细介绍。
→ 继续阅读:主题与样式