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.