PROJET - Polymorphisme
Nous vous proposons de compléter le développement d’un logiciel de dessin vectoriel. Cet outil permet de dessiner des formes géométriques simples et de les modifier par la suite : taille, couleur…
Nous allons tout d’abord présenter l’architecture du projet actuel.
Les classes de base
V2
Cette classe fournit une structure pour stocker des coordonnées 2D de points ou de vecteurs.
Type |
Nom |
Description |
---|---|---|
Constructeurs |
V2(x,y) |
valeurs entières |
Opérateurs de base |
+ - * == << |
|
Test |
isInside(…) |
voir ci-dessous |
Utilitaire |
getPLH(…) |
voir ci-dessous |
La fonction isInside détermine si le point courant est à l’intérieur du rectangle défini par le point bas/gauche P et un couple Largeur/Hauteur :
La fonction getPLH à partir de deux sommets opposés d’un rectangle, cette fonction retourne le sommet bas/gauche P et un couple Largeur/Hauteur
Graphics
Lorsque vous aurez des affichages à effectuer, vous disposerez d’un objet Graphics offrant toutes les fonctions pour dessiner à l’écran.
Type |
Nom |
Description |
---|---|---|
Information |
GetWindowSize() |
Retourne la taille de la fenêtre de l’application |
Initialisation |
clearWindow(…) |
Efface le contenu de la fenêtre |
Texte |
drawStringFontRoman(…) |
Affiche un texte |
Texte |
drawStringFontMono(…) |
Affiche un texte |
Dessin |
setPixel(…) |
Positionne la couleur d’un pixel |
Dessin |
drawLine(…) |
Trace un segment |
Dessin |
drawPolygon(…) |
Trace un polygone |
Dessin |
drawRectangle(…) |
Trace un rectangle |
Dessin |
drawCircle(…) |
Trace un cercle |
Sprite |
drawRectWithTexture(…) |
Dessine une image dans le rectangle donnée |
Note
Pour le dessin d’une image, le fichier doit être enregistré au format png et stocké dans le répertoire de projet au coté des fichiers .cpp/.h. Il suffit ensuite de transmettre à la fonction drawRectWithTexture le nom du fichier avec l’extension png, sans le chemin complet.
Color
Dans tout le programme, on utilise une structure Color avec des valeurs normalisées entre 0 et 1.
Type |
Nom |
Description |
---|---|---|
Constructeur |
Color(float r,float g, float b) |
Valeurs RGB entre 0 et 1 |
Constructeur |
Color(float r,float g, float b, float a) |
Paramètre alpha de transparence |
Construction |
ColorFrom255(int r, int g, int b) |
Valeurs RGB entre 0 et 255 |
Construction |
ColorFromHex(int hexCode) |
valeur HTML : 0x12B344 |
Constantes |
Color::White |
Black, White, Red, Green, Blue, Magenta, Cyan, Yellow, Gray |
Les classes du projet
ObjAttr
La plupart des éléments graphiques : rectangle, cercle, polygone, texte possèdent ces 4 paramètres :
Type |
Nom |
Description |
---|---|---|
Attribut |
borderColor |
Couleur de trait |
Attribut |
thickness |
L’épaisseur du bord |
Attribut |
interiorColor |
Couleur de fond |
Attribut |
isFilled |
Intérieur opaque ? |
Il devient alors judicieux de les regrouper dans une structure ObjAttr pour améliorer la lisibilité du code.
Note
Il existe une exception : le segment qui ne requiert que 2 paramètres : couleur et épaisseur. On pourrait faire le choix d’optimiser la mémoire et de faire un cas particulier pour cet élément. Mais, le projet étant assez conséquent, on choisit de simplifier sa structure. Pour cela, on harmonise tous les éléments graphiques en leur donnant un paramètre ObjAttr, segment y compris.
EvenType
Notre application est du type event-driven. Ainsi, l’exécution du code est déclenchée par des actions de l’utilisateur telles que des clics de souris ou des pressions sur les touches. Si l’utilisateur ne fait rien, l’application n’exécute aucun code.
Il existe 5 évènement principaux listés dans l’énumération EvenType :
Type |
Nom |
Description |
---|---|---|
Enum |
MouseMove |
déclenché lors des déplacements de la souris |
Enum |
MouseDown |
déclenché par l’appui sur le bouton 0-1-2 de la souris |
Enum |
MouseUp |
déclenché par le relâchement du bouton 0-1-2 de la souris |
Enum |
KeyDown |
déclenché par l’appui sur une touche |
Enum |
KeyUp |
déclenché lorsque la touche est relâchée |
Event
Pour modéliser les évènements, on aurait pu faire une hiérarchie avec une classe mère Event et 5 classes filles correspondant aux 5 type d’évènements. Cependant, ces classes ne contiennent aucune fonction et elles ne se différencient que par leurs attributs. On choisit une modélisation par classe universelle (universal class modeling). Pour cela, on crée une classe unique regroupant tous les attributs nécessaires pour couvrir plusieurs cas d’utilisation. Au lieu de définir une hiérarchie de classes, une seule classe contient tous les attributs possibles, même si certains ne seront pas utilisés dans certaines configurations.
Attributs de la classe Event :
Type |
Nom |
Description |
---|---|---|
Attribut |
(x,y) |
Position de la souris pour l’évènement MouseMove |
Attribut |
info |
MouveDown-MouseUp : id du bouton de la souris |
info |
KeyDown-KeyUp : la touche appuyée |
A titre indicatif, chaque évènement généré par le système produit un affichage dans la fenêtre console placée en arrière :
Note
On remarque que pour l’évènement MouseDown, les informations (x,y) sont incohérentes car non transmises.
Avertissement
On peut utiliser une classe universelle car ce cas s’y prête bien. En effet, une quarantaine d’évènements sont générés par seconde au maximum et ils sont destinés à être mis à la poubelle une fois leur traitement terminé. Si un objet Event est certes un peu plus volumineux par accumulation d’attributs inutiles, on perd que quelques octets au final.
ObjGeom
L’application affiche différentes formes géométriques : cercles, rectangles, segments, polygones… Nous choisissons de construire une hiérarchie, ceci pour deux raisons :
Les différentes types de formes contiennent des attributs spécifiques
La fonction d’affichage est polymorphe car l’affichage dépend de la classe de l’objet : un objet rectangle s’affiche comme un rectangle, un objet cercle comme un cercle…
Type |
Exemple |
|
---|---|---|
Constructeur |
ObjGeom(…) |
Paramètres définissant la forme |
Méthode |
Draw(…) |
Affichage polymorphe |
Tool
L’application permet de dessiner différentes formes géométriques. Pour chacune d’elles, l’utilisateur va sélectionner un outil de tracé. Comme précédemment, nous choisissons de construire une hiérarchie d’outils :
Type |
Nom |
Description |
---|---|---|
Constructeur |
Tool() |
Pas de paramètre spécifique |
Méthode |
ProcessEvent(…) |
Fonction polymorphe traitant les évènements clavier/souris pour la construction de la forme |
Méthode |
Draw(…) |
Affichage polymorphe |
Note
Pourquoi avoir une méthode d’affichage associée aux outils ? Seuls les objets géométriques sont affichés à l’écran ! Non, pas exactement. Notre application est interactive, c’est à dire que lorsque nous allons tracer un cercle, nous allons cliquer puis faire glisser la souris pour que le cercle grossisse et se positionne à l’endroit désiré. Lors de la construction, l’outil affiche donc un tracé temporaire permettant de positionner la future forme correctement. Le tracé peut être fait en pointillés ou avec une couleur légèrement transparente. Une fois satisfait, l’utilisateur relâche le bouton de la souris et l’outil créé alors l’objet géométrique correspondant et le stocke dans la scène avec les autres.
Avertissement
Il existe une association 1-1 entre la classe d’un objet géométrique et la classe de l’outil associé. On trouve ainsi une classe ObjRectangle qui correspond à un objet géométrique de la scène et un outil ToolRectangle qui correspond à l’outil de tracé des rectangles. Il s’agit de classes différentes avec des rôles différents. Cependant, l’association 1-1 pourra prêter à confusion.
Le modèle : Model-View-Update
Vous avez peut être entendu parler des architectures MVC pour ceux qui ont travaillé sur des interfaces web. Nous allons utiliser un modèle plus simple appelé MVU :
Model : contient toutes les données de l’application
View : à partir des données contenues dans le Model, affiche l’interface de l’application
Update : suite à un évènement clavier/souris met à jour le Model
L'Update, aussi appelé : la Logique, traite propage un évènement utilisateur (clavier/souris) jusqu’à l’entité devant le gérer. Par exemple, un clic souris sur un bouton change l’outil en cours. L’entité effectue son traitement et modifie les données du Model, par exemple, en ajoutant un rectangle dans la scène. A retenir :
L'Update modifie les données du Model
L'Update ne produit aucun affichage
L’étape de View lit les données du modèle sans apporter aucune modification et affiche l’interface de l’application :
La View lit en lecture seule les données du Model
La View n’interagit pas avec le l'Update
La View est seule en charge de l’affichage
Pourquoi le modèle MVU convient-il mieux aux applications graphiques ? A ce niveau, il faut savoir qu’il y a deux points d’entrée dans notre programme présents dans le fichier eleves.cpp :
La fonction ProcessEvent(…) qui reçoit un évènement de l’utilisateur : clavier souris
La fonction DrawApp(…) appelée par le système
Il faut bien comprendre que ces deux appels ne sont pas synchronisés. La fonction ProcessEvent n’est jamais appelée si l’utilisateur ne touche pas le clavier et la souris et ses appels se multiplient lorsque la souris bouge. D’un autre coté, la fonction DrawApp() dépend du système (Windows) qui se charge de l’appeler au moment opportun : déplacement de la fenêtre, agrandissement de la fenêtre, fin de l’appel d’une fonction ProcessEvent. Voici un schéma qui résument les interactions :
Model
La classe Model est instanciée une fois lors du lancement du programme. Cette instance est ensuite transmises aux deux fonctions principales : ProcessEvent() et DrawApp() :
void ProcessEvent(…, Model & Data)
void DrawApp(…, const Model & Data)
On trouve dans la classe Model toutes les informations décrivant notre programme :
Type |
Nom |
Description |
---|---|---|
Attribut |
currentTool |
L’outil de tracé actif |
Attribut |
currentMousePos |
Dernière position connue de la souris |
Attribut |
drawingOptions |
Les attributs de dessin : couleur de trait, de fond, épaisseur… |
Attribut |
LObjets |
La liste des objets géométriques présents dans la scène |
Attribut |
LButtons |
La liste des boutons présents dans l’interface |
DrawApp
Cette fonction est le point d’entrée de la demande d’affichage de notre application. Sa lecture est très instructive et nous détaille comment s’affiche la fenêtre de travail. Vous n’aurez normalement pas de modifications à faire, nous la présentons juste à titre pédagogique :
void DrawApp(Graphics& G, const Model & D)
{
// fond noir
G.clearWindow(Color::Black);
// dessin de toutes les formes géométriques
for (auto& Obj : D.LObjets)
Obj->draw(G);
// dessin des boutons
for (auto& myButton : D.LButtons)
myButton->draw(G);
// dessin de l'outil courant si construction en cours
D.currentTool->draw(G,D);
// dessin du curseur souris
drawCursor(G, D);
}
ProcessEvent
Cette fonction est le point d’entrée d’un évènement utilisateur : clavier / souris. Son process est assez basique :
Elle parcourt les boutons de l’interface pour savoir si un clic a été effectué sur l’un d’eux.
=> Si c’est le cas, elle transmet l’évènement au bouton pour traitement
Si aucun bouton n’a intercepté l’événement, l’évènement est transmis à l’outil courant pour traitement
Le projet
Les outils
Pour effectuer un tracé interactif, il faut pouvoir gérer une machine à états pour chaque outil. Par exemple pour l’outil Segment, nous avons :
Deux états symbolisés par des ronds :
WAIT : où l’outil n’est pas actif
INTERACT : où l’utilisateur déplace la souris pour tracer son segment
Des transitions symbolisées par des flèches :
Depuis l’état WAIT, l’appui sur le bouton gauche de la souris fait passer à l’état INTERACT
Depuis l’état INTERACT, le relachement du bouton gauche de la souris fait passer à l’état WAIT
Ainsi, chaque outil dispose de son attribut interne : currentState qui indique l’état courant. Les différents états ont été définis dans l’énumération suivante :
enum State { WAIT, INTERACT };
Etape 1 : outil Rectangle
Examiner la fonction processEvent() et draw() de la classe ToolSegment. Examinez leur logique interne. Dans la fonction processEvent() des return sont présents, pourquoi ?
Vous avez suffisamment d’informations pour terminer l’outil Rectangle. Programmez ses fonctions processEvent() et draw().
Etape 2 : outil Cercle
Voici les différentes tâches à remplir :
Trouvez l’image associée à cet outil dans le répertoire de projet
Créez la fonction qui change l’outil courant pour l’outil Cercle
Créez le bouton associé à cet outil dans la fonction InitApp présente dans eleves.cpp.
Associez ce bouton à la fonction de changement d’outil
Créez la classe dérivée de la classe mère Tool
Implémentez les fonctions processEvent() et draw()
Ajoutez un objet Cercle dans la liste des objets de la scène en tenant compte des paramètres de tracé courant
Avertissement
Le clic de la souris positionne le centre du cercle. Ensuite, en déplaçant la souris, le curseur indique un point du cercle. Nous avons donc deux points qui définissent le cercle actuel : le centre et un point du bord.
Etape 3 : outil RAZ
Créez un bouton qui lorsque l’on appui dessus vide le contenu de la scène
Etape 4 : outil Sélection/Suppression
Voici les fonctionnalités à mettre en œuvre :
Créez un nouvel outil permettant de sélectionner un objet
Pour chaque objet, créez une fonction retournant une hitbox correspondant à un rectangle englobant cet objet
Lorsque l’utilisateur clique dans la scène, sélectionnez au plus un objet dont la hitbox contient le curseur
Si l’utilisateur reclique, la sélection courante est oubliée et une nouvelle sélection est activée
L’objet sélectionné doit être affiché différemment : couleur inversée, trait plus épais, encadré en rose…
Si l’utilisateur clique sur le bouton de l’outil suppression l’objet sélectionné est retiré de la scène
Etape 5: outil Devant/Derrière
Pour cela, il faut utiliser des objets superposés avec des fonds pleins.
Voici les fonctionnalités à mettre en œuvre :
Si l’utilisateur clique sur le bouton Devant, l’objet sélectionné monte d’un cran dans la liste des objets
Si l’on clique plusieurs fois, l’objet finit par recouvrir tous les autres
Symétriquement, on construit le bouton Derrière, qui fait descendre l’objet sélectionné dans la liste
Si l’on clique plusieurs fois, l’objet finit par être recouvert par tous les autres
Etape 6 : options de tracé
Ajoutez 4 boutons permettant de gérer :
La couleur de trait
La couleur de fond
L’épaisseur
La présence d’un fond ou non
Pour chaque bouton, le clic change le paramètre courant pour le suivant. Par exemple, on peut partir du principe qu’il y a un choix parmi 7 couleurs, 4 épaisseurs. Le bouton affiche le choix courant :
Pour la couleur de trait : il dessine un trait de cette couleur à l’intérieur du bouton
Pour l’épaisseur : il dessine un trait de l’épaisseur correspondante à l’intérieur du bouton
Etape 7 : outil Ligne Polygonale
Ajoutez un nouvel outil pour tracer une ligne brisée
L’interaction est différente, chaque clic de la souris va ajouter un nouveau sommet à la ligne courante
Il faudra utiliser la touche Entrée ou le bouton de droite de la souris pour stopper la construction de la ligne
Etape 8 : Save/Load
La sérialisation est le processus de transformation d’un objet en un format qui peut être facilement stocké.
Créez une fonction polymorphe Serialize qui traduit tout objet graphique en chaîne de caractères. Cette chaîne de caractères doit comporter suffisamment d’information pour permettre de reconstruire l’objet à posteriori.
Créez un bouton Save qui sauvegarde le contenu de la scène dans un fichier. Le nom du fichier peut être fixe.
Créez un bouton Load reconstruit la scène sauvegardée dans un fichier. Le nom du fichier peut être fixe.
Voici un exemple de la classe stringstream qui peut s’avérer utile :
#include <sstream>
using namespace std;
stringstream ss;
// put data into the stream
ss << 4.5 << ", " << 4 << " hello";
// convert the stream buffer into a string
string str = ss.str();
Etape 9 : Undo
Ajoutez un bouton Undo permettant d’annuler la dernière action
Plusieurs approches sont possibles. Nous en décrivons une utilisant la sérialisation :
A chaque modification de la liste des objets, effectuez une sérialisation de la scène dans un string
Archivez ces strings dans un container servant d’historique
Lors de l’appui sur le bouton Undo : videz la scène et remplacez là par la dernière sérialisation disponible
Etape 10 : Edition des points
L’appui sur le bouton Edition des Points fait passer en mode édition l’ensemble des points présents dans les objets :
Chaque point est affiché dans la scène par dessus l’affichage courant
Lorsque l’on clique à proximité d’un point, on a alors la capacité de le déplacer en bougeant la souris
Un nouvel appui sur le bouton Edition des Points réactive l’outil précédemment utilisé.