ex0ns

about
A Computer Science Student's blog, about programming, algorithms, hack and a lot more.

GUI en C++ avec GTKmm !

15 Jan 2013

Je tiens toujours mes promesses, bien que peu respectueux des délais, mais voici la suite de l'article sur la gestion des processus sur linux, dans lequel je vous dois une explication sur GTKmm et son fonctionnement (je ne suis personellement pas très fan des GUI, mais dans notre cas, elles peuvent s'avêrer utiles).

Qu'est ce que GTKmm ?

gtkmm is the official C++ interface for the popular GUI library GTK+

Pour les anglophobes, il s'agit d'une "interface" en C++ afin de manipuler la bibliothèque GTK+ (tout comme pyGTK est une interface python pour cette même bibliothèque). Ainsi, nous alons pouvoir créer des interfaces graphique en C++ tout comme en C, en utilisant tous les bénéfices qu'apporte ce langage !

Un tutoriel d'introduction est proposé sur le site de gnome il peut être interessant d'y jetter un coup d'oeil avant de lire cet article (mais nous n'aborderons que des aspects très simples de la bibliothèque). Une autre page, à laquelle je fairais réference de nombreuses fois dans l'article est la documentation, elle est très bien faite, très complète, alors pourquoi s'en priver ?!

Pour en faire quoi ?

Si vous avez lu mon article sur la gestion des processus (ce que j'espère que vous avez fait étant donné qu'il s'agit de la suite), vous aurez probablement compris que nous allons créer une interface graphique (GUI) nous permettant de visionner les processus actifs, qui les "possède", et la possibilité de les arrêter, à la fin de cet article notre interface ressemblera à quelque chose comme : final

Cette dernière est très sommaire, mais libre à vous d'y rajouter ce que vous voulez, le but de cet article est d'introduire GTKmm afin de vous montrer sa polyvalence (et sa simplicité). Comme d'habitude, le projet final sera disponible en téléchargement ici.

Let's go !

Avec quoi ?

Pour suivre cet article, et pouvoir compiler vos projets, vous autre besoin d'installer GTKmm (version 3.0) (disponible dans les dépots officiels), sur Fedora, un petit:

yum install gtkmm30-devel

Pour compiler nos projets, nous utiliserons la commande (comme indiqué sur la documentation):

g++ program.cpp -o program `pkg-config --cflags --libs gtkmm-3.0`

Il ne faut pas oublier d'inclure le header #include <gtkmm.h> (ou ses dérivés) dans notre fichier source !

Afin que nous partions tous sur les mêmes bases, voici le code source sur lequel nous partirons :

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <map>
#include <sys/types.h>
#include <dirent.h>
#include <pwd.h>

int strToInt(std::string value){
    std::istringstream temp(value);
    int returned;
    temp >> returned;
    return returned; // Retourne la valeur si value était un nom "numérique", 0 dans le cas contraire.
}
int main(int argc, char* argv[]){

    std::ifstream subInput; // Ouverture des fichiers /proc/<PID>/status
    std::stringstream fileName; //  
    std::string buffer, name, value; // Name et Value sont des variables que l'on utilise pour parser le fcihier /proc/<PID>/status
    std::map<std::string,std::string> process; // "tableau associatif", on associe chaque propriété du processus à sa valeur
    DIR *dir; 
    struct dirent *subDir;
    dir = opendir("/proc/"); // On ouvre le /proc pour lister les processus
    while(subDir = readdir(dir)){ // tant qu'il y a des sous dossiers
        if(strToInt(subDir->d_name)){ // On vérifie si le nom du dossier est numérique
            fileName.str(""); // Vide le flux du nom du fichier
            fileName << "/proc/" << subDir->d_name << "/status"; // Récupération du status correspondant au processus qu'on lit
            subInput.open(fileName.str().c_str(), std::ios::in); 
            while(!subInput.eof()){ // Lecture de l'intégralité du fichier status du processus
                /* 
                   ce passage est le plus "complexe" du code, en effet, on récupère la ligne du fichier, et on la stocke dans un stringstream,
                   pourquoi ne pas directement lire les valeurs de la ligne dans les variables ?
                   Car comme vous avez pu le constater, une propriété à parfois plusieurs valeurs, il y aurait donc un problème lors de la lecture du fichier.
                   Si quelqu'un à une autre méthode (sans utiliser sscanf ou fscanf) cela m'interesse.
                */
                std::stringstream bufferString; 
                getline(subInput,buffer); 
                bufferString << buffer; 
                bufferString >> name >> value;
                process.insert(std::pair<std::string,std::string>(name,value)); // On associe la propriété à sa valeur
            }
            std::cout << process["Name:"] << " " << process["Pid:"] << " " << getpwuid(strToInt(process["Uid:"]))->pw_name << std::endl;
            /* 
               Affichage du résultat, tout simplement, petite subtilité pour récupérer le nom d'utilisateur à partir de l'UID, je vous revois à la fonction
               http://www.linux-kheops.com/doc/man/manfr/man-html-0.9/man3/getpwuid.3.html
            */
            subInput.close();
            process.clear();
        }
    }
}

Créeons notre fenêtre

Pour commencer, il nous faut la structure de notre fenêtre (image plus haut dans l'article), afin de créer un header qui contiendra chaque élément de la GUI, on créer donc un nouveau fichier : procWindow.hpp Voici sa structure de base :

#ifndef PROCESSWIN
#define PROCESSWIN
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <map>
#include <sys/types.h>
#include <signal.h> 
#include <dirent.h>
#include <pwd.h>
#include <gtkmm.h> 

class ProcessWindow : public Gtk::Window {
    public:


    protected:

};
#endif

On retrouve dans les inclusions les mêmes que tout à l'heure, qui servent à lister les processus, ainsi que le <gtkmm.h> qui est nécessaire à la compilation du projet ! On créer également une classe, nommée ProcessWindow héritant de Gek::Window, vous l'aurez compris, cette classe sera notre fenêtre qui contiendra toutes les fonctions dont nous avons besoin !

Attention : A partir de maintenant, dès lors que je ferai référence au header c'est de ce fichier .hpp dont je parlerai !

Il nous faut maintenant remplir cette classe, histoire de lui faire faire des trucs ! En "public", on a:

public:
        ProcessWindow();
        virtual ~ProcessWindow();
        void refreshProcessList();

Un constructeur par défaut, un déstructeur, et une methode, qui comme son nom l'indique, sert à réactualiser la liste de processus. Maintenant, nous devons mettre en "protected", tous ce qui nous servira dans l'interface, buttons, appel, fonction, sous-classes, en gros, pas mal de chose, et on se retrouve donc avec quelque chose comme:

protected:
        void processKill();
        void processReload();
        class ModelColumns : public Gtk::TreeModel::ColumnRecord{
            public:
                ModelColumns(){
                    add(m_col_pid);
                    add(m_col_name);
                    add(m_col_owner);
                }
                Gtk::TreeModelColumn<unsigned int> m_col_pid;
                Gtk::TreeModelColumn<Glib::ustring> m_col_name;
                Gtk::TreeModelColumn<Glib::ustring> m_col_owner;
        };
        ModelColumns m_Columns;
        Gtk::VBox m_VBox;
        Gtk::HBox m_HBox;
        Gtk::ScrolledWindow m_ScrolledWindow;
        Gtk::TreeView m_TreeView;
        Glib::RefPtr<Gtk::ListStore> m_refTreeModel;
        Gtk::HButtonBox m_ButtonBox;
        Gtk::Button m_Process_Kill;
        Gtk::Button m_Process_Reload;
        Gtk::Label killValue;
        int lastKill;

Je trouve les fonctions assez explicites, une pour tuer un processus, l'autre pour recharger la liste. La classe ModelColumns qui hérite de Gtk::TreeModel nous permet de personnaliser l'affichage de la liste, en y spécifiant le nombre de colonnes, le type de donnée ainsi que leur nom ou valeur. Dans notre constructeur ModelColumns() on ajoute donc trois colonnes, une de type integer qui affichera le PID du processus, et deux de type Glib:ustring (qui sont l'équivalent de std::string utilisées par GTKmm) pour le nom du processus et de son propriétaire.

Pour ce qui est des autres déclarations, je vous laisse vous referez à la documentation, mais on crée un conteneur vertical, un horizontal, ainsi qu'un permettant de scroller, deux boutons, un label pour afficher du texte, ne vous inquietez pas, je reviendrai sur chaque conteneur lorsque que nous allons écrire le fichier de notre classe, ce que nous allons faire de suite.

Remplir notre classe

Maitenant que nous avons notre header, il nous faut implementer toutes ces (nombreuses...) fonctionnalitées, pour cela, on crée un fichier "procWindow.cpp" que nous allons nous empresser de remplir, en commencant par le constructeur et le destructeur de notre classe, nous n'avons besoin que d'inclure un fichier, le header crée auparavant.

#include "procWindow.hpp" 

/* Constructeur de la classe */
ProcessWindow::ProcessWindow(): m_Process_Kill("Kill"), m_Process_Reload("Reload") {
    lastKill = -1;
    set_title("Process Viewer");
    set_border_width(5);
    set_default_size(400,200);

    add(m_VBox);
    m_ScrolledWindow.add(m_TreeView);
    m_ScrolledWindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
    m_VBox.pack_start(m_ScrolledWindow);
    m_VBox.pack_start(m_HBox,  false, false);
    m_HBox.pack_start(m_ButtonBox, Gtk::PACK_SHRINK);

    m_ButtonBox.pack_start(m_Process_Kill, Gtk::PACK_SHRINK);
    m_ButtonBox.set_border_width(5);
    m_ButtonBox.set_layout(Gtk::BUTTONBOX_END);

    m_ButtonBox.pack_start(m_Process_Reload, Gtk::PACK_SHRINK);
    m_ButtonBox.set_border_width(5);
    m_ButtonBox.set_layout(Gtk::BUTTONBOX_END);

    m_HBox.pack_start(killValue);

    m_Process_Kill.signal_clicked().connect(sigc::mem_fun(*this,&ProcessWindow::processKill));
    m_Process_Reload.signal_clicked().connect(sigc::mem_fun(*this, &ProcessWindow::processReload));
    m_refTreeModel = Gtk::ListStore::create(m_Columns);
    m_TreeView.set_model(m_refTreeModel);


    m_TreeView.append_column("Pid", m_Columns.m_col_pid);
    m_TreeView.append_column("Name", m_Columns.m_col_name);
    m_TreeView.append_column("Owner", m_Columns.m_col_owner);

    for(guint i = 0; i< 2;i++){
        Gtk::TreeView::Column *pColumn = m_TreeView.get_column(i);
        pColumn->set_reorderable();
    }

    show_all_children();
}

/* Destructeur */
ProcessWindow::~ProcessWindow(){

}

Voyons en détail ce que fait notre constructeur, et pourquoi il le fait :

  • mProcessKill("Kill"), mProcessReload("Reload") : On assigne juste un texte à nos deux boutons
  • set_* : le nom des fonctions semble relativement évident, on assigne une taille, un titre, une bordure, pour plus de détails, et de paramètres, la doc est là pour ca !
  • add(mVBox)_ : notre conteneur principal à l'intérieur de cette fenêtre est une VBox, c'est a dire que l'organisation se fait sous forme de ligne
  • mScrolledWindow.add(mTreeView) : notre second conteneur est une fenêtre de scroll, qui nous permettra de nous deplacer dans la liste des processus, on ajoute donc le "m_TreeView" à ce conteneur.
  • mScrolledWindow.setpolicy(Gtk::POLICYAUTOMATIC, Gtk::POLICYAUTOMATIC) : bien qu'anodine, cette fonction a toute son importance pour l'ergonomie de l'interface, on lui précise d'adapter la taille du scrolling à la taille de la fenêtre, mais également au nombre d'éléments présents
  • mVBox.packstart : la première ligne de notre m_Vbox contient donc notre barre de scrolling (et tout ce qui va avec), la seconde contient un conteneur de type horizontale qui lui même contient les boutons dont nous aurons besoin (je sais ce n'est pas facile à vous imaginez, mais regardez le premier schema lorsque vous avez un doute !
  • mProcessKill.signalclicked().connect(sigc::memfun(*this,&ProcessWindow::processKill)) : voici la partie la plus puissante de toute interface graphique, les "callbacks", en effet, un bouton ne sert pas à grand chose s'il ne fait rien quand on clique dessus, dans notre cas, on connecte chaque bouton, sur l'action clique ("clicked()") à une méthode de notre classe (kill ou reload)
  • mrefTreeModel = Gtk::ListStore::create(mColumns): on crée notre liste d'élément, en prenant comme patron la "sous-classe" que nous avons défini dans le header, et on ajuste la GUI sur ca (append_column), puis dans une boucle, on autorise le redimensionnement de chaque colonne.
  • FInalement, showallchildren() permet d'affiche la fenêtre et ce qu'elle contient !

Ayant eu pitié de vous (surtout ceux qui ont réussi à me lire jusqu'ici), j'ai rapidement réalisé un petit schema très rapide du positionnement : layout

Légende:

  • En rouge : m_VBox, conteneur principal, vertical
  • En vert : m_ScrolledWindow, permet de gérer le défilement
  • En bleu : m_TreeView, notre conteneur, avec ses colones
  • En jaune : m_HBox, pour contenir nos boutons et un label de texte !
  • En rose : m_ButtonBox, pour nos boutons

Nous avons fait le plus dur, il ne nous reste plus qu'a re-implementer le code du premier article en l'intégrant avec les callbacks, ce qui nous donne pour la fonction "refreshProcessList":

void ProcessWindow::refreshProcessList(){
    m_refTreeModel->clear();
    std::ifstream input, subInput;
    std::stringstream fileName;
    std::string buffer, name, value;
    std::map<std::string,std::string> process;
    DIR *dir;
    struct dirent *subDir;
    Gtk::TreeModel::Row row;
    dir = opendir("/proc/");
    while(subDir = readdir(dir)){
        if(strToInt(subDir->d_name) && strToInt(subDir->d_name) != lastKill){
            row = * (m_refTreeModel->append());
            fileName.str("");
            fileName << "/proc/" << subDir->d_name << "/status";
            subInput.open(fileName.str().c_str(), std::ios::in);
            while(!subInput.eof()){
                std::stringstream bufferString;
                getline(subInput,buffer);
                bufferString << buffer;
                bufferString >> name >> value;
                process.insert(std::pair<std::string,std::string>(name,value));
            }
            row[m_Columns.m_col_pid] = strToInt(process["Pid:"]);
            row[m_Columns.m_col_name] = process["Name:"];
            row[m_Columns.m_col_owner] = getpwuid(strToInt(process["Uid:"]))->pw_name;

            subInput.close();
            process.clear();
        }
    }
}

Le code principal ne change pas, on y ajoute juste des appels aux fonctions GTKmm afin d'afficher les résultats :

  • refTreeModel->clear() : vide la liste, afin de pouvoir le re-créer
  • row = * (m_refTreeModel->append()) : ajout d'une nouvelle ligne, pour chaque processus, que nous devons remplir
  • row[m_Columns.m_col_*] = *: une rempli la structure de la ligne avec les données récupérées dans le fichier "status"

De même avec la fonction pour tuer un processus :

void ProcessWindow::processKill(){
    Gtk::TreeModel::iterator iter = m_TreeView.get_selection()->get_selected();

    if(iter){
        Gtk::TreeModel::Row item = *iter;
        if(kill((pid_t)item[m_Columns.m_col_pid],1)== 0)
            killValue.set_text("Process Succesfully killed");
            lastKill = item[m_Columns.m_col_pid]; // Contient le dernier PID tué 
        refreshProcessList();
    }
}

Pour le coup, cette fonction est entièrement nouvelle par rapport à l'ancien code, étant donnée qu'elle se base sur des interactions avec GTKmm. On récupère la ligne séléctionnée dans la liste, et on fait appel à la fonction kill, qui prend en paramètre le PID du processus.

Vous avez peut-être remarqué que depuis le début, je trimballe une variable lastKill, dont je n'ai jamais parlé, et bien voici l'explication: Je me suis rendu compte, après avoir tué un processus, que ce-dernier ne disparaissait pas de la liste, même après avoir rafraichie cette dernière, le fait est qu'entre l'appel au kill() et la suppression du PID dans le /proc, il y a un delai, qui est trop grand et donc la fonction de rechargement ne prend pas en compte cette modification, j'ai donc triché, en stockant le dernier PID tué, afin de ne pas l'afficher dans la liste lors du rechargement.

Il ne nous manque plus que cette fameuse fonction qui appelle le rechargement, ainsi nous pouvons recharger très efficacement notre liste \o/ :

    void ProcessWindow::processReload(){
        refreshProcessList();
    }

Ah oui ! Dans le header, après la déclaration de la classe (donc après la "}" de la classe), il faut rajouter une petite méthode :

int strToInt(std::string value);

Cette fonction est utilisée lors de la récupération du PID depuis le fichier "/status" afin de le convertir en integer et de le stocker dans une ligne !

int strToInt(std::string value){
    std::istringstream temp(value);
    int returned;

    temp >> returned;
    return returned;
}

Notre classe est maintenant finie, mais il manque tout de même quelque chose, un main:

#include <gtkmm/main.h>
#include "procWindow.hpp"

using namespace std;

 // g++ main.cpp procWindow.cpp `pkg-config gtkmm-3.0 --cflags --libs`  

int main(int argc, char* argv[]){
        Gtk::Main kit(argc, argv);
        ProcessWindow window;
        window.refreshProcessList();
        Gtk::Main::run(window);
        return 0;
}

Pour résumer, voici les étapes de la réalisation d'une interface graphique avec GTKmm :

  • Création d'une classe qui hérite d'une Gtk::Window
  • Positionnement des éléments à l'intérieur (dans le constructeur), grâce aux conteneurs de GTKmm (scroll, vertical, horizontal et de nombreux autres)
  • Création des fonctions de callback sur les boutons de notre interface (ou les évènements, il ne s'agit pas forcement d'un clique sur un bouton)
  • Création d'un main afin d'instancier notre classe et de l'afficher

Projet complet en téléchargement : ici !

En conclusion, même si je ne suis pas un adepte de GUI, je dois dire que j'ai trouvé GTKmm très simple d'utilisation et très naturel à utiliser, on s'y habitue rapidement et on arrive à faire des interfaces simplistes sympathiques.