跳到主要内容

停靠空间 DockSpace

想搭一个像 VS Code 或者 Unity 那样带侧边栏、底部面板、中间工作区的 IDE 布局?DockSpace 就是你的布局骨架。

DockSpace 把整块屏幕划分成 左、右、上、下、中 五个区域。你可以把不同的 Window 塞进这些槽位里,还能让多个面板共用同一个槽位,自动出现 TabBar 切换。拖拽分割条就能调整各区域大小,关闭程序前保存一下布局,下次打开原样恢复——用户会以为你做了一个真正的原生应用。


函数原型

// 开始一个停靠空间
void BeginDockSpace(const HXString &Id, DockSpaceProfile &Profile);

// 结束停靠空间
void EndDockSpace(const HXString &Id, DockSpaceProfile &Profile);

// 开始一个槽位(旧版重载,默认可关闭)
bool BeginDockSlot(const HXString &DockSpaceId, HXDockSlot Slot,
const HXString &PanelId = HXString());

// 开始一个槽位(新版重载,可指定是否允许关闭)
bool BeginDockSlot(const HXString &DockSpaceId, HXDockSlot Slot,
const HXString &PanelId, bool Closable);

// 强制激活某个槽位里的指定面板
void SetDockSlotActivePanel(const HXString &DockSpaceId, HXDockSlot Slot,
const HXString &PanelId);

// 获取最近一次被关闭的面板 ID(每帧只返回一次)
HXString GetDockSlotClosedPanel(const HXString &DockSpaceId, HXDockSlot Slot);

// 保存所有 DockSpace 分割比例 + Window 位置到文件
void SaveDockLayout(const HXString &path);

// 从文件恢复布局
void LoadDockLayout(const HXString &path);

// 查询某窗口是否有保存过的位置
bool HasSavedWindowPosition(const HXString &title);

// 获取某窗口保存时的位置 / 尺寸
HXPoint GetSavedWindowPosition(const HXString &title);
HXPoint GetSavedWindowSize(const HXString &title);

参数详解

DockSpaceProfile

成员类型默认值说明
AvailableRectHXRectDockSpace 可用的总矩形区域,通常传 {0, 0, 屏幕宽, 屏幕高}
LeftRatiofloat-1.0f左侧面板占可用宽度的比例
RightRatiofloat-1.0f右侧面板占可用宽度的比例
TopRatiofloat-1.0f顶部面板占可用高度的比例
BottomRatiofloat-1.0f底部面板占可用高度的比例
SplitterSizeint8分割条的有效点击宽度(像素)
SplitterDraggablebooltrue分割条是否允许拖拽调整

比例值的三个语义

取值含义
-1.0f恢复保存状态。如果之前调用过 LoadDockLayout,会沿用用户调整后的比例;否则相当于 0.0f
0.0f显式禁用这个槽位,不分配任何空间
> 0.0f设置比例,例如 0.25f 表示占 25%
想避免"幽灵槽位"

如果你只设置了 RightRatio = 0.25f,却把 TopRatio / BottomRatio 留成 -1.0f,那么之前保存过的顶部/底部比例会被意外恢复。建议用不到的槽位一律显式置 0.0f

HXDockSlot 枚举

说明
HX::HXDockSlot::Left左侧边栏
HX::HXDockSlot::Right右侧边栏
HX::HXDockSlot::Top顶部面板
HX::HXDockSlot::Bottom底部面板
HX::HXDockSlot::Center中间主工作区,占据剩余所有空间

返回值

函数返回值说明
BeginDockSlotbool对于 MDI 多面板模式,只有当前被激活的 Tab 才会返回 true,其余返回 false。单面板模式永远返回 true
GetDockSlotClosedPanelHXString本帧被关闭的面板 ID;如果没有则为空字符串
HasSavedWindowPositionbool是否存在已保存的位置记录
GetSavedWindowPositionHXPoint保存的 {X, Y}
GetSavedWindowSizeHXPoint保存的 {W, H}(用 HXPoint 结构承载尺寸)

最小示例:五区布局

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

void MyApp() {
static HX::DockSpaceProfile dsp{}; // 必须是 static / 全局
dsp.AvailableRect = HXRect{0, 0, 1024, 768};
dsp.LeftRatio = 0.20f; // 左侧占 20%
dsp.RightRatio = 0.20f; // 右侧占 20%
dsp.TopRatio = 0.0f; // 顶部禁用
dsp.BottomRatio = 0.15f; // 底部占 15%
// Center 自动占剩余空间

HX::BeginDockSpace(HXStr("main"), dsp);

// ---------- 左侧面板 ----------
if (HX::BeginDockSlot(HXStr("main"), HX::HXDockSlot::Left)) {
HX::Text(HXStr("资源浏览器"));
HX::Separator();
HX::Button(HXStr("导入资源"), HX::ButtonProfile{});
HX::EndDockSlot();
}

// ---------- 右侧面板 ----------
if (HX::BeginDockSlot(HXStr("main"), HX::HXDockSlot::Right)) {
HX::Text(HXStr("属性检查器"));
HX::Slider1f(HXStr("透明度"), alpha, HX::SliderProfile1f{});
HX::EndDockSlot();
}

// ---------- 底部面板 ----------
if (HX::BeginDockSlot(HXStr("main"), HX::HXDockSlot::Bottom)) {
HX::Text(HXStr("输出 / 终端"));
HX::EndDockSlot();
}

// ---------- 中间主工作区 ----------
if (HX::BeginDockSlot(HXStr("main"), HX::HXDockSlot::Center)) {
HX::Text(HXStr("这里是核心编辑区"));
HX::EndDockSlot();
}

HX::EndDockSpace(HXStr("main"), dsp);
}

MDI 多面板 TabBar

BeginDockSlot 传一个 PanelId,同一个 Slot 内可以塞多个面板,HiEasyX 会自动在槽位顶部渲染一个 TabBar

HX::BeginDockSpace(HXStr("ide"), dsp);

// 右侧放两个可切换的面板
if (HX::BeginDockSlot(HXStr("ide"), HX::HXDockSlot::Right, HXStr("Inspector"))) {
HX::Text(HXStr("对象属性..."));
HX::EndDockSlot();
}
if (HX::BeginDockSlot(HXStr("ide"), HX::HXDockSlot::Right, HXStr("Blueprint"))) {
HX::Text(HXStr("蓝图编辑器..."));
HX::EndDockSlot();
}

HX::EndDockSpace(HXStr("ide"), dsp);
TabBar 状态去哪了?

当前激活的 Tab 索引会被 HiEasyX 自动写进 HXStatePool,所以你不需要自己维护变量。下次打开程序时,只要 LoadDockLayout() 就能恢复。


可关闭标签与中键关闭

默认情况下,带 PanelId 的 Tab 是可关闭的,右上角会画一个小 ×

如果你某个面板不想让用户关掉(比如"欢迎页"),用四参数重载把 Closable 设成 false

// 不可关闭的欢迎页
if (HX::BeginDockSlot(HXStr("ide"), HX::HXDockSlot::Center,
HXStr("Welcome"), false)) {
HX::Text(HXStr("欢迎使用 HiEasyX!"));
HX::EndDockSlot();
}

// 可关闭的普通文档
if (HX::BeginDockSlot(HXStr("ide"), HX::HXDockSlot::Center,
HXStr("main.cpp"), true)) {
HX::Text(HXStr("int main() { ... }"));
HX::EndDockSlot();
}

中键关闭:在可关闭的 Tab 上按鼠标中键,也能直接关闭,不用瞄准那个小叉。

想知道用户关了哪个面板?用 GetDockSlotClosedPanel

HXString closed = HX::GetDockSlotClosedPanel(HXStr("ide"), HX::HXDockSlot::Center);
if (!closed.empty()) {
// 从你自己的文档列表里移除 closed
}

布局持久化:SaveDockLayout / LoadDockLayout

// 退出前保存
if (userPressedSaveLayout) {
HX::SaveDockLayout(HXStr("layout.ini"));
}

// 启动时加载
HX::LoadDockLayout(HXStr("layout.ini"));

保存的文件是 INI-like 格式,包含两部分:

  • [DockSpace:id] 段:记录每个 DockSpace 的 LeftRatioRightRatio
  • [Window:Title] 段:记录每个 Window() 的位置和大小
Window 位置自动恢复

只要你先 LoadDockLayout(),之后调用 Window() 时,HiEasyX 会自动把窗口挪到保存的位置。不用手动调用 GetSavedWindowPosition(),除非你想做自定义动画过渡。


完整示例:IDE 风格主界面

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

void IDEDemo() {
// ---- 1. 初始化 / 恢复布局 ----
static bool layoutLoaded = false;
if (!layoutLoaded) {
HX::LoadDockLayout(HXStr("ide_layout.ini"));
layoutLoaded = true;
}

// ---- 2. 定义 DockSpace ----
static HX::DockSpaceProfile dsp{};
dsp.AvailableRect = HXRect{0, 0, 1280, 720};
dsp.LeftRatio = 0.22f;
dsp.RightRatio = 0.25f;
dsp.TopRatio = 0.0f; // 不用顶部
dsp.BottomRatio = 0.20f;

HX::BeginDockSpace(HXStr("ide"), dsp);

// ---- 左侧:项目树 ----
if (HX::BeginDockSlot(HXStr("ide"), HX::HXDockSlot::Left, HXStr("Project"))) {
static std::vector<HX::HXTreeNode> nodes = {
{HXStr("src"), true, false, {
{HXStr("main.cpp"), false, false, {}},
{HXStr("app.h"), false, false, {}},
}},
{HXStr("assets"), false, false, {
{HXStr("logo.png"), false, false, {}},
}},
};
HX::TreeViewProfile tvp{};
HX::TreeView(HXStr("proj_tree"), nodes, tvp);
HX::EndDockSlot();
}

// ---- 右侧:属性 / 蓝图(MDI)----
if (HX::BeginDockSlot(HXStr("ide"), HX::HXDockSlot::Right,
HXStr("Inspector"), true)) {
HX::Text(HXStr("Transform"));
static float x = 0, y = 0, z = 0;
HX::Slider1f(HXStr("Pos X"), x, HX::SliderProfile1f{});
HX::Slider1f(HXStr("Pos Y"), y, HX::SliderProfile1f{});
HX::Slider1f(HXStr("Pos Z"), z, HX::SliderProfile1f{});
HX::EndDockSlot();
}
if (HX::BeginDockSlot(HXStr("ide"), HX::HXDockSlot::Right,
HXStr("Blueprint"), true)) {
HX::Text(HXStr("节点图..."));
HX::EndDockSlot();
}

// ---- 底部:终端 / 问题 ----
if (HX::BeginDockSlot(HXStr("ide"), HX::HXDockSlot::Bottom,
HXStr("Terminal"), true)) {
HX::Text(HXStr("> Build succeeded."));
HX::EndDockSlot();
}
if (HX::BeginDockSlot(HXStr("ide"), HX::HXDockSlot::Bottom,
HXStr("Problems"), true)) {
HX::Text(HXStr("0 errors, 0 warnings."));
HX::EndDockSlot();
}

// ---- 中间:编辑器(不可关闭的欢迎页 + 可关闭的文件)----
if (HX::BeginDockSlot(HXStr("ide"), HX::HXDockSlot::Center,
HXStr("Welcome"), false)) {
HX::Text(HXStr("欢迎使用 HiEasyX IDE 原型!"));
HX::Button(HXStr("新建工程"), HX::ButtonProfile{});
HX::EndDockSlot();
}
if (HX::BeginDockSlot(HXStr("ide"), HX::HXDockSlot::Center,
HXStr("main.cpp"), true)) {
static std::vector<HXString> lines = {
HXStr("int main() {"),
HXStr(" return 0;"),
HXStr("}"),
};
HX::TextEditorProfile ep{};
HX::TextEditor(HXStr("editor"), lines, ep);
HX::EndDockSlot();
}

HX::EndDockSpace(HXStr("ide"), dsp);

// ---- 处理关闭事件 ----
HXString closed = HX::GetDockSlotClosedPanel(HXStr("ide"), HX::HXDockSlot::Center);
if (closed == HXStr("main.cpp")) {
// 用户关了 main.cpp,可以做保存提示
}

// ---- 按 F5 保存布局 ----
if (HX::IsKeyPressed(VK_F5)) {
HX::SaveDockLayout(HXStr("ide_layout.ini"));
}
}

常见坑与注意事项

Profile 必须是 static 或全局

DockSpaceProfile 内部会缓存分割条的拖拽状态(ActiveSplitterDragging 等)。如果你在栈上定义 DockSpaceProfile 且没加 static,每帧都是新对象,分割条会永远拖不动。

先 Load 再 Window

LoadDockLayout() 必须在你要恢复的 Window() 调用之前执行,否则那帧已经按默认位置画完了,恢复就失去意义。

分割条拖不动?

SplitterSize 已经从早期的 4 像素提升到 8 像素。如果你还是觉得难命中,检查是不是被上层控件(比如一个全屏的透明 Panel)拦截了鼠标消息。

中心区域永远有空间

即使四个边全都设了比例,Center 槽位仍然会占据剩余空间。你不需要给 Center 设置任何 Ratio

运行效果

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

截图占位符:请补充 停靠空间 DockSpace 运行效果 的运行效果截图,保存为 ./assets/停靠空间 DockSpace_view.png。`n