Programmation C++ : TP séance 03
- Rendu du TP sur Ametice :
- Suivez les instructions pour le rendu du TP en mettant le bon numéro de séance dans le nom du répertoire.
- N'oubliez pas de rendre votre travail à la fin de la séance comme indiqué, même si vous n'avez pas fini la planche de TP, puis de rendre la version finale avant le début de la séance suivante.
Exercice 1. Jeu de Dominos en GTKmm, modules, vectors
On se propose de réaliser un plateau de jeu de dominos, à partir du programme de dessin en Cairo avec GTKmm effectué au TP précédent.
1.a. Déclaration séparée
Nous allons découper le programme du TP précédent en plusieurs modules, et nous
procéderons en 2 étapes : séparation des déclarations, puis découpe en modules.
Pour compiler les modules, nous aurons besoin d'un nouveau
Makefile
, qui compilera séparément chaque module puis construira un exécutable
en faisant l'édition de liens.
Dans un nouveau répertoire, copier le Makefile
suivant ; vérifier que les
indentations sont bien des caractères tabulation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
Copier ensuite dans ce répertoire le fichier domino.cpp
obtenu à la fin de la
planche du TP02. Pour chacune des classes du programme :
- déplacer chaque méthode après la déclaration de la classe avec le préfixe de résolution de portée ;
- faire une déclaration forward dans la classe pour chaque méthode ;
- vérifier que le programme compile et fonctionne normalement.
Taille des lignes de code
Pensez à limiter la taille de vos lignes de code à (environ) 80 caractères, en alignant lorsque nécessaire (par exemple des tests à rallonge) de manière à en faciliter la relecture.
1.b. Découpe en modules
Créer les fichier utils.hpp
et utils.cpp
.
Placer #pragma once
au début de utils.hpp
, comme vu
dans le cours,
puis déplacer les éléments suivants de domino.cpp
vers ce fichier :
l'inclusion de gtkmm.h
, la macro CONNECT
, et la définition de CairoContext
.
Enfin, inclure utils.hpp
au début de domino.cpp
. Tester la compilation.
Étant donné que ce Makefile
détecte les modifications des fichiers .cpp
,
mais pas celles des fichiers .hpp
, dans la suite il sera plus prudent,
chaque fois qu'un fichier .hpp
sera modifié,
de nettoyer d'abord le répertoire en tapant make clean
avant de compiler.
Pour chaque classe, créer un fichier .hpp
et .cpp
portant le nom de la
classe, en minuscules, en séparant les mots par un caractère souligné _
,
et de plus un fichier app.cpp
.
On devrait obtenir :
$ make clean
$ ls -1
app.cpp
domino.cpp
domino.hpp
main_window.cpp
main_window.hpp
Makefile
my_data.cpp
my_data.hpp
utils.cpp
utils.hpp
Tout le code étant encore dans domino.cpp
,
déplacer chaque déclaration de classe dans le fichier .hpp
correspondant
et l'implémentation des méthodes dans le fichier .cpp
; déplacer la fonction
main
dans le fichier app.cpp
, et placer dans utils.cpp
les fonctions
utilitaires éventuelles, par exemple le calcul de distances.
La dernière étape est le réglage des include
: en principe, chaque fichier
.cpp
devrait inclure le fichier .hpp
correspondant, et les fichiers
.hpp
contenant des types relatifs à GTKmm
ou à Cairo
incluront eux-même
le fichier utils.hpp
.
Il y aura peut-être d'autres inclusions à faire,
le principe étant de ne rajouter une inclusion que si elle est nécessaire au
fichier lui-même. Tester la compilation.
Compléments de cours :
Déclaration d'une constante pour les modules
Pour déclarer une constante de manière à ce qu'elle puisse être utilisée
dans plusieurs modules, il faut la déclarer extern
dans un fichier
.hpp
:
extern const int FOO;
puis la déclarer avec sa valeur dans le fichier .cpp
correspondant :
const int FOO = 1234;
Le symbole FOO
sera résolu à l'édition de lien pour les modules qui
l'utiliseront (à condition qu'ils aient inclus le fichier .hpp
qui
le défini comme extern
).
1.c. Plateau de jeu
Modifier le constructeur de la classe Domino
de manière à ce qu'il accepte en
paramètres optionnels les nombres de points a
et b
, et les mémorise en
utilisant la syntaxe de
member initializer list.
Créer dans des fichiers board.hpp
et board.cpp
une classe Board
,
dont le but sera de mémoriser les éléments du plateau de jeu.
Déclarer dans la classe Board
un vector
de Domino
nommé m_dominos
.
Ajouter une méthode populate_dominos
qui insère les 28 dominos à la fin
de m_dominos
.
Dans la méthode, modifier les coordonnées des dominos de manière à les placer
verticalement sur 4 rangées de 7 colonnes.
Appeler cette méthode dans le constructeur de Board
.
Ajouter une méthode draw_dominos
qui affiche tous les dominos de m_dominos
en appelant leur méthode draw
(penser à itérer par référence avec un range-for).
Dans la classe MyData
, supprimer l'objet membre m_domino1
de la classe Domino
,
et déclarer à la place un objet membre m_board
de la classe Board
.
Supprimer l'initialisation de m_domino1
dans le constructeur de MyData
.
Dans main_window.cpp
, remplacer l'appel de la méthode draw
de m_domino1
par l'appel de la méthode draw_dominos
de l'objet membre m_board
.
Mettre en commentaire les autres lignes où apparaît encore m_domino1
.
1.d. Déplacements
Supprimer dans la classe MyData
le flag booléen indiquant si le domino est
cliqué. Rajouter dans la classe Board
le membre entier m_domino_num_clicked
initialisé à -1
.
Dans la classe Board
rajouter une méthode find_domino_clicked
recevant
en paramètres les coordonnées réelles de la souris. La méthode détermine quel
est le domino cliqué, à l'aide de la méthode Domino::domino_is_clicked
.
Elle stocke dans m_domino_num_clicked
le numéro du premier domino qui réussi,
ou -1
s'il n'y en a aucun. Enfin elle renvoie true
si elle a trouvé un domino.
Dans la classe MainWindow
, lorsque le bouton est pressé,
appeler la méthode find_domino_clicked
.
Toujours dans la classe MainWindow
, lorsque le bouton est relâché :
si un domino était cliqué et si sa méthode rivet_is_clicked
renvoie true
,
alors rajouter 90 degrés (modulo 360) à son angle, et forcer le réaffichage ;
puis remettre systématiquement m_domino_num_clicked
à -1
.
De plus, dans la classe MainWindow
, lorsque la souris est tirée, déplacer
le domino qui a été cliqué (s'il y en a un), en utilisant l'approche qui
avait été employée pour m_domino1
.
Enfin, supprimer le code en commentaire, relatif à m_domino1
.
1.e. Ordre d'empilement
En testant l'interface on se rend tout de suite compte qu'il y a un problème si on superpose deux dominos : un des dominos est toujours au dessus, et c'est celui qui est dessous qui est détecté lorsqu'on le clique.
Nous allons corriger ce problème en gérant un ordre d'empilement :
- on mettra systématiquement au-dessus le domino cliqué, c'est-à-dire en
première position dans le vecteur
m_dominos
; - on affichera les dominos du dernier au premier (c'est l'algorithme du
peintre), donc les dominos du début du
vector
seront affichés par dessus (car après) les dominos de fin.
Comme la recherche du domino cliqué est effectuée par un range-for du premier au dernier, dans le cas de deux dominos superposés on détectera maintenant le domino qui est affiché dessus en premier.
Modifier la méthode Board::draw_dominos
de manière à ce qu'elle affiche
les dominos dans l'ordre décroissant.
Rajouter une méthode Board::move_domino_to_top
prenant en paramètre un numéro
domino_num
de domino dans m_dominos
. La méthode déplace le domino numéro
domino_num
en tête du vector
m_dominos
1. Appeler la méthode dans
MainWindow
lorsque la souris est cliquée sur un domino.
Bugs subtils : quand on déplace un élément en tête dans un vector
,
l'insertion en tête décale la case que l'on voudrait recopier ; d'autre part,
après appel de move_domino_to_top
, la valeur
m_domino_num_clicked
doit peut-être être mise à jour...
Enfin tester que l'on peut empiler des dominos et placer le dernier cliqué au dessus des autres.
Compléments de cours :
Insertion ou suppression dans un vector
Pour insérer ou supprimer un élément dans un vector
à la position i
,
on utilise les méthodes insert
et erase
de la façon suivante :
class Bar;
Bar bar1;
std::vector<Bar> bars;
bars.insert (bars.begin()+i, bar1); // pour insérer
bars.erase (bars.begin()+i); // pour supprimer
Dans les deux cas on utilise la méthode begin()
qui retourne un
pointeur sur le premier élément du vecteur ; on en reparlera plus loin
dans le cours sur les itérateurs.
-
Voir complément de cours à la fin de la question ↩