Building a Cross-Platform C++ GUI App with CMake, Raylib, and Dear ImGui

I want to build cross-platform GUI apps in C++, and I’ve heard that Dear ImGui is awesome. So today, I’m building a simple GUI app using Raylib, Dear ImGui, and CMake. Despite decades of programming experience, I’ve never used ImGui before — so let’s see how easy it really is! By the end of this video, we’ll have a working test app and a solid template for future projects. Let’s get started!

NOTE: As promised in the video, click here to get the template code.

Why Raylib and Dear ImGui?

First though, let’s answer the most obvious question: why Raylib and ImGui for building an app? Aren’t they for making games?

No! Okay, yes, Raylib was written for video game programming. But, it’s also a great multimedia toolkit. And, it’s both cross-platform and pretty easy to use.

Similarly, while Dear ImGui is popular in the game industry, it’s more than capable of building complex GUIs required for any app. Immediate Mode GUIs are popular in gaming because they’re easy to integrate into their real-time graphics engines. Buy, that does not mean you can’t use them for something else.

So, that’s why I’m using these libraries. Raylib provides the cross-platform ability, and a lot of low-level stuff such as loading images, drawing stuff, and playing sounds. Dear ImGui makes creating GUIs easy. At least, that’s what I’ve been told.

Getting Started

With that out of the way, let’s get started for real. First, we need to get Raylib and Dear Imgui to work together. That’s easy, thanks to the rlImGui library. It handles the integration of those two libraries.

Now it’s time to create a skeleton project. I’m using CMake as build system, which has become my go-to build system for new C++ projects.

Here is where we hit our first hurdle: neither ImGui, nor rlImGui have CMake build scripts. ImGui has no build script for the core library, while rlImGui uses premake to build the examples.

Actually, when you look at them closer, it’s no big deal. That’s because rlImGui’s library is just one source file to build, while ImGui has only a five source files, and they’re all in the project’s root directory. So, all that’s required, is to add the source files to your project, and then add the library root directories to the header search path

Here’s my CMake code for building ImGUi. This will fetch it from the internet if needed:

# Fetches the version of Dear ImGui that we want

set(IMGUI_VERSION 1.91.8)
FetchContent_Declare(
    imgui
    URL https://github.com/ocornut/imgui/archive/refs/tags/v${IMGUI_VERSION}-docking.tar.gz
)
FetchContent_MakeAvailable(imgui)
add_library(imgui STATIC 
    ${imgui_SOURCE_DIR}/imgui.cpp
    ${imgui_SOURCE_DIR}/imgui_demo.cpp
    ${imgui_SOURCE_DIR}/imgui_draw.cpp
    ${imgui_SOURCE_DIR}/imgui_tables.cpp
    ${imgui_SOURCE_DIR}/imgui_widgets.cpp)
target_include_directories(imgui INTERFACE ${imgui_SOURCE_DIR})

I actually copied and pasted bits from my own book The CMake Tutorial. it turns out that the tutorial also makes a great reference.

Now copy and paste one of the rlImGui examples into our skeleton app’s Main.cpp. Build it. And, we have a working app. Great!

Let’s take a quick look at the code, to see how it works. If you’re familiar with Raylib, then you should recognize the basic initialization, game loop, and cleanup code.

Up here you have the initialization code:

	int screenWidth = 1280;
	int screenHeight = 800;

	SetConfigFlags(FLAG_MSAA_4X_HINT | FLAG_VSYNC_HINT | FLAG_WINDOW_RESIZABLE);
	InitWindow(screenWidth, screenHeight, "raylib-Extras [ImGui] example - simple ImGui Demo");
	SetTargetFPS(144);
	rlImGuiSetup(true);

Notice the rlImGuiSetup() call above. This initializes rlImGui so that we can use it.

After that comes the main loop, with the usual Raylib BeginDrawing() & EndDrawing() calls. You can use Raylib to draw anything here. The example code simply clears the background, and then draws the GUI on top. All the ImGui code is sandwiched between rlImGuiBegin() and rlImGuiEnd().

	// Main game loop
	while (!WindowShouldClose())    // Detect window close button or ESC key
	{
		BeginDrawing();
		ClearBackground(DARKGRAY);

		// start ImGui Conent
		rlImGuiBegin();

		// show ImGui Content
		bool open = true;
		ImGui::ShowDemoWindow(&open);

		open = true;
		if (ImGui::Begin("Test Window", &open))
		{
			ImGui::TextUnformatted(ICON_FA_JEDI);

			rlImGuiImage(&image);
		}
		ImGui::End();

		// end ImGui Content
		rlImGuiEnd();

		EndDrawing();
		//----------------------------------------------------------------------------------
	}

Yes, all the code to draw and handle the GUI’s events are called directly from inside the program’s main loop. This is because we’re using an Immediate Mode GUI library. It’s part of what makes Immediate Mode GUIs (IMGUIs) easy to use.

The example above displays the Dear ImGui demo window, and then opens a window of its own, with a Jedi icon and an image of two parrots inside.

So far, using Raylib and Dear ImGui is very straightforward.

Building Our Own App

Now, let’s do something more interesting. After all, copying and pasting code does not make you a programmer. And it also gives us no idea as to how easy or hard Dear ImGui is to use. To do that, we need to build an actual GUI. Nothing too fancy. Here’s what I want to build:

Simple test GUI design.

So on the left we have a toolbar, and on the right we have the main window to display an image. The toolbar has controls to scale the image, and also to change the app’s background colour.

It’s simple, but enough to get our hands dirty, and try this ImGui thing out. Let’s build this…

At this point, I kind of fell off the deep end. I thought it would be a good idea to use Dear ImGui’s docking features. Docking, is more complex by nature, and Dear ImGui’s docking API is still a work-in-progress, and most of it is considered unstable. So I ended up going round and round in circles, frustrated that I couldn’t get docking to do what I wanted. It all seemed so hard.

Eventually, I came to my senses, and realized that I didn’t need docking at all for this GUI design. Simply, create a background window the size of the main window, and then create 2 child windows inside it with the widgets that I needed. After a bit of effort, I got it working.

It felt like magic! Sliding the image scale widget smoothly changed the image’s size, and changing the background colour was instant. Everything was silky smooth. This is exactly what I was trying to build.

 

Raylib + Dear imGui test app screenshot.

And here’s the code that does this:

        // start ImGui Content
        rlImGuiBegin();

        // Our menu bar
        if (ImGui::BeginMainMenuBar()) {
			if (ImGui::BeginMenu("File")) {
				if (ImGui::MenuItem("Quit"))
					running = false;

				ImGui::EndMenu();
            }

			if (ImGui::BeginMenu("Window")) {
                if (ImGui::MenuItem("Demo Window", nullptr, showDemoWindow))
					showDemoWindow = !showDemoWindow;

                ImGui::EndMenu();
            }
			ImGui::EndMainMenuBar();
		}

        // This is the main display area, that fills the app's window
        ImGuiViewport* viewport = ImGui::GetMainViewport();
        ImGui::SetNextWindowPos(viewport->WorkPos);
        ImGui::SetNextWindowSize(viewport->WorkSize);
        ImGui::SetNextWindowViewport(viewport->ID);
        if (ImGui::Begin("Main Window", nullptr, 
                ImGuiWindowFlags_NoDecoration |
                ImGuiWindowFlags_NoBringToFrontOnFocus)) {
            // The toolbar
            const float minScale = 0.1f;
            const float maxScale = 4.0f;
            float toolbarWidth = std::max(260.0f, ImGui::GetContentRegionAvail().x * 0.25f);
            ImGui::BeginChild("Toolbar", ImVec2(toolbarWidth, 0.0f));
            ImGui::NewLine();
            ImGui::Text("Image Scale");
            ImGui::SliderFloat("##Image Scale Slider", &imageScale, minScale, maxScale, "");
            ImGui::SameLine();
            ImGui::InputFloat("##Image Scale Input", &imageScale, minScale, maxScale);
            ImGui::NewLine();
            ImGui::Text("Background Colour");
            ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x);
            ImGui::ColorEdit3("##Background Colour RGB", (float*)&bkgndColour); // Edit 3 floats representing a color
            ImGui::PopItemWidth();
            ImGui::EndChild();

            ImGui::SameLine();
                
            // The main display
            ImGui::PushStyleColor(ImGuiCol_ChildBg, bkgndColour);
            ImGui::BeginChild("Main", ImVec2(0.0f, 0.0f), 
                ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar);
            ImGui::Image((ImTextureID)&image, ImVec2(image.width * imageScale, image.height * imageScale));
            ImGui::EndChild();
            ImGui::PopStyleColor();
        }
        ImGui::End();
        
        if(showDemoWindow) {        
            ImGui::ShowDemoWindow(&showDemoWindow);
        }

        // End ImGui Content
        rlImGuiEnd();

The first section creates a basic menu-bar. After that, these lines create a borderless background window the size of the app’s window:

        ImGuiViewport* viewport = ImGui::GetMainViewport();
        ImGui::SetNextWindowPos(viewport->WorkPos);
        ImGui::SetNextWindowSize(viewport->WorkSize);
        ImGui::SetNextWindowViewport(viewport->ID);
        if (ImGui::Begin("Main Window", nullptr, 
                ImGuiWindowFlags_NoDecoration |
                ImGuiWindowFlags_NoBringToFrontOnFocus)) {

From there, we create two child-windows using BeginChild() and EndChild(). The controls for each are created between the begin and end calls.

I have to say that I found laying out widgets to be very easy. Things are laid out in the order you create them starting from the top left. Want the next widget, group or child-window to be to the right of the previous object? Then call ImGui::SameLine(). Want the next object to be below, then call the function to create the next object immediately, without any other calls in-between.

The functions to create widgets are very logically named. Need a slider, then call SliderFloat(), or SliderInt(), or whatever you need. Need a colour editor? Try ColorEdit3().

The only truly weird thing I encountered, was that each widget needs a unique name, and you stick a double-# in front to if you want to hide that name. That’s just how it works. It’s also very convenient.

So that’s the layout done. Now, how does we transfer values between our app code the GUI. You can see it here:

            ImGui::SliderFloat("##Image Scale Slider", &imageScale, minScale, maxScale, "");

We’ve passed the imageScale variable to SliderFloat() (and InputFloat()) as a pointer. This both passes the current value to the slider, and allows the slider to write a new value back for us to read and use. So if the user changes the value, then we get a new number back.

It’s that simple. This is part of what makes an IMGUI easy to use. No duplicating of variables between GUI objects and the app. No setting and getting of variables, or sending and receiving messages or signals. Our app’s state, are stored in a few variables (imageScale, and bkgndColour), and used directly wherever needed.

This was just a simple test, but I’m liking the Raylib and Dear ImGui combo.

Download the Code

I’ve turned this little app into a template for future use. It’s a great project starter. If you want the template to kickstart your own projects, it’s available to both Kea Campus Creator and higher members, and also subscribers to my newsletter. So join the newsletter using the form below. You’ll get the template, and I’ll send you other useful stuff. I’ll make it worth your while.

Don’t see the form? Click here instead.

Want to build GUI Apps? Get a cross-platform template to kickstart your GUI projects...

Join our newsletter and get the Raylib + Dear ImGui C++ starter template as a free welcome gift. 🎁 You'll be building dynamic multi-platform apps in no time...

We value our privacy just as much as you do, and promise to keep your email safe. See our privacy policy for more information. You're in complete control, and can unsubscribe at any time.

Leave a Comment

Your email address will not be published. Required fields are marked *

 


Shopping Cart
Scroll to Top