跳到主要内容

绘制批处理 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);

参数

参数类型说明
painterHXBufferPainter *真正的目标绘制后端(通常是窗口的 SubPainter)。
listHXDrawList &要刷新的命令列表。执行后列表会被清空。

执行逻辑

  1. ClipRect 对命令排序(相同裁剪区的命令聚在一起)。
  2. 遍历排序后的命令:
    • 如果裁剪区变了,调用 painter->Begin(clipRect) 设置新的工作区域。
    • 执行具体绘制命令。
  3. 最后统一 painter->End()
排序的局限

当前排序只按 ClipRect 分组,不保证重叠命令的原始时序。如果你的绘制严重依赖“后画的覆盖先画的”(如半透明混合),确保它们拥有相同的 ClipRect,否则顺序可能被重排。


窗口如何自动使用 DrawList

你通常不需要手动调用 FlushDrawList,因为 Window() 已经帮你做好了:

  1. Window() 创建一个 HXDrawListRecorder 代理对象。
  2. 把代理设为 window->Painter
  3. 控件拿到的 CurrentPainter() 其实是这个代理。
  4. 控件的 DrawRectDrawText 等调用被转成 HXDrawCmd 追加到 window->DrawList
  5. 下一个 Window() 开始、或 HX::End() 时,自动 FlushDrawList(window->RealPainter, window->DrawList)
SubPainter 持有者例外

以下控件会创建自己的 SubPainter,它们直接画到 SubPainter,只有最后的 blit 被记录到 DrawList:

  • Scroller
  • Viewport
  • DockSlot
  • TextInput(光标闪烁等局部刷新)

这是正确的: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() 时自动处理
}
不要混用立即模式和 DrawList

如果你拿到了 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

MetricsWindow 里看

ShowMetricsWindow() 会显示每个窗口的 DrawList 命令数量。如果某个窗口的命令数异常高(比如上万),说明你的自定义控件可能在无节制地追加命令。考虑用 FilledPolygon 替代大量细长三角形,或用 Image 缓存复杂图案。

命令检查

在开发模式下,你可以遍历 window->DrawList.Commands 打印每个命令的类型和裁剪区,排查“为什么这个文字没画出来”——通常是 ClipRect 设置错误,导致命令被排序到了错误的批次。


总结

概念说明
HXDrawListRecorder拦截绘制调用的代理 painter
HXDrawCmd一条延迟执行的绘制命令
FlushDrawList排序并批量执行命令,减少状态切换
自动集成Window() 自动创建 recorder,End() / 下一 Window() 自动 flush
SubPainter blitViewport、DockSlot 等的最终回贴也被记录为 Painter 命令

DrawList 是 HiEasyX 从“能跑”到“跑得流畅”的关键优化。你不需要时刻想着它,但知道它在那里默默帮你省下了几百次 API 调用,挺好 ⚡