Aller au menu - Aller au contenu

[Plan du site] Vous êtes ici --- > Le Site du Zéro > Les tutoriels > Officiels > Programmation > Apprenez à programmer en C++ ! > [Pratique] Créez vos propres fenêtres avec Qt > TP : ZeroClassGenerator > Lecture du tutoriel

TP : ZeroClassGenerator

Avatar
Auteur : M@teo21
Note : 18 / 20 (8 votes)
Visualisations : 31 172

Plus d'informations Plus d'informations
Je pense que le moment est bien choisi pour vous exercer avec un petit TP. En effet, vous avez déjà vu suffisamment de choses sur Qt pour être en mesure de faire déjà des programmes intéressants.

Quand je me suis dit "Je vais leur faire faire un TP", les idées de sujet ne manquaient pas... mais elles étaient toutes un peu "bateau". J'ai pensé à une calculatrice par exemple, et en effet pourquoi pas, mais ce n'était pas très original.

Finalement, après réflexion, j'ai trouvé une idée qui sort un peu de l'ordinaire et qui pourra même vous être utile à vous, programmeurs. ;)

Notre programme s'intitulera le ZeroClassGenerator... un programme qui génère le code de base des classes C++ automatiquement en fonction des options que vous choisissez !
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Notre objectif

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 :

ZeroClassGenerator


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 :



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.

Pour le champ "Date de création", je vous propose d'utiliser un QDateEdit. Ce widget n'a pas été vu dans le chapitre précédent mais je vous fais confiance, il est proche de la QSpinBox et après lecture de la doc vous devriez savoir vous en servir sans problème.


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 !

Correction

Ding !

C'est l'heure de ramasser les copies. :diable:

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



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 :

Héritage et passage de paramètre


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 :


Des idées d'améliorations

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 :


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. :diable:

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 !

J'espère que le sujet de ce TP vous a plu, j'ai essayé de trouver quelque chose d'original et d'éviter les sujets habituels comme "Faire une calculatrice".
Ceci étant, si vous avez envie de faire une calculatrice, surtout foncez ! Plus vous pratiquez, plus vous progressez (même si vous êtes parfois confrontés à des difficultés). C'est une règle immuable.

Parmi les TP sur lesquels vous pourriez vous entraîner à votre niveau, je vous suggère de regarder du côté de ce qu'on avait fait dans le cours de C en console. C'est l'occasion idéale de faire évoluer ces programmes de la console au GUI :



Amusez-vous bien. :)
Chapitre précédent Sommaire Chapitre suivant
Retour en haut Retour en haut


Créé : le 18/09/2007 à 17:13:58
Modifié : le 23/10/2008 à 14:17:50
Avancement : 100%
Licence : Copie non autorisée

Changer de design | En savoir plus | Plan du site | Politique d'accessibilité | Règles | RSS tutoriels | RSS news
Édité par Simple IT SARL : Nous contacter | Notre blog | Revue de presse | Publicité

Y'a plus rien à lire, faut remonter maintenant !

Hébergement web - Correction de tutoriels - Créer un site
Vous souhaitez apparaître ici ? Contactez-nous.

Nombre de connectés 756 Zéros connectés | Requêtes SQL 8 requêtes | Temps de génération de la page : Total (SQL) 0.2272s (0.2122s)