Remarques de développement avec C++ Builder 5

Gilles Louïse

- Titre : Remarques de développement avec C++ Builder 5

- Auteur : Gilles Louïse

- Parution : 1 mars 2002

- Licence : Copyright ® 2013 Gilles Louïse. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Vue d'ensemble de C++ Builder 5

- 1. À propos de la fonction "Exécuter|jusqu'au curseur"

- 2. Points d'arrêt

- 3. Point d'arrêt pendant l'exécution

- 4. Ouverture d'un projet.

- 5. Conversion AnsiString en Char*

- 6. Aide en ligne

- 7. Erreur de compilation

- 8. Construire et Make

- 9. Appel d'une fonction à l'intérieur du code d'un événement

- 10. Arrêt d'exécution

- 11. Extraction à l'intérieur d'un

- 12. La fonction IntToHex

- 13. Longueur de vos buffers

- 14. Explorateur de classes

- 15. Initialisations

- 16. Déclaration des variables

- 17. Fenêtre d'exécution

- 18. Grand fichier cpp

- 19. Gestionnaire de projet

- 20. Conseil personnel

- 21. Remarques et exemple autour de malloc (memory allocation)

- 22. Autres exemples de malloc

- 23. Exemple de malloc pointant des structures

- 24. Malloc dans un cas de tableaux de pointeurs de séries de structures

- 25. À propos de la réallocation mémoire (realloc)

- 26. À propos des unsigned

- 27. À propos du signe * (étoile)

- 28. Polices de caractères du source C++

- 29. Recherche dans le source

- 30. Les pointeurs de pointeurs

- 31. Création de fichiers d'aide

- 32. Autre exemple lié à des allocations et ré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

- 46. La classe TEdit

- 47. La classe TList

- 48. Conditions sur points d'arrêt

- 49. Série de touches fréquentes

- 50. Les menus variables

- 51. Gestion de fenêtres-enfants

- 52. Le composant ScrollBar

- 53. Le composant PageControl

- 54. Les classes TControlBar, TToolBar, TToolButton et PopupMenu.

- 55. L'éditeur d'images

- 56. La classe TImageList

- 57. Les applications MDI

- 58. Le composant TActionList

- 59. La fonction GetCursorPos

- 60. Initialisation d'une application

- 61. Gestion des exceptions

- 62. La méthode StretchDraw

- 63. Les propriétés ClientWidth et ClientHeight

- 64. Création d'un exécutable indépendant

Vue d'ensemble de C++ Builder 5

Quand vous entrez dans C++Builder, l'écran se divise en quatre parties :

1. Le menu du progiciel en haut ;
2. L'inspecteur d'objets sur la gauche ;
3. Une première fenêtre grise ;
4. Le source de base correspondant (en dessous de la fenêtre, faites F12 pour le voir).

Deux touches sont à connaître dès à présent :

1. F12 qui permute la fenêtre grise avec le source. Si la fenêtre est visible, F12 fera apparaître le source et inversement ;
2. F11 qui fait apparaître l'inspecteur d'objets.

La toute première chose à faire quand vous entrez dans C++Builder est :

1. Soit de sauvegarder le projet vide proposé dès l'entrée ;
2. Soit d'ouvrir un projet existant (via "Rouvrir" du menu principal).

La raison en est que C++Builder, par sécurité, sauvegarde de lui-même le projet courant même complètement vide au bout d'une minute ou deux (il prend la main de son propre chef quelques secondes pour cette sauvegarde automatique), il choisit le nom général de "unit1" pour le source et "projet1" pour l'application en général. Si donc vous n'exécutez pas une des deux opérations données ci-dessus, vous allez vous retrouver avec ce type de fichiers (unit1 et projet1) dans le répertoire courant utilisé.

Notez que la fenêtre source C++ contient sur sa gauche une autre fenêtre, l'explorateur de classes. Cet explorateur, comme son nom l'indique répertorie les classes de votre application ainsi que le contenu de ces classes, données et fonctions. Comme il n'est pas toujours nécessaire de disposer de cette fenêtre, je vous conseille de ne pas toujours l'afficher ce qui donne plus de place pour le source en lui-même. On se débarrasse bien sûr de cette fenêtre en cliquant sur le petit x de la fenêtre, mais si vous n'en voulez pas pour une longue durée, demandez à ce quelle ne soit plus affichée à partir de maintenant en faisant "Outils|Options d'environnement", là sélectionnez l'onglet "explorateur de classes" et décochez simplement la case "Montrer l'explorateur", case que vous pouvez recocher à tout moment. Vous disposez en plus de la fonction du menu "Voir l'explorateur de classes".

Pour bien naviguer durant votre développement de l'inspecteur d'objets à une fiche (qui sera très souvent la fiche principale Form1) et inversement, je vous conseille de ne pas faire se chevaucher ces fenêtres en mettant l'inspecteur d'objets à gauche et la fiche à droite. Vous passez alors agréablement de l'un à l'autre d'autant que quand vous modifiez des éléments de la fiche, les transformations dans l'inspecteur se font en temps réel et inversement allant de l'inspecteur à la fiche.

Je vous conseille de vous créer un répertoire spécial de test, lui-même sous-répertoire du répertoire officiel créé à l'installation "projects", appelez-le par exemple "Essai". Il vous permettra de tester certaines syntaxes ou composants. Ainsi, pour tester un composant avec C++Builder, entrez dans le progiciel (ou ce qui revient au même, faites "Nouvelle application"), sauvegardez tout de suite ce projet vide nouveau dans le répertoire Essai (choisissez "Enregistrer le projet sous" du menu principal). Je vous conseille de choisir non pas unit1 proposé par défaut, mais unit01 et projet01. Ainsi, à chaque nouveau test, vous passez à l'indice suivant, unit02 et projet 02, unit03 et projet03 et ainsi de suite. La raison en est qu'avec deux chiffres, vos tests seront toujours triés même au-delà de dix alors que si vous choisissez un indice d'un seul chiffre, vous aurez projet10 au-dessous de projet1 et non pas au-dessous de projet9 alors que projet10 viendra logiquement au-dessous de projet09. Si vous devez créer une nouvelle fiche, celle-ci sera nécessairement associée à une nouvelle unité (c'est la règle avec C++Builder), utilisez alors les lettres dans le nom de l'unité. Par exemple, vous en êtes à unit05 et projet05, imaginons que ce projet05 doit avoir une seconde fiche. Vous faites donc logiquement "nouvelle fiche" du menu principal, sauvegardez immédiatement cette nouvelle unité (choisissez "enregistrez sous") et choisissez comme nom unit05b, si vous devez en avoir une autre, sauvegardez-la sous le nom de unit05c et ainsi de suite. Tous vos tests personnels seront ainsi triés et structurés.

Je vous conseille aussi de tenir à jour, par exemple avec Notepad, un petit fichier de syntaxes C++ pour les avoir toujours à disposition, car on ne peut pas se souvenir de tout. Quand, dans un de vos tests ci-dessous décrits, une syntaxe C++ vous semble importante et difficile à retenir, faites un copier-coller de l'unité cpp (c plus plus) vers ce petit fichier Notepad doté éventuellement d'un petit commentaire. Ces petits tests structurés et ce petit bloc-notes personnel vous permettront d'évaluer votre propre avancée et de ne pas perdre votre travail.

Quant à l'application particulière que vous allez développer, créez-la dans un répertoire nouveau et spécifique à cette application, je vous conseille d'avoir un répertoire par application. Ce répertoire sera logiquement un sous-répertoire de "projects" dans lequel vous avez déjà créé "Essai" pour vos tests. Comme toujours, vous allez sauvegarder tout de suite le projet vide dès l'entrée dans C++Builder, mais choisissez cette fois-ci un nom signifiant pour votre projet. Pour l'unité (par défaut unit1), donnez un nom qui représente une notion importante du projet, pour le nom du projet, donnez le nom le plus global qui soit, ce sera aussi in fine le nom de l'exécutable. Bien entendu, ces noms peuvent changer au cours du développement, mais une bonne habitude consiste à donner des noms significatifs dès le début.

Notez que pour l'unité (par défaut unit1 proposé), C++Builder vous crée et le cpp (le source C++) et le h (header). Pour faire apparaître le header, faites ctrl F6 (contrôle F6) ou alors, cliquez à droite pour avoir le menu surgissant et choisissez la toute première possibilité "ouvrir le fichier source/en-tête". Ceci est notamment utile si vous devez rajouter des méthodes à la classe créée d'office par C++Builder.

Il est important de comprendre les fonctions les plus courantes dans le menu de C++Builder. Par exemple, nous avons dit que F12 permet de permuter la fiche avec le source, vous avez la possibilité d'avoir la même fonction via une icône. En laissant le curseur quelques instants sur une option du menu, une bulle d'aide (hint en anglais) apparaît, essayez de trouver l'icône correspondant à F12 (l'icône montre deux petits objets, l'un grisé l'autre blanc, munis de deux petites flèches pour évoquer la permutation), la bulle d'aide affichera "Basculer Fiche/Unité [F12]", il sera donc équivalent de faire F12 ou de cliquer cette option du menu. Dans le menu "Voir" vous avez également l'option "Basculer Fiche/Unité" qui fait la même chose.

Notez que le menu de C++Builder se constitue de toolbars (on reconnaît ce composant par ses deux petits traits verticaux sur le côté gauche). Vous pouvez disposer ces toolbars comme vous l'entendez, vous pouvez même les sortir de leur cadre (espace nommé controlbar), ils deviennent alors de petites fenêtres que vous pouvez aussi supprimer. Pour faire réapparaître une toolbar en cas de disparition, faites "Voir|Barre d'outils" et sélectionnez la barre qui vous manque. Vous pouvez aussi cliquer à droite en pointant la zone toolbars de C++Builder, la liste des toolbars s'active et vous pouvez en faire réapparaître ou disparaître.

Pour tout enregistrer facilement, repérer l'icône qui représente plusieurs disquettes en cascade, il suffit de cliquer dessus, l'option se désactive alors. Dès qu'il y a la moindre modification dans votre projet, elle se réactive.

Pour rouvrir facilement un projet, vous pouvez faire "Fichier|Rouvrir», mais vous pouvez aussi repérer une icône jaune qui représente une sorte de dossier qui s'ouvre, juste à côté il y a une petite flèche, cliquez-la et choisissez votre projet. C++Builder a mémorisé pour vous les projets les plus récents. Si vous ouvrez toujours le dernier projet (ce qui est le cas quand on travaille toujours sur un même projet), il vous suffit d'appuyer sur la touche zéro, ce qui revient à sélectionner le premier élément de la liste, donc le plus récent.

Si vous cliquez une icône du menu dans la zone toolbar, vous obtenez alors la fenêtre de dialogue correspondant à cette fonction avec toujours une possibilité d'aide qui vous explique en français les diverses possibilités. Si vous passer par le menu déroulant, il vous suffit de faire F1, vous aurez l'explication de l'option qui était en surbrillance à ce moment-là. De même si vous sélectionnez un composant de la palette, faites F1 pour avoir de l'aide sur ce composant. De même quand le curseur se trouve sur un mot-clé de votre source, F1 vous donnera de l'aide sur cette fonction. Il est impossible de travailler sans ces aides, car il est impossible de se souvenir de tout.

Quand vous utilisez l'aide sur un composant après avoir sélectionné un composant de la palette et fait F1, vous tombez sur une fenêtre avec notamment "propriétés" et "méthodes", il s'agit évidemment des propriétés et des méthodes liées au composant sélectionné, vous avez aussi "événements". Choisissez par exemple "propriétés", une fenêtre apparaît, mais l'autre reste visible. Faites en sorte que ces deux fenêtres ne se chevauchent pas, mettez par exemple la première fenêtre à droite et quand vous cliquez sur "propriétés", la fenêtre qui s'affiche à gauche sans chevauchement. Cette petite astuce va accélérer votre recherche, car quand vous allez cliquer maintenant une propriété, l'aide correspondante va s'afficher sur la fenêtre de droite et vous passez facilement de l'une à l'autre. En général, on cherche un exemple pour avoir les syntaxes d'accès.

Pour commencer avec C++Builder, le mieux est un bon tutoriel c'est-à-dire un exemple complet expliqué au pas à pas, le fichier d'aide vous donne deux tutoriels. D'une manière générale, la règle du jeu est très simple. En entrant dans C++Builder, une fiche principale est déjà créée avec le code associé automatiquement écrit par le progiciel. Sauvez en premier lieu ce projet dans votre répertoire "Essai", pour ce faire cliquez l'icône qui représente des "disquettes en cascade" et choisissez unit01 et projet01. À ce stade, vous pouvez déjà cliquer la petite flèche verte pour exécuter (ou faire F9), le code écrit par C++Builder lui-même se compile puis s'exécute. Vous ne voyez qu'une fenêtre, mais vous pouvez la déplacer, elle "sait" se redessiner, vous pouvez modifier sa taille et son menu système (i.e. les trois petits boutons en haut à droite de la fenêtre) est opérationnel. En cliquant le petit x de ce menu pour sortir, vous revenez à C++Builder.

Le principe de C++Builder consiste simplement à ajouter des composants de la palette de composants et à écrire le code relatif à certains événements. Par exemple, sélectionnez le composant "Bouton" en cliquant dessus, il se trouve dans l'onglet "Standard" de la palette de composants, c'est un petit rectangle à l'intérieur duquel on lit "OK". Une fois sélectionné dans la palette, cliquez n'importe où dans la fenêtre principale que C++Builder a nommé Form1. Vous venez de déposer ce composant dans la fenêtre principale et l'inspecteur d'objets confirme la présence de ce bouton. C++Builder suppose que vous n'allez pas garder ce nom de "Button1", c'est pourquoi dans l'inspecteur d'objets vous voyez la propriété Caption (ce qui signifie "nom affiché à l'écran") activée, mettez le curseur dans cette case et choisissez un autre nom par exemple "TEST". Vous voyez qu'en temps réel, l'affichage est mis à jour dans la fenêtre principale. Remarquez le nombre important de propriétés disponibles par exemple Left et Top qui sont les coordonnées de ce bouton dans la fiche. Déplacez ce bouton dans la fiche, vous verrez que C++Builder a mis a jour ces deux propriétés. De même Width et Height qui sont les dimensions du bouton. Si maintenant nous faisons F9 pour exécuter, ça marche, mais nous n'avons pour l'instant associé aucune action au bouton.

Revenons sous C++Builder (cliquez le petit x du menu système pour stopper l'exécution) et sélectionnez l'onglet "événements" de l'inspecteur d'objets. Vérifiez bien que le bouton est bien sélectionné, car nous avons maintenant deux objets à savoir Form1 et Button1. Si vous cliquez le bouton, l'inspecteur d'objets sélectionne le bouton, mais si vous cliquez la fiche, il sélectionne logiquement Form1. D'ailleurs, en haut de l'inspecteur d'objets vous disposez d'un petit menu déroulant pour choisir un objet. L'objet Button1 étant visé par l'inspecteur d'objets, sélectionnez l'onglet "événements", on vous montre tous les événements que vous pouvez programmer. Double-cliquez sur l'événement OnClick, C++Builder vous crée ce qu'on appelle le gestionnaire d'événement associé dans le source, c'est simplement la méthode qui sera appelée quand le bouton sera cliqué et il positionne le curseur entre les deux parenthèses de la méthode, car C++Builder attend que vous programmiez ce qui se passera quand le bouton sera cliqué.

void __fastcall TForm1::Button1Click(TObject *Sender)
{

}

Il s'agit bien d'une méthode de TForm1, laquelle a été déclarée dans la classe TForm1, faites ctrl-F6 (contrôle F6) pour faire afficher le source en-tête, vous lisez clairement void __fastcall Button1Click(TObject *Sender); dans la section "published". Affichons un message quand le bouton sera cliqué, à la position du curseur on écrit par exemple :

Application->MessageBox("Bouton cliqué!", "OK", MB_OK);

La méthode Button1Click se présente donc sous cette forme :

void __fastcall TForm1::Button1Click(TObject *Sender)
{
Application->MessageBox("Bouton cliqué!", "OK", MB_OK);
}

Faites F9 pour compiler-exécuter, cliquez sur le bouton, vous voyez bien le message apparaître.

La règle du jeu est donc toujours la même : on dépose des composants dans la fiche, on initialise ses propriétés dans l'inspecteur d'objets, en général C++Builder offre de bonnes initialisations par défaut, mais vous pouvez les adapter, on programme les événements associés à ces composants.

Notez que dès le départ, C++Builder a créé pour vous le constructeur de la fiche,

__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}

c'est là que vous écrirez les initialisations générales de l'application. Quant aux variables générales de l'application, on les déclare juste en dessous de la déclaration du pointeur Form1. Voici les deux endroits-clés par rapport au source créé par C++Builder au départ.

//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop

#include "Unit01.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
/* Toutes les variables et constantes générales ici */
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
/* profitez de ce constructeur pour initialiser votre application*/
}
//---------------------------------------------------------------------------

Cela dit, en POO pure (voir mon article sur les classesRéflexes pour une définition de la POO), on évite autant que possible les variables générales ou globales, on les déclare de préférence dans la classe principale (située dans unit1.h) dans la zone réservée à l'utilisateur. Dans ces conditions, le pointeur Form1 sera en principe la seule variable générale d'un programme C++ Builder. Voyez mes à ce sujet pour plus d'explications sur les variables globales et sur les fonctions isolées à éviter par principe.

Un autre principe fondamental est la création logicielle de composants c'est-à-dire par programme. On crée les composants par new et on les supprime symétriquement par delete le moment venu. Par exemple nous allons faire la même chose que précédemment, mais par programme. Repartons d'un projet vide, appelons-le unit02 et projet02.

Dans les variables générales (juste après TForm1 *Form1;), déclarons un pointeur sur un bouton.

TButton *B;

Dans le constructeur de TForm1, nous allons créer le bouton par logiciel.

//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
B=new TButton(this);
B->Parent=this;
B->Caption="TEST";
B->Height=20;
B->Width=60;
B->Left=15;
B->Top=15;
B->OnClick=Button1ClickX;
}

On crée le bouton par new, on indique son Parent (ici "this" c'est-à-dire Form1, on peut d'ailleurs remplacer this par Form1, ça marchera tout aussi bien), on donne son Caption (le mot qui sera affiché dessus), ses dimensions et sa position à l'écran et aussi la méthode qui sera exécutée en cas de clic.

Maintenant pour construire cette méthode, on procède comme suit. Comme on n'est pas censé connaître par avance la syntaxe d'appel de la méthode OnClick pour un bouton, on pose un bouton sur Form1 puis on double-clique sur l'événement OnClick dans l'inspecteur d'objets, et ce, pour que C++Builder donne la syntaxe d'appel. Il faut donc :

1. Sélectionner ce bouton dans l'inspecteur d'objets ;
2. Sélectionner l'onglet "événements" ;
3. Repérer l'événement OnClick et double-cliquer sur la case de droite en regard.

C++Builder crée pour vous la méthode dans le source :

void __fastcall TForm1::Button1Click(TObject *Sender)
{

}

On copie-colle cette méthode à la fin du source et on lui ajoute une lettre pour changer son nom, j'ajoute un ici X à la fin donc on a :

void __fastcall TForm1::Button1ClickX(TObject *Sender)
{

}

Maintenant on fait ctrl-F6 (contrôle F6) pour afficher l'en-tête, dans la section published, on lit :

void __fastcall Button1Click(TObject *Sender);

C'est la déclaration de la méthode pour le bouton qu'on a déposé sur la fiche. On copie-colle cette ligne en la mettant dans la section "public" et on lui rajoute le X, donc on a copié :

void __fastcall Button1ClickX(TObject *Sender);

Maintenant, on supprime le bouton de Form1 dont on n'a plus besoin, il nous a seulement servi à créer les syntaxes d'appel, on est sûr de ne pas se tromper puisque c'est C++Builder lui-même qui a créé les syntaxes. Maintenant qu'on a déclaré notre propre méthode, on programme comme précédemment la ligne, on obtient :

void __fastcall TForm1::Button1ClickX(TObject *Sender)
{
Application->MessageBox("Bouton cliqué!", "OK",MB_OK);
}

C'est la même chose que précédemment, on affichera un message quand le bouton sera cliqué. Faites F9 pour compiler-exécuter, vous voyez le bouton "TEST" apparaître et le message s'affiche suite à un clic du bouton.

Comme le bouton a été créé par new, il est bon de le détruire par delete. Sélectionnez Form1 dans l'inspecteur d'objets et double-cliquez sur l'événement "OnDestroy", C++Builder vous crée le gestionnaire suivant :

void __fastcall TForm1::FormDestroy(TObject *Sender)
{

}

Cette méthode est exécutée au moment de la destruction de la fenêtre principale (donc à la fin de l'exécution de l'application), il suffit d'écrire à l'emplacement du curseur (suite à la création d'un gestionnaire d'événement, C++Builder positionne automatiquement le curseur entre les deux parenthèses, car il attend que vous écriviez le code de la méthode), écrivez simplement :

delete B;

pour détruire le bouton.

Pour que vous ayez une vision globale, voici le programme complet. Une grande partie a été écrite automatiquement par C++Builder (en rouge), une partie a été écrite par C++Builder suite à une demande de création de gestionnaire d'événement (en vert) et quelques instructions écrites par nous pour compléter (en bleu).

//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop

#include "Unit07.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
TButton *B;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
B=new TButton(this);
B->Parent=this;
B->Caption="TEST";
B->Height=20;
B->Width=60;
B->Left=15;
B->Top=15;
B->OnClick=Button1ClickX;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button1ClickX(TObject *Sender)
{
Application->MessageBox("Bouton cliqué!", "OK",MB_OK);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
delete B;
}

Voici également l'en-tête :

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

C'est donc un petit peu plus compliqué par programme, mais guère plus. On crée le composant par new, on accède logiciellement à ses propriétés, on déclare des méthodes associées aux événements traités. Pour connaître les événements, utilisez l'aide en ligne ou alors déposez le composant sur la fiche pour l'étudier grâce à l'inspecteur d'objets, voyez les événements accessibles, créez-les pour en avoir la syntaxe en double-cliquant dans la case en regard de l'événement, copiez-collez la méthode vide en ajoutant par exemple une lettre, faites de même dans l'en-tête et mettez la déclaration résultante dans la section "public", puis virez le composant de la fiche. Vous vous êtes ainsi créé une méthode personnalisée, en bleu ci-dessus. Une autre méthode consiste à avoir dans un petit fichier de type Notepad ces syntaxes d'événements (en plus de syntaxes C++ particulières) que vous aurez collectées pendant vos tests dans le répertoire Essai prévu à cet effet, ainsi il vous suffira de faire un copier-coller du Notepad vers l'éditeur de C++Builder.

Notez également que vous pouvez ouvrir n'importe quel autre fichier C++ et procéder ainsi à des copier-coller d'un fichier à un autre. Ouvrir un fichier signifie simplement "ouvrir un fichier" et non l'inclure au projet, ce pourquoi vous pouvez toujours ouvrir n'importe quel fichier étranger au projet pour procéder à des copier-coller. En revanche, si vous faites "Nouveau|Fichier cpp", là C++Builder crée un nouveau cpp et l'inclut au projet.

Quand dans un source cpp vous avez à faire des modifications périlleuses dont vous n'êtes pas sûr, une bonne méthode consiste à travailler sur une copie. Ce fichier cpp étant sélectionné dans l'éditeur, faites alors "Fichier|Enregistrer sous" et donnez-lui un autre nom, par exemple rajoutez "_copie" à la fin de son nom, C++Builder a automatiquement exclu l'ancien fichier du projet et l'a remplacé par ce nouveau qui n'est pour l'instant qu'une copie de l'ancien. Si vos modifications ont fonctionné, vous pouvez toujours continuer ainsi (ou alors vous pouvez faire de nouveau "Fichier|Enregistrer sous" pour lui redonner son nom d'origine), mais si vous voulez revenir en situation antérieure, il suffit de supprimer du projet le fichier devenu fautif avec _copie et d'ajouter au projet l'ancien fichier. Dans les icônes du menu de C++Builder, vous devez apercevoir une icône avec un + et une icône avec un -, ce sont ces fonctions qui vous permettront de réaliser cette suppression et cette addition. Sinon, dans le menu "Projet", vous avez clairement "ajouter au projet" et "retirer du projet". Donc vous retirez du projet le fichier avec _copie et vous ajoutez au projet l'ancien fichier correct, par ces deux opérations vous êtes revenu en situation antérieure.

Quand vous déposez un composant sur une fiche, vous ne vous occuper pas de son "instanciation" c'est-à-dire de sa création en mémoire, C++Builder s'en occupe. En revanche, quand vous instanciez un objet par new, vous le déclarez parent de la fiche principale ou d'un autre objet de la fiche, et vous le supprimez par delete au bon moment. De même pour toute allocation mémoire par malloc, n'oubliez pas de libérer cette zone par free au bon moment.

Notez qu'au moment de la compilation, C++Builder se débarrasse des gestionnaires vides. Par exemple double-cliquez sur un événement quelconque de Form1 pour créer la méthode vide correspondant à cet événement puis faites tout de suite ctrl-F9 (contrôle F9) pour compiler seulement l'unité en cours, vous observez que C++Builder a bien vu que la méthode était vide, donc il l'a enlevée du source. Il en est de même quand vous sauvegardez le projet (icône "disquettes en cascade"), les méthodes vides sont supprimées du source cpp et h. La seule méthode que le progiciel garde toujours, c'est le constructeur de la fiche (TForm1::TForm1) même si la méthode ne contient rien. Le plus généralement, cette méthode contiendra quelque chose puisque c'est là qu'on peut initialiser l'application. On peut l'initialiser aussi à l'événement OnCreate de la fiche principale, mais puisque C++Builder nous offre ce constructeur, on s'en sert logiquement pour l'initialisation de l'application.

Si vous êtes amené à créer une autre fiche à la conception, vous aurez fatalement une autre unité .cpp et un autre en-tête .h. Mais ces deux fichiers ne peuvent pas communiquer pour l'instant. Si vous avez par exemple Form1 et Form2, vous ne pourrez pas accéder à Form1 dans unit2.cpp ni à Form2 dans unit1.cpp. Par exemple dans unit1.cpp vous ne pourrez pas écrire Form2->Visible=false car Form2, et donc ses propriétés et autres, ne sont pas encore accessibles à partir d'unit1. Ainsi imaginons que dans unit1.cpp vous vouliez accéder à Form2, sélectionnez dans l'éditeur unit1.cpp de manière à ce que ce soit le fichier actif (car l'éditeur n'a qu'un fichier de code actif à la fois) puis faites "Fichier|Inclure l'en-tête d'unité", là C++Builder vous propose logiquement d'inclure l'en-tête de Form2 (unit2.h), acceptez, à partir de ce moment, Form2 devient accessible dans unit1.cpp. Réciproquement, si unit2.cpp doit accéder à Form1, sélectionnez unit2.cpp dans l'éditeur et incluez l'en-tête proposé qui est unit1.h, là Form1 sera accessible dans unit2.cpp, car C++Builder a rajouté l'include concerné dans le source. Vous pouvez aussi faire cette opération à la main, ça marchera de la même façon, mais en règle générale, il est préférable de passer par les fonctions du progiciel puisqu'il s'occupe de tout.

//-----------------------------------------------------------------------

#include "vcl.h"
#pragma hdrstop

#include "Unit09.h"
#include "Unit09b.h"
//----------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//----------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------

Dans l'exemple précédent, on suppose que Form1 est accessible par unit09.h et Form2 par unit09b.h. Comme il y a autant d'unités que de fiches et de cadres réunis (car un nouveau cadre crée aussi une unité cpp et h), C++Builder connaît l'ensemble des en-têtes possibles pour le projet en cours et vous propose logiquement au moment de l'exécution de la fonction "Fichier|Inclure l'en-tête d'unité" d'inclure un des en-têtes manquants au source cpp actif de l'éditeur. Ce principe est vrai pour un nombre plus grand de fenêtres et cadres. En revanche, si vous créez une fenêtre par logiciel via l'opérateur new, il n'y a pas de création d'unité cpp ni h, vous n'avez alors pas d'en-tête à inclure pour accéder à la fenêtre, vous y accédez directement au même titre qu'à tout objet instancié par new.

Notons pour clore ce petit survol qu'un grand nombre d'exemples d'applications programmées se trouvent dans le répertoire Program Files\Borland\CBuilder5\Examples. Chaque répertoire contient un ou plusieurs programmes d'un certain type, il suffit de faire "Fichier|Ouvrir un projet", de choisir un projet (repérable par le logo jaune C++) et de faire F9 pour l'exécuter. Cela vous donnera une idée sur la façon dont sont conçues les applications.

-:-

1. À propos de la fonction "Exécuter|jusqu'au curseur"

Votre curseur se trouvant sur une ligne quelconque du source, cliquer Exécuter du menu général de C++ Builder et choisissez l'option "jusqu'au curseur" du menu déroulant. Le programme arrêtera son exécution à ce moment-là, mais il faut préciser que le curseur doit se trouver sur une ligne du source où il y a effectivement une instruction, par exemple juste avant cette instruction, mais ça marche aussi si le curseur se trouve au milieu de cette instruction, l'arrêt se fera précisément à cette instruction avant son exécution. Mais si le curseur se trouve sur une ligne blanche ou sur une ligne où il n'y a qu'une accolade, l'arrêt ne se fait pas. La doc ne précise pas ce point ce qui fait qu'on peut croire à un dysfonctionnement. Le mieux est de regarder sur la gouttière à gauche de la fenêtre, il y a un petit point bleu qui s'allume, cela signifie que cette ligne est un point d'arrêt possible, il faut donc toujours positionner le curseur sur une ligne dotée d'un tel point bleu. Le mieux est d'utiliser le raccourci F4. Vous positionnez le curseur avant ou sur une instruction dans votre code, vous faites F4 pour exécuter le programme jusqu'à cette position. Dès que l'instruction est rencontrée, le logiciel vous affiche la fenêtre de code et l'instruction d'arrêt est surlignée. Là vous pouvez interroger certaines variables via Exécuter|Inspecter ou encore Exécuter|Évaluer/Modifier (Ctrl F7). C'est évidemment très pratique pour interroger des variables parce que vous n'avez qu'à donner leur nom sans vous occuper du type (alors qu'un MessageBox ou un ShowMessage dans le programme à titre d'affichage de debug n'affiche qu'une variable du type Char* pour le premier et AnsiString pour le second).

Notez au passage que pour visualiser des tableaux entiers, il vaut mieux passer par "Exécuter|Inspecter" qui donne plusieurs visualisations différentes pour chaque élément du tableau (mode caractère, équivalent décimal, équivalent hexadécimal) alors que "Exécuter|Évaluer/modifier" est plus adéquat pour des variables à visualiser et éventuellement à modifier avant de reprendre l'exécution du programme. En utilisant "Exécuter|Inspecter", vous donnez d'abord le nom du tableau. Vous ne voyez certes que les premiers éléments, mais vous pouvez agrandir cette zone de visibilité. Appuyez sur le bouton droit de la souris et choisissez "Étendue" (ou faites Ctrl R, ce qui revient au même), là vous pouvez indiquez le nombre d'éléments à voir.

Une autre méthode peut-être même plus rapide consiste à pointer quelques instants le curseur dans le source sur le nom d'une variable, le progiciel vous indiquera le contenu de la variable dans une toute petite fenêtre. Vous avez aussi la possibilité de passer par ce qu'on appelle des "points de suivi", voir alinéa 68 en troisième partie.

2. Points d'arrêt

Même principe pour les points d'arrêt, le curseur doit se trouver sur une ligne où il y a effectivement une instruction sinon l'arrêt ne se fait pas. Vous positionnez le curseur sur l'instruction où se fera le point d'arrêt, vous faites F5, la ligne où se trouve l'instruction est alors surlignée. On supprime ce point d'arrêt en faisant F5 de nouveau sur cette même ligne. Vous pouvez aussi cliquer sur la bande grise à gauche appelée gouttière qui sert de marge à la fenêtre du source C juste en face d'une ligne de code, un point d'arrêt sera de la même façon mémorisé à cet endroit, en cliquant de nouveau à ce niveau, sur le point rouge de repère, le point d'arrêt est supprimé. Ces points d'arrêt sont très pratiques, ils vous permettent de contrôler le contenu de certaines variables, ils se suppriment facilement (on pointe la ligne avec le curseur et on fait F5 ou l'on clique sur le point rouge) et votre code n'a pas été altéré par un ordre quelconque d'affichage des variables. Notez que pour continuer l'exécution du programme à partir du point d'arrêt il faut faire Exécuter|Exécuter (ce qui ne va pas de soi, on aurait pu envisager un ordre dans le menu Exécuter du type "continuer l'exécution", la doc ne parle pas de cette possibilité). Notez que Exécuter|Exécuter revient à cliquer sur la petite flèche verte en haut vers la gauche du menu ou encore à appuyer sur la touche F9.

3. Point d'arrêt pendant l'exécution

Quand vous exécutez le programme pour le tester en cliquant sur la petite flèche verte ou via F9, vous remarquerez dans la ou les fenêtres dédiées au source que de petits points bleus s'affichent sur la colonne grisée de gauche en face de chaque instruction. En cliquant sur un de ces petits points, vous créez un point d'arrêt à l'instruction correspondante et vous pouvez ainsi créer facilement d'autres points d'arrêt en cliquant d'autres petits points bleus. On les supprime en recliquant dessus. C'est très pratique, car cela se fait pendant l'exécution même du programme, ces points bleus disparaissent avec leurs points d'arrêt associés quand le programme a terminé son exécution (auquel cas on revient à C++ Builder pour continuer le développement) ou si vous réinitialisez de vous-même le programme (Exécuter|Réinitialiser le programme) pendant l'exécution ou au moment d'un arrêt. Ce sont donc des points d'arrêt temporaires ou volatiles qui n'existent que le temps d'un test.

4. Ouverture d'un projet.

Si vous avez créé un raccourci de C++ Builder sur le bureau et que vous y accédiez en cliquant dessus, le logiciel vous affiche par défaut un projet vierge. Si vous voulez ouvrir un projet en cours, le mieux est de passer par Fichier|Rouvrir, qui vous propose une liste de projets que vous avez ouverts dans vos précédentes utilisations de C++ Builder. Les projets étant proposés dans l'ordre, le premier qui apparaît est celui sur lequel on a travaillé la dernière fois, on l'ouvre aussi en appuyant sur la touche 0 (zéro). C'est assez pratique quand on travaille longtemps sur un même projet, on l'ouvre ainsi de façon automatique sans se poser de question. Sinon, autre possibilité, vous allez avec l'explorateur Windows dans le répertoire où se trouve votre projet (on le reconnaît par le fait qu'il est doté de la même icône que C++ Builder, il s'agit du .bpr unique pour un projet donné) et vous cliquez dessus, C++ Builder est alors chargé avec ce projet. Sinon passez par "Fichier|Ouvrir un projet», mais dans ce cas C++Builder part toujours du répertoire C:\Program Files\Borland\CBuilder5\Projects\ par défaut.

5. Conversion AnsiString en Char*

Dans un code classique en C++, on a tout intérêt à utiliser des chaînes de caractères du type AnsiString, vous avez ainsi accès à des syntaxes beaucoup plus simples qu'en C. Par exemple on utilise simplement le signe = et on met une chaîne entre quotes pour initialiser une variable, par exemple Var_AnsiStr="Bonjour" au lieu de strcpy(VarChar, "Bonjour") du C classique. Idem pour les concaténations qui se font simplement avec le signe + pour des AnsiString au lieu d'un strcat en C. Cela dit, si vous êtes amené à utiliser des fonctions de C pur, comme C ne connaît pas du tout les chaînes de type AnsiString, vous devez alors convertir un AnsiString en Char*. Imaginez par exemple qu'un OpenDialog vous donne le nom d'un fichier à ouvrir et que ce nom soit dans une variable AnsiString, vous ne pourrez pas ouvrir ce fichier par un fopen du langage C, car la chaîne d'un fopen est du type Char*, donc il faut convertir l'AnsiString en Char*. Si donc NomFic est déclaré en AnsiString, il faudra écrire NomFic.c_str() pour ouvrir le fichier via un fopen. Vous appliquez ainsi la méthode c_str à la variable AnsiString la convertissant de cette manière en Char*. Il faudra donc écrire

fopen(NomFic.c_str(), etc.);

pour que ça marche. Pour plus d'information sur cette question, mettez votre curseur juste avant la chaîne "c_str" et appuyez sur F1, l'aide associée à cette question apparaît. (il en va d'ailleurs de même pour tout autre mot-clé). Idem si vous voulez connaître les statistiques concernant ce fichier (accessibilité, longueur, etc.) on écrira une instruction du genre

stat(NonFic.c_str(), &statbuf) ;

à partir de laquelle vous avez accès à toutes les caractéristiques du fichier Voir "stat" dans le fichier d'aide et les exemples pour plus d'information. Idem pour les fonctions C de traitement de chaîne de caractères, strcpy, strcat, etc. N'oubliez pas les parenthèses ouvrante et fermante quand vous ajoutez c_str() sinon vous avez l'erreur E2034 en compilation. Ne pas oublier que VarAS.c_str() où VarAS est un AnsiString est considéré comme une constante, vous ne pouvez donc l'utiliser qu'en lecture, mais vous ne pouvez pas y mettre une valeur. Par exemple un fputs du C pur avec VarAS.c_str() sera correct, car vous écrivez une chaîne de caractères, mais un fgets sera fautif, car vous ne pouvez pas écrire dans VarAS.c_str() qui encore une fois est une constante. Il faudra donc pour lire un fichier via fgets utiliser un char* puis convertir en AnsiString par une affectation classique, VarAS = VarC avec par exemple char VarC[256].

6. Aide en ligne

Pour connaître la signification d'un paramètre dans l'inspecteur d'objet, il suffit de cliquer sur ce paramètre et de faire F1, l'aide concernant ce paramètre apparaît dans une fenêtre. Idem pour se remémorer la signification d'une instruction C ou C++ dans le source, on positionne le curseur juste avant le mot-clé et on fait F1. Idem pour comprendre une option du menu, vous déployez le menu déroulant, vous mettez en reverse vidéo l'option et vous faites F1, l'aide en ligne de cette fonction apparaîtra.

7. Erreur de compilation

Pour avoir une explication de l'erreur de compilation, appelez l'aide de C++ Builder en cliquant sur le petit livre du menu en haut de l'écran ou via Aide|C++ Builder puis entrez dans la zone de saisie Ennnn où nnnn est le numéro d'erreur annoncée par le compilateur (la compilation est automatique avant une demande d'exécution ou par la construction de l'exécutable via Projet|Construire_projet où "projet" est le nom de votre propre projet qui apparaît maintenant dans le menu déroulant) ou encore Projet|Make. La réponse du compilateur se trouve dans une petite fenêtre au bas de l'écran juste en dessous du code source. En entrant Ennnn (où nnnn est le numéro de l'erreur e.g. E2034 pour l'erreur 2034), l'aide vous donnera des explications sur cette erreur particulière. Ou encore (et c'est peut-être mieux), vous double-cliquez sur la ligne signalant l'erreur au bas de l'écran pour la sélectionner (elle se met donc en reverse vidéo) et vous faites comme d'habitude F1, une fenêtre apparaît pour vous donner des explications sur cette erreur. Notez que si le compilateur vous indique une ligne d'erreur où vous ne trouvez de toute évidence rien d'anormal, c'est la ligne précédente qui est souvent en cause notamment parce que votre instruction ne se termine pas comme il se doit par un point-virgule. Pour aller à une ligne précise (par exemple celle annoncée par le compilateur pour vérifier votre code), faites Alt G et entrez le numéro de la ligne. Normalement, le progiciel surligne la première ligne de code trouvée en erreur, mais si vous voulez y retourner par la suite, il suffit de double-cliquer dessus, c'est assez pratique.

8. Construire et Make

Dans le menu déroulant Projet, vous avez deux options importantes, l'une est Construire et l'autre Make, elles sont presque identiques. Construire recompile tout et crée l'exécutable. Make (Ctrl F9) ne recompile que ce qui a été modifié depuis la dernière fois et construit l'exécutable. Cette seconde option est donc préférable, vous ne recompilez que les fichiers modifiés, ce qui est plus rapide que de tout recompiler à chaque fois. Notez qu'il n'y a pas de touche raccourci pour Construire alors qu'il y en a une pour Make (construire ne s'utilise qu'à titre de vérification, cette option vous permet d'être sûr que tout le projet se compile normalement notamment quand vous avez modifié des options de compilation ou des directives). Quant à l'option Compiler l'unité, elle ne compile comme son nom l'indique que la fenêtre active, elle vous permet de vérifier qu'il n'y a pas d'erreur de syntaxe dans votre programme.

9. Appel d'une fonction à l'intérieur du code d'un événement

Quand vous programmez le code correspondant à un événement, par exemple

void __fastcall TForm1::Ouvrir1Click(TObject *Sender) ;

code qui s'exécute quand vous sélectionnez la fonction "ouvrir" d'un menu déroulant, vous avez accès directement aux objets de l'objet principal, en l'occurrence ici TForm1. Si cette fiche contient par exemple un mémo que vous voulez rendre visible à ce moment-là, vous écrirez par exemple simplement

memo1->show();

dans le code de cet événement, le programme comprendra qu'il s'agit de la fiche mémo1 de Form1. Mais si cet événement fait appel à une fonction que vous écrivez, vous ne pourrez plus écrire

memo1->show();

le compilateur dira qu'il ne connaît pas mémo1. Il faut alors dans ce cas préciser qu'il s'agit de la fiche memo1 de Form1, il faut donc écrire à l'intérieur de la fonction appelée

Form1-> memo1->show();

Et ça marchera.

10. Arrêt d'exécution

Si, suite à une erreur de programmation votre exécutable ne vous rend pas la main durant son exécution, vous avez néanmoins toujours accès au menu de C++ Builder qui reste actif malgré ce problème, il suffit donc de faire Exécuter|Réinitialiser le programme ou encore Ctrl F2 qui fait la même chose. Vous avez ainsi repris la main sur votre développement.

11. Extraction à l'intérieur d'un

AnsiString d'un ou plusieurs caractères

Si vous voulez extraire un seul caractère d'un AnsiString, il suffit d'écrire sa position entre crochets sachant que le premier caractère à le numéro 1. Par exemple CarUnique = Chaine[3] fera que la variable CarUnique contiendra le 3e caractère de Chaine, CarUnique et Chaine étant tous deux des AnsiString. Mais si vous voulez extraire une suite de caractères, il faut utiliser la méthode SubString(P,L) où P est la position de départ et L la longueur à partir de cette position. Par exemple SousChaine=Chaine.Substring(3,2) extraira 2 caractères à partir du troisième caractère de la chaîne de caractères. Notons que SousChaine=Chaine[3]+Chaine[4] qui serait censé faire la même chose ne marche pas, le compilateur ne signale pas d'erreur, mais le résultat est faux.

Quand vous saisissez le point dans une syntaxe du type Chaine.SubString (le point annonce la méthode), il suffit d'attendre une seconde ou deux, le progiciel vous propose d'office toutes les méthodes possibles dans le contexte. Si cela ne marche pas, appuyez sur le bouton droit de la souris dans une fenêtre de code C et choisissez tout en bas du menu déroulant l'option propriétés, là choisissez l'onglet "audit de code" et cochez la case "achèvement du code", c'est ce flag qui indique qu'on propose au développeur la liste des méthodes disponibles dans le contexte, cliquez sur aide et vous connaîtrez le détail de ces propriétés.

12. La fonction IntToHex

Cette fonction permet de convertir un integer en hexadécimal (base 16) sur un nombre de digits donné. On écrit par exemple VarAS = IntToHex(i,2) signifie que la chaîne AnsiString VarAS contiendra après exécution l'équivalent décimal du nombre entier i sur deux digits, i pouvant aussi être du type char (un char peut toujours être considéré comme un entier). Mais il faut préciser que i est considéré comme étant codifié en complément à 2 (ce qu'oublie de préciser la doc) et donc peut être négatif (bit le plus significatif au 1 logique). Si donc ce nombre entier est négatif, IntToHex l'écrit d'office sur huit chiffres hexadécimaux (i.e. quatre octets). Cela peut provoquer des erreurs. Imaginez un buffer de caractères par exemple char buffer[100] déclarant ainsi une chaîne de 100 caractères c'est-à-dire de 100 octets. Vous voulez afficher en hexadécimal chacune de ces cases mémoire. Chacune à un contenu allant en décimal de -128 à +127 c'est-à-dire en hexadécimal allant de 0x00 à 0XFF. Les valeurs positives peuvent être extraites normalement sur deux chiffres hexadécimaux. Mais, toutes les valeurs négatives c'est-à-dire celles dont le bit le plus significatif est au 1 logique seront transcrites sur 4 octets. Donc le mieux sera de tout extraire sur 4 octets et ensuite d'utiliser un SubString pour extraire de ce résultat les deux derniers caractères c'est-à-dire 2 caractères à partir de la 7e position. On écrira donc pour traiter la ième case du buffer HexaDec = IntToHex(Buffer[i],8).SubString (7,2) où HexaDec est un AnsiString qui contiendra après exécution les deux digits hexadécimaux. Vous voyez qu'on applique directement la méthode SubString qui extraie 2 caractères à partir de la 7e position de l'AnsiString donné par IntToHex sur 8 chiffres. La logique eût voulu qu'on puisse écrire dans ce cas HexaDec = IntToHex(Buffer[i],2), mais malheureusement ça sera faux pour toutes les valeurs négatives qui donneront non pas deux chiffres comme demandé mais huit. (Bogue ?)

13. Longueur de vos buffers

Attention à ne pas dépasser la longueur de vos buffers. Si vous écrivez au-delà du buffer, vous risquez d'écraser certaines données qui se trouvent par hasard juste après ce buffer mais l'exécution du programme peut très bien continuer encore un peu si ces variables ne sont pas vitales à ce moment-là de l'exécution. Cela dit, tôt ou tard le programme va planter et c'est un des cas où vous avez droit au message "l'application va s'arrêter, car elle a exécuté une opération non conforme". Notez que même dans ce cas, C++ Builder garde toujours la main si vous testez sous cet environnement, vous appuyez sur OK et vous retournez à votre source que vous pouvez maintenant vérifier. Si votre programme fonctionnait au moment du dernier test, mettez un point d'arrêt juste avant les lignes rajoutées ou modifiées et faites du pas à pas (une fois arrêté, faites Maj F7 pour exécuter jusqu'à la ligne suivante et continuez ainsi le débogage).

14. Explorateur de classes

À gauche des fenêtres source, vous avez un explorateur de classes que vous pouvez déployer en cliquant sur le petit +. Cela vous donne la structure de votre programme, en cliquant sur une fonction, le curseur se positionne au début de celle-ci dans le source. C'est donc très pratique pour s'y retrouver. Si vous supprimez cet explorateur en cliquant le petit x sur le bord haut de la fenêtre, ce qui vous donne une fenêtre plus grande au moment d'écrire le source, vous pouvez toujours le récupérer via Voir|Explorateur de classes. Pour que les fonctions soient accessibles à partir de cette liste, il faut déclarer leur prototype en début de listing, ce qui d'ailleurs est obligatoire si l'appel est antérieur dans le listing à la fonction elle-même (sinon le compilateur ne connaît pas le prototype de la fonction). Soit par exemple la fonction Essai qui revoie un AnsiString et qui a pour argument un integer et un AnsiString, vous déclarerez au début son prototype à savoir dans ce cas AnsiString Essai(int, AnsiString); le compilateur connaît alors le type du résultat et le type des arguments. La fonction étant dûment déclarée, elle sera accessible à partir de l'explorateur de classes (elle s'écrit d'ailleurs dans l'explorateur en même temps que vous l'écrivez dans le source). Sinon, si vous ne déclarez pas ainsi la fonction (ce qui est possible à condition que vous écriviez la fonction dans le listing avant que vous l'utilisiez par un appel), elle apparaîtra dans l'explorateur de classes, mais en cliquant dessus, il ne se passera rien. À vous donc de savoir si vous voulez pouvoir accéder rapidement au code d'une fonction ou non. Pour plus d'informations sur les classes, voyez mon Étude autour des classes du C++.

15. Initialisations

Si votre programme doit procéder à des initialisations, créez-les au moment de la construction de la fiche principale dont la fonction (automatiquement créée par le logiciel) est facilement reconnaissable par sa syntaxe

__fastcall TForm1::TForm1(TComponent* Owner): TForm(Owner);

Cette fonction apparaît d'ailleurs dans l'explorateur de classes. Cliquez dessus et le progiciel positionnera le curseur au début du code de cette fonction qui n'est simplement que déclarée mais qui ne contient rien pour l'instant.

16. Déclaration des variables

Vous voulez savoir où une variable ou constante a été déclarée, pointez le curseur sur son nom et appuyez sur Ctrl (la touche contrôle). La variable devient une sorte de lien hypertexte, en cliquant sur elle, le curseur pointe exactement la ligne du source où elle a été déclarée. En faisant Ctrl Z (contrôle Z), vous revenez où vous en étiez dans le source. Pratique non?

17. Fenêtre d'exécution

Si, après avoir cliqué dans le source un point d'arrêt pour déboguer une partie de programme juste après avoir lancé un test par F9 qui exécute l'application en cours de développement, vous avez perdu la fenêtre de l'application en train de s'exécuter (la fenêtre où se trouve le source étant maintenant active puisque vous l'avez sollicitée pour y positionner un point d'arrêt), vous la retrouver facilement en appuyant sur Alt Tab (touche Alt puis Tab). Là, Windows vous propose d'exécuter une autre fenêtre dont il vous indique le nom. Si ce n'est pas celle-là, appuyez de nouveau sur la touche tabulation (Alt étant toujours appuyée) jusqu'à ce que vous tombiez sur le nom de votre application. Cela vous évite d'avoir à modifier l'emplacement des fenêtres qui cache le bas de l'écran où tous les programmes en cours sont affichés dans de petits rectangles. Si toutefois ces petits rectangles sont visibles, vous cliquez alors directement votre application qui est venue s'ajouter au nombre des programmes en cours.

18. Grand fichier cpp

Pour une fiche donnée, le progiciel vous fournit un premier fichier point cpp (C++). Si ce fichier devient trop grand, le mieux est de diviser en deux ce grand fichier d'origine et donc de créer un autre fichier cpp qui contiendra des routines que vous ne toucherez plus ou pratiquement plus, fichier qu'il faut compiler à part. La marche à suivre est la suivante.

1. Faites Fichier|Nouveau et choisissez comme objet un fichier cpp parmi les choix possibles ;

2. Par défaut, ce nouveau fichier s'appelle file1.cpp, le mieux est de faire maintenant Fichier|Enregistrer sous et de lui donner le nom que vous voulez. Comme vous allez essayer de regrouper des routines ayant une signification globale pertinente, donnez-lui un nom en conséquence. Vous venez donc d'enregistrer un nouveau fichier vide cpp ;

3. Observez que dans le cpp portant le nom du projet, C++ a automatiquement rajouté la directive USEUNIT avec le nom de ce nouveau cpp source. D'ailleurs si vous faites maintenant Voir|Gestionnaire de projets, vous voyez que ce nouveau fichier fait maintenant partie du projet ;

4. Recopiez tout l'en-tête du fichier cpp d'origine (il s'agit du fichier créé par C++ Builder au moment de la création de la fiche associée). Il faut recopier la première section contenant tous les fichiers d'include (ou alors si vous voulez peaufiner, ne sélectionnez que les includes dont vous avez besoin dans ce nouveau fichier). De toute façon, il y aura des erreurs à la compilation s'il en manque ;

5. Coupez toutes les routines concernées par ce transfert et collez-les dans ce nouveau fichier. Normalement, leur prototype a été déclaré au début du fichier d'origine, donc si ce fichier d'origine fait appel à ces routines qui sont maintenant dans l'autre fichier, il n'y aura pas de problème de compilation ;

6. Faites maintenant Projet|Compiler l'unité. Si vous appelez des routines situées dans l'autre fichier, le compilateur vous le signalera, il suffira alors de déclarer le prototype de ces fonctions. Par exemple vous appelez la routine "essai" de l'autre fichier (celui d'origine à scinder en deux) qui renvoie un entier et qui a deux arguments, un integer long et un pointeur de caractères, vous déclarerez ce prototype ainsi int essai(long int, char*);

7. Ayant compilé cette unité à part sans erreur de compilation, le progiciel vous a créé le fichier point obj correspondant dont a besoin le lieur au moment de la construction de l'exécutable ;

8. Faites maintenant Projet|Construire (c'est-à-dire créez l'exécutable complet du projet), vous voyez que ça marche parfaitement. Vous avez tout à y gagner, car d'une part cela vous évite d'avoir à manipuler des fichiers trop grands et d'autre part, la compilation est plus rapide puisque le nouveau fichier est déjà compilé ;

9. Remarque. Les fonctions situées dans ce nouveau fichier ne seront accessibles par l'explorateur de classes qu'après sa première compilation. Cela signifie que si vous cliquez sur une des fonctions visibles dans l'explorateur de classes et appartenant à ce nouveau source C++ pour y positionner le curseur, cela ne fonctionnera qu'après une compilation de ce fichier (si le fichier n'est pas compilé, il ne se passe rien). Notez également que quand vous chargez un développement, C++ Builder ne charge que le fichier source de base i.e. celui qu'il a lui-même créé au moment de la création du projet. Mais si vous cliquez dans l'explorateur de classe sur une fonction qui est dans un autre fichier (celui que vous avez créé pour diviser le source en deux), C++ Builder chargera alors automatiquement ce fichier. C'est évidemment très pratique, vous n'avez pas besoin de savoir dans quel fichier source se trouve une fonction (si votre projet est complexe, il peut bien sûr y en avoir plusieurs et ça deviendrait vite compliqué), vous l'appelez via l'explorateur de classes qui se charge d'ouvrir le bon source, mais encore une fois, cela ne fonctionne qu'après une première compilation de ce nouveau source. Il faut signaler que dans certains cas, l'explorateur de classes ne trouve pas la fonction bien qu'elle soit dûment déclarée, il semble qu'il y ait un bogue à ce niveau ;

10. Autre remarque. Pour tester certaines syntaxes C que vous maîtrisez mal, vous pouvez toujours vous créer une sorte de brouillon.cpp isolé que vous pouvez compiler à part juste pour vérifier une syntaxe. De toute façon, il est bon d'avoir un répertoire de travail qui contient une copie temporaire de votre développement pour des tests spécifiques sans risque de toucher votre original. Une fois votre algorithme éprouvé, vous allez dans votre répertoire officiel que vous mettez à jour de ces nouveautés par un copier-coller judicieux.

19. Gestionnaire de projet

Après avoir chargé votre projet (si vous travaillez toujours sur le même, utilisez Fichier|Rouvrir et une fois le menu déroulant actif, appuyez sur la touche 0), vous pouvez visualiser tous les sources liés à ce projet en faisant Voir|Gestionnaire de projet. C'est très pratique notamment quand vous avez différentes variantes pour savoir où vous en êtes. En effet, quand vous utilisez la fonction Enregistrer sous, c'est ce nouveau fichier qui devient actif, l'ancien ne fait plus partie du projet, mais le fichier existe toujours. Si vous voulez revenir à l'ancienne version, il faut alors utiliser les fonctions Projet|retirer du projet (là vous supprimez le source dont vous ne voulez plus) puis Projet|Ajouter au projet (là vous remettez en service l'ancien source). Si vous ne savez plus où vous en êtes suite à plusieurs de ces changements, utilisez la fonction Voir|Gestionnaire de projet qui vous indiquera clairement les fichiers impliqués à ce moment-là.

20. Conseil personnel

Pour aller vite, la meilleure façon est d'aller très lentement, de penser profondément les choses, de les structurer, de les maîtriser. Cioran disait "toute forme de hâte trahit quelque dérangement mental". Festina lente, hâte-toi lentement disaient les anciens. L'extrême lenteur est une sorte de vitesse absolue, vous vous en rendrez compte en en tentant l'expérience dans ce monde de suante précipitation. Ayez aussi cet adage en tête : suus cuique diei sufficit labor (à chaque jour suffit sa peine) c'est-à-dire n'essayez pas de tout faire en une seule fois. L'important est simplement d'avancer dans votre projet, non pas avancer vite, mais simplement avancer. À ce sujet, il est très bon d'utiliser la fonction "ajouter un élément A faire" via Maj Ctrl T (vous appuyez simultanément sur Majuscule, Contrôle et la lettre T) ou encore, en cliquant sur le bouton droit de la souris, un menu déroulant apparaît où cette fonction est disponible. Le principe est simple. Vous positionnez votre curseur à un endroit du listing où vous avez quelque chose à faire et à ne pas oublier. Vous appelez cette fonction et vous écrivez un petit mémo en style télégraphique rappelant brièvement ce qu'il y a à faire. Le texte ainsi saisi est écrit en commentaire dans votre source, mais de plus son endroit dans le source est mémorisé par le progiciel. Ensuite, revenant à votre projet le lendemain, vous cliquez Voir|Liste à faire, toutes les occurrences de ce genre apparaissent, il suffit de cliquer sur l'une d'entre elles pour y accéder, C++ Builder charge le source cpp si ce n'est déjà fait et vous présente l'endroit du source où il y a ce commentaire et où il faut rajouter quelque chose. Vous pouvez aussi assigner une priorité, une catégorie (c'est une chaîne de caractères qui contient un code à vous), etc. Donc avant de fermer votre projet après une journée de travail, notez par un texte du type "À faire" l'idée que vous aviez, c'est très utile. Pour supprimer un élément de cette liste, vous avez le choix entre le supprimer dans le listing ou de visionner cette liste, de sélectionner l'occurrence à supprimer et d'appuyer sur la touche Suppr du clavier.

21. Remarques et exemple autour de malloc (memory allocation)

Je fais une légère déviation vers le C pur, car les syntaxes de malloc sont assez difficiles et l'aide en ligne ne donne qu'un tout petit exemple.

Quand vous devez déclarer un tableau dont vous ignorez la longueur (la longueur va dépendre d'une donnée que vous ignorez au moment du développement soit par exemple une saisie de l'utilisateur au clavier soit suite au chargement d'un fichier, etc.), il est préférable d'utiliser la fonction C malloc (memory allocation). Mais il faut se souvenir que malloc non seulement réserve l'emplacement mémoire demandé et d'autre part initialise un pointeur (c'est toujours un pointeur donc vous devez avoir le signe *) qui pointe le premier élément de cette structure. Par exemple vous ouvrez le fichier N (N étant un AnsiString qui contient le chemin d'accès et le nom du fichier, MES également qui contiendra un message d'erreur le cas échéant, Fichier est du type FILE*)

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;
}

Notez que le fichier a été ouvert ici en mode r+b, cela signifie d'une part en lecture écriture (r pour read et + pour possibilité d'ajout) et d'autre part en binaire, cela signifie qu'on considère que c'est une suite d'octets. Voyez la documentation pour plus d'information sur la manière d'ouvrir les fichiers (vous mettez votre curseur devant fopen et vous faites F1, c'est l'aide en ligne).

Ensuite vous interrogez les stats pour connaître la longueur de ce fichier N, par exemple :

stat(N.c_str(), &statbuf);
LongFic = statbuf.st_size;

Cette syntaxe suppose d'une part que vous ayez déclaré au début du source l'include sys\stat.h ainsi : #include <sys\stat.h> et d'autre part que vous ayez déclaré statbuf (nom de variable arbitraire que je choisis) qui résulte de la structure officielle des statistiques de fichiers, on le déclare ainsi : struct stat statbuf;

Dans ces conditions stat (N.c_str(), &statbuf) signifie que vous demandez de mettre à jour dans la structure statbuf les renseignements concernant le fichier N. Ces statistiques étant maintenant en mémoire, on peut les lire notamment la longueur du fichier (mais bien d'autres choses, voyez l'aide en ligne). On a donc écrit LongFic = statbuf.st_size, LongFic est donc maintenant égal à la longueur du fichier en nombre d'octets que l'on a ouvert. Si maintenant on veut lire ce fichier en mémoire, il faut réserver une place de LongFic octets que l'on ne peut pas connaître à l'avance. Donc on utilise malloc qui va allouer LongFic octets. Il est bon de tester LongFic qui ne doit pas être nul sinon cela signifie que le fichier est vide :

if(!LongFic)
{
MES="Le fichier "+N+" est complètement vide, zéro octet.";
Application->MessageBox(MES.c_str(),NULL,MB_OK);
return;
}

Remarquez que if(!a) équivaut à if(a==0), de même if(a) équivaut à if(a!=0). Un moyen mnémotechnique consiste à se souvenir que "non nul" c'est "quelque chose" et que "nul" c'est "rien". Ainsi while(a) signifiera "tant que a vaut quelque chose" et while(!a) "tant que a ne vaut pas quelque chose" et donc "tant que a est nul".

On suppose avoir préalablement déclaré un pointeur de caractères au début du programme (ou au début de la fonction) par exemple char* PtrFic; PtrFic est donc un pointeur qui pointe un élément ou une suite d'éléments du type char.

Il faut donc maintenant réserver une zone mémoire de LongFic octets à partir de ce pointeur. On utilise malloc et l'on écrit :

// allocation de cette mémoire à partir de PtrFic
if ((PtrFic = (char *) malloc(LongFic)) == NULL)
{
MES="Le fichier "+N+" est trop grand. Je n'arrive pas à "
"allouer la mémoire correspondante. "
"Sa longueur est de "+IntToStr(LongFic)+" octets. "
"Essayez de libérer un peu de mémoire et recommencez";
Application->MessageBox(MES.c_str(),NULL,MB_OK);
return;
}

Notez que si LongFic est nul, cela revient à demander 0 octet via malloc qui renvoie logiquement NULL. C'est pourquoi nous avons écarté ce cas précédemment, à ce stade du programme on sait que LongFic est non nul.

Remarquez aussi la façon de concaténer les chaînes de caractères. On écrit entre quotes une première chaîne puis une deuxième à la ligne suivante et ainsi de suite. Cela évite d'avoir des lignes trop grandes. Le compilateur comprend qu'il s'agit d'une seule et même chaîne.

La fonction C malloc renvoie NULL si la demande d'allocation mémoire a échoué. Cela n'arrive pratiquement jamais avec nos ordinateurs d'aujourd'hui dotés de 64 voire 128 MO, mais c'est toujours possible si l'utilisateur a ouvert trop d'applications en même temps. De toute façon il faut toujours tester un code retour pour une bonne qualité de programme, c'est ce qu'on appelle la programmation paranoïaque (defensive programmation), c'est le seul cas où il est excellent d'être parano. Faites attention au nombre et à la place des parenthèses. Si l'allocation a réussi, PtrFic pointe le premier octet d'une zone mémoire de LongFic octets. Dans ces conditions on peut lire le fichier en une seule fois ainsi :

if((NbData=fread(PtrFic,1,LongFic,Fichier))!=LongFic)
{
MES="Impossible de lire le fichier "+N+
" dans sa totalité bien que l'allocation "
"mémoire vient d'être acceptée, "
"le fichier a une longueur de "+IntToStr(LongFic)+
», mais je n'ai pu en lire que "+IntToStr(NbData)+".";
Application->MessageBox(MES.c_str(),NULL,MB_OK);
fclose(Fichier);
return;
}

où NbData est un unsigned long int. Comme la fonction fread renvoie le nombre d'octets effectivement lus, il suffit de comparer le nombre d'octets à lire (ici LongFic) au nombre d'octets effectivement lus (ici NbData) pour savoir si la lecture s'est bien passée. Voyez l'aide en ligne pour le détail de fread. Vous accédez à chacun de ces octets en mettant entre crochets son numéro d'ordre qui ira de 0 à LongFic - 1, par exemple PtrFic[i] est le ième octet pointé par PtrFic, i étant un unsigned long int (codé sur 4 octets donc sur 32 bits).

Pour libérer cette mémoire allouée, il faudra écrire free(PtrFic);

22. Autres exemples de malloc

Il faut se souvenir que malloc renvoie toujours un pointeur qui pointe le premier élément d'une série d'éléments d'un certain type. Par exemple, vous voulez allouer à partir d'un pointeur d'unsigned long int Longueur (nom arbitraire de ce pointeur) une zone mémoire de NbULI unsigned long int, vous écrirez :

if ((Longueur = (unsigned long int*)
malloc((NbULI)*sizeof(long int))) == NULL) return;

Dans cet exemple si la mémoire n'a pu être allouée, le pointeur est égal à NULL et il y aura ici retour de la fonction sinon on continue. Notez que cette écriture suppose que vous ayez déclaré au début (ou quelque part avant l'appel à malloc) la variable Longueur comme pointeur sur un unsigned long int ainsi : >

unsigned long int* Longueur;

Notez que malloc peut être utilisé au moment de la déclaration d'une variable, par exemple :

char* P=(char*)malloc(1000);

Le compilateur C de C++ Builder exige que l'on reprécise clairement au moment du malloc le type char* même dans ce cas bien qu'il y ait redondance. La réservation mémoire est tellement dérisoire que le code retour du malloc n'est pas testé dans ce cas. Cette instruction réserve une zone de mille octets à partir de l'adresse P et est équivalente à char P[1000] si ce n'est que cette dernière notation ne permettra pas par la suite la possibilité d'une réallocation via realloc.

23. Exemple de malloc pointant des structures

Supposons qu'on déclare la structure suivante :

struct StructureEssai

struct StructureEssai
{
unsigned char, CAR;
int N1,N2;
} STE;

Cette structure contient trois éléments, un caractère CAR et deux entiers N1 et N2. StructureEssai est le nom de ce type de structure et STE une variable de ce type. Vous n'êtes pas tenu de déclarer ces deux éléments. Si vous ne voulez qu'une seule variable de ce genre, la structure n'a alors pas besoin de nom et il vous suffit d'écrire :

struct
{
unsigned char, CAR;
int N1,N2;
} STE;

Si vous n'avez pas besoin de variable, mais seulement de la déclaration de la structure, vous écrirez :

struct StructureEssai
{
unsigned char, CAR;
int N1,N2;
};

auquel cas la structure n'existe qu'à titre de prototype.

Si vous déclarez la variable STE (nom arbitraire, structure essai), vous accédez au caractère de cette structure STE par la syntaxe STE.CAR et aux entiers via STE.N1 et STE.N2. Remarquez le point qui sert à désigner un des éléments de la structure.

On veut maintenant déclarer via malloc une zone mémoire d'une longueur de NBS fois cette structure (NBS étant un nombre arbitraire non connu à l'avance, NBS est le nom arbitraire de cette variable pour nombre de structures) avec un pointeur qui de pointera la première structure de la série. Dans ce cas, la variable STE ne vous sert à rien, vous n'avez besoin que du prototype de la structure.

On déclare le pointeur ainsi :

StructureEssai* PointeurStruc;

PointeurStruc est donc un pointeur qui pointe un élément de type StructureEssai.

On réserve via malloc la mémoire ainsi :

if ((PointeurStruc = (StructureEssai*)
malloc((NBS)*sizeof(StructureEssai))) == NULL) return;

D'une part vous allouez NBS fois une structure de longueur StructureEssai et d'autre part PointeurStruc pointe le premier élément de cette structure.

Soit i un entier non signé (unsigned int ou unsigned long int) compris entre 0 et NBS-1,

PointeurStruc[i].Car sera le caractère de la ième structure,

PointeurStruc[i].N1 sera le premier int de la ième structure,

PointeurStruc[i].N2 sera le deuxième int de la ième structure,

avec toujours ce fameux point pour accéder à un élément de la ième structure.

Notez que si vous déclarez la variable STE comme c'est possible (revoir plus haut dans ce même alinéa les différentes possibilités de déclaration), vous pouvez alors lire en une seule fois la ième structure ainsi : STE = PointeurStruc[i]; et dans ce cas vous accédez au caractère de la structure STE qui vient d'être lue par la syntaxe STE.CAR et aux entiers via STE.N1 et STE.N2 comme nous le disions déjà plus haut.

24. Malloc dans un cas de tableaux de pointeurs de séries de structures

Si vous déclarez un tableau de pointeurs vers ces structures par exemple StructureEssai* PointeurStruc[50]; qui déclare un tableau de 50 pointeurs vers une structure de type StructureEssai, vous déclarerez la kème allocation mémoire via malloc par :

if ((PointeurStruc[k] = (StructureEssai*)
malloc((NBS)*sizeof(StructureEssai))) == NULL) return;

k étant supposé compris entre 0 et 49 puisque dans notre exemple nous déclarons 50 pointeurs vers une structure (les autres variables ont la même signification que précédemment).

Dans ce cas, pour accéder au caractère de la ième structure de la kème série de structures il faudra écrire : PointeurStruc[k][i].CAR; où il faut bien sûr remarquer les deux doubles crochets, le premier invoque le kème pointeur, il pointe donc la kème série de structures de ce type et le second la ième structure de cette série numéro k, la bonne structure étant maintenant pointée, on complète par le point et le nom de l'élément à invoquer ici, CAR.

25. À propos de la réallocation mémoire (realloc)

Quand une zone mémoire allouée par malloc s'avère insuffisante dans le contexte, on réalloue cette mémoire via l'instruction standard C realloc, mais il ne faut pas oublier que realloc renvoie la nouvelle position du pointeur qu'il faut assigner au pointeur courant. Par exemple vous avez besoin d'une zone mémoire de bloc octets, bloc étant une constante arbitraire du programme initialisée ainsi : const int bloc = 1000; Vous avez déclaré un pointeur P sur une chaîne de caractères ainsi char* P; vous initialisez la longueur de la zone LZ à bloc (LZ = bloc; LZ étant un unsigned int) et vous déclarez une zone de LZ octets via malloc pointée par P :

P = (char*) malloc (LZ);

À ce stade du programme P pointe le premier octet d'une zone de LZ octets. Un offset o initialisé à 0 naviguera dans cette mémoire et y écrira via une syntaxe du type P[o] = aliquid; (le oème octet pointé par P prend la valeur aliquid, comme c'est un octet, aliquid ira de 0 à 255 ou de -128 à +127 si vous êtes en signé). Mais quand o, suite à une incrémentation sera égal à LZ, il sera hors zone (la zone ne va que de 0 à LZ-1), il faudra donc réallouer la mémoire pour l'agrandir et y ajouter par exemple un bloc à cette zone, on écrira donc :

LZ+=bloc;
P= (char*) realloc(P,LZ);

ou encore en une seule ligne :

P=(char*) realloc(P,LZ+=bloc);

Notez bien cette syntaxe du realloc qui a deux arguments, pointeur et longueur et qui renvoie la nouvelle position du pointeur après réallocation. La longueur de la zone LZ toujours pointée par P est rallongée de bloc octets (première instruction) et P pointe maintenant cette nouvelle zone mémoire plus longue de bloc octets. Si P==NULL après ce realloc, la réallocation a échoué, cela devrait ne jamais se produire, mais s'il faut toujours tester un code retour par sécurité (programmation paranoïaque). Il se peut que P n'ait pas bougé de position s'il se trouve possible à ce moment-là du point de vue du système de rallonger la zone sans ce changement, mais il se peut tout aussi bien que P ait complètement changé de position dans la mémoire. Cela ne vous regarde en rien, le programme continue comme si de rien n'était, toutes vos données sont bien sûr conservées, mais la nouvelle zone est maintenant plus grande. On peut faire autant de realloc que l'on veut. Dans tous les cas, ne pas oublier de libérer la mémoire après utilisation via free(P);

La nouvelle réservation peut aussi être plus petite que l'ancienne (utilisation probablement rare, mais possible), dans ce cas, seule la section commençante des données de l'ancienne zone est disponible dans la nouvelle.

La fonction realloc est très utile quand on ne sait pas à l'avance combien de mémoire sera nécessaire. On procède alors par blocs. Un premier malloc crée une première réservation. On surveille de près l'offset d'accès en lecture-écriture, dès que cet offset est hors zone, on agrandit alors l'allocation via realloc en testant le code retour (si le pointeur est égal à NULL, il y a échec de l'allocation et donc probablement arrêt pur et simple du programme qui ne peut pas continuer). Si vous y allez par incrémentation, l'offset est hors zone dès qu'il est exactement égal à la longueur de la zone, si o est cet offset et LZ la longueur réservée à un moment donné, o ne pourra aller que de 0 à LZ-1, dès que o = LZ il est en dehors de la zone, a fortiori si o > LZ, et c'est à ce moment-là qu'une réallocation peut s'avérer nécessaire dans le contexte.

26. À propos des unsigned

Quand vous ne voulez pas considérer le complément à deux d'une valeur binaire, il faut faire la précision unsigned au moment de sa déclaration. Par exemple un élément du type char est un octet. Par défaut il sera considéré comme signé (si toutefois vous considérez ce char comme un entier ce qui est toujours possible) si vous ne faites aucune précision c'est-à-dire qu'on ira de -128 à +127. Si vous ne voulez pas de ce signe il faut le préciser par le mot-clé unsigned, par exemple :

unsigned char C;

C est donc un unsigned de type char (octet unique), on ira donc de 0 à 255 puisque le signe est ignoré.

Il en va de même pour les int (sur 2 octets) et les long int (sur 4 octets). Par exemple, dans un alinéa précédent, vous écrivions PointeurStruc[i],Car pour accéder au caractère de la ième structure, il est évident que i est un unsigned, il faut donc le déclarer ainsi :

unsigned int i;

Ainsi on ira sur 16 bits de 0 à 65 535 (sinon, si cette précision n'avait pas été faite, le nombre aurait été considéré comme codifié en complément à 2 et l'on irait de -32 768 à +32 767). Si votre zone mémoire contient un nombre de structures inférieur ou égal à 32 767, ça marcherait quand même puisque dans ce cas le nombre serait toujours positif même en considérant le complément à deux mais à partir de 32 768 et en l'absence de la précision unsigned, le programme considérerait que i est négatif, vous pointeriez donc n'importe où dans la mémoire. Le mieux est donc de toujours préciser unsigned et même, pour les zones mémoire dont vous ignorez la longueur, mais qui peuvent être assez grande, d'utiliser un unsigned long int, le pointeur d'éléments est alors codifié sur 4 octets donc sur 32 bits, vous êtes ainsi sûr de pointer toujours correctement la mémoire sans surprise par une possibilité de nombres négatifs.

27. À propos du signe * (étoile)

Ce signe s'utilise pour exprimer un pointeur qui pointe un élément d'un certain type, par exemple

char * P;

Cette notation signifie que P est un pointeur sur un élément de type caractère, on peut aussi considérer que P pointe le premier caractère d'une chaîne de caractères ou encore le premier octet d'une série d'octets, c'est le contexte du programme qui le dira. De façon assez curieuse dans cette notation, l'étoile "appartient" plus au pointeur P qu'au type char, c'est une convention arbitraire même si cela peut paraître inattendu. Cela se voit quand vous avez à déclarer deux pointeurs P1 et P2, il faut écrire char *P1, *P2; où l'étoile a dû être répétée, car la notation char *P1, P2; déclarerait non pas deux pointeurs, mais un pointeur P1 et un caractère P2. Avec la première déclaration, vous pourriez écrire dans le cours du programme P1 = P2 ce qui signifie que le pointeur P1 prend la position du pointeur P2, mais cette possibilité serait interdite avec la deuxième déclaration, car alors le compilateur vous dira qu'il ne peut convertir un char* en char.

En revanche, quand vous déclarez le prototype d'une fonction, le type char* se met à exister. Imaginons une fonction ESSAI qui renvoie un entier et qui a pour argument un pointeur sur octets ou sur caractères. Le prototype de cette fonction se déclarera ainsi int ESSAI(char*); où l'étoile "appartient" cette fois-ci au type char et non à une variable qui d'ailleurs n'est pas mentionnée. L'étoile appartient donc à la variable au moment de la déclaration d'un pointeur, mais au type au moment de la déclaration d'un prototype.

Notez également la quasi-équivalence qu'il y a entre les notations * et [ ], étoile et crochets. L'exemple précédent pourrait tout aussi bien être déclaré par int ESSAI(char[ ]); il en est de même au moment de l'appel de la fonction, vous pouvez aussi bien écrire nombre_entier = ESSAI(char* P) que nombre_entier = ESSAI(char P[ ]); ces deux écritures sont équivalentes du point de vue du compilateur et du fonctionnement du programme. En revanche le compilateur refusera les crochets au moment de la déclaration de la variable, la notation par étoile est alors nécessaire par exemple

char *P;

28. Polices de caractères du source C++

Si vous voulez changer de police de caractères pour l'affichage de votre code, cliquez sur le bouton droit de la souris dans une fenêtre de code C puis choisissez l'option propriétés tout en bas du menu déroulant. Là, sélectionnez l'onglet affichage. C'est dans ce menu que vous décidez de votre police et de sa taille pour l'affichage du source. Il semble que la police proposée par défaut (courrier New taille 9) soit excellente pour du code C, les lignes sont ainsi assez longues, les italiques des commentaires entre les signes /* et */ (ou après le signe // pour un commentaire monoligne) sont très lisibles et les caractères gras des mots-clés bien prononcés, mais sans excès.

29. Recherche dans le source

La recherche d'une chaîne de caractères dans le source se fait via Ctrl F (contrôle F, Find). Après avoir saisi le mot recherché et validé par OK, le curseur se positionne sur la première occurrence trouvée et la fenêtre appelée par Ctrl F disparaît. Pour trouver l'occurrence suivante, faites F3 (et F3 à nouveau pour la suivante, etc.). Si vous recherchez un mot qui se trouve à l'écran, positionnez le curseur dessus (ou juste avant) puis faites Ctrl F, le mot visé est déjà écrit dans la zone de recherche du formulaire, ce qui vous en épargne la saisie.

30. Les pointeurs de pointeurs

On utilise deux fois le signe * (étoile) pour déclarer un pointeur de pointeurs. Imaginons que vous vouliez réserver NZM zones mémoire à chaque fois de longueur variable parce leur longueur dépend du contexte informatique (e.g. chargement de fichiers) ou humain (e.g. entrée de données au clavier). NZM n'est pas connu à l'avance, il faut donc, une fois ce nombre connu réserver une zone qui contiendra NZM pointeurs. En premier lieu on déclare un pointeur de pointeurs PP ainsi :

if((PP=(char**)malloc(NZM*sizeof(char*)))==NULL)
{
Application->MessageBox("Erreur d'allocation mémoire",NULL,MB_OK);
return;
}
if((PP[i]=(char*)malloc(bloc))==NULL)
Application->MessageBox("Erreur d'allocation mémoire",NULL,MB_OK) ;

À ce stade, vous disposez que NZM pointeurs, PP[0], PP[1], etc. jusqu'à PP[NZM-1].

Pour chacun de ces pointeurs PP[i], vous allez devoir réserver une zone mémoire de bloc octets (bloc étant soit une constante définie par vous soit un nombre positif quelconque même assez grand). La fonction test ESSAI ci-dessous procède à ces déclarations et remplit arbitrairement la mémoire allouée puis fait une réallocation. Ce type de remplissage n'a d'autre but que de montrer que la zone mémoire est bien accessible.

void ESSAI(void)
{
const int NZM=500,bloc=200000;
char** PP;
int i,j;

// Petit message avant test
Application->MessageBox("AVANT test","TEST",MB_OK);

// réservation pour NZM pointeurs
if((PP=(char**)malloc(NZM*sizeof(char*)))==NULL)
{
Application->MessageBox("Erreur d'allocation mémoire",NULL,MB_OK);
return;
}

// Réservation pour les NZM pointeurs d'une zone de bloc octets
for(i=0;i!=NZM;i++)
if((PP[i]=(char*)malloc(bloc))==NULL)
{
Application->MessageBox("Erreur d'allocation mémoire",NULL,MB_OK) ;
return;
}

// remplissage arbitraire des NZM zones de bloc octets
for(i=0;i!=NZM;i++)for(j=0;j!=bloc;j++) PP[i][j]='a';

// Réallocation de toutes les zones du double de la première réservation (bloc*2)
for(i=0;i!=NZM;i++)
if((PP[i]=(char*)realloc(PP[i],bloc*2))==NULL)
{
Application->MessageBox("Erreur d'allocation mémoire",NULL,MB_OK) ;
return;
}

// remplissage arbitraire des NZM zones maintenant toutes doublées
for(i=0;i!=NZM;i++)for(j=0;j!=bloc*2;j++) PP[i][j]='a';

// Libération des NZM zones mémoire
for(i=0;i!=NZM;i++) free(PP[i]);
// et de la zone des NZM pointeurs
free(PP);

/*Petit message une fois le test terminé qui prouve que tout est correct
sinon il y aurait un retour suite à une allocation fautive */
Application->MessageBox("Tout est correct","TEST",MB_OK);
}// Fin de cette petite fonction de TEST MÉMOIRE.

Si l'augmentation d'une zone mémoire via la fonction realloc se fait à l'intérieur d'une fonction, il faut alors déclarer le pointeur de la zone mémoire en référence grâce à l'opérateur & et ce, parce que realloc peut modifier la position du pointeur. Il faut donc que le programme appelant récupère cette nouvelle valeur en cas de changement. C'est un des avantages du C++ à savoir l'utilisation de ce signe qui, à lui seul, exprime la référence. En C pur, la référence était plus difficile à exprimer. Si par exemple on voulait qu'un entier i soit du type référence dans une fonction C, il fallait prendre trois précautions :

1. Déclarer que l'argument de la fonction est int* (pointeur sur un entier) ;

2. Appeler la fonction en utilisant &i comme argument ;

3. Affecter (*i) dans la fonction elle-même.

C++ a apporté une grande simplification à cette problématique puisque vous n'aurez qu'à déclarer non plus i, mais &i, l'opérateur & à lui seul exprime la référence. Notez qu'il y a donc en C++ un dual relativement à la référence puisque l'ancienne syntaxe est tout à fait possible (C++ intégrant la totalité des notations C).

Imaginons un programme qui déclare une zone mémoire de bloc octets, bloc étant une constante arbitraire, et qui va écrire très longtemps dans cette zone mémoire et l'allonger dès que nécessaire. On initialise un entier LZ (longueur de la zone) à bloc qui est bien la première longueur de cette zone. On écrit dans cette zone mémoire réservée en la pointant par incrémentation d'un offset. Dès que cet offset est égal à LZ, il pointe le premier octet hors zone, on décide alors d'augmenter la zone de bloc octets par la fonction booléenne ALLOC qui renvoie « true » si l'allocation a réussi, « false » en cas d'échec et qui a deux arguments, le pointeur envoyé par référence avec l'opérateur & et la nouvelle longueur de réallocation.

// prototype de la fonction ALLOC
bool ALLOC(char*&,long int);

void ESSAI(void)
{
const int bloc=1000;
char *Pointeur =(char*)malloc(bloc);
long int LZ=bloc;
int i=0,OK;
Application->MessageBox("AVANT test","TEST",MB_OK);
do
{
/* si i pointe le premier octet hors zone
on augmente cette zone de bloc octets*/
if(i==LZ)
{
// LZ augmente de bloc octets
if(!ALLOC(Pointeur,LZ+=bloc))
{
Application->MessageBox("Erreur d'allocation ","TEST",MB_OK);
// fin de la boucle si erreur
ok=0;
}
}
// i pointe bien dans la zone avant son incrémentation, remplissage arbitraire de la case pointée
Pointeur[i++]='a';

/*fin de la boucle si i est arbitrairement grand
cette instruction signifie, si i n'est pas égal à 500000 alors
ok=1 (non nul = vrai, true) sinon ok=0 (nul = faux, false). */
OK=(i!=500000);
}
while(OK) ;

// Libération de la mémoire allouée
free(Pointeur);
Application->MessageBox("APRES test","TEST",MB_OK);
}
//--------------------------------------
bool ALLOC(char *&P,long int L)
{
return (P=(char*) realloc(P,L))!=NULL;
}
//---------------------------------------

Remarquez bien sûr la concision de la syntaxe. P pointe la nouvelle adresse de la réallocation et est en même temps comparé à !=NULL. Si c'est vrai (true), le pointeur ne pointe pas NULL après le realloc ce qui signifie que l'allocation a réussi, sinon, si P pointe NULL, l'allocation a échoué. On peut donc considérer que la fonction booléenne ALLOC répond à la question : la réallocation mémoire a-t-elle réussi ? true oui, false non.

Si cette précaution n'avait pas été prise à savoir celle de déclarer le pointeur par référence avec la syntaxe char *&P, la nouvelle position en cas de changement de position mémoire du realloc ne serait pas transmise au programme appelant, par conséquent ça ne pourrait jamais marcher, car le pointeur pointerait n'importe où après un changement de position de cette zone. Ça ne marcherait qu'aussi longtemps que la réallocation se situerait par hasard au même endroit. Faites-en l'expérience en supprimant le signe & aussi bien dans le prototype que dans la fonction. Durant l'exécution, vous aurez très vite le message « une erreur d'application s'est produite, violation d'accès, etc. » et ce, parce que la zone mémoire allouée a changé d'adresse à un moment donné alors que Pointeur pointe toujours l'ancienne adresse, laquelle n'est plus d'aucune utilité. L'instruction d'écriture en mémoire Pointeur[i++]='a' est alors fautive, car on pointe n'importe où.

Dans cet exemple, la variable Pointeur du programme appelant est transmise à P de la fonction par référence c'est-à-dire par l'adresse mémoire de ce pointeur. Comme ce contenu de la mémoire est modifié par le realloc, ce nouveau contenu est comme retransmis à la variable Pointeur du programme appelant.

Il est toujours très important de savoir si une variable, argument d'une fonction, doit être retournée ou non à l'appelant. Sinon, on n'écrit que le nom de la variable, si oui, on préfixe le nom de cette variable par l'opérateur &. Dans le premier cas, la fonction travaille avec une copie de la variable, son contenu à l'issue de l'exécution n'est pas retransmis à l'appelant. Dans le second cas, on donne à la fonction non pas la variable, mais son adresse mémoire (c'est la signification de l'opérateur &). Si donc la fonction écrit dans cette mémoire, ce contenu sera logiquement répercuté par la variable de l'appelant qui pointe précisément cette mémoire.

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 clic 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éons 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 votre 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 clic. 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 clic 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é au nombre entier 1. Si la proprié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, consultez 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 où 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 première 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 rempli 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 quel 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 quel 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 pointeurs 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&#8230; 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 la 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 le 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 une 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 pour quoi 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électionnez 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 le 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. Le 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 trouvent 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

TInt
Array *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éments 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ébogage|CPU (ou Ctrl Alt C), cliquez dans le rectangle en bas à gauche avec le bouton droit et choisissez "Aller à l'adresse" (1re 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 vidéo)
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
une 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 défaut, ce pour quoi 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 du 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.

46. La classe TEdit

L'objet TEdit vous permet entre autres de dialoguer avec l'utilisateur. Par exemple, vous pouvez l'inviter par ce petit objet d'entrez une chaîne de caractères. Dans ce cas on intercepte la chaîne saisie après que l'utilisateur a appuyé sur la touche Entrée (code ASCII 13). Par exemple, sélectionnez l'objet Edit, il se trouve dans l'onglet standard de la palette de composants (petit logo montrant les deux lettres "ab"). Positionnez-le sur la fiche principale Form1. Dans l'inspecteur d'objets, sélectionnez "événements" et cliquez sur "OnKeyPress", C++ Builder vous crée la méthode correspondante. Saisissez la ligne suivante :

void __fastcall TForm1::Edit1KeyPress(TObject *Sender, char &Key)
{
if(Key==13) Application->MessageBox(Edit1->Text.c_str(),"OK",MB_OK);
}

Cette instruction signifie que quand l'utilisateur a appuyé sur "Entrée", on affiche le contenu de l'objet Edit dans un MessageBox. Vous remarquez qu'on entre dans cette méthode avec en main la variable Key qui est précisément le code ASCII de la touche appuyée par l'utilisateur. Tant que cette touche n'est pas Entrée (code ASCII 13), on ne fait rien, on sort immédiatement de la méthode. Notez que si vous voulez connaître les codes ASCII des touches du clavier, il vous suffit de les afficher, par exemple :

void __fastcall TForm1::Edit1KeyPress(TObject *Sender, char &Key)
{
Application->MessageBox(IntToStr(Key).c_str(),"OK",MB_OK);
}

On affiche ici le code ASCII de la touche appuyée dans un MessageBox à chaque fois qu'une touche est appuyée.

Remarquez le flag MB_OK qu'on utilise souvent à titre d'exemple dans nos MessageBox. C'est le plus courant, mais il y en a bien d'autres : MB_ABORTRETRYIGNORE, MB_OKCANCEL, MB_RETRYCANCEL, MB_YESNO, MB_YESNOCANCEL, etc. Positionnez le curseur sur le mot MessageBox et faites F1 (aide en ligne) pour plus d'informations.

Il est très souvent utile de n'accepter dans un TEdit qu'une certaine catégorie de caractères. Par exemple, si le TEdit représente un nombre entier, il faudra n'accepter que les chiffres (codes ASCII compris entre 48 et 57). Pour une meilleure utilisation, on accepte aussi toujours la touche "back space" (code ASCII \b) qui permet d'effacer le dernier caractère. Donc, dans le gestionnaire d'événement OnKeyPress, on annulera le caractère envoyé si ce n'est ni un chiffre ni la touche d'effacement (touche qui se trouve au-dessus de la touche Entrée et qui représente une flèche vers la gauche).

void __fastcall TForm1::Edit1KeyPress(TObject *Sender, char &Key)
{
if((Key<48||Key>57)&&Key!='\b') Key=NULL;
}

Vous voyez que la variable Key est envoyée comme référence (grâce à l'opérateur & qui exprime que la variable Key est envoyée non pas en tant que valeur, mais en tant qu'adresse, cela revient à dire que son contenu est comme renvoyé au programme appelant dans le cas où Key serait modifiée à l'intérieur de la méthode). Cela permet à l'utilisateur de contredire l'événement puisque si Key (c'est-à-dire la touche appuyée) n'est ni un chiffre ni la touche d'effacement alors Key est annulée (Key=NULL;), ce qui a pour effet d'ignorer complètement la touche appuyée par l'utilisateur. Ainsi, si l'utilisateur appuie sur une lettre, celle-ci ne sera pas affichée. On s'assure ainsi qu'il n'y aura que des chiffres dans le TEdit. Notez la propriété MaxLength qui permet d'exprimer le nombre de caractères maximum qui sera accepté dans le TEdit. Notez aussi que pour une valeur entière, on évitera d'afficher un zéro non significatif au début du TEdit. Ceci se programmera à l'événement OnChange.

void __fastcall TForm1::Edit1Change(TObject *Sender)
{
int n;
if((n=Edit1->Text.Length())!=0)
if(Edit1->Text[1]==48)
Edit1->Text=Edit1->Text.SubString(2,n-1);
}

On teste d'abord la longueur du Tedit et on en profite pour mettre cette longueur dans l'entier n. Si cet entier est non nul, on teste le premier caractère du Tedit c'est-à-dire le premier caractère de l'AnsiString Edit1->Text (n'oubliez pas que l'indice des AnsiString commence à 1). Si donc ce premier caractère est égal à 48 c'est-à-dire le code ASCII du caractère zéro (if(Edit1->Text[1]==48)), on supprime ce caractère en chargeant dans Edit1->Text son propre contenu à partir du deuxième caractère sur une longueur de n-1, ce qui a pour effet de supprimer le premier caractère si celui-ci est zéro.

47. La classe TList

C'est une liste de pointeurs. On la déclare par l'opérateur new :

TList* TL = new TList;

TL étant ainsi une instance de type TList, si P est un pointeur de type char*, on ajoutera le pointeur P à la liste en écrivant :

TL->Add((char*)P);

Les pointeurs de TList étant du type void* c'est-à-dire finalement que ce sont des pointeurs indéterminés, il faut donc convertir ce pointeur en char* dans notre cas ce pourquoi il faut écrire char* entre parenthèses pour effectuer une conversion de void* en char*. Si vous voulez déclarez une zone mémoire via malloc et ajouter un pointeur vers cette zone, on écrira par exemple :

TL->Add((char*)malloc(bloc)); // où bloc est une constante arbitraire

Le ième pointeur de la liste est donné par l'expression TL->Items[i], si ce pointeur est un pointeur de type char*, le jème caractère de ce pointeur sera donné par l'expression ((char*)TL->Items[i])[j] où l'on écrit clairement que TL->Items[i] est du type char*. Pour supprimer le ième pointeur de la liste, on écrira simplement ;

TL->Delete(i);

Voici un petit exemple complet. On déclare la constante bloc à 1000 pour un premier malloc. On déclare NbBlocs à 40. On déclare ensuite TL du type Tlist et on déclare 10 zones mémoire via malloc de bloc/2 octets et à chaque fois on ajoute le pointeur à la liste. On a donc une liste de 10 pointeurs chacun pointant une zone de bloc/2 octets. Ensuite, on va réallouer la mémoire pour chacun de ces pointeurs, mais on va le faire en ajoutant à chaque fois un pointeur à la liste. On a donc après cette boucle 20 pointeurs, les pointeurs de 0 à 9 contiennent maintenant les anciennes adresses devenues inutiles et les pointeurs de 10 à 19 les nouvelles suite au realloc. On supprime alors les 10 premières adresses en supprimant dix fois le pointeur d'indice 0. Au bout de 10 fois, on n'a plus que les pointeurs d'indice 0 à 9 qui sont les pointeurs nouveaux suite au realloc. On fait cette réallocation NbBlocs fois en allouant à chaque fois un bloc de plus c'est-à-dire (j+1)*bloc, et ce pour éprouver les realloc et vérifier le bon fonctionnement de tous les pointeurs. Au bout du compte, on a 10 pointeurs sur 10 zones de NbBlocs*bloc octets. On remplit toutes ces zones, le fait de pouvoir y accéder est la preuve que c'est parfaitement correct. Ensuite on libère la mémoire de ces dix pointeurs et on efface l'objet TL. Copiez-collez ce code entre les parenthèses {} du tout premier constructeur TForm1 en entrant dans C++Builder et faites F9 pour exécuter, vous constatez que ça marche parfaitement, car si l'accès n'était pas autorisé, on aurait eu nécessairement un accès violation, ce qui n'est pas le cas ici.

const bloc =1000,NbBlocs=40;
int i,j;
TList* TL = new TList;
for(i=0;i<10;i++) TL->Add((char*)malloc(bloc/2));
for(j=0;j<NbBlocs;j++)
{
/* On réalloue les dix pointeurs avec une zone de (j+1)*bloc octets
comme j va de 0 à NbBlocs-1, la dernière allocation allouera
NbBlocs*bloc octets */
for(i=0;i<10;i++) TL->Add((char*)realloc(TL->Items[i],(j+1)*bloc));
// On supprime dix fois le pointeur d'indice 0
for(i=0;i<10;i++) TL->Delete(0);
}

/* À ce stade on a 10 pointeurs de NbBlocs*bloc octets, on remplit
arbitrairement toute la mémoire allouée à titre de vérification d'accès */
for(i=0;i<10;i++) for(j=0;j<NbBlocs*bloc;j++) ((char*)TL->Items[i])[j]=0;

// On libère la mémoire pour chacun des pointeurs
for(i=0;i<10;i++) free(TL->Items[i]);

// On détruit l'objet TL
delete TL;

48. Conditions sur points d'arrêt

On donne un point d'arrêt via la touche F5, la ligne pointée par le curseur devient rouge, le programme s'arrêtera à cet endroit au moment de l'exécution et vous pourrez vous lancer dans un pas à pas (notamment via la touche F8, voir menu "Exécuter" pour plus d'informations). Maintenant, en cliquant sur le bouton droit de la souris pointant une ligne marquée "point d'arrêt" (ligne rouge), vous obtenez un menu déroulant qui vous donne la possibilité d'affiner ce point d'arrêt, c'est l'option "Propriétés du point d'arrêt". En choisissant cette option, une fenêtre apparaît et vous indiquez une condition d'arrêt. Si cette condition est réalisée, l'exécution s'arrêtera et vous aurez la main pour poursuivre en pas à pas. Par exemple, si vous voulez arrêter l'exécution si une certaine variable D=2, vous écrirez dans le formulaire présenté : D==2. Il faut juste se souvenir que s'il y a plusieurs conditions, elles doivent être isolées par des parenthèses par exemple (D1==2)&(D2==3).

49. Série de touches fréquentes

Pour accéder à vos fichiers réguliers, utilisez les touches du clavier. Ainsi, si vous travaillez toujours sur le même projet (ce qui est le cas de projets de longue haleine), vous l'ouvrez par Alt+F+R+0 (Alt active le menu général, F pour fichier, R pour rouvrir et 0 pour le dernier projet ouvert). Si vous avez plusieurs fichiers cpp à ouvrir, faites Alt+V+G, là cliquez "+" et choisissez le cpp voulu (Alt pour activer le menu, V pour Voir, G pour gestionnaire). Ou encore Alt+V+U qui appelle les unités, c'est d'ailleurs peut-être un meilleur choix puisqu'on peut par ce moyen charger plusieurs fichiers en même temps en indiquant un premier fichier puis un second tout en appuyant sur la touche MAJ ce qui provoque la sélection de toute la zone.

50. Les menus variables

Pour créer des menus variables, vous avez grosso modo deux possibilités : soit vous prédéclarez vos options à l'avance et à la main à la conception de votre application, soit vous créez ces options par programme. Si vous connaissez à l'avance le maximum d'emplacements à allouer durant l'exécution, la première solution est alors préférable. C'est le cas par exemple quand vous proposez à l'utilisateur les plus récents fichiers à rouvrir, vous les rajoutez dans le menu "Fichier" qui est en général la première option du menu principal d'une application, vous décidez par exemple de proposer les dix fichiers les plus récents.

Dans ce cas, vous prédéclarez ces options dans votre menu en sélectionnant dans votre fiche Form1 le menu principal, vous cliquez sur le bouton droit de la souris, vous choisissez "concepteur de menus". Là le concepteur de menu s'affiche et sélectionne pour vous la première option "Fichier", celle précisément où l'on va rajouter des options. Cliquez la case la plus en bas pour la sélectionner, rajoutez un séparateur (dans Caption saisissez le signe moins, c'est le code pour créer une fausse option qui servira de ligne séparatrice dans votre menu) puis sélectionnez à nouveau la dernière case et saisissez n'importe quoi dans Caption par exemple la lettre a puis recommencez l'opération par exemple dix fois pour réserver dix options en proposant à Caption les lettres de a à j. En réalité, ce qu'on entre dans Caption n'a aucune importance puisque cela sera renseigné par programme, c'est simplement une astuce pour réserver l'espace mémoire associé à ces options. On a donc un menu "Fichier" avec les options classiques puis à un moment donné une ligne séparatrice suivie de dix options qui contiendront jusqu'à dix noms de fichiers les plus récemment utilisés. Ici je conseille un petit truc, c'est d'initialiser le tag de la première option à 1, celle que nous avons déclarée avec Caption=lettre a. Les tags sont initialisés à 0 à chaque création d'option de menu, il suffit de mettre à 1 celui-là. Cela va nous servir dans le programme à repérer la première option destinée à recevoir des noms de fichiers récents. Si on ne prend pas cette petite précaution, il faudrait alors mémoriser l'indice de la première option variable (sachant qu'on part de 0 c'est-à-dire que Fichier1->Items[0] est la première option du menu Fichier que C++Builder a nommé Fichier1). Mais de ce fait, il faudrait modifier le source si par la suite vous insérez une option au menu Fichier avant ces noms, car tous les indices se décalent d'une unité. En utilisant le tag de la première option des noms de fichiers récents et en l'initialisant à 1, on ne changera rien même si vous insérez des options dans votre menu. Cherchons dans ces conditions le numéro d'options correspondant à la première option variable :

ok=true;
Prem=-1;
while(OK) if(Fichier1->Items[++Prem]->Tag==1) OK=false;

où OK est un booléen et Prem un entier. Après exécution de ce code, Prem contient l'indice de la première option variable du menu repéré par le fait que son tag est à 1, Fichier1->Items[Prem] est donc cette première option.

Ensuite, à l'initialisation, il faut rendre invisibles toutes les options et donc inaccessibles d'où :

for(i=Prem;i<Prem+MaxFic;i++) Fichier1->Items[i]-> Visible=false;

où MaxFic est le nombre maximum de fichiers autorisés à venir s'afficher dans le menu variable. Prem étant le premier indice, Prem+MaxFic-1 sera le dernier, pour tous ces indices le champ Visible sera mis à false, ce qui désactive l'espace entier réservé à ces noms de fichiers.

Imaginons maintenant que dans une TStringList, vous avez les paramètres utilisateurs et donc notamment les plus récents fichiers utilisés, vous allez alors renseigner le menu variable en fonction de cette TStringList que j'appelle ici AllParam (tous les paramètres de votre application). Je suppose ici que toute ligne de la TStringList commençant par CodeFic="Fic " indique pour les caractères suivants le nom du fichier à inscrire au menu.

j=Prem;
i=0;
OK=true;
while(OK)
{
LigMen=AllParam->Strings[i++];
if(LigMen.SubString(1,4)==CodeFic)
{
LG=strlen(LigMen.c_str());
LigMen=IntToStr(i-1)+" "+LigMen.SubString(5,LG);
Fichier1->Items[j]-> Visible=true;
Fichier1->Items[j]-> Caption=LigMen;
Fichier1->Items[j++]-> OnClick=HistoClick;
}
if(j==MaxFic+Prem|i==(unsigned int)AllParam->Count) ok=false;
}

L'entier i pointe ici la TStringList et j les options du menu, j est donc initialisé à Prem. Pour chaque ligne de la TStingList pointée par i, je lis cette ligne dans un AnsiString LigMen (LigMen=AllParam->Strings[i++]). Si les 4 premiers caractères sont CodeFic, cela signifie (dans la codification arbitraire que j'ai proposée) qu'elle contient bien un nom de fichier à insérer au menu variable. Ici (petite astuce), j'extrais ce nom, mais je rajoute au début de la chaîne son numéro d'ordre à partir de 0, ce qui fait qu'on pourra accéder à cette option en utilisant les dix chiffres du clavier de 0 à 9, car C++Builder cherche automatique un caractère de raccourci dans la propriété Caption, ici il sera tout trouvé, ce sera le chiffre ajouté en début de chaîne. Pour ce faire je calcule d'abord le nombre de caractères de la chaîne (LG=strlen(LigMen.c_str())) puis je concatène son numéro et le nom du fichier commençant à partir du 5e caractère (LigMen=IntToStr(i-1)+" "+LigMen.SubString(5,LG)), LigMen (ligne de menu) contient maintenant ce qu'il faudra afficher dans le menu variable. Caption prend donc la valeur LigMen et l'option sera visible. Ensuite il faut indiquer au programme l'adresse de la fonction au moment où l'utilisateur choisira cette option. Ici pour toutes les options du menu, on va à la même adresse à savoir à HistoClick (Fichier1->Items[j++]-> OnClick=HistoClick). HistoClick (nom arbitraire que je choisis) est donc une fonction membre de la classe TForm1 créée par C++Builder, il ne faudra donc pas oublier de déclarer cette fonction dans le "point h" associé.

void __fastcall HistoClick(TObject *Sender);

Sinon, si vous ne faites pas cette déclaration, le compilateur vous dira [C++ Erreur] MonProj.cpp(922): E2316 '_fastcall TForm1::HistoClick(TObject *)' n'est pas un membre de 'TForm1', [C++ Erreur] MonProj.cpp(971): E2451 Symbole 'HistoClick' non défini.

Dans notre exemple, les MaxFic options rajoutées vont aller au même endroit en cas de sélection par l'utilisateur. En effet, il s'agit en fait de la même fonction à savoir traiter le fichier sélectionné. Pour savoir quelle option a été sélectionnée, on utilise la syntaxe TMenuItem *ClickedItem = dynamic_cast<TMenuItem *>(Sender);

À partir de là, ClickedItem est l'option choisie, j'en extrais de Caption le nom du fichier invoqué en lisant d'abord cette option (Choix=ClickedItem->Caption), en calculant sa longueur (LG=strlen(Choix.c_str())), en extrayant à partir de 4e caractère, car bien sûr le chiffre qu'on a nous-même ajouté à Caption n'appartient pas au nom du fichier (Choix=Choix.SubString(4,LG)). Ici je suppose qu'on va traiter le fichier par une fonction que j'appelle TraiteFic, laquelle renvoie un numéro d'erreur en cas d'erreur (sinon 0 si pas d'erreur). Je suppose que tous les messages d'erreur sont dans une TStringList que j'appelle ici MErr, j'affiche donc le message d'erreur correspondant dans ce cas. Voici donc comment pourrait se présenter la fonction :

void __fastcall TForm1::HistoClick(TObject *Sender)
{
int LG,erreur;
AnsiString Choix;
TMenuItem *ClickedItem = dynamic_cast<TMenuItem *>(Sender);

Choix=ClickedItem->Caption;
LG=strlen(Choix.c_str());
Choix=Choix.SubString(4,LG);

// Traitement du fichier
if((erreur=TraiteFic(Choix))!=0)
Application->MessageBox(MErr->Strings[erreur].c_str(),NULL,MB_ICONSTOP);
}

Notez que comme TraiteFic est appelé à modifier le menu, il sera obligatoire pour y avoir accès par programme de le déclarer comme fonction membre de TForm1 sinon vous n'aurez pas accès aux éléments. En effet, si l'utilisateur sélectionne un mauvais fichier, TraiteFic ne fera rien, mais s'il en choisit un bon, ce qui sera le cas le plus fréquent, ce fichier passera en première position dans le menu variable étant le plus récemment utilisé, il faudra donc bien avoir accès au menu. Voici comment cela pourrait se passer. MemorFic reçoit le nom du fichier à mémoriser (i.e. le fichier sélectionné par l'utilisateur).

/* Mémorisation du nom du fichier dans les paramètres */
void MemorFic(AnsiString N)
{
int i=0;
bool OK=true;
AnsiString Fic=CodeFic+N;

/* Recherche et suppression s'il existe de ce nom de fichier dans la liste */
while(OK)
{
if(i==AllParam->Count) OK=false;
else
{
if(Fic==AllParam->Strings[i++])
{
/* Si trouvé on le supprime et on l'insère en première position */
AllParam->Delete(--i);
AllParam->Insert(0,Fic);
return;
}
}
}//fin du while

/* Ici on n'a pas trouvé ce nom, il est donc nouveau
Si le nb de codes "Fic " (CodeFic) est inférieur maxi on l'insère
sinon on vire celui dont l'indice est le plus élevé */
EtudeMaxFic();

/* On peut maintenant insérer ce nouveau nom, on est sûr ici
de ne jamais dépasser le maximum autorisé */
AllParam->Insert(0,Fic);
}
//---------------------------------
/* On compte le nombre de noms de fichiers mémorisés. S'il n'est pas
égal au maximum, on sort sans rien faire, car on peut en ajouter un
nouveau. Sinon, on vire celui dont l'indice est le plus élevé, ce qui nous
permettra d'en insérer un plus récent */
void EtudeMaxFic(void)
{
int i,N=0;

/* On compte dans N le nombre de noms de fichiers */
for(i=0;i<AllParam->Count;i++)
if(AllParam->Strings[i].SubString(1,4)==CodeFic) N++;

/* Rien si ce n'est pas le max */
if(N!=MaxFic) return;

/* sinon on vire celui dont l'indice est le plus élevé */
i=AllParam->Count;
while(true)
{
if(AllParam->Strings[--i].SubString(1,4)==CodeFic)
{
AllParam->Delete(i);
return;
}
}
}

À ce stade nous avons une TStringList qui contient le nouvel ordre des fichiers, il suffit de les faire apparaître dans le menu variable ainsi que cela a été expliqué au début de cet alinéa.

Si maintenant vous décidez de programmer les options nouvelles par programme (et non de les prédéclarer), il faut utiliser d'autres syntaxes. Par exemple, rajoutons une option au moment de la création de la fenêtre principale :

void __fastcall TForm1::FormCreate(TObject *Sender)
{
TMenuItem *Item1 = NewItem("Essai", TextToShortCut("Ctrl+N"), false, true, Action, 0, "Item1");
Fichier1->Add(Item1);
}

Ici on a ajouté l'option Essai et elle a été associée la méthode Action à déclarer comme membre de TForm1.

void __fastcall TForm1::Action(TObject *Sender)
{
// Code associé à l'option "Essai" du menu.
}

Autre syntaxe, on déclare un pointeur sur un TmenuItem (TMenuItem* NewItem), on déclare une instance via l'opérateur new (NewItem=new TMenuItem(Fichier1)), à partir de là on accède à tous les champs de cet item nouveau via NewItem-> notamment pour Caption (nom qui apparaîtra dans le menu) et pour OnClick (action à exécuter en cas de sélection). Déclarons trois options par programme dans le menu Fichier1 associées respectivement à Action1, Action2 et Action3.

__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner)
{
TMenuItem* NewItem;

// on déclare en plus une ligne séparatrice dans le menu
NewItem=new TMenuItem(Fichier1);
NewItem->Caption="-";
Fichier1->Add(NewItem);

// Essai1 associé à Action1
NewItem=new TMenuItem(Fichier1);
NewItem->Caption="Essai1";
NewItem->OnClick=Action1;
Fichier1->Add(NewItem);

// Essai2 associé à Action2
NewItem=new TMenuItem(Fichier1);
NewItem->Caption="Essai2";
NewItem->OnClick=Action2;
Fichier1->Add(NewItem);

// Essai3 associé à Action3
NewItem=new TMenuItem(Fichier1);
NewItem->Caption="Essai3";
NewItem->OnClick=Action3;
Fichier1->Add(NewItem);
}

Les trois actions doivent être membre de TForm1 et donc déclarées dans le "point h" associé.

void __fastcall Action1(TObject *Sender);
void __fastcall Action2(TObject *Sender);
void __fastcall Action3(TObject *Sender);

Vous n'avez plus qu'à écrire le code correspondant à ces actions.

void __fastcall TForm1::Action1(TObject *Sender)
{
// Code associé à l'option "Essai1" du menu.
}

void __fastcall TForm1::Action2(TObject *Sender)
{
// Code associé à l'option "Essai2" du menu.
}

void __fastcall TForm1::Action3(TObject *Sender)
{
// Code associé à l'option "Essai3" du menu.
}

À noter cet avertissement de la documentation : "L'élément renvoyé par NewItem n'a pas de propriétaire. Vous êtes responsable de la libération de sa mémoire lorsqu'elle n'est plus nécessaire. Les méthodes Delete et Remove de TMenuItem ne libèrent pas la mémoire."

C'est donc à vous de libérer la mémoire en temps voulu et aussi (pour faire bien les choses) au sortir de l'application. Par exemple, avant d'ajouter une occurrence au menu, mémorisez son indice :

NumOp=Fichier1->Count;

Dans l'exemple précédent, cette ligne est à insérer juste après avoir inséré la ligne séparatrice. Continuons l'exemple précédent et libérons la mémoire correspondant aux trois options rajoutées, on écrira :

for(i=0;i<3;i++) delete Fichier1->Items[NumOp];

Observez ce fait amusant qu'on efface toujours l'option d'indice NumOp. En effet, après avoir supprimé celle indicée par NumOp, tout le monde remonte en sorte que la suivante à libérer a toujours pour indice NumOp (numéro d'option) et ainsi de suite.

51. Gestion de fenêtres-enfants

Quand un certain nombre (non connu à l'avance) de fenêtres-enfants sont susceptibles de s'ouvrir et de se fermer au cours de l'exécution d'une application, il faut gérer par programme les créations de ces fenêtres et de ses composants. Pour étudier les syntaxes de déclaration, nous partons d'un projet vierge et nous posons simplement un bouton dans la fenêtre principale, Form1 par défaut. L'idée est d'ouvrir une fenêtre-enfant à chaque clic sur le bouton. On doit refuser l'ouverture de cette fenêtre si le nombre maximal est atteint. On inclura dans chaque fenêtre nouvelle un composant PaintBox. Dans les variables générales, on déclare ceci :

const unsigned int NbFMax=3, PBW=10000,PBH=10000, FenH=300,FenL=400;
unsigned int NbFE=0;
bool FE[NbFMax];
TForm *Form[NbFMax];
TPaintBox *PB[NbFMax];

NbFMax est un nombre maximal de fenêtres pouvant être ouvertes simultanément à un moment donné. PBW et PBH (Paint Box Width et Paint Box Height) sont les dimensions de la PaintBox que l'on va créer à chaque création de fenêtre. FenH et FenL sont les dimensions d'une fenêtre-enfant à la création. On initialise NbFE à 0, c'est le nombre de fenêtres-enfants ouvertes pour l'instant, aucune au début de l'exécution du programme. Ensuite on déclare un tableau de NbFMax booléens. Tous les éléments de ce tableau seront initialisés à false à la construction de Form1. À chaque création de fenêtre (obtenue dans notre exemple par le clic du composant bouton que l'on a posé sur notre fenêtre principale), on cherchera le premier élément à false de ce tableau puis on mettra cet élément à true. Dans ces conditions, si FE[i] est true, la fenêtre numéro i est présente dans Form1 sinon elle n'existe pas. On déclare ensuite NbFMax pointeurs de TForm et NbFMax pointeurs de TPaintBox.

Comme nous le disions, on initialise à false tous les éléments du tableau FE au moment de la construction de TForm1. C'est d'ailleurs ici qu'on initialise les variables générales d'un programme. On pourrait le faire aussi à l'événement OnCreate de la fenêtre principale, mais comme C++Builder propose de lui-même la méthode Form1::Form1, on l'utilise logiquement.

__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner)
{
for(int i=0;i<NbFMax;i++) FE[i]=false;
}

Voici maintenant ce qui se passe quand l'utilisateur clique sur le bouton i.e. comment nous allons créer la fenêtre-enfant demandée.

void __fastcall TForm1::Button1Click(TObject *Sender)
{
unsigned int N=-1;
if(NbFE==NbFMax)
{
Application->MessageBoxA("Refusé!",NULL,MB_OK);
return;
}

while(FE[++N]);

Form[N] = new TForm(Form1);

Form[N]->Parent=Form1;
Form[N]->Width=FenL;
Form[N]->Height=FenH;

Form[N]->HorzScrollBar->Range=PBW;
Form[N]->VertScrollBar->Range=PBH;

Form[N]->Visible=true;
Form[N]->Align=alNone;
Form[N]->Color=clWhite;
Form[N]->Caption="Fenêtre "+IntToStr(N+1);
Form[N]->OnClose=FormCloseE;
Form[N]->OnCanResize=FormCanResizeE;
Form[N]->Tag=N+1;

PB[N]= new TPaintBox(Form1);
PB[N]->Parent=Form[N];

PB[N]->Width=PBW;
PB[N]->Height=PBH;

PB[N]->Tag=N+1;

PB[N]->OnPaint=PaintE;
PB[N]->OnMouseDown=MouseDownE;

PB[N]->Visible=true;
PB[N]->Align=alClient;

FE[N]=true;
NbFE++;
}

À l'entrée de cette méthode, on commence par tester NbFE (nombre de fenêtres-enfants déjà créées et présentes à l'intérieur de Form1). Si donc NbFE est égal à NbFMax, on refuse la création de la fenêtre, car nous sommes arrivés à saturation. Notez que si NbFMax est assez grand, ce cas devrait ne jamais se produire. Si NbFE est inférieur à NbFMax, on est alors sûr qu'il reste au moins un élément à false dans le tableau de booléens FE. On cherche l'indice de cet élément par la seule instruction while(FE[++N]), N étant initialisé à -1. Comme on procède par préincrémentation, le premier indice testé sera 0. Dès qu'un élément est trouvé faux, la boucle en while s'arrête et N sera désormais l'indice de la fenêtre que nous allons créer. N étant connu, on crée tout de suite la fenêtre par Form[N] = new TForm(Form1), ce qui signifie que le nième pointeur du tableau de pointeurs de fenêtres pointe cette fenêtre créée. On peut écrire aussi Form[N] = new TForm(this), ça marcherait tout aussi bien, les deux écritures sont équivalentes. On indique maintenant que cette fenêtre créée est parent de la fenêtre principale Form1 : Form[N]->Parent=Form1; c'est cette instruction qui fait savoir durant l'exécution que Form[N] est enfant par rapport à Form1, ce pour quoi l'enfant sera dessiné à l'intérieur du parent. Ensuite on donne les dimensions de la fenêtre-enfant Form[N]->Width=FenL; Form[N]->Height=FenH; où FenH et FenL sont des constantes arbitraires générales que nous avons déclarées. Notez que si vous voulez que la fenêtre-enfant occupe toute la place de la fenêtre-parent, il suffit d'écrire ceci (à la place des deux instructions précédentes) Form[N]->Height=Form1->ClientHeight; Form[N]->Width=Form1->ClientWidth; ce qui signifie que la hauteur de la fenêtre-enfant est égale à la hauteur de la surface cliente de la fenêtre-parent et la longueur égale à la longueur de la surface cliente. Dans ce cas, la fenêtre-enfant prend la place maximale dans son parent. Ceci nous montre que Form1->Height et Form1->Width sont les dimensions totales de Form1 alors que Form1->ClientHeight et Form1->ClientWidth sont la surface cliente excluant les bordures, c'est la zone intérieure accessible. Ensuite nous donnons les dimensions du scroll de la fenêtre-enfant par Form[N]->HorzScrollBar->Range=PBW; Form[N]->VertScrollBar->Range=PBH; où PBW et PBH sont les dimensions de la surface de la future PaintBox que l'on va créer à l'intérieur de la fenêtre-enfant. En effet, une PaintBox n'a pas de scroll, une fenêtre en a un. Si donc la PaintBox est plus grande que la fenêtre, on crée ce composant en le calant en haut à gauche de la fenêtre qui l'accueille et on active les deux scrollbars, horizontal et vertical de Form1. Le scroll sera perçu par l'utilisateur comme étant un scroll de la PaintBox, en réalité il s'agit du scroll de la fenêtre qui en tient lieu. Ensuite, il ne faut pas oublier de rendre visible la fenêtre-enfant sinon bien sûr elle n'apparaît pas à l'écran donc Form[N]->Visible=true; ensuite nous exprimons que la fenêtre-enfant est libre à l'intérieur de son parent, on pourra la déplacer et aussi changer ses dimensions visibles donc Form[N]->Align=alNone; si au lieu de alNone nous avions écrit alClient, la fenêtre-enfant aurait pris les dimensions maximales à l'intérieur de son parent et il y aurait eu impossibilité de la déplacer à l'intérieur de son parent. Nous déclarons maintenant une couleur de fond de la fenêtre-enfant Form[N]->Color=clWhite; ici, le fond de la fenêtre sera blanc. Ensuite, nous écrivons un titre à cette fenêtre-enfant qui va appraître sur la bordure en haut Form[N]->Caption="Fenêtre "+IntToStr(N+1); comme l'indice réel de fenêtre part de 0, on affiche N+1 donc pour 0 on aura fenêtre 1, pour 1 fenêtre 2, etc. Ensuite on déclare la méthode à exécuter quand la fenêtre se fermera Form[N]->OnClose=FormCloseE; cette méthode c'est FormCloseE, on utilise ici les mêmes écritures proposées par C++Builder, on rajoute simplement E pour Enfant. Comme les fenêtres créées à l'exécution ont un petit menu système par défaut, lequel se situe en haut à droite de la fenêtre, l'utilisateur pourra cliquer le x de ce menu pour fermer la fenêtre. On passera alors par la fonction membre de TForm1 FormCloseE de manière à libérer la mémoire correspondant à cette fermeture. On donne aussi la méthode à exécuter quand la fenêtre est redimensionnée Form[N]->OnCanResize=FormCanResizeE; puis on initialise le Tag de la fenêtre-enfant crée qui prend la valeur N+1 donc Form[N]->Tag=N+1; cette petite astuce nous permettra à tout moment de savoir quelle fenêtre est concernée par une action. En effet, quelle que soit la fenêtre visée, on ira toujours à la même méthode e.g. FormCloseE, il faudra donc bien savoir quelle fenêtre est concernée d'où l'utilisation du Tag qui est un nombre entier associé à un composant.

Maintenant que la fenêtre-enfant est créée avec certaines de ses propriétés initialisées par nous, on crée maintenant la PaintBox qu'on va insérer dans la fenêtre-enfant PB[N]= new TPaintBox(Form1); ce qui signifie que le Nième pointeur du tableau PB pointe cette nouvelle PaintBox. On dit maintenant au programme que cette PaintBox est un composant de la fenêtre-enfant créée PB[N]->Parent=Form[N]; i.e. le parent de la PaintBox est la fenêtre-enfant. On indique ensuite les dimensions de la PaintBox PB[N]->Width=PBW; PB[N]->Height=PBH; où PBW et PBH sont des constantes que nous avons déclarées. On initialise aussi le Tag de la PaintBox PB[N]->Tag=N+1; ainsi, à tout moment, on saura par cet entier de quelle PaintBox il s'agit en cas d'action sur elle. On indique maintenant la méthode à exécuter en cas de Paint PB[N]->OnPaint=PaintE; c'est là qu'on va dessiner dans la PaintBox. On indique aussi la méthode à exécuter quand l'utilisateur appuie sur le bouton de la souris PB[N]->OnMouseDown=MouseDownE; ensuite on dit que la PaintBox est visible PB[N]->Visible=true; elle l'est d'ailleurs par défaut, on peut ne pas écrire cette instruction. Puis on indique le type d'alignement de la PaintBox par rapport à son parent, ici il s'agira de alClient, PB[N]->Align=alClient; cela signifie que la PaintBox prend toute la place dans son parent.

La fenêtre-enfant est maintenant créée, on met donc à true le Nième élément de notre tableau de booléens FE[N]=true; et on incrémente le nombre de fenêtres-enfants présentes dans Form1, NbFE++;

Voici comment nous procédons au moment où une fenêtre se ferme. Cet événement survient parce que l'utilisateur a cliqué le petit x du menu système de la fenêtre-enfant. Au moment de la création de la fenêtre, nous avons déclaré FormCloseE comme étant la méthode à exécuter liée à l'événement OnCLose.

void __fastcall TForm1::FormCloseE(TObject *Sender, TCloseAction &Action)
{
Application->MessageBoxA("Close","OK",MB_OK);
int i=((TComponent*)Sender)->Tag-1;
delete PB[i];
delete Form[i];
FE[i]=false;
NbFE--;
}

On affiche un message fermeture. Dans une vraie application, on n'affiche évidemment rien, mais cet affichage vous prouve que la méthode est exécutée. Remarquez bien la syntaxe pour lire le Tag de la fenêtre : int i=((TComponent*)Sender)->Tag-1; dans ces conditions, i représente l'indice de la fenêtre à détruire. On détruit donc la PaintBox puis la fenêtre (dans cet ordre bien sûr, car la PaintBox est un enfant de la fenêtre), l'élément du tableau de booléens passe à false, libérant cet indice pour une future ouverture et enfin, le nombre de fenêtres-enfants est décrémenté. Comme vous le constatez, c'est le système qui se charge de la destruction visuelle de la fenêtre, mais la libération mémoire est à notre charge.

Notons qu'on peut s'épargner le tableau de booléens FE, il suffit pour cela d'initialiser les pointeurs Form[i] à NULL et de remettre chaque pointeur à NULL juste après le delete Form[i] au moment de la fermeture de la fenêtre. Ainsi pour tout i, si Form[i]==NULL, la fenêtre n'existe pas sinon elle existe, ce qui simule bien le tableau de booléens dont nous avons besoin.

Nous allons maintenant peindre à l'intérieur de chaque PaintBox. Nous avions déclaré PaintE comme méthode de l'événement OnPaint. Nous écrivons donc cette méthode qui nous permettra simplement de constater la persistance du contenu de la PaintBox ainsi que la validité du scroll.

void __fastcall TForm1::PaintE(TObject *Sender)
{
int i=((TComponent*)Sender)->Tag-1, x=20,y=20;

for(int j=0;j<30;j++)
{
AnsiString M="Fenêtre numéro "+IntToStr(i+1)+ " ("+IntToStr(j+1)+")";
PB[i]->Canvas->TextOutA(x,y,M.c_str());
x+=150;
}
}

Encore une fois, à partir du Tag du composant, on en déduit l'indice par décrémentation, car l'indice part de 0, mais le Tag de 1. i étant cet indice, on peint dans PB[i]. On affiche ici le numéro de la fenêtre, on affiche trente fois ce numéro et à chaque fois un nombre allant de 1 à 30. La coordonnée en x avance à chaque itération de 150 pixels. Ceci a simplement pour vocation de montrer que chaque fenêtre connaît son numéro, et un affichage très long en x montrera que le scroll est correct.

Voici maintenant ce qui se passe sur OnMouseDown.

void __fastcall TForm1::MouseDownE(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
int i=((TComponent*)Sender)->Tag;
Application->MessageBoxA( (IntToStr(X)+" "+IntToStr(Y)+" "+IntToStr(i)).c_str()
,"OK",MB_OK);
}

On affiche simplement les coordonnées de clic, cela vous prouve qu'il s'agit bien des coordonnées de la fenêtre cliquée. On affiche aussi le numéro du Tag pour montrer qu'à tout moment, on connaît la fenêtre visée par son indice.

Voici ce que nous proposons si l'utilisateur modifie la taille de la fenêtre (méthode FormCanResizeE déclarée en relation à l'événement OnCanResize).

void __fastcall TForm1::FormCanResizeE(TObject *Sender, int &NewWidth,
int &NewHeight, bool &Resize)
{

if(NewWidth==Form1->Width && NewHeight==Form1->Height-19)
{
NewWidth-=5;
NewHeight-=5;
}
}

On simule ainsi l'événement OnMaximize qui n'existe pas. La méthode nous envoie les nouvelles dimensions demandées par l'utilisateur. L'expérience montre que si NewWidth vaut Width et s'il y a une différence de 19 pixels entre la nouvelle hauteur avec la hauteur de la fenêtre, cela signifie que l'utilisateur a cliqué sur le petit carré du menu système pour maximaliser la dimension de la fenêtre. On ne fait que réajuster ces dimensions de manière à ce que la fenêtre-enfant s'encastre parfaitement dans son parent en soustrayant 5 unités sinon l'encastrement n'est pas parfait. La raison en est sans doute de nous rappeler la présence d'un scrollbar dans la fenêtre, on signifie par là que l'image n'est vue que partiellement. Si la fenêtre n'a pas de scrollbar, l'imbrication suite à une demande de maximalisation entre de la fenêtre-enfant à l'intérieur de son parent est parfaite, il n'est alors pas nécessaire d'intervenir logiciellement.

Voici maintenant ce qui se passe à la destruction de la fenêtre principale. Cet événement survient à la fin de l'exécution. Il convient de libérer la mémoire allouée.

void __fastcall TForm1::FormDestroy(TObject *Sender)
{
for(int i=0;i<NbFMax;i++)
{
if(FE[i])
{
delete PB[i];
delete Form[i];
}
}
}

Pour chaque élément booléen du tableau, on ne fait rien s'il est à false car la fenêtre correspondant à cet indice n'existe pas, mais on supprime la PaintBox puis la fenêtre (dans cet ordre, car la PaintBox appartient à la fenêtre) si l'élément est à true.

Bien entendu, toutes les méthodes liées aux composants sont membres de TForm1 qui apparaît donc comme suit :

class TForm1 : public TForm
{
__published: // Composants gérés par l'EDI
TButton *Button1;
void __fastcall Button1Click(TObject *Sender);
void __fastcall FormDestroy(TObject *Sender);
private: // Déclarations utilisateur
public: // Déclarations utilisateur
__fastcall TForm1(TComponent* Owner);
void __fastcall FormCloseE(TObject *Sender, TCloseAction &Action);
void __fastcall PaintE(TObject *Sender);
void __fastcall MouseDownE(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
void __fastcall FormCanResizeE(TObject *Sender, int &NewWidth,
int &NewHeight, bool &Resize);
};

Petite astuce : pour ne pas se tromper dans les arguments d'une méthode que vous voulez associer à un composant, le mieux consiste à la faire créer automatiquement par C++Builder puis procéder à un copier-coller. Par exemple, on veut rajouter l'événement OnMouseDown pour la fenêtre-enfant. On double-clique sur l'événement OnMouseDown de Form1 à seule fin d'en créer la syntaxe d'appel. Là-dessus, C++Builder vous a créé le gestionnaire d'événement, mais il a aussi déclaré la méthode dans l'en-tête associé (le .h). Faisons Ctrl-F6 qui fait afficher cet entête, on peut aussi afficher l'en-tête du fichier cpp en cours par le menu surgissant (clic à droite), c'est la première occurrence. Vous voyez que C++Builder a déclaré l'événement dans la section "published". Copiez-collez cette déclaration dans la section "public" et rajoutez simplement E à la fin (pour Enfant). Ensuite vous revenez au cpp, vous copiez-collez le gestionnaire créé par C++Builder, vous ajoutez également un E. Ainsi, vous avez créé un événement et vous êtes certain que la syntaxe sera correcte puisque c'est celle créée par le progiciel. Ensuite, à la compilation, vous verrez que l'événement OnMouseDown de Form1 a disparu, ce qui est logique puisque vous n'avez rien écrit à l'intérieur, vous n'aviez créé le gestionnaire d'événement OnMouseDown de Form1 que pour en avoir la syntaxe. Il en ira de même pour tout événement. S'il s'agit d'un événement associé à un composant qui ne se trouve pas sur votre fiche, rajoutez-le temporairement, créez le gestionnaire d'événement en double-cliquant sur l'événement voulu, copiez-collez la déclaration dans le .h et dans le .cpp et personnalisez le nom de la méthode, vos syntaxes seront correctes, vous pouvez ensuite virez le composant de la fiche puisqu'en réalité vous n'en avez pas besoin.

52. Le composant ScrollBar

Dans l'alinéa précédent, nous avons créé des fenêtres-enfants à l'intérieur desquelles on a inclus une PaintBox. On peut se poser la question de savoir en quoi cette PaintBox était utile puisqu'en réalité, toute fenêtre ayant un canvas, on dispose avec la seule fenêtre de toutes les fonctions de dessin. Cette PaintBox n'avait en fait qu'une seule utilité, celle de faire gérer automatiquement par C++Builder le scroll horizontal et vertical. En effet, la fenêtre-enfant créée avait de petites dimensions FenH et FenL, alors que la PaintBox avait une très grande surface virtuelle de dessin de PBH*PBW. Comme la PaintBox est enfant de la fenêtre (elle-même enfant de la fenêtre principale), nous avons déclaré la propriété Range des scrolls de la fenêtre-enfant à égalité avec les dimensions virtuelles de la PaintBox (Form[N]->HorzScrollBar->Range=PBW; Form[N]->VertScrollBar->Range=PBH;). C'est ce mécanisme qui fait qu'un scroll est géré automatiquement par C++Builder et c'est pourquoi la PaintBox était utile. C'est l'initialisation des propriétés Range qui rend visibles et utilisables les scrollbars.

Si maintenant vous voulez gérer vous-même le scroll, il faut alors utiliser un composant TScrollBar. Cela peut s'avérer nécessaire dans le cas où (par exemple) votre surface de dessin excède les possibilités d'une PaintBox. Une PaintBox a des dimensions et donc des coordonnées sur 15 bits, on va donc de 0 à 32767 pour les coordonnées en x et en y. Si vous vouliez gérer des coordonnées quasi illimitées (e.g. sur 32 bits), vous ne pouvez plus utiliser le scroll automatique. Dans ce cas, on se contente de déclarer une fenêtre à l'intérieur de laquelle on crée deux scrollbars, l'un horizontal, l'autre vertical et la surface de dessin n'est que la zone client de la fenêtre. Dans ce cas, la création d'une PaintBox dans la fenêtre n'est plus nécessaire puisque sa surface de dessin coïnciderait avec la zone client de la fenêtre. Cela dit, si votre programme gère ces deux modes, vous auriez intérêt malgré tout à déclarer la PaintBox même si sa dimension coïncide avec la zone client de sa fenêtre parent et ce, pour éviter d'avoir différentes syntaxes suivant que vous dessiniez dans le canvas de la fenêtre (avec des syntaxes du type Form[N]->Canvas, je reprends ici les déclarations de l'alinéa précédent) ou dans le canvas d'une PaintBox (avec des syntaxes du type PB[N]->Canvas). Dans tous les cas, on déclarerait la PaintBox, la syntaxe d'accès est alors unique.

Reprenons les éléments de l'alinéa précédent, et voyons comment nous pourrions procéder pour gérer nous même le scroll. D'une part, on déclarerait deux fois NbFMax pointeurs vers des composants de ce type.

TScrollBar *SB[NbFMax*2];

En effet, si on autorise l'ouverture de NbFMax fenêtres-enfants, on aura 2*NbFMax pointeurs de scrollbar, car pour chaque fenêtre il y aura un scrollbar horizontal et un vertical. Ainsi pour la fenêtre d'indice i, le pointeur du scrollbar horizontal sera SB[2*i] et celui du vertical sera SB[2*i+1]. Ensuite, la fenêtre d'indice N ayant été créée via Form[N] = new TForm(Form1); et certaines de ses propriétés initialisées, on crée maintenant les deux scrollbars,

SB[2*N]= new TScrollBar(Form1);
SB[2*N+1]= new TScrollBar(Form1);

On exprime ensuite que ces scrollbars appartiennent à la fenêtre-enfant créée,

SB[2*N]->Parent=Form[N];
SB[2*N+1]->Parent=Form[N];

On initialise aussi les tags pour pouvoir se repérer logiciellement,

SB[2*N]->Tag=N+1;
SB[2*N+1]->Tag=N+1;

On dit ensuite que le scrollbar d'incice 2*N est horizontal et l'autre d'indice 2*N+1 vertical,

SB[2*N]->Align=sbHorizontal;
SB[2*N+1]->Align=sbVertical;

On indique ensuite la façon dont le scrollbar se positionne par rapport à son parent, alBottom pour l'horizontal (il va donc se placer sur toute la longueur en bas de la fenêtre) et alRight pour le vertical (il va se placer à droite sur toute la hauteur de la fenêtre)

SB[2*N]->Align=alBottom;
SB[2*N+1]->Align=alRight;

On simule ainsi un double scrollbar classique, mais ils sont maîtrisés logiciellement. Ensuite on associe une méthode pour l'événement OnScroll,

SB[2*N]->OnScroll=ScrollBar1ScrollEH;
SB[2*N+1]->OnScroll=ScrollBar1ScrollEV;

Ici nous créons deux méthodes, une pour le scroll horizontal et l'autre pour le vertical. On aurait pu aussi n'en créer qu'une seule et faire la distinction à l'intérieur de la méthode. Mais dans ce cas, il faudrait initialiser autrement les tags. Ici nous avons donné au tag le numéro de fenêtre, on ne peut donc pas distinguer le scrollbar horizontal du vertical puisqu'ils ont le même tag. Il faudrait que le tag contienne l'indice du tableau SB, SB[2*N]->Tag=2*N; et SB[2*N+1]->Tag=2*N+1; auquel cas, l'indice du tag nous donne et le numéro de fenêtre et la position du scrollbar invoqué, horizontal ou vertical (pair pour horizontal, impair pour vertical), dans ces conditions une seule méthode de scroll suffirait. Pour savoir si le bouton du scroll est relâché, on teste ScrollCode avec la constante scPosition et la propriété Position du scrollbar nous donne sa position. Considérons POS comme une variable générale du type unsigned int, on renseigne cette variable dans la méthode avec la position du scroll. La variable booléenne Horiz dira si le scroll est horizontal (true) ou vertical (false). Puis on crée par programme un événement paint en appelant simplement la méthode. Voici comment on pourrait programmer ces méthodes.

void __fastcall TForm1::ScrollBar1ScrollEH(TObject *Sender,
TScrollCode ScrollCode, int &ScrollPos)
{
int i=((TComponent*)Sender)->Tag-1;
if(ScrollCode==scPosition)
{
POS=SB[2*i]->Position;
Horiz=true;
PaintE(Sender);
}
}

void __fastcall TForm1::ScrollBar1ScrollEV(TObject *Sender,
TScrollCode ScrollCode, int &ScrollPos)
{
int i=((TComponent*)Sender)->Tag-1;
if(ScrollCode==scPosition)
{
POS=SB[2*i+1]->Position;
Horiz=false;
PaintE(Sender);
}
}

Dans ces conditions, la méthode Paint est appelée. On connaît l'indice de la fenêtre concernée, on connaît la position du scroll (POS), on sait s'il s'agit du scrollbar horizontal ou vertical (Horiz), en fonction de toutes ces données, on repeint la zone concernée de la fenêtre par programme.

53. Le composant PageControl

Ce composant fait partie de l'onglet Win32 de la palette de composants, il sert à gérer une série de pages sous la forme d'onglets successifs que vous pouvez sélectionner. Vous pouvez créer un certain nombre de pages d'office à la création, on procède ainsi quand on connaît d'avance le nombre d'onglets du composant. Dans ce cas, après avoir déposé ce composant sur la fenêtre principale (Form1 par défaut), on clique à droite tout en pointant le composant et on choisit "Nouvelle page" du menu surgissant. Il suffit de répéter cette opération pour avoir d'autres pages. Vous pouvez aussi partir d'un composant vide, dans ce cas vous ne faites que déposer le composant PageControl sur votre fenêtre, le reste se fera par logiciel. L'important est de bien remarquer les syntaxes. Ainsi pour créer une nouvelle page c'est-à-dire un nouvel onglet au composant on écrira :

TTabSheet *pPage = new TTabSheet(PageControl1);

où PageControl1 est le nom par défaut du composant. Pour exprimer que cette page (élément de type TTabSheet créé par l'opérateur new) appartient au composant PageControl1, on écrit :

pPage->PageControl = PageControl1;

Pour afficher dans l'onglet une chaîne, on utilise comme à l'accoutumé la propriété Caption (

pPage->Caption="Chaîne" où pPage est du type TTabSheet c'est-à-dire un onglet du composant PageControl1), on lui affecte un AnsiString. Le nombre de pages inclus dans le composant est égal à PageControl1->PageCount, c'est un entier, la page active à un moment donné est PageControl1->ActivePageIndex, c'est un entier qui va de 0 à n-1 (n étant le nombre de pages i.e. PageControl1->PageCount). La ième page sera accessible via PageControl1->Pages[i].

Vérifions toutes ces syntaxes. On part d'un projet vierge, on dépose le composant PageControl sur la fenêtre principale Form1. On essaie de positionner ce composant vers le haut à droite de la fenêtre, on fait en sorte qu'il ressemble à une barre horizontale assez fine, affectez par exemple la propriété Width du composant à 300, Height à 25, mettez Top à 0 et Left à 250. Dans ces conditions, PageControl1 est une sorte de barre horizontale en haut à droite de la fenêtre. Mettez maintenant deux boutons, Bouton1 et Bouton2 dans la fenêtre principale Form1. Nous allons programmer la création d'un nouvel onglet à chaque clic sur Bouton1, nous allons supprimer l'onglet actif à chaque clic sur Bouton2 et quand un onglet sera sélectionné, on affichera le nombre d'onglets disponibles ainsi que le numéro de l'onglet actif. On initialisera le nom des pages par un numéro à partir de 1. De plus, si aucune page n'est disponible dans le composant, on ne l'affichera pas. Au constructeur de TForm1, on se contente de dire que le composant PageControl1 n'est pas visible, ce qui est logique, car au début il ne contient aucune page.

__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner)
{
PageControl1->Visible=false;
}

À l'événement OnClick du Bouton1, on crée un nouvel onglet comme suit :

void __fastcall TForm1::Button1Click(TObject *Sender)
{
PageControl1->Visible=true;
TTabSheet *pPage = new TTabSheet(PageControl1);
pPage->PageControl = PageControl1;
pPage->Caption = "Page " + IntToStr(PageControl1->PageCount);
}

On rend le composant visible puisqu'il va y avoir au moins une page. Quant au numéro de page affiché sur le composant, il n'est autre que le nombre de pages à savoir PageControl1->PageCount. Ainsi la première page aura pour nom "Page 1", la deuxième "Page 2" et ainsi de suite. Voici ce qui se passe à l'événement OnChange du composant. On va simplement afficher le nombre de pages présentes dans le composant ainsi que le numéro d'onglet sélectionné (attention, celui-ci part de 0, donc pour la page 3 sélectionnée vous lirez dans le MessageBox "et la page activée est la page numéro 2"). Ceci a pour but de montrer qu'on maîtrise parfaitement ce qui se passe et qu'on peut agir logiciellement.

void __fastcall TForm1::PageControl1Change(TObject *Sender)
{
AnsiString A;
A="Il y a "+IntToStr(PageControl1->PageCount)+ " page";
if(PageControl1->PageCount>1) A=A+"s";
A=A+" et la page activée est la page numéro "+
IntToStr(PageControl1->ActivePageIndex);
Application->MessageBox(A.c_str(),"OK",MB_OK);
}

Remarquez qu'on rajoute un "s" au mot "page" si le nombre de pages est supérieur à 1. Voici ce qui se passe quand on clique sur le Bouton2, lequel détruit la page activée à ce moment-là.

void __fastcall TForm1::Button2Click(TObject *Sender)
{
if(PageControl1->PageCount)
{
delete PageControl1->Pages[PageControl1->ActivePageIndex];
for(int i=0;i<PageControl1->PageCount;i++)
PageControl1->Pages[i]->Caption="Page"+ IntToStr(i+1);
}

if(!PageControl1->PageCount) PageControl1->Visible=false;
}

On commence par tester le nombre de pages présentes PageControl1->PageCount, car bien entendu, on ne peut pas supprimer de pages s'il n'y en a aucune. Si donc PageControl1->PageCount n'est pas nul, on supprime la page active par delete. Ensuite, on renumérote les pages à partir de 1 de manière à ce que ces numéros se suivent toujours à l'affichage. Ensuite, on teste de nouveau le nombre de pages de manière à rendre le composant invisible s'il ne contient aucune page. Puisque ces pages ont été créées par l'opérateur new, il sera bon de les détruire au sortir de l'application au moment de la destruction de la fenêtre principale Form1, voici donc ce qu'on écrit à l'événement OnDestroy de Form1 :

void __fastcall TForm1::FormDestroy(TObject *Sender)
{
for(int i=0;i<PageControl1->PageCount;i++)
delete PageControl1->Pages[i];
}

Il est probable, puisque ce composant appartient à Form1, que cette précaution ne soit pas nécessaire et qu'on pourrait s'en passer. Cela dit, il est toujours bon de prendre pour habitude de supprimer par delete ce qui a été créé par new, vous êtes ainsi persuadé d'une excellente gestion mémoire. Vous remarquerez à l'exécution que deux petits boutons fléchés apparaissent à droite du composant dès que tous les onglets ne peuvent plus s'afficher en totalité, ces flèches vous permettent de naviguer en avant et en arrière. N'oubliez pas en testant à l'exécution de cliquer un onglet quelconque pour faire apparaître le message-témoin programmé. À noter enfin que la propriété ActivePageIndex est aussi en écriture, ainsi pour activer logiciellement le premier onglet on écrira

PageControl1->ActivePageIndex = 0;

et pour activer le dernier

PageControl1->ActivePageIndex = PageControl1->PageCount-1;

si vous voulez que cette initialisation se fasse à la création, dans l'inspecteur d'objets sélectionnez "PageControl1" puis choisissez la page voulue (en général la première) dans le menu déroulant proposé à la propriété ActivePage, à l'exécution du programme, la page choisie sera sélectionnée.

54. Les classes TControlBar, TToolBar, TToolButton et PopupMenu.

Les deux premiers objets sont liés puisqu'on met les toolbars dans un controlbar, le controlbar représente un certain espace réservé, le plus généralement le haut de l'écran et les toolbars sont des bandes le plus souvent horizontales et qui apportent un complément au menu sous la forme de boutons souvent associés à un petit bitmap comme image (que vous pouvez créer vous-même grâce à l'éditeur d'images de C++Builder via "Outils|Editeur d'images"). La toolbar se reconnaît par ses deux petits traits verticaux sur la gauche de la bande. Regardez le menu de Word par exemple, vous voyez clairement plusieurs toolbars. L'intérêt des toolbars est de pouvoir les placer à sa guise dans la surface réservée à cet effet par glissement et déplacement voire de les sortir du controlbar. Sous Word, mettez votre curseur sur ces deux traits horizontaux d'une toolbar quelconque, cliquez et tirez le composant vers le milieu de la fenêtre, vous constatez que la toolbar devient elle-même une petite fenêtre que vous pouvez d'ailleurs fermer, la toolbar alors disparaît (en réalité c'est sa propriété Visible qui est devenue false). En la faisant glisser de nouveau dans sa surface associée appelée controlbar, la toolbar reprend son apparence d'origine, s'encastre dans son espace réservé qu'est le controlbar et vous pouvez la disposer comme bon vous semble dans cette zone. En général, ces composants sont créés à la conception, on joue simplement sur leur visibilité, mais rien ne s'oppose à ce que ces éléments soient créés par l'opérateur new comme tout composant d'ailleurs. Sous Word par exemple, vous pouvez réafficher une toolbar disparue via "Affichage|Barre d'outils", le logiciel vous affiche là toutes les toolbars incluses dans le logiciel ainsi qu'un flag visuel indiquant si la toolbar est visible ou non.

Essayons à partir d'un nouveau projet. Entrons dans C++Builder ou bien si vous y êtes déjà faites "Fichier|Nouvelle application" ou encore, ce qui est a même chose, cliquez le petit rectangle blanc légèrement corné en haut à droite puis choisissez "Application". Vous avez comme toujours votre première fiche vide, faites immédiatement "Fichier|Enregistrer le projet sous" dans votre répertoire Essai prévu pour vos tests, donnez comme nom Unit01 et Projet01 ou tout autre indice sur deux chiffres de préférence (avec deux chiffres tous vos projets seront présentés triés au moment de leur réouverture même au-delà de 9, d'où l'intérêt d'indicer sur deux chiffres).

Sélectionnez le composant ControlBar de la palette, il se trouve dans l'onglet "Supplément", on voit deux petits traits verticaux ainsi qu'une flèche noire vers la droite. Si vous ne le voyez pas, scrollez à droite c'est-à-dire cliquez la petite flèche noire à l'extrême droite de la palette (l'onglet "supplément" étant bien sûr toujours sélectionné). Aidez vous du hint c'est-à-dire de la bulle d'aide qui s'affiche automatiquement quand on pointe un composant de la palette, là la bulle d'aide affichera "ControlBar". Cliquez donc sur le controlbar pour le sélectionner puis cliquez n'importe où dans la fenêtre Form1 pour le déposer. Notons au passage qu'après avoir sélectionné un composant en cliquant dessus, il suffit de faire F1 pour faire afficher les renseignements de ce composant notamment les propriétés et les méthodes associées.

On vient donc de déposer un ControlBar dans Form1, il se présente sous la forme d'un rectangle posé n'importe où dans la fenêtre. Or, ce que nous voulons, c'est le coller tout en haut de la fenêtre. Donc cliquez la propriété Align de l'inspecteur d'objets et choisissez alTop dans le petit menu déroulant proposé. Vous voyez qu'immédiatement, le controlbar se colle en haut de la fenêtre. Ensuite, mettez AutoSize à true. Ainsi, le controlbar se redimensionnera de lui-même en fonction du nombre de toolbars présentes dans cette surface et de leur disposition.

Sélectionnez maintenant le composant ToolBar de la palette, onglet "Win32', on le reconnaît par le fait qu'il simule un début de menu classique avec un petit rectangle blanc corné (icône "Nouveau") puis le début du logo "ouvrir" en jaune. Cliquez donc ce composant puis cliquez à l'intérieur du controlbar pour l'y déposer. Vous pouvez d'ores et déjà faire F9 pour exécuter, vous voyez clairement votre toolbar et vous pouvez la déplacer dans son espace. Ajoutons encore deux autres toolbars de la même manière en sélectionnant ce composant dans la palette et en cliquant ensuite dans le controlbar (mais non dans un toolbar déjà déposé, cliquez dans un endroit libre du controlbar). Faites F9 pour exécuter, vous voyez vos trois toolbars et vous pouvez les superposer, le controlbar s'agrandit automatiquement pour créer l'espace nécessaire, car nous avons mis la propriété AutoSize à true du controlbar.

Seulement nos toolbars sont coincées dans leur espace, on ne peut pas les sortir de là. Si vous voulez autoriser la sortie possible d'une toolbar hors du controlbar, il suffit de positionner sa propriété DragKind à dkDock. La propriété DragKind n'a que deux possibilités, dkDrag et dkDock, la première interdit la sortie du composant hors du controlbar, la deuxième autorise cette possibilité. Comme DrakKind est à dkDrag par défaut, vous comprenez pourquoi vous ne pouviez jusqu'à présent la mouvoir hors du controlbar. En général, les composants ont la propriété DragKind à dkDrag, car ils n'ont aucune raison de sortir de leur espace, mais pour les toolbars, on trouve les deux possibilités.

À partir de là, vous pouvez mettre ce que vous voulez dans les toolbars, en général on essaie de regrouper les fonctions par type avec une toolbar par type de fonctions, c'est la règle respectée dans tous les logiciels.

Il existe des boutons spéciaux pour les toolbars, on les obtient en sélectionnant la toolbar dans Form1 puis en cliquant à droite, choisissez dans le menu "Nouveau bouton" qui est la première occurrence de ce menu, un bouton se dessine alors. Répétez cette opération plusieurs fois pour ajouter quelques boutons à cette même toolbar. Puis, donnez à cette toolbar la propriété AutoSize à true et la propriété ShowCaptions à true également. Vous voyez que le nom des boutons apparaît maintenant, faites F9 pour essayer, vous constatez que la toolbar peut avoir diverses apparences. Cela dit, pour que le bouton réagisse visuellement, il faut cliquer dessus. Si vous voulez que la réaction ait lieu au moment où la souris passe sur le bouton, il faut positionner la propriété flat de la toolbar à true. On simule ainsi le comportement d'un menu. Vous avez le choix ici soit de créer une fonction pour le clic associé soit d'afficher un menu déroulant pour simuler un autre menu que le menu principal.

Par exemple, double-cliquez sur le premier bouton et créez le gestionnaire d'événements comme suit :

void __fastcall TForm1::ToolButton1Click(TObject *Sender)
{
Application->MessageBox("Bouton1", "OK", MB_OK);
}

Un message vous sera affiché sur clic du bouton.

Essayons avec un PopupMenu. Sélectionnez dans l'onglet "standard" de la palette le composant "PopupMenu" (son icône est un début de menu avec une flèche blanche) et déposez-le dans Form1. Une fois déposé, cliquez à droite sur le composant et choisissez "concepteur de menus". On vous présente un cadre avec un début de menu. Dans l'inspecteur d'objets, le curseur se trouve dans Caption, il s'agit d'entrer une option à ce nouveau menu, entrez par exemple "test1" et validez. Le concepteur de menu affiche cette occurrence et vous propose maintenant une deuxième case, cliquez cette case et saisissez dans l'inspecteur d'objets "Test2". Continuez ainsi plusieurs fois pour obtenir un menu de test. À chaque fois, vous cliquez la nouvelle case proposée dans le concepteur de menu, le curseur se trouve automatiquement dans Caption dans l'inspecteur d'objets, saisissez le nom de l'option, validez et recommencez. Pour que cette saisie soit agréable, il est préférable que l'inspecteur d'objets ne chevauche pas le concepteur de menu, ainsi vous passez facilement d'une fenêtre à l'autre. Après ces saisies, faites disparaître le concepteur de menu. Pour le rappeler, vous pourrez toujours cliquer à droite en pointant ce composant dans Form1 et en choisissant "Concepteur de menus", vous pouvez aussi sélectionner ce composant dans Form1 de manière à le sélectionner dans l'inspecteur d'objets, puis placer le curseur dans la propriété "Items", cliquez alors les points de suspension, vous réobtenez le concepteur de menus.

Notre menu déroulant étant créé, nous allons l'associer à notre deuxième bouton. Double-cliquez ce bouton pour créer le gestionnaire d'événement associé et copiez-collez ce qui suit :

void __fastcall TForm1::ToolButton2Click(TObject *Sender)
{
int x=Form1->Left+ToolBar1->Left+ToolButton2->Left+5;
int y=Form1->Top+ToolBar1->Top+ToolButton2->Top+50;
PopupMenu1->Popup(x,y);
}

Dans cette méthode, on calcule les coordonnées où va apparaître le menu surgissant. Left et Top représentant les coordonnées par rapport au parent, on ajoute toutes ces coordonnées liées aux trois éléments (Form1, ToolBar1 et ToolButton2) et on ajuste in fine par une constante pour régler parfaitement cet affichage.

Pour éviter les complications inutiles, je vous conseille d'interdire la sortie du toolbar pour des pseudomenus de ce genre, donc mettez sa propriété DragKind à dkDrag, c'est d'ailleurs sa position par défaut. La raison en est qu'il est difficile de calculer les coordonnées où il faudrait afficher le menu car ToolBar1->Left et ToolBar1->Top sont tous deux à zéro si la toolbar est sortie du controlbar. C'est logique puisque Left et Top sont les coordonnées par rapport à Form1, or une fois sortie la toolbar est libre par rapport à Form1, la toolbar est toujours un objet-enfant de Form1 pour les problèmes de libération mémoire, mais n'a plus de liens au plan des coordonnées.

Le bouton peut-être également associé à un menu surgissant, il suffit de donner le nom du popupmenu (par défaut PopupMenu1) à la propriété PopupMenu du ToolButton2, mais dans ce cas, c'est par le clic à droite qu'il apparaît. Si vous voulez simuler un menu classique, il vaut mieux procéder comme indiqué ci-dessus à savoir créer le popupmenu indépendamment et l'afficher via une instruction du type PopupMenu1->Popup(x,y) au moment du clic en réglant ses coordonnées (x,y).

Les toolbuttons des toolbars peuvent aussi afficher un bitmap à titre d'icône au lieu d'un mot-clé explicatif. Utilisons notre deuxième toolbar et voyons comment procéder pour avoir de tels toolbuttons. Commençons par créer de petits bitmaps qui nous serviront d'icônes. Pour ce faire, faites "Outils|Éditeurs d'images", c'est un utilitaire intégré dans C++Builder qui vous permet précisément de créer vos propres icônes en couleurs.

Notons que les toolbars peuvent, comme tout composant, être créés logiciellement à l'exécution. Par exemple, on déclare dans les variables générales un pointeur sur une toolbar

TToolBar *pTB;

À l'initialisation, on crée la toolbar (ou à tout autre moment d'ailleurs).

__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
pTB = new TToolBar(Form1);
pTB->Parent = ControlBar1;
pTB->ShowCaptions = True;
pTB->Caption="Pages";
pTB->Color=clAqua;
pTB->AutoSize=true;
}

On suppose ici que la forme principale dispose d'un controlbar qui sert de parent au toolbar instancié. On met sa propriété ShowCaptions à true de manière à ce que les captions des futurs boutons soient affichés. On met Autosize à true, ainsi la toolbar aura la dimension adéquate en fonction du nombre de boutons créés. Voici comment ajouter un bouton à la toolbar :

TToolButton *pButton = new TToolButton(pTB);
pButton->Parent = pTB;
pButton->Caption = IntToStr(pTB->ButtonCount);
pButton->Tag=pTB->ButtonCount;
pButton->OnClick=ButtonClickX;

On déclare le bouton parent de la toolbar. Son caption est ici son numéro d'ordre ButtonCount à savoir le nombre de boutons existant dans la toolbar. On positionne le Tag du bouton avec ce numéro d'ordre. On associe l'événement OnCLick à la méthode ButtonClickX, méthode qu'il faudra déclarer dans la classe TForm1. Dans la classe TForm1, il faudra donc rajouter dans la section public :

void __fastcall ButtonClickX(TObject *Sender);

Par exemple, créons cette méthode ButtonClickX en affichant simplement le numéro du bouton cliqué. Ce numéro est ici son tag, car à la création, le tag a été initialisé avec le nombre de boutons présents dans la toolbar, par conséquent le premier bouton créé a son tag à 1, le deuxième à 2 et ainsi de suite. Cet affichage prouve qu'on maîtrise la situation puisqu'on connaît le numéro du bouton cliqué, on pourrait donc exécuter telle ou telle méthode en fonction de ce numéro.

void __fastcall TForm1::ButtonClickX(TObject *Sender)
{
int i=((TComponent*)Sender)->Tag;
AnsiString A=IntToStr(i);
Application->MessageBox(A.c_str(),"OK",MB_OK);
}

Cela dit, on pourrait aussi créer une méthode spécifique pour chacun des boutons créés. En général, cela n'est pas utile, un numéro d'ordre suffit largement à un bon comportement logiciel (par exemple, on gère ainsi un affichage de pages, le bouton cliqué sera le numéro d'une page à afficher), mais bien entendu, tout est possible. À la fin de l'exécution de l'application, il ne faudra pas oublier de détruire les boutons de la toolbar puis la toolbar, dans cet ordre.

void __fastcall TForm1::FormDestroy(TObject *Sender)
{
for(int i=0;i<pTB->ButtonCount;i++)
delete pTB->Buttons[i];
delete pTB;
}

À noter qu'il ne faudra pas attribuer la valeur dkDock à la propriété DragKind de la toolbar (c'est-à-dire qu'à sa création il ne faudra pas écrire pTB->DragKind=dkDock;), car alors la toolbar pourrait sortir du controlbar et sa destruction créerait une violation d'accès si toutefois elle se trouvait effectivement hors du controlbar à ce moment-là. Je conseille donc de ne pas initialiser DragKing et donc de laisser sa valeur par défaut à savoir dkDrag qui interdit la sortie du toolbar de son controlbar parent, dans ces conditions il n'y aura pas de problème au moment du delete.

55. L'éditeur d'images

Après être entré dans l'éditeur d'images via "Outils|Éditeurs d'images" (ou encore par le menu Démarrer->Programmes->BorlandC++Builder5->Éditeur d'images) pour créer de petits bitmaps qui vont nous servir d'icônes, faites "Fichier|Nouveau" et choisissez "Bitmap" dans le menu déroulant puis donnez ses dimensions par exemple 32 par 32, choisissez un bitmap sur 16 couleurs, c'est d'ailleurs la proposition par défaut. Faites plusieurs fois crlt-i (contrôle i) pour ajuster le niveau de zoom, ctrl-u (contrôle u) pour diminuer le zoom s'il était devenu trop grand pour vous. Après avoir choisi une dimension de zoom satisfaisante, dessinez quelque chose, choisissez une couleur en cliquant dans la palette de couleurs et étalez cette couleur dans le bitmap et répétez l'opération avec une autre couleur (le contenu du dessin pour ce test n'a aucune importance, il s'agit de montrer comment associer un bitmap à un toolbutton). Une fois ce dessin de test terminé, faites "Fichier|enregistrez sous", gardez pour ce test le nom de bitmap1, mais donnez votre répertoire de développement en utilisant le navigateur habituel jusqu'à vous trouver "chez vous", par exemple dans votre répertoire Essai prévu pour les tests. En enregistrant le bitmap dans ce répertoire, vous n'aurez à donner au LoadFromFile que le nom du bitmap, c'est l'avantage. Créez ainsi trois bitmaps 16 couleurs 32 par 32 nommés Bitmap1, Bitmap2 et Bitmap3. Notons au passage qu'une grande quantité de bitmaps en tous genres sont prêts à être utilisés dans le répertoire Program Files\Fichiers communs\Borland Shared\Images.

Nous avons donc trois bitmaps. Revenons à notre deuxième toolbar de l'alinéa précédent. Mettez sa propriété AutoSize à true, puis les propriétés ButtonHeight et ButtonWidth à 32 toutes deux. Ensuite cliquez à droite tout en pointant cette toolbar et choisissez "Nouveau bouton", faites cette opération trois fois, vous avez donc trois toolbuttons de dimensions 32 par 32.

Maintenant, posez le composant ImageList sur la fiche principale.

56. La classe TImageList

Ce composant se situe dans l'onglet "Win32" de la palette de composants, sélectionnez-le dans la palette et cliquez dans la fiche principale Form1 pour l'y déposer. Ensuite (car nous continuons nos tests des deux alinéas précédents), mettez ses propriétés Height et Width toutes deux à 32 puisque nos bitmaps dessinés précédemment sont de 32 par 32.

Maintenant, il faut signaler à la toolbar2 (dans notre exemple, c'est la deuxième toolbar qui va afficher les bitmaps) qu'elle est associée à cette liste d'images. Il suffit de sélectionner la propriété "Images" et de sélectionner ImageList1 dans la liste déroulante. Vous concluez facilement que vous pouvez avoir plusieurs listes d'images de ce genre et les associer à une toolbar.

Notez que quand vous avez créé les toolbuttons, C++Builder a incrémenté pour vous la propriété ImageIndex. Il a donc notifié 0 pour le premier toolbutton, 1 pour le deuxième et ainsi de suite. C'est ici qu'on affecte une image de la liste par son indice au toolbutton concerné, vous pouvez évidemment modifier cet ordre.

Le reste se fait par logiciel. Dans le constructeur de TForm1, nous allons charger ces images via trois bitmaps créés localement.

__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner)
{
Graphics::TBitmap* B1,*B2,*B3;

B1 = new Graphics::TBitmap();
B2 = new Graphics::TBitmap();
B3 = new Graphics::TBitmap();

B1->LoadFromFile("Bitmap1.bmp");
B2->LoadFromFile("Bitmap2.bmp");
B3->LoadFromFile("Bitmap3.bmp");

ImageList1->Add(B1,NULL);
ImageList1->Add(B2,NULL);
ImageList1->Add(B3,NULL);

delete B1;
delete B2;
delete B3;
}

En premier lieu on déclare trois pointeurs de bitmaps puis on crée les bitmaps par l'opérateur new, ensuite on les charge via l'instruction LoadFromFile, comme ces bitmaps ont été enregistrés dans notre répertoire de développement, nous n'avons eu à signaler que le nom de ces bitmaps sans le répertoire, C++Builder comprend qu'il s'agit du répertoire courant. Ensuite via Add, on ajoute les bitmaps à la liste d'images puis on supprime ces bitmaps dont on n'a plus besoin.

Tout est prêt, faites F9 pour compiler et exécuter, vous voyez votre toolbar avec les bitmaps que vous avez créés vous-même.

Bien entendu, ce type de syntaxe est possible si vous avez assez peu de bitmaps, si vous en avez beaucoup, il faut procéder via une boucle, c'est un peu plus élégant. Voici comment cela se programmerait. Nous ne gardons dans l'exemple suivant que trois bitmaps, mais nous procédons par boucle simplement pour montrer le mécanisme.

__fastcall TForm1::TForm1(TComponent* Owner):TForm(Owner)
{

/* Nombre d'images ici 3 pour notre test*/
const int NbBM=3;

/* Déclaration d'une TStringList qui contiendra
le nom des bitmaps */
TStringList *SL=new TStringList;

/* Déclaration d'un bitmap unique */
Graphics::TBitmap *BM = new Graphics::TBitmap();

/* On ajoute dans la StringList le nom des bitmaps*/
SL->Add("Bitmap1.bmp");
SL->Add("Bitmap2.bmp");
SL->Add("Bitmap3.bmp");

/* Pour chaque nom de la StringList, on charge notre
bitmap et on ajoute son image à la liste d'images */
for(int i=0;i<NbBM;i++)
{
BM->LoadFromFile(SL->Strings[i]);
ImageList1->Add(BM,NULL);
}

/* on supprime la stringList et le bitmap*/
delete SL;
delete BM;
}

On déclare une StringList (voir alinéa 44 de la deuxième partie) SL et un bitmap unique BM. On ajoute via Add le nom des bitmaps successifs dans la StringList. Puis on boucle. À chaque itération, on charge le bitmap dont le nom est la ième occurrence de la StringList puis on ajoute ce bitmap à la liste d'images. Ensuite on supprime et la StringList et le bitmap, mais les images sont enregistrées dans la liste. Faites F9 pour compiler-exécuter, le résultat est le même que précédemment.

Autre possibilité si vous voulez vous épargner une StringList : affecter directement un tableau de chaînes avec les noms successifs de bitmaps avec une syntaxe du type char Liste[][50]={"Bitmap1.bmp","Bitmap2.bmp","Bitmap3.bmp"};

Voici comment nous pourrions procéder :

__fastcall TForm1::TForm1(TComponent* Owner):TForm(Owner)
{
const int NbBM=3;
char Liste[][50]={"Bitmap1.bmp","Bitmap2.bmp","Bitmap3.bmp"};
Graphics::TBitmap *BM = new Graphics::TBitmap();

for(int i=0;i<NbBM;i++)
{
BM->LoadFromFile(Liste[i]);
ImageList1->Add(BM,NULL);
}

delete Liste;
delete BM;
}

Ici la suite des noms s'enregistre automatiquement dans la variable Liste indicée, on boucle de la même façon que précédemment, mais on lit le ième nom de la liste par Liste[i] puis on supprime cette liste de noms devenue inutile et le bitmap. Le résultat est le même à l'exécution.

Il existe une autre possibilité qui consiste à charger à la conception tous ces bitmaps, c'est d'ailleurs probablement une meilleure solution, cela permet à l'application de ne pas s'en occuper, les exemples précédents se proposaient simplement de montrer un exemple de codage, car il est bon de maîtriser les syntaxes d'accès. Dans ce cas, une fois chargés dans le composant ImageList, vous n'avez plus besoin de ces bitmaps et notamment ils n'ont plus besoin de se trouver dans le répertoire par défaut puisqu'ils ne seront plus lus à l'exécution étant préchargés. Pour ce faire, créez un répertoire particulier et stockez-y tous vos bitmaps de même dimension, créez par exemple un répertoire Bitmap32x32 pour les bitmaps 32x32 et bitmap16x16 si vous avez une autre série de bitmaps en 16x16 (comme ces ressources vont se trouver maintenant à l'intérieur de l'application, un peu à la façon des constantes qui sont chargées à l'exécution, elles n'ont plus besoin de se trouver dans le répertoire par défaut). Cliquez à droite sur le composant ImageList et choisissez dans le menu surgissant l'option "Éditeur d'ImageList". Grâce à ce formulaire, vous pouvez charger les bitmaps les uns après les autres dans le composant, il suffit de cliquer "Ajouter", d'indiquer le bitmap à insérer dans la liste et de recommencer l'opération. Le logiciel décode en principe une couleur transparente, cela améliore la présentation. Normalement le choix calculé et proposé par défaut est correct, mais si cela ne convient pas (c'est très rare), vous pouvez changer cette couleur transparente ou même indiquer qu'il n'y a pas de couleur transparente. En principe la couleur transparente améliore la présentation, car seul le dessin s'incruste dans le bouton. Il y a un autre avantage, c'est que le bitmap s'affiche à la conception dans le bouton de la ToolBar. Si la propriété ImageIndex est correctement renseignée pour le bouton concerné, alors le dessin apparaîtra soit en sortant du formulaire soit en faisant "Appliquer". En principe, les boutons de ToolBar font plutôt 16x16. Il est donc bon au moment de créer un logiciel d'avoir un répertoire nommé par exemple Bitmap16x16 dans lequel on emmagasine nos bitmaps ayant cette dimension. On les charge un à un à la conception comme indiqué ci-dessus. Puis on indique aux boutons l'index de l'image-bitmap correspondant. Si vous avez un menu, il suffira de renseigner aussi l'index du bitmap associé pour chaque option du menu et le bitmap apparaîtra à côté de l'option visée. Voyez les logiciels en général, en faisant dérouler le menu principal, on voit parfois un petit bitmap dessiné en regard à gauche de certaines options, ce n'est pas obligatoire, si vous gardez comme index la valeur -1 qui signifie "pas de bitmap associé", il n'y aura pas de bitmap dessiné à gauche, mais si vous renseignez cet index, il apparaîtra pendant la conception même. Notez aussi que cette technique est en relation avec le composant TActionList (dont nous allons parler alinéa 58). En effet, le composant très utile ActionList peut être associé à une ImageList et donc chaque action de la liste peut s'associer à un index de cette liste d'images. Or, comme à un bouton ou à une option de menu on peut associer une action (faisant partie de la liste d'actions), le bitmap se dessinera automatiquement, C++Builder renseigne par défaut lui-même les index. Ainsi on procédera dans l'ordre suivant : premièrement on enregistre les bitmaps dans ImageList, deuxièmement on déclare les actions dans ActionList en indiquant la liste d'images associée puis pour chaque action l'index de l'image correspondant, troisièmement on choisit pour un bouton l'action (propriété Action) de la liste d'actions (le bitmap se dessinera alors automatiquement), quatrièmement idem pour une fonction de menu, la propriété Action sera renseignée avec la bonne action de ActionList et donc le bitmap associé se dessinera. Nous avons un peu anticipé sur l'alinéa 58 (TActionList).

Remarquez aussi ceci. Si un bitmap vous intéresse dans un logiciel quelconque, il est très aisé de le récupérer. On commence par faire une copie d'écran. Pour ce faire, on active la fenêtre du logiciel où se trouve le bitmap à extraire puis on fait "Alt-Impr écran" (on appuie sur Alt puis sur "Impr écran" à droite de la touche F12). Par cette opération, vous avez copié dans le presse-papier la fenêtre active qui contient notre bitmap. À ce stade, on entre dans l'éditeur d'images de C++Builder (Outil->Éditeur d'images, voir alinéa 55), on crée un nouveau bitmap par Fichier->Nouveau, on choisit "Bitmap" parmi les choix possibles, on surdimensionne en choisissant comme dimension 800x600 pour copier l'image dans sa totalité, on fait "coller" ("ctrl v", contrôle v) ce qui colle l'image dans ce bitmap 800x600, on repère l'endroit où se trouve le petit bitmap à dupliquer en naviguant dans l'image et en jouant avec les zooms ("ctrl i" pour zoom avant et "crlt u" pour zoom arrière). Là, on sélectionne le sélecteur de l'éditeur, c'est le premier logo en haut à gauche qui représente un carré avec une flèche blanche, cette fonction permet d'extraire un cadre dans le bitmap 800x600, on encadre à peu près le petit bitmap à extraire (en l'entourant largement), on refait Fichier->Nouveau, on rechoisit "Bitmap", on accepte cette fois-ci les dimensions par défaut 100x100, on colle ce que nous avons copié par le sélecteur (crtl v) et là, en grossissant bien le bitmap, on extrait en réutilisant le sélecteur un bitmap de 16x16, on recrée un nouveau bitmap de 16x16, on colle ce que nous avons extrait en 16x16, on obtient ainsi le bitmap de 16x16 voulu, puis on l'enregistre dans notre répertoire prévu à cet effet. On se débarrasse ensuite des deux premiers bitmaps (800x600 et 100x100) qu'on a créés simplement pour extraire in fine le petit bitmap de 16x16 qui lui a été enregistré dans notre répertoire. Par la suite, il suffira de l'insérer dans le composant ImageList comme indiqué ci-dessus.

57. Les applications MDI

C++Builder vous permet de développer deux types d'application, les applications SDI (Single Document Interface) et les applications MDI (Multiple Documents Interface). On reconnaît une application MDI au fait qu'il y a deux menus système, on appelle ainsi les trois petits boutons en haut à droite des fenêtres, un petit bouton avec un petit trait pour minimiser la fenêtre, un petit bouton avec deux carrés en cascade pour réduire la fenêtre et un petit bouton avec un x à l'intérieur pour fermer la fenêtre.

Ouvrez par exemple le bloc-notes appelé "Notepad", c'est un petit éditeur de texte pour prendre des notes simples, vous n'avez qu'une seule fenêtre. En faisant "Fichier|Nouveau", vous ne faites qu'effacer la fenêtre en cours, mais vous n'ouvrez pas d'autre document, c'est une application SDI à menu système unique.

Ouvrez maintenant Word, il est doté de deux menus systèmes superposés, celui tout en haut est le menu système de Word lui-même et celui en dessous est le menu système de la fenêtre. Réduisez la fenêtre-texte en cliquant sur le bouton du milieu (deux carrés en cascade) du menu système de la fenêtre-texte c'est-à-dire celui qui se trouve juste en dessous du menu système de Word, vous observez que le texte se trouve maintenant dans une fenêtre à lui doté de son propre menu système et que le menu système qui se trouvait en haut a disparu. De plus, en faisant "Fichier|Nouveau", vous ouvrez vraiment une autre fenêtre dotée de son propre menu système. Word est donc une application MDI.

Nous allons voir comment procéder avec C++Builder pour obtenir un comportement de fenêtres identique à celui de Word. Créez un nouveau projet vierge et sauvegardez-le dans votre répertoire Essai de test en utilisant les noms proposés, mais en les indiçant selon notre habitude unitxx et projetxx (où xx est l'indice sur deux chiffres). Vous avez donc une fiche principale Form1. La première chose à faire est d'indiquer que notre application sera du type MDI, il suffit pour ce faire d'affecter la propriété FormStyle avec la valeur fsMDIForm proposée dans le menu déroulant. Par défaut, cette propriété est fsNormal, c'est la valeur utilisée pour les applications SDI.

Ensuite une application MDI doit obligatoirement avoir un menu principal. La raison en est que le menu système des futures fenêtres-enfants se dessinera dans ce même menu sur la droite (comme c'est le cas par exemple sous Word ou toute autre application MDI). Si vous n'aviez pas de menu principal, cela poserait problème, car alors, en cas de maximalisation d'une fenêtre-enfant, les menus systèmes de deux fenêtres parent et enfant se superposeraient et on ne pourrait plus distinguer les fenêtres. Donc sélectionnez "MainMenu" de la palette de composants (onglet "Standard") et posez-le dans Form1. Créez ensuite un menu de test, la méthode est la même que pour les popupmenus, voyez l'alinéa 54 à ce sujet. Faites F9 pour compilez-exécuter, vous voyez que la présentation est légèrement différente, la zone client de la fenêtre principale est encadrée.

Nous allons maintenant programmer la création de fenêtres enfants, mais avant nous allons déclarer un tableau F de pointeurs de fiche ainsi qu'une constante NbFenMax pour contrôler le nombre de fenêtres maximum que l'on peut ouvrir. Voici donc à quoi ressemble le début de notre unité cpp.

#include <vcl.h>
#pragma hdrstop

#include "Unit04.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
const int NbFenMax=3;
TForm1 *Form1;
TForm *F[NbFenMax];

Ceci vous montre l'endroit où vous pouvez déclarer vos variables générales, ici F notre tableau de pointeurs de fiche. Ensuite, dans le constructeur de TForm1, on initialise à NULL tous les pointeurs du tableau.

__fastcall TForm1::TForm1(TComponent* Owner):TForm(Owner)
{
for(int i=0;i<NbFenMax;i++) F[i]=NULL;
}

Cette petite astuce nous permettra de savoir pour un indice donné s'il est associé ou non à une fenêtre. Une des options de notre menu sera "Nouveau" pour créer une fenêtre. On cherchera dans le tableau le premier indice libre entre 0 et NbFenMax-1. Voici cette petite fonction :

int __fastcall Prem()
{
for(int i=0;i<NbFenMax;i++) if(F[i]==NULL) return i;
return -1;
}

Vous voyez qu'on boucle dans notre petit tableau de pointeurs, si l'indice lu est à NULL, cela signifie qu'il est libre et alors on sort tout de suite avec cet indice sinon on s'en retourne avec -1 qui indiquera au programme appelant qu'il est impossible d'ouvrir une nouvelle fenêtre parce que le maximum autorisé a été atteint. Dans nos tests, NbFenMax est à trois pour tester précisément ce cas, mais si NbFenMax est assez grand, il est peut probable que cela se produise. Pour les puristes, j'accorde que l'on peut écrire if(!F[i]) au lieu de if(F[i]==NULL), les deux syntaxes sont équivalentes.

Double-cliquons dans l'option "Nouveau" de notre menu principal pour créer le gestionnaire d'événement correspondant et programmons l'ouverture d'une fenêtre-enfant.

void __fastcall TForm1::NouveauClick(TObject *Sender)
{
int i=Prem();
if (i==-1) Application->MessageBox("Refusé", NULL, MB_OK);
else
{
F[i]=new TForm(Form1);
F[i]->FormStyle=fsMDIChild;
F[i]->Color=clWhite;
F[i]->Caption="Fenêtre "+IntToStr(i+1);
F[i]->Tag=i;

F[i]->OnClose=FormCloseE;
}
}

Nous appelons d'abord notre fonction Prem qui nous renvoie le premier indice disponible du tableau de pointeurs de fiches. Si cet indice i est -1, la demande est refusée, car nous n'avons plus de pointeurs disponibles, tous pointent une fenêtre-enfant. Dans ce cas, on affiche un message dans un MessageBox et on quitte la méthode. Sinon, l'indice i est valide, on crée alors la fenêtre via l'opérateur new puis on indique immédiatement qu'elle est du type fsMDIChild. Dans ces conditions, la fenêtre viendra se dessiner dans la zone client de son parent. Ensuite on donne la couleur de la fenêtre (ici clWhite pour la différencier de son parent en gris par défaut), puis le titre de la fenêtre via son numéro, on ajoute simplement 1, on aura donc "Fenêtre 1" pour l'indice 0, "Fenêtre 2" pour l'indice 1 et ainsi de suite. Ensuite on initialise son Tag avec l'indice du tableau, cela nous permet à tout moment de savoir quel est l'indice concerné si besoin est. Ensuite on indique que la méthode FormCloseE est associée à l'événement OnClose.

Créons cette méthode OnCloseE. Comme on ne peut pas connaître toutes les syntaxes, le mieux est d'en faire créer le prototype par C++Builder. Pour ce faire, sélectionnez Form1 puis double-cliquez l'événement "OnClose" de Form1. C++Builder vous en a créé la syntaxe. Revenez au source et faites maintenant ctrl-F6 (contrôle F6) pour afficher l'en-tête. Vous voyez que C++Builder a déclaré cette méthode dans la classe Form1, copiez-collez cette méthode dans la section "public" et rajouter un E pour Enfant à son nom, à partir de OnClose, on obtient OnCloseE. Revenez au source C++ et copiez-collez la méthode OnClose à la suite du source et rajoutez également le E à son nom. Par ces deux opérations, vous venez de créer la méthode OnCloseE associée à l'événement OnClose de la fenêtre-enfant. Voici comment nous allons programmer cette méthode.

void __fastcall TForm1::FormCloseE(TObject *Sender, TCloseAction &Action)
{
int i=MDIChildren[0]->Tag;
delete F[i];
F[i]=NULL;
}

Remarquez qu'on utilise le tableau MDIChildren indicé créé automatiquement par C++Builder. Or, l'indice 0 représente toujours la fenêtre active au moment de l'exécution de la méthode, on lit le Tag du pointeur de la fiche active ce qui nous donne l'indice réel par rapport à notre tableau. Ensuite on supprime la fiche et on réinitialise à NULL l'indice de ce tableau. Cette programmation est nécessaire sinon, en cliquant le petit x du menu système d'une fenêtre-enfant, cette fenêtre se réduit, mais ne disparaît pas.

Dans cette méthode TForm1::FormCloseE, on dispose de l'argument Sender de type TObject. Cet objet, c'est évidemment la fenêtre dont on demande la fermeture et c'est aussi la fenêtre active pour l'instant. Il en résulte que pour obtenir l'indice correct on pouvait aussi écrire int i=((TComponent*)Sender)->Tag; au lieu de int i=MDIChildren[0]->Tag; et ce, parce que la fenêtre active correspond à l'objet envoyé. Il n'en est pas toujours ainsi. Imaginons que chaque fenêtre-enfant soit dotée, comme ce sera pratiquement obligatoire, d'une méthode OnPaint, l'objet envoyé ne sera pas toujours la fenêtre active, car on repeint aussi les fenêtres inactives suite à des déplacements de fenêtres. Imaginons qu'à chaque fenêtre F[i] soit associée deux bitmaps BM1[i] et BM2[i]. Au plan algorithmique, on dessine dans BM1[i], puis on recopie le dessin dans BM2[i] au moyen d'un Draw (ceci nous permet de toujours dessiner hors écran, on n'affiche que le résultat final), la méthode OnPaint d'une fenêtre se contentera de recopier BM2[i] dans le canvas de la fenêtre envoyée. La syntaxe d'écriture serait donc du type :

void __fastcall TForm1::FormPaintE(TObject *Sender)
{
int i=((TComponent*)Sender)->Tag;
Fen[i]->Canvas->Draw(0,0,BM2[i]);
}

Grâce à l'écriture int i=((TComponent*)Sender)->Tag; on récupère l'indice non pas de la fenêtre active (ce serait insuffisant pour la méthode OnPaint qui ne repeindrait pas les autres fenêtres), mais la fenêtre envoyée par le système (le système a repéré qu'un paint était nécessaire pour cette fenêtre-enfant). Le bitmap BM2[i] étant censé être prêt, on se contente de le recopier dans la fenêtre visée.

Si votre menu a une option "Fermer", il suffit de dupliquer ces lignes au gestionnaire correspondant, mais il faut s'assurer qu'il existe au moins une fenêtre i.e. que MDIChildCount n'est pas nul sinon MDIChildren[0] ne pointerait rien et il y aurait une erreur d'exécution si aucune fenêtre n'était active à ce moment-là. Cette précaution n'a pas été nécessaire dans la méthode précédente parce qu'elle est exécutée suite au clic du menu système de la fenêtre-enfant, il existe donc bien une fenêtre active au moment de l'exécution de la méthode. En revanche nous n'avons plus cette certitude dans une simple option du menu principal.

void __fastcall TForm1::FermerClick(TObject *Sender)
{
if(MDIChildCount)
{
int i=MDIChildren[0]->Tag;
delete F[i];
F[i]=NULL;
}
}

Les puristes remarqueront qu'on a dupliqué du code alors qu'une méthode peut en appeler une autre. C'est vrai donc on améliore, FormCloseE va simplement appeler FermerClick comme suit :

void __fastcall TForm1::FormCloseE(TObject *Sender,TCloseAction &Action)
{
FermerClick(Sender);
}

Si on choisit l'inverse c'est-à-dire FermerClick qui appelle FormCloseE, il faudrait envoyez à FormCloseE une variable de type TCloseAction, une variable et non une constante (e.g. caMinimize qui est d'ailleurs la valeur par défaut pour une fenêtre-enfant MDI, voir l'aide en ligne pour les différentes valeurs possibles de type TCloseAction) puisque sa déclaration dans FormCloseE se fait avec l'opérateur &. Si dans FermerClick on n'écrivait pas exemple FormCloseE(Sender, caMinimize), on aurait alors un warning du compilateur disant "un temporaire a été utilisé pour le paramètre Action". Pour éviter ce warning même s'il n'est pas fatal, il faudrait déclarer pour rien une variable par exemple A du type TCloseAction et écrire alors FormCloseE(Sender, A); c'est pour éviter ces difficultés d'ailleurs sans importance que nous avons opté pour l'appel inverse.

Il est bon de détruire par delete toutes les fenêtres-enfants, cela doit se faire à l'événement OnCloseQuery de Form1 (car Form1 doit encore exister pendant la destruction des fenêtres-enfants). Double-cliquez sur cet événement pour créer le gestionnaire d'événement et procédez comme suit :

void __fastcall TForm1::FormCloseQuery(TObject *Sender, bool &CanClose)
{
for(int i=0 ; i<MDIChildCount; i++) delete MDIChildren[i];
}

On utilise la variable gérée par C++Builder MDIChildCount qui représente le nombre de fenêtres-enfants ouvertes à ce moment-là. On aurait pu utiliser aussi notre tableau, par exemple :

void __fastcall TForm1::FormCloseQuery(TObject *Sender, bool &CanClose)
{
for(int i=0;i<NbFenMax;i++) delete F[i];
}

Faites F9 pour compiler-exécuter, vous constatez que vous pouvez ouvrir des fenêtres-enfants et qu'en cas de maximalisation, le menu système de la fenêtre vient bien se dessiner à droite du menu principal, ce qui est le comportement MDI type, et le nom de la fenêtre active vient s'inscrire sur la bordure de la fenêtre principale. Si vous préférez que les fenêtres enfants soient maximalisées dès la création, il suffit de rajouter (par exemple juste après F[i]->Tag=i;) dans la méthode TForm1::NouveauClick (voir ci-dessus) l'instruction :

F[i]->WindowState=wsMaximized;

La fenêtre enfant sera maximalisée dès sa création. Une propriété intéressante de la fenêtre principale est WindowMenu. Puisque nous avons un menu principal (quasi obligatoire rappelons-le pour les applications MDI), on aura comme dans Word une option "Fenêtre", en général placée tout à droite du menu, juste avant le point d'interrogation donnant accès à la rubrique d'aide. Cette option "Fenêtre" sera censée offrir diverses fonctions relatives à l'agencement des fenêtres. Si vous donnez à WindowMenu cette option "Fenêtre" (il suffit pour cela de la choisir dans le menu déroulant proposé) du menu principal, la gestion des fenêtres ouvertes se fera automatiquement c'est-à-dire qu'à l'exécution vous aurez la liste tenue à jour des fenêtres disponibles avec un petit signe indiquant la fenêtre active avec la possibilité d'en activer une autre puisque ces occurrences font partie du menu. C'est assez pratique puisque vous n'avez qu'à initialiser la propriété WindowMenu avec une option du menu principal pour que ça marche. C++Builder propose dans la liste déroulante associée à WindowMenu toutes les options du menu principal, mais en réalité seule une fonction visible à l'écran (c'est-à-dire première d'une colonne dans le constructeur de menu) fait fonctionner ce mécanisme.

Remarquons que dans notre exemple, aucune fenêtre-enfant n'est créée à l'entrée de notre application. Si vous désirez en créer une au tout début de l'exécution, le mieux est de disposer d'une "Liste d'actions" (Composant TActionList, voir alinéa suivant), de créer l'action Ouverture (sans catégorie précise) et d'initialiser la propriété Action de la fenêtre principale Form1 avec cette méthode. D'ailleurs, dès que vous avez une liste d'actions (c'est-à-dire dès que le composant TActionList fait partie de votre projet étant déposé dans la fenêtre principale), un menu déroulant automatique en regard de la propriété Action vous invite à sélectionner une action parmi la liste. Ainsi, une fenêtre-enfant sera ouverte dès l'entrée dans l'application. Voyons donc comment fonctionne ce composant très utile et très pratique dont la vocation est de centraliser les actions importantes d'une application.

58. Le composant TActionList

Ce composant se situe sous l'onglet "standard" de la palette de composants, il se trouve normalement en dernière position, on y voit un petit rectangle à l'intérieur duquel on lit OK et une petite flèche blanche déroulant un menu. Ce petit logo est très explicite, car si un bouton fait la même chose qu'une option du menu, on a tout intérêt à centraliser l'action dans une "liste d'actions". L'idée est la suivante. Pour un objet donné par exemple un bouton ou une option d'un menu, il existe une action particulière plus importante que les autres associée à cet objet, en général l'action en relation au clic de l'objet. C'est pourquoi pour chaque objet il existe la propriété "Action" qui est d'ailleurs la toute première étant donné que les propriétés sont données par ordre alphabétique dans l'inspecteur d'objets. Si vous disposez d'un composant TListAction et que vous y déclariez des actions, vous aurez alors à disposition un menu déroulant à cette propriété Action et il vous suffit de choisir une action pour l'associer à cet objet. Cela vous dispense d'avoir à écrire un gestionnaire d'événement notamment OnClick. Cela dit C++Builder vous confirme dans les événements que l'action est bien liée à OnClick. Bien entendu, si vous n'utilisez pas ce composant, vous pouvez aussi appeler une méthode, comme nous l'avons fait dans l'alinéa précédent, pour éviter de dupliquer du code, mais l'idée est plus de centraliser les actions. Si plusieurs objets font une certaine action, vous saurez que cette action se trouve dans la liste d'actions, vous n'aurez pas à chercher quelle méthode centralise ce code.

Ce composant est très simple à utiliser. On le dépose sur la fiche principale. Pour créer une action, on double-clique sur le composant ou bien on clique à droite en le pointant et on choisit "éditeur de liste d'actions". On tombe sur une petite fenêtre divisée en deux parties, une partie pour les catégories d'actions et l'autre pour le nom de ces actions. En effet, C++Builder a préparé pour vous des actions standard qu'on peut regrouper sous quatre catégories. Pour observer ces catégories, cliquez la petite flèche à côté de l'icône jaune (la bulle d'aide affiche "Nouvelle action") et choisissez "nouvelle action standard". Là, on vous affiche toutes les actions standard disponibles préfixées respectivement par TData, TEdit, THelp et TWindow donc des actions concernant respectivement les données, l'édition, l'aide et les fenêtres (à l'exception de la première qui est TAction et qui représente le type d'action neutre, sans catégorie). Annuler pour sortir de ce tableau puis cliquez sur l'icône jaune pour insérer une nouvelle action sans catégorie (ou bien appuyez sur la touche ins du clavier). On vous affiche "Action1" en regard de la catégorie "vide" (i.e. pas de catégorie particulière). Nous allons créer une action pour créer une fenêtre-enfant MDI, nous reprenons donc les éléments de l'alinéa précédent. Dans Name, nous donnons un nom à cette action par exemple "Ouvre" (nous ne choisissons pas "Nouveau», car on suppose que ce nom est déjà utilisé dans notre menu, voir alinéa précédent). Revenez à l'éditeur de liste d'actions et cliquez ce nouveau nom "Ouvre", C++Builder vous crée la méthode correspondant à cette action.

void __fastcall TForm1::OuvreExecute(TObject *Sender)
{

}

Là, vous écrivez le code correspondant à l'ouverture d'une fenêtre (voir méthode NouveauClick de l'alinéa précédent). Cette action est donc maintenant créée. Sélectionnons maintenant l'option "Nouveau" de notre menu, on initialise la propriété Action avec le nom de cette méthode à savoir OuvreExecute. Dans ce cas, vous n'avez pas de gestionnaire OnClick, cette déclaration en tient lieu. Si maintenant, un bouton quelconque exécute la même action, il suffira d'initialiser sa propriété Action avec la même valeur à savoir OuvreExecute, vous n'aurez rien d'autre à faire pour que ça marche immédiatement. Pour créer une fenêtre, nous sommes partis d'une action sans catégorie parce que dans la catégorie Window il n'y a pas cette possibilité. En revanche, pour sa fermeture, on choisira une action standard du type TWindowClose. Le procédé est le même que pour les actions sans catégorie. Ainsi dans l'alinéa précédent, la fenêtre-enfant était liée à la méthode FormCloseE associée à l'événement OnClose. Nous avions écrit le code de cette action dans la méthode FormCloseE, car nous ne disposions pas de liste d'actions. En disposant d'une liste d'actions, nous aurions créé une action de type TWindowClose que nous aurions appelée par exemple "Fermeture", en double-cliquant ce nom, C++Builder nous aurait créé la méthode et nous aurions écrit le code de fermeture d'une fenêtre-enfant comme précédemment :

void __fastcall TForm1::FermetureExecute(TObject *Sender)
{
if(MDIChildCount)
{
int i=MDIChildren[0]->Tag;
delete F[i];
F[i]=NULL;
EtatFen[i]=0;
}
}

Dans ces conditions, il nous suffit d'appeler cette action dans la méthode FormCloseE.

void __fastcall TForm1::FormCloseE(TObject *Sender,TCloseAction &Action)
{
FermetureExecute(Sender);
}

Notons au passage que nous n'aurions pas pu associer directement l'événement OnClose à FermetureExecute (c'est-à-dire écrire au moment de la création de la fenêtre-enfant F[i]->OnClose=FermetureExecute;) à cause de l'argument Action supplémentaire dans FormCloseE, les deux prototypes sont incompatibles. Un moindre mal consiste à appeler l'action de la liste comme proposé ci-dessus. En revanche, pour l'option "Fermer" de notre menu, on associe sa propriété Action avec FermetureExecute.

En résumé, quand vous développez avec une TActionList, vous créez d'abord votre action dans la liste d'actions et ce n'est qu'ensuite que vous associez cette action à la propriété Action d'un objet, en général plusieurs, car une même action s'effectue souvent par des chemins différents. Ainsi, toutes les actions de votre application sont centralisées et donc faciles à retrouver. Un autre avantage de passer par des listes d'actions est de bénéficier de comportements automatiques des composants sans aucune programmation supplémentaire. Par exemple, si un bouton est lié à une opération de fermeture de fenêtre-enfant dans une application MDI, il sera automatiquement désactivé s'il n'y a plus de fenêtre enfant présente dans la fenêtre-parent (i.e. si MDIChildCount est nul), l'option de menu qui fermerait une fenêtre-enfant serait, elle aussi, désactivée.

Remarquons pour terminer qu'on ne peut pas insérer ni ajouter d'actions logiciellement. Le mieux, pour simuler un tel effet, est d'insérer un certain nombre d'actions à la main sans rien faire d'autre, cela réserve l'espace de déclaration à ces actions. Par exemple, on initialise normalement la section commençante de la liste d'actions puis on en insère un certain nombre en plus à la suite sans rien faire d'autre. On repère l'indice à partir duquel commence la section de ces actions non encore initialisées sachant que les indices commencent à partir de zéro (il y a ActionList1->ActionCount actions dans la liste indicée de 0 à ActionList1->ActionCount-1). Maintenant, pour affecter l'action d'indice n à une méthode du programme, on écrira :

ActionList1->Actions[n]->OnExecute=Nom_de_la_fontion_a_executer;

Si l'on veut associer le clic d'un bouton à l'action d'indice n de la liste, on écrira :

Button->OnClick=ActionList1->Actions[n]->OnExecute;

59. La fonction GetCursorPos

Cette fonction renvoie les coordonnées réelles par rapport à l'écran, cela peut être utile dans certains cas, par exemple quand on veut déterminer la position d'un menu surgissant, lequel est fonction de la position d'une ToolBar déplacée. Il faut déclarer une variable P de type POINT, donner &P en argument (ce qui est logique puisque la fonction doit retourner le résultat) et interroger P.x et P.y (x et y en minuscule). Ci-après l'appel de GetCursorPos suite à un clic dans la fiche principale. Les coordonnées sont les coordonnées réelles par rapport à l'écran et non plus par rapport à la fiche.

void __fastcall TForm1::FormClick(TObject *Sender)
{
POINT P;
if(GetCursorPos(&P))
{
AnsiString A=IntToStr(P.x)+" "+IntToStr(P.y);
Application->MessageBox(A.c_str(),"OK",MB_OK);
}
}

60. Initialisation d'une application

En principe, ainsi que nous l'avons dit dans notre vue d'ensemble au début de ces "remarques", on initialise une application au moment du constructeur de la fiche principale TForm1::TForm1.

__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
/* initialisation ici de l'application */
}

Si, pour une raison ou une autre, vous devez prendre la main avant la construction de la classe TForm1, il faut alors utiliser Application->Initialize(). Entrez dans C++Builder et sauvez le projet vide unitxx et projetxx. Ensuite faites "Voir|Unités" (contrôle F12) et choisissez projetxx. C'est le programme maître créé par C++Builder lui-même. Vous constatez que la première instruction est Application->Initialize().

#include <vcl.h>
#pragma hdrstop
USERES("Project1.res");
USEFORM("Unit1.cpp", Form1);
//--------------------------------------------------------------------
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
Application->Initialize();
Application->CreateForm(__classid(TForm1), &Form1);
Application->Run();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
return 0;
}

En réalité, Application->Initialize() ne fait rien (car InitProc est initialisé à NULL par défaut), on peut même supprimer cette instruction pour le cas où l'on n'en a pas besoin. Si toutefois vous voulez prendre la main avant la construction de TForm1, il faut initialiser la variable InitProc avec le nom d'une fonction d'initialisation, c'est cette fonction qui sera exécutée. Vérifions. Nous allons initialiser InitProc avec la fonction InitAppli laquelle ne fait que nous afficher un message, ce qui nous prouvera que la fonction est effectivement exécutée.

#include <vcl.h>
#pragma hdrstop
USERES("Project10.res");
USEFORM("Unit10.cpp", Form1);
void InitAppli(void);
//------------------------------------------------------------------
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
InitProc=InitAppli;
Application->Initialize();
Application->CreateForm(__classid(TForm1), &Form1);
Application->Run();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
return 0;
}
//-----------------------------------------------------------
void InitAppli()
{
Application->MessageBox("Initialisation de l'application","OK",MB_OK);
}

61. Gestion des exceptions

Quand une instruction risque de se retrouver en erreur, on la protège par l'instruction try et on gère l'erreur par catch. Le programme maître type proposé par C++Builder (voir alinéa précédent) en est un exemple. Il faut juste remarquer que par défaut cette gestion ne fonctionne pas sous C++Builder c'est-à-dire finalement en mode debug mais seulement en mode réel i.e. quand vous cliquez directement l'exe et ce, parce C++Builder détourne les exceptions en premier. Si vous voulez quand même tester sous C++Builder le comportement de telles exceptions, il faut faire "Outils|Options du débogueur" et décocher la case "Arrêter sur exceptions C++" en bas du formulaire puis valider. Dans ces conditions, vous pouvez tester les exceptions.

Imaginons que l'on veuille tester la création d'une zone mémoire par new, on programmera ceci :

try
{
PtrFic=new char[Longueur];
}
catch (...)
{
Application->MessageBox("Problème d'allocation dynamique",NULL,MB_ICONERROR);
}

où Longueur est une variable ou constante arbitraire. Si l'allocation réussit, le programme continue sinon il passe par le gestionnaire inauguré par catch, car il y a une exception du type bad_alloc. Pour vérifier ce point, vous donnez à Longueur un nombre très grand, l'allocation sera alors refusée et vous constaterez qu'un message vous est affiché. Une fois ce test réalisé, vous n'avez plus qu'à recocher la case "Arrêter sur exceptions C++" pour continuer votre développement, car il est préférable de travailler en mode protégé.

62. La méthode StretchDraw

Il s'agit d'un draw dans un rectangle. La méthode Draw ne fait que recopier tel quel un dessin (e.g. un bitmap dans un canvas) alors que StretchDraw recopie dans un rectangle. Il en résulte que si le rectangle est de dimension identique au bitmap à recopier, StretchDraw équivaut alors à un draw simple. Mais si le rectangle est de dimension différente, le bitmap est alors plus grand ou plus petit, on programme facilement des zooms de cette façon.

Voici un petit exemple. Nous déclarons un bitmap Bmp à l'intérieur duquel nous écrivons le mot Essai. Nous déclarons un rectangle R que nous initialisons en faisant coïncider le point en haut à gauche avec celui en bas à droite, autant dire que ce rectangle est néant pour le moment. On met un bouton sur Form1. À chaque clic du bouton, nous allons d'une part, copier le bitmap par draw de manière à le faire apparaître tel quel et d'autre part, recopier ce bitmap par StretchDraw en augmentant ses coordonnées, on ajoute chaque fois la variable aug initialisée à 20. Vous constaterez que le bitmap grandit à chaque clic.

Pour réaliser ce test, il suffit donc de poser un bouton sur Form1 (de préférence vers le haut sur la droite car l'affichage a lieu vers la gauche dans ce petit exemple) et de copier-coller le code suivant dans l'unité cpp associée.

#include <vcl.h>
#pragma hdrstop

#include "Unit10.h"
//------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
Graphics::TBitmap *Bmp;
int aug=20;
TRect R;
//---------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
Bmp=new Graphics::TBitmap();
Bmp->Height=50;
Bmp->Width=50;
Bmp->Monochrome=true;
Bmp->Canvas->TextOutA(10,10,"Essai");

R.left=120;
R.top=20;
R.right=R.left;
R.bottom=R.top;
}
//---------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
delete Bmp;
}
//---------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
R.right+=aug;
R.bottom+=aug;
Form1->FormPaint(Sender);
}
//----------------------------------------------------------
void __fastcall TForm1::FormPaint(TObject *Sender)
{
Form1->Canvas->Draw(20,20,Bmp);
Form1->Canvas->StretchDraw(R,Bmp);
}

Remarquez que la méthode Button1Click appelle FormPaint. Ce petit programme ne fonctionnera pas immédiatement suite à ce copier-coller, car les méthodes Button1Click et FormPaint ne font encore pas partie de la classe TForm1 puisqu'on est supposé partir d'une application vierge. Pour que le programme fonctionne, il faut double-cliquer sur l'événement OnClick de Bouton1 (pour que C++Builder crée le gestionnaire) et idem sur l'événement OnPaint de Form1 puis vous effacez immédiatement ces deux gestionnaires dans le cpp (ou même à la limite vous ne faites rien, ces deux gestionnaires disparaîtront d'eux-mêmes à la prochaine compilation puisqu'ils sont vides, par exemple faites alt-F9 pour compiler uniquement l'unité, les deux gestionnaires créés disparaissent). En effet, le but n'est pas d'utiliser ces gestionnaires (car ils existent déjà dans le source copié-collé), mais de déclarer ces deux méthodes dans la classe TForm1 situées dans unitxx.h. D'ailleurs, si vous faites Ctrl-F6 (contrôle F6) pour consulter le source en-tête, vous constatez que les deux méthodes Button1Click et FormPaint ont bien été insérées dans la classe TForm1.

void __fastcall Button1Click(TObject *Sender);
void __fastcall FormPaint(TObject *Sender);

Cette petite astuce de manipulation ayant été réalisée, vous pouvez tester ce petit programme.

63. Les propriétés ClientWidth et ClientHeight

La zone client de la fenêtre principale (Form1 par défaut) est donnée par les propriétés ClientWidth et ClientHeight, donc Form1->ClientWidth sera la largeur de la fenêtre et Form1->ClientHeight sera sa hauteur en nombre de pixels. Mais on constate une petite déperdition en X d'environ 10 pixels et en Y de 30 pixels. Il faut donc en tenir compte notamment si vous voulez centrer une image ou encore adresser une zone centrale avec bordure. Si vous utilisez la fenêtre sans avoir à gérer de bordure, utilisez Form1->ClientWidth et Form1->ClientHeight sans vous préoccuper de cette petite zone non visible. Nous allons tester ce point en dessinant un cadre dans la forme et en adaptant la dimension de ce cadre en cas de redimensionnement de la fenêtre de manière à ce qu'il y ait toujours une bordure à gauche, à droite, en haut et en bas de 20 pixels. Par ce petit test, nous constatons qu'effectivement les coordonnées sont correctes à gauche et en haut, mais qu'il y a une déperdition d'environ 10 pixels à droite et de 30 en bas. Il s'agit de chiffres approximatifs pour obtenir un cadre toujours centré quelle que soit la dimension de la fenêtre. Entrez dans C++Builder et sauvez le projet (unitxx et projetxx). Comme nous allons travailler sur le canvas de Form1, il n'y a aucun composant à ajouter, il suffit de copier-coller le source de l'unité ci-dessous. Nous avons créé ici la méthode Dessine, il faut donc la rajouter dans la classe TForm1. Pour ce faire, allez sous éditeur de code où se trouve le source C++, faites ctrl-F6 (contrôle F6), cela provoque l'édition de l'en-tête de l'unité (si par exemple l'unité s'appelle unit17.cpp, vous aurez après ctrl-F6 le fichier unit17.h), dans la section "public", rajouter la déclaration void __fastcall Dessine(void); c'est une méthode de dessin, elle calcule les coordonnées du cadre à dessiner et à centrer.

#include <vcl.h>
#pragma hdrstop

#include "Unit17.h"
//-----------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
int Bord,FenW,FenH;
Graphics::TBitmap *BM;
const PerteX=10,PerteY=30;
//-------------------------------------------------------------
void __fastcall TForm1::Dessine()
{
TRect R;
R.left=0;
R.top=0;
R.right=FenW-1;
R.bottom=FenH-1;

BM->Width=FenW;
BM->Height=FenH;

BM->Canvas->FillRect(R);

BM->Canvas->MoveTo(Bord,Bord);
BM->Canvas->LineTo(FenW-Bord-PerteX,Bord);
BM->Canvas->LineTo(FenW-Bord-PerteX,FenH-Bord-PerteY);
BM->Canvas->LineTo(Bord,FenH-Bord-PerteY);
BM->Canvas->LineTo(Bord,Bord);

}
//-----------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
Bord=20;

BM=new Graphics::TBitmap();
BM->Monochrome=true;

FenW=Form1->ClientWidth;
FenH=Form1->ClientHeight;
}
//---------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
delete BM;
}
//--------------------------------------------------------
void __fastcall TForm1::FormPaint(TObject *Sender)
{
Form1->Canvas->Draw(0,0,BM);
}
//---------------------------------------------------------
void __fastcall TForm1::FormCanResize(TObject *Sender, int &NewWidth,
int &NewHeight, bool &Resize)
{
FenW=NewWidth;
FenH=NewHeight;
}
//----------------------------------------------------------
void __fastcall TForm1::FormResize(TObject *Sender)
{
Dessine();
FormPaint(Sender);
}

La variable Bord est initialisée dans le constructeur de TForm1 à 20. Vous pouvez modifier cette valeur, c'est en nombre de pixels la bordure à gauche, à droite, en haut et en bas. On utilise deux variables FenW et FenH qui sont les dimensions de la fenêtre principale. Dans le constructeur de TForm1, on initialise ces deux variables avec Form1->ClientWidth et Form1->ClientHeight, donc la zone client de la fenêtre telle que renseignée par C++Builder. Dans la méthode FormCanResize qui reçoit les nouvelles dimensions de la fenêtre, on se contente de renseigner les deux variables générales FenW et FenH qui contiennent donc toujours les dimensions de la fenêtre. Dans le constructeur de TForm1, on déclare par new un bitmap monochrome que pointe la variable générale BM. La méthode FormPaint ne fait que recopier le bitmap BM à partir du coin haut gauche i.e. à partir de la coordonnée (0,0). C'est la méthode FormResize qui seule appelle la méthode de dessin. Comme cette méthode est appelée au début de l'exécution du programme (ce qui est logique puisqu'on dimensionne la fenêtre pour la première fois), le dessin va être créé dans le bitmap BM puis on se contente d'appeler FormPaint(Sender); ce qui revient à forcer un événement OnPaint. La méthode de dessin commence par dimensionner dans une variable R les coordonnées d'un rectangle correspondant aux dimensions de la fenêtre via les variables FenW et FenH. Le bitmap prend ces valeurs, on redimensionne donc à chaque fois le bitmap aux dimensions de la zone client de la fenêtre. On efface le bitmap par l'instruction BM->Canvas->FillRect(R); où R est notre rectangle correspondant à la zone client de la fenêtre. Puis on dessine dans le bitmap le cadre centré :

BM->Canvas->MoveTo(Bord,Bord);
BM->Canvas->LineTo(FenW-Bord-PerteX,Bord);
BM->Canvas->LineTo(FenW-Bord-PerteX,FenH-Bord-PerteY);
BM->Canvas->LineTo(Bord,FenH-Bord-PerteY);
BM->Canvas->LineTo(Bord,Bord);

Vous remarquez qu'on utilise deux constantes PerteX et PerteY qu'on a initialisé respectivement à 10 et 30 et qui représente ce qu'on perd respectivement à droite et en bas. Ce réajustement est nécessaire pour que notre cadre soit centré. Ainsi la coordonnée maximale en X est FenW-Bord-PerteX et en Y FenH-Bord-PerteY. Quant à la méthode FormDestroy, elle restitue la mémoire allouée pour le bitmap BM.

64. Création d'un exécutable indépendant

Si vous essayez de faire tourner un exécutable créé par C++Builder sur un autre ordinateur qui n'a pas C++Builder, il risque de manquer un certain nombre d'éléments notamment des DLL. Tant qu'on n'utilise le programme ou qu'on le développe sur son propre ordinateur, la question ne se pose pas, car tous les fichiers nécessaires à l'exécution sont présents, mais dès qu'un programme est susceptible d'être exécuté sur un ordinateur distant (par exemple suite à un téléchargement sur Internet), il faut alors rendre l'exécutable totalement autonome, il faut que l'EXE soit autosuffisant. Pour cela, deux précautions sont à prendre, d'une part ne pas utiliser la RTL dynamique et d'autre part ne pas construire l'exécutable avec les paquets d'exécution. Donc, entrez dans C++Builder et chargez votre application que vous voulez rendre indépendante, faites "Projets|Options" (c'est tout en bas du menu déroulant). Puis sélectionnez l'onglet "lieur" et décochez la case "utilisez la RTL dynamique". Le mieux serait aussi de décocher les cases "informations de débogage" et "bibliothèque de débogage" puisque le programme est censé maintenant être au point. Ensuite sélectionnez l'onglet "paquets" et décochez la case "construire avec les paquets d'exécution". Le mieux serait aussi de sélectionner l'option "compilateur" et de cliquer sur la case "Finale" indiquant par là que c'est une version finale de votre programme. Mais les deux choses les plus importantes concernent la RTL et les paquets.