IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Le widget Gtk::DrawingArea de la bibliothèque Gtkmm

Création et manipulation d’objets graphiques dans ce widget

La bibliothèque Gtk propose des composants « widgets » pour créer des interfaces graphiques au cours de la construction d'un logiciel.

Dans son sillage, la bibliothèque Gtkmm, basée sur le concept de la programmation objet, nous propose des classes pour utiliser Gtk dans un environnement objet.

L'article qui suit s’appuie sur l'exemple d'un logiciel simple réalisé en deux étapes destiné à l'usage des bibliothèques gtkmm 4.8.0 et cairo 1.16.0, dont le but est de créer une zone Gtk::DrawingArea dans laquelle nous afficherons des objets graphiques que nous pourrons déplacer, supprimer ou grouper, à l'aide de la sélection et du déplacement de la souris.

Vous devrez maîtriser un minimum du langage C++ et de la bibliothèque Gtkmm afin d'assimiler le contenu de ce cours. 2 commentaires Donner une note à l´article (5)

Article lu   fois.

L'auteur

Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Récupérer les fichiers sources

Le dépôt public GitHub de cet article contient deux projets représentant chacun une étape du développement du logiciel de travaux pratiques que vous pouvez télécharger en cliquant sur « code » (bouton vert) et choisissez « download ZIP ».

Vous trouverez les liens sur les différents tutoriels developpez.net concernant Gtk et autres bibliothèques en fin de document.

Si vous utilisez Geany, chaque répertoire du projet contient un fichier de configuration objectactif_.geany prévu pour compiler avec CMake 3.25, précisez le chemin de votre projet dans les variables « base_path= » et « last_dir= » où se trouve ce fichier objectactif_.geany dans ce fichier avant de l’utiliser.

II. Petit rappel

Une interface graphique permet la communication entre l'utilisateur et le logiciel greffé à cette interface. Elle comprend généralement des boutons, des zones de saisie de donnée et, parfois, une zone d'affichage graphique. Cette dernière nous amène à ce cours pour améliorer son utilisation qui, habituellement, ne consiste qu'en un affichage fixe d'éléments graphiques, mais que nous allons commuer en objets graphiques mobiles. Il n'est pas à l'ordre de cet article d'exposer de l'animation, mais simplement de « bouger » des éléments graphiques à l'aide du marqueur de la souris.

II-A. Classes usuelles

La structure de l'interface Gtk et les bibliothèques que nous utiliserons pour dessiner nos objets : Image non disponible

Le concept de cet interfaçage est basé sur l'imbrication de conteneurs et d'éléments de saisie : dans la mesure où nous n'imposons pas de dimensions, ces derniers se disposent et occupent la surface de l'écran qui leur est nécessaire. Cette propriété confère à l’interface un espace visuel dont la géométrie s'adapte aisément au graphisme de la langue du texte.

II-A-1. Box

Cette classe est un conteneur. Son objectif est de grouper les éléments qui lui sont associés pour les disposer dans les meilleures conditions possibles. Dans le schéma ci-dessus, le Gtk::Box total regroupe tous les objets de l’application, dont le Gtk::Box « zone bouton », contenant lui-même le Gtk::Box « bouton centre ».

II-A-2. DrawingArea

Pour utiliser cette classe, nous créerons une classe enfant AireDeDessin qui sera son héritière. Cette classe supportera la gestion des signaux de la souris lorsque celle-ci circulera dans sa surface :

  • GestureClick pour gérer l’action sur les boutons ;
  • EventControllerMotion pour gérer le déplacement de la souris durant l’appui sur un bouton.

Elle sera aussi chargée, par l’entremise de la bibliothèque Cairomm, d’afficher les graphiques que nous invoquerons.

À noter : les graphes attachés à un objet de la classe DrawingArea n’ont pas de limites positionnelles, excepté la limite du type numérique de stockage. Ces dessins ne seront visibles que dans la surface accordée par l’interface à l’objet de DrawingArea.

II-A-3. Cairomm

Nous utiliserons la bibliothèque 2D Cairomm pour représenter les objets dans la zone graphique. Elle est issue de la bibliothèque Cairo pour être utilisée dans l’environnement objet C++. Nous la solliciterons via Gtk::DrawingArea pour définir le contexte d’affichage des graphiques.

II-A-4. Make_managed

La classe Gtk::make_managed a été créée pour gérer la mémoire de façon « propre » lors de la sortie d’une application utilisant Gtk.

Gtk::Button *ptrbouton = Gtk::make_managed<Gtk::Button>("bouton");

Le pointeur initialisé avec cette méthode sera effacé sans que le programmeur n’ait à utiliser delete pour libérer son occupation mémoire ni new pour sa création. Cette méthode, à l’intérieur d’une fonction, ne permet pas d’utiliser ptrbouton hors de celle-ci. Il est alors plus judicieux de créer dans le fichier d’entête l’instance du bouton Gtk::Button *ptrbouton; et de préférence en private. Puis dans le constructeur du fichier source qui en dépend, nous initialisons le pointeur à nullptr. ptrbouton = nullptr;. Dans la description de l’interface, l’instanciation sera alors ptrbouton = Gtk::make_managed<Gtk::Button>("bouton");accessible dans toutes les méthodes de la classe, et bénéficiant de la libération sécurisée due à make_managed.

II-B. Signaux

Un bouton est, en général, connecté à une fonction, dans le cas suivant la fonction se situe dans la méthode d’appel d’où le membre *this.

ptrbouton->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &Fenetre::Fonction)));

Mais nous pouvons aussi connecter le signal à la fonction d’une autre classe :

bcarre->signal_clicked().connect(sigc::bind(sigc::mem_fun(*zonegraphique, &AireDeDessin::SymboleCarre)));

… sous réserve que la méthode SymboleCarre se trouve en zone publique. Nous n’utiliserons pas le cas qui suit, mais nous devons savoir qu’il est possible de transmettre des paramètres avec une connexion :

Ptrbouton->signal_clicked().connect(sigc::bind(sigc::mem_fun( *this, &Fenetre::Fonction ), "chaine", 2 ));

sur la méthode :

void Fenetre::Fonction( std::string valstr, int valint ){...}.

II-C. Style des objets de l’interface

Pour définir le style des widgets, nous utiliserons la classe Gtk::StyleContext en joignant un fichier CSS. Pour affecter à un objet des propriétés de style, nous devons au préalable lui attribuer un nom de repérage :

 
Sélectionnez
zonegraphique = Gtk::make_managed<AireDeDessin>();
//...
    zonegraphique->set_name("zonegraphe");
//...

où le libellé zonegraphe définit le repère dans le fichier objetactif01.css qui décrit le style :

 
Sélectionnez
# zonegraphe {
background - color : rgb (60 ,75 ,80) ;
border:              solid white 1 px ;
border-radius:       8 px ;
}

À noter : les libellés des directives de CSS ne sont pas toutes incluses dans Gtk. Il est préférable de faire des tests avant de les employer.

Étant donné la diversité d’affectation de ce principe de style, voir la documentation.

III. Première étape

Pour cette étape, nous travaillerons avec le projet objetactif01 que nous avons téléchargé.

III-A. Fenêtre de l’application

Pour comprendre le fonctionnement nous construirons une simple fenêtre équipée de boutons dont les actions seront de créer des dessins 2D représentant trois genres de formes rectangle, carré et rond que nous pourrons ajouter, déplacer et effacer à volonté en maniant la souris, et d’une surface destinée à l’affichage de ces objets graphiques.

III-A-1. Classe Fenetre

Comme cette application n’est utilisée que comme support pour un apprentissage, la fenêtre de l’application sera réduite au plus simple et nous éviterons les menus déroulants standards. Le visuel montre :

  • sur le dessus, une zone de boutons pour les commandes générales ;
  • en dessous, une zone destinée à recevoir les dessins graphiques.

La classe Fenetre contient les objets :

  • Gtk::DrawingArea, zone graphique où se dessineront les objets graphiques ;
  • des boutons :

    • les boutons "carré", "rectangle" et "rond", destinés à créer les objets graphiques,
    • le bouton "grouper" qui rassemble tous les objets au centre de la zone graphique,
    • le bouton "supprimer" pour effacer un des éléments de la zone.

Dans la philosophie de la bibliothèque graphique Gtk, la structure de la composition de la fenêtre contenant les graphiques de commandes est étudiée pour répondre à la diversité des langues. Elle est basée sur l’assemblage de conteneurs dont les dimensions et positions seront définies, entre autres choses, suivant la langue utilisée.

III-A-1-a. Remarques sur le fichier fenetre.h de la classe Fenetre

Avec Gtkmm, la configuration de certains éléments graphiques sera donnée par un fichier CSS :

 
Sélectionnez
protected:
             Glib::RefPtr<Gtk::CssProvider> styledecor;

Nous devons alors créer une instance de l’objet chargé de cette tâche.

Le pointeur de l’instance d’objet de la classe AireDeDessin, héritière de la classe DrawingArea, est déclaré à ce niveau pour qu’il persiste jusqu’à l’arrêt du logiciel. Il sera supprimé par le destructeur de l’instance.

 
Sélectionnez
private: 
       AireDeDessin *zonegraphique;

III-A-2. Description du fichier source : fenetre

Dans le constructeur, l’instruction set_default_size(x,y); donne les dimensions minimales de la fenêtre. Ce paramétrage n’est pas obligatoire puisque les dimensions finales dépendent du calcul par Gtk de la dimension de la fenêtre suivant les éléments la constituant. Par contre, il sera nécessaire d’initialiser les dimensions de la zone graphique par des valeurs minimales de façon à obtenir, au démarrage, un espace reconnu.

La plus grande partie du constructeur contient la définition des boutons et de la zone graphique de la fenêtre de l’application. Ces créations sont déjà expliquées dans des articles bien documentés.

Le plus important pour nous est d’étudier la façon de contrôler cette zone avec la souris pour :

  • « cliquer/maintenir » un objet et le déplacer ;
  • « sélectionner » un objet puis le supprimer.

Pour ce faire, nous utiliserons la classe GestureClick afin de créer une première instance destinée au déplacement de l’objet.

L’instruction auto controle = Gtk::GestureClick::create(); crée une instance de contrôle que nous déterminons à « capturer » un objet controle->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); et nous lui affectons la touche gauche de la souris controle->set_button(1); pour activer deux actions qui nous connecteront chacune à une méthode :

La première action consiste à signaler que la touche est appuyée en continu :

controle->signal_pressed().connect(sigc::mem_fun(*this, &Fenetre::AppuyerSouris));

et la seconde consiste à prévenir si la touche est relâchée :

controle->signal_released().connect(sigc::mem_fun(*this, &Fenetre::LacherSouris),true);

La première instance est réalisée, passons à la deuxième, qui nous permettra de sélectionner un objet :

 
Sélectionnez
auto controledroit = Gtk::GestureClick::create();
controledroit->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
controledroit->set_button(3);
controledroit->signal_pressed().connect(
                   sigc::mem_fun(*this, &Fenetre::ChoisirObjet));

Cette salve d’instructions est identique à la première instance sauf le numéro du bouton pour définir le clic droit et que la connexion vers signal_released() est inutile.

Grâce à l’instance controle, nous pouvons sélectionner l’objet, puis le fixer en place au relâchement du bouton. Dans les faits, nous ne pourrions pas suivre le déplacement de l’objet, il resterait à sa position initiale jusqu’à ce que nous relâchions le bouton, en ce cas il se dessinerait à cette nouvelle position. Pour visualiser ce déplacement, il nous faut créer un contrôle du mouvement de la souris, ce qui est possible avec la classe EventControllerMotion, dont l’instanciation du contrôle et l’instruction pour la capture du signal sont simples :

 
Sélectionnez
auto controlebouge = Gtk::EventControllerMotion::create();
controlebouge->signal_motion().connect(sigc::mem_fun(*this, &Fenetre::SourisBouge));

Dans la conception Gtkmm, il nous faut maintenant relier ces trois contrôles à l’objet auquel ils sont destinés :

 
Sélectionnez
zonegraphique->add_controller(controle);
zonegraphique->add_controller(controledroit);
zonegraphique->add_controller(controlebouge);

À la fin du constructeur, nous pouvons placer les lignes pour connecter le fichier css aux objets :

 
Sélectionnez
styledecor = Gtk::CssProvider::create();
Gtk::StyleContext::add_provider_for_display(
     get_display(),
     styledecor,
     GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
m_styledecor->load_from_path("objetactif01.css");

À noter : le fichier CSS est de plus en plus utilisé pour les propriétés de formes et couleurs, de même que la définition des zones de l’interface peuvent être générée par l’intermédiaire de leur description dans un fichier type XML.

III-B. Fonctions de connexions

Les fonctions virtuelles de connexion vues dans la section précédente renvoient des paramètres spécifiques obligatoires à instaurer dans les déclarations :

 
Sélectionnez
auto controle = Gtk::GestureClick::create();
   controle->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
   controle->set_button(1);
…
   controle->signal_pressed().connect(sigc::mem_fun(*this,
                                                   &Fenetre::AppuyerSouris));

La dernière instruction ci-dessus doit être rattachée à une fonction déclarée comme void Fenetre::AppuyerSouris( int n_press, double x, double y ) et, contrairement aux fonctions appelées par les objets Gtk::Button où nous sommes les décideurs en ce qui concerne les paramètres à traiter, leur appel ne demande pas d’arguments, ici signal_pressed transmet les valeurs en interne.

Comme exemple, ci-dessous signal_clicked se réfère à une fonction avec un paramètre que nous avons établi :

 
Sélectionnez
ptrbouton→signal_clicked().connect(sigc::bind(sigc::mem_fun( *this,
                                    &Fenetre::Fonction_bouton), j+i ));
zonegraphique→add_controller(controlebouge);

//dans la déclaration :

void Fenetre::Fonction_bouton( int cas )

Analysons les méthodes appelées par les fonctions de connexions de notre application test :

  • AppuyerSouris : cette fonction est appelée quand le bouton de la souris est appuyé et maintenu, mais uniquement lorsque celle-ci se situe dans la zone graphique. Si c’est le cas, elle envoie l’instruction zonegraphique->TesterObjet(x,y); pour déterminer si le marqueur se trouve dans un objet ;
  • LacherSouris : durant le déplacement d’un objet, dès que le bouton est relâché l’opération en cours s’arrête et l’objet est libéré. Dans le cas où nous avons déplacé l’objet hors de la zone graphique, il disparaît mais continue à être déplacé, c’est-à-dire que les coordonnées récupérées par la souris sont toujours envoyées à l’objet. Cependant, au relâchement, l’objet ne sera existant mais invisible, d’où le bouton grouper pour replacer tous les objets au centre de la zone graphique.
  • SourisBouge : en permanence, le contrôle de la zone graphique teste si la souris se trouve dans son aire et quelles y sont ses activités. La détection du passage de la souris émet une position, à cet instant et si le bouton gauche est en appui constant au cas où un objet se trouve sous le marqueur, la position de cet objet est mise à jour avec la fonction ActualiserPositionElement(x,y), et ce, jusqu’à ce que le bouton soit relâché. Cette fonction nous permettra de suivre à vue le déplacement de l’objet.
  • ChoisirObjet : activée par une pression sur le bouton droit controledroit->signal_pressed(), cette fonction stocke l’identité de l’objet sous le marqueur dans la mémoire pour, le cas échéant, le supprimer.

Nous remarquons que toutes ces fonctions font appel à l’objet zonegraphique, si bien que nous

aurions pu, dans l’instruction du signal, indiquer :

 
Sélectionnez
... sigc::mem_fun(*zonegraphique, &AireDeDessin::AppuyerSouris));
// au lieu de :
... sigc::mem_fun(*this, &Fenetre::AppuyerSouris));

en plaçant la fonction en public dans la Classe AireDeDessin, mais nous sommes dans un objectif de démonstration et non pas d’efficacité.

III-C. La gestion de la zone graphique

Le centre de commande est la classe AireDeDessin par laquelle toutes les commandes du graphisme seront traitées. Elle stocke en private le pointeur des formes à dessiner dans un conteneur extensible : std::vector<ElementGraphique*> lstgraphe; qui sera traité par la fonction Dessiner. Dès qu’il y a un changement dans la zone graphique, chaque élément de cette table est ReDessiner comme le décrit le schéma de la séquence de réaffichage :

Image non disponible

Les occurrences des objets à dessiner sont contenues dans la table lstgraphe. Chaque appel à AireDeDessin::Dessiner déclenche la boucle qui exécutera chacune d’elles pour la redessiner.

À Noter : le contexte cairo est récupéré par Airedessin::Dessiner et retransmis à ElementGraphique::ReDessiner pour accéder aux instructions de dessins de la classe Cairomm.

III-C-1. Contexte Cairo

Cette classe fournit les instructions nécessaires pour dessiner en 2D dans la zone de dessin. Dans le constructeur de la classe AireDeDessin, un premier appel pour initialiser la zone de graphique définit ce contexte :

 
Sélectionnez
AireDeDessin::AireDeDessin()
...
    set_draw_func(sigc::mem_fun(*this, &AireDeDessin::Dessiner));
...

L’utilisation de set_draw_func implique de prévoir pour la déclaration de la fonction AireDeDessin::Dessiner, des paramètres comme indiqué ci-dessous :

void AireDeDessin::Dessiner(const Cairo::RefPtr<Cairo::Context>& cr,int width, int height)

Ctte fonction retourne le contexte par le paramètre cr et les limites de la zone actuelle de dessin. Ces limites dépendent des actions exercées par l’utilisateur sur la fenêtre de l’application, dans notre cas : agrandir ou rétrécir la fenêtre.

La récupération du pointeur cr, nous autorise à créer le dessin dans la zone :

  • nous créons un rectangle suivant les valeurs cr->rectangle(xg,yh,xd-xg,yb-yh); ;
  • et nous remplissons ce rectangle avec une couleur cr->fill();.

À noter : consulter la documentation pour découvrir toutes les autres instructions qui permettent la création des dessins.

III-C-2. Éléments graphiques

Dans le but d’afficher des objets graphiques indépendants dans une zone et de pouvoir en sélectionner un pour le traiter, il faut que chacun de ces objets ait ses propres instructions de représentation et que la classe chargée de redessiner les graphiques, ici, AireDeDessin héritière de Gtk::DrawingArea, collecte ces données pour les afficher dans sa zone. N’oublions pas que, suivant le principe de Gtk::DrawingArea après avoir affiché les objets une première fois, à chaque modification de son contenu ou de ses propres propriétés, elle redessine toute sa zone.

Pour mettre en place cette solution, nous créons des classes d’éléments graphiques qui seront héritières d’une classe de base appelée ElementGraphique contenant des valeurs de base communes à chaque objet.

Image non disponible

III-C-2-a. Classe ElementGraphique

Elle sera la classe de base de nos objets graphiques, elle contient les valeurs situant leur position et utilisées par tous les objets instanciés inclus dans la zone de dessin :

 
Sélectionnez
protected:
...
    double xg,xd,yh,yb;
    double xctr,yctr;
...

La première ligne stocke le rectangle dans lequel est inscrit la forme, la deuxième, le centre de ce rectangle. Cette façon de stocker la position permet d’accélérer la recherche pour la méthode ContientPoint.

Dans l’entête de cette classe au niveau public, nous écrivons :

void ReDessiner(const Cairo::RefPtr<Cairo::Context>& cr);. Cette instruction sera appelée à chaque modification impliquant la zone de dessin, pour redessiner l’objet de la classe parente. Le paramètre passé par cette commande est le pointeur sur le contexte de dessin Cairomm. Pour que ce paramètre soit disponible aux classes enfants, nous le stockons en protected et déclarons la variable Cairo::RefPtr<Cairo::Context> tmpcr; de manière à ce que tous les enfants et petits enfants de cette classe puissent y accéder et y dessiner.

Dans le fichier source, étudions la méthode ReDessiner :

 
Sélectionnez
void ElementGraphique::ReDessiner( const Cairo::RefPtr<Cairo::Context>& cr)
{
   tmpcr = cr;
   Dessiner();
}

Elle stocke la valeur du pointeur du contexte, puis appelle la fonction Dessiner().

Dessiner est une méthode virtuelle déclarée en protected qui contient des instructions qui ne seront jamais effectuées pendant une utilisation usuelle de l’application, puisque c’est la méthode enfant qui sera exécutée, mais celle-ci pourraient être nécessaire lors de tests en développement.

Arrêtons-nous un instant sur la fonction ContientPoint, elle sera chargée de détecter si le pointeur de la souris se trouve dans la zone de la forme :

 
Sélectionnez
bool ElementGraphique::ContientPoint( const double x, const double y )
{
   return (
           ((x>xg)&&(x<xd))
            &&
           ((y>yh)&&(y<yb))
          );
}

Les valeurs tests mini/maxi étant mémorisées, l’instruction est simple. Toutefois, pour d’autres formes, comme le cercle, la détection est plus complexe. Cette fonction est donc virtualisée pour permettre d’autres méthodes de contrôle dans d’autres implémentations que le rectangle.

III-C-2-b. Classe Rectangle

Elle hérite de ElementGraphique et son constructeur initialise les valeurs de la forme :

 
Sélectionnez
Rectangle::Rectangle( const double xc, const double yc,
                      const double largeur, const double hauteur)
{
   xctr = xc;
   yctr = yc;
   xg = xc-largeur/2.0;
   xd = xc+largeur/2.0;
   yh = yc-hauteur/2.0;
   yb = yc+hauteur/2.0;
}

Le constructeur calcule, suivant les paramètres donnés, l’encombrement suivant un rectangle de la forme de l’objet, et stocke le centre.

Et la méthode de dessin est simple :

 
Sélectionnez
void Rectangle::Dessiner()
{
   tmpcr->rectangle(xg,yh,xd-xg,yb-yh);
   tmpcr->fill();
}

Comme écrit plus haut c’est une fonction virtuelle qui va chercher le paramètre de contexte Cairo : tmpcr dans la classe parente.

III-C-2-c. Classe Carre

Elle est une enfant de rectangle et ne connaît que des arêtes égales :

 
Sélectionnez
Carre::Carre( const double xc, const double yc,const double valcote)
{
   xctr = xc;
   yctr = yc;
   double demicote = valcote/2.0;
   xg = xc-demicote;
   xd = xc+demicote;
   yh = yc-demicote;
   yb = yc+demicote;
}

La mise en mémoire des valeurs est identique à sa classe parente.

C’est la méthode virtuelle Dessiner de Rectangle qui sera utilisée pour tracer le dessin de cette

forme.

III-C-2-d. Classe Rond

Dans le fichier de déclaration en private, nous ajoutons double rx; surtout pour des raisons de confort pour le traçage de la forme.

L’appel au constructeur envoie les paramètres définissant les valeurs pour le dessin.

 
Sélectionnez
Rond::Rond( const double xc, const double yc, const double rayon )
{
    xctr = xc;
    yctr = yc;
    rx = rayon;
    xg = xctr - rx;
    xd = xctr + rx;
    yh = yctr - rx;
    yb = yctr + rx;
}

Quant à la fonction Dessiner, elle appelle la méthode d’origine de Cairomm :

 
Sélectionnez
tmpcr→arc( xctr, yctr, rx, 0.0, 2.0*M_PI );
tmpcr->fill();

À noter : l’utilisation de la valeur macro M_PI qui n’est pas standard dans la norme ISO du C, mais généralement définie dans les compilateurs par #define M_PI 3.14159265358979323846.

Précédemment, nous avons discuté sur la méthode pour détecter si le marqueur de la souris était dans la surface de la forme. Pour un cercle, il ne suffit pas de chercher dans les limites maxi/mini du carré exinscrit au cercle :

Image non disponible

Ci-dessus, une recherche rectangulaire dans le schéma de gauche donnerait positif dans les zones bleues. En ajoutant un calcul simple, nous obtiendrons un résultat exact :

 
Sélectionnez
if(ElementGraphique::ContientPoint( x, y ))
{
   double vx = abs(xctr-x);
   double vy = abs(yctr-y);
//if(vx==0) return (vy<rx);
//if(vy==0) return (vx<rx);
   return (hypot (vx, vy)<rx);
}

Les instructions tests commentées sont inutiles, car il est quasi impossible d’obtenir des valeurs à 0, lors du pointage. Mais, si vous le souhaitez, pour accélérer dans ces cas très rares la détection, vous pouvez dé-commenter. Toutefois, il est préférable de laisser tel quel parce que les cas non nuls devraient systématiquement être testés inutilement et la détection serait ralentie.

L’appel à la méthode parente ElementGraphique::ContientPoint permet de filtrer rapidement les valeurs hors zone, de cette manière les calculs sont évités et font gagner du temps.

III-D. Créer, déplacer et supprimer les objets

À présent, il est temps de détailler le fonctionnement de cette application test.

III-D-1. Classe AireDessin

Dans le constructeur de cette classe, nous définirons les dimensions de la surface graphique. Ce n’est pas une obligation, mais à l’affichage de l’interface nous ne pourrions pas augurer de la taille de cette zone. Comme de coutume en conception objet, nous initialisons les variables déclarées dans le fichier d’entête à des valeurs de base ou nulles. Et, pour finir, l’instruction set_draw_func détermine la fonction qui sera utilisée par DrawingArea pour dessiner dans sa zone.

 
Sélectionnez
AireDeDessin :: AireDeDessin ()
{
   set_content_width( 600 );
   set_content_height( 450 );
   stx = sty = 0.0;
   graphestocke = grapheselectionne = nullptr;
   set_draw_func( sigc::mem_fun(*this, &AireDeDessin::Dessiner ) );
}

Lorsque l’application est arrêtée, tous les destructeurs de chaque classe sont appelés afin de nettoyer la mémoire. Puisque nous avons stocké les objets en dynamique dans cette mémoire, nous devons les supprimer :

 
Sélectionnez
AireDeDessin ::~ AireDeDessin ()
{
   for ( unsigned i=0; i<lstgraphe.size(); i++)
      delete lstgraphe[i];
}

III-D-2. Création du dessin d’un élément

Un clic sur le bouton libellé carré déclenche la connexion bcarre->signal_clicked().connect qui nous envoie à la méthode &AireDeDessin::SymboleCarre, pour initialiser un objet graphique de forme carrée :

Carre *graphtmp = new Carre(30,30,40); crée une occurrence d’objet dynamique de cette classe.

lstgraphe.push_back(graphtmp); stocke le pointeur dans la table.

queue_draw(); cette dernière instruction commande l’actualisation de la surface graphique. Cette fonction de Gtk::DrawingArea appelle, entre autres méthodes, la fonction Dessiner intégrée à la classe par le set_draw_func dans le constructeur de AireDeDessin.

III-D-3. Supprimer un élément

Au préalable, nous devons sélectionner l’élément graphique à supprimer en cliquant dessus avec la touche droite de la souris, nous avons vu plus haut comment, dans la définition de l’interface, configurer l’action sur la touche droite de la souris pour activer la méthode ChoisirObjet. Elle récupère la position du marqueur de la souris dans la surface graphique et les envoie à la fonction chargée de détecter un objet :

 
Sélectionnez
void Fenetre::ChoisirObjet( int n_press, double x, double y )
{
   zonegraphique->SelectionnerObjet(x,y) ;
}

les paramètres « int n_press, double x, double y » sont imposés par la fonction signal_pressed().connect.. La détection d’un objet sous le marqueur s’opère en appelant, pour chaque occurrence d’objet, leur fonction ContientPoint et retourne le premier objet trouvé, sinon nullptr :

 
Sélectionnez
ElementGraphique *AireDeDessin::ChercherAuPoint(const double vx, const double vy)
{
    for( unsigned i=0; i<lstgraphe.size(); i++)
    {
        if(lstgraphe[i]->ContientPoint(vx,vy)) return lstgraphe[i];
    }
    return nullptr;
}

Cette valeur sera affectée au membre grapheselectionne, jusqu’à ce que l’appui sur le bouton libellé supprimer déclenche la méthode :

 
Sélectionnez
void AireDeDessin::SupprimerSelection()
{
    ctmp = std::find (lstgraphe.begin(), lstgraphe.end(), grapheselectionne);
    if(ctmp != lstgraphe.end())
    {
        lstgraphe.erase(ctmp);     // retirer l’occurrence de la table
        delete grapheselectionne;  // libérer la mémoire
        NettoyerSelection();       // remise à zéro des membres
        queue_draw();              // redessiner la zone graphique
    }
}

Attention : l’itérateur ctmp est déclaré en tant que std::vector<ElementGraphique*>::iterator ctmp; dans le fichier entête et l’instruction std::find impose la déclaration #include <algorithm> dans le fichier entête ou le fichier source.

III-D-4. Déplacer un élément

En cliquant/maintenant la touche gauche de la souris sur un objet de la zone graphique, nous provoquons la mise en œuvre de AppuyerSouris. Cette méthode, si elle trouve un objet sous le marqueur, initialise le membre graphestocke de AireDeDessin avec le pointeur de l’occurrence dynamique de l’objet, ainsi que la position du marqueur. Ensuite, tant que la touche est maintenue, le moindre mouvement de la souris est capté par le contrôle Controlebouge = Gtk::EventControllerMotion défini lors de la création de l’interface, et grâce au signal signal_motion() active la méthode :

 
Sélectionnez
void Fenetre::SourisBouge( double x, double y )
{
    if(zonegraphique->ElementStocke())
    {
        zonegraphique->ActualiserPositionElement(x,y);
    }
}

Dans laquelle, après la vérification de l’existence d’un objet à mouvoir, nous actualisons sa position :

 
Sélectionnez
void AireDeDessin::ActualiserPositionElement(const double nx, const double ny)
{
    graphestocke->BougerElement((nx-stx),(ny-sty));
    stx=nx;
    sty=ny;
    queue_draw();
}

en mettant à jour les valeurs de placement de l’objet et en le redessinant dans la zone graphique :

 
Sélectionnez
void ElementGraphique::BougerElement( const double dx, const double dy )
{
    xctr += dx;
    yctr += dy;
    xg += dx;
    xd += dx;
    yh += dy;
    yb += dy;
    Dessiner();
}

Comme à chaque modification de ou dans la zone graphique, l’appel à la fonction queue_draw(); rafraîchit la vue.

Tant que la touche gauche est appuyée et la souris déplacée, cette série de méthodes est appliquée, mais dès que nous relâchons la pression l’interface déclenche le signal_released() pour appeler la méthode de libération de l’acquisition de l’objet :

 
Sélectionnez
void Fenetre::LacherSouris(int n_press, double x, double y)
{
    if(zonegraphique->ElementStocke())
        zonegraphique->NettoyerSelection();
}

où l’instruction NettoyerSelection mettra simplement à nullptr le pointeur mémorisé, le dessin ayant été déjà actualisé par le dernier mouvement de la souris.

III-D-5. Grouper

Comme nous l’avons constaté, lors du déplacement d’un objet, si nous ne limitons pas les valeurs de positionnement aux dimensions de la zone visible, nous pouvons nous retrouver avec des objets, « quelque part » dans la zone écran du moniteur sans pouvoir les retrouver. Et comme nous sommes dans une application test, cette situation pourrait exister.

Nous allons donc programmer une commande pour rassembler le troupeau. Son principe sera de replacer tous les éléments au centre de la zone graphique affichée.

 
Sélectionnez
void AireDeDessin::Grouper()
{
    if(lstgraphe.size()>0)
    {
        int tpxc    = get_content_width()/2;
        int tpyc    = get_content_height()/2;
        for (unsigned i=0; i<lstgraphe.size(); i++)
        {
            lstgraphe[i]->PlacerElement(tpxc,tpyc);
        }
        queue_draw();
    }
}

Nous avons terminé cette première étude sur le widget DrawingArea en intégrant des connexions avec le périphérique souris. Dans le chapitre suivant, nous insérerons textes et images et nous ferons un écart vers CMake et l’emploi des ressources.

IV. Deuxième étape

Pour cette étape nous travaillerons avec le projet objetactif02 que nous avons téléchargé.

Ce projet enrichit le graphisme des objets à afficher en ajoutant une image et un texte et nous étudierons comment charger icônes, fichier CSS, texte et binaire à partir d’un fichier de ressources compilé avec le fichier exécutable. Pour notre application-test, c’est surfait, mais cette digression nous permettra d’appréhender le maniement des ressources intégrées au logiciel.

Image non disponible

Les boutons de création des formes sont supprimés pour laisser place à des créations de symboles.

IV-A. Le fichier de ressources

Dans le dossier objetactif02/ressource nous trouverons les fichiers :

  • edit48.png, ftrav48.png et rect48.png qui définissent les icônes à afficher ;
  • donnees.bin et donnees.txt qui contiennent les valeurs de configuration des dessins ;
  • objetactif02.css destiné à la configuration de la fenêtre Gtk ;
  • resobjetactif.xml qui sera traité pour réaliser le fichier source à inclure dans notre exécutable.

Mis à part le fichier donnees.bin, les autres sont éditables soit avec un logiciel de dessin en ce qui concerne les *.png, soit avec un éditeur de texte pour le reste.

Le fichier resobjetactif.xml répertorie les fichiers et la structure de la ressource créée pour être reconnue dans notre logiciel test :

 
Sélectionnez
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
  <gresource prefix="/icone">
    <file>ftrav48.png</file>
    <file>edit48.png</file>
    <file>rect48.png</file>
  </gresource>
  <gresource prefix="/fichier">
    <file>donnees.txt</file>
    <file>objetactif02.css</file>
  </gresource>
  <gresource prefix="/fichbin">
    <file>donnees.bin</file>
  </gresource>
</gresources>

IV-A-1. Compiler les ressources avec CMake

Pour utiliser Cmake, plusieurs cours ont été réalisés sur ce site. Nous ne nous intéresserons donc qu’à la compilation des ressources et l’intégration des fichiers.

Dans le script ci-dessous, la commande find_program recherche l’existence du programme de création du fichier de ressources à inclure, en suivant les indications du fichier XML. En cas d’absence cmake s’interrompt en diffusant un message d’erreur.

Avec l’instruction add_custom_command nous désignons le nom des deux fichiers à créer puis la série des arguments, il y a deux lignes puisque la commande activera glib-compile-resources deux fois. Le paramètre WORKING_DIRECTORY désigne l’endroit où se trouvent tous les fichiers à traiter.

À noter : le fichier résultat « resobjetactif.cpp » DOIT se trouver dans le dossier où sera placé l’exécutable résultant de la compilation.

Pour terminer la compilation, il suffit d’indiquer à la commande add_executable les deux fichiers à intégrer.

Puisqu’à la première analyse, avant de procéder au traitement, CMake remarquerait qu’il n’y a pas les fichiers de ressources .h et .cpp créés, il interromprait son job. Nous éviterons cet inconvénient en indiquant après le add_executable la commande set_source_files_propertie de façon à mettre à true la propriété GENERATED de façon à empêcher l’interruption de la première compilation.

 
Sélectionnez
cmake_minimum_required(VERSION 3.5)
#…
#….
find_program(GLIB_COMPILE_RESOURCES NAMES glib-compile-resources REQUIRED)

set(FICRES_CPP resobjetactif.cpp)
set(FICRES_H resobjetactif.h)
set(FICRES_XML resobjetactif.xml)

add_custom_command(
    OUTPUT ${FICRES_CPP} ${FICRES_H}
    COMMAND ${GLIB_COMPILE_RESOURCES}
    ARGS
        --target=${CMAKE_CURRENT_BINARY_DIR}/${FICRES_CPP}
        --generate-source ${FICRES_XML}
    COMMAND ${GLIB_COMPILE_RESOURCES}
    ARGS
        --target=${CMAKE_CURRENT_SOURCE_DIR}/entete/${FICRES_H}
        --generate-header ${FICRES_XML}
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/ressource
    VERBATIM
    MAIN_DEPENDENCY ressource/${FICRES_XML}
    DEPENDS ressource/objetactif02.css
)
#…
#…
add_executable( objetactif02 ${SOURCE} ${ENTETE} 
                ${CMAKE_CURRENT_BINARY_DIR}/${FICRES_CPP}
                ${CMAKE_CURRENT_SOURCE_DIR}/entete/${FICRES_H} )
#…
#…
set_source_files_properties(
    ${CMAKE_CURRENT_SOURCE_DIR}/entete/${FICRES_H}
    ${CMAKE_CURRENT_BINARY_DIR}/${FICRES_CPP}
    PROPERTIES GENERATED TRUE
)

À noter : le fichier « FICRES_H resobjetactif.h » est inutile pour notre application. Je l’ai placé dans ce CMakeLists comme exemple pour utilisation de la fonction add_custom_command contenant plus d’une commande.

Dans cet exemple, nous devons tenir compte que, contrairement aux autres fichiers entête et source qui ne sont traités que s’ils ont été modifiés, les fichiers ressources cpp et h seraient à chaque fois recréés. Mais, en plaçant la directive MAIN_DEPENDENCY ressource/${FICRES_XML}, nous prescrivons que la commande ne soit exécutée que si ce fichier est modifié, ainsi que DEPENDS ressource/objetactif02.css qui provoquerait aussi cette exécution en cas de modification. Le fait d’utiliser ces directives évite une recompilation systématique de tous les fichiers ressources. L’avantage de DEPENDS est éloquent lors de la mise au point de la décoration de l’interface, puisque les fichiers de construction cpp et h seront ignorés alors que ceux de la ressource modifiée seront compilés et liés au programme ne demandant qu’un temps reconstruction du logiciel insignifiant.

IV-A-2. Compiler les ressources en ligne de commande

Pour utiliser cette technique, il est préférable de se placer dans le dossier de resobjetactif.xml :

 
Sélectionnez
glib-compile-resources --target=objetactifres.cpp --generate-source objetactifres.xml

produit le fichier cpp à placer dans le répertoire de compilation de l’exécutable.

 
Sélectionnez
glib-compile-resources --target=objetactifres.h --generate-header objetactifres.xml

produit le fichier h à placer dans le répertoire des entêtes de compilation de l’exécutable, mais n’oublions pas qu’il nous est inutile, et seule la ligne :

 
Sélectionnez
add_executable( objetactif02 ${SOURCE} ${ENTETE} 
                ${CMAKE_CURRENT_BINARY_DIR}/resobjetactif.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/entete/resobjetactif.h )

sera nécessaire puisque les fichiers seront présents.

Dans le fichier CmakeLists les trois directives set(FICRES… ainsi que add_custom_command et set_source_files_properties deviennent inutiles.

Maintenant que le traitement des ressources de cette étape est éclairci, nous étudierons sa mise en œuvre dans les prochaines sections.

IV-B. Classe Fenetre

Elle est identique à l’étape 1 pour les commandes générales. Quant aux deux boutons Gtk::Button *blogiciel et Gtk::Button *bfichierjoint, leurs connexions suivent le même schéma que pour les anciens boutons de création de formes.

En fin de constructeur, nous appellerons le fichier CSS présent dans notre application en utilisant l’instruction load_from_resource :

 
Sélectionnez
   styledecor = Gtk::CssProvider::create();
   Gtk::StyleContext::add_provider_for_display(
           get_display(),
           styledecor,
           GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
   styledecor->load_from_resource("/fichier/objetactif02.css");

IV-C. Éléments graphiques

Les valeurs des propriétés des éléments sont stockées comme ressources dans les fichiers donnees.bin et donnees.txt et pour les récupérer, nous créons deux structures.

 
Sélectionnez
struct dimsymbole
{
        double hauteur, largeur, xctr, yctr;
};

dimsymbole est une structure contenant quatre valeurs de type double indiquant les dimensions du symbole.

 
Sélectionnez
struct donneesymbole
{
    std::string txtcartouche;
    std::string txtcorps;
    std::string policecar;
    std::string imagecorps;
    int    taillepol;
    tabcoul    couleurpolice;
    tabcoul    couleurfond;
    tabcoul    couleurcartouche;
    dimsymbole    dimension;
};

La structure donneesymbole rassemble toutes les valeurs des symboles et intègre une valeur de type dimsymbole.

Puisque c’est la classe AireDeDessin qui stocke les occurrences des objets affichés sur la zone des graphiques et reçoit l’ordre de l’interface de les créer, nous stockerons les valeurs dans cette classe.

IV-C-1. Classe AireDeDessin

Dans l’entête, nous créons la table où seront stockées les données extraites des ressources et deux méthodes, ce sont les seules modifications significatives en comparaison de objetactif01.

 
Sélectionnez
private:
….
    std::vector<donneesymbole> lstdonnee;
….    
    void FormateCouleur( tabcoul &tabcouleur,
                           const std::string &phrasecouleur );
    void ChargerDonneeSymbole();

Dans le source, le constructeur ne subit qu’un ajout, une instruction appelant la fonction ChargerDonneeSymbole() afin d’extraire les valeurs des ressources pour initialiser la table lstdonnee. Pour lire les fichiers texte ou binaire la technique est identique :

  • créer un flux d’entrée :
 
Sélectionnez
Glib::RefPtr<Gio::InputStream> fichierdonnees =
                  Gio::Resource::open_stream_global("/fichier/donnees.txt");
  • créer un buffer pour stocker les données lues :
 
Sélectionnez
Glib::RefPtr<Gio::DataInputStream> lignedonnee =
                  Gio::DataInputStream::create(fichierdonnees);

Ensuite, il suffit de boucler une lecture de ce flux.

Commençons par lire le fichier texte à l’aide de l’instruction read_line , si la lecture est réussie la fonction renvoie true :

 
Sélectionnez
….    
  donneesymbole dontmp;
  bool oklecture;
  do{
    oklecture = false;
    if(lignedonnee→read_line(dontmp.txtcartouche))
….
           if(lignedonnee->read_line(phrcouleur))
           {
              FormateCouleur( dontmp.couleurcartouche, phrcouleur );
              lstdonnee.push_back(dontmp);
              oklecture = true;
           }
….
  }while(oklecture);

Après cette phase la table et partiellement remplie, et pour la finir nous devons lire le fichier binaire :

 
Sélectionnez
Glib::RefPtr<Gio::InputStream> fichbindonnees =
                   Gio::Resource::open_stream_global("/fichbin/donnees.bin");
Glib::RefPtr<Gio::DataInputStream> enrgdonnee =
                   Gio::DataInputStream::create(fichbindonnees);

   dimsymbole dimdon;
   gsize      nboctet = sizeof(dimsymbole);
   int i=0;
   do{
      oklecture = false;
      if(enrgdonnee->read( &dimdon,nboctet) == nboctet)
      {
         lstdonnee[i].dimension = dimdon;
         i++;
         oklecture = true;
      }
   }while(oklecture);

Mais cette fois, nous utilisons la fonction read qui nécessite de connaître le nombre d’octets à lire, et nous initialisons le reste de la structure avec dimdon.

À noter : bien sûr, il aurait été plus judicieux de réaliser la lecture des deux fichiers dans une même boucle.

IV-C-2. Classe ElementGraphique

La méthode Rectangle::Dessiner est bouleversée, ses enfants Logiciel et FichierJoint n’ont pas besoin de leur propre méthode Dessiner puisque le principe de traçage est identique pour les deux héritières.

Nous dessinerons le rectangle principal avec la couleur de fond :

 
Sélectionnez
Couleurs ( pdonnee→couleurfond[0],  pdonnee→couleurfond[1],  pdonnee->couleurfond[2]);    
tmpcr→rectangle(xg,yh,xd-xg,yb-yh);
tmpcr->fill();

Nous utilisons une fonction « Couleurs » pour simplifier la programmation parce que la fonction set_source_rgb demande comme paramètre une valeur décimale qui représente un coefficient résultant de la valeur entre 0 et 255 écrite dans le fichier divisée par 255 :

tmpcr->set_source_rgb( (valR/255.0), (valV/255.0), (valB/255.0) );

À noter : nous pouvons aussi calculer ces valeurs lors de la lecture du fichier.

Ensuite, nous dessinons le cartouche, qui est la partie supérieure du graphisme :

 
Sélectionnez
Couleurs( pdonnee->couleurcartouche[0], pdonnee->couleurcartouche[1],
                    pdonnee->couleurcartouche[2] );    
tmpcr->rectangle( xg, yh, xd-xg, htcartouche );
tmpcr->fill();

Maintenant, traçons le cadre où nous utilisons d’autres fonctions de traçage de Cairo. Nous définissons un chemin pour réaliser le pourtour de l’objet, puis nous donnons l’ordre de tracer le trait en suivant le chemin :

 
Sélectionnez
Couleurs(0, 0, 0);       // ou tmpcr->set_source_rgb( 0,0,0 );    
   tmpcr→set_line_width(3.0); // épaisseur du trait
   tmpcr→move_to(xg,yh);      // placer le crayon
      tmpcr→line_to(xd,yh);      //
      tmpcr→line_to(xd,yb);      // tracer trois lignes
      tmpcr→line_to(xg,yb);      //
  tmpcr→close_path();            // fermer le rectangle
  tmpcr→stroke();                   // créer le trait

La bibliothèque Pangomm permet de disposer le texte dans la zone graphique. Par les instructions ci-dessous nous formatons la description du texte :

 
Sélectionnez
Couleurs( pdonnee->couleurpolice[0], pdonnee->couleurpolice[1],
          pdonnee->couleurpolice[2] );    
Pango::FontDescription lettrage;
  lettrage.set_family( pdonnee->policecar );
lettrage.set_weight( Pango::Weight::NORMAL );
lettrage.set_size( Pango::SCALE * Pango::SCALE_X_LARGE * pdonnee->taillepol );

Attention au calcul de l’échelle, la taille de police donnée dans le fichier doit être adaptée au périphérique moniteur. Nous devons donc utiliser Pango::SCALE qui est une constante de 1024px qui sera multipliée par un coefficient Pango::SCALE_X_LARGE. Les valeurs de ces coefficients vont de SCALE_XX_SMALL = 0.578 etc... à SCALE_XX_LARGE = 1.728et nous pouvons les trouver ici.

Lorsque la description des caractères est fixée, nous pouvons définir le contenant dans lequel le texte sera écrit et nous lui assignons la description et le texte :

 
Sélectionnez
Glib::RefPtr< Pango::Layout > cadrecorp = Pango::Layout::create( tmpcr );
cadrecorp->set_font_description( lettrage ) ;
cadrecorp->set_text( pdonnee→txtcorps ) ;

Et pour terminer, nous positionnons ce contenant dans le cadre du corps de l’objet :

 
Sélectionnez
int lgtxt, httxt;
cadrecorp->get_pixel_size( lgtxt, httxt );
tmpcr->move_to( xctr + ( lgimg*0.375 ) - lgtxt/2, yctr + htcartouche/2 – httxt/2 );

et nous l’intégrons au contexte : cadrecorp->show_in_cairo_context(tmpcr);.

Nous procéderons de la même façon pour le texte du cartouche.

Préparons maintenant l’objet image en récupérant le fichier png stocké dans les ressources, ici la variable imagecorps est égale à « /icone/edit48.png » , pour initialiser un gdk::pixbuf :

 
Sélectionnez
Glib::RefPtr<Gdk::Pixbuf> image = Gdk::Pixbuf::create_from_resource(
                                                                                             pdonnee->imagecorps );
int lgimg = image->get_width();
int htimg = image->get_height();

Pour terminer la préparation du dessin, attachons l’image en la positionnant dans le cadre:

 
Sélectionnez
Gdk::Cairo::set_source_pixbuf( tmpcr, image,
                   xg – image->get_width()*0.25,
                   yh + htcartouche + image->get_height()*0.25 );

et nous terminerons par l’instruction tmpcr->paint(); qui incorpore dans la zone graphique toutes les occurrences de dessins définies ci-dessus.

Après compilation, nous pouvons tester cette application.

Cette dernière section clôt l’étude sur les bibliothèques Gtkmm Cairomm et Pangomm, avec un petit écart vers CMake et sur l’usage des ressources.

V. Remerciements

Merci à dourouc05 de ses conseils avisés pour la rédaction.

Merci à f-leb de ses observations perspicaces pour la correction de mon texte.

VI. Autres sources de documentation

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

En complément sur Developpez.com

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2024 Daniel Génon. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.