Remarques de développement avec C++ Builder 5
Partie III - par Gilles
Louise
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
Retour à la partie I
Retour à la partie II 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 seront acceptés 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é "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 réouvrir 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 à réouvrir, 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 s'il 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 ayez 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 5ème 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 4ème
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 élévé */
EtudeMaxFic();
/* On peut maintenant insérer ce nouveau nom, on est sûr ici
de ne jamais dépasser la 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 élévé, 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 click
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 click
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 pourquoi 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 le 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 condition, 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 click, 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ée 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 la 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 associez
à 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
(click à 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 n'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
la 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 click sur Bouton1, nous allons supprimer
l'onglet actif à chaque click 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 soit
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és 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 click 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 click 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 rappelez, 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
suspensions, 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 évitez les complications inutiles, je vous
conseille d'interdire la sortie du toolbar pour des pseudo-menus
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 click à 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 click 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|Editeurs 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|Editeurs
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 extraitre 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é, 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 click 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 é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 soit 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 click 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 s'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ées 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 click 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 click 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 click 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 click.
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 à 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 la 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 debogage" 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.
|