Dear-ImGUI:不太一样的GUI

之前介绍过Qt,作为c++界面(事实上不只是界面,Qt本身已经成了写C++的一种工具了)工具,采用的是创建控件然后绑定事件处理的逻辑,与winform,WPF等都是类似的,但是这里介绍一种不太一样的GUI库,其使用immediate mode,也就是在每一帧进行处理,没有保留状态维护. 在游戏界面开发中受到热捧.

这种立即模式可能受到一些游戏开发的喜爱,ImGuiocornut/imgui: Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies (github.com)直到最近还不停有commit,Unity也采用了类似模式构建控件IMGUI 基础知识 - Unity 手册. 在Rust也有emilk/egui: egui: an easy-to-use immediate mode GUI in Rust that runs on both web and native (github.com),Go也有Gio UI.

下面写个简单的ImGUI程序学习学习.

开始

我们把GUI分成了retained mode和immediate mode

a diagram that shows retained-mode graphics.

保留模式(retained mode) API 是声明式的。应用程序通过图形基元(如形状和线条)构建场景。图形库将场景模型存储在内存中。要绘制帧,图形库会将场景转换为一组绘图命令。在帧与帧之间,图形库会将场景保存在内存中。要更改渲染的内容,应用程序会发出更新场景的命令,例如添加或删除一个形状。然后图形库负责重新绘制场景。

a diagram that shows immediate-mode graphics.

即时模式 API (immediate mode)是程序性的。每次绘制新帧时,应用程序都会直接发出绘制命令。图形库不会在帧与帧之间存储场景模型。相反,应用程序会跟踪场景

保留模式 API 使用起来可能更简单,因为 API 会为您完成更多工作,如初始化、状态维护和清理。另一方面,它们通常灵活性较差,因为 API 强加了自己的场景模型。此外,保留模式 API 对内存的要求可能更高,因为它需要提供通用场景模型。使用即时模式 API,您可以实现有针对性的优化。

此外注意到还有DirectUI的说法,区分win32窗体 DirectUI与GUI框架有什么区别,如MFC,QT,wxWidgets的区别是什么? - 知乎 (zhihu.com)

系统窗口的消息路由是操作系统负责的,比如按钮上的消息就会被自动分发给按钮的窗口过程。而DirectUI这类框架创建的窗口的消息是由应用程序负责的路由的,无论你点击了窗口中哪个按钮,消息统统分发给单一的窗口过程,它再根据鼠标的坐标判决应该由哪个对象进一步处理消息

搭建环境

imgui背后需要调用图形backend和窗体调用. 它提供了几种不同的头文件,包括win32(窗体)+direcx11/12(图形api),glfw+opengl,sdl+opengl,glfw+vulkan等等.

这里我使用glfw+opengl.

你可以在在对应的官方仓库下载源码或者预编译包链接使用,但是为了更方便地使用,可以直接使用包管理器vcpkg等下载然后再cmake中加上两句即可. vcpkg具体使用可以参考xmake:另一个C++现代构建系统 | Sekyoro的博客小屋.

简单来说就是

1
2
3
4
5
mkdir project && cd project
vcpkg new --application
vcpkg add port opengl
cmake install # 如果在cmakepresets中设置好了直接cmake --preset=<preset_name>即可
cmake --build build

然后在main.cpp添加相应代码,执行程序

image-20240914172255349

很多时候一些c++程序员喜欢把源码下载到本地然后进行编译,得到的库统一放在vendor/3rd_party目录,但是利用现代工具(vcpkg,conan等)可以方便地进行交叉编译(在宿主机上通过不同工具链得到其他架构的库/可执行程序),何乐而不为呢?

其与Qt等高等框架相比,直接使用图形api和系统或在系统之上封装的窗口管理生成窗口、进行绘图.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
At initialization:
call ImGui::CreateContext()
call ImGui_ImplXXXX_Init() for each backend.

At the beginning of your frame:
call ImGui_ImplXXXX_NewFrame() for each backend.
call ImGui::NewFrame()

At the end of your frame:
call ImGui::Render()
call ImGui_ImplXXXX_RenderDrawData() for your Renderer backend.

At shutdown:
call ImGui_ImplXXXX_Shutdown() for each backend.
call ImGui::DestroyContext()

基本逻辑如上,我们需要在while循环中针对每帧做动作,比如下面代码表示如果窗口最小化等待10ms再进行,

1
2
3
4
5
6
7
8
while (!glfwWindowShouldClose(window)) {
// Poll and handle events (inputs, window resize, etc.)
glfwPollEvents();
if (glfwGetWindowAttrib(window, GLFW_ICONIFIED) != 0) {
ImGui_ImplGlfw_Sleep(10);
continue;
}
}

一些简单代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static int clicked = 0;
// 按钮
if (ImGui::Button("Button"))
clicked++;
if (clicked & 1)
{
ImGui::SameLine();
ImGui::Text("Thanks for clicking me!");
}
// 选择框
static bool check = true;
ImGui::Checkbox("checkbox", &check);
// 选择按钮
static int e = 0;
ImGui::RadioButton("radio a", &e, 0); ImGui::SameLine();
ImGui::RadioButton("radio b", &e, 1); ImGui::SameLine();
ImGui::RadioButton("radio c", &e, 2);

// Color buttons, demonstrate using PushID() to add unique identifier in the ID stack, and changing style.
for (int i = 0; i < 7; i++)
{
if (i > 0)
ImGui::SameLine();
ImGui::PushID(i);
ImGui::PushStyleColor(ImGuiCol_Button, (ImVec4)ImColor::HSV(i / 7.0f, 0.6f, 0.6f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, (ImVec4)ImColor::HSV(i / 7.0f, 0.7f, 0.7f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, (ImVec4)ImColor::HSV(i / 7.0f, 0.8f, 0.8f));
ImGui::Button("Click");
ImGui::PopStyleColor(3);
ImGui::PopID();
}

// Use AlignTextToFramePadding() to align text baseline to the baseline of framed widgets elements
// (otherwise a Text+SameLine+Button sequence will have the text a little too high by default!)
// See 'Demo->Layout->Text Baseline Alignment' for details.
ImGui::AlignTextToFramePadding();
ImGui::Text("Hold to repeat:");
ImGui::SameLine();

参考资料

  1. ocornut/imgui: Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies (github.com)

  2. 云风的 BLOG: 游戏 UI 模块的选择 (codingnow.com)

    如果你不想使用Qt庞然大物,也不想使用提供内置功能太少的界面库,可以试试SFMLSFML (sfml-dev.org).

  3. Writing GUI apps for Windows is painful - Samuel Tulach一篇介绍windows上桌面程序开发框架选择的文章,提到了winui3,qt以及使用其他技术栈链接.dll库,比如wpf等.事实上我已经多次谈论界面开发/桌面开发/移动端开发,注意一个事实,一些库不只是用于界面绘制的,除去界面绘制,一些基本工具库的提供往往也非常有必要.所以在windows下使用微软带来的生态是很好的(wpf,maui,winui3等),在移动端下使用compose也很舒服.还有Flutter也吃Dart的生态. 此外也有很多使用web的技术,比如electron,tauri,pywebview (flowrl.com)

    很多时候没有必要考虑跨平台的需求(那就做web).

    (delphi和lazarus有谁用过?)用 Lazarus 做 GUI 程序合适吗? - 知乎 (zhihu.com)

  4. ImGui Manual (pthom.github.io)第三方文档

  5. 其他教程使用C++界面框架ImGUI开发一个简单程序 - 二次元攻城狮 - 博客园 (cnblogs.com)

-------------本文结束感谢您的阅读-------------
感谢阅读.

欢迎关注我的其它发布渠道