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).
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
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})
Para gerar o Makefile, basta rodar:
mkdir build
cd build
cmake ../
Antes de executar o script do Makefile, é preciso instalar a lib-gtk:
sudo apt install libgtk-3-dev
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
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;
}
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());
    }
}
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)