Aller au contenu

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
# Makefile pour Gtkmm - version avec modules
#
# CC BY-SA Edouard.Thiel@univ-amu.fr - 23/10/2024
#
# Pour tout compiler, tapez : make all
# Pour tout compiler en parallèle, tapez : make -j all
# pour supprimer les .o et l'exécutable : make clean
# Pour tout recompiler : make clean all

SHELL   = /bin/bash
CPP     = g++
RM      = rm -f
CFLAGS  = $$(pkg-config gtkmm-3.0 --cflags) -Wall -O2 --std=c++17  # -g pour gdb
LIBS    = $$(pkg-config gtkmm-3.0 --libs)

# Fichiers à compiler :
CFILES  := $(wildcard *.cpp)
OBJS    := $(CFILES:%.cpp=%.o)

# Nom de l'exécutable à produire :
EXEC1   := domino

# Règle pour fabriquer les .o à partir des .cpp
%.o : %.cpp
	$(CPP) $(CFLAGS) -c $*.cpp

# Déclaration des cibles factices
.PHONY : all clean

# Règle pour tout compiler
all : $(EXEC1)

# Règle de production de l'exécutable
$(EXEC1) : $(OBJS)
	$(CPP) -o $@ $^ $(LIBS)

# Règle de nettoyage - AUTOCLEAN
clean :
	$(RM) *.o *~ $(EXEC1) tmp*.*

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


  1. Voir complément de cours à la fin de la question