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 :
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 :
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 :
#
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 :
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.
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 :
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 :
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 :
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 :
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 :
…
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 :
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 :
... 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 :
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 :
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.
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 :
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 :
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 :
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 :
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 :
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 :
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.
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 :
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 :
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 :
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.
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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.
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.
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 :
<?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.
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 :
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.
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 :
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 :
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.
struct
dimsymbole
{
double
hauteur, largeur, xctr, yctr;
}
;
dimsymbole est une structure contenant quatre valeurs de type double indiquant les dimensions du symbole.
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.
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 :
Glib::
RefPtr<
Gio::
InputStream>
fichierdonnees =
Gio::Resource::
open_stream_global("/fichier/donnees.txt"
);
- créer un buffer pour stocker les données lues :
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 :
….
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 :
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 :
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 :
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 :
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 :
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.728
et 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 :
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 :
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 :
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:
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.