2 juin 2025
L’objectif de cet exercice est de créer un outil permettant de
visualiser et éditer des dessins vectoriels. Pour simplifier l’exercice,
cet outil ne permet de gérer que des cercles. Il est capable de charger
des dessins, stockés dans des fichiers formatés en XML,
dont visage.xml
est un exemple :
<?xml version="1.0"?>
<!-- Dans cette version de format, les coordonnees (x,y) sont exprimees dans un -->
<!-- repere dont le centre est au centre de l'ecran, les valeurs positives de x -->
<!-- etant vers la droite, les valeurs positives de y etant vers le haut. -->
Drawing>
<<!-- Le noeud suivant cree un cercle dont le centre est en (0,0), -->
<!-- de rayon 200, de couleur noir, qui a pour etiquette -->
<!-- "contourVisage" -->
Circle label="contourVisage" x="0" y="0" r="200" color="Black"/>
<Circle label="nez" x="0" y="0" r="20" color="Red"/>
<<!-- Le noeud suivant cree un groupe "oreilles" positionne en (0,210) -->
Group label="oreilles" x="0" y="210">
<Group label="oreille1" x="-210" y="0">
<Circle label="c11" x="0" y="0" r="100" color="Black"/>
<Circle label="c12" x="0" y="0" r="70" color="Magenta"/>
<Group>
</Group label="oreille2" x="210" y="0">
<Circle label="c21" x="0" y="0" r="100" color="Black"/>
<Circle label="c22" x="0" y="0" r="70" color="Magenta"/>
<Group>
</Group>
</Drawing> </
Pour réaliser cet exercice, nous allons procéder par étapes, la visualisation graphique n’arrivant qu’à l’étape 4.
NB :
Reprenez le code que vous avez obtenu à la fin de l’exercice Analyse d’un fichier XML ou bien utilisez directement son corrigé.
Modifiez le code de la manière suivante :
class
au lieu du mot-clé
struct
. Ainsi, désormais, par défaut, les déclarations
d’attribut ou de méthode sont privées. Il vous faut donc ajouter le
mot-clé public:
pour rendre les déclarations visibles de
l’extérieur (et, si besoin, mentionner private:
pour celles
que vous souhaitez garder privées).Circle(const pugi::xml_node& node);
et un constructeur
Group(const pugi::xml_node& node);
myMain.cpp
et dans unitTests.cpp
, par exemple,
en créant un groupe simplement avec :{ doc.child("Group") };` Group g
myMain.cpp
en ne gardant que
:#include "myMain.h"
int myMain() {
return 0;
}
Enfin, vérifiez que vos tests unitaires s’exécutent correctement et que, par conséquent, votre code est toujours en mesure de lire correctement les XML suivants :
<?xml version = "1.0"?>
Circle label="testCircle" x="0" y="1" r="2" color="Black" />)"; <
et
<?xml version = "1.0"?>
Group label="testGroup" x="0" y="1">
<Circle label="testCircle1" x="2" y="3" r="4" color="Black"/>
<Circle label="testCircle2" x="5" y="6" r="7" color="Black"/>
<Group> </
Corrigé (à exploiter selon la procédure Construire un projet C++ avec cmake).
Dans une démarche TDD (Test Driven Development), commencez
par coder le test unitaire
TEST(TestReadXML, TestGroupHybrid)
qui :
<?xml version = "1.0"?>
Group label="testGroupHybrid" x="0" y="1">
<Circle label="testCircle1" x="2" y="3" r="4" color="Black"/>
<Group label="testGroup" x="5" y="6">
<Circle label="testCircle2" x="7" y="8" r="9" color="Black"/>
<Group>
</Circle label="testCircle3" x="10" y="11" r="12" color="Black"/>
<Group> </
g
correspondant à cet XMLg.dump()
est égal à une
string c_dump_ref
que vous initialisez avec le contenu du
dump que vous vous attendez à obtenir, par exemple :Group "testGroupHybrid", x: 0, y: 1, children: [
| Circle "testCircle1", x: 2, y: 3, r: 4, color: "Black"
| Group "testGroup", x: 5, y: 6, children: [
| | Circle "testCircle2", x: 7, y: 8, r: 9, color: "Black"
| ]
| Circle "testCircle3", x: 10, y: 11, r: 12, color: "Black"
]
NB : L’ordre d’apparition des éléments doit être respecté : En
particulier, pour ce test, votre code doit afficher
Circle "testCircle1"
, puis Group "testGroup"
contenant le Circle "testCircle2"
, et enfin
Circle "testCircle3"
. Votre code ne doit
pas afficher Circle "testCircle1"
,
Circle "testCircle3"
, puis
Group "testGroup"
contenant le
Circle "testCircle2"
.
Modifiez votre code jusqu’à ce que tous vos tests soient OK.
Corrigé (à exploiter selon la procédure Construire un projet C++ avec cmake).
Ca y est, tous vos tests unitaires de l’étape 1 passent.
Mais, il est possible que vous ayez des fuites mémoire dans le code que vous avez écrit. Vérifiez-le en appliquant la procédure Analyse de fuites mémoire.
Après le point de cours sur la gestion des fuites mémoire en C++ moderne grâce aux pointeurs « intelligents », faites évoluer votre code pour éradiquer vos fuites mémoire.
NB : Si vous rencontrez (ou non !) des soucis à convertir vos
pointeurs nus en unique_ptr
, consultez ce mini guide de survie avec les
unique_ptr
pour en savoir plus.
Dans cette section, vous modifiez myMain()
(et d’autres
parties de votre code) pour que le fichier visage.xml
(cf. section Introduction de cet énoncé)
soit lu et affiche, grâce à un dump()
, l’arborescence d’un
dessin.
Avant de vous lancer dans le codage, remarquez que
visage.xml
est un fichier de ressources. Nous allons donc
le stocker dans un endroit adéquat et configurerons notre IDE pour qu’il
le mette à disposition de votre exécutable. qui doit être stocké au même
endroit que le main()
de votre programme. Donc :
resources
(NB : un seul « s » à
resources
, car mot écrit en anglais) à la racine de votre
projet, puis le fichier resources/visage.xml
et son contenu
(cf. section Introduction).src/main/CMakeLists.txt
les lignes :add_custom_target(copy-resources ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/resources)
file(GLOB RESOURCES CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/resources/*.*)
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/resources
DEPENDS ${CMAKE_SOURCE_DIR}/resources
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/resources
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${RESOURCES}
${CMAKE_CURRENT_BINARY_DIR}/resources)
add_dependencies(main copy-resources)
resources/visage.xml
dans la
variable doc
:::xml_document doc;
pugiif (auto result = doc.load_file("resources/visage.xml"); !result) {
std::cerr << "Could not open file visage.xml because " << result.description() << std::endl;
return 1;
}
visage.xml
(vous devriez pouvoir vous
contenter de travailler avec des string
qui contiennent le
XML que vous souhaitez analyser). Toutefois, si vous souhaitez y accéder
quand même, vous devez ajouter à la fin de
src/test/CMakeLists.txt
les lignes :add_custom_target(copy-resources-for-test ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/resources)
file(GLOB RESOURCES_FOR_TEST CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/resources/*.*)
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/resources
DEPENDS ${CMAKE_SOURCE_DIR}/resources
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/resources
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${RESOURCES_FOR_TEST}
${CMAKE_CURRENT_BINARY_DIR}/resources)
add_dependencies(unitTests copy-resources-for-test)
Maintenant, vous pouvez coder l’affichage en mode texte de l’arborescence du dessin.
Corrigé (à exploiter selon la procédure Construire un projet C++ avec cmake).
Dans cette section, vous modifiez myMain()
(et d’autres
parties de votre code) pour que le fichier visage.xml
soit
lu et affiche, avec SFML, la figure qu’il contient.
Avant de vous lancer dans le codage, vous devez signifier à votre IDE que vous allez utiliser SFML au cours de cette étape.
CMakeLists.txt
de plus haut niveau,
ajoutez les lignes comprises entre
###### DEBUT lignes a rajouter
et
###### FIN lignes a rajouter
comme dans le modèle suivant
(respectez scrupuleusement l’ordre des lignes, sinon vous aurez des
soucis au moment de l’édition des liens) :.......
set (BUILD_SHARED_LIBS FALSE)
###### DEBUT lignes a rajouter
FetchContent_Declare( sfml
URL https://github.com/SFML/SFML/archive/refs/tags/3.0.1.tar.gz
)option(SFML_BUILD_AUDIO "Build audio" OFF)
option(SFML_BUILD_NETWORK "Build network" OFF)
FetchContent_MakeAvailable(sfml)
###### FIN lignes a rajouter
set(CMAKE_CXX_STANDARD 23)
.......
src/main/CMakeLists.txt
, pour spécifier que votre
exécutable main
utilise sfml-graphics
, ajoutez
sfml-graphics
à la ligne
target_link_libraries()
:target_link_libraries(main PUBLIC lib_core pugixml sfml-graphics)
src/core/CMakeLists.txt
, pour spécifier que vos
fichiers sources dans src
peuvent référencer les fichiers
d’include de SFML, ajoutez sfml-graphics
à la
ligne target_link_libraries()
:target_link_libraries(lib_core PUBLIC pugixml sfml-graphics)
src/test/CMakeLists.txt
, pour spécifier que votre
exécutable unitTests
utilise sfml-graphics
,
ajoutez sfml-graphics
à la ligne
target_link_libraries(unitTests lib_core pugixml)
:target_link_libraries(unitTests lib_core pugixml sfml-graphics)
Maintenant, vous pouvez vous lancer dans le codage de cette nouvelle fonction avec un dernier conseil pour la route : Pour dessiner, vous aurez besoin de coordonnées absolues ; calculez-les dynamiquement (ne les stockez pas dans les objets, car cela serait incompatible avec les questions suivantes).
Corrigé (à exploiter selon la procédure Construire un projet C++ avec cmake).
src/core/myMain.cpp
la fonction suivante
:void modify_drawing(Drawing &d) {
.find_shape_by_label("nez")->set_color("Yellow"); // Si set_color est appliqué à un groupe, il doit être
d// appliqué à chaque élément du groupe.
// d.find_shape_by_label("c22")->translate(-10,-10);
// d.find_shape_by_label("oreille1")->clone(); // Le groupe "oreille1" est cloné en un groupe de label
// // "oreille1+" contenant les cercles "ci1+" et "ci2+",
// // clones de "ci1" et "ci2".
// cout << d.dump("") << endl;
// d.find_shape_by_label("oreille1+")->set_color("Blue");
// d.find_shape_by_label("oreille1+")->translate(0, -455);
// d.find_shape_by_label("c12+")->translate(210, -30);
// d.find_shape_by_label("c11+")->set_color("Cyan");
// d.find_shape_by_label("c11+")->clone(); // Le cercle "c11+" est cloné en cercle "c11++".
// d.find_shape_by_label("c11++")->translate(420, 0);
}
Dans la fonction myMain()
, insérez un appel à
modify_drawing()
entre l’instruction qui lit le fichier
XML dans la variable contenant votre Drawing
et
l’instruction qui fait son affichage graphique.
Modifiez vos classes pour que le code compile et s’exécute correctement : le nez est jaune.
Décommentez la ligne
d.find_shape_by_label("c22")->translate(-10,-10);
Modifiez vos classes pour que le code compile et s’exécute correctement : le lobe droit est légèrement excentré.
Décommentez les lignes
d.find_shape_by_label("oreille1")->clone();
jusqu’à
cout << d.dump("") << endl;
(incluse).
Modifiez vos classes pour que le code compile et s’exécute
correctement : le dump doit afficher le Group oreille1+
avec le bon contenu et au même niveau que le
Group oreille1
. NB : Le graphique est inchangé car
oreille1
et oreille1+
sont
superposées.
Décommentez toutes les autres lignes : A vous de décider ce qui est apparu.
Tâche optionnelle (destinées à celles·ceux qui ont pris de
l’avance) : Implantez une interface graphique qui vous permet d’invoquer
les méthodes clone()
, translation()
et
set_color()
de votre Drawing
. Pour cette
interface graphique, nous vous proposons d’utiliser :
Corrigé étape 5 sans interface graphique (à exploiter selon la procédure Construire un projet C++ avec cmake).
Corrigé étape 5 avec Dear Imgui (à exploiter selon la procédure Construire un projet C++ avec cmake).
Un outil d’édition de dessins vectoriel comme Adobe Illustrator propose la notion de symbole réutilisable. Leur aide en ligne explique : « Un symbole est un objet artistique que vous pouvez réutiliser dans un document. Par exemple, si vous créez un symbole à partir d’une fleur, vous pouvez ajouter plusieurs instances de ce symbole à l’image sans devoir effectivement ajouter l’objet artistique complexe à maintes reprises. »
Dans le cas de cette dernière étape, nous aimerions ajouter des yeux à notre dessin. Pour ce faire, nous souhaitons :
"oeil"
contenant 2 Circle
:
Circle
de label "sclere"
(car c’est
ainsi que s’appelle le blanc des yeux), de centre (0,0)
, de
rayon 30
et de couleur "White"
Circle
de label "iris"
, de centre
(0,0)
, de rayon 15
et de couleur
"Black"
Drawing
, avoir un Group
"yeux"
de coordonnées (0,70)
contenant :
"oeil"
, de label
"oeil_gauche"
et de coordonnées (-70,0)
"oeil"
, de label
"oeil_droit"
et de coordonnées (70,0)
"oeil_gauche"
et
"oeil_droit"
en ajoutant la ligne
d.find_shape_by_label("iris")->translate(0, -10);
à la
fin de modify_drawing(Drawing &d)
dans
myMain.cpp
.Quels changements apporter :
unique_ptr
.shared_ptr
(cette version permet de
libérer la mémoire contenant la définition d’un symbole réutilisable
quand le Drawing
ne contient plus aucune référence à ce
symbole).Corrigé à exploiter de la manière suivante :
unique_ptr
:
src/code/symbols_and_intelligent_pointers.h
,
donnez la valeur 1
à la constante de pré-compilation
SYMBOL_USES_UNIQUE_PTR_AND_NOT_SHARED_PTR
src/code/CMakeLists.txt
, l’instruction
add_library
doit contenir le nom de fichier
DrawingVersionUniquePtr.cpp
(et non
DrawingVersionSharedPtr.cpp
)shared_ptr
:
src/code/symbols_and_intelligent_pointers.h
,
donnez la valeur 0
à la constante de pré-compilation
SYMBOL_USES_UNIQUE_PTR_AND_NOT_SHARED_PTR
src/code/CMakeLists.txt
, l’instruction
add_library
doit contenir le nom de fichier
DrawingVersionSharedPtr.cpp
(et non
DrawingVersionUniquePtr.cpp
)