GUI Development with C++, SDL2, and Dear ImGui

The many ways of GUI
Looking at all those tools, libraries, and many ways of developing GUIs in C++, it can become easily overwhelming. This at least happened to me; every time I set out to "finally solve how I will create GUIs in C++" I got blasted by all the possibilities. From very simplistic to huge monster like frameworks with own language extensions. In the end I never just created.
Over time though, I became more and more familiar with Dear ImGui, an immediate mode "GUI for C++ with minimal dependencies", creating tools for game development. Never taking it serious for "end-user" applications, I've got the final push to try it from a video from Yan Chernikov. Trying ImGui, in combination with SDL2 I just started learning, I was pleasantly surprised about the outcome.
Here it goes, a basic setup I started using for end-user applications. Of course, as many frameworks there are opinions, and this one is only mine.
Basic setup
For the setup of the application I use my C++ starter template I showed in the last post, Basic C++ setup with dependency management in CMake. The template for this can be found on GitHub: github.com/MartinHelmut/cpp-base-template or as a step-by-step explanation in the blog post.
The first thing I did was to rename src/some_library
to
src/core
. This then contains the logging and debugging
helper, also this is where I will create the Window
and
Application
classes.
SDL2 and Dear ImGui
SDL2 will be added as dependency through
FetchContent_Declare
, same as the other dependencies in
vendor/CMakeLists.txt
.
# vendor/CMakeLists.txt
FetchContent_Declare(
SDL2
GIT_REPOSITORY "https://github.com/libsdl-org/SDL.git"
GIT_TAG release-2.0.22
)
add_subdirectory(sdl)
# Other dependencies ...
The new file vendor/sdl/CMakeLists.txt
will serve to
show a message on fetching and make SDL2 available.
# vendor/sdl/CMakeLists.txt
message(STATUS "Fetching SDL ...")
FetchContent_MakeAvailable(SDL2)
Adding Dear ImGui as dependency works the same, by adding the
content fetching in vendor/CMakeLists.txt
. Instead of
using a regular release I will use the "docking" branch,
enabling a flexible way of moving and docking UI widgets.
# vendor/CMakeLists.txt
FetchContent_Declare(
imgui
GIT_REPOSITORY "https://github.com/ocornut/imgui.git"
# docking-latest
GIT_TAG cb8ead1f7198924f97e52973d115e1d4eaeda2f3
)
add_subdirectory(imgui)
# Other dependencies ...
As ImGui does not support CMake, there needs to be a bit more done
inside the new file vendor/imgui/CMakeLists.txt
. The
whole file will set up ImGui as a library by adding all necessary
source files, defining the including directory and linking
SDL2 to it.
# vendor/imgui/CMakeLists.txt
message(STATUS "Fetching imgui ...")
set(CMAKE_CXX_STANDARD 20)
FetchContent_GetProperties(imgui)
if (NOT imgui_POPULATED)
FetchContent_Populate(imgui)
endif ()
add_library(imgui
${imgui_SOURCE_DIR}/imgui.cpp
${imgui_SOURCE_DIR}/imgui.h
${imgui_SOURCE_DIR}/imconfig.h
${imgui_SOURCE_DIR}/imgui_demo.cpp
${imgui_SOURCE_DIR}/imgui_draw.cpp
${imgui_SOURCE_DIR}/imgui_internal.h
${imgui_SOURCE_DIR}/imgui_tables.cpp
${imgui_SOURCE_DIR}/imgui_widgets.cpp
${imgui_SOURCE_DIR}/imstb_rectpack.h
${imgui_SOURCE_DIR}/imstb_textedit.h
${imgui_SOURCE_DIR}/imstb_truetype.h
${imgui_SOURCE_DIR}/backends/imgui_impl_sdl.h
${imgui_SOURCE_DIR}/backends/imgui_impl_sdl.cpp
${imgui_SOURCE_DIR}/backends/imgui_impl_sdlrenderer.h
${imgui_SOURCE_DIR}/backends/imgui_impl_sdlrenderer.cpp)
target_include_directories(imgui PUBLIC ${imgui_SOURCE_DIR})
target_link_libraries(imgui PUBLIC SDL2::SDL2)
FetchContent_MakeAvailable(imgui)
Now, ImGui and SDL2 need to be added to the Core library.
# src/core/CMakeLists.txt
set(NAME "Core")
include(${PROJECT_SOURCE_DIR}/cmake/StaticAnalyzers.cmake)
add_library(${NAME} STATIC
Core/Log.cpp Core/Log.hpp Core/Debug/Instrumentor.hpp
Core/Application.cpp Core/Application.hpp
Core/Window.cpp Core/Window.hpp)
target_include_directories(${NAME}
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_features(${NAME} PRIVATE cxx_std_20)
target_link_libraries(${NAME}
PRIVATE project_warnings
# Added SDL2::SDL2 and imgui here.
PUBLIC fmt spdlog SDL2::SDL2 imgui)
Basic application setup
The basic application will run the main loop and handle properly stopping the application on close. There will also be an exit status in case of an issues.
// src/core/Core/Application.hpp
#pragma once
#include <SDL.h>
namespace App {
class Application {
public:
Application();
~Application();
int run();
void stop();
private:
int m_exit_status{0};
bool m_running{true};
};
} // namespace App
The basic implementation of Application.hpp
looks like
the following.
// src/core/Core/Application.cpp
#include "Application.hpp"
#include "Core/Log.hpp"
namespace App {
Application::Application() {
unsigned int init_flags{
SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER
};
if (init_flags) != 0) {
APP_ERROR("Error: %s\n", SDL_GetError());
m_exit_status = 1;
}
}
Application::~Application() {
SDL_Quit();
}
int App::Application::run() {
if (m_exit_status == 1) {
return m_exit_status;
}
m_running = true;
while (m_running) {
// ...
}
return m_exit_status;
}
void App::Application::stop() {
m_running = false;
}
} // namespace App
Step-by-step, the constructor will initialise SDL2 and catch possible issues on creation. The destructor will only quit SDL2 for now.
Application::Application() {
unsigned int init_flags{
SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER
};
if (SDL_Init(init_flags) != 0) {
APP_ERROR("Error: %s\n", SDL_GetError());
m_exit_status = 1;
}
}
Application::~Application() {
SDL_Quit();
}
Inside run
will be the main loop and some error
handling.
int App::Application::run() {
if (m_exit_status == 1) {
return m_exit_status;
}
m_running = true;
while (m_running) {
// ...
}
return m_exit_status;
}
Last but not least, calling stop
will end the main
loop.
void App::Application::stop() {
m_running = false;
}
To actual run the application it needs to be added to the projects
Main.cpp
file.
// src/app/App/Main.cpp
#include "Core/Application.hpp"
int main() {
App::Application app{};
return app.run();
}
Returning the exit status from app.run()
will serve as
exit code of the application. Last step to make this work is to add
the newly created files to CMake.
# src/core/CMakeLists.txt
set(NAME "Core")
include(${PROJECT_SOURCE_DIR}/cmake/StaticAnalyzers.cmake)
add_library(${NAME} STATIC
Core/Log.cpp Core/Log.hpp Core/Debug/Instrumentor.hpp
# Application files here:
Core/Application.cpp Core/Application.hpp)
target_include_directories(${NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_features(${NAME} PRIVATE cxx_std_20)
target_link_libraries(${NAME}
PRIVATE project_warnings
PUBLIC fmt spdlog SDL2::SDL2 imgui)
Creating the window
Next is setting up the window for rendering. The
Settings
struct will define the base size of the window
of 1280
times 720
, I decided to pick.
// src/core/Core/Window.hpp
#pragma once
#include <string>
#include "SDL.h"
namespace App {
class Window {
public:
struct Settings {
std::string title;
const int width{1280};
const int height{720};
};
explicit Window(const Settings& settings);
~Window();
[[nodiscard]] SDL_Window* get_native_window() const;
[[nodiscard]] SDL_Renderer* get_native_renderer() const;
private:
SDL_Window* m_window{nullptr};
SDL_Renderer* m_renderer{nullptr};
};
} // namespace App
Here the full implementation of Window
.
// src/core/Core/Window.cpp
#include "Window.hpp"
#include "Core/Log.hpp"
namespace App {
Window::Window(const Settings& settings) {
auto window_flags{
static_cast<SDL_WindowFlags>(
SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI
)
};
constexpr int window_center_flag{SDL_WINDOWPOS_CENTERED};
m_window = SDL_CreateWindow(
settings.title.c_str(),
window_center_flag,
window_center_flag,
settings.width,
settings.height,
window_flags
);
auto renderer_flags{
static_cast<SDL_RendererFlags>(
SDL_RENDERER_PRESENTVSYNC | SDL_RENDERER_ACCELERATED
)
};
m_renderer = SDL_CreateRenderer(
m_window, -1, renderer_flags
);
if (m_renderer == nullptr) {
APP_ERROR("Error creating SDL_Renderer!");
return;
}
}
Window::~Window() {
SDL_DestroyRenderer(m_renderer);
SDL_DestroyWindow(m_window);
}
SDL_Window* Window::get_native_window() const {
return m_window;
}
SDL_Renderer* Window::get_native_renderer() const {
return m_renderer;
}
} // namespace App
One at a time, the biggest part is the constructor.
Window::Window(const Settings& settings) {
auto window_flags{
static_cast<SDL_WindowFlags>(
SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI
)
};
constexpr int window_center_flag{SDL_WINDOWPOS_CENTERED};
m_window = SDL_CreateWindow(
settings.title.c_str(),
window_center_flag,
window_center_flag,
settings.width,
settings.height,
window_flags
);
// ...
}
First step is to actually create a window — it should be resizable and work on high DPI displays, like Retina displays. Besides that, the window will be created centered on the screen with the provided settings for size and window title.
Besides the window, a renderer is needed to actually draw something into the window.
Window::Window(const Settings& settings) {
// ...
auto renderer_flags{
static_cast<SDL_RendererFlags>(
SDL_RENDERER_PRESENTVSYNC | SDL_RENDERER_ACCELERATED
)
};
m_renderer = SDL_CreateRenderer(
m_window, -1, renderer_flags
);
if (m_renderer == nullptr) {
APP_ERROR("Error creating SDL_Renderer!");
return;
}
}
The renderer will use VSync and hardware acceleration. Both, renderer and window, will get cleaned up in the destructor.
Window::~Window() {
SDL_DestroyRenderer(m_renderer);
SDL_DestroyWindow(m_window);
}
For the setup of ImGui, Window
will also expose the
native window and renderer.
SDL_Window* Window::get_native_window() const {
return m_window;
}
SDL_Renderer* Window::get_native_renderer() const {
return m_renderer;
}
Not forgetting to add those new files to CMake again.
# src/core/CMakeLists.txt
set(NAME "Core")
include(${PROJECT_SOURCE_DIR}/cmake/StaticAnalyzers.cmake)
add_library(${NAME} STATIC
Core/Log.cpp Core/Log.hpp Core/Debug/Instrumentor.hpp
Core/Application.cpp Core/Application.hpp
# New Window files
Core/Window.cpp Core/Window.hpp)
target_include_directories(${NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_features(${NAME} PRIVATE cxx_std_20)
target_link_libraries(${NAME}
PRIVATE project_warnings
PUBLIC fmt spdlog SDL2::SDL2 imgui)
Now, the Application
will create and manage the window
holding a reference through a unique_ptr
.
// src/core/Core/Application.hpp
// ...
private:
int m_exit_status{0};
bool m_running{true};
// Hold reference to window.
std::unique_ptr<Window> m_window{nullptr};
// ...
The window will be created inside the
Application
constructor.
// src/core/Core/Application.cpp
Application::Application() {
unsigned int init_flags{
SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER
};
if (SDL_Init(init_flags) != 0) {
APP_ERROR("Error: %s\n", SDL_GetError());
m_exit_status = 1;
}
// Create new window with the title "Application".
m_window = std::make_unique<Window>(
Window::Settings{"Application"}
);
}
The renderer set up inside the main loops run
method.
// src/core/Core/Application.cpp
int App::Application::run() {
// ...
m_running = true;
while (m_running) {
SDL_SetRenderDrawColor(
m_window->get_native_renderer(),
// Gray clear color (rgba)
100, 100, 100, 255
);
SDL_RenderClear(m_window->get_native_renderer());
SDL_RenderPresent(m_window->get_native_renderer());
}
// ...
}
Showing something on the screen
To actually get something on screen, ImGui and the SDL2 ImGui backend and renderer need to be set up. The SDL2 renderer will, depending on the operating system, pick a different render backend — DirectX 10, 11 or 12 for Windows, Metal for Mac, and so on. You can see for yourself inside the SDL_render.c file if you want.
The imports are ImGui, the SDL2 ImGui implementation, and the ImGui SDL2 renderer.
// src/core/Core/Application.cpp
#include <backends/imgui_impl_sdl.h>
#include <backends/imgui_impl_sdlrenderer.h>
#include <imgui.h>
// ...
Now, the run
method needs to create the ImGui context,
initialise the SDL2 renderer, poll events, create a new
frame and actually render something. But, everything step-by-step.
First thing is to create a new ImGui context and set the flags
NavEnableKeyboard
and ViewportsEnable
for
enabling keyboard navigation and multiple viewports.
// src/core/Core/Application.cpp
int App::Application::run() {
if (m_exit_status == 1) {
return m_exit_status;
}
// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io{ImGui::GetIO()};
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
m_running = true;
while (m_running) {
SDL_SetRenderDrawColor(
m_window->get_native_renderer(),
100, 100, 100, 255
);
SDL_RenderClear(m_window->get_native_renderer());
SDL_RenderPresent(m_window->get_native_renderer());
}
return m_exit_status;
}
Next comes the SDL2 renderer.
// src/core/Core/Application.cpp
int App::Application::run() {
// ...
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
// Setup Platform/Renderer backends
ImGui_ImplSDL2_InitForSDLRenderer(
m_window->get_native_window(),
m_window->get_native_renderer()
);
ImGui_ImplSDLRenderer_Init(
m_window->get_native_renderer()
);
m_running = true;
while (m_running) {
SDL_SetRenderDrawColor(
m_window->get_native_renderer(),
100, 100, 100, 255
);
SDL_RenderClear(m_window->get_native_renderer());
SDL_RenderPresent(m_window->get_native_renderer());
}
return m_exit_status;
}
Inside the main loop will be the event polling — the
SDL_QUIT
event will stop the main loop by calling
stop()
from Application
.
// src/core/Core/Application.cpp
int App::Application::run() {
// ...
m_running = true;
while (m_running) {
// Poll SDL events
SDL_Event event{};
while (SDL_PollEvent(&event) == 1) {
ImGui_ImplSDL2_ProcessEvent(&event);
// Listen for the quit event to stop the application
if (event.type == SDL_QUIT) {
stop();
}
}
SDL_SetRenderDrawColor(
m_window->get_native_renderer(),
100, 100, 100, 255
);
SDL_RenderClear(m_window->get_native_renderer());
SDL_RenderPresent(m_window->get_native_renderer());
}
return m_exit_status;
}
Finally, creating a new frame and calling the ImGui render method.
// src/core/Core/Application.cpp
int App::Application::run() {
// ...
m_running = true;
while (m_running) {
// ...
// Start the Dear ImGui frame
ImGui_ImplSDLRenderer_NewFrame();
ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame();
// Rendering
ImGui::Render();
SDL_SetRenderDrawColor(
m_window->get_native_renderer(),
100, 100, 100, 255
);
SDL_RenderClear(m_window->get_native_renderer());
// Render data through the SDL renderer
ImGui_ImplSDLRenderer_RenderDrawData(
ImGui::GetDrawData()
);
SDL_RenderPresent(
m_window->get_native_renderer()
);
}
return m_exit_status;
}
The whole run
method now looks like this:
// src/core/Core/Application.cpp
int App::Application::run() {
if (m_exit_status == 1) {
return m_exit_status;
}
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io{ImGui::GetIO()};
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
ImGui_ImplSDL2_InitForSDLRenderer(
m_window->get_native_window(),
m_window->get_native_renderer()
);
ImGui_ImplSDLRenderer_Init(
m_window->get_native_renderer()
);
m_running = true;
while (m_running) {
SDL_Event event{};
while (SDL_PollEvent(&event) == 1) {
ImGui_ImplSDL2_ProcessEvent(&event);
if (event.type == SDL_QUIT) {
stop();
}
}
ImGui_ImplSDLRenderer_NewFrame();
ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame();
ImGui::Render();
SDL_SetRenderDrawColor(
m_window->get_native_renderer(),
100, 100, 100, 255
);
SDL_RenderClear(m_window->get_native_renderer());
ImGui_ImplSDLRenderer_RenderDrawData(
ImGui::GetDrawData()
);
SDL_RenderPresent(
m_window->get_native_renderer()
);
}
return m_exit_status;
}
One last important thing, adding some cleanup functions inside the
Application
destructor.
// src/core/Core/Application.cpp
Application::~Application() {
ImGui_ImplSDLRenderer_Shutdown();
ImGui_ImplSDL2_Shutdown();
ImGui::DestroyContext();
SDL_Quit();
}
Using
Litr, I can build and run the application with
litr build,start
. Without Litr, the same can be
achieved by running CMake and the generated executable.
$ cmake -DCMAKE_BUILD_TYPE=Debug -B build/debug
$ cmake --build build/debug
$ cd build/debug/src/app && ./App
$
is used to show a command will be entered.
This is how it looks like so far.

Rendering a widget
Let's get something rendering inside that window. For this I add a
private variable to Application
holding the information
if the new widget should be visible.
// src/core/Core/Application.hpp
// ...
private:
int m_exit_status{0};
bool m_running{true};
std::unique_ptr<Window> m_window{nullptr};
// Show "some panel", let's default to true.
bool m_show_some_panel{true};
After creating the ImGui frame, before the ImGui render, the first widget "some panel" will be defined.
// src/core/Core/Application.cpp
// ...
while (m_running) {
// ...
ImGui_ImplSDLRenderer_NewFrame();
ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame();
// Render "some panel".
if (m_show_some_panel) {
ImGui::Begin("Some panel", &m_show_some_panel);
ImGui::Text("Hello World");
ImGui::End();
}
ImGui::Render();
// ...
}
// ...
There it is, in all it's tiny glory — looking too small and not reacting to any click of the mouse. The reason for this is that the SDL2 renderer-scale is not yet adjusted to my Retina display on Mac. Let's fix it next.

Handling high DPI displays
For high DPI displays, like Apples Retina display, I need to change
the renderer scaling. For this I created a new public method
get_scale
in Window
.
// src/core/Core/Window.hpp
class Window {
// ...
public:
[[nodiscard]] float get_scale() const;
// ...
To implement get_scale
, it is necessary to divide the
render output size by the window size. The method will return one
value, in that case x
computed from width, where height
is only needed for the computation.
// src/core/Core/Window.cpp
float Window::get_scale() const {
int window_width{0};
int window_height{0};
SDL_GetWindowSize(
m_window,
&window_width, &window_height
);
int render_output_width{0};
int render_output_height{0};
SDL_GetRendererOutputSize(
m_renderer,
&render_output_width, &render_output_height
);
const auto scale_x{
static_cast<float>(render_output_width) /
static_cast<float>(window_width)
};
return scale_x;
}
The scale is then set up at the end of the
Window
constructor.
Window::Window(const Settings& settings) {
// ...
m_renderer = SDL_CreateRenderer(
m_window, -1, renderer_flags
);
if (m_renderer == nullptr) {
APP_ERROR("Error creating SDL_Renderer!");
return;
}
// Set render scale for high DPI displays
const float scale{get_scale()};
SDL_RenderSetScale(m_renderer, scale, scale);
}
Building and running the application now shows a way better result then before.

Dockspace
The basics are up and running, dock spaces will now enable rearranging widgets and docking those to the window and each other, giving a user the option to freely define the application layout.
For this to work the ImGui flag DockingEnable
needs to
be set in run
.
// src/core/Core/Application.cpp
int App::Application::run() {
// ...
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
// Enable docking
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
// ...
}
Inside the main loop, after the frame creation, but before the widget, will the setup for the dock space be placed.
// src/core/Core/Application.cpp
int App::Application::run() {
// ...
m_running = true;
while (m_running) {
// ...
ImGui::NewFrame();
// This is all that needs to be added for this:
ImGui::DockSpaceOverViewport();
// The previously created widget
if (m_show_some_panel) {
ImGui::Begin("Some panel", &m_show_some_panel);
ImGui::Text("Hello World");
ImGui::End();
}
// ...
}
}
This will create a full sized dock space, covering the whole window. Have a look at the following video to see the dock space in action.
Event handling
Customising the event handling from the previous implemented polling
is not much more work; setting up a new method in
Application
called on_event
.
// src/core/Core/Application.hpp
class Application {
public:
// ...
// General event handler
void on_event(const SDL_WindowEvent& event);
// ...
} // namespace App
The implementation will use a switch
statement to
handle different events.
// src/core/Core/Application.cpp
void Application::on_event(const SDL_WindowEvent& event) {
switch (event.event) {
// ...
}
}
Furthermore, three specialised events that will be called from the
generic event handler. The on_minimize
and
on_shown
handler will set the
m_minimized
state of the application. This will be used
to optimise the application when in idle mode. The
on_close
event will call stop
.
// src/core/Core/Application.hpp
class Application {
public:
// ...
void on_event(const SDL_WindowEvent& event);
// Specialised events
void on_minimize();
void on_shown();
void on_close();
// ...
private:
// ...
// Keep minimized state
bool m_minimized{false};
} // namespace App
From the generic event handler on_event
the three
specialised events are called.
// src/core/Core/Application.cpp
void Application::on_event(const SDL_WindowEvent& event) {
switch (event.event) {
case SDL_WINDOWEVENT_CLOSE:
return on_close();
case SDL_WINDOWEVENT_MINIMIZED:
return on_minimize();
case SDL_WINDOWEVENT_SHOWN:
return on_shown();
}
}
void Application::on_minimize() {
m_minimized = true;
}
void Application::on_shown() {
m_minimized = false;
}
void Application::on_close() {
stop();
}
To get on_event
hooked up is by adding it to the
current event polling inside the main loop.
// src/core/Core/Application.cpp
int App::Application::run() {
// ...
m_running = true;
while (m_running) {
SDL_Event event{};
while (SDL_PollEvent(&event) == 1) {
ImGui_ImplSDL2_ProcessEvent(&event);
if (event.type == SDL_QUIT) {
stop();
}
// Capture events inside the window
if (
event.type == SDL_WINDOWEVENT &&
event.window.windowID ==
SDL_GetWindowID(m_window->get_native_window())
) {
on_event(event.window);
}
}
// ...
}
return m_exit_status;
}
Using the m_minimized
state to optimise the application
in idle mode by wrapping the viewport and dock space creation, as
well as the widget, to not run when minimised.
// src/core/Core/Application.cpp
int App::Application::run() {
// ...
m_running = true;
while (m_running) {
// ...
ImGui::NewFrame();
if (!m_minimized) {
ImGui::DockSpaceOverViewport();
// ...
if (m_show_some_panel) {
// ...
}
}
ImGui::Render();
// ...
}
}
Application menu
Inside the dock space, before the widget setup, is a good place to add the code for the application menu. This will serve to exit the application and show or hide the "some panel" widget.
// src/core/Core/Application.cpp
// ...
ImGui::DockSpace(ImGui::GetID("DockSpace"));
if (ImGui::BeginMainMenuBar()) {
if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("Exit")) {
stop();
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("View")) {
ImGui::MenuItem(
"Some Panel", nullptr, &m_show_some_panel
);
ImGui::EndMenu();
}
ImGui::EndMainMenuBar();
}
if (m_show_some_panel) {
// ...
}
// ...
This will render the menu at the top of the window, enabling some basic application control.

Custom font
Making things look a little less "Debugger UI" like, adding
a custom font can go a long way. I will use the amazing Open Source
font
Manrope. After downloading it all fonts will be placed relative to
src/app/App/Main.cpp
file inside a
fonts
folder. The full path of the folder is
src/app/App/fonts
.
The code for the font is placed inside
Application
after the ConfigFlags
, but
before the renderer setup.
// src/core/Core/Application.cpp
int App::Application::run() {
// ...
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
// Get proper display scaling for high DPI displays
const float font_scaling_factor{m_window->get_scale()};
// Font size will be 18pt
const float font_size{18.0F * font_scaling_factor};
// Load font and set as default with proper scaling
io.Fonts->AddFontFromFileTTF(
"fonts/Manrope.ttf",
font_size
);
io.FontDefault = io.Fonts->AddFontFromFileTTF(
"fonts/Manrope.ttf",
font_size
);
io.FontGlobalScale = 1.0F / font_scaling_factor;
ImGui_ImplSDL2_InitForSDLRenderer(
m_window->get_native_window(),
m_window->get_native_renderer()
);
// ...
}
A final look at the application now shows the amazing new font, everything coming together and starting to look like an end-user application.
Epilogue
And that is that! A base to continue when creating GUIs in C++ for me. And again, there is a more extended version of this on GitHub.
GitHub template: github.com/MartinHelmut/cpp-gui-template-sdl2
There is also an OpenGL version of the GitHub template with multi-viewport and detachable widgets: github.com/MartinHelmut/cpp-gui-template-sdl2-opengl