Ne vous laissez pas impressionner par le nom "
ZeroClassGenerator". Ce TP ne sera pas bien difficile et réutilisera toutes les connaissances que vous avez apprises pour les mettre à profit dans un projet concret.
Ce TP est volontairement modulaire : je vais vous proposer de réaliser un programme de base assez simple, que je vous laisserai coder et que je corrigerai ensuite avec vous. Puis, je vous proposerai un certain nombre d'améliorations intéressantes (non corrigées) pour lesquelles il faudra vous creuser un peu plus les méninges si vous êtes motivés.
Notre ZeroClassGenerator est
un programme qui génère le code de base des classes C++. Qu'est-ce que ça veut dire ?
Un générateur de classe C++
Ce programme est un outil graphique qui va créer automatiquement le code source d'une classe en fonction des options que vous aurez choisies.
Vous n'avez jamais remarqué que les classes avaient en général une structure de base similaire qu'il fallait réécrire à chaque fois ? C'est un peu laborieux parfois. Par exemple :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | #ifndef HEADER_MAGICIEN
#define HEADER_MAGICIEN
class Magicien : public Personnage
{
public:
Magicien();
~Magicien();
protected:
private:
};
#endif
|
Rien que ça, ça serait bien si on avait un programme capable de générer le squelette de la classe, de définir les portées public, protected et private, de définir un constructeur par défaut et un destructeur, etc.
Nous allons réaliser un GUI (une fenêtre) contenant plusieurs options. Plutôt que de faire une longue liste, je vous propose une capture d'écran du programme final à réaliser :
La fenêtre principale est en haut à gauche, en arrière-plan. L'utilisateur renseigne obligatoirement le champ "Nom", pour indiquer le nom de la classe. Il peut aussi donner le nom de la classe mère.
On propose quelques cases à cocher pour choisir des options comme "Protéger le header contre les inclusions multiples" (la fameuse technique du #ifndef, pratique mais un peu lourde à écrire à chaque fois). Il faudra que le nom du define soit généré automatiquement à partir du nom de la classe, et mis en majuscules. Pour la mise en majuscules, renseignez-vous auprès de la doc de la classe
QString qui propose plein de choses.
Enfin, on donne la possibilité d'ajouter des commentaires en haut du fichier pour indiquer quel est l'auteur, quelle est la date de création et quel est le rôle de la classe. C'est une bonne habitude en effet que de commenter un peu le début de ses classes pour que l'on ait une idée de ce à quoi elle sert.
Lorsqu'on clique sur le bouton "Générer" en bas, une nouvelle fenêtre s'ouvre (une QDialog). Elle affiche le code généré dans un QTextEdit, et vous pouvez à partir de là copier/coller ce code dans votre IDE comme Code::Blocks.
C'est un début, et je vous proposerai à la fin du chapitre des améliorations intéressantes à ajouter à ce programme. Essayez déjà de réaliser ça correctement, ça représente un peu de travail je peux vous le dire !
Quelques conseils techniques
Avant de vous lâcher tels des fauves dans la jungle, je voudrais vous donner quelques conseils techniques pour vous guider un peu.
Architecture du projet
Je vous recommande de faire une classe par fenêtre. Comme on a 2 fenêtres, et qu'on met toujours le main à part, ça fait 5 fichiers :
- main.cpp : contiendra uniquement le main qui ouvre la fenêtre principale (très court).
- FenPrincipale.h : header de la fenêtre principale.
- FenPrincipale.cpp : l'implémentation des méthodes de la fenêtre principale.
- FenCodeGenere.h : le header de la fenêtre secondaire qui affiche le code généré.
- FenCodeGenere.cpp : ... et l'implementation de ses méthodes.
Pour la fenêtre principale, vous pourrez hériter de QWidget comme on l'a toujours fait, ça me semble le meilleur choix.
Pour la fenêtre secondaire, je vous conseille d'hériter de QDialog. La fenêtre principale ouvrira la QDialog en appelant sa méthode exec().
La fenêtre principale
Je vous conseille très fortement d'utiliser des layouts. Mon layout principal, si vous regardez bien ma capture d'écran, est un layout vertical. Il contient des QGroupBox.
A l'intérieur des QGroupBox, j'utilise à nouveau des layouts. Je vous laisse le choix du layout qui vous semble le plus adapté à chaque fois.
Pour le QGroupBox "Ajouter des commentaires", il faudra ajouter une case à cocher. Si cette case est cochée, les commentaires seront ajoutés. Sinon, on ne mettra pas de commentaires. Renseignez-vous sur l'utilisation des cases à cocher dans les QGroupBox.
Vous "dessinerez" le contenu de la fenêtre dans le constructeur de FenPrincipale. Pensez à faire de vos champ de formulaire des attributs de la classe (les QLineEdit, QCheckbox...), afin que toutes les autres méthodes de la classe aient accès à leur valeur.
Lors d'un clic sur le bouton "Générer !", appelez un slot personnalisé. Dans ce slot personnalisé (qui ne sera rien d'autre qu'une méthode de FenPrincipale), vous récupèrerez toutes les infos contenues dans les champs de la fenêtre pour générer le code dans une chaîne de caractères (de type QString de préférence).
C'est là qu'il faudra un peu réfléchir sur la génération du code, mais c'est tout à fait faisable.
Une fois le code généré, votre slot appellera la méthode exec() d'un objet de type FenCodeGenere que vous aurez créé pour l'occasion. La fenêtre du code généré s'affichera alors...
La fenêtre du code généré
Beaucoup plus simple, cette fenêtre est constituée d'un QTextEdit et d'un bouton de fermeture.
Pour le QTextEdit, essayez de définir une police à pas fixe (comme "Courier") pour que ça ressemble à du code (parce que le Times New Roman pour rédiger du code c'est moche

). Personnellement, j'ai rendu le QTextEdit en mode readOnly pour qu'on ne puisse pas modifier son contenu (juste le copier), mais vous faites comme vous voulez.
Vous connecterez le bouton "Fermer" à un slot spécial de la QDialog qui demande la fermeture et qui indique que tout s'est bien passé. Je vous laisse trouver dans la doc duquel il s'agit.
Minute euh... Comment je passe le code généré (de type QString si j'ai bien compris) à la seconde fenêtre de type QDialog ?
Le mieux est de passer cette QString en paramètre du constructeur. Votre fenêtre récupèrera ainsi le code et n'aura plus qu'à l'afficher dans son QTextEdit !
Allez hop hop hop, au boulot, à vos éditeurs ! Vous aurez besoin de lire la doc plusieurs fois pour trouver la bonne méthode à appeler à chaque fois, donc n'ayez pas peur d'y aller.
On se retrouve dans la partie suivante pour la... correction !
Ding !
C'est l'heure de ramasser les copies.
Bien que je vous aie donné quelques conseils techniques, je vous ai volontairement laissé le choix pour certains petits détails (comme "quelles cases sont cochées par défaut"). Vous pouviez même présenter la fenêtre un peu différemment si vous vouliez.
Tout ça pour dire que ma correction n'est pas la correction ultime. Si vous avez fait différemment, ce n'est pas grave. Si vous n'avez pas réussi, ce n'est pas grave non plus, pas de panique : prenez le temps de bien lire mon code et d'essayer de comprendre ce que je fais. Vous devrez être capable par la suite de refaire ce TP sans regarder la correction.
main.cpp
Comme prévu, ce fichier est tout bête et ne mérite même pas d'explication.
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12 | #include <QApplication>
#include "FenPrincipale.h"
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
FenPrincipale fenetre;
fenetre.show();
return app.exec();
}
|
Je signale juste qu'on aurait pu charger la langue française comme on l'avait fait dans le chapitre sur les boîtes de dialogue, afin que les menus contextuels et certains boutons automatiques soient traduits en français. Mais c'est du détail, ça ne se verra pas vraiment sur ce projet.
FenPrincipale.h
La fenêtre principale hérite de QWidget comme prévu. Elle utilise la macro Q_OBJECT car nous définissons un slot personnalisé :
Code : C++ 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 | #ifndef HEADER_FENPRINCIPALE
#define HEADER_FENPRINCIPALE
#include <QtGui>
class FenPrincipale : public QWidget
{
Q_OBJECT
public:
FenPrincipale();
private slots:
void genererCode();
private:
QLineEdit *nom;
QLineEdit *classeMere;
QCheckBox *protections;
QCheckBox *genererConstructeur;
QCheckBox *genererDestructeur;
QGroupBox *groupCommentaires;
QLineEdit *auteur;
QDateEdit *date;
QTextEdit *role;
QPushButton *generer;
QPushButton *quitter;
};
#endif
|
Ce qui est intéressant, ce sont tous les champs de formulaire que j'ai mis en tant qu'attributs (privés) de la classe. Il faudra les initialiser dans le constructeur. L'avantage d'avoir défini les champs en attributs, c'est que toutes les méthodes de la classe y auront accès, et ça nous sera bien utile pour récupérer les valeurs des champs dans notre méthode qui génèrera le code source.
Notre classe est constituée de 2 méthodes, ce qui est ici largement suffisant :
- FenPrincipale() : c'est le constructeur. Il initialisera les champs de la fenêtre, jouera avec les layouts et placera les champs à l'intérieur. Il fera des connexions entre les widgets et indiquera la taille de la fenêtre, son titre, son icône...
- genererCode() : c'est une méthode (plus précisément un slot) qui sera connectée au signal "Le bouton Générer a été cliqué". Dès qu'on cliquera sur le bouton, cette méthode sera appelée.
J'ai mis le slot en privé car il n'y a pas de raison qu'une autre classe l'appelle, mais j'aurais aussi bien pu le mettre public.
FenPrincipale.cpp
Bon là c'est le plus gros morceau. Il n'y a que 2 méthodes mais elles sont grosses, ne vous laissez pas impressionner pour autant.
Prenez le temps de bien les comprendre.
Code : C++ 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140 | #include "FenPrincipale.h"
#include "FenCodeGenere.h"
FenPrincipale::FenPrincipale()
{
// Groupe : Définition de la classe
nom = new QLineEdit;
classeMere = new QLineEdit;
QFormLayout *definitionLayout = new QFormLayout;
definitionLayout->addRow("&Nom :", nom);
definitionLayout->addRow("Classe &mère :", classeMere);
QGroupBox *groupDefinition = new QGroupBox("Définition de la classe");
groupDefinition->setLayout(definitionLayout);
// Groupe : Options
protections = new QCheckBox("Protéger le &header contre les inclusions multiples");
protections->setChecked(true);
genererConstructeur = new QCheckBox("Générer un &constructeur par défaut");
genererDestructeur = new QCheckBox("Générer un &destructeur");
QVBoxLayout *optionsLayout = new QVBoxLayout;
optionsLayout->addWidget(protections);
optionsLayout->addWidget(genererConstructeur);
optionsLayout->addWidget(genererDestructeur);
QGroupBox *groupOptions = new QGroupBox("Options");
groupOptions->setLayout(optionsLayout);
// Groupe : Commentaires
auteur = new QLineEdit;
date = new QDateEdit;
date->setDate(QDate::currentDate());
role = new QTextEdit;
QFormLayout *commentairesLayout = new QFormLayout;
commentairesLayout->addRow("&Auteur :", auteur);
commentairesLayout->addRow("Da&te de création :", date);
commentairesLayout->addRow("&Rôle de la classe :", role);
groupCommentaires = new QGroupBox("Ajouter des commentaires");
groupCommentaires->setCheckable(true);
groupCommentaires->setChecked(false);
groupCommentaires->setLayout(commentairesLayout);
// Layout : boutons du bas (générer, quitter...)
generer = new QPushButton("&Générer !");
quitter = new QPushButton("&Quitter");
QHBoxLayout *boutonsLayout = new QHBoxLayout;
boutonsLayout->setAlignment(Qt::AlignRight);
boutonsLayout->addWidget(generer);
boutonsLayout->addWidget(quitter);
// Définition du layout principal, du titre de la fenêtre, etc.
QVBoxLayout *layoutPrincipal = new QVBoxLayout;
layoutPrincipal->addWidget(groupDefinition);
layoutPrincipal->addWidget(groupOptions);
layoutPrincipal->addWidget(groupCommentaires);
layoutPrincipal->addLayout(boutonsLayout);
setLayout(layoutPrincipal);
setWindowTitle("Zero Class Generator");
setWindowIcon(QIcon("icone.png"));
resize(400, 450);
// Connexions des signaux et des slots
connect(quitter, SIGNAL(clicked()), qApp, SLOT(quit()));
connect(generer, SIGNAL(clicked()), this, SLOT(genererCode()));
}
void FenPrincipale::genererCode()
{
// On vérifie que le nom de la classe n'est pas vide, sinon on arrête
if (nom->text().isEmpty())
{
QMessageBox::critical(this, "Erreur", "Veuillez entrer au moins un nom de classe");
return; // Arrêt de la méthode
}
// Si tout va bien, on génère le code
QString code;
if (groupCommentaires->isChecked()) // On a demandé à inclure les commentaires
{
code += "/*\nAuteur : " + auteur->text() + "\n";
code += "Date de création : " + date->date().toString() + "\n\n";
code += "Rôle :\n" + role->toPlainText() + "\n*/\n\n\n";
}
if (protections->isChecked())
{
code += "#ifndef HEADER_" + nom->text().toUpper() + "\n";
code += "#define HEADER_" + nom->text().toUpper() + "\n\n\n";
}
code += "class " + nom->text();
if (!classeMere->text().isEmpty())
{
code += " : public " + classeMere->text();
}
code += "\n{\n public:\n";
if (genererConstructeur->isChecked())
{
code += " " + nom->text() + "();\n";
}
if (genererDestructeur->isChecked())
{
code += " ~" + nom->text() + "();\n";
}
code += "\n\n protected:\n";
code += "\n\n private:\n";
code += "};\n\n";
if (protections->isChecked())
{
code += "#endif\n";
}
// On crée puis affiche la fenêtre qui affichera le code généré, qu'on lui envoie en paramètre
FenCodeGenere *fenetreCode = new FenCodeGenere(code, this);
fenetreCode->exec();
}
|
Vous noterez que j'appelle directement la méthode connect(), au lieu d'écrire QWidget::connect(). En effet, si on est dans une classe qui hérite de QWidget (et c'est le cas), on peut se passer de mettre le préfixe "QWidget::".
Pour le constructeur, je pense ne rien avoir à ajouter, je ne fais rien de bien nouveau. Il faut juste être organisé parce qu'il y a pas mal de lignes pour générer la fenêtre.
Par contre, le slot genererCode a demandé du travail, même s'il n'est pas si compliqué que ça au final. Il récupère la valeur des champs de la fenêtre (via des méthodes comme text() pour les QLineEdit). J'ai dû lire la doc plusieurs fois pour chacun de ces widgets afin de savoir comment récupérer le texte, la valeur (si la case est cochée ou pas), etc. Là, c'est juste de la lecture de la doc.
Une QString
code se génère en fonction des choix que vous avez fait.
Une erreur se produit et la méthode s'arrête s'il n'y a pas au moins un nom de classe défini.
Tout à la fin de genererCode(), on n'a plus qu'à appeler la fenêtre secondaire et à lui envoyer le code généré :
Code : C++1
2 | FenCodeGenere *fenetreCode = new FenCodeGenere(code, this);
fenetreCode->exec();
|
Le code est envoyé lors de la construction de l'objet. La fenêtre sera affichée lors de l'appel à exec().
FenCodeGenere.h
La fenêtre du code généré est beaucoup plus simple que sa parente :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | #ifndef HEADER_FENCODEGENERE
#define HEADER_FENCODEGENERE
#include <QtGui>
class FenCodeGenere : public QDialog
{
public:
FenCodeGenere(QString &code, QWidget *parent);
private:
QTextEdit *codeGenere;
QPushButton *fermer;
};
#endif
|
Il y a juste un constructeur et deux petits widgets de rien du tout.
FenCodeGenere.cpp
Le constructeur prend 2 paramètres :
- Une référence vers la QString qui contient le code.
- Un pointeur vers la fenêtre parente.
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | #include "FenCodeGenere.h"
FenCodeGenere::FenCodeGenere(QString &code, QWidget *parent = 0) : QDialog(parent)
{
codeGenere = new QTextEdit();
codeGenere->setPlainText(code);
codeGenere->setReadOnly(true);
codeGenere->setFont(QFont("Courier"));
codeGenere->setLineWrapMode(QTextEdit::NoWrap);
fermer = new QPushButton("Fermer");
QVBoxLayout *layoutPrincipal = new QVBoxLayout;
layoutPrincipal->addWidget(codeGenere);
layoutPrincipal->addWidget(fermer);
resize(350, 450);
setLayout(layoutPrincipal);
connect(fermer, SIGNAL(clicked()), this, SLOT(accept()));
}
|
C'est un rappel, mais je pense qu'il ne fera pas de mal : le paramètre
parent est transféré au constructeur de la classe-mère QDialog dans cette ligne :
Code : C++1 | FenCodeGenere::FenCodeGenere(QString &code, QWidget *parent = 0) : QDialog(parent)
|
Schématiquement, le transfert se fait comme ceci :
Je pense que s'il y avait juste ça vous comprendrez tous :
Code : C++1 | FenCodeGenere::FenCodeGenere(QString &code, QWidget *parent = 0)
|
La nouveauté (enfin, on en a parlé dans le chapitre sur l'héritage quand même

), c'est qu'on appelle aussi le constructeur de la classe-mère QDialog et on lui transfère le paramètre parent avec le code
: QDialog(parent)
Pourquoi avoir appelé le constructeur de QDialog et pourquoi lui avoir envoyé en paramètre un pointeur vers la fenêtre mère ?
Même si ce n'est pas obligatoire en fait, il est conseillé lors de la création d'une QDialog d'indiquer quelle est la fenêtre mère. La QDialog se centre automatiquement par rapport à la fenêtre mère, entre autres choses.
Télécharger le projet
Vous pouvez aussi télécharger le projet zippé :
Ce zip contient :
- Les fichiers source .cpp et .h
- Le projet .cbp pour ceux qui utilisent Code::Blocks
- L'exécutable Windows et son icône. Attention, il faudra mettre les DLL de Qt dans le même dossier si vous voulez que le programme puisse s'exécuter.
Vous pensiez en avoir fini ?
Que nenni ! Un tel TP n'attend qu'une seule chose : être amélioré !
Voici une liste de suggestions qui me passent par la tête pour améliorer le ZeroCodeGenerator, mais vous pouvez inventer les vôtres :
- Lorsqu'on coche "Protéger le header contre les inclusions multiples", un define (aussi appelé "header guard") est généré. Par défaut, ce header guard est de la forme HEADER_NOMCLASSE. Pourquoi ne pas l'afficher en temps réel dans un libellé lorsqu'on tape le nom de la classe ? Ou, mieux, affichez-le en temps réel dans un QLineEdit pour que la personne puisse le modifier si elle le désire.
Le but est de vous faire travailler les signaux et les slots.
- Ajoutez d'autres options de génération de code. Par exemple, vous pouvez proposer d'inclure le texte légal d'une licence libre (comme la GPL) dans les commentaires d'en-tête si la personne fait un logiciel libre, vous pouvez demander quels headers inclure, la liste des attributs, générer automatiquement les accesseurs pour ces attributs, etc. Attention, il faudra peut-être utiliser des widgets de liste un peu plus complexes, comme le QListWidget. Je ne vous l'ai pas encore présenté, mais rien ne vous interdit de prendre de l'avance.

- Pour le moment on ne génère que le code du fichier .h. Même s'il y a moins de travail, ça serait bien de générer aussi le .cpp. Je vous propose d'utiliser un QTabWidget (des onglets) pour afficher le code .h et le .cpp dans la boîte de dialogue du code généré.
- On ne peut que voir et copier / coller le code généré. C'est bien, mais comme vous je pense que si on pouvait enregistrer le résultat dans des fichiers ce serait du temps de gagné pour l'utilisateur. Je vous propose d'ajouter dans la QDialog un bouton pour sauvegarder dans des fichiers.
Ce bouton ouvrira une fenêtre qui demandera dans quel dossier enregistrer les fichiers .h et .cpp. Le nom de ces fichiers sera automatiquement généré en fonction du nom de la classe.
Pour l'enregistrement dans des fichiers, regardez du côté de la classe QFile. Bon courage. 
- C'est un détail, mais les menus contextuels (quand on fait un clic droit sur un champ de texte par exemple) sont en anglais. Je vous avais parlé dans un des chapitres précédents d'une technique permettant de les avoir en français, un code à placer au début du main(). Je vous laisse le retrouver !
- On vérifie si le nom de la classe n'est pas vide, mais on ne vérifie pas s'il contient des caractères invalides (comme un espace, des accents, des guillemets...). Il faudrait afficher une erreur si le nom de la classe n'est pas valide.
Pour valider le texte saisi, vous avez 2 techniques : utiliser un inputMask(), ou un validator(). L'inputMask est peut-être le plus simple des deux, mais ça vaut le coup d'avoir pratiqué les deux. Pour savoir faire ça, direction la doc de QLineEdit.
Voilà pour un petit début d'idées d'améliorations. Il y a déjà de quoi faire pour que vous ne puissiez pas dormir pendant quelques nuits, gnark gnark gnark.
Comme toujours pour les TP, si vous êtes bloqués rendez-vous sur les forums du Site du Zéro pour demander de l'aide. Bon courage à tous !