跳到主要内容

核心概念

要真正用好 HiEasyX,我们需要理解几个关键的设计思想。别担心,这些概念都不难,我会尽量用大白话讲清楚。

IMGUI 范式:每帧重建界面

HiEasyX 采用的是 IMGUI(Immediate Mode GUI) 设计。这和传统的"保留模式 GUI"(比如 Windows 的 Win32 API、Qt)有本质区别。

保留模式 GUI(传统方式)

在传统 GUI 中,你需要:

  1. 创建一个按钮对象 → 它一直"存在"在内存里
  2. 绑定一个回调函数 → 点击时自动调用
  3. 手动销毁对象 → 否则内存泄漏
// 传统方式的伪代码
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);
}
ℹ️ 哪些 Profile 需要 static?
控件跨帧状态举例
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 之前做一些预处理(比如截获某些快捷键、做输入映射)。把消息泵的控制权交给你,灵活性更高。

完整的消息处理流程

  1. EasyX 从 Windows 获取原始消息(WM_MOUSEMOVEWM_LBUTTONDOWN 等)
  2. GetHXMessage(&msg) 把它转换成 HiEasyX 的 HXMessage 结构体
  3. PushMessage() 把它放入内部消息队列
  4. 控件内部通过查询 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() 等容器控件使用。我们会在后续文档中详细介绍。

→ 继续阅读:主题与样式