IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)


Janvier 2002


Remarques de développement avec C++ Builder 5

par Gilles Louise


65. Les fonctions GetPixel et SetPixel
66. Les fichiers texte
67. Lancer un exécutable
68. Les points de suivi
69. La classe TIniFile
70. La classe TStatusBar
71. Les fichiers via TMemoryStream
72. Les classes TPanel et TPaintBox
73. Les fenêtres dynamiques
74. Application multilingue

Une fonction complète : création d'un dictionnaire

Retour à la partie I
Retour à la partie II
Retour à la partie III

65. Les fonctions GetPixel et SetPixel

Il peut être intéressant dans certains cas de lire ou d'écrire directement les pixels à l'écran en coordonnées écran. Pour cela, il faut d'abord lire le handle de l'écran via la fonction GetWindowDC, on écrit donc

HDC deskTopDC;
deskTopDC = GetWindowDC(GetDesktopWindow()) ;

Dans ces conditions, si couleur est un long int, on lira la couleur à la coordonnés X,Y comme suit

couleur = GetPixel(deskTopDC,X,Y);

et on écrira à l'écran le pixel pointé par (X,Y) ainsi

SetPixel(deskTopDC,X,Y,couleur);

66. Les fichiers texte

Ces fichiers vous permettent simplement d'écrire des notes par exemple sur l'état de votre développement. Cela vous évite d'avoir à utiliser un autre logiciel, NotePad ou Word, pour rédiger des notes personnelles sur votre projet ou même des commentaires algorithmiques. Ils s'enregistrent logiquement dans le répertoire courant de développement et s'ouvrent simplement par Fichier|Ouvrir en donnant bien sûr comme type de fichiers "fichier texte". Il est très utile de prendre quelques instants, par exemple juste avant une sauvegarde quotidienne, pour rédiger de telles notes générales. Ces notes sont différentes des commentaires dans le code et déférentes aussi des "listes à faire" dont nous avons parlé alinéa 20. Donc n'hésitez pas à écrire des notes sur votre étude via un fichier texte. Si votre développement, une fois fini, est censé avoir un "Lisez-moi" ou "Readme", vous pouvez aussi le rédiger via un fichier texte.

67. Lancer un exécutable

Il est parfois utile de lancer un exécutable depuis une application créée par C++Builder, on utilise alors au choix ShellExecute ou WinExec. Par exemple, pour lancer un Notepad (bloc notes ou petit éditeur de texte), on écrira

ShellExecute(NULL,NULL,"c:\\windows\\NOTEPAD.EXE",NULL,NULL,SW_SHOW);

ou encore

WinExec( "Notepad",SW_MAXIMIZE);

Voyez l'aide en ligne sur ces fonctions pour plus de détails.

68. Les points de suivi

Ce sont des variables ou des expressions dont vous voulez visualiser le contenu quand vous êtes en mode pas à pas. Nous avons vu (alinéas 1, 2 et 3) les fonctions Exécuter|Inspecter ou encore Exécuter|Évaluer/modifier (Ctrl F7) pour visualiser des variables ou de la mémoire à un moment donné en mode pas à pas. Une autre possibilité probablement plus pratique est de préparer ce qu'on appelle des "points de suivi", cela vous permet de mémoriser facilement les variables et les expressions dont vous voulez connaître le contenu sans avoir à saisir leur nom sans cesse. Un autre avantage est d'avoir immédiatement le contenu de plusieurs variables puisque tout s'affiche en même temps dans la fenêtre "Liste de suivis". Deux opérations seulement sont à connaître, d'une part ajouter un point de suivi à la liste via "Executer|Ajouter un point de suivi" (Ctrl F5) et d'autre part afficher la "liste de suivis" via "Voir|Fenêtre de déboggage|points de suivi" (Ctrl+Alt+W). L'utilisation est donc très simple. Vous ajoutez un point de suivi via Ctrl F5, vous mémorisez ainsi une variable ou expression que vous voulez surveiller en mode debug. Si vous voulez connaître le contenu d'une variable à un endroit précis du source, le mieux consiste à positionner le curseur sur cette variable et de faire Ctrl F5, C++Builder considère que vous voulez suivre cette variable et l'insère dans la liste des suivis. Si votre curseur ne pointe nulle part, vous devez alors entrer vous-même la variable ou l'expression et préciser le mode d'affichage à choisir dans le formulaire. Suite à cette saisie, le point de suivi est mémorisé avec la note "processus non accessible" car vous n'êtes pas encore censé visualiser cette variable. Ensuite, positionnez le curseur quelque part dans le source comme point d'arrêt (point bleu dans la gouttière de gauche), faites F4 (Exécuter jusqu'au curseur), si la fenêtre "Liste de suivis" n'est pas affichée, faites-là apparaître (Ctrl+Alt+W), vous voyez d'un seul coup le contenu de toutes les variables à ce moment-là parmi celles bien sûr qui sont accessibles à partir de ce point d'arrêt, en faisant du pas à pas, la fenêtre est sans cesse mise à jour. En cliquant à droite avec le curseur pointant la fenêtre "Liste de suivis", vous avez le menu associé pour ajouter, modifier, supprimer des points de suivi, vous pouvez même les supprimer tous d'un seul coup dès que vous n'en avez plus besoin. C'est donc une très bonne méthode pour espionner une ou plusieurs variables en mode debug.

69. La classe TIniFile

Cette classe très pratique permet de mémoriser et de récupérer très facilement des paramètres propres à une application. Pour pouvoir l'utiliser, il faut rajouter #include <inifiles.hpp> au début du cpp (faites F1 avec le curseur positionné sur le mot TIniFile, c'est le nom situé en dessous de la rubrique Unité dans l'écran d'aide, c'est ainsi que l'on connaît le nom de l'include associé à un objet lorsqu'il ne fait pas partie des "include" standard). Les fichiers INI sont en fait des fichiers texte que l'on peut visualiser sous Notepad (bloc notes standard) ou bien sûr avec l'éditeur de fichier texte intégré à C++Builder (voir alinéa 66). Les fichiers INI se divisent en sections et pour chaque section vous donnez simplement une clé et une valeur, en général ce sera soit un integer, soit une chaîne de caractères (par exemple pour mémoriser facilement les derniers fichiers ouverts par l'utilisateur ou un répertoire fréquemment utilisé), soit un booléen (par exemple pour savoir si la fenêtre était maximalisée ou non à sa fermeture) soit un float soit encore une date ou une heure. Le principe est très simple. Au moment de l'initialisation de l'application (e.g. à la construction de la fiche principale), vous lisez vos paramètres et au moment de la fermeture de l'application (e.g. OnClose ou OnDestroy), vous sauvegardez ces mêmes paramètres. L'intérêt est évidemment un travail moindre que si vous gériez vous-même un fichier de paramètres, notamment vous ne vous occupez pas de savoir si le fichier INI existe ou non, ce sont les appels aux méthodes qui s'en chargent. Donc, à l'initialisation de votre application, vous instanciez une classe de type TIniFile,

TIniFile *ini;
ini = new TIniFile(ChangeFileExt(Application->ExeName,".INI"));

Notez que ChangeFileExt (le nom de cette fonction est un peu trompeur) ne fait que concaténer le nom de l'application avec l'extension INI. C'est très pratique car Application->ExeName est le nom de votre exécutable avec son chemin complet, ainsi le fichier d'INI se trouvera logiquement dans le répertoire où se trouve l'application elle-même. À ce stade, nous pouvons lire certains éléments du fichier, par exemple on lit ses propriétés Top et Left c'est-à-dire les coordonnées de la fenêtre principale.

Top = ini->ReadInteger("Form", "Top", 100);
Left = ini->ReadInteger("Form", "Left", 100);

Les syntaxes de lecture sont toutes les mêmes, on donne le nom de la section (c'est une chaîne de caractères arbitraire, vous avez autant de sections que vous le voulez), on donne le nom de la clé à l'intérieur de cette section puis une valeur par défaut pour le cas où le fichier n'existerait pas ou encore, si le fichier existe, pour le cas où cette clé dans la section n'existerait pas. Ainsi, le programme n'est jamais pris au dépourvu, il renverra soit une valeur trouvée dans le fichier soit une valeur par défaut mais vous ne vous occupez de rien. On lira de même les dimensions de la fenêtre principale,

Width = ini->ReadInteger("Form", "Width", 500);
Height = ini->ReadInteger("Form", "Height", 300);

On lira par Readbool si la fenêtre est ou non maximalisée,

if(ini->ReadBool("Form", "InitMax", false)) WindowState = wsMaximized;
else WindowState = wsNormal;

On suppose dans cet exemple que la fenêtre n'est pas maximalisée par défaut c'est-à-dire qu'elle ne sera pas maximalisée lors de la toute première exécution. On peut donc lire facilement des chaînes (ReadString), des nombres réels (ReadFloat), une date (ReadDate) etc.. On peut également savoir si une section existe dans le fichier d'initialisation (SectionExists) ou encore si une clé existe (ValueExists) dans une sections donnée. Puis on n'oubliera pas de détruire la classe instanciée.

delete ini;

Maintenant, au moment de la fermeture de l'application, on mémorise ces mêmes informations par les fonction WriteInteger, WriteBool, WriteString etc. Voici par exemple ce qui se passerait au moment de la destruction de l'application :

void __fastcall TForm1::FormDestroy(TObject *Sender)
{
TIniFile *ini;
ini = new TIniFile(ChangeFileExt( Application->ExeName, ".INI" ) );
ini->WriteInteger("Form", "Top", Top);
ini->WriteInteger("Form", "Left", Left);
ini->WriteInteger("Form", "Width", Width);
ini->WriteInteger("Form", "Height", Height);
ini->WriteBool("Form", "InitMax", WindowState == wsMaximized);
delete ini;
}

Suite à une première exécution le fichier INI existe, donc à la prochaine utilisation de l'application, vous aurez vos paramètres mémorisés dans ce fichier INI, lesquels, comme nous l'avons dit, seront lus au moment de la construction de la forme principale c'est-à-dire à l'initialisation de l'application.

__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
TIniFile *ini;
ini = new TIniFile(ChangeFileExt(Application->ExeName,".INI"));
Top = ini->ReadInteger("Form", "Top", 100);
Left = ini->ReadInteger("Form", "Left", 100);
Width = ini->ReadInteger( "Form", "Width", 500);
Height = ini->ReadInteger( "Form", "Height", 300);
if(ini->ReadBool( "Form", "InitMax", false)) WindowState = wsMaximized;
else WindowState = wsNormal;
delete ini;
}

Vous voyez que vous ne vous occupez ni de l'ouverture du fichier INI ni de sa fermeture ni des recherches dans les différentes sections. Vous vous contentez d'instancier une classe de type TIniFile par new, de lire ou d'écrire (en général lire à l'initialisation et écrire les mêmes informations à la fin de l'exécution). Puis, après utilisation, vous détruisez la classe par delete.

70. La classe TStatusBar

C'est une barre d'état le plus souvent située en bas de la fenêtre de l'application. Elle est composée de plusieurs sections appelées "panel", chacune étant censée donner un renseignement sur l'exécutable en cours d'exécution. C'est très pratique pour donner à l'utilisateur des indications en tous genres sur l'état de l'application en cours. Par exemple, l'éditeur de code de C++Builder a trois panels dans son status bar, le premier indique la position du curseur dans le texte du code, le deuxième indique si le projet a été modifié ou non et le troisième si vous être en mode insertion ou en mode remplacement. Le composant TStatusBar se trouve dans l'onglet Win32 de la palette de composants, cliquez pour le sélectionner et déposez-le sur la fenêtre principale, il vient logiquement se dessiner en bas de la fenêtre car le plus souvent, c'est à cet endroit qu'il se trouvera, C++Builder a donné à sa propriété Align la valeur alBottom par défaut. Double-cliquez sur ce composant ou cliquez à droite et choisissez l'option "Éditeur de volets", la toute première fonction du menu surgissant. Là, vous tombez sur une petite fenêtre, c'est avec ce petit formulaire qu'on peut rajouter des panels au status bar. Cliquez par exemple trois fois sur le logo d'ajout (la bulle d'aide de cette icône dit "Ajouter un nouveau") ou encore appuyez trois fois sur la touche Inser pour insérer trois panels. Ceux-ci se dessinent en temps réel dans la status bas et les occurrences s'ajoutent dans le petit formulaire de mise au point. Pour chacun de ces panels, vous pouvez régler un certain nombre de paramètres notamment la longueur (Width) de chacun et l'alignement qui indique si la chaîne de caractères sera justifiée à gauche ou à droite ou encore centrée dans le panel. Vous avez donc maintenant trois volets d'indication indicés de 0 à 2. Pour écrire dans le panel d'indice i on écrira simplement

StatusBar1->Panels->Items[i]->Text = "Renseignement";

Un status bar est donc très pratique, très facile à mettre en œuvre et très utile pour l'utilisateur qui a ainsi à disposition divers renseignements sur l'état de l'application.

71. Les fichiers via TMemoryStream

Une méthode très simple et immédiate pour lire ou écrire des fichiers est d'utiliser la classe TMemoryStream et ses méthodes associées LoadFromFile et ReadToFile. Par exemple, imaginons un fichier essai.txt que l'on veuille sauvegarder dans copie_essai.txt, on écrira simplement

TMemoryStream *MS;
MS=new TMemoryStream();
MS->LoadFromFile("ESSAI.TXT");
MS->SaveToFile("COPIE_ESSAI.TXT");
delete MS;

Difficile d'imaginer plus simple, on déclare le flux (TMemoryStream *MS), on l'instancie (MS=new TMemoryStream()), on charge le fichier dans s'occuper de rien en une seule instruction (MS->LoadFromFile("ESSAI.TXT")) le répertoire étant en l'absence de précision le répertoire par défaut où se trouve l'application (sinon donnez un chemin complet en n'oubliant pas qu'il faut écrire à chaque fois deux fois le signe \, voir alinéa 35), on le sauvegarde également en une seule ligne (MS->SaveToFile("COPIE_ESSAI.TXT")) puis on détruit le flux (delete MS).

Si vous devez lire les octets successifs d'un flux lu par LoadFromFile, on déclare alors un pointeur de caractères (char *F), une fois le fichier lu par LoadFromFile on instancie par new une zone égale à la longueur du fichier (F=new char[MS->Size]) où MS->Size représente la longueur en nombre d'octets du fichier lu, là on lit tout le flux dans cette zone (MS->Read(F,L)). À ce stade vous avez accès à tous les octets du flux par la syntaxe classique F[i]. Si maintenant vous voulez après modification de la zone F recopier cette zone dans le flux il faudra d'abord remettre le pointeur de lecture-écriture à zéro (MS->Position=0) car le fait d'avoir lu précédemment par Read l'a fait avancer de MS->Size octets; puis on écrit la zone F dans le flux (MS->Write(F,L)). Notez que si la zone a changé de longueur par rapport au Read, cela n'a aucune importance, la méthode de classe s'occupe de tout, L n'a pas besoin d'avoir la même valeur qu'au moment du Read, la zone pointée par F sera recopiée par MS->Write(F,L) quelle qu'en soit la longueur, c'est évidemment très pratique car on ne s'occupe de rien. Le nouveau flux étant sauvegardé en mémoire, on peut alors le sauver par un SaveToFile comme précédemment. Ne pas oublier bien sûr de détruire le flux (delete MS) et de restituer la mémoire allouée par new (delete[] F) en n'oubliant pas les crochets car une instanciation par new[] doit être libérée par delete[] (le compilateur ne donnerait aucune erreur en n'écrivant delete tout court mais la mémoire serait mal libérée, à new tout court doit correspondre delete tout court, à new[] delete[]). Voici un petit exemple où un fichier texte est lu dans un flux mémoire, ce flux à son tour écrit dans une zone instanciée par new, on lit ensuite tous les caractères et on change tout les e en z (ceci prouvera qu'on accède bien aux caractères) puis on sauvegarde dans une copie le fichier modifié.

TMemoryStream *MS;
int L;
char *F;

MS=new TMemoryStream();
MS->LoadFromFile("ESSAI.TXT");
L=MS->Size;
F=new char[L];
MS->Read(F,L);
for(int i=0;i<L;i++) if(F[i]=='e') F[i]='z';
MS->Position=0;
MS->Write(F,L);
MS->SaveToFile("COPIE_ESSAI.TXT");

delete MS;
delete[] F;

On constate donc que le flux bien qu'en mémoire et lu par LoadFromFile n'est pas accessible directement, il se comporte comme un fichier à cela près qu'il est déjà en mémoire. Pour accéder aux octets, il faut alors le lire une seconde fois dans une zone prévue à cet effet et instanciée par new. Dans ces conditions, on accède à tous les caractères de la zone. Puis réciproquement, cette zone est recopiée dans le flux, elle n'a pas besoin d'avoir la même longueur, ça marchera dans tous les cas, le Write enregistre la longueur donnée en argument (ici la même longueur L mais ce n'est pas obligatoire) puis ce nouveau flux est sauvegardé dans le disque dur par SaveToFile dans le répertoire courant.

Notez que Read renvoie en fait un entier qui est égal à la longueur effectivement lue. Ici nous avons lu tout le fichier en une seule fois sans tester l'entier renvoyé par Read dans cet exemple très simple. Mais si votre fichier est assez grand, vous pouvez aussi le lire par petite partie. Vous demandez au Read de vous lire un nombre arbitraire d'octets, tant que le Read renvoie un entier égal à ce que vous demandez vous continuez la lecture. Mais dès que cet entier renvoyé par Read est inférieur au nombre demandé, on sait alors qu'on est arrivé à la fin du flux. Voici comment nous programmerions la même fonction en lisant et écrivant par petite zone de LZ octets (Longueur Zone, entier arbitraire).

const int LZ=10;
TMemoryStream *MS;
char F[LZ];
int i, Der;

MS=new TMemoryStream();
MS->LoadFromFile("ESSAI.TXT");

while((Der=MS->Read(F,LZ))==LZ)
   {
   for(i=0;i<LZ;i++) if(F[i]=='e') F[i]='y';
   MS->Position-=LZ;
   MS->Write(F,LZ);
   }// fin du while

/* traitement de le dernière section de Der octets */
for(i=0;i<Der;i++) if(F[i]=='e') F[i]='z';
MS->Position-=Der;
MS->Write(F,Der);

MS->SaveToFile("COPIE_ESSAI.TXT");
delete MS;

Ici, la zone tampon n'est plus instanciée par new, on se contente de déclarer un tableau de caractères (char F[LZ]). On lit le flux par LoadFromFile comme précédemment. Puis aussi longtemps que la demande de lecture dans le flux de LZ octets renvoie LZ (while((Der=MS->Read(F,LZ))==LZ)), on traite la zone lue qui est alors complète en remplaçant les e par des y (traitement arbitraire pour prouver qu'on accède bien aux octets). Puis on recule le pointeur de lecture-écriture de LZ octets (MS->Position-=LZ) car il avait avancé de LZ octets par le Read (situé dans la condition While). On écrit cette zone (MS->Write(F,LZ)) dans le flux, on écrase donc dans le flux la section en cours en faveur de la nouvelle version. Comme après ce Write, le pointeur a avancé de LZ octets, on n'a pas besoin de positionner ce pointeur qui pointe maintenant la section suivante à lire. Dès que le While échoue, cela signifie que le nombre d'octets lus est inférieur à LZ et ce nombre d'octets effectivement lu, nous le connaissons puisque nous avons pris la précaution dans la condition du While de le mémoriser dans l'entier Der. Donc, dès que la boucle en While s'arrête, on sait qu'il nous reste à traiter Der octets dans la zone. Une fois traités, on recule de Der octets (MS->Position-=Der) puis on écrit ces Der octets (MS->Write(F,Der)). Le flux a ainsi été modifié par partie, on le sauvegarde par un SaveToFile.

Cette technique est très pratique quand la fichier est structuré c'est-à-dire constitué d'enregistrements de longueur identique. On crée alors un tableau local ayant pour longueur la longueur d'un enregistrement puis on lit les enregistrements et on les écrit comme précédemment. La seule petite différence est qu'il n'y aura pas de section finale car dès que la boucle en While s'arrêtera, le traitement sera terminé car on suppose le fichier structuré. Ici, nous avons lus par groupe de LZ octets puis traités Der octets à la fin. Dans le cas d'un fichier structuré, le fait que le Read renvoie un nombre inférieur suffit à arrêter le traitement, Le Read a d'ailleurs renvoyé zéro la dernière fois puisqu'il n'a rien lu (dans le cas d'un fichier structuré, chaque Read renvoie un entier égal à la longueur d'un enregistrement sauf le dernier qui renvoie zéro).

72. Les classes TPanel et TPaintBox

Ce composant (icône représentant simplement un carré dans l'onglet standard de la palette de composants) est très utile quand on veut gérer facilement des bordures sans avoir à les calculer par programme et connaître au pixel près la surface réelle résultante à l'intérieur du cadre i.e. la surface réelle de représentation, tout cela sans aucun calcul. Ce composant sait gérer trois types de bordures, un bord extérieur (cela simule par exemple le bord en bois d'un tableau), un bord simple (par exemple la bande blanche tout autour d'une aquarelle) puis éventuellement un bord intérieur, cela dépend de l'utilisation que l'on veut en faire. En général, on n'utilisera que deux de ces bordures, d'ailleurs la bordure extérieure aurait les mêmes dimensions que la bordure intérieure et de toute façon, ces rectangles s'imbriquent dans cet ordre : bord extérieur, bord simple, bord intérieur. De plus, comme ce composant a une couleur, la bordure simple sera de la couleur de ce composant. Si toutefois vous ne voulez qu'une bordure toute simple, Tpanel convient encore très bien, vous initialisez les bords extérieurs ou intérieurs à 0 et vous donnerez une largeur à votre bordure simple.

Pour les bordures extérieures ou intérieures, vous avez trois propriétés qui sont BevelInner, BevelOuter et BevelWidth. Par exemple, si vous voulez simuler un tableau avec deux bords, un premier qui simulera le cadre en bois du tableau et un second qui sera une sorte de bande autour du dessin lui-même, vous mettrez la propriété BevelInner (bordure intérieure) à bvNone (cela signifie pas de bordure intérieure), vous choisirez pour BevelOuter (bordure extérieure) la possibilité bvLowered (bordure s'éloignant) ou bvRaised (bordure surélevée) ou bvSpace (espace plat) puis vous donnerez à BevelWidth la largeur de la bordure.

Maintenant, pour avoir une bande plate autour de la surface restante, on positionnera la propriété BorderWidth avec la largeur voulue, cette bande sera de la couleur du Panel.

Si vous ne voulez qu'un bord simple, il suffit que mettre BevelInner et BevelOuter à bvNone (dans ces conditions, la propriété BevelWidth est ignorée) et de donner à BorderWidth la largeur voulue.

Cela dit, attention : un panel n'a pas de canvas, il se sert qu'à gérer des bords et une couleur de bord, en conséquence de quoi on ne peut pas encore dessiner. Pour pouvoir dessiner dans le rectangle résultant entouré (entouré simplement, doublement ou triplement), il faut y déposer un PaintBox qui lui a un canvas.

Voici un petit exemple simple et rapide. Entrez dans C++Builder et sauvegardez le projet vide d'entrée de jeu unitxx et projectxx (où xx est le numéro de votre test). Sélectionnez un Panel et déposez-le dans Form1 (la fenêtre principale par défaut). Vous voyez un rectangle s'inscrire dans Form1 avec pour nom Panel1. Sélectionnez dans l'inspecteur d'objet sa propriété Caption (le Panel est automatiquement sélectionné dans l'inspecteur d'objet du fait qu'il vient d'être déposé dans la fenêtre sinon, si vous l'avez "perdu", vous pouvez toujours le sélectionner dans la liste déroulante de l'inspecteur d'objets), sélectionnez sa propriété Caption et supprimez "Panel1" car dans notre exemple nous ne voulons pas d'affichage du nom. Le nom est donc effacé aussi dans Form1. Sélectionnez maintenant la propriété Align et choisissez alClient. Maintenant le Tpanel prend toute la surface client de Form1. Sélectionnez sa propriété Color et choisissez une couleur très visible par exemple le rouge (clRed), tout le panel devient rouge mais en réalité comme un panel ne sert qu'à contenir lui-même autre chose, ce n'est que finalement sa bordure qui sera rouge (à ce stade nous sommes en construction, ce qui explique que tout le panel est rouge). Laissez sa propriété BevelInner à bvNone car nous ne voulons pas dans notre exemple de bordure intérieure (probablement rare d'ailleurs). Laissez BevelOuter à Raised (la bordure extérieure sera donc surélevée) et mettez BevelWidth à 20 (nous exagérons un peu cette bordure pour que vous vous rendiez compte du rendu). Mettez maintenant BorderWidth à 10, la bordure rouge autour du bord surélevé sera donc de 10 pixels.

Maintenant sélectionnez un PaintBox dans l'onglet système de la palette de composants et déposez-le à l'intérieur du panel rouge. Un carré noir représente le PaintBox. Sélectionnez maintenant sa propriété Align et choisissez AlClient. C++Builder comprend très bien que le PaintBox a pour surface la zone client résultant dans le Panel, tout l'intérêt du panel est là et vous voyez clairement la zone client du PaintBox tenant compte des deux bordures. Maintenant sélectionnez sa propriété Color et choisissez clYellow (jaune). Attention, il ne s'agit pas de la couleur du PaintBox (c'est assez trompeur) mais de la couleur de remplissage c'est-à-dire la couleur qui sera utilisée au moment d'utiliser FillRect (remplissage d'un rectangle). Comme FillRect utilise la propriété PaintBox1->Canvas->Brush->Color, ce que nous venons de faire correspond logiciellement à l'instruction

PaintBox1->Canvas->Brush->Color=clYellow;

À ce stade, si vous faisions F9 pour exécutez, nous ne verrions rien et nous risquerions de croire que ça ne marche pas. En effet, tant qu'il n'y a rien dans l'événement OnPaint du PaintBox, rien ne se passera. Donc, pour voir déjà quelque chose, sélectionnez l'onglet "événements" dans l'inspecteur d'objets (le PaintBox étant toujours l'élément sélectionné de l'inspecteur d'objets) et double-cliquez sur l'événement OnPaint, C++Builder vous crée la méthode suivante

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

C'est là que le programme ira chaque fois qu'il aura à repeindre le PaintBox. Le curseur se trouve après la parenthèse ouvrante car C++Builder attend que vous lui disiez ce qu'il doit faire au moment du paint. Pour le moment, nous allons simplement remplir le PaintBox de sa couleur de fond (ici le jaune, clYellow, choisi précédemment) donc écrivons

TRect r;
r.left=0;
r.top=0;
r.right=PaintBox1->Width-1;
r.bottom=PaintBox1->Height-1;
PaintBox1->Canvas->FillRect(r);

Maintenant faites F9 pour exécuter. Vous voyez l'intérieur du PaintBox en jaune, une bordure rouge (couleur du panel) et un bord surélevé tout autour simulant le cadre d'un tableau, vous pouvez modifier les dimensions de la fenêtre, tout se recalcule en temps réel. Vous pouvez essayer diverses valeurs pour BorderWidth (bordure rouge) et BevelWidth (premier bord) dans l'inspecteur d'objets. Attention, après avoir modifié une valeur dans l'inspecteur d'objets, il faut sortir le curseur ou faire "Entrée" (Return) pour qu'elle soit prise en compte. Essayez également BevelInner, vous verrez que ce bord se trouvera après la bordure rouge. Pourquoi cela est-il intéressant? Tout simplement parce que sans aucun calcul et quelle que soit la dimension de la fenêtre (que l'utilisateur peut redimensionner à sa guise), vous aurez PaintBox1->Width et PaintBox1->Height comme surface de dessin, ces propriétés sont toujours disponibles et le logiciel peut agir en conséquence en fonction de ces valeurs obtenues sans rien faire. Notez bien les minuscules l dans letf, t dans top etc. dans les syntaxes r.left, r.top, r.right et r.bottom.

Le PaintBox ayant pour dimensions PaintBox1->Width et PaintBox1->Height, ses coordonnées en x vont de 0 à PaintBox1->Width-1 et ses dimensions en y de 0 à PaintBox->Height-1. Cela dit, C++Builder n'est pas à cela près, si vous dépassez les limites dans un dessin quelconque, ça n'a aucune importance, C++Builder se charge de ce type de vérifications, ce pourquoi on omet fréquemment de soustraire cette unité s'agissant d'un simple remplissage. On peut également écrire ceci en déclarant le rectangle à l'intérieur même de l'instruction :

void __fastcall TForm1::PaintBox1Paint(TObject *Sender)
{
PaintBox1->Canvas->FillRect(Rect(0,0,PaintBox1->Width,PaintBox1->Height));
}

Si vous voulez modifier la couleur de remplissage (qui est la couleur de fond du PaintBox), ajoutez juste avant le FillRect :

PaintBox1->Canvas->Brush->Color=clAqua;

Ici, le PaintBox sera en bleu clair. Modifiez le nom de PaintBox1 pour éviter d'avoir à saisir chaque fois ce nom assez long (propriété Name) et appelez-le PB, dans ces conditions le OnPaint s'écrira :

void __fastcall TForm1::PBPaint(TObject *Sender)
{
PB->Canvas->Brush->Color=clAqua;
PB->Canvas->FillRect(Rect(0,0,PB->Width,PB->Height));
}

Notez que dans ces conditions, C++Builder a modifié pour vous le nom de la méthode (PBPaint au lieu de PaintBox1Paint) mais n'a pas changé dans le source les PaintBox1 en PB, c'est à vous de la faire par un remplacement. C'est pourquoi, s'agissant des noms des composants, on préfère changer son nom avant qu'il intervienne dans le programme en tant que saisi par vous. C++Builder ne corrige en cas de modification du nom que ce qu'il a écrit lui-même. Comme il a écrit le prototype des méthodes, il rectifiera ces déclarations dans le source et dans l'en-tête (header) mais non le source saisi par le programmeur.

Cela dit, on évite normalement d'écrire directement dans le canvas d'un composant car cela a pour effet de s'écrire directement à l'écran en temps réel, on risque des scintillements désagréables. On préfère généralement écrire dans un bitmap, on écrit alors hors écran (nous avions déjà abordé ce problème alinéa 40) et le OnPaint ne fait que recopier le bitmap dans le canvas du PaintBox affichant tout en une seule fois. Une autre petite astuce consiste à surdimensionner légèrement le bitmap en utilisant les propriétés Screen->Width et Screen->Height qui sont les dimensions réelles de l'écran. Ainsi, si l'utilisateur redimensionne la fenêtre, vous gardez néanmoins les mêmes dimensions de bitmap surdimensionné. Cela nous épargne d'avoir à modifier à chaque resize les dimensions du bitmap, lesquelles sont déclarées une fois pour toutes. Ainsi, déclarons dans les variables générales un pointeur de bitmap conne ceci :

#include <vcl.h>
#pragma hdrstop

#include "Unit6.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
Graphics::TBitmap *BM;

Au moment du constructeur de TForm1 (ou à OnShow ou encore à OnActivate de Form1), on déclare et on remplit le bitmap :

__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
BM=new Graphics::TBitmap();
BM->Width=Screen->Width;
BM->Height=Screen->Height;
BM->Canvas->FillRect(Rect(0,0,PB->Width,PB->Height));
}

On a surdimensionné ici comme prévu le bitmap de manière à ne jamais modifier ses dimensions même si l'utilisateur fait un resize de la fenêtre. Cela dit, on respecte les coordonnées réelles de dessin à l'intérieur du bitmap, ces dimensions sont celles du PaintBox à savoir PB->Width et PB->Height. Dans ces conditions, le OnPaint ne fait que recopier le bitmap dans le canvas du PaintBox, donc :

void __fastcall TForm1::PBPaint(TObject *Sender)
{
PB->Canvas->Draw(0,0,BM);
}

Ne pas oublier de restituer la mémoire allouée pour le bitmap au moment du OnDestroy de la fenêtre principale :

void __fastcall TForm1::FormDestroy(TObject *Sender)
{
delete BM;
}

Faites F9 pour exécuter. Remarquez que le PaintBox est devenu blanc. En effet, nous avons rempli avec le Canvas->Brush->Color du bitmap et non le Canvas->Brush->Color du PaintBox (qui d'ailleurs n'a plus d'utilité si nous travaillons avec un bitmap hors écran), pour modifier la couleur de remplissage du bitmap, modifiez la propriété Canvas->Brush->Color du bitmap juste avant le FillRect, par exemple :

BM->Canvas->Brush->Color=clOlive;

Notez que le nom des couleurs standard sont accessibles dans l'inspecteur d'objets. Sélectionnez la propriété Color d'un composant quelconque, le menu déroulant vous indique toutes les couleurs standard associées à leur nom logiciel que vous pouvez utiliser dans un programme. Si vous voulez affecter une couleur personnalisée, il faut utiliser TColor avec une valeur 32 bits du type 0x00bbvvrr où bb est la quantité de bleu, vv la quantité de vert, rr la quantité de rouge, chaque composante étant comprise entre 0x00 et 0xff. Ainsi, on peut écrire :

BM->Canvas->Brush->Color=(TColor)0x0010aaff;

avec un peu de bleu (10), pas mal de vert (a0) et tout le rouge (ff), ce qui va donner une sorte d'orange tirant sur le marron. Voyez l'aide en ligne sur les possibilités du MSB (Most Significant Byte, octet le plus significatif) de la couleur que nous mettons ici à 0.

Pour prouver que nous maîtrisons parfaitement les dimensions, nous allons dessiner les deux grandes diagonales du PaintBox dans son événement OnPaint.

void __fastcall TForm1::PBPaint(TObject *Sender)
{
BM->Canvas->Brush->Color=clWhite;
BM->Canvas->FillRect(Rect(0,0,PB->Width,PB->Height));
BM->Canvas->MoveTo (0,0);
BM->Canvas->LineTo (PB->Width-1,PB->Height-1);
BM->Canvas->MoveTo (PB->Width-1,0);
BM->Canvas->LineTo (0,PB->Height-1);
PB->Canvas->Draw(0,0,BM);
}

Pour le FillRect nous n'avons pas supprimé l'unité car le fait de dépasser le remplissage d'un pixel n'a aucune importance et est même souhaitable car l'expérience montre qu'il y a parfois des pixels résiduels suite à des redimensionnements rapides, le fait d'aller au-delà d'une unité évite ce problème, ces résidus sont d'ailleurs en plus grand nombre si on redimensionnait à chaque fois le bitmap car on risque d'accéder à ces zones non initialisées, nous avons résolu cet inconvénient en surdimensionnant le bitmap. En revanche pour le dessin, il faut utiliser PB->Width-1 et PB->Height-1 qui sont les coordonnées maximales. Le principe sera toujours le même, dans le bitmap surdimensionné, on n'écrit qu'à l'intérieur du rectangle (0,0,PB->Width-1, PB->Height-1). Faites F9, vous voyez que la diagonale correcte s'affiche quelle que soit la dimension de la fenêtre.

Si vous voulez éviter d'avoir à chaque fois soustraire cette unité, le mieux sera de déclarer dans les variables générales deux entiers PBMax_x et PBMax_y :

#include <vcl.h>
#pragma hdrstop

#include "Unit6.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
Graphics::TBitmap *BM;
int PBMax_x,PBMax_y;

Puis, dans l'événement OnResize du panel, on renseignera ces deux variables en fonction des dimensions du PaintBox.

void __fastcall TForm1::Panel1Resize(TObject *Sender)
{
PBMax_x=PB->Width-1;
PBMax_y=PB->Height-1;
}

Ainsi, après un resize, PBMax_x et PBMax_y seront toujours correctement positionnés. L'événement OnPaint s'écrira alors :

void __fastcall TForm1::PBPaint(TObject *Sender)
{
BM->Canvas->Brush->Color=Panel1->Color;
BM->Canvas->FillRect(Rect(0,0,PB->Width,PB->Height));
BM->Canvas->MoveTo (0,0);
BM->Canvas->LineTo (PBMax_x,PBMax_y);
BM->Canvas->MoveTo (PBMax_x,0);
BM->Canvas->LineTo (0,PBMax_y);
PB->Canvas->Draw(0,0,BM);
}

Essayez d'utiliser PBMax_x et PBMax_y dans le FillRect et vous verrez parfois, ainsi que nous l'avons dit plus haut, des pixels résiduels suite à des redimensionnements de la fenêtre. On résout cette difficulté en utilisant PB->Width et PB->Height qui remplit un pixel au-delà.

Quant au scintillement au moment du redimensionnement, il est dû au fait que la couleur du panel n'est pas la même que la couleur de remplissage du bitmap à savoir BM->Canvas->Brush->Color. Comme nous avons choisi précédemment comme couleur de remplissage du bitmap le blanc (BM->Canvas->Brush->Color=clWhite;) il suffit de choisir le blanc comme couleur du panel ou encore dire logiciellement que la couleur de remplissage du bitmap sera la couleur du panel, quelle qu'elle soit, ce qui s'écrirait comme suit :

BM->Canvas->Brush->Color=Panel1->Color;

Dans ces conditions, le scintillement disparaît totalement. C'est ainsi qu'on utilisera de préférence le panel, on utilisera sa couleur comme couleur de remplissage du bitmap, la bordure existe toujours physiquement mais n'apparaît simplement plus à l'écran car le panel n'a pas pour vocation d'offrir une bordure visible mais de permettre l'insertion d'un PaintBox qui nous donnera automatiquement les dimensions réelles de dessin PB->Width et PB->Height (pour cela il faut alors que le PaintBox ait, comme nous l'avons proposé, sa propriété Align à alClient, il est alors enfant du panel et prend toute sa zone client).

Bien entendu, dans un vrai logiciel de dessin, cela ne se passera pas exactement ainsi. On préférera créer une fonction "Dessin" qui dessinera, appeler cette fonction au début de l'application (e.g. à l'événement OnShow de Form1), appeler cette fonction après un resize (car les dimensions du PaintBox ont été modifiées) de manière à ce que l'événement OnPaint ne contienne que la recopie du bitmap dans le canvas du PaintBox (une seule instruction, PB->Canvas->Draw(0,0,BM);)

Remarque : si vous voulez écrire des caractères dans le bitmap mais sans recopier la couleur de fond du caractère (qui n'est qu'un tout petit bitmap) il faut alors affecter la propriété bsClear à Brush->Style et écrire :

BM->Canvas->Brush->Style=bsClear;

Ceci aura pour effet de n'écrire que les pixels correspondant au caractère et non les autres. C'est très utile quand on veut superposer des caractères. Imaginons que l'on veuille écrire "Bonjour" dans le bitmap et souligner le mot, on va écrire d'abord Bonjour puis on va écrire un certain nombre de fois le caractère de soulignement (underscore). Si on ne prend pas la précaution bsClear, les underscores qu'on va écrire après avoir écrit Bonjour vont écraser les lettres et on ne verra que les underscores (ou que les lettres si on écrit d'abord les underscores). On résout ce problème en affectant la propriété bsClear à Brush->Style. On écrira donc :

BM->Canvas->Brush->Style=bsClear;
BM->Canvas->TextOut(20,20,"Bonjour");
BM->Canvas->TextOut(20,20,AnsiString::StringOfChar('_',7));

Imaginons un logiciel musical qui affiche des partitions, il aura besoin de cette propriété car par exemple la clé de sol s'ajoute à la portée (sans écraser les lignes de portée) ainsi que tous les signes musicaux (notes, silences, barres de mesure) qui ne sont que des caractères d'une police particulière dédiée à l'affichage musical.

Autre remarque. La propriété bsClear est en fait en conflit avec la propriété Canvas->Brush->Color, en conséquence de quoi, à chaque fois que vous aurez à effacer le bitmap par FillRect, il faudra lui donner juste avant la couleur de remplissage le plus souvent le blanc, donc

BM->Canvas->Brush->Color=clWhite;
BM->Canvas->FillRect(Rect(0,0,PB->Width,PB->Height));
et à chaque fois que vous aurez ensuite à superposer des caractères dans le bitmap, il faudra repréciser bsClear, donc

BM->Canvas->Brush->Style=bsClear;

Dans ces conditions, tout fonctionnera bien (sinon il y aurait des problèmes d'affichage). D'ailleurs, si on voulait dessiner le cadre correspondant à la surface visible du bitmap, via la fonction Polygon, après en avoir tracé les deux diagonales, il faudrait utiliser bsClear. Par exemple, rajoutez ces lignes juste avant le draw du PBPaint.

Windows::TPoint points[4];
points[0] = Point(0,0);
points[1] = Point(PBMax_x,0);
points[2] = Point(PBMax_x,PBMax_y);
points[3] = Point(0,PBMax_y);
BM->Canvas->Brush->Style=bsClear;
BM->Canvas->Polygon(points,3);

Le cadre s'affiche tout autour et correspond à la partie cliente du PaintBox (et donc du bitmap qui prend ses dimensions). Si vous supprimez la précision bsClear (en mettant en commentaire l'avant-dernière instruction), vous ne verrez que le cadre sans les diagonales. On détourne ainsi les conflits possibles entre Canvas->Brush->Color et Canvas->Brush->Style. Pour les différents styles, voir l'aide en ligne.

Nous allons maintenant faire la même chose mais le tout par logiciel en partant d'une application vide. On va donc créer logiciellement le panel et le PaintBox.

#include <vcl.h>
#pragma hdrstop
#include "Unit7.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 Form1;
Graphics::TBitmap *BM;
TPaintBox *PB;
TPanel *Pan;
int PBMax_x,PBMax_y;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------
void Diagonale(void)
{
BM->Canvas->Brush->Color=Pan->Color;
BM->Canvas->FillRect(Rect(0,0,PB->Width,PB->Height));
BM->Canvas->MoveTo (0,0);
BM->Canvas->LineTo (PBMax_x,PBMax_y);
BM->Canvas->MoveTo (PBMax_x,0);
BM->Canvas->LineTo (0,PBMax_y);
}
//---------------------------------------------------------------------------
/* fonction correspondant à l'événement OnPaint du PaintBox */
void __fastcall TForm1::Dessine(TObject *Sender)
{
PB->Canvas->Draw(0,0,BM);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
delete PB;
delete Pan;
delete BM;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormShow(TObject *Sender)
{
Pan=new TPanel(this);
Pan->Parent=this;
Pan->Align=alClient;
Pan->Color=clWhite;
Pan->BevelWidth=20;
Pan->BorderWidth=10;

PB=new TPaintBox(this);
PB->Parent=Pan;
PB->Align=alClient;
PB->OnPaint=Dessine;

BM=new Graphics::TBitmap();
BM->Width=Screen->Width;
BM->Height=Screen->Height;
Diagonale();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormResize(TObject *Sender)
{
PBMax_x=PB->Width-1;
PBMax_y=PB->Height-1;
Diagonale();
}

Comme nous avons créé la méthode Dessine, il ne faut pas oublier de la déclarer dans l'en-tête.

#ifndef Unit7H
#define Unit7H
//---------------------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
//---------------------------------------------------------------------------
class TForm1 : public TForm
{
__published: // Composants gérés par l'EDI
void __fastcall FormDestroy(TObject *Sender);
void __fastcall FormShow(TObject *Sender);
void __fastcall FormResize(TObject *Sender);
private: // Déclarations utilisateur
public: // Déclarations utilisateur
__fastcall TForm1(TComponent* Owner);

void __fastcall Dessine(TObject*);
};
//---------------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//---------------------------------------------------------------------------
#endif

En partant d'une application vide, nous avons d'abord déclaré dans les variables générales un pointeur de PaintBox (TPaintBox *PB;) puis un pointeur de Panel (TPanel *Pan;) puis un pointeur de bitmap (Graphics::TBitmap *BM;) et nous avons déclarer deux entiers qui contiendront les coordonnées maximales du PaintBox (int PBMax_x,PBMax_y;).

Nous avons ensuite créé l'événement OnShow de Form1 pour y déclarer le Panel, le PaintBox et le bitmap. On a donc créé le panel (Pan=new TPanel(this) où this=Form1 qui devient propriétaire du panel). On a indiqué que son parent était Form1 (Pan->Parent=this;). Puis on a donné ses propriétés Align (Pan->Align=alClient;), Color (Pan->Color=clWhite;), BevelWidth (Pan->BevelWidth=20;) et BorderWidth (Pan->BorderWidth=10;). On a ensuite créé le PaintBox (PB=new TPaintBox(this);), son parent est le panel (PB->Parent=Pan;), sa propriété Align est alClient (PB->Align=alClient;) et on a donné le nom de la méthode à exécuter en cas d'événement OnPaint (PB->OnPaint=Dessine;). Ensuite on a créé le bitmap (BM=new Graphics::TBitmap();) qu'on a surdimensionné en x (BM->Width=Screen->Width;) et en y (BM->Height=Screen->Height;) avec les dimensions de l'écran. On appelle alors une fonction créée par nous qui dessine la diagonale (Diagonale();). La méthode Dessine ne fait que recopier le bitmap dans le canvas du PaintBox (PB->Canvas->Draw(0,0,BM);).

On a ensuite défini l'événement OnResize de Form1 de manière à renseigner les variables PBMax_x (PBMax_x=PB->Width-1;) et PBMax_y (PBMax_y=PB->Height-1;) avec les coordonnées maximales en x et en y. Comme les dimensions du PaintBox ont changé, on redessine logiquement (Diagonale();).

À noter au passage que dans une application MDI (multiple Document Interface, voir alinéa 57), il sera bon d'ajuster aussi les coordonnées à l'événement OnCanResize. Cette méthode est sans cesse appelée quand la fenêtre est redimensionnée alors que le Resize n'est appelé qu'après le redimensionnement, si on n'utilise que le OnResize, l'expérience montre que les dimensions sont parfois fausses en MDI. Le mieux est alors des les ajuster au moment du OnCanResize. Seulement, comme cette méthode est appelée bien avant la construction des objets, il faut en vérifier l'existence. Ici, nous écrivons if(PB), cela signifie "si le PaintBox existe" (car s'il n'existe pas encore, la méthode ne peut pas nous délivrer ses coordonnées).

void __fastcall TForm1::FormCanResize(TObject *Sender, int &NewWidth,
        int &NewHeight, bool &Resize)
{
if(PB)
   {
   PBMax_x=PB->Width-1;
   PBMax_y=PB->Height-1;
   }
}

Dans ces conditions, PBMax_x et PBMax_y seront toujours corrects.

Nous avons ensuite créé la fonction Diagonale. On définit la couleur de remplissage du bitmap comme étant celle du panel (BM->Canvas->Brush->Color=Pan->Color;), nous avons effacé de bitmap en imposant dans toute sa partie visible sa couleur Canvas->Brush->Color (BM->Canvas->FillRect(Rect(0,0,PB->Width,PB->Height));). Puis nous avons dessiné les deux grandes diagonales utilisant PBMax_x et PBMax_y renseignés lors du resize.

Nous avons enfin défini l'événement OnDestroy de Form1 pour restituer toute la mémoire allouée. On a rendu d'abord le PaintBox (delete PB;) puis le Panel (delete Pan;) puis le bitmap (delete BM;). La seule contrainte est que le PaintBox doit être détruit avant le Panel car il est enfant du Panel (si on détruit d'abord le panel, il y aurait une erreur à la destruction du PaintBox), il faut détruire dans l'ordre inverse de la construction. En revanche, le bitmap n'étant lié à rien, on le détruit quand on veut.

Dans l'entête, nous avons déclaré dans la classe TFom1 la méthode Dessine (void __fastcall Dessine(TObject*);). Quand vous avez à créer vous-même des méthodes standard, il faut que son prototype soit respecté. Par exemple, ici nous avons déclaré que l'événement OnPaint serait pris en charge par la méthode Dessine (PB->OnPaint=Dessine;) ce qui implique que la méthode Dessine ait le même prototype qu'une méthode OnPaint classique. Une bonne solution consiste à double-cliquer sur l'événement OnPaint d'un composant (ici Form1 est le seul dont nous disposions), C++Builder présente alors le prototype,

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

vous dupliquez ce prototype dans le source cpp en lui donnant un autre nom (ici Dessine)

void __fastcall TForm1::FormPaint(TObject *Sender)
{
}
void __fastcall TForm1::Dessine(TObject *Sender)
{
// prototype dupliqué à déclarer dans la classe TForm1.
}

puis vous déclarez cette méthode personnalisée bien à vous dans la classe TForm1 (void __fastcall Dessine(TObject*);). Lors de la prochaine compilation, la méthode FormPaint créée par C++Builder disparaîtra car elle ne contient rien (elle ne nous a servi qu'à créer un duplicata correct de l'événement OnPaint). Vous avez de cette façon créé une méthode OnPaint, son prototype est correct puisqu'il est le duplicata d'une méthode créée par C++Builder lui-même, on peut alors assigner cette méthode à l'événement OnPaint d'un composant (PB->OnPaint=Dessine;) du fait que son prototype est correct. Reste à définir ce que fait cette méthode, ici elle copie le bitmap dans le PaintBox (PB->Canvas->Draw(0,0,BM);).

73. Les fenêtres dynamiques

J’appelle ainsi les fenêtres créées logiciellement. Elles permettent en général de donner un certain nombre d’options au fonctionnement du logiciel. Vous pouvez n’avoir qu’une seule fenêtre de paramétrage, auquel cas on utilise des onglets pour sélectionner le thème du paramètre à renseigner (c’est le cas par exemple de Word dans Outils->Options), vous pouvez aussi avoir une fenêtre par thème, tout dépend de la complexité des paramètres et de l’allure que vous voulez donner à votre logiciel.

Pour ce type de fenêtres auxiliaires, vous avez deux possibilités : soit les créer à la conception par des fiches supplémentaires, soit les créer dynamiquement c’est-à-dire par programme. Les créer à la conception via de nouvelles fiches est probablement plus rapide mais cette méthode a deux inconvénients : d’une part, les instances de ces fenêtres auxiliaires sont créées au démarrage de l’application et sont présentes en mémoire jusqu’à la fin de l’exécution de l’application, d’autre part, il y aura pour chaque fiche deux fichiers correspondants, un .cpp (source C++) et un .h (header). Imaginez que vous ayez vingt écrans de paramètres, vous aurez vingt .cpp et vingt .h, cela alourdit considérablement le logiciel. Le mieux est donc à mon avis de créer ces fenêtres dynamiquement, ce n’est pas très difficile, il faut simplement être précis et ça ne coûtera que quelques lignes de code par composant créé dynamiquement. Voici dans les grandes lignes la marche à suivre que je vous conseille pour la création de fenêtres auxiliaires.

Pour chaque fenêtre auxiliaire, créez une classe qui contiendra un pointeur f de fenêtre auxiliaire du type TForm, tous les pointeurs de composants qui seront créés dans cette fenêtre et les méthodes associées à ces composants. Chaque classe aura donc un pointeur f qui représentera le pointeur de la fenêtre auxiliaire et chaque composant de la fenêtre aura pour propriétaire cette fenêtre f, cette déclaration se fait au moment du new pour le composant : si c est un pointeur pour un composant donné, la syntaxe de création du composant c sera donc du type c=new Composant(f), syntaxe qui signifie de f est propriétaire de c, ce qui signifie que f est chargé de la libération de c. Le parent sera en général f (on écrira donc souvent c->Parent=f;) mais cela pourra être aussi un composant de f. Dans ces conditions, le seul fait d’écrire delete f après utilisation de la fenêtre libérera non seulement la fenêtre mais aussi tous les composants de cette fenêtre. Mais bien entendu, pour que ça marche il faut d’une part que toute classe ait un pointeur f et que tous les composants de f aient f pour propriétaire. C’est le fait de déclarer f comme propriétaire des composants au moment du new qui nous assure qu’ils seront détruits au moment de la destruction de f elle-même. C’est l’avantage de déclarer un propriétaire au moment du new.

Chaque classe contiendra toujours, en plus des ses membres propres, deux pointeurs de boutons, un pointeur Ok pour confirmer les modifications (bouton OK), un pointeur Annul pour annuler ces modifications (bouton Annuler). Il ne sera pas nécessaire d’associer à ces boutons une méthode d’événement OnClick, on affectera simplement la propriété ModalResult pour ces boutons avec la valeur mrOk pour OK (OK->ModalResult=mrOk;) et avec mrCancel pour pAnnul (Annul->ModalResult=mrCancel;). Pour que mrOk et mrCancel soient connus, n’oubliez d’inclure le fichier controls.hpp, c’est dans ce fichier que se trouvent ces déclarations (mrOk, mrCancel et autres) sinon le compilateur vous dira que mrOk et mrCancel sont inconnus. Ainsi, en cliquant le bouton OK ou Annul, la fenêtre modale se fermera d’elle-même et la méthode ShowModal() de f renverra le code choisi, donc soit mrOk soit mrCancel. Si c’est mrOk, il faudra alors tenir compte du contenu des composants de la fenêtre auxiliaire qui ont peut-être été modifiés (mais ce n’est pas sûr, on peut cliquer OK sans avoir modifié quoi que ce soit), sinon on ne fait rien (car si ce n’est pas OK, c’est Annul).

Cette fenêtre modale va s’afficher par une option du menu principal de votre application et/ou par un bouton car on peut en général accéder à une même fonction par des chemins différents. Ce gestionnaire d’événement créera une instance de notre classe. À cette occasion, la fenêtre et ses composants vont être créés dynamiquement par le constructeur de la classe. On teste alors la valeur de retour de la fonction ShowModal() et on intervient si ce résultat est mrOk. L’intervention consistera en général à mémoriser les paramètres de la fenêtre modale dans un fichier de type ini (et de tenir compte des modifications possibles pour la suite de l’exécution du logiciel).

Le constructeur de cette classe créera tous les composants de la fenêtre auxiliaire à commencer bien sûr par la fenêtre f elle-même. Cette fenêtre aura pour propriétaire l’application en général ou, ce qui revient au même, Form1 (nom par défaut de la fenêtre principale de l’application). On écrira donc f=new TForm1(Application); ou encore f=new TForm1(Form1); La fenêtre étant créée, tous ses composants auront f pour propriétaire. Ainsi, par le simple fait de détruire f, tous ses composants seront automatiquement détruits (automatiquement car c’est C++Builder qui s’en charge sans que nous ayons à écrire une seule ligne de code). Les composants auront en général f pour parent mais ce n’est pas obligatoire, ils pourront aussi avoir un composant de f pour parent. Par exemple, imaginons que la fenêtre auxiliaire soit dotée de boutons radio (ces boutons permettent un choix unique parmi plusieurs, la sélection d’un de ces boutons désélectionne automatiquement le bouton précédemment sélectionné), on créera d’abord un Radio group r dans lequel ces boutons seront ensuite réunis, r aura donc f pour parent mais les boutons radio auront r pour parent en non f (car ces boutons se situent à l’intérieur de r, les coordonnées Left et Top sont par rapport à r).

Il sera bon au moment de la construction de la classe d’utiliser les fichiers INI, ce qui permettra d’initialiser ces composants avec les valeurs anciennement choisies par l’utilisateur ou des valeurs par défaut si ces choix n’existent pas encore.

Le gestionnaire d’événement qui crée une fenêtre auxiliaire se constituera donc d’une instanciation de la classe correspondant à la fenêtre demandée, appelons-la FenAux (p=new FenAux();), d’un test de la valeur de ShowMadal() car si ShowModal() renvoie mrOK alors il faut tenir compte des valeurs contenues dans les composants (if(p->f->ShowModal()==mrOk){//ACTION};) et d’une destruction de l’instance de classe (delete p;)

Le constructeur de la classe créera la fenêtre et ses composants, le destructeur ne détruira que la fenêtre f, ses composants étant automatiquement détruits par le seul fait que f en est propriétaire.

Partez d’un projet vide et sauvegardez immédiatement le projet, unit1 devient principal et projet1 devient Modal. Mettez simplement deux boutons sur la forme principale. Copiez-collez le programme ci-dessous dans principal.cpp en remplacement des quelques lignes écrites automatiquement par C++Builder.

Chaque bouton crée une fenêtre modale différente via les classes Param1 et Param2. Les paramètres sont mémorisés dans un fichier ini. Pour Param2, chaque TEdit est associé à l’événement OnExit qui vérifiera que le mot a au moins quatre caractères. Cela permet de voir la syntaxe d’association à un événement.

#include <vcl.h>
#pragma hdrstop

#include "Principal.h"
/* Controls.hpp nécessaire pour mrOk/mrCancel que renvoient
les fenêtres modales */
#include "Controls.hpp"
/* nécessaire pour les fichiers ini */
#include <inifiles.hpp>
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
/* Première fenêtre modale, un choix entre A, B et C */
class Param1
{
public:
TForm* f;
TRadioGroup* r;
TRadioButton* choixA;
TRadioButton* choixB;
TRadioButton* choixC;
TButton* OK;
TButton* Annul;
Param1();
~Param1();
};

/* Deuxième fenêtre modale, quatre chaînes dans des TEdit */
class Param2
{
public:
TForm* f;

TEdit* e1;
TLabel* l1;

TEdit* e2;
TLabel* l2;

TEdit* e3;
TLabel* l3;

TEdit* e4;
TLabel* l4;

TButton* OK;
TButton* Annul;
Param2();
~Param2();

/* Méthodes de vérification après update */
void _fastcall PostUPD1(TObject* O);
void _fastcall PostUPD2(TObject* O);
void _fastcall PostUPD3(TObject* O);
void _fastcall PostUPD4(TObject* O);
};
//---------------------------------------------------------------------------
const AnsiString ExtIni=".ini";
const AnsiString Param="Parametres";
const AnsiString IniABC="ABC";
const AnsiString Inie1="mot1";
const AnsiString Inie2="mot2";
const AnsiString Inie3="mot3";
const AnsiString Inie4="mot4";
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
                : TForm(Owner)
{
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
/* Création de la fenêtre modale via la création de sa classe */
Param1* p=new Param1();

/* C'est c'est OK qui est choisi on met à jour le fichier ini
prévu à cet effet */
if(p->f->ShowModal()==mrOk)
  {
TIniFile *ini;
  int Choix;
  ini = new TIniFile(ChangeFileExt(Application->ExeName, ExtIni));
  if(p->choixA->Checked) Choix=1;
  if(p->choixB->Checked) Choix=2;
  if(p->choixC->Checked) Choix=3;

  /* mémorisation du choix dans le fichier ini */
  ini->WriteInteger(Param, IniABC, Choix);

  delete ini;

  }

/* Destruction de la fenêtre modale
Tous les composants de la fenêtre sont détruits automatiquement car
la fenêtre en est propriétaire
*/
delete p;
}
//---------------------------------------------------------------------------
/* Constructeur, création de la fenêtre et de ses composants */
Param1::Param1()
{
f=new TForm(Form1);
f->Width=400;
f->Height=300;

r=new TRadioGroup(f);
r->Parent=f;
r->Width=200;
r->Height=200;
r->Top=10;
r->Left=10;

choixA=new TRadioButton(r);
choixA->Parent=r;
choixA->Top=10;
choixA->Left=10;
choixA->Caption="Choix A";

choixB=new TRadioButton(r);
choixB->Parent=r;
choixB->Top=30;
choixB->Left=10;
choixB->Caption="Choix B";

choixC=new TRadioButton(r);
choixC->Parent=r;
choixC->Top=50;
choixC->Left=10;
choixC->Caption="Choix C";

OK=new TButton(f);
OK->Parent=f;
OK->Top=50;
OK->Left=300;
OK->Caption="OK";
OK->ModalResult=mrOk;

Annul=new TButton(f);
Annul->Parent=f;
Annul->Top=80;
Annul->Left=300;
Annul->Caption="Annuler";
Annul->ModalResult=mrCancel;

/* Choix par défaut lu dans le fichier ini s'il existe */
TIniFile *ini;
int Choix;
ini = new TIniFile(ChangeFileExt(Application->ExeName, ExtIni));

/* choix A lors de la toute première utilisation */
Choix=ini->ReadInteger(Param, IniABC, 1);
switch(Choix)
  {
  case 1 : choixA->Checked=true;
            break;
  case 2 : choixB->Checked=true;
            break;
  case 3 : choixC->Checked=true;
            break;
  }

delete ini;
}
//---------------------------------------------------------------------------
/* Le destructeur ne détruit que la fenêtre */
Param1::~Param1()
{
delete f;
}
//---------------------------------------------------------------------------
/* Constructeur, création de la fenêtre et de ses composants */
Param2::Param2()
{
f=new TForm(Form1);
f->Width=400;
f->Height=300;

l1=new TLabel(f);
l1->Parent=f;
l1->Top=10;
l1->Left=10;
l1->Caption="Paramètre 1";

e1=new TEdit(f);
e1->Parent=f;
e1->Left=90;
e1->Top=10;
e1->Width=130;
e1->Height=15;
e1->OnExit=PostUPD1;

l2=new TLabel(f);
l2->Parent=f;
l2->Top=30;
l2->Left=10;
l2->Caption="Paramètre 2";

e2=new TEdit(f);
e2->Parent=f;
e2->Left=90;
e2->Top=30;
e2->Width=130;
e2->Height=15;
e2->OnExit=PostUPD2;

l3=new TLabel(f);
l3->Parent=f;
l3->Top=50;
l3->Left=10;
l3->Caption="Paramètre 3";

e3=new TEdit(f);
e3->Parent=f;
e3->Left=90;
e3->Top=50;
e3->Width=130;
e3->Height=15;
e3->OnExit=PostUPD3;

l4=new TLabel(f);
l4->Parent=f;
l4->Top=70;
l4->Left=10;
l4->Caption="Paramètre 4";

e4=new TEdit(f);
e4->Parent=f;
e4->Left=90;
e4->Top=70;
e4->Width=130;
e4->Height=15;
e4->OnExit=PostUPD4;

OK=new TButton(f);
OK->Parent=f;
OK->Top=50;
OK->Left=300;
OK->Caption="OK";
OK->ModalResult=mrOk;

Annul=new TButton(f);
Annul->Parent=f;
Annul->Top=80;
Annul->Left=300;
Annul->Caption="Annuler";
Annul->ModalResult=mrCancel;

/* Choix par défaut lu dans le fichier ini s'il existe */
TIniFile *ini;
int Choix;
ini = new TIniFile(ChangeFileExt(Application->ExeName, ExtIni));

e1->Text=ini->ReadString(Param, Inie1, "MOT 1");
e2->Text=ini->ReadString(Param, Inie2, "MOT 2");
e3->Text=ini->ReadString(Param, Inie3, "MOT 3");
e4->Text=ini->ReadString(Param, Inie4, "MOT 4");

delete ini;
}
//---------------------------------------------------------------------------
/* Le destructeur ne détruit que la fenêtre */
Param2::~Param2()
{
delete f;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
/* Création de la fenêtre modale via la création de sa classe */
Param2* p=new Param2();

/* C'est c'est OK qui est choisi on met à jour le fichier ini
prévu à cet effet */
if(p->f->ShowModal()==mrOk)
  {
  TIniFile *ini;
  int Choix;
  ini = new TIniFile(ChangeFileExt(Application->ExeName, ExtIni));

  /* mémorisation des choix dans le fichier ini */
  ini->WriteString(Param, Inie1, p->e1->Text);
  ini->WriteString(Param, Inie2, p->e2->Text);
  ini->WriteString(Param, Inie3, p->e3->Text);
  ini->WriteString(Param, Inie4, p->e4->Text);

  delete ini;

  }

/* Destruction de la fenêtre modale
Tous les composants de la fenêtre sont détruits automatiquement car
la fenêtre en est propriétaire
*/
delete p;
}
//---------------------------------------------------------------------------
void _fastcall Param2::PostUPD1(TObject *O)
{
AnsiString M="Attention, les paramètres "
              "doivent avoir au moins quatre caractères, "
              "le premier paramètre est fautif.";

AnsiString T="Longueur des paramères";
if(strlen(e1->Text.c_str())<4)
  {
  Application->MessageBox(M.c_str(),T.c_str(),MB_OK);
  e1->SetFocus();
  }
}
//---------------------------------------------------------------------------
void _fastcall Param2::PostUPD2(TObject *O)
{
AnsiString M="Attention, les paramètres "
              "doivent avoir au moins quatre caractères, "
              "le deuxième paramètre est fautif.";

AnsiString T="Longueur des paramères";
if(strlen(e2->Text.c_str())<4)
  {
  Application->MessageBox(M.c_str(),T.c_str(),MB_OK);
  e2->SetFocus();
  }
}
//---------------------------------------------------------------------------
void _fastcall Param2::PostUPD3(TObject *O)
{
AnsiString M="Attention, les paramètres "
              "doivent avoir au moins quatre caractères, "
              "le troisième paramètre est fautif.";

AnsiString T="Longueur des paramères";
if(strlen(e3->Text.c_str())<4)
  {
  Application->MessageBox(M.c_str(),T.c_str(),MB_OK);
  e3->SetFocus();
  }
}
//---------------------------------------------------------------------------
void _fastcall Param2::PostUPD4(TObject *O)
{
AnsiString M="Attention, les paramètres "
              "doivent avoir au moins quatre caractères, "
              "le quatrième paramètre est fautif.";

AnsiString T="Longueur des paramères";
if(strlen(e4->Text.c_str())<4)
  {
  Application->MessageBox(M.c_str(),T.c_str(),MB_OK);
  e4->SetFocus();
  }
}

On pourra utiliser cette technique pour les fenêtres du type AboutBox ("À propos de") qui donne des informations sur le logiciel avec la date et la version. Cette fenêtre n'aura que le bouton Ok, il ne sera donc pas nécessaire de tester la valeur de retour de la méthode ShowModal(), on se contentera d'écrire p->f->ShowModal(); (ou directement f->ShowModal(); si vous préférez ne pas déclarer de classe associée à cette fenêtre). Pour une meilleure présentation, il sera bon de passer par des TLabel pour afficher les phrases car chaque TLabel a une Font et une couleur de Font. Si le panel contenait par exemple un TPaintBox, il n'y aurait qu'une seule couleur et qu'une seule police de caractères (et il faudrait aussi créer la méthode OnPaint sans laquelle rien ne se dessinerait dans la PaintBox, auquel cas d'ailleurs la déclaration d'une classe deviendrait nécessaire). Si la fenêtre AboutBox contient plusieurs phrases, le mieux sera de créer un panel et d'y inclure des TLabel en leur donnant des coordonées précises par rapport à la surface client du panel (on ne renseignera donc pas la propriété Align des TLabel et notamment surtout pas alClient qui signifie que la label prend toute la surface client du panel, or ici chaque label est libre), dans ce cas vous aurez autant de couleurs de que TLabel et il ne sera pas nécessaire de déclarer une méthode OnPaint.

74. Application multilingue

Voici une solution possible assez facile pour rendre un logiciel multilingue. L’exemple suivant est bilingue français/anglais mais le principe pourrait facilement s’étendre à un nombre plus important de langues. On commence par construire la liste des mots ou phrases dans une très longue chaîne de caractères, par exemple :

const char TM[]=
//0
"Bonjour\0"
"Good morning\0"

//1
"Bonsoir\0"
"Good evening\0"

//2
"Voici un long message\0"
"Here is a long message\0"

//3
"Le saviez-vous?\0"
"Did you know?\0"

//4
"Voir le message au démarrage\0"
"See the message on startup\0"

//Préparer quelques structures d'avances pour les futurs ajouts
//5
"\0"
"\0"

//6
"\0"
"\0"

//7
"\0"
"\0"

//8
"\0"
"\0"

//9
"\0"
"\0"

;// fin de la suite des messages

Comme il y a deux langues ici, on écrit les occurrences par groupe de deux, le premier élément de chaque groupe est en français et le second en anglais. Chaque mot se termine par \0 qui est le code fin de chaque mot ou expression. On voit que le tableau TM (Tous les Messages) est en fait unidimensionnel et qu’ainsi la longueur de chaque mot est arbitraire, elle se termine simplement par zéro. Par cette méthode on évite un tableau structuré qui perdrait beaucoup de place car il faudrait surdimensionner ou du moins imposer comme longueur la plus grande longueur, ce qui serait assez pénible car à chaque ajout, il faudrait se poser la question de savoir si on ne dépasse pas cette limite et la modifier le cas échéant. En procédant ainsi, rien de tel car le tableau n’est pas structuré mais il va falloir écrire une petite fonction qui va nous renvoyer la chaîne numéro n dans la bonne langue. Pour ce faire, on va utiliser une constante qui dira le nombre de langues en présence, ici deux :

const unsigned char NbLang=2;

On va aussi utiliser un indice qui dira quelle est la langue sélectionnée. La variable IndLang sera donc à 0 pour le français (premier mot de chaque groupe de deux) ou à 1 (deuxième mot de chaque groupe). Comme l’alinéa précédent a montré comment créer une fenêtre modale de paramètres, il vous sera aisé de compléter cet exemple en créant une fenêtre dynamique avec un Radio group, deux radio boutons, un pour le français et un pour l’anglais, un bouton OK et un bouton Annuler. Dans l’exemple suivant, on se contente (car le but est simplement de montrer qu’on accède avec très peu d’effort à un logiciel multilingue) d’initialiser IndLang dans le constructeur de TForm1 (0 pour le français et 1 pour l’anglais). Dans ces conditions, voici la fonction magique qui va nous renvoyez l’AnsiString correspondant à un numéro de message demandé (cet indice virtuel part ici de zéro, pour n messages l’indice sera donc compris entre 0 et n-1) dans la langue IndLang.

AnsiString Mess(const char* p,
              unsigned int n,
              unsigned char l)
{
AnsiString A;
bool ok=true;
unsigned int i=0, c=0, j;
char car;

while(ok)
  {
  if(c==n)
    {
    while(l--) while(p[i++]);
    while((car=p[i++])!=0) A+=car;
    ok=false;
    }
  else
    {
    for(j=0 ;j<NbLang ;j++) while(p[i++]);
    c++;
    }
}
return A;
}

Voilà donc le petit effort auquel il a fallu consentir en échange d’un tableau linéaire non structuré à savoir écrire cette fonction qui renvoie l’occurrence numéro n en langue l d’une grande chaîne censément pointée par p. Cette fonction peut donc être utilisée pour un autre type de chaîne puisqu’elle reçoit en argument le pointeur p qui pointe le premier caractère d’une chaîne ainsi composée. Le principe est assez simple. La variable booléenne ok est initialisée à true (bool ok=true;) et la boucle générale en while s’exécute aussi longtemps que ok est à true (while(ok)). Le compteur c a été initialisé à 0 (c=0) et le pointeur i est aussi initialisé à 0 (i=0). Il est vrai que i n’est pas un pointeur, c’est simplement un entier mais en réalité on peut dire (par abus de langage) que i pointe le premier caractère de la chaîne puisque p[i] est égal à ce caractère. La boucle générale en while commence, si c est égal à n (if(c==n)) alors on pointe le premier caractère du bon groupe. Il faut maintenant pointer le mot de la bonne langue c’est-à-dire qu’il faut passer au mot suivant l fois, ce qui s’exprime par une double boucle en while (while(l--) while(p[i++]);). Imaginons que l soit égal à 0, alors rien n’est exécuté du tout, ce qui est logique car alors i pointe le premier caractère du bon mot. Si l=1, la seconde boucle en while (while(p[i++]);) va s’exécuter une fois. Or, que fait cette boucle, elle nous fait pointer tout simplement au mot suivant. On voit donc que cette double boucle en while signifie de passer au mot suivant l fois. Après cette double boucle en while, i pointe le premier caractère du bon mot dans la bonne langue donc on concatène dans l’AnsiString A, caractère après caractère, le mot (while((car=p[i++])!=0) A+=car;), ce qui signifie que tant que le caractère pointé par i est un vrai caractère (i.e. un octet non nul), ajouter ce caractère à A. Après cette boucle, A contient le bon mot dans la bonne langue. On met ok à false, ce qui stoppera la boucle générale en while. Si c n’est pas égal à n (else), il faut alors pointer le groupe de mots suivant c’est-à-dire aller NbLang fois au mot suivant (for(j=0 ;j<NbLang ;j++) while(p[i++]);). Après cette boucle, i pointe le groupe de mots suivant, on incrémente c (c++;) et on rend la main à la boucle générale qui va tester si c est maintenant égal à n. Au sortir de la boucle générale en while, l’AnsiString A contient le bon mot dans la bonne langue, on le renvoie à l’appelant (return A;).

Dans ces conditions, Mess(TM,NumMess,IndLang) représentera l’occurrence numéro NumMess en langue IndLang de la chaîne TM. Ainsi, en développant votre logiciel, vous aurez une fonction unique qui mettra à jour tous les captions (libellés de composants), hints (bulles d’aide) ainsi que les noms de fichiers (car il y aura alors un fichier d’aide par langue). Le constructeur de TFom1 lira dans le fichier ini la langue en cours et en déduira toutes les initialisations dans la bonne langue. Notez qu’en principe, le tableau de mots va s’enrichir petit à petit au fur et à mesure du développement et que les indices de ces mots ne vont pas changer. Donc, il est tout à fait acceptable de les écrire " en dur " dans la fonction. Si par exemple vous avez déjà 23 mots et que l’indice 24 correspond au Caption d’un composant, vous écrirez simplement Composant->Caption=Mess(TM,24,IndLang) ;

Nous avons précisé que cette fonction reçoit le pointeur p en argument. Donc on peut utiliser la même fonction Mess pour des séquences de mots différentes. C’est ce qu’exploite l’exemple suivant qui utilise deux types de chaîne composée. La première pour les mots assez courts et qu’on utilisera pour tout ce qui est Caption (libellé de composant), Hint (bulle d’aide) ou encore nom de fichier (par exemple les fichiers d’aide auront des noms différents en fonction de la langue). Dans cet exemple nous ne faisons qu’afficher un mot chaque fois différent suite au clic d’un bouton. La deuxième va se constituer de phrases plus complètes à titre d’astuce du jour. La constante NbAst indique simplement le nombre d’astuces (ou de mots de démarrage) présentes dans la chaîne composée Ast. Un numéro sera chaque fois incrémenté modulo NbAst dans le fichier ini, ainsi au démarrage de l’application, on saura le numéro d’astuce à afficher. La fenêtre créée dynamiquement permettra de déflaguer le bouton à cocher pour ne plus afficher de message au démarrage de l’application, valeur booléenne enregistrée dans le fichier ini et testée dans la fonction OnActivate. Remarquez que la fenêtre modale qui affichera le mot de démarrage ne résulte pas d’une classe, les pointeurs sont simplement déclarés à l’intérieur de la fonction. En réalité, cela prouve qu’une classe n’est pas absolument nécessaire, elle est simplement obligatoire quand un composant est associé à un événement. Dans l’alinéa précédent, la classe Param2 avait des TEdit, chaque TEdit avait l’événement OnExit associé à une méthode de Param2. Dans ce cas une classe est obligatoire ou pour être plus précis, l’événement est forcément méthode d’une classe. Si donc vous ne déclarez pas de classe pour la fenêtre et qu’un composant est lié à un événement, il faudra alors que cet événement soit méthode d’une classe par exemple celle de TForm1. Le mieux est encore, comme nous le proposions de déclarer une classe par fenêtre s’il y a des événements à gérer. Sinon, on peut procéder comme ci-dessous car il n’y a que des pointeurs. L'exemple ci-dessous fonctionne avec simplement un bouton sur la forme principale.

#include <vcl.h>
#pragma hdrstop

#include "Maitre.h"
/* nécessaire pour les fichiers ini */
#include <inifiles.hpp>
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
unsigned char IndLang;
unsigned int NumMess;
AnsiString Mess(const char*, unsigned int, unsigned char);
//---------------------------------------------------------------------------
const AnsiString ExtIni=".ini";
const AnsiString Param="Parametres";
const AnsiString IniNumMess="Message";
const AnsiString IniAff="Affichage";
//---------------------------------------------------------------------------
/* Tous les messages du logiciel */
const unsigned char NbMess=5;
const unsigned char NbLang=2;
const char TM[]=
//0
Fenêtre\0"
Window\0"

//1
"Cliquez ici\0"
"Click here\0"

//2
"Bonjour\0"
"Good morning\0"

//3
"Le saviez-vous?\0"
"Did you know?\0"

//4
"Voir le message au démarrage\0"
"See the message on startup\0"

//Préparer quelques structures d'avances pour les futurs ajouts
//5
"\0"
"\0"

//6
"\0"
"\0"

//7
"\0"
"\0"

//8
"\0"
"\0"

//9
"\0"
"\0"

;// fin de la suite des messages
//---------------------------------------------------------------------------
/* Les astuces du logiciel */
const unsigned char NbAst=3;
const char Ast[]=
//0
"VCL signifie librairie de composants visuels.\0"
"VCL means Visual Components library.\0"

//1
"Vous pouvez aussi utiliser le menu.\0"
"You can also use the menu.\0"

//2
"Message numéro 2 en français "
"qui peut s'écrire sur plusieurs lignes\0"
"Message number 2 in english "
"which can be written on few lines\0"

//3
"Message numéro 3 en français\0"
"Message number 3 in english\0"


;// Fin de la suite des messages

//---------------------------------------------------------------------------
AnsiString AffMess(const char[], unsigned int, unsigned char);
//---------------------------------------------------------------------------
void _fastcall AstuceDuJour(int n, unsigned char langue)
{
/* AStuce à afficher dans A */
AnsiString A=Mess(Ast,n,langue);
TIniFile *ini;
ini = new TIniFile(ChangeFileExt(Application->ExeName, ExtIni));


TForm* f;
TPanel* p;
TLabel* l;
TLabel* t;
TButton* b;
TCheckBox* c;

f=new TForm(Application);
f->Position=poScreenCenter;
f->Width=300;
f->Height=200;
f->BorderStyle=bsDialog;
f->Color=clAqua;
f->BorderIcons=f->BorderIcons>>biSystemMenu;

/* Titre, messaqe d'indice 3 */
t=new TLabel(f);
t->Parent=f;
t->Top=2;
t->Left=10;
t->Font->Size=16;
t->Font->Style>>fsBold;
t->Caption=Mess(TM,3,IndLang);

p=new TPanel(f);
p->Parent=f;
p->Color=clWhite;
p->Width=260;
p->Height=100;
p->Left=10;
p->Top=30;
p->BevelInner=bvNone;
p->BevelOuter=bvRaised;
p->BevelWidth=6;
p->BorderWidth=10;

l=new TLabel(f);
l->Parent=p;
l->Align=alClient;
l->WordWrap=true;
l->Font->Name ="Courier";
l->Font->Color=clRed;
l->Caption=A;

/* CheckButton actif lors de la première utilisation */
c=new TCheckBox(f);
c->Parent=f;
c->Top=140;
c->Left=10;
c->Width=250;
c->Checked=ini->ReadBool(Param, IniAff, true);
c->Caption=Mess(TM,4,IndLang);

b=new TButton(f);
b->Parent=f;
b->Caption="OK";
b->Left=180;
b->Top=140;
b->ModalResult=mrOk;

/* Affiche l'astuce de démarrage */
f->ShowModal();

/* Enregistre dans le fichier ini l'état du CheckButton pour savoir
si la prochaine astuce doit être affichée la prochaine fois */
ini->WriteBool(Param, IniAff, c->Checked);
delete ini;

/* Libération de la fenêtre et de ses composants */
delete f;
}
//---------------------------------------------------------------------------
void _fastcall InitLangue()
{
/* init en fonction de la langue
Comme les mots de la table non structurée apparaissent petit à petit
au fur et à mesure du développement, on peut écrire "en dur" les indices
car ils ne vont jamais changer. Dans ce petit exemple, il n'y a que deux
affectations mais pour un logiciel complet, il y en aura beaucoup plus
(captions, hints, texte, nom de fichiers et autres)
*/
Form1->Caption=Mess(TM,0,IndLang);
Form1->Button1->Caption=Mess(TM,1,IndLang);
}
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
               : TForm(Owner)
{

/* Messages en français (1 pour anglais)
Dans une vraie application, cette variable doit être lue dans
le fichier INI
*/
IndLang=0;

/* Init numéro du message à afficher avec le bouton */
NumMess=0;

/* Positionne les captions */
InitLangue();

}
//---------------------------------------------------------------------------
AnsiString Mess(const char* p,
               unsigned int n,
               unsigned char l)
{
AnsiString A;
bool ok=true;
unsigned int i=0, c=0, j;
char car;

while(ok)
  {
    if(c==n)
    {
    while(l--) while(p[i++]);
    while((car=p[i++])!=0) A+=car;
    ok=false;
    }
  else
    {
    for(j=0;j<NbLang;j++) while(p[i++]);
    c++;
    }
  }
return A;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
/* Affiche le message NumMess dans la bonne langue
L'affiche prouve simplement qu'on accède bien aux messages corrects
par la fonction Mess.
Pour changer de langue, modifier IndLang dans le constructeur de TForm1
*/
Application->MessageBox(Mess(TM,NumMess,IndLang).c_str(),
              "Message",MB_OK);

/* Si tous les messages ont été affichés, revenir au
premier message d'indice 0 */
if(++NumMess==NbMess) NumMess=0;

}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormActivate(TObject *Sender)
{
TIniFile *ini;
int NM;
bool b;
ini = new TIniFile(ChangeFileExt(Application->ExeName, ExtIni));

/* Numéro du message à afficher au début du logiciel */
NM=ini->ReadInteger(Param, IniNumMess, 0);

/* dit si oui ou non on affiche le message */
b=ini->ReadBool(Param, IniAff, true);

if(b) AstuceDuJour(NM,IndLang);

/* Mise à jour de ce numéro pour la prochaine exécution */
if(++NM==NbAst)NM=0;
ini->WriteInteger(Param, IniNumMess, NM);

delete ini;
}

-:-

Une fonction complète :

création d'un dictionnaire.

Je vous livre une fonction complète pour C++ Builder 5, la création d'un dictionnaire pour Word par exemple. Quand vous chargez un texte dans une langue étrangère, Word vous souligne pratiquement tous les mots et il est assez fastidieux d'entrer un à un tous les mots pour se constituer un dictionnaire perso. Donc, l'idée est la suivante. Vous disposez d'un texte de préférence assez long, par exemple en anglais qui ne contient aucune faute, c'est pour vous une certitude, vous l'avez bien vérifié. Convertissez ce fichier en point txt i.e. faites sous Word "enregistrer sous" et choisissez le format texte. Vous donnez au programme le nom de ce fichier txt (il faut l'écrire en dur dans le code à l'endroit où c'est écrit en bleu car cet exemple n'est pas vraiment un tutoriel, si vous maîtriser bien C++ Builder vous pouvez facilement améliorer en utilisant un OpenDialog), la fonction va créer pour vous le dictionnaire Dico.dic (sinon choisissez un autre nom) qui contiendra tous les mots différents du ou des fichiers donnés en entrée. Le temps d'exécution dépend de la longueur du ou des fichiers que vous allez donner au programme en référence. Vous pouvez en donner plusieurs. Si vous avez déjà un dictionnaire, donnez-le également en entrée de ce programme de façon à ce que le dictionnaire final que crée le programme contienne aussi les occurrences du dico que vous possédez déjà. Vous pouvez ainsi enrichir de proche en proche votre dictionnaire. Vous donnez au programme et un nouveau fichier contenant un grand nombre de mots nouveaux et votre dictionnaire actuel.

Vous n'avez plus qu'à mettre ce nouveau dictionnaire dans le bon répertoire et de l'activer dans Word : outil > option >grammaire et orthographe >dictionnaires, c'est là que vous déclarez vos dictionnaires perso.

Si dans la langue en question vous avez des caractères spéciaux, adaptez la fonction ci-dessous en conséquence. Il faut connaître les codes ASCII de ces caractères à accepter dans les mots et ajouter des clauses case (voir code en bleu). Pour connaître ces codes ASCII, faites démarrer>programmes>accessoires>outil système>table de caractères. Pour les caractères spéciaux, cliquez la case de ce caractère et regardez ce qui est écrit en bas à droite de la fenêtre, vous lisez "ALT nnn" où nnn est le code ASCII de ce caractère. Il suffit de rajouter un case avec ce nombre décimal et ce caractère sera accepté dans le dictionnaire. Si vous ne lisez pas "ALT nnn" (le caractère est alors simplement affiché dans cette zone, cela signifie qu'il est directement accessible au clavier), il faut alors déduire le code ASCII de la case, ce qui est très facile puisqu'ils sont dans l'ordre. Par exemple pour le a minuscule accent grave, ALT nnn n'est pas affiché mais comme a minuscule accent aigu donne ALT 225 et que c'est la case suivante, on en déduit facilement que le code ASCII du a minuscule accent grave est 224.

Un copier-coller sauvage et un appel à la fonction CREDIC doit vous permettre d'utiliser rapidement cette fonction et de l'adapter selon vos besoins.

#include <vcl.h>
#pragma hdrstop

#include <string.h>
#include <dstring.h>
#include <stdio.h>
#include <sys\stat.h>
#include <stdlib.h>
#include <dir.h>

//----------------------
void CREDIC (void);
//----------------------

const int bloc=10000;
const int LongMot=50;

void TraiteMot(char*,char*&,long int&,long int&);
bool Existe(char*,char*,long int);
bool Ecrit(char*,unsigned char*&,FILE*,long int&,long int&);
bool accepte(unsigned char);
void InsFic(char*,unsigned char*&,long int&,long int&);

void CREDIC (void)
{
FILE *Fichier;
long int c,i,j,L=0,ok,e,lde,NbF;
long int Longueur;
unsigned char *P1=NULL,*P2=NULL;
char mot[LongMot+1];


Application->MessageBox("On va commencer","Début",MB_OK);

/* Chargement du ou des fichiers de base */
i=0;
/* Nom du fichier de référence */
InsFic("
c:\\program files\\borland\\CBuilder5\\projects\\gilles\\toto.txt",P1,L,i);

/* Insérez ici d'autres appels à InsFic de manière à concaténer les fichiers
qui servent de référence. Mettez-y notamment votre dico personnel,
vous n'aurez ainsi rien perdu de cet existant qui fera logiquement
partie dans ces conditions du fichier final */

// Longueur total en octets du texte de référence
Longueur=i;

if(!i)
   {
   Application->MessageBox("Longueur nulle, je ne peux rien faire",NULL,MB_OK);
   return;
   }

/* Création du dictionnaire, cette création se fera dans le répertoire
courant où vous travaillez avec C++ Builder. Rajoutez le chemin
si vous voulez mettre ce dico de résultat ailleurs */
if((Fichier=fopen("
Dico.dic","w"))==NULL)
   {
   Application->MessageBox("Je ne peux pas créer le dico",NULL,MB_OK);
   return;
   }

i=0;
j=0;
ok=1;
e=0;
lde=0;
while(ok)
  {
  /* si le mot est trop long, on l'ignore
  Ce cas devrait ne jamais se produire mais l'algorithme se tient
  sur ses gardes */
  if(j>LongMot) j=0;

  if(accepte((P1[i]))) mot[j++]=P1[i++];

  else
  {
  mot[j]=0;
  if ((!Existe(mot,P2,e)) && (j>=2))
  if(!Ecrit(mot,P2,Fichier,e,lde))
   {
   Application->MessageBox("Réallocation d'écriture refusée",NULL,MB_OK);
   return;
   }
  j=0;
  i++;
  }

  /* si on pointe la fin de la zone où se trouve écrit le ou les fichiers
  de base, on termine alors la boucle en while */
  if(i==Longueur) ok=0;
  }// fin du while

fclose(Fichier);
free(P1);
free(P2);
Application->MessageBox("C'est fini","Fin",MB_OK);
}// fin du main

//-------------------
void InsFic(char* NomFic,unsigned char*& P, long int& L,long int& i)
{
FILE* Fichier;
long int c;

if((Fichier=fopen(NomFic,"rb"))==NULL)
  {
  Application->MessageBox("Je ne peux pas ouvrir le   fichier",NULL,MB_OK);
  return;
  }
while((c=fgetc(Fichier))!=EOF)
  {
  if(i==L) P=(char*) realloc(P,L+=bloc);
  P[i++]=c;
  }
fclose(Fichier);
}

//-------------------
bool accepte(unsigned char c)
{
// Majuscules
if((c>=65)&&(c<=90)) return true;

// Minuscules
if((c>=97)&&(c<=122)) return true;

// Caractères isolés supplémentaires
switch(c)
{
case 230: // e dans l'a
case 198: // E dans l'A
case 156: // e dans l'o
case 140: // E dans l'O

case 192: // A accent grave
case 224: // a accent grave
case 226: // a accent circonflexe
case 194: // A accent circonflexe
case 228: // a tréma
case 196: // A tréma

case 199: // C cédille
case 231: // c cédille

case 200: // E accent grave
case 232: // e accent grave
case 201: // E accent aigu
case 233: // e accent aigu
case 234: // e accent circonflexe
case 202: // E accent circonflexe
case 235: // e tréma
case 203: // E tréma

case 238: // i accent circonflexe
case 206: // I accent circonflexe
case 239: // i tréma
case 207: // I tréma

case 210: // O accent grave
case 242: // o accent grave
case 244: // o accent circonflexe
case 212: // O accent circonflexe
case 246: // o tréma
case 214: // O tréma

case 217: // U accent grave
case 249: // u accent grave
case 251: // u accent circonflexe
case 219: // U accent circonflexe
case 252: // u tréma
case 220: // U tréma

case 130: // apostrophe

return true;
}

/* sinon le caractère n'est pas acceptable,
on pointe probablement une zone hors texte du fichier.
Ce cas ne peut normalement pas se produire si le fichier
de référence est du type point txt */
return false;
}
//-------------------
bool Existe(char* P1, char* P2,long int e)
{
long int i=0,j=0,ok=1;
char l;
bool Rep;

if(!e) return false;

while(ok)
{
if((l=P1[i++])==P2[j++])
  {
  if(!l)
   {
   ok=0;
   Rep=true;
   }
  }

else

  {
  i=0;
  while(P2[j++]);
  if(j>e)
   {
   Rep=false;
   ok=0;
   }
  }
}// fin du while
return Rep;
}
//-------------------
bool Ecrit(char* P1,unsigned char*& P2,FILE* F , long int& e,long int& L)
{
long int i=0,ok=1;
unsigned char c,Retour=10;

while(ok)
 {
 if(e==L)
  {
  L+=bloc;
  if((P2=(char*)realloc(P2,L))==NULL) return false;
  }
 c=P1[i++];
 if (c) fputc(c,F); else fputc(Retour,F);
 P2[e++]=c;
 if(!c) ok=0;
 }//fin du while
return true;
}

-:-

Si vous visitez mon site perso ( http://perso.club-internet.fr/glouise/ ) vous comprendrez que j'ai facilement pu me créer un dictionnaire de latin sans avoir à cliquer tous les mots sous Word.

par Gilles Louise

Septembre 2000

Hit-Parade