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 :

../_images/PLH.jpg

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

../_images/PLH2.jpg

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 :

../_images/event.png

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.

Button

Dans notre fenêtre graphique, des boutons vont nous permettre de sélectionner un outil de dessin comme : l’outil rectangle, l’outil cercle ou l’outil sélection.

Une première option consiste à mettre en place une hiérarchie issue de la classe Button avec une fonction polymorphe OnClick. Dans cette optique, la classe fille RectangleButton va redéfinir la méthode OnClick pour activer l’outil rectangle. Cependant cette approche a un problème. En effet, on doit éviter de créer des classes si peu de différences existent entre elles. Ainsi, on ne crée pas une classe RectangleRouge et une classe RectangleVert, on crée une seule classe Rectangle avec un paramètre couleur. Lorsque l’on analyse notre hiérarchie de Button, seule l’action effectuée par la méthode OnClick change, si l’on pouvait faire en sorte que cette action soit un attribut de la classe Button, nous n’aurions pas besoin d’une hiérarchie.

Heureusement, la STL, dans sa librairie <functional>, fournit une classe template std::function permettant d’affecter une fonction à une variable ! Voici un exemple :

#include <iostream>
#include <functional>
using namespace std;

int mini(int a,int b) { if (a<b) return a; return b; }
int maxi(int a,int b) { if (a>b) return a; return b; }

int main()
{
        function<int(int, int)> myfnt;

        myfnt = mini;
        cout << myfnt(4,5) << endl;

        myfnt = maxi;
        cout << myfnt(4,5) << endl;
}
>> 4
>> 5

Ainsi, une seule classe Button peut être utilisée pour tous les outils :

Type

Exemple

Info

getPos()

Donne la position du coin bas/gauche du bouton

Info

getSize()

Donne la taille H/V du bouton

Constructeur

Button(…)

Nom, dimension, fichier image, fonction à appeler lors du clic

Méthode

manageEvent(…)

Fonction traitant un évènement utilisateur

Méthode

Draw(…)

Dessine le bouton à l’écran

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 :

../_images/MVU.png

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

../_images/etats.jpg

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é.