Réflexes C++ Builder 6

Gilles Louïse

Quelques réflexes à avoir pour utiliser au mieux C++ Builder en respectant au maximum la POO (Programmation Orientée Objet) ♪

- Titre : Réflexes C++ Builder 6

- Auteur : Gilles Louïse

- Parution : 1 juin 2002

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Gilles Louïse. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Trois noms : forme, unité, projet

- II. Noms des composants

- III. Application MDI ou SDI

- IV. Pas de variables générales

- V. Pas de fonctions isolées

- VI. La directive __fastcall

- VII. Initialisation de l'application

- VIII. New et delete

- IX. Zone fixe ou variable ?

- X. Paramètres de l'application

- XI. Les fichiers

- XII. Centralisation des fonctions de l'application

- XIII. La propriété Tag

- XIV. En résumé

I - Trois noms : forme, unité, projet

Avant de commencer un nouveau projet C++ Builder, ayez en tête trois noms :

- celui qu'aura la forme principale à la place de Form1 ;

- celui qu'aura l'unité C++ à la place d'unit1 ;

- celui qu'aura le projet dans son ensemble à la place de Projet1.

Une fois entré dans C++ Builder, modifiez la propriété Name de la fenêtre principale dans l'inspecteur d'objets puis sauvegardez immédiatement ce projet vide. Le nouveau nom donné à Unit1 sera celui du cpp et du header associé (Unit1.cpp et Unit1.h par défaut). Le nouveau nom donné à Projet1 sera celui du projet en tant que tel et aussi celui de l'exécutable final. Notez que C++ Builder ne veut pas que ces noms soient identiques, il faut donc penser trois noms différents ayant trait à la nature du projet. Les deux noms remplaçant Projet1 et Unit1 seront visibles dans le répertoire qui contiendra le projet alors que le nom de la forme principale sera visible dans le source C++ puisqu'au lieu d'une syntaxe du type Form1-> pour accéder aux composants situés sur cette fenêtre, vous y accéderez par une syntaxe du type Nouveau_Nom->.

Il est vrai qu'aussi longtemps que vous vous trouvez à l'intérieur de votre classe principale c'est-à-dire dans une méthode de cette classe, vous n'avez pas besoin de faire cette précision, mais elle deviendra nécessaire à l'extérieur. Ainsi, imaginons que vous avez appelé votre fenêtre principale Dessin (propriété Name=Dessin), la classe associée sera TDessin (la déclaration créée automatiquement par C++ Builder sera TDessin *Dessin), le nom de la classe est préfixé par la lettre T (Template) et le pointeur par lequel on accède aux membres de classe est le nom sans ce T initial. Toute méthode de cette classe, que sa déclaration ait été créée automatiquement par C++ Builder suite au double clic d'un événement dans la palette ou que vous ayez ajouté une méthode à vous dans cette classe, toute méthode de cette classe sera préfixée par TDessin:: ce qui fait que bien entendu la précision Dessin-> devient inutile à l'intérieur d'une méthode de cette classe. Mais si vous avez à adresser un membre quelconque (propriété ou méthode) de cette forme principale nommée Dessin dans une méthode d'une autre classe, la précision Dessin-> devient alors obligatoire (sinon le compilateur croira que vous cherchez à accéder à un membre de la classe en cours).

II - Noms des composants

D'une manière générale, dès que vous posez un composant sur une fenêtre, il est bon de modifier immédiatement sa propriété Name c'est-à-dire de donner un nom signifiant à ce composant, car c'est à partir de ce nom qu'on accédera à ses propriétés par programme. Pour une classe générique du type TComposant, le nom par défaut sera Composant1, c'est ce nom qu'il est préférable de changer dans l'inspecteur d'objets (propriété Name). Le source sera ainsi plus agréable à lire. Notez qu'il faut faire cette modification immédiatement après avoir déposé le composant sur la fenêtre, car si vous l'utilisiez par programme avec des syntaxes d'accès du type Composant1->, vous auriez plus de difficultés à modifier ce nom, car C++ Builder ne s'amuse pas à scruter le source après modification de la propriété Name pour mettre à jour votre document en changeant l'ancien nom par le nouveau. Il est donc toujours délicat de modifier un nom de composant a posteriori, ce pour quoi il est conseillé de changer le nom proposé par défaut avant de l'invoquer par programme. C++ Builder ne se charge que de changer le nom du pointeur correspondant au composant dans la classe associée située dans le header (Unit1.h par défaut).

III - Application MDI ou SDI

Vous devez savoir dès le départ de votre développement si votre application sera du type MDI (Multiple Document Interface) ou SDI (Single Document Interface). Ce détail se spécifie à la propriété FormStyle de la fenêtre principale. Voir mes Remarques, alinéa 57 pour plus de détails à ce sujet.

IV - Pas de variables générales

En principe, il n'y a aucune raison avec C++ Builder de déclarer des variables générales c'est-à-dire des variables qui seraient accessibles de n'importe où dans le source. Quand vous entrez dans C++ Builder, vous lisez dans Unit1.cpp

TForm1 *Form1;

Form1 est donc une variable générale, en l'occurrence ici un pointeur vers un objet de type TForm1, mais c'est une exception au sens où c'est le pointeur vers la forme principale (avec ici son nom par défaut). Si donc vous vouliez ajouter des variables générales, ce serait juste après cette déclaration du pointeur Form1. Mais les variables générales sont en contradiction avec la POO. Imaginons que nous déclarions un entier juste après TForm1 *Form1 par exemple int Nombre, ce nombre serait bien entendu accessible dans ce fichier source C++, mais il faudrait le déclarer externe dans un autre cpp par une déclaration du type extern int Nombre. Et ainsi de suite pour toute autre variable générale. Un programme devient ainsi vite illisible. Le mieux donc, pour respecter la POO, sera de déclarer les variables qui regardent l'ensemble du projet dans la classe de la fenêtre principale située dans le header associé (Unit1.h par défaut). Là, à l'intérieur de la classe composée automatiquement par C++ Builder, il y a une zone destinée à l'utilisateur prévue à cet effet. Ainsi, vous invoquerez cet entier par Form1->Nombre et vous n'aurez jamais d'externes à déclarer dans un autre cpp, il suffira, pour avoir accès à cette variable, d'avoir accès à la classe en général et donc que le header contenant la classe soit incluse au cpp. Donc, dans un autre cpp, il suffira qu'il y ait parmi les « include »

#include "Unit1.h"

Et vous aurez accès à la classe de la forme principale et donc à ses membres sans avoir à déclarer de variables externes. Pour insérer ce type d'include dans le source, on peut passer par la fonction Fichier|Inclure l'en-tête d'unité (Alt F11) du menu.

Imaginons que votre fenêtre principale s'appelle Musique (propriété Name). Dans cette classe TMusique, vous déclarez un entier NbPor (nombre de portées). Dans toute méthode de cette classe (donc préfixée par TMusique::) la variable NbPor sera directement accessible en écrivant simplement NbPor, car le compilateur en l'absence de précision cherche dans la classe courante. Mais à l'extérieur de la classe TMusique, il faudra écrire Musique->NbPor pour accéder à la variable.

V - Pas de fonctions isolées

Dans le même ordre d'idée, il n'y a aucune raison pour qu'une fonction soit isolée c'est-à-dire ne faisant pas partie d'une classe. D'ailleurs, si celle-ci était appelée dans un autre cpp, il faudrait la déclarer en externe, ce qui ne se fait pas en POO, en POO pure il n'y a pas d'externe, car tout est accessible dans des classes, il suffit que le header contenant la classe à invoquer fasse partie des « include » en début de cpp. Donc, faites toujours de vos fonctions des méthodes de classe. Si votre fonction est d'ordre général, déclarez-la dans la classe principale de l'application (située dans Unit1.h par défaut). Sinon insérez-la dans une classe concernée par ce que fait cette fonction.

Imaginons que votre fenêtre principale s'appelle Texte (propriété Name). Sa classe est donc du type TTexte (et toute méthode est préfixée par TTexte::) Vous voulez ajouter à la classe TTexte une fonction ayant pour nom Correct qui reçoit un caractère et un entier et renvoie un booléen. Sa déclaration dans la classe TTexte ressemblera à ceci :

bool __fastcall Correct(char, int);

Observez au passage que dans le prototype vous n'avez pas besoin de donner le nom des variables. C++ Builder, dans ses prototypes, ajoute par exemple « Sender » comme nom d'une variable de type TObject*, mais ce n'est pas obligatoire puisqu'il ne s'agit que d'un prototype. Dans le source C++, vous programmerez la fonction.

bool __fastcall TTexte::Correct(char c, int i)

{
/* corps de la méthode qui reçoit le caractère c
et l'entier i et renvoie un booléen */
}

Si vous appelez cette fonction depuis une autre méthode de la même classe, vous n'aurez qu'à donner son nom par exemple :

Rep=Correct(x,y);

Où Rep est supposé être un booléen, x un char et y un int. Mais si la méthode est appelée depuis l'extérieur donc dans une méthode d'une autre classe, il faudra préciser la classe à laquelle appartient la méthode et donc préfixer l'appel par Texte :

Rep=Texte->Correct(x,y);

La fonction Correct devient ainsi une méthode de votre classe principale au même titre que les méthodes déclarées automatiquement par C++ Builder.

VI - La directive __fastcall

Utilisez le plus souvent possible la directive __fastcall dans la déclaration de vos méthodes. Elle signifie simplement que la transcription en code machine, au moment de la compilation, sera optimisée par l'utilisation des registres internes du microprocesseur. En principe, cette directive est valide si le nombre d'arguments est inférieur ou égal à trois, que ce soient des nombres entiers, des pointeurs, des caractères, bref des éléments qui peuvent tenir sur 32 bits. Voyez l'aide en ligne pour plus de détails sur certaines restrictions. En principe, toute méthode ayant un nombre d'arguments inférieur ou égal à trois devrait être gratifiée de cette directive. Observez que C++ Builder lui-même ne déroge jamais à cette règle quand il crée pour vous la déclaration d'une fonction membre dans le cadre d'une gestion d'événements.

VII - Initialisation de l'application

En principe, on initialise l'application au moment du constructeur de la fiche principale (alinéa 15 de mes RemarquesRemarques). Si vous devez prendre la main avant la construction de la fiche principale (cas malgré tout rarissime), il faut donner un nom à InitProc (alinéa 60 de mes ). Il s'agit ici d'initialisations au sens général. Si vous avez à adresser des objets censés se trouver dans la fiche principale (Form1 par défaut) et d'une manière générale accéder à cette fenêtre principale avant son apparition, on utilise de préférence l'événement OnShow.

Si votre fenêtre principale s'appelle Dessin, C++ Builder créera pour vous le constructeur de cette classe au début du cpp, c'est là que vous pourrez initialiser votre application, par exemple mettre certaines variables à zéro et des pointeurs à NULL.

__fastcall TDessin::TDessin(TComponent* Owner)
: TForm(Owner)
{
// Initialisation de l'application
}

Notez au passage que c'est la seule méthode qui ne disparaît au moment de la compilation si elle est vide. Si vous cliquez par exemple un événement dans l'inspecteur d'objets, C++ Builder crée pour vous la déclaration de cette méthode dans le source, le curseur se trouve à l'intérieur des parenthèses, vous êtes ainsi invité à saisir les lignes de programme qui seront exécutées si cet événement a lieu à l'exécution. Mais si vous compilez cette unité sans rien entrer (Alt F9), C++ Builder voit que la méthode est vide et la supprime du programme à la seule exception des constructeurs des classes gérées par C++ Builder (donc notamment du constructeur de la classe principale) qui se maintiennent toujours à la compilation même s'ils ne contiennent rien. Mais l'expérience montre que le constructeur de la classe principale contiendra le plus souvent quelque chose puisque c'est là qu'on donne aux variables et pointeurs leurs valeurs de départ.

Notons également que cliquer un événement pour forcer C++ Builder à créer le gestionnaire d'événement correspondant est une bonne méthode pour connaître le prototype d'un événement. En effet, quand vous reliez par programme un événement à une méthode, vous êtes tenu de respecter le prototype de l'événement concerné. Par exemple, entrez dans C++ Builder, posez un bouton sur la fenêtre et double-cliquez dessus, C++ Builder vous crée le gestionnaire d'événement associé :

void __fastcall TForm1::Button1Click(TObject* Sender)
{
}

Cela signifie que cette méthode sera exécutée si l'utilisateur clique sur ce bouton. Mais cela signifie aussi que pour tout événement de type OnClick vous devrez respecter ce prototype à savoir l'envoi d'un pointeur du type TObject. C'est d'ailleurs le prototype le plus courant. Donc, imaginons que vous avez créé une classe Chose et qu'à l'intérieur vous avez déclaré un pointeur de composant de VCL. Appelons p ce pointeur. Au moment du constructeur de Chose, vous allez créer ce composant avec une syntaxe du type :

p= new TComposant(F);

Où F est le propriétaire de ce composant (la fenêtre principale ou autre).

Vous voulez agir en cas de clic sur cet objet, vous allez donc indiquer au composant la méthode à exécuter sur l'événement OnClick par une syntaxe du type :

p->OnClic=FonClic;

ce qui signifie que la méthode FonClic de la classe en cours sera exécutée sur clic du composant. Dans ces conditions, vous devez déclarer cette méthode FonClic dans votre classe, mais vous devez respecter le prototype de l'événement OnClic, votre classe Chose devra donc contenir cette déclaration :

void __fastcall FonClic(TObject*);

Vous respectez ici le prototype de l'événement OnClic, il ne vous restera qu'à écrire cette méthode :

void __fastcall Chose::FonClic(TObject* Sender)

{

}

Laquelle sera exécutée au moment du clic de cet objet créé par programme.

VIII - New et delete

On doit bannir définitivement la triade obsolète malloc/realloc/free du C et n'utiliser que les seuls new et delete du C++. On sait que le new n'a pas de realloc associé, on pallie cette difficulté en utilisant les composants purs POO de la VCL (Visual Components Library). Souvenez-vous qu'à new correspond delete (pour un seul élément) et qu'à new[] correspond delete[] pour une série, liste ou tableau d'éléments. Vous pouvez aussi utiliser la technique des listes chaînées (voir mon article à ce sujet), mais les composants purs POO de la VCL feront toujours l'affaire.

IX - Zone fixe ou variable ?

Chaque fois que vous avez à allouer de la mémoire pour quelque usage que ce soit, que ce soient des octets, des pointeurs, des nombres ou autres, il faut toujours vous poser la question de savoir si le nombre d'éléments à allouer est connu ou non. Par connu, on entend soit connu du programmeur, soit connu du programme. Si par exemple vous voulez créer dix objets, vous connaissez ce nombre, mais si vous voulez créer N objets, N ayant été saisi par l'utilisateur ou issu d'un calcul, vous ne connaissez pas, vous, la valeur de N mais le programme, lui, la connaît. Dans ce cas, on utilise invariablement l'instruction new[].

En revanche, si ce nombre n'est pas connu à l'avance ou, ce qui revient au même, s'il est susceptible de variations dans le temps, on utilise les composants de la VCL notamment AnsiString pour une zone mémoire variable dont la longueur peut changer à tout moment tant en plus qu'en moins, TList (alinéa 47 de mes Remarques) pour une liste de pointeurs, TStringList (alinéa 44 de mes Remarques) pour une série d'AnsiString ou encore TSmallIntArray pour une suite d'entiers (alinéa 43 de mes Remarques), pour ne citer que les principaux. Tous ces composants purs POO sont variables, on peut ajouter ou soustraire très facilement des éléments sans avoir à s'occuper de leur nombre ni à gérer la mémoire, c'est le composant lui-même en tant que tel qui se charge de tout, ils correspondent à des classes très complexes munies d'un très grand nombre de propriétés et de méthodes. Pour vous en rendre compte, entrez simplement à l'intérieur d'une fonction

AnsiString A;

Puis, juste après, entrez ce nom A suivi d'un point (accès à une méthode ou à une propriété de l'AnsiString A), C++ Builder vous affiche la liste impressionnante des possibilités, fonctions ou propriétés, liées à cette classe. Sinon, écrivez un mot clé, par exemple TList, mettez le curseur entre deux lettres de ce mot, faites F1 et consultez-les très nombreuses propriétés et méthodes disponibles dans la documentation. D'ailleurs, avant d'utiliser un composant qu'on connaît peu, il est excellent de s'informer de ses possibilités. Il suffit de sélectionner dans la palette le composant à interroger et de faire F1 pour obtenir l'aide en ligne correspondant à cette classe.

En résumé :

- new[]/delete[] pour tout ce qui est fixe ou connu à l'avance ;

- un composant adéquat de la VCL pour tout ce qui est variable ou inconnu.

oO pourrait nous objecter que dans tous les cas de figure on utilise new et delete. C'est vrai à l'exception toutefois des AnsiString (qui se déclarent comme tels et non via un pointeur, ce pour quoi, soit dit en passant, la syntaxe d'accès à ses membres est le point et non la flèche, la flèche signifiant que le composant a été créé par new via un pointeur). Je fais simplement ici une distinction entre des éléments standard du C++ et des composants. Quand le nombre d'éléments est connu on utilise new[] alors qu'avec des composants de la VCL on utilise new tout court sans crochets pour un seul composant, lequel nous fait accéder dès lors à la possibilité de créer une pluralité d'éléments dont le nombre n'est pas connu à l'avance. Ainsi, si vous avez besoin de 10 entiers, vous déclarerez d'abord un pointeur vers des entiers dans la classe qui gère ces nombres

int* Tab;

Puis vous créerez ces entiers par new[] le plus souvent dans le constructeur de la classe :

Tab = new int[10];

À ce stade, 10 entiers ont été créés, le premier est Tab[0] et le dernier Tab[9]. Et si vous en avez besoin de N, vous écrirez une syntaxe similaire :

Tab = new int[N];

Obtenant ainsi N entiers dont le premier est Tab[0] et le dernier Tab[N-1] qu'il faudra détruire le moment venu avec delete[] puisqu'ils ont été créés par new[] :

delete[] Tab;

Il en serait de même pour un nombre connu d'objets. Imaginons que vous avez une classe Chose et que vous vouliez créer N objets de ce type, vous déclarerez dans un premier temps un pointeur de pointeurs vers des éléments de type Chose :

Chose** TabChose;

Puis vous créerez N pointeurs vers un objet de type Chose :

TabChose = new Chose*[N];

À ce stade, vous avez N pointeurs de type Chose dont le premier est TabChose[0] et le dernier TabChose[N-1]. Il ne vous restera qu'à créer ces N objets par une boucle par exemple :

For(i=0;i<N;i++) TabChose[i]=Chose();

En appelant ici le constructeur par défaut.

Mais si vous vouliez créer un nombre d'objets non connu à l'avance, vous ne pourriez plus utiliser cette méthode, car le new[] instancie une suite fixe de pointeurs dont le nombre est connu à l'avance. Pour résoudre cette difficulté, on peut soit passer par la STL (Standard Template Library), mais puisque vous disposez d'un outil puissant à savoir C++ Builder, il est plus naturel d'utiliser les composants de la VCL notamment ici TList qui gère précisément une liste de pointeurs dont le nombre n'est pas connu et qui peut aussi bien augmenter que diminuer à tout moment. Ainsi, dans la classe qui gérerait ces objets (par exemple la classe principale), vous déclareriez un pointeur vers un objet de type TList :

TabChose* TList;

Puis dans le constructeur de cette classe, vous créeriez le composant en tant que tel via new (et non new[] et c'est là la différence que je fais avec les composants de la VCL) :

TabChose=new TList;

À ce stade, vous avez instancié un objet complexe de type TList par new, celui-ci est capable maintenant de recevoir un nouveau pointeur à tout moment, et ce, autant de fois que vous le voudrez, il suffit d'écrire :

TabChose->Add(new Chose());

Cette instruction crée un nouvel objet de type Chose avec le constructeur par défaut et ajoute à la liste TabChose le pointeur pointant ce nouvel objet. À tout moment le nombre de pointeurs disponibles sera TabChose->Count, le premier sera TabChose->Items[0] et le dernier sera TabChose->Items[TabChose->Count-1]. Cela dit, tous ces pointeurs sont du type void*, donc pour accéder aux objets il faudra convertir ce pointeur de void* à Chose* par une syntaxe du type (Chose*)TabChose->Items[i] qui est le pointeur d'indice i vers un objet de type Chose. Pour détruire l'objet d'indice i, on détruira d'abord l'objet en tant que tel donc

delete (Chose*)TabChose->Items[i];

Puis on supprimera ce pointeur dans la liste :

TabChose->Delete[i];

À la fin de l'exécution du programme, on détruira les objets restants :

For(i=0;i<TabChose->Count;i++) delete (Chose*)TabChose->Items[i];

Puis on détruira la liste de pointeurs de type Tlist :

delete TabChose;

X - Paramètres de l'application

Pour tout ce qui concerne les différents paramétrages de votre application, utilisez la classe TIniFile, c'est très simple et très pratique (alinéa 69 de mes Remarques). Pour chaque paramètre, il suffit de trouver un nom et de proposer une valeur par défaut. On enregistre très facilement de nouveaux paramètres dans le fichier INI. Et à la lecture, ou bien un paramètre a été trouvé dans le fichier INI et on renvoie ce qui a été lu, ou bien il n'a pas été trouvé et on vous renvoie la valeur par défaut que vous avez spécifiée, le tout en quelques instructions élémentaires (car c'est le composant lui-même qui s'occupe de tout).

XI - Les fichiers

N'utilisez plus les anciennes instructions du C pour manipuler des fichiers, mais utilisez la classe TMemoryStream (alinéa 71 de mes Remarques), TFileStream (voir documentation et rubriques connexes) ou encore la méthode LoadFromFile du composant utilisé que ce soit TMemo ou TRichEdit ou TStringList (alinéa 44 de mes Remarques) ou bien d'autres encore, c'est très simple et immédiat d'utilisation.

XII - Centralisation des fonctions de l'application

Utilisez le composant TActionList (alinéa 58 de mes Remarques) pour centraliser les actions du programme. Ce composant est très facile à utiliser et très pratique, car toute action (liée à clic d'un bouton, à un choix dans un menu, à une ouverture d'une fenêtre ou à toute autre circonstance) aura son adresse centralisée dans ce composant. Il suffira en général de renseigner la propriété Action du composant concerné par une des actions que vous aurez enregistrées dans cette liste pour que ça marche immédiatement. Mais tout événement de tout composant peut aussi se connecter à une action centralisée. Et la même action pourra être invoquée par un autre composant, soit en renseignant sa propriété Action, soit en renseignant l'événement incriminé dans la palette d'événements. D'ailleurs, dans tous les cas, les actions disponibles apparaissent spontanément dans la liste déroulante proposée en regard, qu'il s'agisse de la propriété Action du composant ou d'un événement quelconque géré par le composant.

XIII - La propriété Tag

Il est souvent indiqué d'utiliser la propriété Tag des composants. C'est simplement un nombre entier, mais il peut rendre de grands services, car il peut notamment représenter un indice dans un tableau.

Imaginons une application MDI musicale, la fenêtre principale s'appelle Musique. Cette application va pouvoir ouvrir plusieurs partitions dans des fenêtres enfants. Une des techniques possibles (ce n'est pas la seule, car la POO est très souple) consiste à créer une classe Partition, laquelle contiendra notamment un pointeur f de fenêtre enfant.

TForm *f;

Le constructeur de la classe va créer cette fenêtre enfant.

f=new TForm(Musique);
f->FormStyle=fsMDIChild;
f->Caption="Partition "+IntToStr(++Musique->NumPtt);
f->OnClose=Musique->Fermeture;
f->WindowState=wsMaximized;

On crée la fenêtre enfant en indiquant son propriétaire à savoir la fenêtre principale de l'application (f=new TForm(Musique);), on dit que c'est une fenêtre enfant (f->FormStyle=fsMDIChild;), on affiche le titre via la propriété Caption (f->Caption=« Partition »+IntToStr(++Musique->NumPtt);). On suppose que la classe principale TMusique contient un entier NumPtt (numéro de partition) et que ce nombre est initialisé à 0 au moment du constructeur de TMusique. On préincrémente donc ce nombre (++Musique->NumPtt) ce qui permet d'afficher « Partition 1 » la première fois, « Partition 2 » à la deuxième création et ainsi de suite. On renseigne la méthode à exécuter en cas d'événement OnClose (f->OnClose=Musique->Fermeture;), nous appelons ici Fermeture cette méthode de la classe principale TMusique. Puis on indique que la fenêtre sera maximalisée à sa création (f->WindowState=wsMaximized;).

Comme nous ne savons pas a priori combien de partitions vont être ouvertes simultanément, on utilise logiquement le composant TList qui contiendra les pointeurs de partitions créées. Ainsi, dans la classe principale, on déclare un pointeur vers un tel composant :

TList *Ptt;

Et dans le constructeur de cette classe principale, on instancie ce composant par new :

Ptt=new TList;

Dans ces conditions, nous pouvons créer à tout moment une nouvelle partition :

Ptt->Add(new Partition());

En appelant ici le constructeur par défaut. Maintenant, à chaque création de partition et à chaque fermeture de document, nous appelons une méthode AjusteTag. Cette méthode est déclarée dans la classe principale Tmusique :

void __fastcall AjusteTag();

Elle va ajuster tous les tags des fenêtres enfants, chaque Tag sera égal à l'indice de fenêtre accessible via Ptt->Items[i].

void __fastcall TMusique::AjusteTag()
{
for(int i=0;i<Ptt->Count;i++) ((Partition*)Ptt->Items[i])->f->Tag=i;
}

Ainsi, quel que soit f, on sait que f->Tag coïncide avec l'indice accessible via Ptt->Items[i]. Regardons maintenant ce qui se passe au moment de l'événement OnClose d'une fenêtre enfant.

void __fastcall TMusique::Fermeture(TObject *Sender, TCloseAction &Action)
{
int i=((TComponent*)Sender)->Tag;
delete (Partition*)Ptt->Items[i];
Ptt->Delete(i);
AjusteTag();
}

On note déjà que le prototype de l'événement OnClose est respecté (sinon le compilateur indiquera une erreur). Pour ce faire, nous avons cliqué sur l'événement OnClose de la fenêtre principale pour que C++ Builder nous crée le prototype correct.

void __fastcall TMusique::FormClose(TObject *Sender, TCloseAction &Action)

{

}

Puis nous avons dupliqué ce code en modifiant le nom de la méthode, on a donc remplacé le nom FormClose par Fermeture :

void __fastcall TMusique::Fermeture(TObject *Sender, TCloseAction &Action)

{

}

On est ainsi certain d'avoir respecté le prototype puisqu'il est la copie de ce que crée C++ Builder lui-même. Et, dans la classe principale TMusique, nous n'avons pas oublié de déclarer cette fonction.

void __fastcall Fermeture(TObject*, TCloseAction&);

Notez aussi que cette méthode est membre de la classe principale et non pas de la classe Partition. En effet, cette méthode est censée détruire la partition concernée et une méthode de classe ne peut pas détruire l'objet dont elle fait partie, ce serait une forme d'autodestruction impossible en POO (erreur abstraite), la destruction doit se faire à l'extérieur de la classe, on choisit donc logiquement la classe principale. La variable Sender représente en fait le pointeur de la fenêtre concernée par l'événement, c'est pourquoi nous lisons le Tag de cette fenêtre dans l'entier i (int i=((Component*)Sender)->Tag;). On sait maintenant qu'il s'agit de détruire la partition d'indice i (delete (Partition*)Ptt->Items[i];). La partition étant détruite, on supprime le pointeur dans la liste (Ptt->Delete(i);) puis on réajuste les Tags de manière à ce que tout f->Tag soit toujours égal à l'indice du Ptt->Items[i] associé.

La fenêtre va nécessairement contenir d'autres composants par exemple une PaintBox pour dessiner la partition et bien d'autres. À chaque composant susceptible d'être instancié dans la fenêtre f correspondra un pointeur dans la classe Partition.

TComposant *pc;

pc est un pointeur de composant quelconque. Dans le constructeur de la classe, on créera ce composant. Mais comme ce composant aura f pour propriétaire et f pour parent, il faudra le créer après f, car f doit déjà exister.

pc=new TComposant(f);
pc->Parent=f;

Ne pas confondre le propriétaire et le parent. Le propriétaire est chargé de détruire et donc de libérer la mémoire de tous les composants dont il est le propriétaire au moment de sa propre destruction alors que le parent indique où le composant doit s'afficher, notamment pc->Left et pc->Top représentent les coordonnées d'affichage par rapport au parent. Si ce composant est une PaintBox, celle-ci s'étendra sur la totalité de la zone client et on aura une instruction du genre pc->Align=alClient.

Mais le point le plus important est que f, en tant que propriétaire des composants, est chargé de leur destruction. Il en résulte que dans le destructeur de la classe Partition, il suffira de détruire f, il est inutile de détruire les composants, c'est le propriétaire qui s'en chargera.

Partition::~Partition()
{
delete f;
}

Bien entendu, cela n'est vrai que pour les composants qui ont un propriétaire c'est-à-dire pour tous les composants visuels en général. Mais pour les composants non visuels de la VCL, ceux-ci n'ont pas de propriétaire, vous êtes alors tenu de procéder à la libération de la mémoire. Par exemple, nous n'allons pas dessiner directement dans le canvas du PaintBox, mais nous allons travailler hors écran dans un bitmap (alinéa 40 de mes Remarques) et l'événement OnPaint du PaintBox se contentera de recopier le bitmap dans son canvas, ce qui aura pour effet d'afficher le bitmap à l'écran. La classe Partition va donc avoir un pointeur vers un bitmap.

Graphics::TBitmap *BM;

Dans le constructeur de la classe Partition, on créera ce bitmap :

BM = new Graphics::TBitmap;
BM->Width=Screen->Width;
BM->Height=Screen->Height;

Mais ce bitmap n'a pas de propriétaire, c'est donc vous qui devrez détruire le bitmap au moment de la destruction de la classe Partition. Il faudra donc rajouter :

;

delete BM;

Dans le destructeur de la classe Partition. Il en sera ainsi pour tout composant n'ayant pas de propriétaire. Mais pour tout composant ayant f pour propriétaire, celui-ci sera détruit automatiquement au moment du delete f. Par où l'on voit que l'esprit RAD (Rapid Application Development) reste valide même si vous créez des composants par programme puisqu'il suffit de ne détruire que le propriétaire sans vous occuper du nombre de composants qui s'y trouvent.

XIV - En résumé

Trois noms à penser pour Form1, Unit1 et Projet1.

Noms des composants à modifier immédiatement : propriété Name.

Application MDI ou SDI : propriété FormStyle.

Pas de variables générales : les déclarer dans la classe principale.

Pas de fonctions isolées : les déclarer dans la classe principale.

Utiliser __fastcall le plus souvent possible.

Initialisation de l'application dans le constructeur de la classe principale.

Pas de malloc/realloc/free : utilisez uniquement new et delete.

new[] pour du fixe, un composant de la VCL pour du variable.

Paramètres de l'application via TiniFile.

Les fichiers via TMemoryStream, TFileStream ou LoadFromFile d'un composant.

Actions centralisées dans TactionList.

Utilisez les tags notamment comme mémoire d'indice.