DEV Community

Misael Braga de Bitencourt
Misael Braga de Bitencourt

Posted on

Criando interface gráfica Desktop nativa utilizando C++ com GTK3

Trabalho há muitos anos desenvolvendo interface gráfica para a web, soluções desktop Java e Android. Atualmente, eu estou curioso sobre como seria criar essas soluções para Desktop sem utilizar alguma ferramenta multiplataforma.

Com o intuito de experimentar como seria desenvolver uma interface gráfica para PCs Desktop nativa em C/C++, decidi criar uma pequena aplicação usual "TODO list" (lista de afazeres) nesses moldes.

Quando eu me refiro a "Desktop nativo", estou falando de soluções que não envolvam containers web ou outras soluções multi-plataforma como o Java FX ou Swing. Apesar de soluções híbridas serem ideais para criar aplicações para várias plataformas com o mesmo código, elas possuem um GAP em questão de performance. Além disso, diferentes sistemas operacionais se comportam de maneiras distintas e um código genérico para todos eles pode apresentar comportamentos inesperados.

Os diferentes sistemas operacionais possuem diferentes APIs e frameworks para criar apps Desktop. Para a minha pequena aplicação, decidi que ela seria executada no Linux e codificada em C++ com o compilador GCC. Dessa forma, utilizo apenas software livre tanto no S.O. quanto nas ferramentas utilizadas. Caso o programa necessite ser executado em Windows, o WSL pode abrir o software. Já no MacOS, é muito fácil de emular outros sistemas.

Continuando com a utilização de software livre, a biblioteca GTK nos permite construir interfaces gráficas. Ferramenta esta utilizada em programas famosos como GIMP (editor de imagens) e Transmission (cliente BitTorrent).

Sample

O repositório com o código fonte do projeto encontra-se aqui: https://github.com/misabitencourt/gtk3-cpp-todolist

Criando um arquivo CMAKE para o projeto

Foi feito o uso do CMAKE para definir as configurações de build. Eu acho muito cômodo o fato de o CMAKE gerar o Makefile no Linux e também poder criar um projeto Visual Studio no Windows.

O CMAKE pode ser instalado via APT em distribuições Debian.

sudo apt install cmake
Enter fullscreen mode Exit fullscreen mode

O arquivo CMakeLists ficou da seguinte forma:

cmake_minimum_required(VERSION 3.0)
project(todolist_app)
set(CMAKE_CXX_STANDARD 17)

# Find GTK3 package
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK3 REQUIRED gtk+-3.0)

file(GLOB SOURCES
    "src/sample.cpp"
)

add_compile_options(-fpermissive)
include_directories(./)
include_directories(${GTK3_INCLUDE_DIRS})

add_executable(todolist_app ${SOURCES})


target_link_libraries(todolist_app ${GTK3_LIBRARIES})

add_definitions(${GTK3_CFLAGS_OTHER})
Enter fullscreen mode Exit fullscreen mode

Para gerar o Makefile, basta rodar:

mkdir build
cd build
cmake ../
Enter fullscreen mode Exit fullscreen mode

Antes de executar o script do Makefile, é preciso instalar a lib-gtk:

sudo apt install libgtk-3-dev
Enter fullscreen mode Exit fullscreen mode

Obviamente, o compilador GCC (e o build-essential) junto com o make também têm de estar instalados. Geralmente, esses pacotes já estão configurados em praticamente toda a máquina de desenvolvedores. Para compilar o projeto, basta usar o make:

cd build
make
./todolist_app
Enter fullscreen mode Exit fullscreen mode

Main loop

O código abaixo é o arquivo que possui o void main do projeto. Nele, o loop principal da aplicação é iniciado. Antes disso, foram definidas algumas structs e módulos para salvar, atualizar, listar e deletar a lista de afazeres. Como o objetivo é apenas o foco na interface, esses métodos persistem em uma lista local na memória.

Main loop:

#include <gtk/gtk.h>
#include<list>
#include<string>

// Types
#include "types/models.h"

// Services
#include "services/todo-service.h"

// Views
#include "views/dialogs.h"
#include "views/main.h"


int main(int argc, char *argv[]) 
{
    gtk_init(&argc, &argv);

    // Show all widgets
    MainWindow::openMainWindow();

    gtk_main();

    return 0;
}

Enter fullscreen mode Exit fullscreen mode

Antes da criação da janela da aplicação, a API da GTK requer uma chamada de inicialização passando os parâmetros dos argumentos da chamada. O método gtk_main inicia o event loop da interface gráfica.

O módulo MainWindow foi definido em views/main.h:

#define APP_MAIN_WINDOW_WIDTH 400
#define APP_MAIN_WINDOW_HEIGHT 800
#define SUCCESS_COLOR "#99DD99"

namespace MainWindow
{
    // Main widget pointers
    GtkWidget *mainWindow = nullptr;
    GtkWidget *mainWindowContainer = nullptr;
    GtkWidget *(*mainWindowRefresh)();
    GtkWidget *todoInputText;
    int inEdition = 0;

    // Button events
    void mainWindowViewTodoListSaveClicked(GtkWidget *widget, gpointer data)
    {
        GtkEntry *entry = GTK_ENTRY(data);
        const gchar *text = gtk_entry_get_text(entry);
        Todo todoDto;
        todoDto.description = (char *)text;
        if (inEdition)
        {
            todoDto.id = inEdition;
            TodoService::todolistUpdate(&todoDto);
        }
        else
        {
            TodoService::todolistSave(&todoDto);
        }
        inEdition = 0;
        mainWindowRefresh();
        gtk_widget_show_all(mainWindowContainer);
    }

    void mainWindowViewTodoListEditClicked(GtkWidget *widget, int id)
    {
        Todo *todo = TodoService::todolistLoad(id);
        inEdition = id;
        g_print("Editing %i %s\n", todo->id, todo->description);
        gtk_entry_set_text(GTK_ENTRY(todoInputText), todo->description);
    }

    void mainWindowViewTodoListDeleteClicked(GtkWidget *widget, int id)
    {
        TodoService::todolistDelete(id);
        mainWindowRefresh();
        gtk_widget_show_all(mainWindowContainer);
    }

    // Render function
    GtkWidget *createMainWindowView()
    {
        GtkWidget *list_box;
        GtkWidget *row;
        GtkWidget *hbox;
        GtkWidget *label;

        mainWindowRefresh = createMainWindowView;

        // Creates a new window
        if (mainWindow == nullptr)
        {
            mainWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
            gtk_window_set_title(GTK_WINDOW(mainWindow), "Todo List");
            gtk_window_set_default_size(GTK_WINDOW(mainWindow), APP_MAIN_WINDOW_WIDTH, APP_MAIN_WINDOW_HEIGHT);
            g_signal_connect(mainWindow, "destroy", G_CALLBACK(gtk_main_quit), NULL);
        }
        else
        {
            gtk_widget_destroy(mainWindowContainer);
        }

        GtkWidget *windowContainer;
        windowContainer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
        mainWindowContainer = windowContainer;
        gtk_container_add(GTK_CONTAINER(mainWindow), windowContainer);

        GtkWidget *todoFormContainer;
        todoFormContainer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
        gtk_container_add(GTK_CONTAINER(windowContainer), todoFormContainer);

        GtkWidget *inputVbox;
        inputVbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
        gtk_widget_set_size_request(inputVbox, 250, -1);
        gtk_container_add(GTK_CONTAINER(todoFormContainer), inputVbox);
        todoInputText = gtk_entry_new();
        gtk_box_pack_start(GTK_BOX(inputVbox), todoInputText, TRUE, TRUE, 0);

        GtkWidget *saveBtn;
        saveBtn = gtk_button_new_with_label("Save");
        gtk_widget_set_size_request(saveBtn, 80, -1);
        gtk_container_add(GTK_CONTAINER(todoFormContainer), saveBtn);
        g_signal_connect(saveBtn, "clicked", G_CALLBACK(mainWindowViewTodoListSaveClicked), todoInputText);

        GtkWidget *cancelBtn;
        cancelBtn = gtk_button_new_with_label("Cancel");
        gtk_widget_set_size_request(cancelBtn, 80, -1);
        gtk_container_add(GTK_CONTAINER(todoFormContainer), cancelBtn);

        // // Creates a new GtkListBox
        list_box = gtk_list_box_new();
        gtk_container_add(GTK_CONTAINER(windowContainer), list_box);

        // // Creates and add rows to the GtkListBox
        int i = 0;
        for (std::list<Todo>::iterator it = todoListRepository.begin(); it != todoListRepository.end(); ++it)
        {
            row = gtk_list_box_row_new();
            hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);

            // Column 1
            gchar *label_text1 = g_strdup_printf(it->description, i);
            label = gtk_label_new(label_text1);
            gtk_widget_set_size_request(label, 250, -1);
            gtk_box_pack_start(GTK_BOX(hbox), label, TRUE, TRUE, 0);
            g_free(label_text1);

            // Column 2
            GtkWidget *editBtn = gtk_button_new_with_label("Edit");
            gtk_widget_set_size_request(saveBtn, 80, -1);
            gtk_box_pack_start(GTK_BOX(hbox), editBtn, TRUE, TRUE, 0);
            g_signal_connect(editBtn, "clicked", G_CALLBACK(mainWindowViewTodoListEditClicked), it->id);

            // Column 3
            GtkWidget *removeBtn = gtk_button_new_with_label("Remove");
            gtk_widget_set_size_request(removeBtn, 80, -1);
            gtk_box_pack_start(GTK_BOX(hbox), removeBtn, TRUE, TRUE, 0);
            int index = i;
            g_signal_connect(removeBtn, "clicked", G_CALLBACK(mainWindowViewTodoListDeleteClicked), it->id);

            gtk_container_add(GTK_CONTAINER(row), hbox);
            gtk_container_add(GTK_CONTAINER(list_box), row);
            i++;
        }

        return mainWindow;
    }

    // Open function
    void openMainWindow()
    {
        gtk_widget_show_all(createMainWindowView());
    }
}
Enter fullscreen mode Exit fullscreen mode

Semelhante a abordagem utilizada nas tecnologias web e no desenvolvimento Android, no GTK, temos uma interface para criar uma janela gtk_window_new e dentro dela, adicionamos vários elementos que podem conter vários outros elementos dentro, assim, formando uma árvore. Os elementos aqui são GtkWidget. Na documentação da lib podemos encontrar os diversos tipos de Widgets que podemos utilizar.

Em meu experimento, não utilizei nenhuma ferramenta para o design da tela. Tudo foi feito via interface de programação. Todavia, o GTK permite a criação de arquivos XML que representam a tela. E o Glade é uma interface gráfica que permite a criação destes XML.

Top comments (0)