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


Janvier 2002

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 :

  1. 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.
  2. 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.
  3. NBZ est un pointeur d'entiers, NBZ[i] sera le nombre de zones du fichier numéro i.
  4. 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**));
  5. 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é.
  6. Idem pour le pointeur LZ de longueur de zones LZ=(int**)  realloc(LZ,(NumFic+1)*sizeof(int*));
  7. 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.
  8. 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.
  9. 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.
  10. 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*));
  11. 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.
  12. 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.
  13. 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.
  14. 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.
  15. 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.
  16. 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.
  17. 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 à :

  1. 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)
  2. 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));
  3. 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.
  4. 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 cœur, 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.

-:-

par Gilles Louise

Septembre 2000





Hit-Parade