视口 Viewport
想在一个窗口里再嵌套一个"小屏幕",比如预览缩略图、小地图、或者把一段复杂 UI 隔离渲染?
Viewport就是为此而生的离屏画布。
Viewport 会创建一个独立的子缓冲区(SubPainter),你在 BeginViewport 和 EndViewport 之间写的所有控件,都会被绘制到这个离屏缓冲区里,最后整体贴回父窗口。它自带裁剪和坐标偏移,鼠标点进去也不用你手动算坐标。
函数原型
// 开始一个视口。返回 true 表示成功创建了子缓冲区
bool BeginViewport(ViewportProfile &Profile);
// 结束视口,将子缓冲区的内容 blit 回父 painter
void EndViewport(ViewportProfile &Profile);
参数详解
ViewportProfile
| 成员 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Size | HXPoint | {300, 200} | 视口的宽和高(像素) |
ViewportProfile 还包含 _internal 字段(SubPainter、SavedPainter、SavedScrollerX/Y 等),这些由 HiEasyX 在 BeginViewport/EndViewport 之间自动维护,不要手动修改。
返回值
| 函数 | 返回值 | 说明 |
|---|---|---|
BeginViewport | bool | 子缓冲区创建成功返回 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);
}
}
常见坑与注意事项
SavedPainter、SubPainter、SavedScrollerX/Y 都由框架自动填充。你改了轻则画面错位,重则崩溃。
如果你直接在 Viewport 的 SubPainter 上调原始的 DrawLine 或 DrawText 且线段超出了 Size,它不会被额外裁剪。不过因为 SubPainter 本身只有 Size 那么大,越界部分自然就画到了缓冲区外面,不会污染父窗口——只是你自己看不到而已。
Viewport 创建的子缓冲区由 HXSubPainterPool 统一管理,帧与帧之间会复用,不会每帧都 new/delete。放心大胆地用,但别在热路径上每帧变尺寸,否则池子命中率下降。
Window() 会创建独立的窗口面板和 SubPainter,和 Viewport 的子缓冲区机制冲突。如果你需要在 Viewport 内部做"子窗口"效果,请用 Panel 或 Group。
运行效果
截图占位符:请补充 $name 的运行效果截图。
截图占位符:请补充
视口 Viewport 运行效果的运行效果截图,保存为./assets/视口 Viewport_view.png。`n