Project - Step 1 : GUI
To translate this course in your native langage:
In Chrome: right-click anywhere on the page and select “Translate to…”
Or use this language selector:
Les mots colorés en anglais représentent des mots-clés de la norme du langage C++. Ils sont conservés à l’identique lors de l’utilisation d’une traduction automatique avec Google Translate. Exemple : l’héritage (inheritance) est un principe fondamental de C++.
Nous allons utiliser notre noyau d’application pour créer un logiciel de dessin vectoriel. La première étape va consister à créer la GUI (Graphical User Interface) permettant de choisir la couleur de tracé, les outils de dessin ou encore la police de caractères…
Tile
Pour uniformiser le style de notre interface, un designer de l’équipe a mis en place une classe Tile pour représenter des zones rectangulaires de différents styles :
Afficher un rectangle avec une couleur de fond
Afficher le symbole trait barré rouge pour indiquer l’absence de couleur de remplissage
Afficher un texte
Voici différents exemples de rendu en utilisant la classe Tile :
Nous voulons construire les différents éléments de l’interface en nous servant de cette brique de base. Nous donnons donc le contexte suivant à OpenCode :
OpenCode : Nous allons construire plusieurs interfaces graphiques à partir de la structure Tile contenue dans Tile.h. Peux-tu prendre en compte cette demande et analyser les membres de cette classe.
Problème de conception : Color or No Color
Dans notre application de dessin, l’utilisateur pourra créer des cercles, des rectangles…
Chaque forme possède deux propriétés :
Une couleur de contour (Stroke color) : la couleur utilisée pour dessiner le bord de la forme.
Une couleur de remplissage (Fill color) : la couleur utilisée pour remplir l’intérieur de la forme.
L’utilisateur peut décider de ne pas donner de contour à la forme ou de ne pas la remplir :
Nous savons représenter une couleur en C++ à l’aide de la structure Color. Mais comment représenter une propriété qui peut désigner :
Soit une couleur précise (rouge, bleu, vert, etc.)
Soit l’absence de couleur (aucune couleur de remplissage ou de bord)
Cette question soulève déjà un problème de conception à petite échelle. Prenez un instant pour réfléchir aux différentes façons de coder cela et comparez ensuite vos propositions à celles ci-dessous :
Propositions
Option 1 : Deux variables : Color c; bool isFilled;. Une variable stocke la couleur, l’autre indique si le remplissage est actif. Approche simple, mais il faut gérer deux variables en parallèle.
Option 2 : Une valeur spéciale (-1, -1, -1) représente l’absence de couleur. Pratique car il n’y a plus qu’une variable, mais si cette valeur était utilisée par erreur pour dessiner, cela produirait un bug.
Option 3 : Une liste de couleurs : vector<Color>. Une liste vide signifie « pas de couleur », tandis qu’une liste contenant un élément représente la couleur choisie. Cela fonctionne, mais cette option paraît excessive pour stocker au maximum une couleur.
Option 4 : Une classe dédiée : EmptyOrColor. On crée une classe qui encapsule une couleur et son éventuelle absence. Mais que doit retourner la fonction getColor() lorsqu’aucune couleur n’est définie ?
Toutes ces solutions sont envisageables, mais aucune n’est vraiment idéale. Demandons l’avis de ChatGPT :
ChatGPT : (Recopiez toutes les options) - Comment représenter simplement l’absence de remplissage dans une application de dessin ?
Réponse : Pour une application de dessin en C++, je choisirais sans hésiter std::optional<Color> si tu utilises C++17 ou plus récent. C’est précisément pour répondre à ce besoin que std::optional a été conçu : lorsqu’une valeur peut exister ou ne pas exister.
#include <optional>
optional<Color> fillColor;
fillColor = Color(255, 0, 0); // Fill Color
fillColor = nullopt; // no fill
// How to draw
if (fillColor) { drawFill(*fillColor); }
Soyons francs, cette problématique n’est pas très importante en elle-même. Néanmoins, à travers cet exemple simple, nous pouvons découvrir une véritable démarche de conception logicielle :
Exprimer le problème : que voulons-nous représenter exactement ?
Explorer les solutions possibles : quelles sont les différentes approches ?
Élargir la réflexion : interroger une IA pour découvrir d’autres possibilités.
Comparer et choisir : retenir une solution adaptée aux contraintes du projet, sans complexité inutile.
Important
A ce stade, plusieurs solutions sont envisageables. L’objectif n’est pas de choisir la plus ingénieuse, mais celle qui répond le plus simplement aux contraintes, tout en restant adaptée à votre niveau d’expérience.
Deux variables : Color c; bool isFilled; : la solution la plus simple mais avec deux variables à gérer
Une classe dédiée : EmptyOrColor : pour ceux qui préfèrent l’approche POO
std::optional : optional<Color> : pour les curieux
Dans la suite, nous travaillons avec std::optional<Color>. De votre côté, vous pouvez faire un autre choix.
OpenCode : Pour les formes graphiques (cercle, rectangle…) utilise std::optional<Color> afin d’indiquer la présence ou l’absence de couleur. Applique cette convention lors des prochaines implémentations concernées.
Réponse (19 s): Convention ajoutée à AGENTS.md pour les prochaines implémentations :
std::optional<Color> pour un remplissage ou une bordure
std::nullopt pour leur absence.
Aucun fichier source modifié.
La palette de couleurs
Mise en place
Nous allons mettre en place une palette de couleurs. L’équipe de designers vous a fourni la maquette suivante :
Spécifications :
Plusieurs couleurs présélectionnées sont présentées à l’utilisateur dans des Tiles alignées sur deux rangées
La couleur de tracé doit être affichée dans un Tile plus gros sur la gauche en haut
La couleur de remplissage doit être affichée dans un Tile plus gros sur la gauche en bas
La transparence doit être présente dans les choix de couleur. Elle est symbolisée par un fond blanc et un trait rouge
Le clic gauche sur un des rectangles de la palette sélectionne la couleur de tracé
Le clic droit sur un des rectangles de la palette sélectionne la couleur de remplissage
Lorsque la couleur de tracé ou de remplissage change, le Tile associé doit afficher cette nouvelle couleur
Le panneau doit faire 80 pixels de haut
OpenCode : (Faites un copier-coller de l’image de la maquette et des spécifications)
Tu dois implémenter cette palette de sélection des couleurs en tenant compte des spécifications et de la maquette fournie.
Réponse (2 min 30) : Je vais créer un panneau réutilisable de 80 pixels de haut, en bas de la fenêtre, avec deux rangées de couleurs et deux grandes tuiles à gauche. Le clic gauche choisira le tracé, le clic droit le remplissage ; std::nullopt représentera la transparence, affichée en blanc avec un trait rouge.
Panneau implémenté avec :
Deux rangées de sept choix, dont la transparence blanc/trait rouge.
Deux grandes tuiles à gauche : tracé en haut, remplissage en bas.
Clic gauche pour le tracé, clic droit pour le remplissage.
Mise à jour immédiate des indicateurs.
Hauteur de 80 pixels.
Compilation Debug et Release réussie ; test de démarrage réussi. Interactions et rendu visuel restent à vérifier manuellement.
Nous allons examiner le rendu.
Ouvrez le projet dans Visual Studio et lancez-le :
Premières remarques :
La couleur de fond de la palette ne correspond pas
Au lieu d’avoir des carrés pour sélectionner et afficher des couleurs, nous avons des rectangles
Les clics fonctionnent correctement
La palette devrait être collée dans le coin sans espacement, mais sur ce point, nous n’avions rien demandé
Note
Pour analyser la maquette de la palette, OpenCode a dû utiliser un agent spécialisé dans la vision. Cet agent a décrit l’image textuellement et a ensuite transmis ces informations à l’agent de codage. Cela a plutôt bien fonctionné. Mais ça reste imprécis.
Nous effectuons une deuxième série de demandes :
OpenCode : Voici plusieurs demandes de modification. La palette doit être alignée sur les bords gauche et inférieur de la fenêtre, sans aucun espacement. Le fond doit être gris clair. Les zones affichant les couleurs de tracé et de remplissage doivent être carrées et plus grosses. Supprime l’affichage de démo actuel.
Réponse (1 min 50) :
Je conserve la hauteur de 80 pixels.
Modifications effectuées :
Palette collée au bord gauche en bas.
Fond gris clair.
Indicateurs agrandis en carrés de 40 × 40 pixels.
Démonstration supprimée.
Compilation Debug et test de démarrage réussis.
Ouvrez le projet dans Visual Studio et testez-le
L’affichage et le comportement sont validés.
Responsabilité et propriété des données
En programmation orientée objet (POO), une question fondamentale de conception consiste à déterminer qui est responsable de quoi.
Lorsqu’on crée une classe, il ne suffit pas de décider quelles variables et quelles méthodes elle contiendra. Il faut surtout définir son rôle dans le programme.
C’est ce qu’on appelle la répartition des responsabilités Assignment of Responsibilities.
La notion de responsabilité
La responsabilité d’une classe recouvre deux aspects :
Ce qu’elle doit savoir : Data ownership
Ce qu’elle doit faire : Behavior Assignment
Autrement dit :
Connaître une information : conserver ou fournir certaines données.
Effectuer une action : réaliser un calcul, modifier un état, afficher un élément, etc.
Prenons notre application de dessin :
Le modèle est responsable de conserver les couleurs sélectionnées.
La logique est responsable de traiter les actions de l’utilisateur et de modifier ces couleurs.
La palette est responsable de présenter les couleurs disponibles et de permettre leur sélection.
Ces responsabilités sont complémentaires, mais elles ne doivent pas être confondues.
La propriété des données
Lorsqu’une donnée représente un état partagé par plusieurs composants, il faut déterminer qui en détient la valeur de référence.
Imaginons que notre application propose trois palettes de styles différents :
Si chacune conserve ses propres variables strokeColor et fillColor, nous obtenons trois copies d’une même information.
Plusieurs problèmes apparaissent alors :
Quelle palette possède la valeur correcte ?
Comment synchroniser les différentes palettes ?
Que se passe-t-il si une palette n’est pas mise à jour ?
Pour éviter ces difficultés, on applique le principe de source unique de vérité Single Source of Truth.
Dans notre architecture MLR, le modèle conserve les couleurs strokeColor et fillColor. Toutes les palettes consultent et modifient ces informations par l’intermédiaire de la logique.
Responsabilité et encapsulation
La responsabilité et l’encapsulation sont deux notions complémentaires, mais de nature différente.
La responsabilité permet de décider où placer les données et les comportements.
L’encapsulation permet de décider comment protéger ces données et contrôler leur accès.
Par exemple :
struct Model
{
private:
std::optional<Color> strokeColor = Color::Yellow;
std::optional<Color> fillColor = std::nullopt;
public:
void setStrokeColor(std::optional<Color> color)
{
strokeColor = color;
}
std::optional<Color> getStrokeColor() const
{
return strokeColor;
}
};
Dans cet exemple :
Responsabilité : le modèle est chargé de conserver la couleur sélectionnée.
Propriété des données : le modèle détient la valeur de référence de cette couleur.
Encapsulation : l’attribut est privé et son accès passe par une interface publique.
L’encapsulation ne garantit donc pas, à elle seule, une bonne répartition des responsabilités.
On peut parfaitement encapsuler une donnée dans la mauvaise classe !
Toutes les données doivent-elles appartenir au modèle ?
Non. Une palette peut posséder ses propres données :
Sa position et ses dimensions.
La liste des couleurs disponibles.
L’indice de la couleur survolée par la souris.
Ces informations relèvent de sa responsabilité.
En revanche, les couleurs sélectionnées pour dessiner appartiennent à l’état de l’application et doivent être conservées dans le modèle.
Le critère n’est donc pas simplement de savoir si une donnée est utilisée à plusieurs endroits, mais de déterminer quel composant doit en être responsable.
Ce qu’il faut retenir
Important
Une bonne conception commence par une bonne répartition des responsabilités.
Avant d’ajouter une variable ou une méthode à une classe, il faut se demander :
À quoi sert cette information ou ce comportement ?
Quelle classe doit en être responsable ?
Cette décision reste-t-elle pertinente si le programme évolue ?
Ce n’est pas parce qu’un composant permet de modifier une donnée qu’il doit en être propriétaire.
Vérification
A ce niveau, on peut être tactique. Il n’est pas forcément nécessaire d’examiner le code interne de la palette généré par l’agent. Un éventuel problème de fonctionnement serait probablement localisé dans son fichier cpp, et donc facile à identifier et à corriger.
Le point stratégique, c’est le modèle, car ses données sont utilisées à de nombreux endroits du programme. Une erreur de conception à ce niveau risque donc de se propager dans toute l’application et de devenir difficile à corriger.
Nous n’avons rien demandé explicitement à l’IA, mais, elle doit respecter l’architecture MLR. Ainsi, il faudrait qu’elle place les deux paramètres strokeColor et fillColor dans le modèle. Or, en examinant le modèle :
struct Model
{
private:
ColorPanel colorPanel{V2(0, 0)};
public :
V2 currentMousePos;
ColorPanel& getColorPanel() { return colorPanel; }
};
On constate, qu’à ce niveau, aucun paramètre n’est présent. L’agent a décidé de stocker les données strokeColor et fillColor dans la palette :
struct ColorPanel
{
V2 position;
V2 size = V2(340, 80);
optional<Color> strokeColor = Color::Yellow;
optional<Color> fillColor = std::nullopt;
...
};
C’est une erreur de conception : des données du modèle sont stockées dans un élément de l’interface qui n’en a pas la responsabilité.
OpenCode : Tu dois respecter l’architecture MLR, les variables strokeColor et fillColor appartiennent au modèle et non à l’interface. Modifie l’architecture en conséquence.
Réponse (2 min 30) : Je vais déplacer les couleurs sélectionnées dans Model. Le panneau ne fera que détecter la case cliquée et afficher les couleurs fournies par le modèle ; processEvent() appliquera la sélection.
Architecture adaptée :
Modèle : possède strokeColor et fillColor en std::optional<Color>.
Logique : processEvent() modifie les couleurs du modèle selon les clics.
Rendu : ColorPanel affiche les valeurs reçues, sans conserver la sélection.
Compilation Debug et test de démarrage réussis.
Les modifications ont réussi :
struct Model
{
private:
std::optional<Color> strokeColor{Color::Yellow};
std::optional<Color> fillColor{std::nullopt};
...
};
Pour vérifier que les modifications ont été correctement effectuées, nous demandons à l’agent d’afficher à l’écran les valeurs de strokeColor et fillColor.
OpenCode : Affiche les valeurs de strokeColor et fillColor à l’écran à chaque affichage à des fins de Debug.
Lancez le projet dans Visual Studio et testez-le
Problème de conception : la responsabilité
Examinons le code généré par l’IA.
Affichage
Nous examinons comment l’IA gère l’affichage, pour cela, nous regardons la fonction drawApp() dans student.cpp :
void drawApp(Graphics& graphics, Model& data)
{
graphics.clearWindow(Color::Black);
...
G.clearWindow(Color::Black);
getColorPanel().draw(G, D.getStrokeColor(), D.getFillColor());
...
}
L’IA a encapsulé l’affichage du panneau à travers la fonction draw() de la classe ColorPanel. Cette approche respecte le critère de responsabilité, c’est bien la palette qui doit savoir se dessiner et non la fonction générale de rendu de l’application.
Gestion des évènements
Nous examinons maintenant comment l’IA gère les évènements souris depuis la fonction processEvent() dans student.cpp :
if (Ev.Type == EventType::MouseDown && (Ev.x == 1 || Ev.x == 3))
{
std::optional<Color> selectedColor;
if (getColorPanel().findColor(Data.currentMousePos, selectedColor))
{
if (Ev.x == 1)
Data.setStrokeColor(selectedColor);
else
Data.setFillColor(selectedColor);
}
}
A ce niveau, le choix de conception fait par l’IA est critiquable. Pourquoi ? La responsabilité de la gestion du clic dans la palette appartient à la palette. Ainsi, le code gérant le clic souris dans un ColorPanel ne devrait pas être géré au niveau de la logique générale de l’application.
Correction
OpenCode : La responsabilité de la gestion du clic souris dans la palette appartient à la palette. Déplace la logique gérant le clic souris dans la fonction processEvent() du colorPanel.
Réponse (1 min 30) : Je déplace la détection des clics et l’interprétation des boutons dans ColorPanel::processEvent(). Le panneau retournera une action de sélection ; la logique générale l’appliquera au modèle, qui reste propriétaire des couleurs.
La gestion du clic appartient désormais à ColorPanel::processEvent() : détection de la case et distinction clic gauche/droit.
Elle retourne une action ColorSelection. La logique générale applique cette action au modèle, qui reste propriétaire des couleurs. L’absence d’action est distincte de la sélection « transparent ».
Problème de conception : le couplage
Le problème
Examinons le code généré par l’agent
void processEvent(const Event& Ev, Model & Data)
{
if (Ev.Type == EventType::MouseMove)
Data.currentMousePos = V2(Ev.x, Ev.y);
const auto selection = getColorPanel().processEvent(Ev, Data.currentMousePos);
if (selection)
{
if (selection->getTarget() == ColorSelection::Target::Stroke)
Data.setStrokeColor(selection->getColor());
else
Data.setFillColor(selection->getColor());
}
}
La gestion du clic souris est maintenant déléguée à la palette : getColorPanel().processEvent(…) ce qui est une très bonne chose.
Cependant, l’IA a rédigé un code un peu compliqué. Elle semble vouloir gérer la modification de la strokeColor et de la fillColor au niveau de la logique générale plutôt que dans la logique de la palette. Pourquoi ? Beaucoup d’entre vous auraient simplement passé par référence le modèle à la palette pour qu’elle applique la modification directement.
Le couplage entre deux classes
Le couplage mesure à quel point deux classes dépendent l’une de l’autre :
Une forte dépendance, appelée couplage fort, fait que si l’un modifie une classe, l’autre doit aussi être modifiée pour continuer à fonctionner.
Une faible dépendance, appelée couplage faible, garantit une forme d’indépendance entre les deux classes. Cela sous-entend que l’on peut exporter une de ces classes vers un autre projet et qu’elle sera directement exploitable.
Si pour gérer le clic dans la palette nous avions choisi cette approche :
getColorPanel().processEvent(Ev, Data);
Nous donnons la responsabilité de la gestion du clic à la palette, ce qui est valide. Cependant, sans le vouloir, nous créons un couplage fort :
La palette va utiliser les fonctions et les variables disponibles dans le modèle
Tout changement de fonctions ou de variables dans le modèle peut casser le fonctionnement de la palette
Par exemple, si nous voulons une application qui gère plusieurs documents, nous pourrions avoir une strokeColor par document ouvert. Ainsi, on ferait évoluer le modèle mais la logique de la palette serait à revoir. Le couplage est fort, la logique de la palette ne peut fonctionner qu’avec cette version du modèle. Si on faisait évoluer le modèle, la palette ne fonctionnerait plus.
En choisissant cette approche, qu’à voulu faire l’IA ?
const auto selection = getColorPanel().processEvent(Ev, Data.currentMousePos);
if (selection)
{
if (selection->getTarget() == ColorSelection::Target::Stroke)
Data.setStrokeColor(selection->getColor());
else
Data.setFillColor(selection->getColor());
}
Avec ce choix de conception, la logique de la palette consiste à retourner :
La couleur sélectionnée
A indiquer s’il s’agit d’une demande concernant la strokeColor ou la fillColor.
De cette façon, la palette devient totalement autonome et son comportement ne dépend d’aucune autre classe. Nous avons atteint notre objectif :
Le couplage est faible
Le périmètre de responsabilité est optimal : la palette ne gère rien qui ne la concerne pas et elle gère tout ce qui la concerne.
Savoir quoi modifier dans le modèle relève maintenant de la responsabilité de la logique de l’application.
Avantage
Ce choix de conception peut sembler superflu : on ajoute du code pour déplacer un traitement dans la logique générale alors qu’on pouvait le faire en deux lignes dans la palette. Cela peut sembler excessif. Mais ce choix de conception prend tout son sens si on examine cette demande du client :
Lorsque je clique dans la palette, si un objet est sélectionné, je ne veux pas que la couleur de dessin change mais que la couleur de l’objet sélectionné soit modifiée.
Cette demande est tout à fait valable et c’est d’ailleurs le fonctionnement de nombreux logiciels aujourd’hui. Si vous mettez toute la logique de gestion du clic souris dans la palette, c’est la palette qui va décider de modifier la couleur de tracé ou la couleur de l’objet sélectionné. Clairement, cela dépasse sa responsabilité. Sans compter que si l’on a trois palettes actives, on va devoir implémenter cette logique dans chaque palette, ce qui est vraiment dangereux.
Travail à effectuer
Width selector
Nouvelle demande du client :
Ce composant permet de sélectionner rapidement l’épaisseur du trait de contour. Il fera la même hauteur que le précédent et sera placé à sa droite.
Travail à effectuer :
Spécifiez le fonctionnement de ce composant - il faut donner tous les détails qui correspondent à la requête que vous allez formuler à l’IA
Réfléchissez à la conception et à l’intégration de ce composant
Utilisez OpenCode pour donner vos consignes
Vérifiez l’implémentation de l’IA et cherchez à savoir si elle valide les contraintes de conception
VIDEO : (max 40 sec) Explicitez vos choix de conception + faire une démo
Ribbon
Nous avons deux panneaux actifs : la palette et le width selector. Plus nous allons avancer dans l’application, plus nous allons avoir de nouveaux panneaux. Le client aimerait pouvoir les réorganiser facilement. Voici sa demande :
Chaque panneau doit comporter deux boutons sur la gauche placés l’un sur l’autre
Un bouton permet de décaler le panneau vers la gauche et l’autre vers la droite
Travail à effectuer :
Réfléchissez aux modifications que cette demande implique sur le modèle
Réfléchissez à la conception et l’intégration de cette demande
Utilisez OpenCode pour donner vos consignes
Vérifiez l’implémentation de l’IA
Conseil : vous pouvez créer plusieurs fois la même palette pour faire vos tests.
VIDEO : (max 1 min) Explicitez vos choix de conception + faire une démo