Dear ImGui 官方 FAQ 深度解读:输入分发、ID Stack、纹理标识与字体 DPI 的权威实践
本文以 Dear ImGui 仓库中的官方文档 docs/FAQ.md 为骨架,系统梳理该库从集成、输入分发、ID 唯一性、纹理显示到字体 DPI 处理的核心问题,并结合 imgui.h、imgui.cpp、backends/imgui_impl_dx11.cpp 等源码给出可验证的实现依据。读完本文,你能够独立完成:判断鼠标/键盘输入该交给 ImGui 还是宿主应用、解决标签冲突导致的 ID 撞车、理解 1.92 之后 ImTextureID/ImTextureRef 双标识体系、以及按 DPI 正确缩放字体与样式。
一、基本认知:Dear ImGui 与立即模式 UI 的本质
这个库到底叫什么
官方明确:库的全称是 Dear ImGui(既不是 ImGui,也不是 IMGUI)。"IMGUI"(immediate-mode graphical user interface,立即模式图形用户界面)这个术语在库诞生之前就已存在,是 Unity 等其他项目也在使用的范式名称。为避免歧义、又不影响既有代码基础,作者于 2015 年 12 月将其正式命名为 "Dear ImGui"。
与 Qt/GTK/WPF 等传统工具包的对比
FAQ 给出了一张简化对比表,这里完整保留其核心差异点:
| 维度 | Dear ImGui | 传统工具包(Qt/GTK/WPF 等) |
|---|---|---|
| UI 生成时机 | 每帧全量提交 | 只创建一次,之后被修改 |
| 布局特性 | 完全动态,可随时变化,通常以编程方式发出,适合反映动态数据 | 布局基本静态;若需反映动态数据需要额外且易错的代码 |
| 性能优化方向 | 针对最坏情况优化,频繁变化时依然高效 | 针对"什么都不变"的情况优化,一旦有变化性能下降 |
| 状态存储 | 库只存极少数据,不知道也不记住其他控件 | 库保存完整的控件树和状态,便于排版但数据冗余 |
| 数据同步 | 数据天然保持同步 | 依赖回调/信号槽同步,容易出错 |
| API 复杂度 | 简单、易学,基础操作非常容易;偏底层、抽象少 | 更复杂、更高层抽象、更依赖专业程序员 |
| 运行平台 | 几乎所有平台(含 Web、主机、移动端、老旧系统) | 主要是主流桌面平台 |
FAQ 用两段惯用代码直观展示了两种范式的差别:
// 惯用的 Dear ImGui 代码
if (ImGui::Button("Save"))
MySaveFunction();
ImGui::SliderFloat("Slider", &m_MyValue, 0.0f, 1.0f);
// 传统工具包的惯用代码
UiButton* button = new UiButton("Save");
button->OnClick = &MySaveFunction;
parent->Add(button);
UiSlider* slider = new UiSlider("Slider");
slider->SetRange(0.0f, 1.0f);
slider->BindData<float>(&m_MyValue);
parent->Add(slider);
值得注意的是 FAQ 对 "IMGUI" 一词的界定:IMGUI 指的是 API——应用与 UI 系统之间的接口本身,其特征是应用代码持有数据、成为单一事实来源,双方都尽量少保存对方相关的数据,从而让同步自然且少出错。IMGUI 不指实现——UI 库内部发生什么都不重要。因此市面上"IMGUI 一定怎样怎样"的说法很多是把特定库(如 Dear ImGui,源自游戏行业需求)的特性当成了范式特性。
该用哪个版本
FAQ 建议:Release 标签只是偶尔打一下,同步 master 分支一般是安全且推荐的,库相当稳定,回归问题报告后修复很快。另外存在一个 docking 分支,额外包含 Docking(停靠)与 Multi-viewport(多视口)功能,很多项目在使用,且该分支会定期与 master 保持同步。当前仓库版本号为 imgui.h 中声明的 IMGUI_VERSION "1.93.0 WIP",文中涉及 "1.92 之后" 的新行为(如 ImTextureRef、动态字号)均以该版本线为适用前提。
去哪里找更多资料
FAQ 开篇坦白:这个库目前文档偏少,假定使用者熟悉 C/C++。可依赖的资料包括:examples/ 目录下 20 多个独立示例应用(如 examples/README.txt);imgui_demo.cpp 中的 ImGui::ShowDemoWindow(),它覆盖了大部分特性;docs/BACKENDS.md、docs/EXAMPLES.md、docs/FONTS.md 三份随仓库发布的文档;imgui.cpp 顶部的文档注释与 imgui.h 的 API 注释;以及 ImGui::ShowMetricsWindow(),它虽然定位是调试工具,但暴露大量内部信息,对理解概念非常有帮助。
二、集成要点:输入分发是第一道坎
如何判断输入该给 ImGui 还是给应用
读 ImGuiIO 结构中的 io.WantCaptureMouse、io.WantCaptureKeyboard 和 io.WantTextInput 三个标志:
io.WantCaptureMouse置位时:丢弃/隐藏传给你的主应用的鼠标输入;io.WantCaptureKeyboard置位时:丢弃/隐藏键盘输入;io.WantTextInput置位时:可在移动端/主机上通知系统弹出屏幕键盘。
关键原则:无论上述标志取值如何,鼠标/键盘输入都必须始终传给 Dear ImGui——因为例如需要在"点击空白处取消窗口聚焦"这类场景中检测事件。FAQ 给出的标准写法是:
void MyLowLevelMouseButtonHandler(int button, bool down)
{
// (1) 永远先把鼠标数据转发给 ImGui!默认后端是自动的,自定义后端则:
ImGuiIO& io = ImGui::GetIO();
io.AddMouseButtonEvent(button, down);
// (2) 只有在 ImGui 不捕获时才转发给你的应用/游戏。
if (!io.WantCaptureMouse)
my_game->HandleMouseData(...);
}
从源码看,这三个标志的语义与 FAQ 完全一致,见 imgui.h:
bool WantCaptureMouse; // Set when Dear ImGui will use mouse inputs, in this case do not dispatch them
// to your main game/application (either way, always pass on mouse inputs to imgui).
bool WantCaptureKeyboard; // Set when Dear ImGui will use keyboard inputs, ...
bool WantTextInput; // Mobile/console: when set, you may display an on-screen keyboard.
FAQ 特别警告:io.WantCaptureMouse 比任何手工的"检测鼠标是否悬停在窗口上"都更正确——它能正确处理从你的应用或 ImGui 窗口发起的拖拽、弹窗与模态窗口对输入的屏蔽等情形,不要用悬停判断替代它。imgui.h 中 IsWindowHovered() 的注释也明确写道:如果你是想决定把鼠标分发给谁,不要用这个函数,请用 io.WantCaptureMouse。
另一个容易踩坑的细节:文本输入控件在 Return 键的 KeyDown 事件就释放焦点,因此你应用随后收到的对应 KeyUp 事件通常已经是 WantCaptureKeyboard == false。如果应用逻辑介意这个 KeyUp,可以自己记录哪些 keydown 被 ImGui 消费(比如一个 bool 数组),再过滤掉对应的 keyup。
启用键盘/手柄导航
- 键盘:
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; - 手柄:
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;(需后端支持)
FAQ 说明手柄导航最初是重点(尤其适合 PS4、Switch、XB1 等无鼠标场景),键盘导航现在也越来越可用。更多细节见 imgui.cpp 中 "USING GAMEPAD/KEYBOARD NAVIGATION CONTROLS" 一节的文档注释。
无鼠标、无键盘、无屏幕的机器怎么办
FAQ 给出的方案梯队:
- 共享主机鼠标:使用 Synergy 类方案,把 PC 鼠标无缝共享给主机/平板/手机;其中 micro-synergy-client 项目提供了简洁可移植的
uSynergy.c/.h可嵌入客户端,基于 Synergy 1.x 协议——本仓库也附带了同款文件 examples/libs/usynergy/uSynergy.c 与 examples/libs/usynergy/uSynergy.h,可直接作为嵌入参考。 - 主机用户:考虑用 DualShock4 触摸板或闲置摇杆模拟鼠标光标作为兜底。
- 第三方远程渲染方案:把渲染顶点通过局域网发送出去(netImgui、Remote ImGui、imgui-ws 等思路),让无屏机器也能使用 Dear ImGui。
- 触摸输入:FAQ 建议增大控件命中区域来适应触摸精度不足,但推荐使用鼠标或手柄以便优化屏幕空间利用率。
如何自己写一个后端
FAQ 指向两处权威资料:docs/BACKENDS.md 与 imgui.cpp 顶部的文档。backends/ 目录下有 20 余套现成实现(OpenGL3、Vulkan、DirectX9/10/11/12、Metal、SDL2/3、Win32、Android 等)可作参照。
三、集成排错:小方块与裁剪问题
文本显示成"小方块"
说明渲染器后端没有正确使用字体纹理,或纹理还没上传到 GPU。FAQ 按情形给出排查路径:
- 使用标准后端且版本较老:是否在
ImGui_ImplXXX_NewFrame()之后修改了字体图集?纹理图集过大也可能导致上传失败;并参见 docs/FONTS.md。 - 使用自定义后端:确认字体纹理已上传 GPU,shader 与渲染状态(尤其是纹理绑定)设置正确;对照
backends/现有实现,并使用 RenderDoc 之类的图形调试器检查渲染状态。
移动窗口时元素被裁剪/消失,或画到窗口边界外
多半是渲染函数中裁剪矩形处理错误。每条绘制命令都必须使用 ImDrawCmd->ClipRect 提供的裁剪矩形,且 Dear ImGui 的矩形定义为 (x1=left, y1=top, x2=right, y2=bottom),不是 (x1, y1, width, height)。FAQ 引用了 DirectX11 后端的做法,本仓库 backends/imgui_impl_dx11.cpp 的当前实现与之对应:
// 将 scissor/裁剪矩形投影到帧缓冲空间(相对 DisplayPos 偏移)
ImVec2 clip_off = draw_data->DisplayPos;
ImVec2 clip_min(pcmd->ClipRect.x - clip_off.x, pcmd->ClipRect.y - clip_off.y);
ImVec2 clip_max(pcmd->ClipRect.z - clip_off.x, pcmd->ClipRect.w - clip_off.y);
if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y)
continue; // 裁剪矩形无效,跳过
const D3D11_RECT r = { (LONG)clip_min.x, (LONG)clip_min.y, (LONG)clip_max.x, (LONG)clip_max.y };
device->RSSetScissorRects(1, &r);
核心要点:把 ClipRect 的 (x, y, z, w) 视为 (left, top, right, bottom),减去显示区域偏移后再应用 scissor。
四、ID Stack 系统:最常见的用户错误区
FAQ 用加粗强调了两句话:同一作用域使用相同 label+ID 是最常见的用户错误;空 label 等价于使用与父控件相同的 label。核心规则(TL;DR):
- 控件 label 同时用于计算控件唯一标识符;
- 唯一标识是 label + 父作用域(父窗口、父 Tree Node 等 label)的哈希;
PushID()追加不可见的标识前缀;"##something"把后缀并入 ID 但不显示;"###something"让 ID 计算忽略显示部分。
多个同 label 控件
同一作用域、数量有限时,用 "##suffix":
Button("Play"); // Label = "Play", ID = hash of ("MyWindow", "Play")
Button("Play##foo1"); // Label = "Play", ID = hash of ("MyWindow", "Play##foo1")
Button("Play##foo2"); // Label = "Play", ID = hash of ("MyWindow", "Play##foo2")
循环等更一般的情况,用 PushID()/PopID() 压入前缀:
// 用指针/字符串
for (int i = 0; i < 100; i++)
{
MyObject* obj = Objects[i];
PushID(obj->Name);
Button("Click"); // ID = hash of ("Window", obj->Name, "Click")
PopID();
}
// 用索引
for (int i = 0; i < 100; i++)
{
PushID(i);
Button("Click"); // ID = hash of ("Window", i, "Click")
PopID();
}
空 label
Checkbox("##On", &b); // Label = "", ID = hash of (..., "##On") // 无可见标签,只有一个复选框
动态 label(### 用法)
Dear ImGui 每帧都可以提交不同控件,但为保持控件状态(哪个 Tree Node 展开、哪个按钮被聚焦),内部依赖唯一 ID。想在改变 label 的同时保持 ID 不变,用 ###:
Button("Hello###ID"); // ID = hash of (..., "ID")
Button("World###ID"); // 同一 ID,不同 label
// 窗口标题带动态 FPS,窗口 ID 始终是 "MyGame" 的哈希
char buf[128];
sprintf(buf, "My game (%.1f FPS)###MyGame", io.Framerate);
ImGui::Begin(buf);
// label 在 Enable/Disable 间切换,ID 始终为 ("MyGame", "MyButton")
if (ImGui::Button(enabled ? "Disable###MyButton" : "Enable###MyButton", { -FLT_MIN, 0.0f }))
enabled = !enabled;
ImGui::End();
label 与 ID Stack 的完整机制
从源码结构看,这套机制的实现非常紧凑。不可点击元素(如 Text())不需要 ID;交互控件(如 Button())需要唯一 ID;唯一 ID 用于内部跟踪活动控件、偶尔关联状态,并且隐式地由标识"路径"的多个元素的哈希构成。
imgui.cpp 中 ImGuiWindow::GetID() 的三个重载展示了 ID 的生成方式:以 IDStack.back() 作为种子,对字符串(ImHashStr)、指针(ImHashData)或整数做哈希。而 imgui.cpp 中 PushID() 就是"把当前层种子哈希出的 ID 压入 window->IDStack"。这正对应 FAQ 的解释:栈的每一层保存该层项目的种子,最终 ID 是压入 ID 栈的一切内容的级联哈希。
Begin("MyWindow");
if (TreeNode("MyTreeNode")) // Tree Node 会隐式 PushID
{
Button("OK"); // ID = hash of ("MyWindow", "MyTreeNode", "OK")
TreePop();
}
End();
不同窗口、不同树位置的两个 "OK" 按钮不会冲突;但在同一位置出现两个相同 ID 就是冲突——第二个 "OK" 与第一个撞车,交互任一都会触发第一个;而 Button("") 会与 Begin("MyWindow") 撞车(空 label 等价于父作用域 label)。
Tree 场景中 ID 的选取需要设计考量:跟踪一个会变化的指针时用静态字符串作 ID 可保持展开状态稳定;展示对象列表时用索引或指针作 ID 则有不同行为,按需求选择。
调试利器:Demo > Tools > ID Stack Tool,或直接调用 ImGui::ShowIDStackToolWindow()(见 imgui.h),鼠标悬停控件即可看到生成唯一 ID 的中间值,非常适合理解与排查 ID 问题。
五、显示图片:ImTextureID 与 ImTextureRef 双标识体系
FAQ 对图片问题的回答是本仓库最新、信息量最大的部分之一。
简短结论
- 用
ImGui::Image()、ImGui::ImageButton()或更底层的ImDrawList::AddImage()发出使用自定义纹理的绘制调用; - 纹理标识方式完全由用户/引擎决定,以不透明的
ImTextureID值存储并传递; - 默认
ImTextureID可存 64 位;需要时可在 imconfig.h 中#define ImTextureID MyType替换为自定义类型/结构体; - 从磁盘加载图片文件并变成纹理不属于 Dear ImGui 的职责(这是有意设计)。
1.92 引入的 ImTextureRef
imgui.h 中可以看到基础类型定义,与 FAQ 描述一致:
#ifndef ImTextureID
typedef ImU64 ImTextureID; // Default: store up to 64-bits (any pointer or integer).
#endif
#ifndef ImTextureID_Invalid
#define ImTextureID_Invalid ((ImTextureID)0)
#endif
而 imgui.h 中的 ImTextureRef:
struct ImTextureRef
{
ImTextureRef() { _TexData = NULL; _TexID = ImTextureID_Invalid; }
ImTextureRef(ImTextureID tex_id) { _TexData = NULL; _TexID = tex_id; }
inline ImTextureID GetTexID() const; // == (_TexData ? _TexData->TexID : _TexID)
// Members (either are set, never both!)
ImTextureData* _TexData; // 一般由 ImFontAtlas 持有的纹理,渲染时上传后转换为 ImTextureID
ImTextureID _TexID; // _OR_ 低层后端纹理标识,已由用户创建/上传
};
要点(与 FAQ 逐条对应):
- 1.92 起,所有原来接受
ImTextureID的绘制函数改接受ImTextureRef;ImTextureRef可由ImTextureID平凡地隐式构造,所以你自己加载/创建的纹理,绝大多数情况下只需存ImTextureID,传参时自动变成ImTextureRef; - 只有处理"后端自管纹理"(目前主要是字体图集)时,才需要真正操纵
ImTextureRef;由ImTextureData*创建用ImTextureData::GetTexRef()——官方刻意不提供从ImTextureData*的构造器,因为预期用户很少需要,且会被大量旧代码误用; - 官方刻意不提供
ImTextureRef -> ImTextureID的隐式转换,因为渲染前做这个转换在技术上有损; - 需要绑定当前图集时可用
io.Fonts->TexRef;面向 C 等无构造器语言的绑定生成器,建议提供ImTextureRefFromID(ImTextureID)之类的辅助函数。
长解释:为什么这样设计
Dear ImGui 的职责是生成"网格"——由渲染器无关格式的绘制命令与顶点组成,帧末由你的渲染函数显示。"纹理"这个概念完全绑定在底层引擎/图形 API 上,ImTextureID 只是携带识别信息的类型,默认 8 字节(ImU64),足够放一个指针或整数;Dear ImGui 不解释你存的是什么,只是原样透传给渲染函数。
各示例后端的取法(FAQ 原列表):
- OpenGL:
ImTextureID存GLuint,见 backends/imgui_impl_opengl3.cpp 的ImGui_ImplOpenGL3_RenderDrawData(); - DirectX9:存
LPDIRECT3DTEXTURE9指针,见 backends/imgui_impl_dx9.cpp; - DirectX11:存
ID3D11ShaderResourceView*指针,见 backends/imgui_impl_dx11.cpp; - DirectX12:存
D3D12_GPU_DESCRIPTOR_HANDLE(固定 64 位),见 backends/imgui_impl_dx12.cpp。
自定义引擎可以更进一步:用高层纹理/材质类型做 ImTextureID,只要你的引擎有这类类型就值得用;若刚起步、引擎层较薄,跟随示例后端的默认表示即可。透传模型的典型用法:
// 用户代码:把自定义纹理类型强转为 ImTextureID
MyTexture* texture = g_CoffeeTableTexture;
ImGui::Image((ImTextureID)(intptr_t)texture, ImVec2(texture->Width, texture->Height));
// 渲染函数(ImGui::Render() 之后调用):原样取回
MyTexture* texture = (MyTexture*)(intptr_t)pcmd->GetTexID();
MyEngineBindTexture2D(texture);
C/C++ 小技巧:u64 是 8 字节,可以安全地用 (ImTextureID)(intptr_t)value 存取任意指针或整数并还原。理解了这套设计,就明白"加载 PNG 变成纹理"为什么不在 Dear ImGui 范围内——这让你对自己的数据类型和显示方式拥有完全控制权。调试时可调用 ImGui::ShowMetricsWindow() 观察 ImDrawList 是如何生成的。
六、数学类型与标准 C++ 类型
ImVec2 的数学运算符
默认不在 imgui.h 中导出数学运算符,避免与你的数学类型冲突。需要时,在包含头文件前(或写入 imconfig.h)定义 IMGUI_DEFINE_MATH_OPERATORS 即可获得基础运算符;imgui.h 中的 #ifdef IMGUI_DEFINE_MATH_OPERATORS 即其实现入口。
用自己的数学类型
在 imconfig.h 中配置 IM_VEC2_CLASS_EXTRA / IM_VEC4_CLASS_EXTRA 宏,添加与 ImVec2/ImVec4 的隐式转换,之后就可以在任何接受 ImVec2 的 API 处直接传 glm::vec2 或自定义 MyVector2。
与 std::string / std::vector 的交互
- Dear ImGui 为保持高可移植性(多语言绑定、多框架、老旧平台/编译器)与实时游戏引擎所需的兼容性和性能,不使用任何 std C++ 类型,用裸类型(
char*而非std::string); - 用
std::string等可伸缩字符串接InputText(),见现成封装 misc/cpp/imgui_stdlib.h; - 对
std::vector做下拉框/列表框:优先用BeginCombo()/EndCombo()和ListBoxHeader()/ListBoxFooter()这类"自己迭代提交条目"的 API,而不是老式且别扭的Combo()/ListBox(); - 对大多数高层类型可以直接访问底层数据,自己写一行小封装(提示:可以在自己的文件里往
ImGui::命名空间加函数,但不要修改 imgui 源码文件); - 性能提示:大量字符串的 UI 遍历中,
std::string可能带来不满意的性能。现代std::string的小字符串优化(SSO)不可配置且各实现不一致。若发现 UI 遍历开销大,检查是否产生过多堆分配,优先字面量、固定大小缓冲和自研辅助函数——典型场景是每帧动态构造大量有界长度的字符串; - 想用
std::string_view:FAQ 指出上游存在一个持续维护的features/string_view分支,等待合适时机合入主线。
七、自定义图形:低层 ImDrawList API
FAQ 给出的标准示例(可直接复制):
ImGui::Begin("My shapes");
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 p = ImGui::GetCursorScreenPos(); // 当前 ImGui 光标位置
// 红色实心圆
draw_list->AddCircleFilled(ImVec2(p.x + 50, p.y + 50), 30.0f, IM_COL32(255, 0, 0, 255));
// 3 像素粗的黄色线
draw_list->AddLine(ImVec2(p.x, p.y), ImVec2(p.x + 100.0f, p.y + 100.0f), IM_COL32(255, 255, 0, 255), 3.0f);
// 推进光标以在窗口中占据空间(否则窗口会显得很小)
ImGui::Dummy(ImVec2(200, 200));
ImGui::End();
FAQ 补充的实践要点:
- 演示窗口中
Demo > Examples > Custom Rendering有更多示例,对应源码是 imgui_demo.cpp 中的ShowExampleAppCustomRendering(); - 颜色生成:
IM_COL32(r,g,b,a)在编译期生成;ImGui::GetColorU32(...)则会与当前style.Alpha相乘; - 数学类型:若在 imconfig.h 配置了
IM_VEC2_CLASS_EXTRA,直接用自己的数学类型;ImVec2默认不导出运算符; - 全局绘制层:
ImGui::GetBackgroundDrawList()/ImGui::GetForegroundDrawList()提供位于所有窗口之后/之前的绘制列表(每个视口各一对),适合快速绘制不归属于任何窗口的内容; - 创建"透明画布"窗口:
Begin()时传NoBackground | NoDecoration | NoSavedSettings | NoInputs(其中NoDecoration本身是NoTitleBar | NoResize | NoScrollbar | NoCollapse的快捷方式),再用GetWindowDrawList()任意绘制; - 甚至可以创建自己的
ImDrawList实例(用ImGui::GetDrawListSharedData()初始化),再把自定义的ImDrawList/ImDrawData交给渲染函数。
八、多线程
FAQ 的四条原则:
- 同一个 Dear ImGui 上下文不能被多个线程并行使用;
- 若想在并行任务中偶尔用于调试同一上下文,加锁即可;
- 若要在主/更新线程提交内容、在专用渲染线程渲染,需要暂存(stage)
ImDrawData与纹理请求,参考作者 imgui_club 仓库中的ImDrawDataSnapshot与ImTextureQueue辅助; - 若使用多个上下文且想跨线程使用,需
#define GImGui使其成为 TLS(线程局部存储)变量,见 imgui.cpp 中GImGui定义附近的说明。
九、字体与文本
DPI 处理(1.92 起字体可动态任意尺寸)
缩放字体——从源码看,ImGuiStyle 中有对应的两个字段(imgui.h):
// ImGuiStyle 相关字段
float FontSizeBase; // 应用外部全局缩放因子之前的基础字号
float FontScaleDpi; // 来自视口/显示器内容缩放的额外全局因子
FAQ 的操作方式:
style.FontSizeBase = 20.0f; // 选择默认字号
style.FontScaleDpi = 2.0f; // 缩放所有字体
ImGui::PushFont(nullptr, 42.0f); // 只改字号(会再乘以 style.FontScaleDpi)
ImGui::PushFont(new_font, 42.0f); // 同时改字体和字号
在 docking 分支/多视口下:
io.ConfigDpiScaleFonts = true; // (仅 docking 分支) 显示器 DPI 变化时在 Begin() 自动覆写 style.FontScaleDpi(只缩放字体,暂不缩放尺寸/内边距)
io.ConfigDpiScaleViewports = true; // (仅 docking 分支) 显示器 DPI 变化时缩放 Dear ImGui 与平台窗口
缩放样式(内边距、间距、粗细):FAQ 明确标注这仍是大规模进行中的工作。样式值目前不能方便地动态缩放,单视口应用可一次性调用 style.ScaleAllSizes(factor);(该 API 在 imgui.h 中有定义,注释强调不要缩放字体、初始缩放因子小于 1 时慎用)。需要改缩放因子时,目前最实际的做法是重置 style 后重新调用。
FAQ 给出的代码风格建议:UI 代码应避免硬编码尺寸/定位常量,优先用参考值的倍数表达,例如用 30 * ImGui::GetFontSize() 代替硬编码高度 500。examples/ 中的应用部分 DPI 感知但无法从文件系统加载自定义字体,所以观感欠佳;DPI 没有"自动魔法"的根本原因是"多 DPI"问题(docking 分支下多个视口横跨不同 DPI 的显示器)对 ImGuiStyle 结构尚无满意方案——但字体如今是完全可缩放的。
Windows 平台必须声明 DPI 感知,否则 Windows 会缩放窗口、文字发虚。可选方案:
- SDL2:
SDL_CreateWindow()传SDL_WINDOW_ALLOW_HIGHDPI+ 调用::SetProcessDPIAware(); - SDL3:
SDL_CreateWindow()传SDL_WINDOW_HIGH_PIXEL_DENSITY; - GLFW:自动完成;
- 其他后端/封装项目:Win32 后端提供
ImGui_ImplWin32_EnableDpiAwareness()辅助方法;或使用应用清单文件设置<dpiAware>属性。
加载非默认字体
用字体图集加载 TTF/OTF(AddFontFromFileTTF 声明见 imgui.h):
ImGuiIO& io = ImGui::GetIO();
io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels);
// 之后把纹理数据交给后端上传:
io.Fonts->GetTexDataAsRGBA32(); // 或 GetTexDataAsAlpha8()
默认字体是等宽的 ProggyClean.ttf,以 13 像素嵌入源码。等宽字体便于在字符串层面做水平对齐。更多细节见 docs/FONTS.md。FAQ 还特别提醒新手:C/C++ 字符串字面量中反斜杠要写双份:
io.Fonts->AddFontFromFileTTF("MyFolder\MyFont.ttf", size); // 错误(转义了 M)
io.Fonts->AddFontFromFileTTF("MyFolder\\MyFont.ttf", size); // 正确(Windows)
io.Fonts->AddFontFromFileTTF("MyFolder/MyFont.ttf", size); // 同样正确
图标、多字体、非拉丁字符
- 图标:最实用方式是把 FontAwesome 等图标字体合并进主字体,之后在字符串中直接引用图标码点;细节见 docs/FONTS.md。
- 多字体:用字体图集把它们打包进一张纹理;仓库自带的字体文件可在 misc/fonts/ 查看(Cousine、DroidSans、Karla、ProggyClean、Roboto 等)。
- 中日韩/西里尔等非拉丁字符:1.92(2025 年 6 月)起配合更新后的后端,不再需要指定字形范围。1.92 之前需手动传 Unicode 范围,如:
// [1.92 之前] 加载日文字形范围
io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels, nullptr, io.Fonts->GetGlyphRangesJapanese());
// 或自定义范围(游戏里可以喂入全部剧本文本,只构建用到的字符)
ImVector<ImWchar> ranges;
ImFontGlyphRangesBuilder builder;
builder.AddText("Hello world"); // 添加字符串
builder.AddChar(0x7262); // 添加单个字符
builder.AddRanges(io.Fonts->GetGlyphRangesJapanese()); // 添加默认范围之一
builder.BuildRanges(&ranges);
io.Fonts->AddFontFromFileTTF("myfontfile.ttf", 16.0f, nullptr, ranges.Data);
- 所有字符串必须使用 UTF-8 编码;需告知编译器使用 UTF-8,或在 C++11 用
u8"hello"语法;用本地代码页(日文 CP-923、西里尔 CP-1251 等)写源码字面量不行。详见 docs/FONTS.md 的 "About UTF-8 Encoding" 一节。 - 文本输入:由你的应用调用
io.AddInputCharacter()传入正确码点,examples/中的应用都做了这件事。Windows 上可用WM_CHAR/WM_UNICHAR/WM_IME_CHAR消息(取决于 Unicode/MultiByte 构建模式),或用MultiByteToWideChar()/ToUnicode()取码点。依赖 IME 的语言可把 HWND 写入ImGui::GetMainViewport()->PlatformHandleRaw,让默认Platform_SetImeDataFn()正确放置微软 IME。
十、常见疑虑
能否用它做严肃的工具
FAQ 的回答是肯定的:已有游戏编辑器、数据浏览器、调试器、性能分析器等非平凡工具。作者的体会是 API 的简单性非常有赋能感——你的 UI 贴近实时数据运行,工具"始终在线"会让团队每个人都愿意造新工具;该库面向"全天候运行的 AAA 级应用"做了效率与可扩展性设计,IMGUI 范式提供的优化机会与传统 RMGUI 范式不同。
能换肤吗
有限度。可以改颜色、尺寸、内边距、圆角、字体,但 Dear ImGui 设计目标是调试工具,换肤空间有限,官方明确它不是为做游戏界面而设计的(当然,巧妙使用低层 API 可以做到)。
为什么用 C++ 而不是 C
Dear ImGui 只用到一个很小的 C++11 特性子集:不依赖任何 C++ 头文件,主要利用函数重载与默认参数让 API 更简洁,此外用到命名空间、构造器以及模板(ImVector<>)。放弃这些特性会让 API 更啰嗦。面向 C 的自动生成的 C 接口 cimgui(第三方项目)可用于构建其他语言绑定;FAQ 建议尽量在目标语言里复刻重载与默认参数,否则 API 会更难用。
如何参与/支持
FAQ 列出的途径:企业可通过商业支持/赞助资助开发;个人可通过捐赠支持维护;熟悉 Dear ImGui 和 C++ 者可关注 Issues、Discussions、Wiki 与 docs/TODO.txt 寻找切入点;公开分享使用案例(博客、截图)能帮库积累可信度;即使不求支持,分享遇到的问题或不完整的 PR 也有价值。
十一、参考文件索引
| 主题 | 仓库内参考 |
|---|---|
| FAQ 原文 | docs/FAQ.md |
| 输入捕获标志定义 | imgui.h |
| ImTextureID / ImTextureRef | imgui.h |
| ID 哈希与 PushID 实现 | imgui.cpp |
| 裁剪矩形应用(DX11 参考实现) | backends/imgui_impl_dx11.cpp |
| 样式缩放因子 | imgui.h |
| std::string 适配 | misc/cpp/imgui_stdlib.h |
| 字体加载细节 | docs/FONTS.md |
| 后端实现参考 | docs/BACKENDS.md、backends/ |
| 示例工程 | examples/README.txt |
| 配置宏入口 | imconfig.h |
适用前提说明:本文以当前仓库(IMGUI_VERSION "1.93.0 WIP",imgui.h)为准;ImTextureRef、动态字号、免字形范围等非拉丁加载等行为均自 1.92 起生效,升级到 1.92 之前版本时需按 FAQ 中标注的"1.92 之前"分支处理;ConfigDpiScaleFonts/ConfigDpiScaleViewports 等自动 DPI 行为仅在 docking 分支可用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00