Pre-Order Kelboy 2.0 Kit

Ayuda al proyecto y aprovéchate temporalmente del código de descuento GBZEROKELBOY del 10% en pre-orders
kelboy research

Portable GUI on any platform

Learn how to easily build a portable GUI

Advance
5


Introduction

The idea of having a program running on any platform is quite appealing to a programmer.

In the last decades, the irruption of new programming languages proposed very important ideas, such as the widespread use of the mouse or touch screens.

In this context, a programmer who wants to expand to a wider audience must know how to generate interfaces compatible with this type of input peripherals, since not everyone runs programs from the console, and more so when the vast majority of everyday devices they do not have it.


Build a graphical user interface

What is a GUI?

When we talk about programming a GUI, what we are really doing is providing the user with a graphical interface with which, using a set of images and graphic objects, the user can perform with little knowledge of the functionality of a program.

More deeply when we talk about a GUI we talk about a structure divided into layers, which allows communication from the highest level (user) to the lowest level (hardware).

Capas interfaz gráfica

When programming you have to consider a series of options:

  • Modern operating systems introduce dynamic and reusable components into the graphical interface layer so that new programs easily inherit those components and the program can have a friendly user interface (requires dependency on the window manager, the components used in our programs will be dependent):ejemplo componentes GTK para Gnome

  • Use frameworks that speak directly to hardware (at least graphically) by providing developers with an API that they can use to "draw" content or build our components without the need for the window manager layer (adding more portability to our programs by removing the dependence on using one specifically for drawing nuestros componentes):Capas de comunicación usando OPENGL

Real portability

Choose the libraries that best suit our needs

As we have seen before, this of the graphical interfaces seems to have more guts than the naked eye might seem.

This slight explanation wants to justify the following concept, if I want to run my program on different platforms I cannot choose libraries or dependencies that anchor me to one platform, that is, if I make a program with the GTK library I cannot run that program on a platform that do not have GTK libraries or their dependencies. As I speak of GTK it happens with any type of component dependent on the window manager.

For years the solutions to this problem have been two:

  1. Generate the necessary libraries and / or translation layers to be able to interpret the dependencies
  2. Build the "from scratch" components and / or use libraries that do not depend on the window manager.

In this case we will exploit the second way.

Agility and portability

Our next starting point puts us in the position of what programming language should we choose and if we have community-maintained libraries that allow us to be more agile in development.

As we have observed in one of the previous points, if we want to skip the layer of dependent components of the platform's window manager, we will need to build our program based on OpenGL-type solutions.

Going step by step in this brief and light tutorial we will choose C as the programming language to follow and the basic programming methodology with SDL libraries.


OpenGL - The door to graphics power

Initialization

Before you start drawing, you first need to initialize yourself in the graphical and OpenGL interfaces.

To compile a program what we must understand is what we must generate:

  • The Makefile file that generates the "routine" or configuration to link and compile our program (make command, although it is not necessary to generate a Makefile is highly recommended)
  • Source code that we want to compile (myprogram)

Makefile files follow a structure that could be complicated by tasks, for this tutorial we will simply use a file with very basic content:

CPPFLAGS=$(shell sdl2-config --cflags) $(EXTRA_CPPFLAGS)
LDLIBS=$(shell sdl2-config --libs)
EXTRA_LDLIBS?=-lGL
all: myprogram
clean:
	rm -f *.o myprogram
.PHONY: all clean

A basic main of a GUI in pseudocode could be the following:

#include 

int main(){


    //Inicializar el programa
    createWindow(title,width,height);
    //Inicializar el contexto
    createOpenGLContext(settings);

    /* 
    * Programar: 
    * 1) Eventos
    * 2) Bucle con la lógica
    * 3) Pintar 
    */
    while(programOpened){
          //escuchar los eventos  
          while(event = newEvent()){
              handleEvents(event);
          }
          //lógica
          updateScene();
          //pintar
          drawGraphics();
      
    }

    return 0;
}

In the pseudocode example we have separated updateScene and drawGraphics to highlight that they are two different code snippets, but as you can easily deduce, what you do in updateScene has direct repercussions on drawGraphics so you would unify the code in a "render" function.

Generating code:

To start with this type of programming what we will do will be a simple base program with all the elements that describe the previously defined structure:

#include 
 
int main(int argc, char** argv)
{
        SDL_Init(SDL_INIT_EVERYTHING);
        SDL_Surface *screen;
        screen = SDL_SetVideoMode(640, 480, 32, SDL_SWSURFACE);
//      screen = SDL_SetVideoMode(640, 480, 32, SDL_SWSURFACE|SDL_FULLSCREEN);
        bool running = true;
        while(running) {
                SDL_Event event;
                while(SDL_PollEvent(&event)) {
                        switch(event.type) {
                                case SDL_QUIT:
                                        running = false;
                                        break;
                        }
                }
                //logic && render
                SDL_Flip(screen);
        }
        SDL_Quit();
        return 0;
}

If we want to compile it through the console (without generating a Makefile):

g++ myprogram.cpp -lSDL

Showing an image on screen:

Maybe if you are not familiar with the programming of graphical interfaces, the concept seems too complex and what you want is simply that an image appears (it is the first example given when choosing OpenGL as the programming API).

In the following example you can see how to display a screen image contained in a .bmp file:

#include "SDL/SDL.h"

int main( int argc, char* args[] ){

    SDL_Surface* targetImage = NULL;
    SDL_Surface* screen = NULL;

    //init
    SDL_Init( SDL_INIT_EVERYTHING );

    //buffer principal
    screen = SDL_SetVideoMode(640, 480, 32, SDL_SWSURFACE );

    //volcar el archivo bmp
    targetImage = SDL_LoadBMP( "mylogo.bmp" );

    //pintamos la imagen en el buffer principal (screen)
    SDL_BlitSurface(targetImage, NULL, screen, NULL);

    //update
    SDL_Flip(screen);

    //mantiene el programa 5 segundos abierto
    SDL_Delay(5000);

    //liberamos memoria
    SDL_FreeSurface(targetImage);

    //salir
    SDL_Quit();

    return 0;
}

It is an interesting example to understand how SDL works when painting on the screen; This example lacks events as it loads a buffered image, writes it to the main buffer (screen), pauses the program for 5 seconds and disappears.


Portable GUI

The importance of the previous points is to understand some of the most important pieces of a graphic program. No one who appreciates you can tell you that the above code will save your life. It will only serve to understand or create a knowledge base for the next step. Create your GUI with the programming paradigms of the 21st century.

One of the main drawbacks when programming in C / C ++ is that our projects can have a series of dependencies that make our program work perfectly, but we are not able to manage beyond the importation of those dependencies to be used. . A consequence of this is that our source code is accompanied by very heavy files that have nothing to do with what comes out of our fingers. Today's current network devices and technologies make this not very problematic, but we depend on a very modern infrastructure, and that is not correct.

To solve this problem, yours will be to use a package manager appropriate to our programming language. Having used C / C ++ for this example, a large majority of programmers do not know this type of technology based on remote pools from which to download our dependencies (both source code and binaries).

Learn and understand how to use Conan and its advantages, if you want to know more information you have its website and its github, we will stick to what you need to have it in your system:

Conan installation and configuration

(install pip)

curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python get-pip.py

Via pip:

pip install conan

Or use the method they describe on their website:

git clone https://github.com/conan-io/conan.git
cd conan && sudo pip install -e .

Once it is installed by default there is only one central pool, so if we want to increase the number of available dependencies we must add another one:

conan remote add bincrafters https://api.bintray.com/conan/bincrafters/public-conan

With this we will have our Conan ready for the next step.

Modern GUIs programming in C / C ++

If we wanted to start from scratch all our GUI we would have to dedicate many hours to achieve it, and many times we will support you in this since in many occasions it is a smart decision, but in this case we want to demonstrate that we can make portable interfaces in C / C ++ with OpenGL using Conan, and we use Conan because we want to add external dependencies to be used in our program.

A project with conan must have 3 files:

  1. Main source code (main.cpp)
  2. CMakeLists.txt (file that will create our Makefile and what is necessary to compile with the make command, if you don't know CMake I invite you to look at its website)
  3. conanfile.txt (where we declare the dependencies to use and import in our program)

Our intention is to import the incredible free imgui library into our program, generate a window with opengl and use the events with glew.

For this we must build a conanfile.txt with the following content:

[requires]
imgui/1.76
mesa/20.0.1@bincrafters/stable
sdl2/2.0.9@bincrafters/stable
glfw/3.3.1@bincrafters/stable
glew/2.1.0@bincrafters/stable

[generators]
cmake

[imports]
./res/bindings, imgui_impl_glfw.cpp -> .
./res/bindings, imgui_impl_glfw.h -> .
./res/bindings, imgui_impl_opengl2.cpp -> .
./res/bindings, imgui_impl_opengl2.h -> .

The table / 20.0.1 requirement is optional since if not indicated 19.0.3 will be used, our intention was to use the latest and most modern dependencies (that's why we have added more repositories). To search for the latest dependencies use the conan search command explained in your docs.

Imports are the dependencies that must be copied in order to be used in our program. These dependencies are in the imgui github in case you want to take a closer look, for our example we will use these two that we want to use OpenGL 2.0.9 for our window.

We continue with the CMakeLists.txt:

cmake_minimum_required(VERSION 2.8.12)

project(opengl-gui)

add_definitions("-std=c++11")

include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup()

find_package(imgui CONFIG)
find_package(OpenGL REQUIRED)
find_package(glfw3 CONFIG)
find_package(GLEW CONFIG)

add_executable( opengl-gui
                main.cpp
                imgui_impl_glfw.cpp
                imgui_impl_glfw.h
                imgui_impl_opengl2.h
                imgui_impl_opengl2.cpp
            )


target_include_directories(opengl-gui PRIVATE ${OPENGL_INCLUDE_DIR})

target_link_libraries(opengl-gui ${OPENGL_LIBRARIES} glfw imgui)

install(TARGETS opengl-gui DESTINATION bin)
set_target_properties(opengl-gui PROPERTIES DEBUG_POSTFIX _d)

At this point we have defined that we are going to use our dependencies in our program, so when compiling, the libraries will be included and the compiler will know what the binary has to link with.

Now we only have the main piece of the puzzle, our source code, for this example we will only show something basic:

#include 
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl2.h"
#include 
#include 

static void error_callback(int error, const char* description){
    fprintf(stderr, "Error %d: %s\n", error, description);
}

int main(){
    // Setup window
    glfwSetErrorCallback(error_callback);
    if (!glfwInit())
        return 1;
    GLFWwindow* window = glfwCreateWindow(640, 480, "Lemoncrest OPENGL IMGUI example", NULL, NULL);
    if (window == NULL)
        return 1;
    glfwMakeContextCurrent(window);
    glfwSwapInterval(1); // Enable vsync

    // Setup Dear ImGui context
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();

    ImGuiIO& io = ImGui::GetIO(); (void)io;

    ImGui::StyleColorsDark();

    ImGui_ImplGlfw_InitForOpenGL(window, true);
    ImGui_ImplOpenGL2_Init();

    bool show_demo_window = true;
    bool show_another_window = false;
    ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);

    while (!glfwWindowShouldClose(window)){

        glfwPollEvents();

        ImGui_ImplOpenGL2_NewFrame();
        ImGui_ImplGlfw_NewFrame();

        ImGui::NewFrame();

        //window1 
        static float f = 0.0f;
        static int counter = 0;

        ImGui::Begin("Hola amigos");

        ImGui::Text("Hoy os enseñaremos a como programar en el siglo XXI");
        ImGui::Checkbox("Otra ventana", &show_another_window);

        ImGui::SliderFloat("float", &f, 0.0f, 1.0f);
        ImGui::ColorEdit3("clear color", (float*)&clear_color);

        if (ImGui::Button("Botón")){
            counter++;
        }
        ImGui::SameLine();
        ImGui::Text("contador: %d", counter);

        ImGui::Text("Midiendo: %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
        ImGui::End();
        //end window1

        //checkbox shows window2
        if (show_another_window){
            ImGui::Begin("Ventana dependiente del checkbox", &show_another_window);
            ImGui::Text("Uso interactivo");
            if (ImGui::Button("Cerrar"))
                show_another_window = false;
            ImGui::End();
        }

        // Rendering
        ImGui::Render();
        int display_w, display_h;
        glfwGetFramebufferSize(window, &display_w, &display_h);
        glViewport(0, 0, display_w, display_h);
        glClearColor(clear_color.x, clear_color.y, clear_color.z, clear_color.w);
        glClear(GL_COLOR_BUFFER_BIT);

        ImGui_ImplOpenGL2_RenderDrawData(ImGui::GetDrawData());

        glfwMakeContextCurrent(window);
        glfwSwapBuffers(window);

    }

    // Cleanup
    ImGui_ImplOpenGL2_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    ImGui::DestroyContext();

    glfwDestroyWindow(window);
    glfwTerminate();

    return 0;
}

If you look closely the code (like any other) is divided into the basic parts that we have defined in the previous steps, initialize, create a window, collect events, define logic and paint, and finally clean and exit.

With these few lines you will get a modern cross-platform GUI:

portable gui opengl

Compile and run our program:

Compile and generate the dependencies for our program with the optimal configuration:

conan install . -s build_type=Release --build missing

Generate the makefiles or configuration files to compile and link:

cmake .

Compile:

make

Launch:

./bin/opengl-gui