How to start developing Dear ImGui Applications in C#

In the previous getting started tutorial, we discussed how it is possible to build Dear ImGui applications in C# using Raylib as the backend. Now that we have everything in place, we can continue our long journey from start-to-finish, building an application.

As you may have noticed, I created my project as "Story Creator". For those who may know me on other platforms, Story Creator is currently a WinForms application. But we are going to change this so that it can be compiled to Linux and MacOS using .NET Core 6.

These tutorials will use this project to serve us for the future.

Setting up our "Main View"

In previous projects, I tend to stick to a procedural form of programming. This means keeping most methods and functions static and only using classes as our data, like in data-oriented programming. This may be unconventional for C#, but this is how I like to code. It also works better for Dear ImGui and Raylib.

In our code base, we have created a new folder in our project called "Views" and a file called "MainView.cs":

Inside MainView, we have two methods:

public static void Init()
{
    
}

public static void Render()
{

}

They pretty much say exactly what you think. Init will initialise all our data before we begin our render loop, then Render will be called in our render loop to give us back results.

Going back to Program.cs, we can modify the file to do exactly this.

Raylib.InitWindow(1280, 720, "Story Creator");
Raylib.SetTargetFPS(60);

var context = ImGui.CreateContext();
ImGui.SetCurrentContext(context);

var controller = new Controller();
MainView.Init(); // our init method
            
while (!Raylib.WindowShouldClose())
{
    controller.NewFrame();
    controller.ProcessEvent();
    ImGui.NewFrame();

    Raylib.BeginDrawing();
    Raylib.ClearBackground(Color.DARKGRAY);

    MainView.Render(); // our render method

    ImGui.Render();
    controller.Render(ImGui.GetDrawData());
    Raylib.EndDrawing();
}

controller.Shutdown();

Adding Some Dear ImGui Stuff

Now that we have a class setup to perform some of our initialisation and rendering, we will use this class for the most part as we go through this tutorial series.

Let's add some code to our Render method.

ImGui.BeginMainMenuBar();

if (ImGui.BeginMenu("Views"))
{
    if (ImGui.MenuItem("Story Progression"))
    {
        
    }

    ImGui.End();
}

ImGui.EndMainMenuBar();

If we run this now, we get the following result:

On my 4K screen, the text is tiny. This should probably be fixed, but I will discuss this later and add an option to modify font size (or UI ratio).

Let's add how to implement new views or frames we will interact with.

Adding Views

Firstly, we will create an enum representing each View we can add to the main screen.

[Flags]
enum ViewState
{
    None = 0,
    StoryProgression = 0x00000001,

}

This method is much more efficient than using entire classes to display certain windows, as there is no instantiation of objects or memory allocations. We add the Flags attribute to indicate that the enum can be used to match flags from an integer. This is particularly useful as certain methods will function as we expect them to at runtime.

To add this variable, we will simply make a static version of it.

static ViewState Views;

Inside our Init method, we will add the following:

Views = ViewState.None;

This will tell us we do not want anything displayed initially.

Back in our Render method, we can update our code to allow for us to display this view.

if (ImGui.MenuItem("Story Progression"))
{
    Views |= ViewState.StoryProgression;
}

As you can see, we bitwise OR the value ViewState.StoryProgression to display this specific window.

Later, we will perform a check.

if (Views.HasFlag(ViewState.StoryProgression))
{
    RenderStoryProgression();
}

We have not defined RenderStoryProgression yet, so let's do that.

static void RenderStoryProgression()
{
    bool open = true;
    if (ImGui.Begin("Story Progression", ref open, ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.AlwaysAutoResize))
    {
        if (ImGui.Button("New Chapter"))
        {

        } ImGui.SameLine();

        if (ImGui.Button("New Conversation"))
        {

        } ImGui.SameLine();

        if (ImGui.Button("New Event"))
        {

        } ImGui.SameLine();

        if (ImGui.Button("Discovery"))
        {

        }

        ImGui.End();
    }

    if (!open)
    {
        Views &= ~ViewState.StoryProgression;
    }
}

And the following is the result:

So, what exactly happened?

When we click on Views -> Story Progression, we get a window appear in the screen with the ability to close it. We force it to always auto-resize. The reason why this can be useful is to avoid horizontal and vertical scrollbars. Typically, it is good practice to only allow scrollbars in child frames to avoid squeezing too much in one space.

Dear ImGui also does a pretty good job of laying out out forms neatly so we don't have to.

We also check to see if the window close button is about to be clicked. We do this with the use of the bool open = true at the top of the method. This means that when it is passed into the window, if the user presses the button, the result of open becomes false.

We can check this later and then perform another bitwise operation on the Views variable. By doing Views &= ~ViewState.StoryProgression, we remove the value of that enum from the Views variable, which effectively closes the window.

This is a lot more efficient than using an array or List for our views and we don't require additional information for it.

Now that we have something useful on-screen, we can start applying some real code that will give us something more useful to work with. But we will work with this later.

For now, stay tuned for the next post when we fix up the font sizes.

Final Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ImGuiNET;

namespace StoryCreator.Views
{
    internal class MainView
    {

        static ViewState Views;

        public static void Init()
        {
            Views = ViewState.None;
        }

        public static void Render()
        {
            ImGui.BeginMainMenuBar();

            if (ImGui.BeginMenu("Views"))
            {
                if (ImGui.MenuItem("Story Progression"))
                {
                    Views |= ViewState.StoryProgression;
                }

                ImGui.End();
            }

            ImGui.EndMainMenuBar();

            if (Views.HasFlag(ViewState.StoryProgression))
            {
                RenderStoryProgression();
            }
        }

        static void RenderStoryProgression()
        {
            bool open = true;
            if (ImGui.Begin("Story Progression", ref open, ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.AlwaysAutoResize))
            {
                if (ImGui.Button("New Chapter"))
                {

                } ImGui.SameLine();

                if (ImGui.Button("New Conversation"))
                {

                } ImGui.SameLine();

                if (ImGui.Button("New Event"))
                {

                } ImGui.SameLine();

                if (ImGui.Button("Discovery"))
                {

                }

                ImGui.End();
            }

            if (!open)
            {
                Views &= ~ViewState.StoryProgression;
            }
        }

    }

    [Flags]
    enum ViewState
    {
        None = 0,
        StoryProgression = 0x00000001,

    }
}
You've successfully subscribed to Luke Selman Blog
Great! Next, complete checkout to get full access to all premium content.
Error! Could not sign up. invalid link.
Welcome back! You've successfully signed in.
Error! Could not sign in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.