Learn how to easily build a portable GUI
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.
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).
When programming you have to consider a series of options:
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:
In this case we will exploit the second way.
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.
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:
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.
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
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.
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:
(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.
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:
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:
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