跳到主要内容

视口 Viewport

想在一个窗口里再嵌套一个"小屏幕",比如预览缩略图、小地图、或者把一段复杂 UI 隔离渲染?Viewport 就是为此而生的离屏画布。

Viewport 会创建一个独立的子缓冲区(SubPainter),你在 BeginViewportEndViewport 之间写的所有控件,都会被绘制到这个离屏缓冲区里,最后整体贴回父窗口。它自带裁剪和坐标偏移,鼠标点进去也不用你手动算坐标。


函数原型

// 开始一个视口。返回 true 表示成功创建了子缓冲区
bool BeginViewport(ViewportProfile &Profile);

// 结束视口,将子缓冲区的内容 blit 回父 painter
void EndViewport(ViewportProfile &Profile);

参数详解

ViewportProfile

成员类型默认值说明
SizeHXPoint{300, 200}视口的宽和高(像素)
内部字段说明

ViewportProfile 还包含 _internal 字段(SubPainterSavedPainterSavedScrollerX/Y 等),这些由 HiEasyX 在 BeginViewport/EndViewport 之间自动维护,不要手动修改


返回值

函数返回值说明
BeginViewportbool子缓冲区创建成功返回 true。极少失败(仅在内存不足或尺寸为 0 时返回 false

最小示例

#include <include/hex.h>
#include <include/impl/EasyX/hex_impl_easyx.h>

void MiniMapDemo() {
// 假设外面已经有一个 Window
HX::Text(HXStr("主场景——下面是小地图视口"));
HX::Spacing();

static HX::ViewportProfile vp{};
vp.Size = {200, 150};

if (HX::BeginViewport(vp)) {
// 下面所有绘制都发生在 200x150 的子缓冲区里
HX::Text(HXStr("MiniMap"));

// 画一些标记点
HX::Button(HXStr("Player"), HX::ButtonProfile{});
HX::SameLine();
HX::Button(HXStr("Boss"), HX::ButtonProfile{});

HX::EndViewport(vp);
}
// 这里 200x150 的图像已经被贴到主窗口的当前布局位置
}

嵌套在 Scroller / Panel 里

Viewport 完全可以放在 Scroller 或者 Panel 内部,而且鼠标坐标会自动偏移。你不需要自己减去滚动条位置或者父窗口坐标。

static HX::PanelProfile panel{};
panel.Size = {400, 300};
HX::BeginPanel(HXStr("preview_panel"), panel);

static HX::ScrollerProfile scroller{};
if (HX::BeginScroller(HXStr("scroller"), scroller)) {

// 视口放在滚动容器里
static HX::ViewportProfile vp{};
vp.Size = {380, 260};
if (HX::BeginViewport(vp)) {
HX::Text(HXStr("被滚动着的离屏内容"));
HX::Image(HXStr("preview.png"), HX::ImageProfile{});
HX::EndViewport(vp);
}

HX::EndScroller(scroller);
}

HX::EndPanel(panel);

Viewport vs. Image:什么时候用谁?

场景推荐方案理由
显示一张已有的图片文件HX::Image()直接贴图,零额外内存开销
需要在这个"小屏幕"里放按钮、文本、滑动条等交互控件Viewport子缓冲区拥有完整的控件生命周期和消息路由
预览一个复杂子界面(比如材质预览、节点预览)Viewport可以复用已有 UI 代码,不用重写一套贴图逻辑
只需要画几何图形/线框,不需要交互两者皆可Viewport 更灵活;如果只是静态图,Image 更轻
需要逐像素操作或者自定义 shader(未来后端)Viewport子缓冲区本质是 HXBufferPainter,可随时扩展
一句话总结

Image显示一张图Viewport在里面再搭一套 UI


完整示例:带交互的小地图 + 主视口

#include <include/hex.h>
#include <include/impl/EasyX/hex_impl_easyx.h>

void GameEditorUI() {
HX::WindowProfile mainWin{};
mainWin.Size = {1024, 768};
HX::Window(HXStr("GameEditor"), mainWin);

// ========== 左上:属性面板 ==========
HX::Text(HXStr("角色属性"));
static float hp = 100.0f;
HX::Slider1f(HXStr("HP"), hp, HX::SliderProfile1f{});
HX::Separator();

// ========== 右侧大视口:3D 预览 ==========
static HX::ViewportProfile previewVp{};
previewVp.Size = {600, 500};

if (HX::BeginViewport(previewVp)) {
// 在 600x500 的离屏画布上画图
HX::Text(HXStr("Scene Preview (Viewport)"));
HX::Spacing();

// 模拟一些编辑器控件
static bool showGrid = true;
HX::Checkbox(HXStr("Show Grid"), HX::CheckboxProfile{showGrid});
HX::SameLine();
HX::Button(HXStr("Recompile Shaders"), HX::ButtonProfile{});

// 再嵌套一个小视口做缩略图(Viewport 支持嵌套!)
static HX::ViewportProfile thumbVp{};
thumbVp.Size = {120, 90};
if (HX::BeginViewport(thumbVp)) {
HX::Text(HXStr("Thumb"));
HX::EndViewport(thumbVp);
}

HX::EndViewport(previewVp);
}

// ========== 左下:小地图视口 ==========
static HX::ViewportProfile miniMapVp{};
miniMapVp.Size = {200, 200};

if (HX::BeginViewport(miniMapVp)) {
HX::Text(HXStr("MiniMap"));

// 小地图里的按钮真的可以点!
if (HX::Button(HXStr("Center"), HX::ButtonProfile{})) {
// 点击后把主相机移回原点
}

HX::EndViewport(miniMapVp);
}
}

常见坑与注意事项

不要手动改 _internal 字段

SavedPainterSubPainterSavedScrollerX/Y 都由框架自动填充。你改了轻则画面错位,重则崩溃。

Viewport 不裁剪 DrawLine / DrawText 越界

如果你直接在 Viewport 的 SubPainter 上调原始的 DrawLineDrawText 且线段超出了 Size,它不会被额外裁剪。不过因为 SubPainter 本身只有 Size 那么大,越界部分自然就画到了缓冲区外面,不会污染父窗口——只是你自己看不到而已。

SubPainter 会自动进对象池

Viewport 创建的子缓冲区由 HXSubPainterPool 统一管理,帧与帧之间会复用,不会每帧都 new/delete。放心大胆地用,但别在热路径上每帧变尺寸,否则池子命中率下降。

不能在 Viewport 里调用 Window()

Window() 会创建独立的窗口面板和 SubPainter,和 Viewport 的子缓冲区机制冲突。如果你需要在 Viewport 内部做"子窗口"效果,请用 PanelGroup

运行效果

截图占位符:请补充 $name 的运行效果截图。

截图占位符:请补充 视口 Viewport 运行效果 的运行效果截图,保存为 ./assets/视口 Viewport_view.png。`n