EZDraw++
1.2.2-3
Une mini-bibliothèque minimale pour réaliser un programme graphique en C++ sur Windows ou Linux
|
Un programme graphique comportant des fenêtres et réagissant aux interactions de son utilisateur (via la souris, le clavier, etc.) ne se conçoit pas dans le style procédural classique par lequel les débutants découvrent habituellement la programmation.
Dans le style procédural, c'est le programmeur qui choisit la séquence d'actions que le programme va suivre et l'utilisateur ne pourra pas sortir de ce déroulement prévu à moins d'interrompre et quitter (Ctrl-C) le programme lui-même.
Dans le style événementiel, le programmeur doit renoncer à contrôler le déroulement global de l'application. Il se contente de programmer un ensemble de fonctions et d'indiquer en réponse à quelle(s) action(s) de l'utilisateur elles devront se déclencher. Par exemple, il est possible d'écrire un message sur la console à chaque fois que l'utilisateur appuiera sur le bouton gauche de la souris... mais il n'est (évidemment) pas possible de forcer l'utilisateur à le faire ! De manière amusante, le main()
d'un tel programme est souvent très bref : il se contente de préparer rapidement le terrain (initialisations, constructions de certains objets... en particulier les fenêtres) puis il va simplement appeler une fonction de la bibliothèque d'interfaçage graphique qui va s'occuper d'attendre que l'utilisateur fasse quelque chose. Cette fonction devient alors le vrai chef d'orchestre de votre programme. Elle s'apparente à une boucle infinie, dont vous n'avez pas le contrôle. Le seul moyen de sortir de cette boucle est que l'utilisateur réclame la fin du programme (en général, en cliquant sur la croix de fermeture de la fenêtre). La sortie de cette boucle est alors définitive et la suite du main()
sera effectuée, essentiellement pour détruire ce qui aura été créé initialement puis terminer le programme.
Une des subtilités importantes à retenir est que la boucle d'attente des événements est faite de telle façon que tant qu'il ne se passe rien, votre programme ne fait vraiment rien : il est dans un état stoppé (il ne sollicite alors plus le processeur). C'est important car s'il s'agissait d'une véritable boucle infinie qui tournait en permanence, la puissance de calcul de votre ordinateur serait sollicitée très lourdement et parfaitement inutilement. Pire encore, la batterie de nos ordinateurs portable baisserait à vue d'œil !
Ouvrez dans un éditeur de texte le premier programme événementiel d'exemple fourni : « demo++01.cpp
». Compilez cet exemple et lancez-le avant de lire les explications qui suivent. Ce programme ouvre simplement une unique fenêtre. Il se termine lorsque l'utilisateur clique sur le bouton de fermeture de la fenêtre. C'est le programme le plus simple que vous pouvez écrire avec EZ-Draw++.
Pour utiliser EZ-Draw++, il faut commencer par inclure dans l'en-tête « ez-draw++.hpp
» avec des guillemets car ce fichier se trouve normalement dans le même répertoire que votre fichier source. Cela importe les déclarations des classes de la bibliothèque et leurs fonctions membres.
Dans le main()
, on initialise ensuite une instance de la classe EZDraw
. C'est cet objet qui va gérer les évènements alors il est indispensable de le créer. Il ne faut pas en créer plus d'un.
Ensuite on crée une (ou plusieurs) fenêtre(s) qui sont des instances de la classe EZWindow
. Le constructeur de EZWindow
est :
et ses paramètres sont :
width
: la largeur de l'intérieur de la fenêtre en pixels,height
: la hauteur,title
: le titre de la fenêtre. Une fenêtre créée ainsi restera simplement blanche puisqu'on n'a encore rien fait pour qu'elle ait un comportement plus intéressant.Il est important de comprendre que les fenêtres, même si elles sont créées au moment de leur construction C++, ne deviennent visibles que quand le programme atteint le gestionnaire d'évènements qui est déclenché par l'appel à ezDraw.mainLoop()
. C'est cette fonction qui fait "vivre" les fenêtres. C'est une boucle sans fin, qui peut être interrompue par un appel à EZDraw::quit()
, ou lorsque toutes les fenêtres sont détruites par un clic de l'utilisateur sur la croix de fermeture.
Ouvrez dans un éditeur de texte le deuxième programme d'exemple fourni : « demo++02.cpp
». Compilez cet exemple et lancez-le. Ce programme illustre comment traiter certains évènements : souris, clavier, etc.
Pour être en mesure de réagir aux actions de l'utilisateur (touches du clavier, mouvement de souris, clics de souris, etc.), il faut réaliser ce qu'on nomme une gestion d'évènements. Pour cela, il faut améliorer la classe EZWindow
en lui disant quoi faire pour chaque type d'évènement. En effet, par défaut (si on ne précise pas), elle ne fait rien de spécial... d'où le comportement un peu aride de demo++01
.
Nous créons une nouvelle classe MyWindow
qui hérite publiquement de EZWindow
, puis pour définir des réactions aux différents évènements, il suffit d'implanter sous forme de surcharge certaines fonctions membres qui correspondent aux événements auquel on veut réagir. Par exemple pour réagir à l'appui sur une des touches du clavier, il faut implanter la fonction membre
qui sera appelée à chaque fois qui l'utilisateur enfonce une touche. L'information permettant de savoir quelle touche a été enfoncée est reçue grâce au premier paramètre keysym
. La valeur de cette variable du type EZKeySym
est une valeur numérique (qui n'a pas de rapport avec les codes ASCII) et qu'il est plus facile de représenter dans votre code par un symbole équivalent (défini dans ez-draw++.hpp
) formé de «EZKeySym::
» suivi de la touche (en anglais). Ainsi la touche « q » correspond au EZKeySym
nommé EZKeySym::q
, la touche « Echap » à EZKeySym::Escape
et la touche « ² » à EZKeySym::twosuperior
. Les touches numériques ont un caractère souligné en préfixe dans le nom du symbole, ainsi « 0 » correspond à EZKeySym::_0
. Si nécessaire, vous pouvez trouver la liste des symboles dans le manuel de référence.
Le système d'exploitation (Windows ou Linux/X11) indique à votre programme si vos fenêtres doivent être redessinées (par exemple si une autre fenêtre est passée devant). Lorsque cela arrive, l'évènement
est déclenché. Vous devez alors redessiner l'ensemble du contenu de la fenêtre. Il faut aussi noter que cette fonction est automatiquement déclenchée lorsque la fenêtre arrive pour la première fois à l'écran après sa création : c'est normal, il faut bien la dessiner au moins la première fois après tout !
Il est également important de se souvenir qu'il n'y a que dans cette fonction qu'il est légal de réaliser des tracers (lignes, textes, images, etc.) dans cette fenêtre.
Toute la gestion d'évènement, y-compris les autres sources d'événements,est expliquée plus en détail à l'exemple « demo++05.cpp
».
Ouvrez dans un éditeur de texte le troisième programme d'exemple fourni : « demo++03.cpp
». Compilez cet exemple et lancez-le. Ce programme illustre comment tracer un certain nombre de figures géométriques simples dans vos fenêtres.
Comme expliqué un peu avant, l'évènement expose
signifie qu'il faut redessiner le contenu de la fenêtre. À noter, pour chaque expose
, EZ-Draw vide entièrement (en blanc) la fenêtre avant que votre fonction membre n'ait à dessiner le contenu de la fenêtre.
La liste des dessins possibles figure dans le tableau ci-dessous. Pour plus de précision, consulter le manuel de référence de la classe EZWindow
.
Méthode | Dessin obtenu |
---|---|
drawPoint() | un point |
drawLine() | un segment de droite |
drawRectangle() | un rectangle vide |
fillRectangle() | un rectangle plein |
drawCircle() | une ellipse vide |
fillCircle() | une ellipse pleine |
drawTriangle() | un triangle vide |
fillTriangle() | un triangle vide |
drawText() | un texte |
Les coordonnées sont relatives à l'origine, qui est le coin en haut à gauche de l'intérieur de la fenêtre, avec x
orienté vers la droite et y
vers le bas.
Les dessins sont automatiquement coupés par le bord de la fenêtre, il n'y a donc pas à se préoccuper de savoir si un dessin risque de dépasser ou pas.
Les dessins sont faits dans l'épaisseur courante (par défaut 1 pixel). On peut changer l'épaisseur courante avec void EZWindow::setThick(int)
.
Les dessins sont faits dans la couleur courante (par défaut en noir). Pour changer la couleur courante, on appelle la fonction membre EZWindow::setColor(EZColor)
en lui donnant un numéro de couleur. Quelques couleurs sont prédéfinies : ez_black
, ez_white
, ez_grey
, ez_red
, ez_green
, ez_blue
, ez_yellow
, ez_cyan
et ez_magenta
. On peut créer d'autres couleurs (consulter la référence ainsi que demo++11.cpp
).
Lancez et examinez le comportement de demo++04
: ce programme montre les différentes façons d'écrire du texte dans une fenêtre. Il montre également que le contenu de la fenêtre peut s'adapter à ses changements de taille.
On peut afficher du texte n'importe où dans la fenêtre à l'aide de la fonction membre void EZWindow::drawText(EZAlign align,int x1,int y1,const std::string &str) const
. Elle prend en paramètre le type d'alignement align
, puis des coordonnées x1,y1
, enfin une chaîne de caractères C++ (std::string
) à imprimer. Une autre méthode existe sur le même principe pour imprimer une chaîne C (const char *
). Tout est détaillé dans le manuel de référence.
Le résultat peut comporter des « \n
» provoquant des saut de lignes dans l'affichage.
L'affichage de texte se fait dans la couleur courante, modifiable par void EZWindow::setColor(EZColor)
.
On profite de cet exemple pour montrer aussi l'usage de unsigned int EZWindow::getWidth() const
et unsigned int EZWindow::getHeight() const
pour faire un dessin qui s'adapte aux changements de taille de la fenêtre.
Lancez et examinez le comportement de demo++05
: on recense tous les évènements possibles et on en affiche les paramètres à l'écran (sur la console).
Par défaut, le bouton "Fermer" dans la barre de titre d'une des fenêtres de l'application provoque la fin de tout le programme. On peut changer ceci en changeant le réglage d'origine en utilisant la fonction membre void EZDraw::setAutoQuit(bool)
: pour une valeur de true
le programme se fermera tout seul, pour une valeur de false
la fermeture ne sera plus automatique, et le bouton "Fermer" provoquera l'évènement WindowClose
.
Seuls les évènements de type TimerNotify
ne sont pas traités ici : consultez le manuel de référence et l'exemple demo++10
plus bas.
Ce programme est un petit exemple dans lequel on peut dessiner à la souris dans la fenêtre. Le dessin est fait ici dans les évènements ButtonPress
et MotionNotify
, mais (pour garder l'exemple simple) il n'est pas mémorisé. Il est donc impossible de le retracer si jamais la fenêtre est recouverte...
Quand on appuie sur la touche espace, l'application appelle void EZWindow::sendExpose() const
, ce qui provoque l'envoi d'un évènement Expose
, qui efface la fenêtre en blanc, puis appelle notre fonction membre MyWindow::expose()
, où il n'y a pas actuellement de code pour refaire le dessin à la souris. Ceci est laissé à titre d'exercice ! Indice : il suffit de mémoriser tous les segments dans une liste pour pouvoir les retracer à chaque fois qu'on reçoit l'évènement Expose
.
On peut créer autant de fenêtres EZWindow
(ou de classes qui en sont dérivées) que l'on veut. Chaque fenêtre créée est affichée par dessus les autres fenêtres. Pour faire disparaître une fenêtre, on peut soit la détruire complètement (c'est-à-dire opérer sa destruction au sens C++ du terme), soit la cacher temporairement en déclenchant void EZWindow::setVisible(bool)
avec false
en paramètre (elle continue alors d'exister tant qu'elle n'est pas détruite, même si elle n'est plus visible). Sous Unix, ne soyez pas surpris si, lorsque vous faites réapparaître la fenêtre, elle se place à un autre emplacement : votre gestionnaire de fenêtres est libre de faire ce qu'il lui plaît ; c'est indépendant de votre programme ! Vous remarquerez cependant que la taille a été conservée.
Dans l'exemple, on crée trois classes dérivées par héritage de EZWindow
:
MyMainWindow
qui va servir de fenêtre principale de l'application ;MyWindow2
qu'il est possible de cacher et montrer sans la détruire-recréer ;MyWindow3
qu'on peut créer et détruire à volonté. On rappelle qu'ici on crée trois classes car on veut bien trois comportements différents. Si on avait voulu plusieurs fenêtres ayant le même comportement, il aurait suffis de créer plusieurs instances de la même classe.On crée également une classe MyApplication
qui va hériter publiquement de EZDraw
(c'est-à-dire organiser la vie du programme), et qui va contenir les trois fenêtres en tant que données membres. On fait le choix de stocker directement les deux premières fenêtres comme données membres "classiques" de la classe, c'est-à-dire que la durée de vie de ces fenêtres est fixée par la durée de vie de l'instance de MyApplication
à laquelle elles sont liées. La troisième fenêtre par contre sera créée dynamiquement (avec un new
) donc on a besoin d'un pointeur pour mémoriser son adresse.
Remarque : les déclarations et les définitions des classes et fonctions membres pourraient très bien se trouver dans plusieurs fichiers .hpp
et .cpp
séparés du fichier main.cpp
dans lequel se trouverait la fonction main()
: c'est uniquement par commodité qu'on a tout mis dans un seul fichier ici.
Le constructeur de MyApplication
après avoir créées dans sa liste d'initialisation la fenêtre principale et la fenêtre numéro 2, appelle setAutoQuit(false)
pour modifier le comportement de la boucle principale : le bouton "Fermer" de la barre de titre d'une fenêtre ne provoquera donc plus la fin du programme, mais provoquera l'évènement WindowClose
.
Une fois la classe MyApplication
complétée, on en crée une instance dans le main()
, ainsi elle sera utilisable pendant toute la durée du programme. De plus, pour qu'elle soit accessible à n'importe laquelle de nos fenêtres, on prend soin de mémoriser au moment de la construction de chacune d'entre elles, un pointeur vers l'instance de MyApplication
. Cette astuce (très fréquente et à connaître !) permet de ne pas avoir recours à une variable globale (ce qui serait très critiquable en C++).
On profite de cet exemple pour montrer aussi l'usage de int EZWindow::getWidth()
et int EZWindow::getHeight()
pour faire un dessin qui s'adapte à la taille de la fenêtre ; une autre méthode consisterait à capter l'évènement ConfigureNotify
(changement des dimensions de la fenêtre) afin de connaître et mémoriser la nouvelle taille quand elle change.
L'exemple tracer.cpp
illustre également comment ouvrir plusieurs fenêtres pour tracer dans chacune le graphe d'une fonction mathématique différente.
L'exemple montre comment lire depuis le clavier une chaîne de caractères, supprimer des caractères avec la touche Backspace, et détecter la frappe de la touche Entrée pour déclencher une action.
La saisie ainsi obtenue reste assez rudimentaire mais devrait suffire à la plupart des besoins simples.
Pour réaliser une animation, il faut deux ingrédients supplémentaires : un timer pour entretenir la séquence temporelle, et un double buffer d'affichage pour éviter que l'affichage ne clignote (consultez le manuel de référence pour plus d'informations).
Dans l'exemple suivant, on fait tourner les aiguilles d'une montre dans une fenêtre (on peut faire pause avec la touche espace), tandis qu'une balle rebondit sur une raquette dans une seconde fenêtre (que l'on peut agrandir).
Un autre exemple d'animation, le fichier demo++17.cpp
, montre en plus l'utilisation d'images et de pixmaps pour accélérer le tracé à l'écran.
D'autres fichiers d'exemples sont fournis. Les commentaires qu'ils contiennent devraient, avec l'aide également de la documentation de référence, vous permettre de les comprendre sans plus d'explication.
Fichier | Illustration |
---|---|
demo++11.cpp | Choisir une couleur précise en RGB ou en HSV ou un niveau de gris |
demo++12.cpp | Choisir une couleur dans une palette HSV |
demo++13.cpp | Charger et utiliser une image PNG, JPEG ou GIF |
demo++14.cpp | Utilisation des images et de la transparence |
demo++15.cpp | Étirement des images |
demo++17.cpp | Animation optimisée d'image grâce à la classe EZPixmap |