玩了好长时间 imgui,最近才知道 imgui 的 docking 版本,直接讲如何实现自定义窗口布局吧。
docking 版本的布局控制接口在 imgui_internal.h 里面,一直没发现,偶然搜索到了几个关键字,查阅了 github 的几个库的源码,大概了解到了如何使用。
首先要包含头文件:
#include "imgui-docking/imgui_internal.h"
停靠窗口布局设置代码:
1 // 窗口的 ID 和 标题 2 ImGuiID dockspaceID = ImGui::GetID("##ui.dock_space"); 3 const char* UI_DOCK_WINDOW = "##ui.dock_window"; 4 const char* UI_PROJECT_BOX = u8"工程##ui.project"; 5 const char* UI_PROPERTY_BOX = u8"属性##ui.property"; 6 const char* UI_TOOL_BOX = u8"工具##ui.tools"; 7 const char* UI_MESSAGE_BOX = u8"消息##ui.message"; 8 const char* UI_LOG_BOX = u8"日志##ui.log"; 9 const char* UI_VIEW_BOX = u8"##ui.view"; 10 11 const ImGuiViewport* viewport = ImGui::GetMainViewport(); 12 ImGui::SetNextWindowPos(viewport->WorkPos); 13 ImGui::SetNextWindowSize(viewport->WorkSize); 14 ImGui::SetNextWindowViewport(viewport->ID); 15 16 int windowFlags = ImGuiWindowFlags_NoDecoration // 无标题栏、不可改变大小、无滚动条、不可折叠 17 | ImGuiWindowFlags_NoMove // 不可移动 18 | ImGuiWindowFlags_NoBackground // 无背景(背景透明) 19 | ImGuiWindowFlags_MenuBar // 菜单栏 20 | ImGuiWindowFlags_NoDocking // 不可停靠 21 | ImGuiWindowFlags_NoBringToFrontOnFocus // 无法设置前台和聚焦 22 | ImGuiWindowFlags_NoNavFocus // 无法通过键盘和手柄聚焦 23 ; 24 25 // 压入样式设置 26 ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); // 无边框 27 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); // 无边界 28 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); // 无圆角 29 // ImGui::SetNextWindowBgAlpha(0.0f); // 窗口 alpha 为 0,同样可以不显示背景 30 31 ImGui::Begin(UI_DOCK_WINDOW, 0, windowFlags); // 开始停靠窗口 32 ImGui::PopStyleVar(3); // 弹出样式设置 33 34 // 创建停靠空间 35 if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_DockingEnable) { // 判断是否开启停靠 36 // 判断是否有根节点,防止一直重建 37 if (!ImGui::DockBuilderGetNode(dockspaceID)) { 38 // 移除根节点 39 ImGui::DockBuilderRemoveNode(dockspaceID); 40 41 // 创建根节点 42 ImGuiID root = ImGui::DockBuilderAddNode(dockspaceID, ImGuiDockNodeFlags_None); 43 44 // 设置根节点位置大小 45 ImGui::DockBuilderSetNodePos(root, { 0.0f, 0.0f }); 46 ImGui::DockBuilderSetNodeSize(root, ImGui::GetWindowSize()); 47 48 // 分割停靠空间 49 50 // 根节点分割左节点 51 ImGuiID leftNode = ImGui::DockBuilderSplitNode(root, ImGuiDir_Left, 0.25f, nullptr, &root); 52 53 // 根节点分割右节点 54 ImGuiID rightNode = ImGui::DockBuilderSplitNode(root, ImGuiDir_Right, 0.25f / 0.75f, nullptr, &root); 55 56 // 根节点分割下节点 57 ImGuiID bottomNode = ImGui::DockBuilderSplitNode(root, ImGuiDir_Down, 0.25f, nullptr, &root); 58 59 // 左节点分割上下节点 60 ImGuiID leftTopNode, leftBottomNode; 61 ImGui::DockBuilderSplitNode(leftNode, ImGuiDir_Up, 0.5f, &leftTopNode, &leftBottomNode); 62 63 // 设置节点属性 64 65 // 禁止其他窗口/节点分割根节点 66 // ImGui::DockBuilderGetNode(dockspaceID)->LocalFlags |= ImGuiDockNodeFlags_NoDockingSplit; 67 68 // 设置分割到最后的根节点隐藏标签栏 69 ImGui::DockBuilderGetNode(root)->LocalFlags |= ImGuiDockNodeFlags_HiddenTabBar; 70 71 // 设置节点停靠窗口 72 ImGui::DockBuilderDockWindow(UI_PROJECT_BOX, leftTopNode); // 左上节点 73 ImGui::DockBuilderDockWindow(UI_PROPERTY_BOX, leftBottomNode); // 左下节点 74 ImGui::DockBuilderDockWindow(UI_TOOL_BOX, rightNode); // 右边节点 75 76 ImGui::DockBuilderDockWindow(UI_MESSAGE_BOX, bottomNode); // 下面节点同时停靠两个窗口 77 ImGui::DockBuilderDockWindow(UI_LOG_BOX, bottomNode); 78 79 ImGui::DockBuilderDockWindow(UI_VIEW_BOX, root); // 观察窗口平铺“客户区”,停靠的节点是 CentralNode 80 81 // 结束停靠设置 82 ImGui::DockBuilderFinish(dockspaceID); 83 84 // 设置焦点窗口 85 ImGui::SetWindowFocus(UI_VIEW_BOX); 86 } 87 88 // 创建停靠空间 89 ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); 90 ImGui::PushStyleColor(ImGuiCol_DockingEmptyBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); 91 ImGui::DockSpace(dockspaceID, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_None); 92 ImGui::PopStyleVar(); 93 ImGui::PopStyleColor(); 94 } 95 ImGui::End(); // 结束停靠窗口 96 97 // 工程框 98 if (ImGui::Begin(UI_PROJECT_BOX)) { 99 ImGui::LabelText("label", "text"); 100 ImGui::Button("button"); 101 } 102 ImGui::End(); 103 104 // 属性框 105 if (ImGui::Begin(UI_PROPERTY_BOX)) { 106 ImGui::LabelText("label", "text"); 107 ImGui::Button("button"); 108 } 109 ImGui::End(); 110 111 // 工具框 112 if (ImGui::Begin(UI_TOOL_BOX)) { 113 ImGui::LabelText("label", "text"); 114 ImGui::Button("button"); 115 116 if (ImGui::Button(u8"重置布局")) { 117 // 移除根节点,布局会自动重建 118 ImGui::DockBuilderRemoveNode(dockspaceID); 119 } 120 } 121 ImGui::End(); 122 123 // 消息框 124 if (ImGui::Begin(UI_MESSAGE_BOX)) { 125 ImGui::LabelText("label", "text"); 126 ImGui::Button("button"); 127 } 128 ImGui::End(); 129 130 // 日志框 131 if (ImGui::Begin(UI_LOG_BOX)) { 132 ImGui::LabelText("label", "text"); 133 ImGui::Button("button"); 134 } 135 ImGui::End(); 136 137 // 观察窗口,背景设置透明,窗口后面就能进行本地 API 的绘制 138 // 压入样式设置 139 ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); // 无边框 140 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); // 无边界 141 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); // 无圆角 142 // ImGui::SetNextWindowBgAlpha(0.0f); // 窗口 alpha 为 0,同样可以不显示背景 143 144 if (ImGui::Begin(UI_VIEW_BOX, 0, ImGuiWindowFlags_NoBackground)) { // 无背景窗口 145 // 获取窗口坐标 146 ImVec2 pos = ImGui::GetWindowPos(); 147 ImVec2 size = ImGui::GetWindowSize(); 148 149 ImGui::Text("position: %0.2f, %0.2f", pos.x, pos.y); 150 ImGui::Text("size: %0.2f, %0.2f", size.x, size.y); 151 152 // 记录下视口位置给本地 API 使用 153 g_viewportPos = glm::ivec2(pos.x, size.y); 154 g_viewportSize = glm::ivec2(size.x, size.y); 155 } 156 ImGui::End(); 157 ImGui::PopStyleVar(3); // 弹出样式设置
关键的停靠节点分割函数比较绕,比如:
// 左节点分割上下节点
ImGuiID leftTopNode, leftBottomNode;
ImGui::DockBuilderSplitNode(leftNode, ImGuiDir_Down, 0.5f, &leftTopNode, &leftBottomNode);
分割左节点,最后两个参数是输出的两个子节点。而
ImGuiID leftNode = ImGui::DockBuilderSplitNode(root, ImGuiDir_Left, 0.25f, nullptr, &root);
则是分割 root 节点,把分割的第一个节点返回给 leftNode,第二个子节点重新赋值给 root,root 实际上是新的子节点了,可以继续再分割。每个节点只能分割一次,分割成两个节点,分割的方向比如:ImGuiDir_Left、ImGuiDir_Up 等。
最终窗口显示效果:
sdragonx https://github.com/sdragonx
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现