绘制批处理 DrawList
每一帧你的窗口可能画几百个矩形、文字和图片。如果每个控件都
SetWorkingImage一次,CPU 会累死在 API 调用上。HiEasyX 的DrawList是一套延迟渲染机制:先把命令记下来,最后按裁剪矩形排序,一批搞定。
核心设计
控件调用 DrawListRecorder FlushDrawList
------------> -------------------> ------------->
painter->DrawRect() 追加 HXDrawCmd 到列表 排序 + 批量执行
painter->DrawText() (不真正画图) (真正 SetWorkingImage)
为什么快?
| 模式 | 1000 个矩形耗时 |
|---|---|
| 立即模式(传统) | 1000 次 SetWorkingImage |
| DrawList 批处理 | 可能只要 5~10 次 SetWorkingImage |
关键不是少画东西,而是减少了渲染状态的切换。
数据结构
HXDrawCmd
struct HXDrawCmd {
HXDrawCmdType Type; // 命令类型
HXRect ClipRect; // 该命令的裁剪区域
// 联合体或变体,存储具体参数:
// - Rect/FilledRect: 坐标 + 颜色
// - Text: 字符串 + 字体 + 颜色 + 位置
// - Line: 起点 + 终点 + 颜色 + 样式
// - Image: IMAGE* + 目标矩形
// - Painter: SubPainter + 目标位置(用于 Viewport/DockSlot 的 blit)
};
HXDrawCmdType
enum class HXDrawCmdType {
Rect,
FilledRect,
FilledRoundRect,
Text,
Line,
LineStyled,
FilledPolygon,
Image,
Painter // SubPainter 回贴到父 painter
};
HXDrawList
struct HXDrawList {
std::vector<HXDrawCmd> Commands;
};
API 详解
FlushDrawList
void FlushDrawList(HXBufferPainter *painter, HXDrawList &list);
参数
| 参数 | 类型 | 说明 |
|---|---|---|
painter | HXBufferPainter * | 真正的目标绘制后端(通常是窗口的 SubPainter)。 |
list | HXDrawList & | 要刷新的命令列表。执行后列表会被清空。 |
执行逻辑
- 按
ClipRect对命令排序(相同裁剪区的命令聚在一起)。 - 遍历排序后的命令:
- 如果裁剪区变了,调用
painter->Begin(clipRect)设置新的工作区域。 - 执行具体绘制命令。
- 如果裁剪区变了,调用
- 最后统一
painter->End()。
当前排序只按 ClipRect 分组,不保证重叠命令的原始时序。如果你的绘制严重依赖“后画的覆盖先画的”(如半透明混合),确保它们拥有相同的 ClipRect,否则顺序可能被重排。
窗口如何自动使用 DrawList
你通常不需要手动调用 FlushDrawList,因为 Window() 已经帮你做好了:
Window()创建一个HXDrawListRecorder代理对象。- 把代理设为
window->Painter。 - 控件拿到的
CurrentPainter()其实是这个代理。 - 控件的
DrawRect、DrawText等调用被转成HXDrawCmd追加到window->DrawList。 - 下一个
Window()开始、或HX::End()时,自动FlushDrawList(window->RealPainter, window->DrawList)。
以下控件会创建自己的 SubPainter,它们直接画到 SubPainter,只有最后的 blit 被记录到 DrawList:
ScrollerViewportDockSlotTextInput(光标闪烁等局部刷新)
这是正确的:SubPainter 的内容已经在自己的缓冲区里画好了,DrawList 只需要记录“把它贴到父窗口的哪里”。
手动使用 DrawList
如果你要写一个需要大量图元的自定义控件,可以手动操作 DrawList:
#include <include/hex.h>
#include <include/impl/EasyX/hex_impl_easyx.h>
void CustomHeavyControl() {
// 获取当前窗口的 DrawList(假设框架暴露此接口)
HX::HXDrawList *drawList = HX::CurrentPainter() // 实际框架自动管理 DrawList,如需手动操作请查阅源码;
// 追加大量命令
for (int i = 0; i < 100; ++i) {
HX::HXDrawCmd cmd;
cmd.Type = HX::HXDrawCmdType::FilledRect;
cmd.ClipRect = currentClip;
// ... 填充坐标颜色 ...
drawList->Commands.push_back(cmd);
}
// 不需要手动 Flush,Window 切换或 End() 时自动处理
}
如果你拿到了 RealPainter 直接画,同时又在往 DrawList 里追加命令,可能导致时序错乱:RealPainter 的内容会立即出现,而 DrawList 的内容要等到 Flush 才出现。建议统一走 DrawList 或统一走立即模式。
完整示例:理解渲染流程
#include <include/hex.h>
#include <include/impl/EasyX/hex_impl_easyx.h>
void DemoDrawList() {
// ========== 窗口 A ==========
HX::WindowProfile wpa;
wpa.Title = HXStr("Window A");
HX::Window(HXStr("win_a"), wpa);
HX::ButtonProfile btn;
HX::Button(HXStr("Btn A1"), btn); // 这些调用被记录到 win_a->DrawList
HX::Button(HXStr("Btn A2"), btn);
HX::End();
// <-- 这里自动 Flush win_a 的 DrawList 到它的 RealPainter
// ========== 窗口 B ==========
HX::WindowProfile wpb;
wpb.Title = HXStr("Window B");
HX::Window(HXStr("win_b"), wpb);
HX::Text(HXStr("Hello from Window B")); // 记录到 win_b->DrawList
HX::End();
// <-- 自动 Flush win_b
// ========== Render() 把所有 RealPainter blit 到屏幕 ==========
HX::Render();
}
调试 DrawList
ShowMetricsWindow() 会显示每个窗口的 DrawList 命令数量。如果某个窗口的命令数异常高(比如上万),说明你的自定义控件可能在无节制地追加命令。考虑用 FilledPolygon 替代大量细长三角形,或用 Image 缓存复杂图案。
在开发模式下,你可以遍历 window->DrawList.Commands 打印每个命令的类型和裁剪区,排查“为什么这个文字没画出来”——通常是 ClipRect 设置错误,导致命令被排序到了错误的批次。
总结
| 概念 | 说明 |
|---|---|
HXDrawListRecorder | 拦截绘制调用的代理 painter |
HXDrawCmd | 一条延迟执行的绘制命令 |
FlushDrawList | 排序并批量执行命令,减少状态切换 |
| 自动集成 | Window() 自动创建 recorder,End() / 下一 Window() 自动 flush |
| SubPainter blit | Viewport、DockSlot 等的最终回贴也被记录为 Painter 命令 |
DrawList 是 HiEasyX 从“能跑”到“跑得流畅”的关键优化。你不需要时刻想着它,但知道它在那里默默帮你省下了几百次 API 调用,挺好 ⚡