Remarques de développement avec C++ Builder 5
Partie II - par
Gilles Louise
31. Création de fichiers d'aide
32. Autre exemple lié à des allocations
33. Conversion d'un tableau de longueur fixe en tableau de longueur variable
34. Gestion de versions et retour à une situation antérieure
35. Nom des fichiers
36. Plein écran
37. Écriture d'un entier long par un pointeur de type char*
38. Réalisation de condition
39. Le composant Image
40. Bitmap hors écran
41. Savoir utiliser l'aide en ligne
42. Envoi à une fonction d'un tableau à plusieurs dimensions
43. La classe TSmallIntArray (version Entreprise uniquement)
44. La classe TStringList
45. La classe TListBox
Retour à la partie I
Continuer : Partie III
31. Création de fichiers d'aide
On crée les fichiers d'aide sous éditeur de texte par exemple Word qui est parfait pour ce travail, on le sauvegarde au format RTF puis on le compile avec hwc (Help WorkShop Copyright) dit aussi "WinHelp", utilitaire que vous trouverez dans le répertoire Program Files\Borland\Cbuilder5\Help\Tools. Ce fichier d'aide étant créé, on l'associe au projet C++Builder qui est à même alors d'afficher des pages d'aide.
Commencez par créer un répertoire "Test" à l'intérieur de "Program Files\Borland\Cbuilder5\Help\Tools", nous allons tester le mécanisme dans ce répertoire nouveau.
Sous Word, trois signes sont à connaître :
# annonce l'identifiant de la rubrique
$ annonce le titre de la rubrique
K annonce les mots clés de la rubrique.
Ces trois signes s'associent à des notes de bas de page, c'est dans ces notes qu'on écrit les renseignements. En général, chaque page commencera par trois notes de bas de page, une note avec # pour identifier la page, une note avec $ pour donner son titre et une notre avec K pour indiquer les mots clés de la rubrique, donc chaque page commencera par ces trois signes "#$K". Vous pouvez très bien créer ces notes sans les renseigner entièrement dans un premier temps, par exemple vous n'êtes pas censé connaître tous les mots clés avant d'avoir rédigé votre rubrique mais il est bon de créer les notes de bas de page quand même. Ces trois signes "#$K" sont des codes à l'intention du compilateur, ils seront invisibles dans la page finale, ils indiquent au compilateur que la note associée contient des renseignements. Une fois la page rédigée, vous appelez vos notes de bas de page en plaçant le curseur sur un de ces signes #$K, Word vous affiche en bas de l'écran les notes et vous les complétez alors.
Chaque sujet est délimité par un saut de page, on obtient un saut de page par ctrl-entrée (contrôle Entrée). Donc après avoir saisi une page, on fait "contrôle Entrée", une ligne se dessine à l'écran avec en son milieu "saut de page".
Deux fonctions sont à connaître :
ALT+CTRL+U qui crée un double soulignement pour un lien hypertexte
CTRL+MAJ+U qui crée un texte masqué pour le lien réel non vu.
Le double soulignement crée un lien hypertexte donc une petite main se dessinera au moment où le curseur passera sur le mot ou le groupe de mots doublement soulignés et le texte invisible est le lien réel c'est-à-dire l'identifiant correspondant au mot double-souligné. Le mieux consiste à écrire ces deux renseignements à la suite sous Word puis sélectionner la première partie pour procéder au double soulignement puis à la deuxième pour le lien caché. Par exemple on écrit "page 1IDH_P1", les deux zones sont "Page 1" et "IDH_P1", on sélectionne "Page 1" et on double-souligne, on sélectionne "IDH_P1" et on masque. À l'exécution du fichier compilé, "Page 1" deviendra un lien hypertexte, donc une main se dessinera et le click sur ce mot fera afficher la page visée par l'identifiant IDH_P1 non visible à l'exécution du fichier compilé.
Nous allons créer un petit exemple de deux pages avec un lien pour chacune d'elle vers l'autre, la page 1 aura donc un lien hypertexte vers la page 2 et inversement.
Sous Word faites "Fichier|Nouveau" ou cliquez l'icône blanc (bulle d'aide "Nouveau") pour partir d'une feuille complètement vide. Là, nous créez immédiatement une note de bas de page par "Insertion|Note" puis cliquez sur le bouton "personnalisée" puis entrez le signe # (dièse) dans la case blanche et validez par OK. On vient donc de créer une note de bas de page avec un signe spécial à savoir # que saura interpréter le compilateur d'un tel fichier. Il s'agit maintenant de donner l'identifiant de cette rubrique d'aide. Cet identifiant doit commencer par IDH_ pour être accessible par programme. Si vous développez un fichier d'aide non lié à un programme, ce n'est pas obligatoire mais si votre fichier d'aide, en plus d'être utilisable directement une fois créé, doit être accessible à une application C++Builder, il faut alors que son identifiant commence par ces quatre caractères IDH_ que saura interpréter le compilateur WinHelp. Donc après le # dans la note de bas de page, écrivons IDH_P1 où P1 signifiera pour nous "page 1". Notre note de bas de page est donc créée. Revenons maintenant à la zone texte (où le # est aussi visible) et écrivons juste après le # "Première page" puis en dessous on écrira "lien vers la page 2IDH_P2.". Là vous sélectionnez "page 2" de ce texte et vous le double-soulignez par ALT+CTRL+U puis vous sélectionnez IDH_P2 et vous le rendez invisible par CTRL+MAJ+U. Créez un saut de page (contrôle Entrée) à la ligne suivante et recommencez pour la page 2. Créez une note de page avec #, identifiez-là par IDH_P2. Puis écrivez après le # "Deuxième page" puis en dessous "lien vers la page 1IDH_P1." puis double-soulignez "Page 1" et rendez invisible "IDH_P1". On vient donc de créer deux petites pages qui contiennent un lien vers l'autre.
Voici donc ce fichier :
#Première page
Lien vers la page 2IDH_P2.
-------------------------------------------saut de page---------------------
#Deuxième page
Lien vers la page 1IDH_P1.
et les deux notes de bas de page
# IDH_P1
# IDH_P2
Ce petit fichier d'aide étant créé, faites "Fichier|Enregistrez sous" en choisissant comme nom "Test" au format "Texte mis en forme (RTF)" dans notre répertoire "Test". On a donc créé le fichier Test.rtf. Exécutons maintenant le programme hcw.exe (WinHelp) dans le répertoire Program Files\Borland\Cbuilder5\Help\Tools. Faites "File|New" puis sélectionnez "Help Project" (c'est d'ailleurs la sélection par défaut), vous tombez sur une fenêtre qui attend un nom de projet et un chemin, positionnez-vous dans votre répertoire de test (Program Files\Borland\Cbuilder5\Help\Tools\Test) puis entrez comme nom "Aide" puis cliquez "enregistrez", une fenêtre vous est présentée indiquant la création de Aide.hlp, ce sera le nom du fichier qui n'existe pour l'instant qu'en mémoire. Nous allons maintenant relier ce futur fichier Aide.hlp à notre texte Test.rtf. Pour cela, faites "Files" (bouton sur la droite de la fenêtre), vous tombez sur une autre fenêtre, faites "Add" puis sélectionnez notre fichier Test.rtf puis confirmez par OK. Vous retombez sur la fenêtre Aide.hpj qui vous confirme ce choix, vous voyez que Test.rtf fait maintenant partie de votre projet. Vous comprenez aisément que vous pouvez inclure de cette façon plusieurs fichiers rtf, c'est évidemment très pratique pour les fichiers d'aide assez volumineux, on crée un rtf par type de problème en prenant soin toutefois que les identifiants soient uniques. À ce stade on ne peut encore rien faire parce que notre fichier n'existe pas encore dans le répertoire Test. On fait donc "File|Save", on vient de créer le fichier Aide.hpj, vous pouvez le constater en consultant notre répertoire Test, il y a maintenant deux fichiers, Test.rtf et Aide.hpj, ce dernier correspond au contenu de la fenêtre que vous voyez sous WinHelp. Faites maintenant "File|compile" ou cliquez le logo situé à gauche du point d'interrogation jaune (la bulle d'aide de ce logo affiche "Compile"). La fenêtre se minimise (si vous voulez éviter cet effet, décochez l'option "Minimize window while compiling") puis vous affiche le résultat de la compilation.
Creating the help file Aide.hlp.
Processing c:\program files\borland\cbuilder5\help\tools\test\Test.rtf
Resolving keywords...
Adding bitmaps...
2 Topics
2 Jumps
0 Keywords
0 Bitmaps
Created c:\program files\borland\cbuilder5\help\tools\test\Aide.hlp, 5,735 bytes
Compile time: 0 minutes, 0 seconds
0 notes, 0 warnings
Vous pouvez également cliquer "Save and Compile" en bas à droite qui regroupe ces deux opérations. Vous voyez que le compilateur a repéré deux "topics" c'est-à-dire deux identifiants IDH_ et deux "jumps". C'est là que vous verrez vos erreurs dans la codification du rtf, dans ce cas il faudra retourner sous Word pour corriger. À ce stade, WinHelp a déjà créé le fichier d'aide, il se trouve logiquement dans notre répertoire de test. Vous pouvez cliquer dessus, vous verrez donc une page s'afficher avec un lien hypertexte vers la page 2 et à la page 2 un lien hypertexte vers la page 1 puisque c'est ce que nous avons programmé. En général, on teste à l'intérieur de WinHelp, pour ce faire on coche la case "Automatically display Help File in WinHelp when done", dans ce cas, juste après la compilation, le fichier d'aide s'exécute. Après exécution, on retombe alors sur le résultat de la compilation que l'on peut faire disparaître pour se retrouver dans la fenêtre Aide.hpj.
Ces premiers pas étant faits, vous n'aurez plus besoin à partir de maintenant d'appeler WinHelp (hwc.exe) dans le répertoire Tools, vous irez directement dans votre répertoire de Test et vous cliquerez le fichier Aide.hpj, Winhelp alors s'exécutera en chargeant votre fichier.
Nous avons donc deux pages s'appelant l'une l'autre mais nous n'avons encore de structure de "contenu", laquelle doit se trouver dans un autre fichier ayant pour extension cnt. Le fichier compilé se présente sous la forme d'un livre fermé, le contenu se présentera sous la forme d'un livre ouvert. Sous WinHelp donc, faites "File|New", là choisissez "Help Contents". Une nouvelle fenêtre s'ouvre, le curseur se trouve dans la case "Default Filename", entrez "Aide" puisque nous allons détailler le contenu de Aide.hlp. Maintenant cliquez "Add Below", vous tombez sur une fenêtre. Là vous avez deux possibilités, soit vous choisissez "Heading" et on crée un livre, soit vous choisissez "Topic" (c'est le choix par défaut) et on crée une page de livre. Choisissez donc "Heading" et entrez le nom du livre par exemple "Présentation". Vous voyez qu'une occurrence "livre" a été créée. Recommencez "Add below", choisissez cette fois-ci "Topic" (comme c'est le choix par défaut, laissez tel quel), entrez le titre par exemple "Page 1" et en dessous donnez son identifiant à savoir IDH_P1. Comme nous avons dit que le fichier d'aide par défaut était Aide.hlp, nous n'avons pas besoin de renseigner la case "Help File" juste en dessous. Validez, vous voyez qu'une page a été créée. Recommencez "Add below" et donnez comme titre "Page 2" et comme identifiant IDH_P2. Maintenant faites "File|Save" et choisissez "Aide" comme nom, WinHelp créera Aide.cnt, le contenu et la structure de l'aide. Si maintenant vous cliquez le petit livre d'aide, vous voyez un livre que vous pouvez déployer avec ses deux pages.
Vous voyez que WinHelp gère deux types de fichiers, les fichiers hpj pour la déclaration des fichiers rtf, la gestion des identifiants et autres; et les fichiers cnt pour la structure des livres. En créant un fichier d'aide, on navigue donc sans cesse entre Word pour rajouter des pages ou corriger s'il y a eu des erreurs de compilation et les deux fichiers de WinHelp, le fichier hpj pour déclarer de nouvelles occurrences et le fichier cnt pour la structure du contenu.
Nous avons conseillé un répertoire par application, je vous conseille donc de créer vos fichiers d'aide dans ce même répertoire. La première fois, vous appelez WinHelp (hwc.exe) dans le répertoire Program Files\Borland\Cbuilder5\Help\Tools, vous créez un premier hpj puis un cnt, Winhelp créera le fichier final hlp pour vous, il sera alors facile de donner à votre application le nom de ce fichier d'aide. Ces fichiers étant créés, vous appelez Winhelp en cliquant soit sur le hpj soit sur le cnt dans votre répertoire de développement.
Pour l'instant, en cliquant sur le livre nous n'avons pas l'onglet index. En effet, nous n'avons pas encore utilisé l'opérateur K dans notre fichier rtf. Son utilisation est très simple, vous n'avez qu'à écrire les mots clés relatifs à la page séparés par des points-virgules dans la note de bas de page associée à K. N'oubliez pas de compiler sous Winhelp, là un index sera créé à l'exécution.
Utilisez aussi le signe $ qui donne un titre général à la page. Ce titre ne sera pas vu, il est à l'intention du compilateur. Il est nécessaire sinon l'option "rechercher" ne fonctionnera pas. Donc juste après le # de la page 1, créez une note de bas de page avec le signe $ et donnez un nom général à la page. À partir de là, compilez sous WinHelp, exécutez, cliquez "Recherchez", faites "Suivant" puis "Terminer", là tous les mots trouvés sont listés et chaque mot du titre de la page apparaît aussi.
Revenons au fichier Aide.hpj. Cliquez "Map" puis Add, là entrez l'identifiant de la première page IDH_P1 et en dessous un nombre pour désigner cette page par exemple 1, puis faites de même avec IDH_P2, associez cet identifiant au nombre 2. C'est avec ce numéro que vous affecterez la propriété HelpContext d'un composant de votre application C++Builder. Ainsi la propriété HelpFile de la fiche principale sera initialisée avec le nom du fichier hlp créé par WinHelp (par exemple Aide.hlp), comme nous avons dit qu'on allait le sauver dans notre répertoire de développement, on ne donne que son nom sans chemin. Et vous donnerez le nombre entier associé à la page voulue dans HelpContext, ainsi un composant est associé à une page d'aide, laquelle apparaît quand l'utilisateur demande de l'aide dans votre application via la touche F1.
EN RÉSUMÉ : Vous rédigez votre fichier d'aide par page. Chaque page commence par trois notes de bas de page, # indique l'identifiant de la page préfixée par IDH_, $ indique le nom de la page et K donne la liste des mots clés séparés par des virgules. Ces trois notes étant renseignées en partie, rédigez la rubrique en elle-même et présentez-la comme vous l'entendez. Si des liens existent vers d'autres pages, écrivez l'identifiant de ce lien juste après le mot sur lequel on pourra cliquer pour accéder à ce lien puis double-soulignez ce mot par ALT+CTRL+U et masquez le lien qui se trouve juste après par CTRL+MAJ+U. Par ce mécanisme, une petite main se dessinera quand le curseur sera placé sur le ou les mots double-soulignés et la page visée par l'identifiant sera affichée en cas de click. Une fois la page de la rubrique rédigée, complétez les notes de bas de page notamment les mots clés puis revenez au texte et faites "contrôle Entrée" à la fin de la page pour créer un saut de page. Écrivez de cette manière autant de pages qu'il en faudra pour votre fichier d'aide. Une fois le texte rédigé, sauvegardez-le au format RTF. Sous WinHelp, créez d'abord un "Help Project" (ce sera le fichier hpj) et associez ce projet d'aide au fichier RTF créé, créez les liens via MAP entre les identifiants et des nombres entiers. Si on appelle IDH_Pnnn l'identifiant de la page nnn, il suffira d'associer IDH_Pnnn à nnn, ainsi nnn sera le nombre entier associé à la page. Sauvegardez ce fichier hpj, vous pouvez le compiler (création alors du fichier hlp, logo livre fermé) et l'exécuter soit en cliquant le hlp créé par la compilation soit en l'exécutant sous WinHelp. Ensuite créez un "Help Contents" (ce sera le fichier cnt, logo livre ouvert), créez vos occurrences par "Add below" (ou "Add above" pour insérer une page à partir de la page sélectionnée dans le cadre), choisissez "Heading" pour les livres et "Topic" pour une page de livre, ce fichier ne se compile pas, il indique simplement la structure du fichier hlp. WinHelp peut donc ouvrir deux types de fichiers, les hpj qui compilés donneront les hlp et les cnt qui donnent la structure des hlp.
Créons maintenant le gestionnaire d'événement lié à une demande d'aide dans le menu.
void __fastcall TForm1::FichierAideClick(TObject *Sender)
{
Application->HelpFile="Aide.hlp";
Application->HelpCommand(HELP_CONTENTS, 0);
}
La première instruction indique le nom du fichier hlp et la deuxième demande son affichage. On appelle ici l'aide en général et non pas une page particulière de l'aide. Pour afficher une page précise de l'aide par exemple associée au click d'un bouton, on écrira :
void __fastcall TForm1::Button3Click(TObject *Sender)
{
Application->HelpFile="Aide.hlp";
Application->HelpContext(1);
}
On appelle ici la page 1 c'est-à-dire la page dont l'identifiant préfixé par IDH_ est associée au nombre entier 1. Si la proriété HelpFile de votre fiche principale est déjà initialisée avec Aide.hlp, on peut alors supprimer la première ligne de ces méthodes. Cette instruction s'utilise surtout quand le répertoire contient plusieurs fichiers d'aide, auquel cas il faut spécifier à chaque fois son nom. En créant une application, mettez à jour régulièrement votre fichier d'aide et initialisez la propriété HelpContext avec le bon numéro, ainsi l'utilisateur aura déjà la touche F1 opérationnelle, la page visée par HelpContext s'affichera automatiquement. Pour plus de précision sur ces possibilités, reportez-vous au fichier d'aide Win32 situé dans le répertoire Program Files\Fichiers communs\Borland Shared\MSHelp.
Si vous voulez copier votre fichier d'aide hlp (logo livre fermé) dans un autre répertoire pour une autre application, n'oubliez pas de copier aussi le fichier cnt (logo livre ouvert), c'est grâce à ce fichier que vous pourrez consulter votre aide structurée en livres sinon vous n'aurez que les pages successives de l'aide et non sa structure en livres. Pour plus d'informations encore sur les fichiers d'aide, consulter l'aide de WinHelp lui-même.
32. Autre exemple lié à des allocations
et réallocations
Voici un autre exemple complet lié à diverses
allocations et réallocations. Dans une application Windows, il est tout
à fait fréquent d'avoir à ouvrir plusieurs fichiers
simultanément, le nombre de ces fichiers n'étant pas connu
à l'avance. Si, de surcroît, ces fichiers se subdivisent en
différentes parties, il faudra savoir allouer autant de pointeurs qu'il
y aura de parties pour chacun de ces fichiers. C'est par exemple le cas des
fichiers MIDI, lesquels de divisent en pistes (track en anglais, normalement
une piste par portée). Il faudra donc un pointeur général
qui pointera lui-même plusieurs pointeurs (un pointeur par fichier),
chacun de ces pointeurs pointera lui-même plusieurs pointeurs (un
pointeur par piste pour le fichier) et pour tous ces pointeurs, il faudra
allouer une certaine quantité de mémoire et savoir la
réallouer si nécessaire pour l'agrandir. Voici quelques remarques
sur cette méthode :
- PointFic est un pointeur de pointeurs de pointeurs de caractères
(char***). Cela signifie que PointFic[i] sera pointeur pour le fichier
numéro i, PointFic[i][j] sera le pointeur de la zone (ou piste)
numéro j du fichier numéro i, PointFic[i][j][k] sera le
kème octet (ou caractère) de la piste numéro j du fichier
numéro i.
- LZ est pointeur de pointeur d'entiers (int **) et contiendra les
longueurs des zones ou pistes. LZ[i] sera donc le pointeur pour le fichier
numéro i, LZ[i][j] sera la longueur en octets de la piste numéro
j du fichier numéro i.
- NBZ est un pointeur d'entiers, NBZ[i] sera le nombre de zones du fichier
numéro i.
- Dans ce petit test de syntaxes C++, on simule l'existence ou l'ouverture
de ces fichiers via la variable NumFic qui part de 0 et s'incrémente
dans une boucle en for. À chaque fichier nouveau, on commence par
allouer de la place pour un nouveau pointeur de fichier
PointFic=(char***)realloc(PointFic,(NumFic+1)*sizeof(char**));
- Remarquez que ce realloc se comporte la première fois comme un
malloc. Pour que cela soit correct, il faut initialiser PointFic à NULL,
sinon vous risquez d'avoir des problèmes à l'exécution. Il
en est d'ailleurs de même des pointeurs LZ et NBZ qui sont
initialisés à NULL. Sinon, il faudrait faire une distinction
entre la première allocation et les autres en disant si NumFic=0 alors
faire un malloc sinon faire un realloc. Pour éviter d'avoir à
faire ce petit détour, on utilise realloc mais on initialise ces
pointeurs à NULL. Les deux possibilités sont correctes. La
première fois donc, quand NumFic sera égal à 0, on
disposera d'un premier Pointeur qui est PointFic[0], réservé au
tout premier fichier. La seconde fois, le realloc agrandira cette petite zone
en allouant de la place pour un second pointeur, on en aura alors deux,
PointFic[0] qui n'a pas été touché ni détruit et
maintenant PointFic[1] pour le second fichier. Notez qu'on ne teste pas le code
retour de ces minuscules realloc, un pointeur demandant 4 octets, il est
impossible que le système nous refuse 4 octets. Le code retour des
realloc n'est testé qu'au moment ou les zones sont arbitrairement
allongées, c'est là qu'il y a un vrai risque de
difficultés mais de toute façon très limité.
- Idem pour le pointeur LZ de longueur de zones LZ=(int**)
realloc(LZ,(NumFic+1)*sizeof(int*));
- La première fois, cette instruction réservera le premier
pointeur LZ[0] pour le premier fichier, la seconde fois LZ[1] pour le
deuxième fichier et ainsi de suite.
- Idem pour NBZ qui s'agrandit d'un entier à chaque session
dirigée par NumFic. La premier fois, ce pointeur ne pointera que sur un
seul entier qui est NBZ[0] égal au nombre de zones du premier fichier,
la deuxième fois NBZ=(int*) realloc(NBZ,(NumFic+1)*sizeof(int));
allouera de la place pour un nouvel entier, NBZ[1] qui sera égal au
nombre de zones du deuxième fichier et ainsi de suite.
- Ensuite on dit que le nombre de zones du fichier NumFic est NbZon
NBZ[NumFic]=NbZon; NbZon est une constante arbitraire, cela n'a aucune
importance pour ce test syntaxique, il faut simplement se souvenir que NBZ[i]
est égal au nombre de zones, quel qu'il soit du fichier numéro i.
- Ce nombre de zones du fichier NumFic étant maintenant connu, il
faut donc réserver NBZ[NumFic] pointeurs, un pointeur par zone du
fichier numéro NumFic et NBZ[NumFic] entiers qui seront la longueur en
octets de chacune de ses pistes. On a donc PointFic[NumFic]=(char**)
malloc(NBZ[NumFic]*sizeof(char*));
- Cette instruction réserve NBZ[NumFic] pointeurs à partir de
PointFic[NumFic] i.e. PointFic[0] pointeur de la première zone,
PointFic[1] pointeur de la deuxième zone et ainsi de suite.
- De même LZ[NumFic]=(int*) malloc(NBZ[NumFic]*sizeof(int));
réserve la place pour NBZ[NumFic] pointeurs vers une série
d'entiers (longueur en octets de chaque zone) i.e. LZ[NumFic][0] sera la
longueur de la première zone du fichier numéro NumFic,
LZ[NumFic][1] sera la longueur de la deuxième zone et ainsi de suite.
- Ensuite, les NBZ[NumFic] pointeurs sont initialisé à NULL et
sa longueur à 0. Le tout premier realloc de la fonction
"remplit" se comportera donc comme un malloc.
- Ensuite on remplit à outrance chacune de ces zones et on les
agrandit dès qu'on se trouve hors zone. C'est la boucle en while qui
simule ce remplissage, à l'intérieur de laquelle i est le
numéro de la zone considérée, j l'offset à
l'intérieur de cette zone. Dès que cet offset est arbitrairement
grand (ici 60 fois la constante bloc), on considère qu'on a assez
remplit et on passe à la zone suivante, la variable i va donc de 0
à NBZ[NumFic]-1. La boucle en while se termine soit parce que la
fonction "remplit" a renvoyé false, ce qui signifie qu'une
nouvelle réallocation a échoué soit parce que i est
égal à NBZ[NumFic], qui est le premier numéro de piste
inexistant puisque i va de 0 à NBZ[NumFic]-1.
- La fonction "remplit" reçoit 4 arguments : le pointeur de
zone PointFic[NumFic][i] i.e. le pointeur de la zone numéro i du fichier
numéro NumFic. Notez que ce pointeur est susceptible d'être
modifié, il est donc envoyé en référence
grâce à l'opérateur &. L'argument n'est donc pas du
type char* mais char*&. La fonction "remplit" reçoit aussi
l'offset d'écriture, le caractère à inscrire à cet
offset et aussi la longueur de la zone. Ce dernier argument est
également envoyé en référence grâce à
l'opérateur & car s'il y a réallocation, la zone va changer
de longueur, il faut que l'appelant le sache. L'opérateur & vous
assure que la variable en question sera retournée à l'appelant.
Le principe est maintenant simple : si l'offset donné est égal
à la longueur de la zone, il pointe alors le premier octet hors zone, on
décide alors de réallouer la mémoire en l'agrandissant de
bloc octets, bloc étant une constante arbitraire. Si maintenant,
réallocation ou non, P n'est pas NULL, il pointe alors correctement la
mémoire et on peut écrire l'octet à l'offset
indiqué. Notez que P n'est pas NULL soit parce que 'il n'y a pas eu
réallocation soit parce que la réallocation a réussi.
Notez également que, comme chaque pointeur de zone est initialisé
à NULL et la longueur de la zone correspondante à 0, il y aura
allocation dès le premier appel à la fonction
"remplit". Le tout premier offset étant 0, la fonction verra
que cet offset coïncide avec la longueur de la zone qui est aussi 0, le
programme considérera que l'offset pointe hors zone (ce qui la
première fois n'est pas tout à fait exact puisque qu'aucune zone
n'a encore été allouée pour ce pointeur) et il
procédera à une première allocation mémoire de bloc
octets, ce premier realloc étant équivalent à un malloc.
Ensuite la fonction booléenne se contente de renvoyer la comparaison en
le pointeur et non NULL. Si c'est vrai (true i.e. pointeur non NULL), le
pointeur renvoyé à l'appelant pointe toujours correctement la
mémoire donc on continue, sinon la boucle en while s'arrêtera, ok
prenant alors la valeur false.
- N'oubliez pas que true correspond à un entier non nul quel qu'il
soit et false à un entier nul. Comme ok est initialisé à
1, il est "vrai". Tant qu'il est "vrai" (non nul) on
continue la boucle. S'il y a un problème de réallocation,
"remplit" renverra false, ok devient alors nul et la boucle
s'arrêtera pour cette raison.
- Cette boucle en while terminée, cela signifie qu'on a simulé
le traitement de NbFic fichiers, on libère alors la totalité des
pointeurs.
bool remplit(char*&,int,char,int&);
const unsigned int bloc=500;
void ESSAI(void)
{
const int NbFic=10;
const int NbZon=45;
char ***PointFic=NULL;
int **LZ=NULL;
int *NBZ=NULL;
int i,j;
int NumFic;
int ok;
// Petit message de démarrage
Application->MessageBox("On va
commencer","Début",MB_OK);
/* On va traiter NbFic fichiers, NbFic est une constante arbitraire.
Dans le corps de la boucle, on considérera qu'on traite le fichier
numéro NumFic */
for(NumFic=0;NumFic!=NbFic;NumFic++)
{
/* Place pour un nouveau pointeur pour le fichier numéro NumFic */
PointFic=(char***) realloc(PointFic, (NumFic+1)*sizeof(char**));
/* Place pour un nouveau pointeur de longueurs de zones */
LZ=(int**) realloc(LZ,(NumFic+1)*sizeof(int*));
/* Place pour un nouvel entier qui contiendra le nb de zones
du fichier numéro NumFic */
NBZ=(int*) realloc(NBZ,(NumFic+1)*sizeof(int));
/* Le fichier numéro NumFic aura NbZon zones (affectation arbitraire) */
NBZ[NumFic]=NbZon;
/* Réserve NBZ[NumFic] pointeurs, chacun pointant une des NBZ[NumFic]
zones du fichier numéro NumFic */
PointFic[NumFic]=(char**) malloc(NBZ[NumFic]*sizeof(char*));
// LZ est un tableau de NBZ[NumFic] longueurs
LZ[NumFic]=(int*) malloc(NBZ[NumFic]*sizeof(int));
/* Pour chaque zone du fichier numéro NumFic, on initialise
à NULL le pointeur correspondant et sa longueur à 0*/
for(i=0;i!=NBZ[NumFic];i++)
{
PointFic[NumFic][i]=NULL;
LZ[NumFic][i]=0;
}
// Remplissage arbitraire et realloc si dépassement
i=0;
j=0;
ok=1;
while(ok)
{
if((ok=remplit(PointFic[NumFic][i],j,'a',LZ[NumFic][i]))==true)
{
j++;
/* si j pointe la fin d'une piste arbitrairement étendue
on remet cet offset à 0 et on passe à la piste suivante */
if(j==bloc*60)
{
// offset à 0
j=0;
// piste suivante
i++;
/* fin si i est égal au nb de zones, i va de 0
au nb de zones du fichier numéro NumFic donc de
0 à NBZ[NumFic]-1, donc si i=NBZ[NumFic], il correspond
au premier numéro de zone hors fichier, ce qui signifie
que toutes les pistes ou zones du fichier numéro NumFic
on été remplies */
ok=(i!=NBZ[NumFic]);
}
}
else Application->MessageBox("Erreur d'allocation
mémoire",NULL,MB_OK);
}// fin du while
}//fin du for NumFic
// Libération de toutes les réservations
for(NumFic=0;NumFic!=NbFic;NumFic++)
{
// Liberation des réservations pour NumFic
for(i=0;i!=NBZ[NumFic];i++) free(PointFic[NumFic][i]);
free(PointFic[NumFic]);
free(LZ[NumFic]);
}
// Petit message de fin de test
Application->MessageBox("C'est fini","Fin",MB_OK);
}// fin de la fonction ESSAI
/*--------------------------------------
Renvoie true s'il n'y a pas de problème soit parce qu'il n'y a pas
eu de réallocation soit parce que la réallocation a
réussi,
renvoie false si problème de réallocation, l'espace
mémoire
n'a pas pu être agrandi */
bool remplit(char*&P,int o,char c,int &L)
{
if(o==L) P=(char*) realloc(P,L+=bloc);
if (P!=NULL)P[o]=c;
return (P!=NULL);
}
33. Conversion d'un tableau de
longueur fixe en tableau de longueur variable
On déduit facilement des syntaxes exposées
précédemment la manière très rapide de convertir un
tableau de longueur fixe en tableau de longueur variable. Imaginez que vous
vouliez déclarer un tableau de 20 entiers, vous écrirez
simplement int Tab[20];
Les éléments de ce tableau sont accessibles
par les indices de 0 à 19, Tab[0] est la première valeur de ce
tableau d'entiers, Tab[1] la seconde etc. Tab[19] la dernière. Vous vous
apercevez dans le cours du développement que finalement vous ignorez
quelle sera le nombre d'éléments de ce tableau. Vous pouvez
certes le surdimensionner mais ce n'est jamais une bonne méthode. Si
vous savez que vous aurez besoin d'une centaine de valeurs, vous pouvez
toujours écrire Tab[500], vous en déclarez beaucoup plus mais ce
n'est pas très élégant.
La bonne méthode consiste à :
- supprimer dans la déclaration du tableau les crochets et le nombre
qui se trouve à l'intérieur puis rajouter une étoile au
nom de cette variable, quel que soit le nombre d'étoiles qui s'y
trouvent déjà, le tableau devient alors pointeur, initialisez-le
à NULL. Par exemple, int Tab[20]; deviendra int *Tab=NULL;
Cette précaution est nécessaire car on procède par realloc
qui plante si le pointeur n'est pas initialisé (sinon il faut utiliser
un malloc la première fois et un realloc les fois suivantes, cette
précaution d'initialiser à NULL nous permet de toujours utiliser
realloc sans se préoccuper de savoir si c'est la première fois ou
non)
- Soit NbVal le nombre de valeurs ou d'éléments dont vous avez
maintenant besoin, vous allouerez la place pour ces NbVal
éléments ainsi Tab=(int*)realloc(Tab,NbVal*sizeof(int));
- Ainsi si NbVal est égal à 1, ce realloc vous
réservera un élément, vous n'aurez donc qu'un
élément dans ce tableau à savoir Tab[0]. Si ensuite NbVal
est égal à 2, vous réserverez un entier de plus et ainsi
de suite.
- Quelle que soit la valeur de NbVal, le tableau aura NbVal valeurs
accessibles par les indices allant de 0 à NbVal-1.
void ESSAI(void)
{
const int Nb=2000;
int NbVal,i, *Tab=NULL;
Application->MessageBox("On va
commencer","Début",MB_OK);
for(NbVal=1;NbVal!=Nb+1;NbVal++)
{
/* ajout d'un entier au tableau TAB qui compte maintenant i
éléments
accessible par les indices allant de 0 à NbVal-1 */
Tab=(int*)realloc(Tab,NbVal*sizeof(int));
/* remplissage de vérification. Comme le tableau d'entiers Tab
a i éléments, on accède à ces valeurs par tous les
indices
allant de 0 à NbVal-1. L'affectation de l'entier est arbitraire
elle prouve simplement la possibilité d'accéder à toutes
les valeurs du tableau */
for(i=0;i!=NbVal;i++) Tab[i]=3;
}
Application->MessageBox("Fin du test","Fin",MB_OK);
}
Une autre méthode très
voisine consiste à déclarer déjà un
élément au moment de la déclaration via un malloc :
int *Tab=(int*) malloc(sizeof(int));
Ainsi, vous disposez déjà du premier entier
Tab[0] de ce tableau. Dès que vous en avez besoin d'autres, vous
programmez une réallocation.
Il est donc très important de savoir si un tableau
est de longueur fixe ou variable car un tableau fixe ne peut pas faire l'objet
d'une réallocation. Si vous déclarez par exemple int Tab[20];
vous disposez certes de 20 entiers mais comme Tab n'est pas un pointeur mais
seulement le nom du tableau, vous ne pourrez jamais en modifier la longueur via
realloc, le compilateur refusera la syntaxe (car la variable d'un realloc est
toujours du type pointeur, il y a donc au moins une étoile dans sa
déclaration ce qui n'est pas le cas si vous écrivez int
Tab[20];).
Notez que cette méthode reste correcte quelle que
soit le nombre d'étoiles qu'il y a déjà dans la
déclaration. Si vous transformez un tableau fixe en tableau variable, il
suffit d'en rajouter une (une étoile c'est-à-dire finalement un
niveau d'indirection). Si vous aviez un tableau de 15 pointeur de pointeur
d'entiers déclaré ainsi : int**P[15]; celui-ci se
déclarera maintenant ainsi int***P=NULL; et vous allouerez le nombre
d'éléments N que vous voudrez via un realloc :
P=(int***)realloc(N,sizeof(int**)); Les éléments de tableau sont
bien du type int**, P est maintenant un tableau de pointeurs de ce type, P[0]
pour le premier, P[1] pour le second et ainsi de suite Il est donc très
aisé avec ce principe de modifier du code pour transformer un tableau de
longueur fixe en un tableau de longueur variable.
34. Gestion de versions et retour à une
situation antérieure
Si vous enregistrez sous un autre nom un fichier cpp (C++)
faisant partie du projet, ce nouveau nom sera automatiquement écrit
à la place de l'ancien dans le cpp portant le nom du projet en regard de
la directive USEUNIT. Si par exemple le nom de votre projet est NomProjet, vous
aurez un fichier NomProjet.cpp créé automatiquement par C++
Builder du type suivant :
//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
USERES("Gilles.res");
USEFORM("Appli.cpp", Form1);
USEUNIT("calcul.cpp");
//---------------------------------------------------------------------------
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
Application->Initialize();
Application->CreateForm(__classid(TForm1), &Form1);
Application->Run();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
return 0;
}
//------
C'est le fichier fondamental de
déclaration de votre application Windows. Dans cet exemple, on voit que
le fichier se lie à deux autres cpp à savoir Appli.cpp et
calcul.cpp. Si maintenant vous utilisez la fonction "enregistrer
sous" pour enregistrer un de ces deux fichiers sous un autre nom, le
nouveau nom sera automatiquement écrit dans NomProjet.cpp à la
place de l'ancien. Par exemple, vous enregistrez le fichier calcul.cpp sous le
nom CalculCopie.cpp, vous verrez dans NomProjet.cpp le USEUNIT modifié,
vous lirez USEUNIT("CalculCopie.cpp"); et non plus
USEUNIT("calculcpp"); c'est ainsi que C++ Builder connaît les
unités à compiler pour créer l'exécutable.
Faites F9, la compilation a lieu normalement et ça
marche toujours, l'unité officielle est maintenant CalculCopie.cpp.
L'intérêt est que le fichier d'origine calcul.cpp existe toujours.
Si vous voulez revenir à cette ancienne version, faites Projet|Retirer
du projet et supprimez CalculCopie.cpp du projet. Puis faites Projet|Ajouter au
projet et sélectionnez calcul.cpp. Vous êtes ainsi revenu à
la version antérieure. Vous pouvez vérifier que dans le fichier
NomProjet.cpp le USEUNIT a été mis à jour, vous avez
USEUNIT("calcul.cpp"); et non plus
USEUNIT("CalculCopie.cpp");
C'est très pratique pour gérer correctement
votre avance dans un projet. Si la version calcul.cpp marche bien, vous avez
tout intérêt à enregistrer ce cpp sous un autre nom par
exemple CalculCopie.cpp qui fait maintenant partie du projet à la place
de l'ancien. Vous avancez dans CalculCopie.cpp sans prendre de risques car si
vous avez commis des erreurs, il vous est très facile de revenir en
situation antérieure, vous supprimez CalculCopie.cpp du projet et vous y
ajouter calcul.cpp, c'est très simple. Si toutefois CalculCopie.cpp est
meilleur suite à vos ajouts, libre à vous de le garder ou
même de l'enregistrer sous son nom d'origine calcul.cpp écrasant
ainsi l'ancienne version dont vous n'avez plus besoin. Vous avez ainsi
avancé prudemment en travaillant temporairement sur une copie.
35. Nom des fichiers
Si vous avez à traiter des fichiers dont le nom est
connu d'avance et donc écrit en dur dans le programme, n'oubliez pas
qu'il faut doubler les \ (dans le chemin donné, deux fois \ n'en vaut
qu'un seul à l'intérieur des doubles quotes de chaînes de
caractères) sinon il y aura une erreur à l'exécution, le
fichier ne pourra pas être ouvert.
if((Fichier=fopen("C:\\Program
Files\\Borland\\CBuilder5\\Projects\\toto.doc","rb"))==NULL)
{
Application->MessageBox("Je ne peux pas ouvrir le
fichier",NULL,MB_OK);
return;
}
En revanche, si vous ouvrez des
fichiers via des OpenDialog, vous ne vous occupez de rien puisque c'est C++
Builder qui vous fournit le nom du fichier, par exemple :
if(OpenDialog1->Execute())
{
/* OpenDialog fournit le nom du fichier choisi par l'utilisateur
ainsi que son chemin, tout est prêt pour un fopen */
NomFic = OpenDialog1->FileName;
// Traitement du fichier
TraiteFic(NomFic);
}
où NomFic est un AnsiString en argument de TraiteFic qui peut
débuter ainsi :
void TraiteFic(AnsiString N)
{
FILE *Fichier;
AnsiString MES;
/* Ouverture du fichier en lecture écriture mode binaire
le fichier étant censé déjà exister */
if ((Fichier = fopen(N.c_str(), "r+b"))== NULL)
{
MES="Impossible d'ouvrir le fichier "+N+".";
Application->MessageBox(MES.c_str(),NULL,MB_OK);
return;
}
etc
suite du programme
}// Fin de la fonction TraiteFic
36. Plein écran
Si vous préférez travailler le source sur une
fenêtre plein écran, faites outils|options de l'éditeur
puis choisissez affichage, il suffit de cocher la case "plein
écran". Vous obtenez le même formulaire d'options en cliquant
sur le bouton droit de la souris tout en pointant la zone grise de le
fenêtre source et en choisissant tout en bas du menu déroulant
"propriétés". Si vous avez choisi "plein
écran", il vous suffit de cliquer sur le rebord bleu de la
fenêtre de saisie du code et elle sera plein écran. Sinon, si la
case n'est pas cochée, le menu du haut reste accessible quand vous
agrandissez la fenêtre en cliquant dans sa zone bleue. Cela dit, cette
dernière présentation est intéressante car vous pouvez
déplacer légèrement la fenêtre vers le haut,
laissant ainsi accessible la liste des programmes windows en cours au bas de
l'écran alors qu'en plein écran, la fenêtre reste immobile
et vous n'avez pas accès aux logiciels windows en cours. Si
l'explorateur de classes est visible, vous pouvez minimisez sa taille et donc
agrandir la fenêtre de code, il suffit de pointeur le curseur sur le bord
gauche de la gouttière grise et de tirer la gouttière vers la
gauche.
37. Écriture d'un entier long par un pointeur de
type char*
Il est toujours intéressant de considérer une
zone mémoire simplement comme une suite d'octets pointée par un
pointeur P de type char*. Imaginons que l'on veuille enregistrer dans la zone
mémoire pointée par P à partir d'un offset o un long int.
On sait qu'un long int tient sur quatre octets (32 bits). Il s'agit donc de
savoir lire chacun de ces octets et de les écrire un à un dans la
mémoire. Pour ce faire, on utilise la fait que si c est du type char et
LI du type long int, l'instruction c=LI; écrira dans c l'octet de poids
le plus faible de LI qu'on appelle aussi LSB (Last Significant Byte). Ensuite,
on procédera à un décalage de LI sur la droite de 8
positions (LI>>=8;) en sorte qu'une instruction du type c=LI; lira dans c
le LSB de LI i.e. le deuxième octet en partant de la droite du LI
d'origine et ainsi de suite quatre fois. Voici par exemple comment pourrait se
présenter une telle fonction. Nous avons choisi de commencer
l'écriture de l'entier long par le MSB (Most Significant Byte). On
considère ici l'entier long non signé mais le code serait
identique pour un entier signé.
/* Enregistre un long int
P pointe la zone mémoire (référence car on suppose une
écriture séquentielle, la nouvelle position sera renvoyée
à l'appelant au sortir de la fonction)
o offset d'écriture
LI long int à écrire */
void EnregLI( char*& P,
unsigned long int& o,
unsigned long int LI)
{
char C[4];
// lecture des quatre octets du long int
C[3]=LI;
C[2]=LI>>=8;
C[1]=LI>>=8;
C[0]=LI>>=8;
// Écriture à partir du MSB
for(int i=0;i!=4;i++)P[o++]=C[i];
}
Dans ces conditions, la fonction LectureLI de
lecture d'un tel long int sera par exemple :
/* Lecture du long int pointé par P
*/
unsigned long int LectureLI( char*& P,
unsigned long int& o)
{
unsigned long int ULI=0;
for(int i=0;i!=4;i++) ULI=(ULI<<=8)+P[o+i];
return ULI;
}
La lecture se fait par incrémentation
du pointeur du fait que nous avons commencé l'écriture par le
MSB. ULI, futur résultat, est initialisé à 0 en sorte que
le premier décalage à gauche (ULI<<=8) ne fera rien. On lui
ajoute alors l'octet pointé par P à savoir le MSB de l'entier
long à lire. Le MSB se trouve donc provisoirement à la position
du LSB du résultat ULI. Mais à la prochaine itération, il
y a décalage vers la gauche, le MSB se trouve donc en deuxième
position et on ajoute l'octet suivant en sorte qu'après les quatre
itérations, on a bien dans ULI la valeur de cet entier long.
Pour schématiser, l'entier long est sauvegardé
ainsi : |octet3|octet2|octet1|octet0| où octet3 est le MSB et
octet0 le LSB. Pour la lecture, P pointe octet3. ULI est initialisé
à 0, son premier décalage à gauche le laisse à 0,
puis octet3 est ajouté à ULI, on a donc ULI=octet3. Puis ULI est
décalé à gauche, son LSB est donc maintenant nul en sorte
qu'on peut lui ajouter octet2 donc ULI=|octet3|octet2|, à la prochaine
itération ULI est décalé à gauche et on ajoute
octet1 donc ULI==|octet3|octet2|octet1| et enfin, ULI est décalé
une dernière fois à gauche en on y ajoute octet0 ce qui donne
bien ULI==|octet3|octet2|octet1|octet0|. Notez que cette addition
équivaut ici à un OU logique puisque le LSB de ULI est nul en
sorte qu'on peut remplacer le signe + par le signe | qui exprime un
OU logique en bit à bit.
/* Lecture du long int pointé par P */
unsigned long int LectureLI( char*& P,
unsigned long int& o)
{
unsigned long int ULI=0;
for(int i=0;i!=4;i++) ULI=(ULI<<=8)|P[o+i];
return ULI;
}
Comme ULI est sur 32 bits et P[o+i] sur 8 seulement, le OU
logique se fait en bit à bit sur 32 bits en considérant trois
autres octets virtuels de P[o+i] à 0, les 24 bits de poids fort de ULI
sont ainsi maintenus en leur état par définition du OU logique
alors que les 8 bits de P[o+i] seront écrits dans le résultat
puisque le LSB de ULI est nul. Il y a donc écriture des 8 bits de P[o+i]
dans ULI avec maintien du reste de l'entier long ULI en train de se construire.
Sinon, si l'on veut éviter ces petites boucles sur
octets consécutifs, il faut alors jongler avec les pointeurs et les
conversions. Si PC est du type char* (pointeur sur un caractère) et si
LI est un long integer :
char* PC;
long int LI;
LI s'écrira à l'adresse PC comme suit :
*(long int*) PC=Nombre;
PC qui est un char* est converti en long int*, on y
écrit Nombre à cette adresse sur quatre octets en
commençant par le LSB. Réciproquement, la lecture de la
mémoire se fera logiquement par :
Nombre=*(long int*) PC;
en sorte qu'il n'est même pas nécessaire de se
souvenir si on commence l'écriture ou la lecture par le MSB ou le LSB
puisque dans ces conditions, l'accès mémoire est parfaitement
symétrique. On se contente de savoir qu'on écrit un long int par
*(long int*) PC=Nombre; et qu'on lit un long int par Nombre=*(long
int*) PC;
38. Réalisation de condition
Si vous voulez savoir si une certaine condition se
réalise à l'exécution du programme, codez l'instruction if
sur deux lignes :
if(condition)
instruction;
Il vous suffit maintenant de positionner le curseur sur
l'instruction et de faire F4 (exécuter jusqu'au curseur). Si la
condition se réalise, le programme s'arrêtera à
l'instruction. Il en va de même si vous voulez savoir si le programme
arrive à un certain endroit, positionnez le curseur à cet endroit
et faites F4, c'est très pratique car cela ne prend que quelques
instants pour avoir cette information. En appuyant de nouveau sur F4 une fois
l'exécution stoppée, le programme continue et s'arrêtera de
nouveau si la condition se réalise de nouveau.
39. Le composant Image
Ce composant se trouve sous l'onglet
"supplément" de la palette des composants, c'est un petite
image avec un soleil. De toute façon, C++ Builder vous affiche le nom
d'un composant si le curseur s'arrête sur lui quelques instants, il
s'agit ici du composant "image". Partez d'une fiche principale vierge
(celle que vous avez en entrant dans C++ Builder). Sélectionnez le
composant Image en cliquant dessus et cliquez maintenant n'importe où
dans la fiche principale (par défaut Form1 au démarrage de C++
Builder). Apparaît alors un carré et dans l'inspecteur d'objets
sur la gauche les caractéristiques par défaut de ce nouvel objet
nommé Image1 (vous pouvez changer ce nom dans le champ Name). Vous
pouvez aussi modifier sa taille, pour ce faire étirez comme vous voulez
ce carré, ses nouvelles dimensions (Height et Width) sont
automatiquement mises à jour dans l'inspecteur d'objets. Cela dit,
l'image vierge n'apparaîtra qu'au moment de sa première
utilisation, ce pourquoi il est d'usage d'initialiser les coordonnées du
stylo d'écriture au moment de la création de la fiche principale.
Donc, cliquez sur cette fiche principale (Form1) ou ce qui revient au
même sélectionnez-la dans l'inspecteur d'objets via un menu
déroulant (qui contient maintenant deux objets, Form1 et Image1),
cliquez sur l'onglet "événements" et double-cliquez sur
l'événement "OnCreate". Là C++ Builder vous a
créé la méthode FormCreate qui ne contient pour l'instant
rien.
void __fastcall TForm1::FormCreate(TObject
*Sender)
{
}
C'est ici qu'ira le programme à l'exécution
quand la fiche principale sera créée, c'est donc ici logiquement
qu'on écrit toutes les initialisations de l'application. Nous allons
maintenant initialiser le stylo en écrivant dans la méthode
FormCreate, cette méthode devient par exemple :
void __fastcall TForm1::FormCreate(TObject
*Sender)
{
Image1->Canvas->MoveTo(0,0);
}
Faites F9 pour compiler et exécuter,
vous voyez une image vierge blanche. Celle-ci s'est affichée grâce
à l'initialisation du stylo (ou en réalité toute
instruction qui invoque Image1). Cette initialisation peut également se
faire (le résultat serait le même) au moment de la toute
première méthode créée par C++ Builder. Quand vous
entrez dans C++ Builder, vous avez une fiche principale vierge mais aussi la
première méthode suivante dans unit1.cpp :
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
Vous pouvez donc initialiser le stylo ici et obtenir :
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
Image1->Canvas->MoveTo(0,0);
}
Remarquez qu'on n'accède pas
directement à Image1 mais qu'on passe par l'intermédiaire d'un
canevas (Canvas), on invoque non pas Image1 mais le canevas d'Image1. Il en
sera de même pour d'autres objets graphiques, si par exemple on
déclare un bitmap, on écrira dans le canevas de ce bitmap. Cela
dit, comme Image1 est ici l'image réelle à l'écran, la
modification des pixels de ce canevas modifiera les pixels à
l'écran. Par exemple, à chaque fois que l'utilisateur appuiera
sur le bouton de la souris, nous allons afficher la ligne allant de la position
courante du stylo qui a été ici initialisée à
(0,0), coin en haut à gauche de l'image, jusqu'à la position du
curseur. Pour ce faire, sélectionez Image1 de l'inspecteur d'objets puis
"événements" puis l'événement
"OnMouseDown". Là, C++ Builder vous crée la
méthode correspondant à cet événement, rajoutez
l'instruction de tracé d'une ligne, on obtient :
void __fastcall
TForm1::Image1MouseDown(TObject *Sender,
TMouseButton Button, TShiftState Shift, int X, int Y)
{
Image1->Canvas->LineTo(X,Y);
}
Durant l'exécution, on se retrouvera
ici chaque fois que l'utilisateur aura cliqué la souris dans l'image (si
le curseur est hors image, on n'exécutera pas cette fonction qui ne
correspond à MouseDown que pour Image1). La, méthode trace ici
une ligne allant de la position courante du stylo à (X,Y) qui sont les
coordonnées du curseur au moment de son arrivée dans cette
fonction et le stylo prend ensuite cette nouvelle position en sorte qu'au
prochain clic, on tracera une ligne allant de cette ancienne position à
la nouvelle et ainsi de suite. Faites F9 pour exécuter et cliquez
plusieurs fois dans l'image, vous voyez une ligne brisée s'afficher
correspondant aux différentes coordonnées du curseur.
40. Bitmap hors écran
Le plus souvent, on a intérêt à
préparer sa présentation dans un bitmap hors écran et
d'afficher ensuite seulement le résultat final, cela évite les
problèmes de présentation, l'image calculée apparaît
en une seule fois. Partons d'un projet vierge et déclarons comme
précédemment un composant Image qu'on va appeler Dessin (modifiez
son nom dans la champ Name). Dans ces conditions, on va initialiser ainsi le
stylo au moment de la première méthode de la fiche
principale :
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
Dessin->Canvas->MoveTo(0,0);
}
Vous voyez que c'est la même chose que
dans l'alinéa précédent si ce n'est que Image1 a
été changé en Dessin puisqu'on a modifié le nom de
ce composant Image. Maintenant, on va créer un bitmap, écrire
hors écran dans ce bitmap et associer ce bitmap à l'image
écran (appelée ici Dessin) de manière à afficher
tout en une seule fois. Cela va se passer comme suit au moment de la
création de la fiche principale :
void __fastcall TForm1::FormCreate(TObject
*Sender)
{
// création du bitmap
Graphics::TBitmap* Bitmap = new Graphics::TBitmap();
// Dimension du bitmap
Bitmap->Width=400;
Bitmap->Height=150;
// écriture hors écran d'une diagonale allant de (30,30) à
(100,100)
Bitmap->Canvas->MoveTo(30,30);
Bitmap->Canvas->LineTo(100,100);
// affectation du bitmap à l'image, ce n'est que maintenant que le
résultat est affiché
Dessin->Picture->Graphic=Bitmap;
}
Vous voyez qu'il s'agit ici de
l'événement de création de la fiche principale
(TForm1::FormCreate). Repérez bien la syntaxe C++ Builder de
déclaration du pointeur Bitmap via l'opérateur new. On donne
ensuite les dimensions du bitmap. Voyez qu'on écrit dans le canevas du
bitmap, donc on écrit clairement hors écran car ce bitmap n'a
aucune raison d'être connecté au composant Image nommé
Dessin. Enfin, la dernière instruction associe ce bitmap au composant
Image nommé Dessin, ce n'est qu'à ce moment là que
l'affichage écran a lieu. Cet exemple vous montre comment dessiner dans
un bitmap hors écran pour n'afficher ce bitmap qu'ensuite. Notez que si
ce bitmap est plus petit que le composant Image, il s'affichera en entier,
sinon seule la partie supérieure en haut à gauche sera
affichée. Si vous voulez avoir accès à tout le bitmap via
le composant Image, il faut alors le préciser dans les
propriétés du composant Image dans l'inspecteur d'objets en
mettant le flag "AutoSize" à true (ce flag est par
défaut à false). Ainsi, si le bitmap est plus grand, on n'en
verra certes qu'une partie mais le reste sera accessible par des barres de
défilement. Notez enfin que si vous avez plusieurs bitmaps, vous pouvez
facilement procéder à des copies de l'un vers l'autre par la
fonction Draw, par exemple :
Bitmap1->Canvas->Draw(0,0,Bitmap);
Cette instruction recopie dans Bitmap1 le
bitmap Bitmap à partir du coin haut gauche (0,0).
41. Savoir utiliser l'aide en ligne
Comme on ne saurait connaître toutes les syntaxes
d'emblée par cur, il faut sans cesse savoir se faire aider par le
progiciel. Ainsi, ayant déclaré un bitmap par l'instruction vue
précédemment
Graphics::TBitmap* Bitmap = new Graphics::TBitmap(); vous
aimeriez maintenant connaître les diverses fonctions et
propriétés qui lui sont rattachées. C'est très
simple. Commencez votre instruction en écrivant simplement le nom de la
variable déclarée suivi de la petite flèche comme
suit :
Bitmap->
et attendez quelques instants. La progiciel
va vous afficher dans un menu déroulant toutes les possibilités
que vous avez maintenant. Par exemple, vous apprenez qu'il y a
possibilité de bitmap monochrome (1 seul bit par pixel) et que cette
propriété est du type bool (i.e. variable booléenne, true
ou false). Sélectionnez dans le menu déroulant cette
possibilité, elle est maintenant surlignée, appuyez sue
"Entrée", vous voyez que le progiciel vous a écrit
lui-même la suite de l'instruction. Comme il s'agit ici d'un
booléen, on écrira par exemple :
Bitmap->Monochrome=true;
Refaites cette manipulation et essayez
maintenant l'option PixelFormat, vous allez avoir maintenant :
Bitmap->PixelFormat
Pour connaître maintenant vos
possibilités, positionnez le curseur juste avant PixelFormat (ou
sélectionnez ce mot par un double clic) et appuyez sur F1, le progiciel
vous propose différentes possibilités concernant PixelFormat dont
celle qui nous intéresse à savoir Tbitmap:: PixelFormat. Cliquez
ce choix, vous voyez maintenant qu'il y a diverses possibilités :
enum TPixelFormat {pfDevice, pf1bit, pf4bit, pf8bit, pf15bit, pf16bit, pf24bit,
pf32bit, pfCustom}; Ainsi pour un bitmap à quatre bits par pixels, on
écrira :
Bitmap->PixelFormat=pf4bit;
Pour tester que la syntaxe est correcte, on
fait Alt F9 qui ne compile que la page cpp en cours (ce qui correspond à
Projet|compiler l'unité), vous constatez immédiatement que c'est
correct. Cette recherche est possible à tous les niveaux, ainsi
après :
Bitmap->Canvas->
le progiciel vous donnera toutes les
possibilités. Choisissez par exemple la propriété Pen et
continuez, on a alors :
Bitmap->Canvas->Pen->
Choisissez maintenant Color :
Bitmap->Canvas->Pen->Color
Sélectionnez le mot "Color"
et faites F1 pour connaître la syntaxe, choisissez maintenant
Tpen::Color, l'aide en ligne vous explique la signification de cette
propriété. Cliquez Tcolor, on vous donnera les mots clés
pour désigner les couleurs. Ainsi pour dessiner en rouge, on
écrira :
Bitmap->Canvas->Pen->Color=clRed;
Notez également qu'on copier-coller
est toujours possible allant de l'aide en ligne vers votre unité cpp de
code. Ainsi après avoir sélectionné le mot
"Pen", vous faites F1, cliquez maintenant sur "exemple" de
l'aide, vous voyez qu'il y a précisément une instruction
d'affectation de couleur. Copiez-collez-la sur votre fiche cpp :
pPB->Canvas->Pen->Color = clWhite;
Il suffit maintenant d'adapter par rapport
à notre contexte en remplaçant "pPB" par
"Bitmap" (nom de notre bitmap) et clWhite par clRed pour un trait en
rouge :
Bitmap->Canvas->Pen->Color=clRed;
42. Envoi à une fonction d'un
tableau à plusieurs dimensions
Dans le cas d'un tableau à une seule dimension, on se
souvient qu'on se contente de rajouter les crochets vides [ ] pour envoyer
ce tableau à une fonction, par exemple :
// Déclaration d'un tableau de 32
entiers
int D[32];
// Prototype de la fonction Calcul (déclaration en utilisant les
crochets vides [ ])
void Calcul(int[ ]);
// Fonction essai qui appelle la fonction Calcul
void essai()
{
Calcul(D);
}
// Fonction Calcul (appel en utilisant les crochets [ ])
void Calcul(int D[ ])
{// initialisation arbitraire du tableau (preuve qu'on accède aux
éléments)
for(int i=0;i!=32;i++) D[i]=0;
}
Si maintenant ce tableau a plusieurs
dimensions, vous êtes obligé de préciser ses dimensions
dans la déclaration du prototype et au moment de la fonction
elle-même pour les derniers indices, le premier faisant exception (ce qui
explique que les seuls crochets sont suffisants pour un tableau
unidimensionnel). Lorsque vous voulez tester des syntaxes de ce genre,
testez-les à part. Partez de zéro en ouvrant C++ Builder et
écrivez les syntaxes dans le fichier unit1.cpp ouvert par défaut
et même exécutez ce petit programme. Par exemple :
//--------Code écrit par C++ Builder
au départ dans unit1.cpp ------
#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//-----Fin du code écrit par C++ Builder au départ dans unit1.cpp
------
// Déclaration du tableau à deux dimensions
int D[3][32];
/* Prototype de la fonction Calcul
Observez les crochets vides pour le premier indice, mais pour le second
il faut repréciser les dimensions */
void Calcul(int[ ][32]);
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner)
{
/* Première fonction proposée par C++ Builder, on appelle la
fonction Calcul pour vérifier la syntaxe */
Calcul(D);
}
//---------------------------------------------------------------------------
/* Fonction Calcul qui reçoit le tableau d'entiers.
De même que tout à l'heure, observez les crochets vides pour le
premier indice,
(ce n'est d'ailleurs pas obligatoire, vous pouvez également indiquer
clairement
le nombre d'éléments par void Calcul(int D[3][32]), idem pour le
prototype)
mais pour le second il faut repréciser les dimensions. */
void Calcul(int D[ ][32])
{
int i,j;
for(i=0;i!=32;i++)
for(j=0;j!=3;j++)
D[0][i]=0;
/* Pour tester la validité de ce petit bout de programme,
positionnez le curseur sur la ligne de l'accolade ci-dessous
et faites F4 (exécuter jusqu'au curseur). Vous verrez que le
programme compilera tout et exécutera. Comme on en est arrivé
là i.e.
à la fin de la fonction, cela signifie clairement qu'il n'y a pas eu
d'erreurs. */
}
Après ce test, il vous suffit de
quitter C++ Builder sans rien sauvegarder ou même faire
Fichier|"Tout fermer" et rouvrir votre projet en cours de
développement. Ce type de possibilité est évidemment
très pratique pour éprouver une syntaxe dont on n'est pas
sûr. Pour des tableaux à plusieurs dimensions, il en ira de
même, on précisera ses dimensions dans le prototype et la
fonction. Notez enfin que pour des tableaux à dimensions fixes, il sera
préférable de donner ses dimensions par l'intermédiaire de
constantes, c'est plus élégant que d'écrire "en
dur" le nombre d'éléments. Cela a aussi l'avantage que si
plus tard ce tableau doit changer de dimensions, vous n'avez qu'à
modifier ces constantes sans avoir à chercher dans le source où
se trouve les différents accès au tableau. L'exemple
précédent se présentera donc de préférence
ainsi :
//--------Code écrit par C++ Builder
au départ dans unit1.cpp ------
#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//-----Fin du code écrit par C++ Builder au départ dans unit1.cpp
------
// Déclaration des deux constantes de dimension du tableau d'entiers
const int d1=3, d2=32;
// Déclaration du tableau à deux dimensions en utilisant les
constantes
int D[d1][d2];
/* Prototype de la fonction Calcul utilisant les constantes */
void Calcul(int[d1][d2]);
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner)
{
/* Première fonction proposée par C++ Builder, on appelle la
fonction Calcul pour vérifier la syntaxe */
Calcul(D);
}
//---------------------------------------------------------------------------
/* Fonction Calcul qui reçoit le tableau d'entiers. */
void Calcul(int D[d1][d2])
{
int i,j;
// utilisation des constantes d1 et d2 pour tester les bornes du tableau
for(i=0;i!=d2;i++)
for(j=0;j!=d1;j++)
D[0][i]=0;
}
43. La classe TSmallIntArray (version
Entreprise uniquement)
Si vous disposez de la version Entreprise de C++ Builder, il
sera préférable de gérer vos tableaux numériques
via les classes qui descendent de la classe virtuelle TBaseArray. Pour les
entiers courts par exemple (16 bits, 2 octets) il s'agit de la classe
TSmallIntArray prévue à cet effet.
Pour utiliser ces tableaux, il faut d'abord inclure le
fichier mxarrays.hpp. Mettez votre curseur devant le mot TSmallIntArray et
faites F1 (aide en ligne), vous voyez en dessous de la rubrique
"unité" le mot "mxarrays", c'est ainsi que l'on
connaît l'include à ajouter au source cpp. Si l'instruction
d'include est absente du source, C++Builder vous répondra à la
compilation "symbole TSmallIntArray non défini". Cela dit,
pour pouvoir déduire l'include à ajouter en cas d'incertitude,
une bonne méthode consiste à essayer un "grep" sous
commande MS-DOS (car l'aide en ligne est parfois succincte). Ainsi (avec un PC
classique) faites Démarrer>programmes>commandes MS DOS (ou
"invite de commandes" ce qui est la même chose). Si vous
arrivez à cette invite de commandes dans le répertoire
"Windows", descendez à la racine via "cd .."
puis allez au répertoire program files\Borland\cbuilder5\include,
à partir de la racine donc on écrit (pour une installation
classique de C++Builder) :
>cd "program
files"\borland\cbuilder5\include
Attention aux quotes nécessaires pour "program
files" du fait que le nom de ce répertoire est en deux mots. Si
maintenant vous faites :
C:\Program
Files\Borland\CBuilder5\Include>grep -l TSmallIntArray *.h
ou encore avec l'option -i en plus (pour ignorer la casse
majuscules/minuscules, ce qui vous permet d'écrire la chaîne
recherchée en minuscules) :
C:\Program
Files\Borland\CBuilder5\Include>grep -l -i tsmallintarray *.h
pour savoir dans quel fichier .h se trouve
déclaré TSmallIntArray, vous n'obtenez rien (pour plus
d'informations sur grep, faites grep ? avec un espace entre grep et ?). Cela
dit, vous voyez qu'il y a un répertoire Vcl (Visual Composant Library).
Allez-y :
C:\program
files\borland\cbuilder5\include>cd vcl
Refaites grep -l TSmallIntArray *.h (ou grep -l -i
tsmallintarray *.h) vous n'obtenez toujours rien. Faites
C:\Program
Files\Borland\CBuilder5\Include>Vcl>dir/p
pour voir le contenu avec arrêt à chaque page
écran de ce répertoire, vous voyez qu'il y a aussi des fichiers
.hpp, c'est-à-dire des headers spécifiques C++. Donc essayez le
même grep avec non plus h mais hpp :*
C:\Program
Files\Borland\CBuilder5\Include>Vcl>grep -l TSmallIntArray
*.hpp
vous voyez maintenant mxarrays.hpp et vous concluez qu'il
faut faire un include de ce fichier dans votre source pour pouvoir utiliser
TSmallIntArray. Ce type de recherche est parfois nécessaire. Par
exemple, allez dans l'aide en ligne de TSmallIntArray puis cliquez sur
propriétés puis SortOrder (tout en bas). L'aide en ligne vous
donne l'énumération
enum TsortOrder { TS_NONE, TS_ASCENDING, TS_DESCENDING};
Cela devrait signifier que si p est une instance de type
TSmallIntArray, j'ai le droit d'écrire :
p->SortOrder=TS_Ascending;
Or, essayez, déclarez d'abord une instance comme
suit :
TSmallIntArray *p = new
TSmallIntArray(0,0);
vous verrez que C++Builder vous répond que le symbole
TS_Ascending n'est pas défini. Que faire? Une solution consiste à
éditer ce fameux fichier mxarrays.hpp, faites Fichier|Ouvrir, dirigez
vous vers le répertoire Include\Vcl, sélectionnez dans le menu
déroulant "Type" du formulaire d'ouverture la série de
types où il y a "*.hpp", saisissez "mx*" dans le
champ Nom et validez par "Entrée" (vous ne voyez alors que les
fichiers du type hpp commençant par mx) et choisissez mxarrays.hpp,
là cherchez la chaîne "SortOrder", vous y trouvez
ceci :
enum TsortOrder { tsNone, tsAscending, tsDescending};
Vous en concluez facilement qu'il y a une petite inadvertance dans la doc en
ligne, le code exact est "tsAscending". L'instruction correcte sera
donc :
p->SortOrder=tsAscending;
Elle signifie que le tableau sera trié par ordre
croissant. À chaque fois qu'un élément rentrera dans le
tableau, celui-ci sera inséré à la bonne place par rapport
au tri demandé, le tableau sera ainsi toujours trié quel que soit
le nombre d'ajouts. Ci-après un petit programme de test au moment du
constructeur TForm1 (premier constructeur proposé à
l'entrée de C++Builder). On y déclare un tableau, on indique un
tri croissant, on remplit le tableau en mettant les valeurs les plus grandes en
premier (pour vérifier que le tri par ordre croissant s'opère
bien), on affiche le tableau dans un MessageBox et on constate qu'effectivement
le tri a fonctionné. Pour tester, faites un copier-coller de ce code
dans unit1.cpp en entrant dans C++Builder en remplacement de ce qui s'y trouve
par défaut.
#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
/* On inclut ici mxarrays.hpp pour pouvoir gérer TSmallIntArray */
#include <mxarrays.hpp>
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
/* Nombre de valeurs dans la tableau */
const NbVal=100;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner)
{
int i;
AnsiString M="";
/* Déclaration du tableau qui contiendra donc ici NbVal valeurs,
notez que le deuxième argument
n'est pas utilisé (type dummy), le premier indique la longueur du
tableau */
TSmallIntArray *p = new TSmallIntArray(NbVal,0);
/* La tableau sera trié par ordre croissant */
p->SortOrder=tsAscending;
/* Petite boucle d'insertion des éléments dans le tableau */
for(i=0;i!=NbVal;i++) p->Add(NbVal-i);
/* On va afficher son contenu pour voir */
for(i=0;i!=p->Count;i++)
M=M+IntToStr(p->GetItem(i))+" ";
Application->MessageBox(M.c_str(),"OK",MB_OK);
/* Libération de la mémoire du tableau */
delete(p);
}
Cela dit, il faut bien reconnaître que
seule la propriété de tri tsAscending fonctionne, si vous
remplacez tsAscending par tsDescending, vous obtenez toujours un tri croissant.
Il semble qu'il n'y ait, dans la version 5 de C++Builder, que deux
possibilités, soit tsNone (pas de tri, c'est le cas par défaut
quand la propriété SortOrder n'est pas initialisée) soit
tsAscending pour un tri croissant. Cela dit, tsNone est en conflit avec un
autre tsNone du fichier comctrls.hpp, l'instruction
p->SortOrder=tsNone;
(qui devrait être correcte) provoque une erreur de
compilation à cause de cette ambiguïté. Comme par
défaut, il n'y a pas de tri, le mieux est de ne pas initialiser cette
propriété si votre tableau n'est pas à trier.
Les autres tableaux du même type fonctionnent de la
même façon : TIntArray, TSingleArray et TDoubleArray etc.
(voir mxarrays.hpp car la documentation est muette sur ces types). Ou encore,
programmez une instance p et écrivez p-> comme suit
TIntArray *p=new TIntArray(0,0);
p->
et attendez quelques instants (avec le
curseur situé juste après p->), vous verrez que le progiciel
vous proposera dans un menu déroulant les propriétés et
méthodes possibles (sous réserve bien sûr que l'include
mxarrays.hpp se trouve dans le source car cette aide va lire
précisément ce fichier hpp pour en déduire les diverses
possibilités).
Si vous essayez ce même exemple avec TSingleArray, le
compilateur vous indiquera une ambiguïté s'agissant de la fonction
IntToStr, il faut alors préciser qu'il s'agit d'un caractère et
faire une conversion en char au moment du GetItem, on écrira donc :
M=M+IntToStr((char)p->GetItem(i))+" ";
Remarquez que pour utiliser ces objets, on utilise
invariablement le couple new/delete, new pour créer une instance de
l'objet et delete pour libérer la mémoire allouée
correspondante. L'aide en ligne vous rappellera toujours ce point, par exemple
pour le constructeur TSmallIntArray::TSmallIntArray, elle précise
"Appelez TSmallIntArray indirectement, en utilisant le mot clé new,
pour créer et initialiser un objet TSmallIntArray", pour d'autres
objets on trouve toujours ce même type de formules. Appelez l'aide en
ligne (ou cliquez le petit dictionnaire en haut de l'écran),
sélectionnez l'onglet index et cherchez les constructeurs d'objets (ils
commencent par T majuscule et le nom du constructeur est par définition
le même que le nom de l'objet donc du type Txxx::Txxx), par exemple pour
TabstractSocket::TabstractSocket on lit "N'appelez pas le constructeur
TAbstractSocket. Utilisez à la place le mot clé new pour appeler
le constructeur pour le descendant approprié de TAbstractSocket" ou
pour Tlist::Tlist "N'appelez pas directement TList. Utilisez à la
place le mot clé new" et ainsi de suite pour tous les objets. C'est
une remarque importante, on crée une instance par new et on
libère la mémoire par delete, syntaxes spécifiquement C++.
Remarquez enfin que si p est une instance de type
TSmallIntArray comme dans le code précédent, une instruction du
type :
p->Count=n;
(où n est un nombre entier) donne une nouvelle
dimension au tableau, plus grande ou plus petite. C'est évidemment
très pratique pour redimensionner un tableau. En général,
on cherche plutôt à agrandir un tableau. Count est donc une
propriété en lecture/écriture, en lecture p->Count sera
égal au nombre d'éléments du tableau au moment de
l'exécution de cette instruction et en écriture à la
nouvelle dimension du tableau.
Il faut savoir que ces procédés sont un peu
plus lents que l'utilisation purement C de malloc/realloc, en revanche vous
êtes dans une vraie structure C++, plus maniable, plus facile aussi et en
tout état de cause vous êtes protégé au niveau
système par la gestion des exceptions. Si vos tableaux ne sont pas
gigantesques, vous devriez plutôt utiliser ces objets, vous ne verrez pas
la différence au plan de la rapidité d'exécution et vous
bénéficiez ipso facto d'une surcouche de protection
système du fait même d'être plus éloigné de la
gestion mémoire en direct.
Comme de toute façon TsmallIntArray est un tableau
dynamique avec réallocation automatique et transparente pour
l'utilisateur, vous pouvez tout aussi bien partir d'un tableau vierge ne
contenant rien du tout, la déclaration par new ne déclarera
finalement que le pointeur qui pointera NULL par exemple :
TSmallIntArray *T = new
TSmallIntArray(0,0);
Ici, T est du type pointeur sur un objet de type
TsmallIntArray et pointe zéro élément. Les
éléments s'ajouteront automatiquement par Add ou Insert. C'est
ainsi que le petit programme suivant est tout à fait correct :
const int NbVal=100;
// Déclaration du tableau qui ne contient rien
TSmallIntArray *T = new TSmallIntArray(0,0);
int i;
AnsiString Mess="";
// On remplit le tableau arbitrairement juste pour essayer la syntaxe
for (i=NbVal;i!=0;i--) T->Add(i*3);
// On va maintenant afficher son contenu
for(i=0;i!=NbVal;i++) Mess=Mess+IntToStr(T->Items[i])+ " ";
Application->MessageBoxA(Mess.c_str(),"Ok",MB_OK);
// On détruit l'instance
delete T;
Notez que si vous voulez ajouter des éléments
non par ajout (Add) mais par insertion (Insert), il faut donner un pointeur sur
un int et non un int c'est-à-dire un int* et non un int. La syntaxe
diffère donc du Add. Par exemple, pour l'insertion d'un entier,
procédez ainsi. Déclarez un tableau d'un seul entier :
int Nombre[1];
Ce tableau ne contient donc qu'un seul entier Nombre[0],
pour faire une insertion la syntaxe sera donc par exemple (on insère ici
l'entier 1234 à l'indice 5 du tableau) :
Nombre[0]=1234;
T->Insert(5,Nombre);
Par exemple, essayez ceci, vous verrez que c'est correct.
const int NbVal=100;
// Déclaration d'un tableau vierge
TSmallIntArray *T = new TSmallIntArray(0,0);
int i;
AnsiString Mess="";
int Nombre[1];
// remplissage du tableau
for (i=NbVal;i!=0;i--) T->Add(i);
// On insère en plus un élément à l'indice 5
Nombre[0]=1234;
T->Insert(5,Nombre);
// On affiche le tableau qui contient maintenant NbVal+1 entiers
for(i=0;i!=NbVal+1;i++) Mess=Mess+IntToStr(T->Items[i])+ " ";
Application->MessageBoxA(Mess.c_str(),"Ok",MB_OK);
// On libère la mémoire
delete T;
Autre difficulté : Dans le cas d'un tableau
trié, vous avez la possibilité de refuser l'insertion de
doublons. Si T est un tableau du type TsmallIntArray, on devrait pouvoir
écrire :
T->Duplicates=dupIgnore;
Positionnez le curseur sur la propriété
Duplicates, faites F1 (aide en ligne) et choisissez TbaseArray::Duplicates (car
TsmalIntArray est un descendant préprogrammé de TbaseArray), vous
voyez que dupIgnore fait partie de l'énumération proposée.
Or, cette écriture est refusée par le compilateur comme
ambiguë, il dit qu'il y a ambiguïté entre dupIgnore et
Classes::dupIgnore. Que faire? On constate effectivement que dans le
répertoire Vcl, la commande suivante :
C:\Program
Files\Borland\Cbuilder5\Include\Vcl>grep -l dupIgnore *.hpp
c'est-à-dire la recherche des fichiers du type hpp
contenant la chaîne "dupIgnore" vous affiche deux fichiers
à savoir mxarrays.hpp et classes.hpp. Ouvrez ces deux fichiers sous
C++Builder et constatez d'une part que mxarrays.hpp appelle par un include
classes.hpp au début et d'autre part que dans ces deux headers hpp vous
avez à un moment donné (recherchez la chaîne
"dupIgnore") les lignes suivantes :
#pragma option push -b-
enum TDuplicates { dupIgnore, dupAccept, dupError };
#pragma option pop
ce dont se plaint le compilateur. C'est sans doute une
petite inadvertance du progiciel, une double déclaration. Une
possibilité consiste à faire une copie de mxarrays.hpp,
appelez-la par exemple mxarraysCopie.hpp (de manière malgré tout
à conserver l'original officiel), à supprimer ces trois lignes
(ou à les mettre en commentaire via le double slash //) dans cette
copie et à faire un include de ce nouveau hpp au lieu de l'ancien.
Constatons par un petit exemple que ça marche :
#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
/*On inclut ici notre version de mxarrays.hpp dans laquelle on a
supprimé l'énumération TDuplicates */
#include "mxarraysCopie.hpp"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
AnsiString Mess="";
int i;
// On déclare ici un tableau vierge
TSmallIntArray *T=new TSmallIntArray(0,0);
// ce tableau sera trié par ordre croissant
T->SortOrder=tsAscending;
// et on refusera les doublons
T->Duplicates=dupIgnore;
/* On remplit ce tableau en commençant par de plus
grandes valeurs vers des petites pour vérifier le tri
et on insère à chaque fois la valeur 13, laquelle ne devrait
être prise en compte que la première fois */
for(i=0;i!=10;i++)
{
T->Add(10-i);
T->Add(13);
}
// On va afficher le nombre d'élément de ce tableau
Mess="Il y a "+IntToStr(T->Count)+ "éléments
dans le tableau : ";
// ainsi que tous ses éléments
for (i=0;i!=T->Count;i++) Mess=Mess+IntToStr(T->Items[i])+" ";
/* On constate effectivement que le tableau est trié et que la valeur 13
n'a été insérée qu'une seule fois */
Application->MessageBox(Mess.c_str(),"TABLEAU",MB_OK);
// Ce petit test est terminé, on libère la mémoire
delete T;
}
Notons que comme dupIgnore est la propriété
par défaut, on peut contourner ce problème en n'initialisant pas
cette propriété. Cela dit, si vous préférez
utiliser comme proposé ici une copie de mxarrays.cpp, profitez-en pour
renommer aussi dans votre copie mxarraysCopie.hpp l'option tsNone (dont on a vu
précédemment qu'elle posait problème), appelez-la par
exemple tsNone1, dans ces conditions, vous pourrez initialiser la
propriété SortOrder, il suffira d'écrire :
T->SortOrder=tsNone1;
ce qui vous donnera la possibilité de passer d'un
tableau trié à un tableau non trié et vice versa. Notez
enfin que pour connaître les caractéristiques du tableau pendant
l'exécution, il suffit après un arrêt d'exécution
(par exemple suite à un "exécuter jusqu'au curseur") et
de faire Exécuter|Inspecter, là vous donnez le nom du tableau (en
l'occurrence ici T), on vous affiche toutes les propriétés (ce
qui vous permet notamment de savoir les choix par défaut) et toutes les
méthodes. Cela dit, vous ne voyez là que les
caractéristiques du tableau. pour voir son contenu suite à un
arrêt, faites Exécuter|inspecter, là entrez le nom du
tableau (dans l'exemple précédent, c'est T), en regard de FMemory
vous avez l'adresse commençant par deux points, copiez cette adresse via
Ctrl C (vous êtes dirigé vers autre fenêtre mais vous
ignorez) puis faites Voir|Fenêtre de déboggage|CPU (ou Ctrl Alt
C), cliquez dans le rectangle en bas à gauche avec le bouton droit et
choisissez "Aller à l'adresse" (1ère option
du menu déroulant, ou faites Ctrl G ce qui revient au même),
là coller l'adresse (Ctrl V) et remplacez les deux points par
"0x" car il faut donner l'adresse en hexadécimal, on vous
affiche alors le contenu du tableau. Ça a l'air assez compliqué,
en fait c'est une petite habitude à avoir :
Exécuter|Inspecter
donnez le nom du tableau
sélectionnez l'adresse en face de Fmemory par déplacement du
curseur sur elle (reverse video)
Ctrl C (copier)
ignorer la fenêtre de modification (obtenue suite à Ctrl C)
Ctrl Alt C (appel de la fenêtre CPU)
Ctrl G (petit formulaire qui demande l'adresse d'affichage)
Ctrl V (copie de l'adresse)
Remplacez les deux points par "0x" (adresse en hexadécimal)
Validez
Les octets sont maintenant affichés dans le rectangle en bas à
gauche
44. La classe TStringList
La classe TstringList vous permet de gérer des listes
de chaînes de caractères très facilement. Vous
déclarez la liste par l'opérateur new, vous ajoutez vos
chaînes via la méthode Add et vous accédez aux
éléments via Strings. Par exemple, vous pouvez déclarer
une telle liste pour tous les messages d'erreur de votre application.
// On déclare L qui un objet de type
TstringList
TStringList *L=new TStringList;
/* Déclarez vos messages d'erreur (avec bien sûr de vrais
messages)
Le premier message aura pour indice 0, le second 1 et ainsi de suite.
Notez que vous ne vous occupez absolument pas de la taille de la liste,
si vous avez une nouvelle chaîne, vous ajoutez simplement un Add. */
L->Add("erreur 1");
L->Add("erreur 2");
L->Add("erreur 3");
L->Add("erreur 4");
L->Add("erreur 5");
L->Add("erreur 6");
/* On affiche ici à titre d'exemple dans un MessageBox le message
correspondant à l'erreur 3 donc d'indice 2. Comme L->Strings[2] est
un AnsiString, on applique la méthode c_str() pour le convertir en
un chaîne terminée par zéro qu'exige MessageBox */
Application->MessageBox(L->Strings[2].c_str(),NULL,MB_OK);
/* À la fin de l'application, on n'oublie pas de libérer la
mémoire */
delete(L);
Pour constater que la syntaxe est correcte,
copiez-collez cette séquence entre les accolades {} du tout premier
constructeur TForm1::TForm1 en entrant dans C++Builder et faites F9
(exécuter), le programme vous affichera "erreur 3" qui
correspond bien au message d'indice 2 qu'on a demandé d'afficher.
Autre possibilité : charger en une seule fois ce
fichier d'erreurs (ou autres bien sûr) par la méthode
LoadFormFile, par exemple :
L->LoadFromFile("C:\\Program
Files\\Borland\\CBuilder5\\Projects\\Gilles\\FicErr.txt");
N'oubliez pas les doubles anti-slaches \\ nécessaires
dans la chaîne désignant le chemin et le fichier. Si la
chaîne est trop longue sur une seule ligne, C++ vous permet
d'écrire comme vous le savez la chaîne sur plusieurs lignes, ce
qui améliore la présentation du source :
L->LoadFromFile("C:\\Program
Files\\Borland"
"\\CBuilder5\\Projects\\Gilles\\FicErr.txt");
C'est assez pratique pour concaténer de longues
chaînes. Le fichier FicErr.txt que vous créez sous Word (dans ce
cas sauvegardez-le au format texte pour avoir un "point txt") ou un
Note Pad (bloc notes) contient toutes les chaînes de caractères
qui seront automatiquement incluses dans l'instance L de type TStringList. Pour
chaque ligne de ce fichier texte, vous aurez une occurrence dans L accessible
par la syntaxe L->Strings[i] qui est la ième occurrence de ce
fichier, 0 étant la première, 1 la deuxième et ainsi de
suite. L->Strings[i] est du type AnsiString, si vous avez besoin d'une
chaîne classique C terminée par zéro du type char*, il
suffit d'appliquer la méthode c_str() et donc d'écrire
L->Strings[i].c_str(). À vous de savoir si vous
préférez que ces chaînes soient internes (donc
ajoutées via Add, obligatoire si ces chaînes vont dépendre
de l'utilisation de l'application) ou externes (donc chargées en une
seule fois via LoadFromFile, toujours possible s'il s'agit de constantes). Voici un petit exemple de lecture d'un fichier Essai.txt en ligne à ligne, ce fichier se trouvant dans le répertoire par defaut, ce pourquoi on n'indique ici que son nom.
int i;
AnsiString A;
TStringList *L;
L=new TStringList();
L->LoadFromFile("Essai.txt");
for(i=0;i<L->Count;i++)
{
A=L->Strings[i];
// La ligne étant lue dans A, on la traite ici
}
delete L;
45. La classe TListBox
C'est une liste de chaînes de caractères qui
peut dans un contexte d'application désigner n'importe quoi : des
options, des noms de fichiers ou de personnes, des couleurs, des villes ou
autres. L'intérêt est que l'on peut cliquer une occurrence et
savoir logiciellement l'occurrence choisie. Constatons en effet qu'en deux
lignes de programme, une ligne au constructeur TForm1::TForm1 et une ligne
à l'événement OnClick du ListBox, on remplit un ListBox
d'occurrences et on affiche dans un label ce qui a été
cliqué dans la liste.
Partez d'un projet vierge (ce que vous obtenez en entrant
dans C++ Builder) et mettez dans la première fiche appelée par
défaut Form1 deux composants, d'une part ListBox et d'autre part Label.
Ces deux composants se trouvent dans l'onglet "standard"
c'est-à-dire le premier onglet de la palette de composants. Donc, pour
ce faire, Cliquez sur l'objet ListBox de la palette puis cliquez sur Form1 pour
y déposer cet objet, ensuite mettez ListBox par exemple vers la gauche
de la fiche Form1, élargissez-le un peu vers la droite et
étirez-le vers le bas de manière à avoir un rectangle
debout de la largeur d'une petite chaîne de caractères. Puis
cliquez sur Label (il est désigné dans la palette par la lettre
A) et posez-le sur Form1 en cliquant sur la fiche, à droite du rectangle
ListBox. Étirez ce label de manière à avoir un rectangle
horizontal qui contiendra une chaîne de caractères. Ensuite
écrivez ceci dans le constructeur TForm1, premier constructeur par
défaut dans unit1.cpp :
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
for(int i=0;i!=1000;i++)
ListBox1->Items->Add("Chaîne numéro
"+IntToStr(i+1));
}
Vous voyez qu'après la construction de
la fichier Form1, on écrit mille chaînes de caractères
numérotées de 1 à 1000. On utilise pour cela la syntaxe
ListBox1->Items->Add(AnsiString) et IntToStr pour convertir un entier en
AnsiString.
Maintenant, sélectionnez dans l'inspecteur d'objets
le composant ListBox (ListBox1 par défaut), sélectionnez l'onglet
"Événements" et choisissez "OnClick".
Là, C++ Builder crée pour vous la déclaration de la
méthode correspondante à savoir TForm1::ListBox1Click.
Écrivez la ligne ci-dessous, elle exprime que la chaîne qui sera
cliquée dans la ListBox sera affichée dans Label. En effet,
ListBox1->ItemIndex désigne le numéro de la chaîne
cliquée (en l'occurrence dans notre exemple, il sera compris en 0 et
999). Comme ListBox1->Items->Strings[i] représente la ième
chaîne de ListBox, ListBox1->Items->Strings[ListBox1->ItemIndex]
représente naturellement l'AnsiString correspondant à la
chaîne cliquée (c'est-à-dire la chaîne numéro
ListBox1->ItemIndex). Quant à Label1->Caption, il est la
chaîne à écrire dans le label, cette chaîne
apparaît au moment de l'exécution de cette instruction. .
void __fastcall TForm1::ListBox1Click(TObject
*Sender)
{
Label1->Caption=ListBox1->Items->Strings[ListBox1->ItemIndex];
}
Faites F9 pour exécuter, vous voyez la
ListBox remplie des mille chaînes numérotées, on voit
"Label1" écrit à l'écran, c'est la chaîne
affichée par défaut pour le label. Cliquez une occurrence de la
ListBox, le programme affiche immédiatement ce choix à la place
du label. Ceci pour vous montrer les syntaxes d'accès aux
éléments. Si vous voulez partir d'un label vierge,
sélectionnez cet élément dans l'inspecteur d'objets,
choisissez "propriétés" et effacez la chaîne
"Label1" dans l'on voit en regard de la propriété
Caption. Refaites alors F9 pour réexécuter. Notez que si vous
voulez faire disparaître ce ListBox de l'écran, il vous suffit
d'écrire :
ListBox1->Visible=false;
Si vous voulez initialiser ce ListBox avec des occurrences
dès le départ, indiquez-les dans l'inspecteur d'objet, cliquez
dans Tstrings en regard de items, là on vous présente un tableau,
saisissez vos occurrences par défaut, c'est tout. Les occurrences
programmées viendront s'ajouter à celles inscrites à
l'initialisation. Si vous voulez modifier la position à l'écran
de cet objet, utilisez les propriétés Top et Left, Top
désigne le nombre de pixels en vertical à partir du haut de la
fenêtre parent et Left la coordonnée horizontale, sachant que le
point (0,0) est le point le plus en haut à gauche de la fenêtre
parent. Ainsi pour afficher ce ListBox à la position (80,40), on
écrira :
ListBox1->Left=80;
ListBox1->Top=40;
Pour obtenir la signification d'une propriété
et souvent un exemple, posez le curseur sur la case de cette
propriété dans l'inspecteur d'objets et faites F1, aide en ligne.
-:-
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.
|