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.
-:-
|