首页
/ Dear ImGui 官方 FAQ 深度解读:输入分发、ID Stack、纹理标识与字体 DPI 的权威实践

Dear ImGui 官方 FAQ 深度解读:输入分发、ID Stack、纹理标识与字体 DPI 的权威实践

2026-09-04 20:52:46作者:温玫谨Lighthearted

本文以 Dear ImGui 仓库中的官方文档 docs/FAQ.md 为骨架,系统梳理该库从集成、输入分发、ID 唯一性、纹理显示到字体 DPI 处理的核心问题,并结合 imgui.himgui.cppbackends/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.mddocs/EXAMPLES.mddocs/FONTS.md 三份随仓库发布的文档;imgui.cpp 顶部的文档注释与 imgui.h 的 API 注释;以及 ImGui::ShowMetricsWindow(),它虽然定位是调试工具,但暴露大量内部信息,对理解概念非常有帮助。

二、集成要点:输入分发是第一道坎

如何判断输入该给 ImGui 还是给应用

ImGuiIO 结构中的 io.WantCaptureMouseio.WantCaptureKeyboardio.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.hIsWindowHovered() 的注释也明确写道:如果你是想决定把鼠标分发给谁,不要用这个函数,请用 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 给出的方案梯队:

  1. 共享主机鼠标:使用 Synergy 类方案,把 PC 鼠标无缝共享给主机/平板/手机;其中 micro-synergy-client 项目提供了简洁可移植的 uSynergy.c/.h 可嵌入客户端,基于 Synergy 1.x 协议——本仓库也附带了同款文件 examples/libs/usynergy/uSynergy.cexamples/libs/usynergy/uSynergy.h,可直接作为嵌入参考。
  2. 主机用户:考虑用 DualShock4 触摸板或闲置摇杆模拟鼠标光标作为兜底。
  3. 第三方远程渲染方案:把渲染顶点通过局域网发送出去(netImgui、Remote ImGui、imgui-ws 等思路),让无屏机器也能使用 Dear ImGui。
  4. 触摸输入:FAQ 建议增大控件命中区域来适应触摸精度不足,但推荐使用鼠标或手柄以便优化屏幕空间利用率。

如何自己写一个后端

FAQ 指向两处权威资料:docs/BACKENDS.mdimgui.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.cppImGuiWindow::GetID() 的三个重载展示了 ID 的生成方式:以 IDStack.back() 作为种子,对字符串(ImHashStr)、指针(ImHashData)或整数做哈希。而 imgui.cppPushID() 就是"把当前层种子哈希出的 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 的绘制函数改接受 ImTextureRefImTextureRef 可由 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 原列表):

自定义引擎可以更进一步:用高层纹理/材质类型做 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 的四条原则:

  1. 同一个 Dear ImGui 上下文不能被多个线程并行使用
  2. 若想在并行任务中偶尔用于调试同一上下文,加锁即可;
  3. 若要在主/更新线程提交内容、在专用渲染线程渲染,需要暂存(stage)ImDrawData 与纹理请求,参考作者 imgui_club 仓库中的 ImDrawDataSnapshotImTextureQueue 辅助;
  4. 若使用多个上下文且想跨线程使用,需 #define GImGui 使其成为 TLS(线程局部存储)变量,见 imgui.cppGImGui 定义附近的说明。

九、字体与文本

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.mdbackends/
示例工程 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 分支可用。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388