Programmation objet

Eric Thirion

Dans le premier cours sur la programmation objet, vous apprendrez à utiliser du code existant écrit en programmation objet. Ou, autrement dit, à utiliser des classes existantes.

Par exemple, la classe TStringList, qui sert à représenter des listes de chaînes de caractères, mais surtout les classes utilisées pour réaliser l'interface graphique d'une application.

Vous apprendrez (enfin !) à lire et afficher des informations sans utiliser la librairie entrees_sorties et vous comprendrez (enfin !) le sens du code généré automatiquement par Lazarus dans l'unité associée à un formulaire.

Dans le deuxième cours, vous apprendrez à définir vous-même de nouvelles classes, à écrire des méthodes ou à définir des relations d'héritage entre classes.

Nous aborderons également de nouvelles notions de programmation objet :

- l'encapsulation : une méthodologie de programmation qui facilite la mise à jour des programmes écrits sous forme de classes ;

- les propriétés : une approche particulière de l'encapsulation de Pascal Objet. Nous verrons en particulier que certains attributs graphiques de Lazarus (comme l'attribut .'text' des zones de texte) sont en réalité des propriétés et que cela permet d'expliquer comment la modification de la valeur de ces attributs se répercute immédiatement à l'écran ;

- la surcharge ou comment redéfinir spécifiquement une méthode pour une sous-classe en récupérant le code de la classe mère ;

- le polymorphisme : comment faire passer un objet d'une classe donnée comme un objet d'une sous-classe de celle-ci.

Commentez

- Titre : Programmation objet

- Auteur : Eric Thirion

- Parution : 15 juillet 2014

- Mise à jour : 30 octobre 2014

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2014 Eric Thirion. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Utilisation de classes et interfaces graphiques

- Prérequis

- Utilisation de classes

- Notion de classe

- Exemple de la classe TStringList

- Objet ou instance d'une classe

- Accès aux attributs

- Application de méthodes

- Interfaces graphiques

- Principes de réalisation d'une interface graphique

- Utilisation de l'E.D.I

- Les onglets de composants de Lazarus

- Les composants en tant qu'objets

- Quelques rappels

- Déclaration de la classe TForm1 et de l'objet Form1

- Accès aux attributs d'un composant

- Notion d'héritage

- La classe TForm

- Composants abstraits

- La classe TControl

- La classe TComponent

- La classe TObject

- Les procédures évènementielles en tant que méthodes

- Préfixage des procédures évènementielles par TForm1

- En-têtes des procédures évènementielles à l'intérieur de la classe TForm1

- Préfixage des composants par Form1

- Signification du paramètre Sender

- Les évènements

- Utilisation de l'onglet évènement

- Pour connaître les évènements associés à un composant

- Pour associer un gestionnaire d'évènement à un composant

- Quelques évènements utiles

- Exemple

- Utilisation de EditingDone

- Clic sur une étiquette

- Utilisation de Resize

- Utilisation de DblClick, CloseQuery

- Composants standard de Lazarus

- Les zones de texte (classe TEdit)

- Afficher dans une zone de texte

- Lire une variable depuis une zone de texte

- Effacer une zone de texte

- Exemple

- La classe TStrings

- Les zones de liste (classe TListBox)

- Exemple

- Les combobox (classe TComboBox)

- Exemple

- Les cases à cocher (classe TCheckBox)

- Exemple

- Les boutons radio (classe TRadioButton)

- Exemple

- Les mémos (classe TMemo)

- Exemple

- Les menus (classes TMainMenu et TMenuItem)

- Exemple

- Présentation de l'interface graphique

- Utilisation de l'éditeur de menu

- Gestionnaires d'évènements des menus

- Boîtes de dialogues de Lazarus

- Ouverture de fichier (classe TOpenDialog)

- Enregistrement de fichier (classe TSaveDialog)

- Ouverture de fichier image (classe TOpenPictureDialog)

- Choix d'une couleur (classe TColorDialog)

- Choix d'une police (classe TFontDialog)

- Confirmation d'une action

- Exemple

- Ouverture de fichier

- Enregistrement de fichier

- Choix de police

- Choix de couleur

- Composants divers

- Composant image (classe TImage)

- L'attribut Picture

- Attributs définissant la manière d'afficher l'image

- Exemple

- Liste de fichiers (classe TFileListBox)

- Exemple

- Multifenêtrage

- Adjonction de nouvelles fenêtres

- Définition de la fenêtre principale

- Renommage des unités et des fenêtres

- Nommage des unités

- Nommage des fenêtres

- Importation des unités

- Code de l'exemple

- Exercices

- Note de la rédaction de Developpez.com

- Notions de programmation objet

- Prérequis

- Définition basique d'une classe

- Présentation du projet

- Définition de la classe et de ses méthodes

- Interface de la classe Voiture

- Implémentation de la classe Voiture

- En-têtes des méthodes

- Corps des méthodes

- Utilisation des méthodes de la classe

- Le bouton Nouvelle Voiture

- Le bouton Rouler

- Encapsulation

- Visibilité des membres d'une classe

- Visibilité des membres en Pascal Objet

- Présentation de l'exemple

- Déclaration de la classe

- Utilisation interdite de la classe

- Utilisation autorisée de la classe

- Les propriétés

- Utilité des propriétés

- Déclaration des propriétés

- Déclaration des propriétés simples

- Déclaration des propriétés tableaux

- Exemple de propriété simple

- Exemple de propriété tableau

- Déclaration et utilisation de la propriété En_Stock

- Interface de la classe Stock

- Implémentation des accesseurs de la méthode En_Stock

- Utilisation de la classe Stock

- Affichage du stock

- Propriétés graphiques

- Héritage, surcharge et polymorphisme

- Présentation de l'exemple

- Interfaces des deux classes

- Implémentation de la classe Forme

- Implémentation de la classe Cercle

- Utilisation des deux classes

- Affichage d'une forme

- Création d'un cercle

- Exercices et exemples

- Note de la rédaction de Developpez.com

Utilisation de classes et interfaces graphiques

Prérequis

Les bases de la programmation doivent être bien assimilées. En particulier, le cours sur les types structurés, puisque les objets sont en quelque sorte une généralisation de ceux-ci.

Utilisation de classes

Notion de classe

Une classe est une manière de définir un type qui peut être vue comme l'association de sous-programmes à un type structuré.

Tout comme un type structuré, une classe possède des attributs, qui peuvent être de type quelconque.

La nouveauté par rapport à un type structuré est l'intégration des sous-programmes, appelés méthodes. Ils représentent les opérations que l'on peut effectuer sur un type de données.

L'idée sous-jacente, à la base de la programmation objet (programmation utilisant des classes) est de voir des données associées à des opérations (les méthodes).

Comme un type structuré, une classe doit en principe être déclarée avant de pouvoir être utilisée. Mais dans ce premier cours sur la programmation objet, nous nous limiterons à l'utilisation de classes prédéfinies et donc intégrées au langage de programmation. Par conséquent, la déclaration d'une classe ne sera pas abordée dans ce cours.

Exemple de la classe TStringList

Pour introduire la notion de classe, nous prendrons comme exemple la classe TStringList du Pascal, qui permet de manipuler des listes de chaînes de caractères.

Cette classe possède (entre autres) deux attributs nommés Count et Strings et quatre méthodes nommées Clear, Add, IndexOf et LoadFromFile :

- Count est un attribut de type entier destiné à contenir le nombre d'éléments de la liste ;

- l'attribut Strings quant à lui contient la liste des chaînes de caractères ;

- la méthode Clear est une procédure sans paramètre qui permet de vider une liste ;

- la méthode Add est une procédure à un paramètre (de type String), qui permet d'ajouter une chaîne de caractères à une liste ;

- la méthode IndexOf est une fonction à un paramètre de type String. Elle retourne la position d'une chaîne de caractères dans la liste. La position de la première chaîne est 0, celle de la deuxième 1, etc. Si la chaîne n'est pas dans la liste, IndexOf retourne -1 ;

- la méthode LoadFromFile est une procédure à un paramètre de type String représentant le nom d'un fichier. Elle remplit la liste avec les lignes contenues dans ce fichier. C'est donc un moyen pratique pour initialiser la liste des chaînes de caractères contenues dans un objet de la classe TStringList.

Objet ou instance d'une classe

À partir du moment où une classe est définie, il est possible de créer des instances de cette classe ou, autrement dit, des objets.

Par comparaison avec un type structuré, la création d'une instance peut être comparée à la déclaration d'une variable de type structuré.

Il y a pourtant une différence importante avec la déclaration d'une variable de type structuré : la déclaration d'un objet n'alloue pas d'espace mémoire pour cet objet, mais pour un pointeur sur cet objet.

Prenons un exemple. Le code suivant ressemble à la déclaration d'une variable de type TStringList :

var Mayonnaise: TStringList;

En réalité, cette ligne de code déclare l'adresse d'un objet (encore inexistant à ce stade) de la classe TStringList. Pour l'instant, cet objet n'existe pas et par conséquent, son adresse est indéfinie.

Pour créer un objet, une deuxième opération appelé instanciation est nécessaire.

En Pascal, l'instanciation se fait par l'intermédiaire d'une méthode spéciale nommée Create, définie pour toutes les classes.

Voici, par exemple, l'instanciation d'un objet de la classe TStringList :

Mayonnaise := TStringList.Create ();

C'est uniquement lors de l'instanciation :

- que la place mémoire est allouée à un objet. Il s'agit plus précisément de la place mémoire nécessaire à stocker les attributs de cet objet ;

- que l'adresse de l'objet est définie. En effet, la méthode Create retourne l'adresse de la zone mémoire contenant les données de l'objet.

Les objets sont stockés dans une zone spéciale de la mémoire appelée le tas (heap en anglais), utilisée de manière plus générale pour l'allocation dynamique de mémoire.

Accès aux attributs

L'accès aux attributs d'un objet se fait de la même manière que l'accès aux champs d'une structure. On utilise le nom de l'objet, suivi d'un point, suivi du nom de l'attribut.

La définition des attributs d'une classe offre toutefois des possibilités supplémentaires, que nous verrons essentiellement dans la deuxième partie du cours. Mais certaines de ces possibilités apparaissent déjà dans la classe TStringList :

- l'attribut count est en lecture seule, c'est-à-dire que l'affectation d'une valeur à cet attribut provoquera une erreur ;

- l'attribut Strings est l'attribut par défaut de la classe. On peut donc écrire Mayonnaise[1] au lieu de Mayonnaise.Strings[1] pour désigner le deuxième élément de la liste Mayonnaise.

La notation Strings[i] peut faire croire que Strings est un tableau. Si c'était le cas, il aurait été nécessaire de définir sa dimension. En réalité, Strings est représenté par une liste utilisant l'allocation dynamique de mémoire (donc stockée dans le tas). Sa dimension peut donc varier pendant l'exécution du programme. La taille de cette liste est uniquement limitée par l'espace mémoire disponible dans le tas.

Application de méthodes

Bien que les méthodes soient des sous-programmes, l'appel d'une méthode ne s'écrit pas comme l'appel d'un sous-programme.

Un des paramètres d'une méthode est forcément un objet de la classe pour laquelle cette méthode a été définie.

Par exemple, toute méthode de la classe TStringList a un paramètre de type TStringList ou, autrement dit, un objet de cette classe.

Mais dans l'écriture de l'appel de la méthode, cet objet ne figurera pas dans les parenthèses, mais en dehors, à gauche du nom de la méthode et séparé par un point. Cette écriture est l'application d'une méthode à un objet.

Voici, par exemple, l'application de la méthode Add (adjonction d'une chaîne) à un objet de la classe TStringList :

Mayonnaise.Add('Oeuf');

Toute méthode s'applique donc nécessairement à un objet de sa classe.

Une autre différence importante avec les sous-programmes se situe au niveau du nom. Le nom d'un sous-programme normal le définit de manière unique. Ce n'est pas vrai pour une méthode.

En effet, deux classes distinctes peuvent avoir des méthodes de même nom qui ne font pas du tout la même chose !

On pourrait, par exemple, imaginer une classe Date avec une méthode Add permettant d'ajouter un certain nombre de jours à une date. L'application de cette méthode à un objet D de la classe Date pourrait par exemple s'écrire :

D.Add(5);

Ce qui aurait comme effet d'ajouter cinq jours à la date D.

Mais dans ce cas, comment le compilateur peut-il distinguer les deux méthodes ?

L'ambiguïté est résolue grâce à la nature de l'objet à laquelle est appliquée la méthode. S'il s'agit d'un objet de la classe TStringList, le compilateur en déduira qu'il s'agit de la méthode Add de la classe TStringList. S'il s'agit d'un objet de la classe Date, il en déduira qu'il s'agit de la méthode Add de la classe Date.

Interfaces graphiques

Principes de réalisation d'une interface graphique

Depuis l'apparition des écrans graphiques, de la souris, du multifenêtrage, la majorité des applications possèdent une interface graphique.

Elle se présente sous la forme d'une ou plusieurs fenêtrescomposants sur lesquelles apparaissent différents (boutons, menus, zones de liste, case à cocher, etc.) sur lesquels l'utilisateur peut agir afin d'obtenir des informations ou bien d'en donner à l'ordinateur.

La conception de l'interface graphique d'une application est donc aujourd'hui une partie très importante du développement d'application.

Utilisation de l'E.D.I

Avec un E.D.I. (comme Lazarus, Visual Basic, Visual C++), la réalisation d'une interface graphique se décompose en deux parties :

- la réalisation du rendu visuel de l'interface. Cette partie ne nécessite aucune programmation. Le développeur sélectionne les composants qu'il veut utiliser dans des palettes de composants et les dépose sur la fenêtre. Il peut ensuite modifier leurs propriétés (position, dimension, couleur, par exemple) ;

- l'écriture des gestionnaires d'évènements ou, autrement dit, la programmation évènementielle. C'est ce qui permettra de rendre l'interface fonctionnelle. C'est ici que des notions de programmation objet vont intervenir.

Les onglets de composants de Lazarus

Lazarus offre un très grand choix de composants graphiques répartis sous 14 onglets :

Par manque de temps, je n'ai pu en présenter qu'une petite partie, principalement ceux de l'onglet Standard et Dialogs (boîtes de dialogue), ainsi que quelques composants des onglets Additional, Common Controls et Misc.

Les onglets Data Controls , Data Access et SQLdb sont spécialisés dans l'accès aux bases de données. Nous en reparlerons dans le cours consacré à l'accès aux bases de données par programmation.

Les composants en tant qu'objets

Quelques rappels

Dans le premier cours (Premières notions), nous avions déjà un peu abordé la création d'interface graphique sous Lazarus.

Dans l'exercice de prise en main de Lazarus, nous avions créé l'interface graphique d'un programme permettant d'additionner deux nombres. Cette interface ne comportait que trois types de composants : les étiquettes, les zones de texte et les boutons.

Pour ajouter un de ces composants à l'interface, il vous suffisait de le sélectionner dans la palette des composants standard :

Rappelez-vous que Lazarus génère un nom par défaut pour chaque nouveau composant déposé sur le formulaire :

et que les noms de ces composants apparaissent dans le code source :

Déclaration de la classe TForm1 et de l'objet Form1

La partie du code représentée ci-dessus est la déclaration d'une classe. Plus précisément, c'est la déclaration de la classe TForm1 qui représente le formulaire de votre application. Le nom TForm qui figure entre parenthèses est le nom d'une autre classe. Il s'agit d'une classe prédéfinie de Lazarus sur laquelle nous reviendrons ultérieurement.

Le texte en blanc est la définition des attributs de la classe TForm1. On voit donc que les attributs de cette classe sont les composants déposés sur le formulaire : les attributs Label1, Label2, Label3 de type TLabel sont les trois étiquettes, les attributs Edit1, Edit2, Edit3 de type TEdit sont les trois zones de texte et enfin l'attribut Button1 de type TButton est le bouton.

En fait, TLabel, TEdit et Tbutton ne sont pas réellement des types, mais des classes prédéfinies de Lazarus. Ainsi, TButton est la classe représentant les boutons, TLabel celle des étiquettes et TEdit, celle des zones de texte.

De manière générale, tous les composants déposés sur l'interface sont des objets appartenant à différentes classes.

Un peu plus bas dans le code, figure la déclaration d'une variable globale nommée Form1 (à moins que cette variable ait été renommée, mais nous supposerons ici pour simplifier les explications que ce n'est pas le cas) :

Avec ce que nous avons vu dans la première partie de ce cours, on comprend qu'il s'agit là de la déclaration d'un objet de la classe TForm1. Le formulaire de l'application est donc également un objet : c'est l'objet Form1 de la classe TForm1. Cette instance de la classe TForm1 est créée automatiquement au démarrage de l'application.

Les composants du formulaire sont donc des attributs de cet objet. Cela explique, par exemple, pourquoi l'écriture Form1.Edit1 peut être utilisée pour désigner la zone de texte Edit1 du formulaire Form1.

Accès aux attributs d'un composant

Dès qu'un composant a été déposé sur le formulaire, l'inspecteur d'objet permet de modifier ses propriétés. Il suffit pour cela de sélectionner ce composant, puis de sélectionner la propriété voulue dans l'inspecteur d'objet et de modifier la valeur de cette propriété.

La figure suivante illustre, par exemple, la modification de la propriété Caption du bouton du projet Addition :

Les propriétés que l'on modifie ainsi sont en réalité des attributs de l'objet qui représente ce composant. On peut donc également y accéder par programmation. Par exemple, l'instruction :

Form1.BoutonAdditionner.Caption := 'Additionner';

a le même effet que la modification de propriété illustrée par la figure précédente.

Notion d'héritage

Pour aller un peu plus loin dans la représentation d'une interface graphique sous Lazarus, il nous faut introduire une autre notion fondamentale de la programmation objet : il s'agit de la notion d'héritage.

L'héritage est un mécanisme qui a été introduit dans la programmation objet dans le but d'éviter la réécriture inutile de code lorsqu'une classe n'est qu'un cas spécial d'une autre classe :

La classe plus spécialisée est appelée sous-classe et la classe plus générale, classe mère.

Lorsqu'une relation d'héritage est définie entre deux classes, la sous-classe hérite de tous les attributs et méthodes de la classe mère. C'est-à-dire que tout se passe comme si les attributs et méthodes de la classe mère avaient été explicitement définis pour la sous-classe.

Une sous-classe d'une classe donnée représente donc une sorte de cas particulier de cette classe qui possède des attributs et méthodes supplémentaires et spécifiques.

La classe TForm

Revenons un instant sur la déclaration de la classe TForm1 du projet Addition :

et plus précisément sur la première ligne de cette déclaration de classe :

TForm1 = class (TForm)

Cette ligne de code définit la classe TForm1 comme une sous-classe de TForm.

TForm est une classe prédéfinie de Lazarus qui représente l'interface graphique par défaut, c'est-à-dire un formulaire sans composant. Cette classe possède déjà un certain nombre de méthodes et d'attributs. Par exemple, la méthode Close, qui permet de fermer le formulaire, l'attribut Caption qui permet de lui donner un titre.

Comme TForm1 est une sous-classe de TForm, elle hérite de tous les attributs et de toutes les méthodes de la classe TForm. Vous pouvez donc accéder au titre de votre formulaire par Form1.Caption et le fermer en lui appliquant la méthode Close.

Les attributs spécifiques de TForm1 sont les composants qui ont été déposés sur le formulaire. Ses méthodes spécifiques sont les procédures évènementielles associées à ces composants.

Composants abstraits

Tout comme TForm1, les composants de l'interface ont des classes mères qui représentent en quelque sorte des « composants abstraits ». La figure suivante montre l'arborescence (très simplifiée) des classes Lazarus intervenant dans la représentation d'une interface graphique :

La classe TControl

On voit que de nombreux composants graphiques héritent de la classe TControl. C'est, par exemple, le cas des composants que vous connaissez déjà : les zones de texte (classe TEdit), les zones de liste (classe TListBox), les étiquettes (classe TLabel) ainsi que les boutons (classe TButton) sont des sous-classes de TControl. De plus, le formulaire lui-même est un objet de la classe TControl, car TForm1 est une sous-classe TForm, qui est elle-même une sous-classe de TControl.

Il est donc utile de connaître les méthodes et attributs de cette classe. En voici quelques-uns :

La classe TComponent

La classe mère de tous les composants graphiques est TComponent. L'attribut Name de cette classe contient le nom du composant. Tout composant graphique possède donc cet attribut.

La classe TObject

Pour terminer, TComponent est une sous-classe de TObject, la classe mère de toutes les classes en Pascal Objet. Elle possède une méthode ClassName, qui retourne le nom de la classe à laquelle appartient un objet. Cette méthode existe par conséquent pour toutes les classes.

Les procédures évènementielles en tant que méthodes

Nous allons à présent éclaircir quelques points concernant les procédures évènementielles.

Pour cela, reprenons l'exemple du projet d'addition. Lorsque vous double-cliquez pour la première fois sur le bouton d'addition, Lazarus génère automatiquement la déclaration de la procédure évènementielle associée à ce bouton dans l'unité du formulaire. Voici ce code :

Dans ce code, il y a deux choses que nous n'avons jamais expliquées : pourquoi le nom de la procédure est-il préfixé par TForm1. ? Que signifie le paramètre Sender de type TObject ?

D'autre part, rappelez-vous que lorsque vous voulez accéder à un composant de l'interface dans une procédure non évènementielle, il faut préfixer le nom du composant par Form1.. Dans une procédure évènementielle, ce n'est pas nécessaire. Pour quelle raison ?

Enfin, vous avez sans doute remarqué que les en-têtes des procédures évènementielles sont automatiquement ajoutés à l'intérieur de la déclaration de la classe TForm1. Par exemple, dans le projet Addition, le double-clic sur le bouton Additionner génère également le code suivant (en blanc) :

On y retrouve donc l'en-tête de la procédure évènementielle. Pourquoi cela ?

Les réponses à toutes ces questions se trouvent dans la programmation objet et, plus précisément, dans la manière dont la programmation objet est utilisée dans Lazarus pour représenter des interfaces graphiques. Reprenons chacune de ces questions.

Préfixage des procédures évènementielles par TForm1

Dans les en-têtes des procédures évènementielles, le nom de la procédure est toujours préfixé par TForm1.. Pour quelle raison ?

Le préfixage des procédures évènementielles par TForm1 s'explique par le fait que les procédures évènementielles sont des méthodes de la classe TForm1. Or, lorsque l'on déclare une méthode en Lazarus, il faut obligatoirement préfixer le nom de la procédure par le nom de la classe (nous reviendrons là-dessus plus en détail dans le deuxième cours de programmation objet).

En-têtes des procédures évènementielles à l'intérieur de la classe TForm1

Pour quelle raison les en-têtes des procédures évènementielles apparaissent-elles à l'intérieur de la déclaration de la classe Tform1 ?

L'explication est encore une fois dans la manière de déclarer une méthode d'une classe : en Pascal Objet, les en-têtes des méthodes d'une classe doivent forcément figurer à l'intérieur de la déclaration de la classe.

Préfixage des composants par Form1

Dans une procédure non évènementielle, les composants doivent être préfixés par Form1., mais ce n'est pas nécessaire dans une procédure évènementielle. Pour quelle raison ?

Rappelons-nous que les procédures évènementielles sont des méthodes de la classe TForm1 et que les composants sont des attributs de cette classe.

Or, à l'intérieur d'une méthode d'une certaine classe, un nom d'attribut de cette classe désigne implicitement l'attribut de l'objet auquel il sera appliqué au moment de son exécution.

Comme les procédures évènementielles sont des méthodes de la classe TForm1, que les composants sont des attributs de cette classe et que les procédures évènementielles sont forcément appliquées à l'objet Form1 de cette classe, tout nom de composant apparaissant à l'intérieur d'une procédure évènementielle est automatiquement interprété comme un attribut de l'objet Form1. À l'intérieur d'une procédure évènementielle, le préfixage des composants par Form1. est donc inutile.

Par exemple, à l'intérieur de la procédure évènementielle Button1Click du projet Addition, Edit1 désigne implicitement la zone de texte Edit1 de l'objet Form1, car Edit1 est un attribut de la classe TForm1.

D'un autre côté, dans une procédure non évènementielle, le préfixage des composants est obligatoire car, tout simplement, les procédures non évènementielles ne sont pas des méthodes de la classe TForm1 (à moins, bien sûr, de les déclarer en tant que telles).

Signification du paramètre Sender

Dans l'en-tête d'une procédure évènementielle figure toujours un mystérieux paramètre de type TObject nommé Sender. Que représente ce paramètre ?

Le paramètre Sender contient l'adresse du composant qui a déclenché l'évènement. Par exemple, dans la procédure Button1Click :

Sender contient l'adresse de l'objet Button1.

On peut se demander quel est l'intérêt de cela, étant donné que l'on peut tout simplement accéder à cet objet en utilisant son nom.

En fait, l'utilité du paramètre Sender apparaît lorsque l'on cherche à réutiliser la même procédure évènementielle pour plusieurs composants de l'interface.

Il est en effet possible, via l'inspecteur d'objet, d'associer la même procédure évènementielle à plusieurs composants. Ceux-ci peuvent même être de types différents (d'où l'intérêt de déclarer Sender comme un objet de la classe Tobject) !

Pour vous en convaincre, ouvrez ProjetSender dans le dossier exemple. Le formulaire contient un bouton et une case à cocher. Ces deux composants sont associés à la même procédure évènementielle qui affiche le nom du composant sur lequel l'utilisateur a cliqué.

Le code de la procédure évènementielle utilisé par les deux composants est le suivant :

procedure TForm1.Button1Click(Sender: TObject);
begin
ShowMessage ('Vous avez cliqué sur le composant '
+(Sender As TComponent).Name);
end;

Ce code utilise le polymorphisme, une notion de programmation objet sur laquelle nous reviendrons dans le cours suivant.

Les évènements

Dans le cours d'introduction à la programmation, nous avons vu qu'un évènement est le résultat d'une action de l'utilisateur sur l'interface graphique d'un programme. Toute action sur le clavier ou la souris provoque une interruption, qui est d'abord capturée par le système d'exploitation. Si le curseur de la souris est à ce moment-là situé au-dessus de la fenêtre d'une application, le système va alors lui retransmettre cette interruption.

Un évènement sera alors généré au sein de l'application.

Jusqu'ici, le seul type d'évènement que nous avons considéré est le clic sur un bouton. Il existe en réalité de nombreux autres types d'évènements et, de plus, ils peuvent être associés à n'importe quel type de composant.

Dans cette partie du cours, nous allons voir de nouveaux types d'évènements et comment leur associer un gestionnaire d'évènement ou, autrement dit, une procédure évènementielle.

Utilisation de l'onglet évènement

Pour connaître les évènements associés à un composant

Dans Lazarus, tout composant graphique peut intercepter des évènements de différents types. Pour les connaître, il suffit de cliquer sur le composant, puis sur l'onglet Évènement de l'inspecteur d'objet.

Voici, par exemple, l'onglet évènement d'un bouton :

Celui d'une zone de texte :

Celui d'une étiquette :

Et d'un formulaire (les 21 premiers puis les 18 suivants) :

Chaque ligne de l'onglet Évènement correspond à un attribut de la classe représentant le composant. Les noms des attributs sont dans la colonne de gauche et leur valeur respective dans la colonne de droite.

Tous les attributs commençant par On servent à stocker un pointeur vers un gestionnaire d'évènement. Le nom de l'évènement en question est défini par la chaîne de caractères qui suit On.

Reprenons, par exemple, l'onglet Évènement du bouton Additionner :

OnClick représente ici un attribut de la classe TButton (classe représentant les boutons). La valeur de cet attribut est BoutonAdditionnerClick. Il pointe donc vers l'adresse en mémoire de la procédure évènementielle BoutonAdditionnerClick.

Notez également que Lazarus génère un nom par défaut pour le gestionnaire d'évènement. Il est formé du nom du composant, suivi du nom de l'évènement.

En comparant les exemples d'onglets Évènement précédents, on constate que certains évènements peuvent se retrouver dans plusieurs composants. C'est le cas, par exemple, de l'évènement OnClick, que l'on retrouve aussi bien dans l'onglet Évènement d'un bouton que dans celui d'une étiquette, d'une zone de texte ou d'un formulaire (voir les illustrations ci-dessus).

D'autres, par contre, sont spécifiques à certains composants. L'évènement OnEditingDone, par exemple, existe pour les zones de texte mais pas pour les boutons, ni les formulaires, ni les étiquettes.

Pour associer un gestionnaire d'évènement à un composant

Chaque composant possède un évènement par défaut. Par exemple, Click pour un bouton ou une étiquette, Change pour une zone de texte et Create pour un formulaire.

Lorsque vous double-cliquez sur un composant, Lazarus considère que vous voulez écrire le gestionnaire d'évènement pour l'évènement par défaut de ce composant. Il va donc automatiquement générer une procédure évènementielle de la forme suivante dans le code de l'application :

procedure TForm1.XY(Sender: TObject);
begin
end;

où X est le nom du composant et Y le nom de l'évènement par défaut de ce composant. D'autre part, cette procédure évènementielle sera déclarée en tant que méthode de la classe TForm1.

Par exemple, avec l'interface graphique suivante :

un double-clic sur le bouton Additionner (dont le nom interne est BoutonAdditionner) génère automatiquement la procédure évènementielle suivante :

procedure TForm1.BoutonAdditionnerClick(Sender: TObject);
begin

end;

car l'évènement Click est l'évènement par défaut des boutons.

Lorsque l'on affiche l'onglet Évènement de ce bouton, on constate que la procédure évènementielle est bien enregistrée dans l'attribut OnClick de cet objet :

Comment fait-on alors pour écrire un gestionnaire d'évènement d'un composant lorsque cet évènement n'est pas l'évènement par défaut de ce composant ?

C'est ici qu'intervient l'onglet Évènement. Supposons, par exemple, que nous souhaitons gérer l'évènement EditingDone d'une zone de texte. Comme il ne s'agit pas de l'évènement par défaut, il faut ouvrir l'onglet Évènement du composant et cliquer sur les … de l'attribut OnEditingDone, comme dans l'exemple suivant :

et Lazarus générera alors le code initial du gestionnaire d'évènement dans le code source (la zone de texte se nomme ZoneTexteX) :

procedure TForm1.ZoneTexteXEditingDone(Sender: TObject);
begin

end;

On constate que cette procédure évènementielle est enregistrée dans l'attribut OnEditingDone :

Quelques évènements utiles

Dans les applications courantes, il est rare de devoir gérer d'autres évènements que le clic. Les suivants pourront toutefois vous être utiles dans de nombreux cas :

- DblClick : le double-clic. Attention :

- EditingDone : en français, « édition achevée ». Cet évènement est généré dès que l'utilisateur a fini de saisir un texte. Il est par exemple disponible pour les zones de texte. L'évènement se produit dès que l'on appuie sur la touche Entrée ou que l'on quitte la zone de texte en allant, par exemple, cliquer dans une autre zone de texte. On peut donc de cette manière saisir une donnée sans avoir à cliquer sur un bouton pour la confirmer ;

- Create : déclenche la création d'un formulaire et la procédure évènementielle FormCreate, que vous connaissez déjà. Pour être plus précis, cet évènement est déclenché par l'instanciation d'un objet de la classe TForm. Il n'existe donc que pour les composants de cette classe ;

- Resize : évènement déclenché par le redimensionnement d'un composant. Particulièrement utile pour gérer le redimensionnement de la fenêtre d'une application. On peut ainsi, par exemple, redimensionner les composants d'une fenêtre lorsque ses dimensions sont modifiées par l'utilisateur ;

- CloseQuery : généré par la fermeture d'un formulaire. Utile pour protéger l'utilisateur contre une fermeture involontaire de l'application qui pourrait entraîner une perte de données. Cet évènement n'existe que pour les formulaires (classe TForm).

- cet évènement n'est pas disponible pour les boutons ;

- d'après mes expérimentations, on ne peut pas gérer à la fois le clic et le double-clic sur un même composant. Un double-clic implique forcément un clic simple. Le clic simple est géré en priorité. Il y a toutefois une exception pour les zones de liste ;

Exemple

L'exemple présenté ici se trouve dans le répertoire Exemple-ProgObjet1/Evenement. Il s'agit d'une nouvelle version du programme d'addition fonctionnant sans bouton. Cet exemple illustre l'utilisation des différents types d'évènements dont nous avons parlé précédemment. Voici le formulaire de cette application :

Utilisation de EditingDone

La somme des deux nombres est effectuée dès que l'utilisateur appuie sur la touche Entrée dans une des deux zones de texte étiquetées X ou Y (ZoneTexteX et ZoneTexteY respectivement). Ceci est réalisé grâce à la gestion de l'évènement EditingDone.

Voici le code du gestionnaire de l'évènement EditingDone pour ZoneTexteX :

procedure TForm1.ZoneTexteXEditingDone(Sender: TObject);
begin
Additionner ();
end;

et celui du gestionnaire de l'évènement EditingDone pour ZoneTexteY :

procedure TForm1.ZoneTexteYEditingDone(Sender: TObject);
begin
Additionner ();
end;

Les deux procédures appellent la procédure Additionner, qui lit les deux nombres à afficher, puis affiche leur somme dans la zone de texte étiquetée X+Y.

Clic sur une étiquette

Un clic sur les étiquettes X ou Y (respectivement Etiquette_X et Etiquette_Y) efface les zones de texte associées (ZoneTexteX et ZoneTexteY respectivement).

Voici le code du gestionnaire de l'évènement Click pour Etiquette_X :

procedure TForm1.Etiquette_XClick(Sender: TObject);
begin
Effacer(ZoneTexteX);
end;

et celui du gestionnaire de l'évènement Click pour Etiquette_Y :

procedure TForm1.Etiquette_YClick(Sender: TObject);
begin
Effacer(ZoneTexteY);
end;

Utilisation de Resize

Si vous élargissez la fenêtre, vous constaterez que la dimension des zones de texte augmente. On obtiendra par exemple ceci :

Nous nous sommes en fait arrangés pour que la différence entre la largeur de la fenêtre et celle des zones de texte reste constante. Pour cela, la différence entre les deux largeurs est mémorisée dans une variable globale dès le démarrage de l'application. Nous avons donc placé cette instruction dans FormCreate :

procedure TForm1.FormCreate(Sender: TObject);
begin
DifferenceLargeur := Form1.width - ZoneTexteX.Width;
end;

Un redimensionnement de la fenêtre génère un évènement Resize associé au formulaire (Form1). Pour maintenir une différence de largeur constante, nous avons écrit le gestionnaire d'évènement Resize de Form1. Le voici :

procedure TForm1.FormResize(Sender: TObject);
var NouvelleLargeur : integer;
begin
NouvelleLargeur := Form1.width - DifferenceLargeur;
ZoneTexteX.width := NouvelleLargeur;
ZoneTexteY.width := NouvelleLargeur;
ZoneTexteSomme.width :=  NouvelleLargeur;
end;

Utilisation de DblClick, CloseQuery

Un double-clic sur le formulaire provoque l'apparition de la boîte de dialogue suivante :

Si l'utilisateur clique sur le bouton Non, l'application reste ouverte. S'il clique sur Oui, elle se termine.

Cette boîte de dialogue est affichée par la fonction Confirmation. Elle affiche une boîte de dialogue demandant à l'utilisateur de confirmer son choix.

Si l'utilisateur ne clique pas le bouton par défaut (ici Non), la fonction retourne True.

Logiquement, si un double-clic sur le formulaire provoque l'affichage de la boîte de dialogue, c'est nécessairement par l'intermédiaire du gestionnaire de l'évènement DblClick de Form1. Le voici :

procedure TForm1.FormDblClick(Sender: TObject);
begin
Form1.Close();
end;

Ô surprise ! Le gestionnaire n'appelle pas la fonction Confirmation. Comment le double-clic peut-il alors faire apparaître la boîte de dialogue ?

Logiquement, Form1.Close devrait fermer immédiatement la fenêtre.

L'explication de ce mystère est la suivante. La méthode Close génère l'évènement CLoseQuery associé au formulaire Form1 et c'est donc dans le gestionnaire de cet évènement que se trouve l'instruction permettant d'afficher la boîte de dialogue et de quitter ou non l'application, selon la réponse de l'utilisateur. Voici ce gestionnaire d'évènements :

procedure TForm1.FormCloseQuery
(Sender: TObject; var CanClose: boolean);
begin
CanClose := Confirmation ('Mise en garde',
'Voulez-vous réellement quitter l''application ?',
'Non','Oui');
end;

Le paramètre CanClose du gestionnaire détermine si l'application doit être effectivement fermée (CanClose = True dans ce cas).

Composants standard de Lazarus

Sans vouloir être exhaustif, nous décrivons ici quelques-uns des composants Lazarus, que vous pouvez trouver sous l'onglet Standard :

Les zones de texte (classe TEdit)

Nous utilisons depuis longtemps des zones de texte pour lire des données ou afficher des résultats avec les procédures de la librairie entrees_sortie.pas (Lire, LireEntier, LireNombre, Afficher, AfficherEntier, AfficherNombre). Avec ce qui suit, vous allez enfin savoir comment faire cela sans utiliser ces procédures.

Afficher dans une zone de texte

Pour afficher une expression de type chaîne de caractères dans une zone de texte, il suffit d'affecter cette expression à l'attribut .Text de cet objet.

Exemple :

ZT.Text := 'Toto';

affiche Toto dans la zone de texte ZT.

Si l'expression n'est pas de type chaîne de caractères, il faut au préalable la convertir en chaîne de caractères. On utilisera par exemple IntToStr pour afficher un nombre entier et FloatToStr pour afficher un nombre décimal.

Il peut paraître étrange qu'une simple affectation produise un affichage à l'écran, étant donné qu'une affectation ne fait que copier de l'information en mémoire. En réalité, l'affectation d'une valeur à l'attribut Text d'une zone de texte n'est pas une simple affectation. Nous reviendrons là-dessus dans le deuxième cours de programmation objet.

Lire une variable depuis une zone de texte

Inversement, pour lire le contenu d'une zone de texte dans une variable de type chaîne de caractères, on affecte à la variable la valeur de l'attribut .Text de la zone de texte.

Exemple :

Toto := ZT.Text;

affecte à Toto la chaîne de caractères contenue dans ZT.Text.

De même que pour l'affichage, il faudra utiliser les fonctions de conversion si la variable n'est pas de type String. On utilisera, par exemple, StrToInt pour une variable de type Integer et StrToFloat pour une variable de type Double.

Effacer une zone de texte

La méthode Clear() appliquée à une zone de texte permet d'effacer son contenu.

Exemple

Reprenons notre bon vieux projet d'addition qui permettait d'additionner deux nombres entiers (la version présentée ici se trouve dans Exemple-ProgObjet1/ZoneTexte). Voici son interface graphique :

Nous avons ici trois zones de texte nommées ZoneTexteX, ZoneTexteY et ZoneTexteSomme. Et voici la nouvelle version de la procédure évènementielle associée au bouton Additionner :

procedure TForm1.BoutonAdditionnerClick(Sender: TObject);
begin
x := StrToInt(zoneTexteX.Text);
y := StrToInt(ZoneTexteY.Text);
somme := x + y;
ZoneTexteSomme.Text := IntToStr(somme);
end;

La classe TStrings

La classe TStrings ne représente pas un composant graphique particulier, mais elle est utilisée dans les classes TListBox (zones de liste), TMemo (mémo) et TComboBox (combobox) que nous présentons plus loin. C'est la raison pour laquelle il nous faut commencer par présenter cette classe.

Tout comme la classe TstringList que nous avons présentée dans la première partie du cours, TStrings est une classe qui permet de manipuler des listes de chaînes de caractères.

En fait, TStringList n'est qu'une sous-classe de TStrings et toutes les méthodes Clear, Add, LoadFromFile, IndexOf ainsi que les attributs Count et Strings, dont nous avions déjà parlé (voir classe), ne sont pas des attributs spécifiques de TStringList, mais des attributs hérités de la classe TStrings.

TStrings possède d'autres méthodes dont nous n'avons pas encore parlé, comme SaveToFile et Delete. La première permet de sauvegarder son contenu dans un fichier et la deuxième de supprimer un élément d'indice donné (la première chaîne est indicée 0).

Les zones de liste (classe TListBox)

Jusqu'à présent, nous avons utilisé les zones de liste pour afficher du texte sur plusieurs lignes à l'aide des procédure Afficher, AfficherEntier… de la librairie entrees_sorties.pas.

En réalité, les zones de liste servent à autre chose. Elles sont utilisées pour permettre à l'utilisateur de choisir un élément dans une liste de choix.

Dans Lazarus, les zones de liste sont représentées par la classe TListBox. Cette dernière contient, parmi ses attributs, un attribut nommé Items de type TStrings. C'est dans cet attribut que sont stockés les différents choix possibles, sous la forme d'un objet de la classe TStrings.

On pourra donc ajouter un choix dans une zone de liste en appliquant la méthode Add à son attribut Items. De même, on pourra initialiser la liste des choix à partir d'un fichier, en appliquant la méthode LoadFromFile à cet attribut.

L'attribut ItemIndex est l'indice de l'élément sélectionné (le premier est indicé 0). S'il vaut -1, cela signifie qu'aucun élément n'est sélectionné.

L'attribut Sorted est un booléen qui détermine si la liste des choix doit être triée automatiquement par ordre alphabétique.

TlistBox contient (entre autres) une méthode nommée GetSelectedText. Il s'agit d'une fonction qui retourne le choix sélectionné par l'utilisateur. Si aucun choix n'est sélectionné, elle retourne la chaîne vide.

Exemple

Pour illustrer l'utilisation des zones de liste, nous présentons ici un exemple de projet simulant le site d'un magasin de bricolage. Ce projet se trouve dans le dossier Exemple-ProgObjet1/ZoneDeListe (ouvrir le fichier MagasinBricolage.lpi). Voici son interface graphique :

La zone de liste de gauche (ZL_Categorie) est initialisée dès le démarrage du programme dans la procédure FormCreate :

procedure TForm1.FormCreate(Sender: TObject);
begin
ZL_Categorie.Items.Add ('Electricite');
ZL_Categorie.Items.Add ('Jardinage');
ZL_Categorie.Items.Add ('Materiaux');
ZL_Categorie.Items.Add ('Plomberie');
end;

Nous avons utilisé ici la méthode Add de la classe TStrings.

Une autre manière de procéder aurait été de saisir manuellement les différentes chaînes de caractères « Électricité », « Jardinage », etc. via l'inspecteur d'objet de Lazarus : on sélectionne la zone de liste ZL_Categorie, puis sa propriété Items dans l'inspecteur d'objet et enfin on clique sur les … :

L'éditeur de chaînes permet alors de saisir les différents choix.

Lorsque l'utilisateur clique sur une des catégories de produits, les types de produits de cette catégorie sont affichés dans la deuxième zone de liste (nommée ZL_TypeProduit) :

Voici le code de la procédure évènementielle gérant cet évènement :

1. procedure TForm1.ZL_CategorieClick(Sender: TObject);

2. var NomCategorie, NomFichierCategorie : String;

3. begin

4. NomCategorie := ZL_Categorie.GetSelectedText;

5. NomFichierCategorie := NomCategorie+'.txt';

6. ZL_TypeProduit.Items.LoadFromFile(NomFichierCategorie);

7. ZL_TypeProduit.ItemIndex:=0;

8. end;

Pour comprendre son fonctionnement, il faut savoir que les types de produits d'une certaine catégorie se trouvent dans un fichier texte de même nom que la catégorie et d'extension '.txt'. Les types de produits de la catégorie Materiaux se trouvent, par exemple, dans le fichier Materiaux.txt.

Détaillons à présent les instructions de cette procédure évènementielle.

La ligne 4 permet de récupérer le nom de la catégorie de produit sélectionnée en appliquant la méthode GetSelectedText à la zone de liste ZL_Categorie.

La ligne 5 construit le nom du fichier contenant les différents types de produits de cette catégorie en concaténant l'extension '.txt' au nom de la catégorie.

La ligne 6 initialise la zone de liste ZL_TypeProduit avec les types de produits contenus dans ce fichier. Pour cela, la méthode LoadFromFile est appliquée à l'attribut Items de la zone de liste.

La ligne 7, enfin, définit le premier choix (choix d'indice 0) comme étant sélectionné. Cela explique pourquoi le premier type de produits apparaît sélectionné dès que la liste des types de produits est affichée.

Si vous comparez le contenu du fichier Materiaux.txt avec la liste des matériaux affichés par le programme, vous constaterez que le programme les affiche dans l'ordre alphabétique alors qu'ils ne sont pas dans l'ordre alphabétique dans le fichier. Cela s'explique par le fait que l'attribut Sorted de la zone de liste ZL_TypeProduit vaut True. Elle est donc automatiquement triée.

Les combobox (classe TComboBox)

Un combobox est une sorte de combinaison entre une zone de texte et une zone de liste. La zone de texte permet à l'utilisateur de saisir un choix qui n'est pas présent dans la zone de liste.

De plus, un combobox prend moins de place qu'une zone de liste, car la liste des choix n'est pas systématiquement déployée.

Voici quelques attributs utiles de la classe TComboBox :

On retrouve les attributs Items et Sorted des zones de listes. Par contre, la méthode GetSelectedText a disparu car le texte sélectionné se retrouve automatiquement dans l'attribut Text.

Autocomplete permet la complétion automatique des choix. Par exemple, s'il existe un choix commençant par un f, dès que l'utilisateur tape f, la zone de texte contiendra automatiquement le premier choix de la liste commençant par f et ce choix sera sélectionné dans la zone de liste. Si ce n'est pas le bon choix, il lui suffira en général de saisir quelques caractères de plus.

Enfin, lorsque l'attribut AutoDropDown vaut True, la liste est déployée dès que l'utilisateur tape un caractère dans la zone de texte.

Exemple

L'exemple présenté ici se trouve dans le dossier ComboBox du dossier Exemple (projet ProjetMoyenTransport). Voici l'allure de l'interface graphique au démarrage du programme :

Le combobox est initialement vide. Comme nous avons mis AutoDropDown à True, la liste est automatiquement déployée dès que l'on tape un caractère :

Comme nous avons mis AutoComplete à True, si on tape « M » dans le combobox, ce choix est automatiquement complété en « Mobylette » car c'est le premier choix de la liste commençant par un M :

L'utilisateur peut également saisir un choix qui ne se trouve pas dans la zone de liste :

Lorsqu'il clique sur le bouton Afficher le choix, le choix sélectionné est affiché dans la zone de texte (ZT_Vehicule). Voici le code de la procédure évènementielle associée à ce bouton :

procedure TForm1.BT_AfficherChoixClick(Sender: TObject);
begin
ZT_Vehicule.Text := CB_Vehicule.Text;
end;

Les cases à cocher (classe TCheckBox)

Les cases à cocher sont utilisées pour saisir des informations du type vrai/faux ou oui/non. L'état coché/décoché de la case est mémorisé dans l'attribut Checked, de type booléen. Avec Checked = True, la case est cochée. Sinon, elle ne l'est pas.

Exemple

L'exemple présenté ici se trouve dans le dossier Exemple-ProgObjet1/CaseACocher (ouvrir ProjetHotel.lpi). Voici la fenêtre de l'application :

Lorsque l'utilisateur sélectionne ou désélectionne les options Petit déjeuner ou Douche, le prix de la chambre est automatiquement actualisé. Avec petit déjeuner :

Avec la douche en plus :

C'est trop cher. On enlève le petit déjeuner :

Voici la procédure évènementielle associée à la case à cocher Petit déjeuner (CC_PetitDej) :

procedure TForm1.CC_PetitDejChange(Sender: TObject);
begin
AfficherLePrix ();
end;

et voici celle associée à la case à cocher Douche (CC_Douche) :

procedure TForm1.CC_DoucheChange(Sender: TObject);
begin
AfficherLePrix ();
end;

Ces deux procédures appellent la procédure non évènementielle AfficherLePrix que voici :

procedure AfficherLePrix ();
var Prix : Double;
begin
Prix := 40;
if Form1.CC_PetitDej.Checked then Prix := Prix + 5;
if Form1.CC_Douche.Checked then   Prix := Prix + 10;
Form1.ZT_Prix.Text := FloatToStr(Prix);
end;

Les boutons radio (classe TRadioButton)

Les boutons radio s'utilisent pour définir un choix parmi n, lorsque n n'est ni trop petit (pour n=2, on peut tout aussi bien prendre une case à cocher), ni trop grand (il vaut mieux dans ce cas utiliser une zone de liste ou un combobox).

Comme pour les cases à cocher, c'est l'attribut Checked qui définit le fait qu'un bouton radio soit coché ou non.

Par contre, les boutons radio ne se comportent pas exactement comme des cases à cocher. En effet, des boutons radio directement déposés sur le formulaire s'excluent mutuellement : si l'on coche un des boutons radio, les autres sont automatiquement décochés.

Cette interdépendance des boutons radio est gênante lorsque l'on souhaite utiliser des boutons radio pour des choix indépendants. C'est ici qu'intervient le composant GroupBox. Pour rendre deux groupes de boutons radio indépendants, on peut déposer deux GroupBox sur le formulaire, puis mettre les boutons radio du premier groupe dans le premier GroupBox et ceux du second groupe dans le second.

Un petit conseil pour terminer : pour mettre des composants à l'intérieur d'un autre, il vaut mieux commencer par le plus gros (le conteneur donc) puis déposer les petits composants dedans. Si on procède dans le sens inverse, les petits composants seront cachés par le gros. Dans ce cas, il faut faire un clic droit sur le conteneur, puis sélectionner Z-Order, puis déplacer en arrière.

Exemple

L'exemple présenté ici se trouve dans le dossier Exemple-ProgObjet1/BoutonRadio (ouvrir StationEssence.lpi ). Voici la fenêtre de l'application à son démarrage :

Nous avons utilisé ici un GroupBox pour chaque pompe. Vous constaterez que les boutons radio d'une même pompe s'excluent mutuellement (un seul choix d'essence possible), mais que les boutons radio de la pompe 1 sont indépendants de ceux de la pompe 2. Ici, par exemple, on a choisi E10 à la pompe 1 et SP95 à la pompe 2 :

Voici les procédures évènementielles des boutons radio de la pompe 1 :

procedure TForm1.BR_SP95_P1Change(Sender: TObject);
begin
AfficherPrixPompe1 ();
end;

procedure TForm1.BR_E10_P1Change(Sender: TObject);
begin
AfficherPrixPompe1 ();
end;

procedure TForm1.BR_Gazole_P1Change(Sender: TObject);
begin
AfficherPrixPompe1 ();
end;

Elles appellent toutes la procédure AfficherPrixPompe1 que voici :

procedure AfficherPrixPompe1 ();
var Prix :double;
begin

if Form1.BR_SP95_P1.Checked Then Prix:=PRIX_SP95;
if Form1.BR_E10_P1.Checked Then Prix:=PRIX_E10;
if Form1.BR_Gazole_P1.Checked Then Prix:=PRIX_Gazole;

Form1.ZT_Prix_P1.Text := FloatToStr (Prix);
end;

Pour les boutons radio de la pompe 2, nous avons utilisé le même principe.

Les mémos (classe TMemo)

Les mémos sont en quelque sorte des zones de texte étendues sur plusieurs lignes. Le texte contenu dans un mémo est stocké dans son attribut Lines de type TStrings.

L'attribut WordWrap définit la manière de gérer le passage à la ligne dans le mémo. Sa valeur par défaut est True, ce qui signifie qu'une ligne de texte sera automatiquement coupée et donc répartie sur plusieurs lignes, lorsque la largeur du mémo n'est pas suffisante pour l'afficher en entier. Dans ce cas, chaque chaîne de caractères contenue dans l'attribut Lines ne représente pas nécessairement une ligne de texte unique dans le mémo (tel qu'il est affiché).

Si WordWrap vaut False, une ligne trop longue pour être affichée en entier sera affichée en partie (ce qui dépasse n'est pas visible). Dans ce cas, chaque chaîne de caractères dans Lines correspond à une unique ligne affichée.

L'attribut Scrollbars définit les barres de défilement horizontales ou verticales. Il ne peut y avoir de barre de défilement horizontale que si Wordwrap vaut False, car dans le cas contraire elle n'est d'aucune utilité. Scrollbars a sept valeurs possibles, qui sont des constantes prédéfinies de Lazarus :

- ssVertical : ajout systématique d'une barre de défilement verticale ;

- SsHorizontal : ajout systématique d'une barre de défilement horizontale ;

- SsBoth : ajout systématique des deux barres de défilement ;

- SsNone : aucune barre de défilement ;

- SsAutoVertical : ajout d'une barre de défilement verticale lorsque c'est nécessaire ;

- SsAutoHorizontal : ajout d'une barre de défilement horizontale lorsque c'est nécessaire ;

- ssAutoBoth : ajout d'une barre de défilement horizontale ou verticale, lorsque c'est nécessaire.

Exemple

L'exemple présenté ici se trouve dans le dossier Exemple-ProgObjet1/Memo (ouvrir le fichier EditeurDeTexte.lpi). Il s'agit d'un éditeur de texte très simplifié qui permet d'ouvrir un fichier, de le modifier et de l'enregistrer. Le fichier ouvert est affiché à l'intérieur d'un mémo. Voici, par exemple, l'affichage du fichier Dylan.txt (que vous trouverez dans le même répertoire que le projet) :

Dans cet exemple, nous avons mis WordWrap à False et Scrollbar à ssAutoBoth. Comme le texte est trop large et trop long, deux barres de défilement ont été ajoutées et on voit que la première ligne est affichée en partie. Si vous n'avez pas très bien compris le rôle des attributs WordWrap et Scrollbar, amusez-vous à modifier leur valeur pour en voir l'effet.

Comme l'attribut Lines d'un mémo est de type TStrings, le chargement d'un fichier dans un mémo se fait simplement en appliquant la méthode LoadFromFile à cet attribut. Nous utilisons ceci dans la procédure évènementielle associée au bouton Ouvrir (ZT_NomFichier est le nom de la zone de texte contenant le nom du fichier et MM_Editeur, le nom du mémo) :

procedure TForm1.BT_OuvrirClick(Sender: TObject);
var NomFichier : String;
begin
NomFichier := ZT_NomFichier.Text;
MM_Editeur.Lines.LoadFromFile(NomFichier);
end;

Pour enregistrer le fichier, même principe avec la méthode SaveToFile :

procedure TForm1.BT_EnregistrerClick(Sender: TObject);
var NomFichier : String;
begin
NomFichier := ZT_NomFichier.Text;
MM_Editeur.Lines.SaveToFile(NomFichier);
end;

Les menus (classes TMainMenu et TMenuItem)

Les menus, comme les boutons, servent à activer différents traitements d'un logiciel. Lorsque le nombre de traitements est important, l'utilisation d'un bouton pour chaque traitement prend trop de place. Dans ce cas, il est préférable d'utiliser des menus. Ce sont en quelque sorte des boutons organisés en listes déroulantes, qui ne se déploient que lorsque l'on clique dessus.

Exemple

Nous reprenons ici l'exemple précédent de l'éditeur de texte en y intégrant une barre de menu. Ce projet se trouve dans Exemple-ProgObjet1/Menu (fichier EditeurDeTexte.lpi ).

Présentation de l'interface graphique

Voici la nouvelle interface graphique du programme, telle qu'elle apparaît à son démarrage :

La fenêtre comprend à présent une barre de menus contenant deux menus, intitulés Fichier et Affichage. Le menu Fichier permet d'ouvrir un fichier, de l'enregistrer ou de quitter l'application :

Remarquez qu'à chaque entrée du menu est associé un raccourci clavier. Nous verrons un peu plus loin comment les définir.

Le menu Affichage offre deux possibilités, intitulées WordWrap et FullWindow, que nous détaillerons un peu plus loin :

Ce menu est un peu différent du précédent car chacune de ses entrées peut être cochée ou décochée.

Pour commencer, voyons comment construire ce menu avec Lazarus.

Utilisation de l'éditeur de menu

Vous pouvez, si vous le désirez, effectuer les manipulations suivantes à partir de la version précédente de l'éditeur de texte (dans Exemple-ProgObjet1/Memo).

Commencez par déposer un menu principal (objet MainMenu1 de la classe TMainMenu) sur le formulaire. Cet objet représente la barre de menus.

Pour modifier le contenu de la barre des menus, il faut utiliser l'éditeur de menu, que l'on ouvre par un double-clic sur le composant MainMenu1 déposé sur le formulaire :

Pour l'instant, le menu principal ne contient qu'une seule entrée, nommée New Item1. Pour en ajouter une deuxième, on fait un clic droit sur le New Item1 affiché dans l'éditeur de menu et on sélectionne Insérer un nouvel élément après.

Notre barre de menu contient à présent deux entrées (donc deux menus). Pour faire apparaître les libellés Fichier et Affichage à la place de New Item1 et New Item2, utilisez l'inspecteur d'objet et modifiez les propriétés Caption de ces deux composants (attention : cela n'est possible que si l'éditeur de menu est ouvert !).

Vous pouvez ensuite ajouter une première entrée au menu Fichier par un clic droit sur Fichier (dans l'éditeur de menu) et en sélectionnant Créer un sous-menu. On obtient ainsi une première entrée dans le menu Fichier. Elle est libellée New Item3 :

Pour ajouter une deuxième entrée après New Item3, faites un clic droit sur New Item3, puis sélectionnez Insérer un nouvel élément après :

À ce stade, vous avez certainement compris le principe de construction d'un menu sous Lazarus. Nous allons donc arrêter ici les explications concernant l'utilisation de l'éditeur de menu.

Encore deux petit détails avant de passer à l'aspect programmation :

- pour rendre une entrée du menu cochable, il faut mettre la propriété AutoCheck à True ;

- pour définir un raccourci clavier, utilisez la propriété ShortCutKey. Voici, par exemple, comment a été défini le raccourci clavier CTRL O pour l'entrée Ouvrir du menu Fichier :

Gestionnaires d'évènements des menus

Pour accéder au gestionnaire du clic sur une entrée de menu (ou le générer s'il n'existe pas encore), il suffit de cliquer une fois dessus.

En cliquant, par exemple, sur l'entrée Ouvrir du menu fichier, vous êtes automatiquement redirigé sur la procédure évènementielle suivante :

procedure TForm1.MN_OuvrirClick(Sender: TObject);
var NomFichier : String;
begin
NomFichier := ZT_NomFichier.Text;
MM_Editeur.Lines.LoadFromFile(NomFichier);
end;

Nous avons simplement réutilisé ici le code qui se trouvait précédemment dans la procédure évènementielle associée au bouton Ouvrir.

Notez que si un raccourci clavier a été défini, l'exécution de la procédure évènementielle sera également déclenchée lorsque l'utilisateur tape ce raccourci. Dans notre exemple, la procédure évènementielle associée à l'entrée Ouvrir sera donc également déclenchée si l'utilisateur tape CTRL O.

Même principe pour l'entrée Enregistrer :

procedure TForm1.MN_EnregistrerClick(Sender: TObject);
var NomFichier : String;
begin
NomFichier := ZT_NomFichier.Text;
MM_Editeur.Lines.SaveToFile(NomFichier);
end;

Enfin, pour déclencher la fermeture de l'application (entrée Quitter), nous appliquons simplement la méthode Close à Form1 :

procedure TForm1.MN_QuitterClick(Sender: TObject);
begin
Form1.Close();
end;

Les gestionnaires du menu Affichage provoquent une action différente selon que l'entrée du menu correspondante est cochée ou non.

L'entrée WordWrap active ou désactive le mode WordWrap du mémo :

procedure TForm1.MN_WordWrapClick(Sender: TObject);
begin
MM_Editeur.WordWrap:= Not MM_Editeur.WordWrap;
end;

Il est inutile de cocher ou décocher l'entrée dans le code, car nous avons mis AutoCheck à True et, dans ce cas, cela se fait automatiquement.

Lorsque l'entrée FullWindow est cochée, le mémo occupe la quasi-totalité de la fenêtre et la zone de texte permettant de saisir le nom du fichier disparaît :

Si on la décoche, le mémo revient à sa taille originale et la zone de texte réapparaît. Nous avons réalisé ceci de la manière suivante :

procedure TForm1.MN_FullWindowClick(Sender: TObject);
begin
if MN_FullWindow.Checked then
begin
MM_Editeur.Width := Form1.Width - 50;
MM_Editeur.Height := Form1.Height - 50;
Label1.Hide();
ZT_NomFichier.Hide();
end
else
begin
MM_Editeur.Width := LargeurMemo;
MM_Editeur.Height := HauteurMemo;
Label1.Show();
ZT_NomFichier.Show();
end;
end;

Ici, nous avons besoin de savoir si l'entrée du menu est cochée ou non, car il ne suffit pas, comme dans l'entrée WordWrap, d'inverser la valeur d'un booléen.

Si l'entrée FullWindow est cochée (MN_FullWindow.Checked), les dimensions du mémo (propriétés Width et Height) sont redéfinies comme celles du formulaire moins 50 pixels. Puis on fait disparaître l'étiquette et la zone de texte en leur appliquant la méthode Hide.

Si l'entrée FullWindow n'est pas cochée, les dimensions du mémo sont restaurées et l'on fait réapparaître la zone de texte et l'étiquette on leur appliquant la méthode Show.

Remarquez que pour pouvoir retrouver les dimensions d'origines du mémo, nous les avons sauvegardées dans les variables LargeurMemo et HauteurMemo au démarrage de l'application :

procedure TForm1.FormCreate(Sender: TObject);
begin
LargeurMemo := MM_Editeur.Width;
HauteurMemo := MM_Editeur.Height;
end;

Lorsque l'on affecte une nouvelle valeur à l'attribut Width (largeur) ou Height (hauteur) d'un composant, celui-ci est immédiatement réaffiché avec les nouvelles dimensions. Comment une simple affectation peut-elle provoquer ce résultat ? Nous avons déjà rencontré ce phénomène dans la modification de la propriété Text d'une zone de texte, qui provoque immédiatement l'affichage de sa nouvelle valeur. Nous reviendrons là-dessus dans le deuxième cours sur la programmation objet.

Boîtes de dialogues de Lazarus

Les boîtes de dialogue sont des fenêtres permettant de choisir des objets complexes (fichiers, couleurs, polices, dates, etc.). Elles possèdent en général un bouton de confirmation du choix ainsi qu'un autre bouton permettant d'annuler le choix. Dès que l'utilisateur clique sur le bouton de confirmation ou d'annulation, la boîte de dialogue disparaît.

Dans Lazarus, toutes les classes représentant des boîtes de dialogue sont des sous-classes de TCommonDialog. Cette dernière possède une méthode Execute, permettant d'afficher une boîte de dialogue à l'écran. De plus, cette méthode retourne la valeur false si l'utilisateur annule son choix et la valeur true s'il le confirme.

Vous trouverez les boîtes de dialogue sous l'onglet Dialog. Nous présenterons ici les boîtes de dialogues suivantes :

ainsi que l'utilisation de la fonction Confirmation, permettant d'afficher une boîte de dialogue pour confirmer un choix.

Cette partie du cours se termine par un exemple : une nouvelle version de l'éditeur de texte utilisant des boîtes de dialogue.

Ouverture de fichier (classe TOpenDialog)

Les boîtes de dialogues d'ouverture de fichier évitent à l'utilisateur de taper le nom (et éventuellement le chemin) d'un fichier. Elles se présentent de la manière suivante :

Vous en avez certainement utilisé des centaines de fois, car on les trouve dans tous les logiciels utilisant des fichiers. Pour sélectionner un fichier, vous vous déplacez dans l'arborescence des fichiers de l'ordinateur. Une fois le répertoire atteint, il vous suffit de cliquer sur le nom du fichier.

Si l'utilisateur confirme le choix de fichier, l'attribut FileName de la boîte de dialogue contiendra le nom complet du fichier (avec extension et chemin si nécessaire).

Les boîtes de dialogue d'ouverture de fichier permettent également de filtrer les fichiers par leur extension. Par exemple, dans la boîte de dialogue de la figure précédente, seuls les fichiers d'extension « .txt » sont visibles. L'utilisateur ne pourra donc sélectionner qu'un fichier texte. Ceci permet d'éviter l'ouverture de fichiers de format incompatible avec le programme qui les traite.

La boîte de dialogue contient une zone de liste permettant à l'utilisateur de sélectionner un certain type de filtrage. La boîte dialogue suivante, par exemple, présente deux choix de filtrage :

On peut également définir un filtrage autorisant plusieurs extensions. Voici, par exemple, le résultat du filtrage simultané des extensions « *.txt » et « *.pas » :

Le filtrage des fichiers est défini par le contenu de la propriété Filter de la boîte de dialogue. Pour définir son contenu, le plus simple est d'utiliser l'éditeur de filtre. On y accède en cliquant sur les « … » de la propriété Filter dans l'inspecteur d'objet.

Chaque ligne de l'éditeur de filtre représente un choix de filtrage dans la zone de liste. Voici, par exemple, comment définir séparément deux modes de filtrage :

La colonne de gauche est un commentaire pour l'utilisateur. Il sert en principe à expliquer la signification de l'extension. La colonne de droite définit le filtre sous la forme « *.??? », où les trois caractères « ??? » sont ceux de l'extension.

Pour autoriser tous les formats, on utilise la notation « *.* » :

Pour définir un filtrage autorisant simultanément plusieurs extensions, il faut séparer les filtres par des points-virgules :

Enregistrement de fichier (classe TSaveDialog)

Les boîtes de dialogue d'enregistrement de fichier fonctionnent selon le même principe que les boîtes de dialogue d'ouverture de fichier, mis à part que le bouton Ouvrir est remplacé par un bouton Enregistrer :

Ouverture de fichier image (classe TOpenPictureDialog)

La classe TOpenPictureDialog est une variante de TOpenFileDialog qui permet de visualiser des fichiers images. L'intérêt principal de cette boîte de dialogue est que les fichiers images sont visualisés sous forme d'icônes. Comme ceci, par exemple :

Comme pour la classe TOpenPictureDialog, le fichier sélectionné se retrouve dans l'attribut FileName.

Choix d'une couleur (classe TColorDialog)

Une couleur est représentée, sous Lazarus, par le type TColor. La boîte de dialogue de sélection de couleur permet à l'utilisateur de sélectionner visuellement une couleur, sans se préoccuper de la manière dont elle est codée en mémoire :

Le programmeur, quant à lui, récupère la couleur dans l'attribut Color. Il pourra ensuite utiliser cette valeur pour affecter cette couleur à n'importe quel composant graphique, sans se préoccuper lui non plus du codage des couleurs.

Choix d'une police (classe TFontDialog)

La police de caractères, c'est-à-dire la manière dont s'affiche une chaîne de caractères à l'écran, est représentée sous Lazarus par la classe TFont, qui possède de très nombreux attributs. La boîte de dialogue de sélection de police permet à l'utilisateur de sélectionner visuellement les propriétés d'une police de caractères :

Le programmeur récupère la police de caractères sélectionnée dans l'attribut Font, sous la forme d'un objet de la classe TFont.

Confirmation d'une action

Cette section ne présente pas une boîte de dialogue Lazarus, mais une fonction que j'ai écrite moi-même permettant d'afficher une boîte de dialogue de ce type :

Cette fonction, nommée Confirmation, est intégrée à la librairie entrees_sorties.pas. Si vous regardez son code, vous constaterez qu'elle utilise la fonction QuestionDlg. Il s'agit d'une fonction standard de Lararus, plus complexe et plus générale, permettant d'afficher une boîte de dialogue posant une question à l'utilisateur avec éventuellement plus de deux boutons.

La fonction Confirmation affiche une boîte de dialogue permettant de confirmer l'exécution d'une action potentiellement dangereuse. Elle retourne un booléen : True si l'utilisateur confirme son choix, False sinon.

Dans l'exemple présenté dans la figure ci-dessus, l'appel de la fonction serait :

Confirmation ('Mise en garde',
'Voulez-vous réellement quitter l''application ?',
'Non','Oui');

De manière générale, vous pouvez utiliser cette fonction pour confirmer des choix potentiellement dangereux de l'utilisateur. Voici son en-tête :

function Confirmation (t,m,td,ta:string) : boolean;

Elle possède donc quatre paramètres de type String :

- t : titre de la boîte de dialogue ;

- m : message affiché dans la boîte de dialogue ;

- td : titre du bouton par défaut ;

- td : titre de l'autre bouton.

Le résultat retourné est True si l'utilisateur ne clique pas sur le bouton par défaut, c'est-à-dire s'il confirme vouloir exécuter l'action potentiellement dangereuse.

Exemple

L'exemple suivant se trouve dans le dossier Exemple-ProgObjet1/boîteDeDialogue. Il s'agit d'une version améliorée de l'éditeur de texte que nous avons utilisé pour expliquer le principe des menus. Cette nouvelle version intègre (entre autres) des boîtes de dialogue pour l'ouverture et l'enregistrement des fichiers.

Le menu Fichier est le même que dans la version précédente :

Le menu Affichage, par contre, a été remplacé par le menu Texte, qui permet de modifier la police de caractères ou la couleur du mémo.

Cette nouvelle version utilise quatre boîtes de dialogue :

- OpenDialog1 est la boîte de dialogue pour l'ouverture des fichiers ;

- SaveDialog1 est la boîte de dialogue pour l'enregistrement des fichiers ;

- FontDialog1 est la boîte de dialogue pour le choix de police ;

- ColorDialog1 est la boîte de dialogue pour choisir la couleur.

Notez que ces boîtes de dialogue ne seront pas immédiatement visibles au démarrage du programme. Pour les afficher, il est nécessaire de leur appliquer la méthode Execute.

Ouverture de fichier

L'entrée Ouvrir du menu Fichier provoque l'exécution de la procédure suivante :

procedure TForm1.MN_OuvrirClick(Sender: TObject);
begin
if (OpenDialog1.Execute()) then
MM_Editeur.Lines.LoadFromFile(OpenDialog1.FileName);
end;

Si la méthode Execute retourne True, l'utilisateur a confirmé le choix de fichier. Son nom se trouve donc dans l'attribut Filename. Ce fichier est chargé dans le mémo MM_Editeur via la méthode LoadFromFile.

Enregistrement de fichier

L'entrée Enregistrer du menu Fichier provoque l'exécution de la procédure suivante :

procedure TForm1.MN_EnregistrerClick(Sender: TObject);
begin
if (SaveDialog1.Execute()) then
if not FileExists (SaveDialog1.FileName)  then
MM_Editeur.Lines.SaveToFile(SaveDialog1.FileName)
else
if
Confirmation('Mise en garde',
'Fichier existant.'+
' Voulez vous écraser l''ancienne version ?',
'Ne rien faire','Ecraser')
then
MM_Editeur.Lines.SaveToFile(SaveDialog1.FileName);
end;

Ici, nous testons l'existence du fichier pour éviter d'écraser par mégarde une version ultérieure. Si le fichier existe, la fonction Confirmation est utilisée pour confirmer le choix de l'utilisateur.

Choix de police

L'entrée Police du menu Texte provoque l'exécution de la procédure suivante :

procedure TForm1.MN_PoliceClick(Sender: TObject);
begin
if (FontDialog1.Execute) then
MM_Editeur.Font:= FontDialog1.Font;
end;

La police sélectionnée est affectée à l'attribut Font du mémo, ce qui a pour effet de modifier la police de caractère du texte affiché dans le mémo.

Choix de couleur

L'entrée Couleur du menu Texte provoque l'exécution de la procédure suivante :

procedure TForm1.MN_CouleurClick(Sender: TObject);
begin
if (ColorDialog1.Execute()) then
MM_Editeur.Color:=ColorDialog1.Color;
end;

La couleur sélectionnée est affectée à l'attribut Color du mémo, ce qui a pour effet de modifier la couleur du fond du mémo.

Composants divers

Composant image (classe TImage)

Le composant image permet d'afficher une image provenant d'un fichier. La plupart des formats images courants (jpg, gif, bmp, etc.) sont acceptés. Vous le trouverez sous l'onglet Additionnal :

L'attribut Picture

L'image elle-même est contenue dans l'attribut Picture (classe TPicture) du composant. La méthode LoadFromFile permet de charger une image dans un objet de cette classe. D'autre part, lorsqu'une image est chargée dans l'attribut Picture d'un composant image, elle est immédiatement affichée.

Par exemple, l'instruction suivante :

Image1.Picture.LoadFromFile('Toto.jpg')

charge le fichier image Toto.jpg dans le composant image Image1 (ou plus exactement dans son attribut Picture), ce qui a pour effet de l'afficher à l'écran.

Pour l'effacer, on utilisera la méthode Clear de la classe TPicture. En effet, cette méthode supprime l'image chargée dans un objet de cette classe. Si on l'applique à l'attribut Picture d'un composant image, l'image affichée sur ce dernier sera immédiatement effacée.

Par exemple, l'instruction :

Image1.Picture.Clear();

efface l'image affichée dans le composant Image1.

Attributs définissant la manière d'afficher l'image

Pour bien comprendre les explications qui suivent, il faut avoir à l'esprit que les dimensions d'un composant image (en pixels) ne coïncident généralement pas avec les dimensions de l'image contenue dans un fichier.

La manière dont une image sera affichée à l'intérieur d'un composant image dépend essentiellement des attributs Stretch, Proportional et Center. Ces trois attributs sont tous des booléens.

Si Stretch vaut true, l'image est agrandie ou rapetissée de manière à remplir le composant image en hauteur ou en largeur. L'effet de Stretch dépend de la valeur de l'attribut Proportional :

- si Proportional vaut False, les dimensions de l'image sont ajustées en largeur et en hauteur. Cela aura généralement pour effet de déformer l'image, car les proportions du composant image ne sont pas nécessairement les mêmes que celles de l'image contenue dans le fichier ;

- si Proportional vaut True, l'image sera agrandie ou rapetissée en conservant ses proportions. Dans ce cas, le composant image sera rempli en largeur, mais pas forcément en hauteur ou inversement.

L'attribut Center définit le centrage de l'image. Par défaut Center = false et l'image n'est pas centrée. Son coin supérieur gauche correspond alors au coin supérieur gauche du composant image. Par contre, si Center vaut true, le centre du composant image coïncidera avec le centre de l'image.

Exemple

L'exemple présenté ici se trouve dans le dossier Exemple-ProgObjet1/Image. Ouvrez le fichier Visualisateur.lpi. Il s'agit d'une application très simple permettant d'afficher une image.

La fenêtre de l'application comprend une barre de menu et un composant image.

Pour charger une image, utilisez l'entrée Ouvrir du menu Fichier. Elle vous permettra de sélectionner un fichier image à l'aide d'une boîte de dialogue d'ouverture d'image.

Des exemples d'images se trouvent dans le répertoire Exemple-ProgObjet1/Oiseaux.

Vous pouvez ensuite vous amuser à modifier les attributs Stretch, Proportional ou Center du composant image via le menu Option.

Voici, par exemple, l'affichage de l'image de la cigogne avec Stretch = true et Proportional = False :

et la même image avec Stretch = true et Proportional = true :

L'entrée Ouvrir du menu fichier active la procédure évènementielle suivante :

procedure TForm1.MN_OuvrirClick(Sender: TObject);
var nfi : string;
begin
if OpenPictureDialog1.execute() then
begin
nfi := OpenPictureDialog1.FileName;
Image1.Picture.LoadFromFile(nfi);
end;
end;

OpenPictureDialog1 est la boîte de dialogue d'ouverture d'image. Si l'utilisateur a sélectionné une image, le nom du fichier image est affecté à la variable nfi, puis chargé dans le composant image Image1, ce qui a pour effet de l'afficher.

Liste de fichiers (classe TFileListBox)

Le composant liste de fichiers vous permettra d'afficher les fichiers contenus dans un répertoire et éventuellement d'effectuer un traitement sur ces fichiers. Vous le trouverez sous l'onglet Misc :

La classe TFileListBox représentant ce composant est une sous-classe de TListBox (zone de liste), si bien que tous les attributs et méthodes des zones de liste peuvent être utilisés avec les listes de fichiers.

Le chemin du répertoire associé au composant est mémorisé dans l'attribut Directory.

La liste des fichiers est initialisée au démarrage de l'application. Si l'utilisateur modifie le contenu du répertoire pendant son exécution, la liste des fichiers affichés dans le composant ne sera pas automatiquement réactualisée. Pour la réactualiser, il faut lui appliquer la méthode UpdateFileList.

Exemple

L'exemple présenté ici (répertoire Exemple-ProgObjet1/ListeDeFichiers) est une nouvelle version du visualiseur d'images avec un composant liste de fichiers permettant de sélectionner l'image à afficher :

On accède à une image en cliquant sur son nom. Les touches de votre clavier représentant des flèches vers le haut ou le bas vous permettront de vous déplacer dans la liste (remarquez que ces touches fonctionnent également dans une zone de liste standard).

Le bouton Rafraîchir permet de réactualiser la liste des fichiers, au cas où elle aurait été modifiée pendant l'exécution du programme.

Un clic sur un élément de la liste de fichiers (ou un déplacement dans la liste via les « touches flèches ») provoque l'exécution de la procédure évènementielle suivante :

procedure TForm1.FileListBox1Click(Sender: TObject);
var nom_fichier : string;
begin
nom_fichier:=FileListBox1.Directory+'\'+
FileListBox1.GetSelectedText;
Image1.Picture.LoadFromFile(nom_fichier);
end;

FileListBox1 est le composant « liste de fichiers » de l'application. La première instruction récupère le nom du fichier grâce à la méthode GetSelectedText des zones de liste. Ce nom est concaténé avec le chemin du répertoire mémorisé dans l'attribut Directory du composant et affecté à la variable nom_fichier.

La deuxième instruction charge l'image dans le composant image Image1.

Enfin, voici le code de la procédure évènementielle associée au bouton rafraîchir :

procedure TForm1.BT_Rafraichir_ListeClick(Sender: TObject);
begin
FileListBox1.UpdateFileList;
end;

Multifenêtrage

Une application peut utiliser plusieurs fenêtres, s'appelant les unes les autres. En général, on aura une fenêtre principale qui permet d'appeler des fenêtres secondaires. Nous allons voir ici comment réaliser une application de ce type en partant directement d'un exemple. Il s'agit d'une nouvelle version du visualiseur d'images, qui permet cette fois-ci de visualiser simultanément deux images dans deux fenêtres distinctes. Vous trouverez ce projet dans le répertoire Exemple-ProgObjet1/MultiFenetrage.

Il y a donc en tout trois fenêtres : une fenêtre principale ainsi que deux fenêtres secondaires pour visualiser les images. La fenêtre principale permet de commander l'affichage.

La fenêtre principale comporte un menu, une liste de fichiers et deux boutons radio :

Les deux entrées du menu Voir permettent de faire apparaître les fenêtres (elles sont invisibles au démarrage de l'application). La liste de fichiers permet de sélectionner une image à afficher. Les boutons radio, quant à eux, définissent dans quelle fenêtre sera affichée l'image sélectionnée.

Adjonction de nouvelles fenêtres

Voyons tout d'abord comment créer une application multifenêtres sous Lazarus. Lorsque vous démarrez la réalisation d'une nouvelle application, vous avez une seule fenêtre :

Par défaut, la classe représentant cette fenêtre est TForm1, sous-classe de TForm, et l'objet représentant votre fenêtre est Form1, déclaré comme objet de la classe TForm1.

L'unité associée à cette classe se nomme par défaut Unit1 et son code sera contenu dans le fichier Unit1.pas.

Pour ajouter une nouvelle fenêtre, sélectionnez l'entrée Nouvelle Fiche du menu Fichier. Cela fera apparaître une nouvelle fenêtre nommée Form2 :

La classe de cette fenêtre se nomme à présent TForm2 et l'unité associée Unit2 sera par défaut enregistrée dans le fichier Unit2.pas.

Définition de la fenêtre principale

Dans une application multifenêtres, la fenêtre principale est celle qui va s'afficher en premier au démarrage de l'application (par défaut, les fenêtres secondaires ne s'affichent pas). C'est donc par l'intermédiaire de cette fenêtre que les fenêtres secondaires pourront être affichées. Lorsque l'utilisateur ferme la fenêtre principale, les autres sont automatiquement fermées. Par contre, l'inverse n'est pas vrai : si l'utilisateur ferme une fenêtre secondaire, la fenêtre principale reste ouverte.

Le fenêtre principale joue donc un rôle particulier par rapport aux autres fenêtres.

Mais comment Lazarus sait-il quelle fenêtre sera la fenêtre principale ?

Par défaut, c'est la première fenêtre créée.

Si, par mégarde, vous avez commencé par une fenêtre secondaire, vous pouvez redéfinir la fenêtre principale en éditant le fichier .lpr.

Voici, par exemple, le contenu du fichier lpr pour un projet à deux fenêtres :

program project1;

{$mode objfpc}{$H+}

uses
{$IFDEF UNIX}{$IFDEF UseCThreads}
cthreads,
{$ENDIF}{$ENDIF}
Interfaces, // this includes the LCL widgetset
Forms, Unit1, Unit2
{ you can add units after this };

{$R *.res}

begin
RequireDerivedFormResource := True;
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.CreateForm(TForm2, Form2);
Application.Run;
end.

Si vous voulez que Form2 soit la fenêtre principale au lieu de Form1, il suffit d'inverser les appels de la méthode CreateForm :

program project1;

{$mode objfpc}{$H+}

uses
{$IFDEF UNIX}{$IFDEF UseCThreads}
cthreads,
{$ENDIF}{$ENDIF}
Interfaces, // this includes the LCL widgetset
Forms, Unit1, Unit2
{ you can add units after this };

{$R *.res}

begin
RequireDerivedFormResource := True;
Application.Initialize;
Application.CreateForm(TForm2, Form2);
Application.CreateForm(TForm1, Form1);
Application.Run;
end.

De manière générale, la fenêtre principale est celle qui correspond au premier appel de la méthode CreateForm.

Renommage des unités et des fenêtres

Dans une application multifenêtres, je vous conseille de renommer les unités et les fenêtres. Car des noms comme Form1, Form2, Unit1, Unit2… sont très peu évocateurs.

Nommage des unités

Le nommage des unités se fait au moment de la sauvegarde du projet, car le nom d'une unité est lié au nom du fichier Pascal qui la contient.

Par exemple, pour changer le nom de l'unité Unit1 en UnitePrincipale, sélectionnez l'entrée Enregistrer le projet sous… dans le menu Projet. Vous aurez à un moment donné une boîte de dialogue vous demandant d'enregistrer Unit1.pas :

Il suffit alors de remplacer Unit1.pas par UnitePrincipale.pas :

et l'unité Unit1 sera alors automatiquement renommée UnitePrincipale dans l'éditeur de source :

Nommage des fenêtres

Pour renommer les fenêtres, il suffit de modifier la propriété Name de Form1, Form2, etc. via l'inspecteur d'objets.

Dans notre exemple, nous avons utilisé Frm_Principal pour la fenêtre principale, Frm1 pour la fenêtre 1 et Frm2 pour la fenêtre 2.

Remarquez que le renommage entraîne automatiquement celui des classes associées. Dans notre exemple, Form1 a été renommée Frm_Principal, ce qui a automatiquement entraîné le renommage de TForm1 en TFrm_Principal :

type
TFrm_Principal = class(TForm)
.
.
end;

var
Frm_Principal : TFrm_Principal;

Importation des unités

Notre exemple de projet multifenêtres contient trois unités nommées UnitePrincipale (associée à la fenêtre principale), UniteImage1 (associée à la fenêtre 1) et UniteImage2 (associée à la fenêtre 2).

En ouvrant le projet, vous constaterez que UniteImage1 et UniteImage2 sont importées dans UnitePrincipale :

unit UnitePrincipale;

{$mode objfpc}{$H+}

interface

uses
Classes, SysUtils, FileUtil, Forms,
Controls, Graphics, Dialogs, Menus,
FileCtrl, StdCtrls, UniteImage1, UniteImage2;

Cette importation est nécessaire car l'unité principale contient des instructions agissant sur les composants des deux autres fenêtres.

De manière générale, si une unité A agit sur un composant d'une unité B alors l'unité A doit importer l'unité B.

Code de l'exemple

Pour bien comprendre le code de notre exemple, il est préférable d'avoir lu les parties du cours concernant les menus, les listes de fichiers, les boutons radio et les composants image.

Précisons que toutes procédures évènementielles décrites ici se trouvent dans l'unité UnitePrincipale associée à la fenêtre principale.

Voici la procédure évènementielle gérant le clic sur une entrée de la liste des fichiers :

procedure TFrm_Principal.FileListBox1Click(Sender: TObject);
var nom_fichier : string;
begin

nom_fichier:=
FileListBox1.Directory+ '\'
+FileListBox1.GetSelectedText;

if BR_Fenetre1.Checked then
Frm1.Image1.Picture.LoadFromFile(nom_fichier)
else
Frm2.Image1.Picture.LoadFromFile(nom_fichier);
end;

FileListBox1 est le nom de la liste de fichiers.

La première instruction construit le nom complet du fichier image à partir du chemin du répertoire qui la contient (stocké dans l'attribut Directory du composant FileListBox) et du nom de fichier sélectionné par l'utilisateur.

Le bouton radio étiqueté Fenêtre 1 se nomme BR_Fenetre1. S'il est coché, l'image est affichée dans la fenêtre 1. Sinon, elle est affichée dans la fenêtre 2.

Comme les composants image ne se trouvent pas dans la fenêtre principale, il est nécessaire de préfixer leurs noms par le nom de la fenêtre qui les contient. On accède donc ici au composant image de la fenêtre 1 par Frm1.Image1 et au composant image de la fenêtre 2 par Frm2.Image2.

L'entrée Fenetre1 du menu Voir déclenche l'exécution de la procédure suivante :

procedure
TFrm_Principal.MN_Voir_Fenetre2Click(Sender: TObject);
begin
Frm2.Show;
end;

On applique la méthode Show à Frm2 pour faire apparaître la fenêtre.

L'entrée Fenetre2 du menu Voir utilise le même principe pour la deuxième fenêtre :

procedure
TFrm_Principal.MN_Voir_Fenetre1Click(Sender: TObject);
begin
Frm1.Show;
end;

Exercices

Téléchargez les exercices :

- sur l'utilisation des classes ;

- sur les interfaces graphiques.

Pour obtenir les corrigés, le téléchargement n'est possible que via un login et un mot de passe, que vous pouvez obtenir en envoyant un mail à l'adresse suivante :

en précisant un peu qui vous êtes et les raisons pour lesquelles ce cours vous intéresse.

Note de la rédaction de Developpez.com

Nous remercions Jacques Théry pour sa relecture.

Notions de programmation objet

Prérequis

Pour comprendre ce cours, il est nécessaire d'avoir acquis la partie Utilisation de classes du premier cours sur la programmation objet.

Définition basique d'une classe

L'exemple de projet, qui va nous servir à illustrer la définition basique d'une classe, se trouve dans le dossier Exemple-ProgObjet2/Voiture1. Dans ce projet, nous avons défini une classe pour représenter une voiture.

Présentation du projet

Voici l'interface graphique de ce programme :

Le bouton Nouvelle Voiture crée une nouvelle instance de la classe voiture, en lisant son immatriculation depuis la zone de texte prévue à cet effet.

Le Bouton Rouler ajoute au compteur la distance indiquée par l'utilisateur, sauf si le compteur atteint ou dépasse 10 000 km. Dans ce cas, le programme affiche un message indiquant que les pneus de la voiture sont usés.

Définition de la classe et de ses méthodes

En Pascal Objet, la définition d'une classe se fait en deux parties :

- définition de l'interface de la classe : elle contient la définition des attributs de la classe, et les en-têtes (ou signatures) de ses méthodes. Elle figure dans la partie Interface du fichier ;

- l'implémentation de la classe : c'est la définition du code des méthodes de la classe. Elle figure dans la partie Implementation du fichier.

Interface de la classe Voiture

Voici l'interface de la classe Voiture :

Compteur et Immat sont les attributs de la classe.

Nouvelle, Initialiser, Rouler et AfficherAttributs sont les méthodes de la classe.

Rouler et AfficherAttributs sont des procédures, ChangerPneu est une fonction et la méthode Nouvelle est une méthode spéciale dont la présence n'est pas obligatoire et que l'on appelle le constructeur de la classe. Nous verrons quel est son rôle un peu plus loin.

Comme vous pouvez le constater, le code des méthodes ne figure pas dans l'interface. On y met simplement les en-têtes.

Implémentation de la classe Voiture

Voici l'implémentation de la classe Voiture :

- La méthode Nouvelle est le constructeur de la classe. Elle permet de créer une nouvelle instance de la classe Voiture en donnant une valeur à son immatriculation (la valeur du paramètre Im). Sans constructeur, il aurait été nécessaire d'utiliser la méthode Create, qui fonctionne pour n'importe quelle classe. Mais Create ne permet pas d'initialiser immédiatement les attributs de l'instance générée. Le constructeur est donc une sorte de méthode Create paramétrée.

- La méthode Rouler augmente le compteur d'une voiture du nombre de kilomètres défini par le paramètre Km, sauf s'il est nécessaire de changer les pneus. Dans ce dernier cas, elle affiche le message « Vos pneus sont usés ! ».

- La méthode ChangerPneu est une fonction booléenne qui retourne la valeur true, si et seulement si le nombre de kilomètres de la voiture est supérieur ou égal à 10 000.

- La méthode AfficherAttribut affiche les valeurs des attributs d'une voiture (immatriculation et compteur) dans les zones de texte prévues à cet effet.

En-têtes des méthodes

Les en-têtes des méthodes sont identiques aux en-têtes définis dans l'interface, sauf que le nom de chaque méthode est précédé du nom de la classe : Voiture.Nouvelle, Voiture.Rouler, Voiture.ChangerPneu…

C'est de cette manière que l'on associe une méthode à sa classe dans la partie Implementation.

Les méthodes qui commencent par « Voiture. » sont celles de la classe Voiture.

Corps des méthodes

Dans le corps des méthodes, on remarque que les attributs de la classe ainsi que les appels de méthodes de la classe ne sont pas précédés du nom d'une instance de cette classe.

Par exemple, dans le corps du constructeur, les attributs Immat, Compteur ne sont pas précédés par le nom d'une instance.

Dans la méthode Rouler, on appelle les méthodes ChangerPneu et Afficher. Mais elles ne sont apparemment appliquées à aucune instance de la classe Voiture !

En fait, cette instance est implicite : c'est l'instance à laquelle sera appliquée la méthode. On ne la connaît donc pas lorsque l'on écrit le code d'une méthode. Dans tout le corps d'une méthode, il faut donc imaginer que le nom de cette instance figure à gauche des attributs et des méthodes de la classe.

Par défaut, les attributs non précédés du nom d'une instance qui figurent dans le corps d'une méthode de la classe Voiture sont interprétés comme des attributs de la classe Voiture.

C'est la raison pour laquelle les noms des zones de textes Zt_Immat et Zt_Compteur sont précédés par Form1 dans le corps de la procédure AfficherAttribut.

Sinon, ils auraient été interprétés comme des attributs de la classe Voiture, ce qui aurait généré une erreur de compilation.

Rappelons-nous (voir cours I sur la programmation objet) que Form1 est une instance de la classe TForm1 qui représente le formulaire d'une application. Les attributs de cette instance sont les différents composants du formulaire. En particulier, la zone de texte Zt_Immat, doit donc être désignée par Form1.Zt_Immat en dehors d'une méthode de la classe TForm1.

Utilisation des méthodes de la classe

Les méthodes de la classe Voiture sont utilisées dans les différentes procédures évènementielles de l'application. Les voici :

Nous avons déclaré une variable nommée LaVoiture de type Voiture. Elle nous servira à mémoriser une instance (unique dans ce programme) de la classe Voiture.

Les procédures évènementielles sont des méthodes de la classe TForm1. C'est la raison pour laquelle elles sont toutes préfixées par TForm1.

Le bouton Nouvelle Voiture

Bt_CreerClick est la procédure évènementielle associée au bouton Nouvelle Voiture.

Dans le corps de cette procédure, on commence par lire l'immatriculation depuis la zone de texte Zt_Immat. Cette chaîne de caractères est affectée à la variable locale immat.

Remarquez qu'elle n'est pas préfixée ici par Form1., car on se trouve dans une méthode de la classe TForm1.

L'instruction suivante génère une instance de Voiture avec immat comme immatriculation, en faisant appel à la méthode Nouvelle, c'est-à-dire au constructeur de la classe.

Puis les attributs de la nouvelle voiture sont affichés en appliquant la méthode AfficherAttributs à l'instance LaVoiture.

Le bouton Rouler

Bt_RoulerClick est la procédure évènementielle associée au bouton Rouler.

Dans le corps de cette procédure, on commence par lire la distance à parcourir depuis la zone de texte Zt_Distance. Cet entier est ensuite affecté à la variable locale d.

Ensuite, on fait rouler la voiture pendant d kilomètres en appliquant la méthode Rouler à LaVoiture.

Remarquez que l'on suppose ici que l'instance de voiture a été générée, c'est-à-dire que l'utilisateur a cliqué sur le bouton Nouvelle Voiture avant de cliquer sur le bouton Rouler.

Dans le cas contraire on obtiendrait une erreur d'exécution, car LaVoiture ne contiendrait pas l'adresse d'une instance de voiture.

Encapsulation

L'encapsulation est une manière de définir une classe de telle sorte que ses attributs ne puissent pas être directement manipulés de l'extérieur, mais seulement indirectement par l'intermédiaire de méthodes spéciales appelées accesseurs.

Un des avantages de cette approche est la possibilité de redéfinir la représentation interne des attributs, sans que cela affecte la manipulation de la classe via les méthodes.

Autrement dit, les programmes utilisant le principe d'encapsulation sont plus faciles à mettre à jour.

Visibilité des membres d'une classe

Un membre d'une classe désigne un attribut ou une méthode définie dans cette classe.

La visibilité des membres d'une classe définit les endroits d'où ils peuvent être utilisés. Dans cette section, nous allons voir deux sortes de visibilité : la visibilité publique et la visibilité privée.

Les membres privés d'une classe ne sont accessibles que par les méthodes de la classe :

- les attributs privés d'une classe ne peuvent être utilisés que par les méthodes de cette classe ;

- les méthodes privées d'une classe ne peuvent être appelées que par les méthodes de cette classe.

Les membres publics d'une classe sont accessibles de l'extérieur de la classe :

- les attributs publics d'une classe sont accessibles (en lecture ou en écriture) depuis n'importe quelle partie du programme ;

- les méthodes publiques d'une classe peuvent être appelées depuis n'importe quelle partie du programme.

Une classe est encapsulée si tous ses attributs sont privés.

Visibilité des membres en Pascal Objet

Pascal Objet offre cinq types de visibilité : Private, Strict Private, Protected, Public et Published. Si rien n'est précisé, c'est implicitement la visibilité Public qui est utilisée.

Je ne parlerai ici que de Strict Private et Public, qui correspondent aux visibilités privée et publique dont j'ai parlé précédemment.

Juste un mot sur Private. On pourrait croire d'après le nom qu'elle correspond à la visibilité privée. Mais non !

La visibilité Private de Pascal n'est pas réellement privée car elle autorise des accès depuis n'importe quel sous-programme ou méthode se trouvant dans la même unité.

Présentation de l'exemple

Pour illustrer le principe d'encapsulation, nous allons reprendre la classe Voiture en définissant la visibilité de ses membres de telle manière qu'elle soit encapsulée. Il s'agit donc d'une nouvelle version du projet Voiture1, que vous trouverez dans le dossier Exemple-ProgObjet2/Voiture2.

L'interface graphique de ce nouveau projet est exactement la même que celle du projet Voiture1 :

De plus, les deux projets font exactement la même chose. Ils ne diffèrent que par le code.

Le projet comporte à présent deux unités :

- l'unité Voiture2.pas contient les procédures évènementielles associées à l'interface graphique ;

- l'unité ClasseVoiture2.pas contient la définition de la classe Voiture.

L'unité Voiture2.pas utilise la classe Voiture définie dans l'unité ClasseVoiture2.pas.

Dans Voiture2.pas, nous donnons deux utilisations de la classe :

- la première est une utilisation interdite, ne respectant pas la visibilité des membres de la classe. Elle provoquera une erreur de compilation ;

- la deuxième respecte la visibilité des membres et ne provoque pas d'erreur de compilation.

L'une ou l'autre version peut être obtenue en mettant ou en enlevant les commentaires dans les procédures AfficherLaVoiture et TForm1.Bt_CreerClick.

Déclaration de la classe

Voici la nouvelle interface de la classe voiture :

Conformément au principe d'encapsulation, les attributs sont privés.

Dans cet exemple, toutes les méthodes sont publiques. Mais certaines d'entre elles auraient pu être privées sans enfreindre le principe d'encapsulation.

Pour pouvoir mieux illustrer nos propos, nous avons enlevé les méthodes Rouler, ChangerPneu et AfficherAttributs ainsi que le constructeur de la classe.

Les nouvelles méthodes, getCompteur, setCompteur, getImmat et setImmat sont les accesseurs de la classe. Ce sont eux qui vont nous permettre d'agir sur une instance de la classe depuis l'unité Voiture2.

Dans notre exemple, extrêmement simplifié, les accesseurs sont les seules méthodes de la classe. En général, ce n'est pas le cas.

Il y a deux types d'accesseurs :

- les accesseurs en lecture : ils permettent de lire la valeur d'un attribut. Ce sont des fonctions. Par convention, leur nom commence par get ;

- les accesseurs en écriture : ils permettent de modifier la valeur d'un attribut. Ce sont des procédures. Par convention, leur nom commence par set.

Dans notre exemple, nous avons donc un accesseur en lecture et un accesseur en écriture pour chacun des attributs de la classe.

Voici le code de ces méthodes :

Dans les accesseurs en écriture, la valeur à affecter à l'attribut est passée en paramètre.

Dans le corps de l'accesseur, il peut être intéressant de n'affecter cette valeur que si elle vérifie certaines conditions. On peut de cette manière éviter des opérations inadéquates sur une instance de la classe.

Dans notre exemple, nous avons fait un contrôle de ce type dans l'accesseur setCompteur : lorsque la valeur passée en paramètre est supérieure à 10 000, elle n'est pas affectée à l'attribut Compteur. Un message d'erreur est affiché.

Utilisation interdite de la classe

Voici une première version du module Voiture2.pas, dans laquelle on essaie d'accéder directement aux membres privés de la classe voiture :

Comme on pouvait s'y attendre, cette version provoque une erreur de compilation.

Par contre, on constate que si on rend les attributs Immat et Compteur publics, le projet devient compilable et fonctionne très bien.

Utilisation autorisée de la classe

Pour qu'une classe encapsulée puisse être utilisée, il est nécessaire d'accéder à ses attributs de manière indirecte, via les accesseurs de la classe.

Voici une deuxième version du module Voiture2.pas, utilisant la classe Voiture encapsulée :

On constate que la compilation se passe bien et que le projet fonctionne correctement.

Les propriétés

En plus des attributs et des méthodes, Pascal Objet offre la possibilité de définir des propriétés. Nous verrons qu'elles permettent d'encapsuler une classe d'une manière intéressante.

C'est avec les propriétés, également, que nous allons pouvoir (enfin !) expliquer le comportement étrange de certains « attributs » (en fait ce sont des propriétés) des composants graphiques.

Vous savez que l'affectation d'une valeur à une variable est une simple copie d'information dans la mémoire centrale. Elle ne peut donc avoir aucun effet sur l'écran. Comment expliquer alors :

- que l'affectation d'une valeur à l'attribut Text d'une zone de texte provoque immédiatement l'affichage de cette valeur ?

- comment la modification des propriétés height ou width d'un composant modifie immédiatement ses dimensions à l'écran ?

Mais, tout d'abord, à quoi servent les propriétés ?

Utilité des propriétés

Pour répondre à cette question, prenons un exemple.

Imaginons que vous souhaitez représenter un article de magasin par une classe. Chaque article possède un prix hors taxes et un prix toutes taxes comprises (TTC).

Si vous définissez le prix hors taxes et le prix TTC en tant que propriétés, vous pouvez par exemple vous arranger pour que le prix TTC soit automatiquement recalculé dès que le prix hors taxes est modifié.

Pour l'utilisateur de votre classe, cela sera totalement transparent. C'est-à-dire qu'il ne verra pas la différence avec l'utilisation d'un attribut.

Supposons, par exemple, que le prix hors taxes soit représenté par la propriété HT et le prix TTC, par la propriété TTC. Pour affecter la valeur 100 au prix hors taxes d'un article a, l'utilisateur de votre classe écrira :

a.HT := 100;

Et, sans qu'il le sache, cette « affectation » (en réalité, ce n'est pas une simple affectation, nous reviendrons là-dessus plus loin) aura en fait provoqué le calcul du prix TTC de cet article.

Supposons, par exemple, une taxe de 20 % sur cet article. S'il affiche la propriété TTC (qui s'écrit donc a.TTC comme s'il s'agissait d'un attribut), l'utilisateur constatera que sa valeur est à présent 120, alors que nulle part dans le code qu'il a écrit ne figure une instruction modifiant cette valeur !

Cet exemple illustre un des aspects utiles des propriétés : le fait de pouvoir rendre des attributs interdépendants. Mais, il y en a d'autres :

- avec les propriétés, il est également possible de contrôler les données enregistrables dans un objet. On pourrait, par exemple, interdire l'affectation d'une valeur négative au prix hors taxes d'un article ;

- il existe un type particulier de propriété, appelé propriététableau, qui est très intéressant pour représenter des ensembles d'objets. Cet aspect est très utile pour la représentation de relations entre objets.

Déclaration des propriétés

Voyons à présent comment déclarer une propriété à l'intérieur d'une classe. Nous allons distinguer deux catégories de propriétés : les propriétés simples et les propriétés tableaux.

Déclaration des propriétés simples

La déclaration d'une propriété simple définit son nom, son type ainsi que le nom d'un accesseur en lecture et, optionnellement, celui d'un accesseur en écriture.

Si l'accesseur en écriture n'est pas donné, la propriété sera en lecture seule (l'affectation d'une valeur à la propriété provoquera une erreur de compilation).

Une propriété avec accesseur en écriture se déclare comme suit :

property nomP : typeP read nomAL write nomAE;

où nomP, typeP, nomAL et nomAE désignent respectivement le nom de la propriété, son type, le nom de l'accesseur en lecture et le nom de l'accesseur en écriture.

Pour déclarer une propriété en lecture, on omet simplement la déclaration de l'accesseur en écriture comme ceci :

property nomP : typeP read nomAL;

L'accesseur en écriture est une méthode qui sera automatiquement appelée chaque fois qu'une expression quelconque sera affectée à la propriété. Il est défini comme une procédure avec un en-tête de la forme suivante :

procedure nomAE (v: typeP);

La valeur de l'expression affectée sera automatiquement transmise au paramètre v lors de l'appel.

L'accesseur en lecture, quant à lui, est une fonction sans paramètre retournant une valeur de même type que la propriété :

function nomAL () : typeP;

Cette fonction sera exécutée chaque fois que la propriété sera utilisée. C'est-à-dire que chaque fois que la propriété apparaît dans une expression, il faut imaginer que l'appel de l'accesseur en lecture figure à la place.

Notez bien qu'une propriété ne permet de stocker aucune valeur dans un objet. Pour cela, il faut prévoir un attribut (en accès privé de préférence, sinon cela n'a aucun intérêt !). L'accesseur en écriture ira a priori modifier cet attribut avec la valeur qui lui sera donnée et l'accesseur en lecture retournera simplement sa valeur. Mais cela n'est que théorique car les accesseurs sont des méthodes quelconques. Elles sont donc libres de faire ce qu'elles veulent !

Un exemple de classe utilisant des propriétés simples est donné plus bas.

Déclaration des propriétés tableaux

La déclaration des propriétés tableaux suit à peu de chose près le même principe, à part qu'il faut à présent ajouter des informations sur la manière d'indicer les éléments.

Contrairement aux tableaux classiques, les indices des propriétés tableaux peuvent être de type quelconque. On ne précise donc pas un indice de début et un indice de fin, mais simplement le type des indices :

property nomP [TypeI]: typeV  read nomAL write nomAE;

TypeI représente ici le type des indices et typeV le type des éléments.

L'accesseur en lecture possède à présent un paramètre qui définit l'indice de l'élément auquel on veut accéder :

function nomAL (i : TypeI) : typeV;

De même pour l'accesseur en écriture :

procedure  nomAE (i : TypeI; V : typeV);

Depuis l'extérieur de la classe, l'élément d'indice i d'un objet o est désigné par o.nomP[i], comme si la propriété nomP était un attribut de type tableau.

Comme tout est géré par les accesseurs, les données de ce « tableau virtuel » peuvent être en réalité stockées dans toute autre chose qu'un tableau. Par exemple, dans une liste (voir le cours sur les types structurés).

D'autre part, les propriétés tableaux ont la particularité de pouvoir être implicites. C'est-à-dire que l'on pourra écrire o[i] au lieu de o.nomP[i].

Il suffit pour cela de déclarer la propriété nomP comme étant la propriété par défaut de la classe en ajoutant le mot-clé default à la fin de la déclaration :

property nomP [TypeI]: typeV  read nomAL write nomAE; default

Bien entendu, il ne peut y avoir qu'une seule propriété par défaut dans une classe donnée.

Je pense que tout cela sera plus clair avec un exemple. En voici un.

Exemple de propriété simple

Voici l'interface de la classe représentant un article de magasin :

Article = class
Strict private
A_HT: Double;
A_TTC: Double;
Function getHT (): Double;
Procedure setHT (c:Integer);
Function getTTC (): Double;

public
property HT : Double read getHT write setHT ;
property TTC : Double read getTTC;
end;

Les attributs A_HT et A_TTC servent respectivement à mémoriser les prix hors taxes et TTC de l'article. Mais ils sont privés et donc totalement inaccessibles depuis l'extérieur de la classe.

Nous avons également rendu les accesseurs privés, car l'accès public aux attributs se fait à présent par les propriétés HT et TTC.

Par contre, nous avons déclaré la propriété TTC en lecture seule. L'utilisateur de la classe ne pourra donc que modifier le prix hors taxes.

Et voici l'implémentation des accesseurs :

Function Article.getHT ():Double;
begin
getHT := A_HT;
end;

Procedure Article.setHT (p:Double);
begin
A_HT := p; A_TTC := p * 1.2;
end;

Function Voiture.getTTC (): Double;
begin
getTTC := A_TTC;
end;

Notez que l'accesseur en écriture de la propriété HT fait plus qu'on ne lui demande : il calcule immédiatement le prix TTC (le prix hors taxes augmenté de 20 %) et l'affecte à l'attribut A_TTC. C'est ainsi que toute modification du prix hors taxes d'un article sera automatiquement répercutée sur son prix TTC.

Exemple de propriété tableau

Nous allons prendre comme exemple une classe TStock représentant le stock d'un magasin. La propriété En_Stock de cette classe nous permettra d'accéder au contenu du stock.

Déclaration et utilisation de la propriété En_Stock

Supposons, par exemple, que s soit un objet de la classe TStock ; alors s.En_Stock['Samsung Q45'] nous dira combien il reste de « Samsung Q45 » dans le stock s. Il s'agit donc d'une propriété tableau indicée par des chaînes de caractères (le libellé d'un article donné) et dont les éléments sont des entiers (quantité de chaque article).

Voici la déclaration de la propriété En_Stock :

property En_Stock [Libelle : String] : integer
read GetEnStock write SetEnStock; default;

Nous l'avons définie comme propriété par défaut, ce qui nous permet le raccourci d'écriture s['Samsung Q45'], au lieu de s.En_Stock['Samsung Q45'].

Les données de la propriété En_Stock sont mémorisées dans deux attributs privés : LesLibelles et LesQuantites. Le premier contient les noms des articles présents dans le stock et il est déclaré comme suit :

LesLibelles : array [1 .. MAX_ARTICLE] of String;

Le deuxième contient la quantité de chaque article. C'est donc un tableau d'entiers. Nous le déclarons de la manière suivante :

LesQuantites : array [1 .. MAX_ARTICLE] of integer;

Et nous prenons les conventions suivantes :

- les valeurs du tableau LesLibelles sont toutes distinctes. Autrement dit, un même article ne peut pas se retrouver plusieurs fois dans ce tableau ;

- la quantité d'un article donné est définie par LesQuantites[i], où i est l'indice de son libellé dans le tableau LesLibelles. Par exemple, si LesLibelles[3] contient « Samsung Q45 », la quantité de Samsung Q45 dans le stock sera donnée par LesQuantites[3] ;

- un article dont le libellé figure dans le tableau LesLibelles existe dans le stock. Il y en a donc nécessairement une quantité non nulle. Cela signifie également que le tableau LesQuantites ne contiendra toujours que des nombres strictement positifs.

Grâce aux propriétés, tout ceci sera totalement transparent à l'utilisateur de la classe, qui pourra facilement ajouter ou retirer des articles du stock en ignorant totalement le travail effectué en arrière-plan par les accesseurs.

Par exemple, pour ajouter dix Samsung Q45 :

s['Samsung Q45'] := s['Samsung Q45'] + 10;

Pour en retirer 15 :

s['Samsung Q45'] := s['Samsung Q45'] - 15;

Et cette opération sera sans effet, s'il n'en restait par exemple que 12.

Pour retirer tous les Samsung Q45 du stock :

s['Samsung Q45'] := 0;

Pour ajouter un nouvel article, on lui affecte une quantité strictement positive. Par exemple, l'instruction

s['Clé USB Sony 8GB'] := 20;

sert a priori à placer vingt « clés USB Sony 8GB » dans le stock. Si, par hasard, cet article est déjà en stock, l'ancienne quantité sera effacée.

Interface de la classe Stock

Voici l'interface de la classe Stock :

TStock = class
strict private

NArticle :  integer;
LesLibelles : array [1 .. MAX_ARTICLE] of String;
LesQuantites : array [1 .. MAX_ARTICLE] of integer;
Curseur : integer;
function Indice (Libelle : String) : integer;
function GetEnStock (Libelle: String) : integer;
procedure SetEnStock (Libelle : String; n : integer);
procedure Supprimer (Libelle : String);

public
property En_Stock [Libelle : String] : integer
read GetEnStock write SetEnStock; default;
constructor Nouveau ();
function ArticleSuivant (): String;
procedure Debut ();
end;

NArticle contient l'indice du dernier article. Un stock vide se reconnaîtra donc par NArticle = 0 et un stock plein, par NArticle = MAX_ARTICLE.

L'attribut Curseur, ainsi que les méthodes ArticleSuivant et Debut, permettent de parcourir les articles d'un stock avec une boucle. Nous en reparlerons plus loin.

La fonction Indice retourne l'indice, dans le tableau LesLibelles, d'un article de libellé donné. Si cet article est absent, elle retourne la valeur 0.

La procédure Supprimer supprime un article de libellé donné en décalant les éléments suivants vers la gauche (voir le cours sur les tableaux, gestion des tableaux remplis partiellement).

Implémentation des accesseurs de la méthode En_Stock

Nous n'allons pas détailler ici l'implémentation de toutes les méthodes de la classe (si cela vous intéresse, consultez le code source du projet Magasin dans le répertoire Exemple-ProgObjet2), mais uniquement les accesseurs de la propriété tableau En_Stock.

Voici l'accesseur en lecture :

function TStock.GetEnStock (Libelle: String) : integer;
var i : integer;
begin
i := Indice (Libelle);
If i = 0 then GetEnStock := 0
else
GetEnStock := LesQuantites[i];
end;

Et voici l'accesseur en écriture :

procedure TStock.SetEnStock (Libelle : String; n : integer);
var position : integer;
begin
position := Indice (Libelle);
If position = 0 then
begin
If (NArticle <> MAX_ARTICLE) and (n > 0) then
begin
NArticle := NArticle  + 1;
LesLibelles [NArticle]:=Libelle;
LesQuantites [NArticle] := n;
end
end
else
if n >= 0 then
if n = 0 then Supprimer (Libelle)
else LesQuantites [position] := n;

end;

La logique de cette procédure est la suivante :

- nous commençons par repérer la position de l'article ;

- si l'article est introuvable (position = 0), on l'ajoute au stock. Mais cela n'est possible que si la quantité à ajouter est un nombre strictement positif (n > 0) et que le stock n'est pas rempli (NArticle <> MAX_ARTICLE). Si ces deux conditions sont vérifiées, on ajoute l'article dans le stock avec la quantité n associée. Ceci est réalisé en empilant le libellé dans le tableau LesLibelles et la quantité dans le tableau LesQuantites (pour la notion de pile, voir également le cours sur les tableaux, gestion des tableaux remplis partiellement) ;

- si l'article existe, il y a trois possibilités :

- n est négatif : on ne fait rien ,

- n = 0 : on supprime l'article ,

- N > 0 : on affecte la quantité n à cet article.

Utilisation de la classe Stock

Dans le dossier Exemple-progObjet2/Magasin, vous trouverez un exemple de projet utilisant la classe stock. Voici son interface graphique :

Voici la procédure évènementielle associée au bouton Commander :

procedure TForm1.BT_CommanderClick(Sender: TObject);
var libelle: string; quantite : integer;
begin
libelle := ZT_LIB.Text;
quantite := StrToInt(ZT_Quantite.Text);
Stock[libelle]:= Stock[libelle]+quantite;
AfficherLeStock ();
end;

Commander un article consiste donc à ajouter la quantité commandée au stock. Le nouveau contenu du stock est affiché à l'écran par la procédure AfficherLeStock, dont nous reparlerons plus loin.

Celle du bouton Vendre suit le même principe. On remplace simplement le + par un - :

procedure TForm1.BT_VendreClick(Sender: TObject);
var libelle: string; quantite : integer;
begin
libelle := ZT_LIB.Text;
quantite := StrToInt(ZT_Quantite.Text);
Stock[libelle]:= Stock[libelle]-quantite;
AfficherLeStock ();
end;

Affichage du stock

La procédure AfficherLeStock affiche le contenu du stock dans la zone de liste. Elle n'utilise pas les propriétés tableaux et n'est donc pas essentielle pour la compréhension de ce cours. Elle vous servira néanmoins à comprendre l'intérêt de l'attribut Curseur et des méthodes ArticleSuivant et Debut.

La méthode ArticleSuivant permet de parcourir le stock avec une boucle, sans connaître le nombre d'articles (rappelez-vous que NArticle est un attribut privé).

Nous utilisons ici le principe du curseur pour parcourir séquentiellement un ensemble de données (on retrouve par exemple ce principe dans le parcours séquentiel d'un fichier ou dans la lecture d'une table d'une base de données).

Dans notre cas, le curseur est un nombre entier qui définit l'indice du dernier article lu. Il est représenté par l'attribut privé Curseur de la classe Stock.

Comme il s'agit d'un attribut privé, l'utilisateur de la classe ne peut pas agir directement sur lui. Par contre, il dispose de la méthode publique ArticleSuivant, qui lui permet d'avancer le curseur d'un cran. Il s'agit plus précisément d'une fonction qui retourne le libellé de l'article suivant. Voici le code de cette méthode :

function TStock.ArticleSuivant (): String;
begin
if Curseur = NArticle then ArticleSuivant := 'FIN'
else
begin
Curseur := Curseur + 1;
ArticleSuivant := LesLibelles[Curseur];
end;
end;

Si le curseur pointe sur le dernier article (Curseur = NArticle), cela signifie que l'on a parcouru tout le stock. Dans ce cas, la fonction n'avance pas le curseur et retourne simplement la chaîne 'FIN' pour signifier que l'on est arrivé au bout du stock.

Par contre, s'il ne pointe pas sur le dernier article, le curseur est incrémenté et la fonction retourne le libellé de l'article sur lequel il pointe après l'incrémentation. C'est-à-dire le libellé de l'article suivant.

La méthode Debut permet de positionner le curseur au début du stock :

procedure TStock.Debut ();
begin
Curseur := 0;
end;

Elle doit donc être appelée avant de parcourir le stock avec une boucle.

À présent, voyons le code de la procédure AfficherLeStock :

procedure TForm1.AfficherLeStock ();
var libelle : String; quantite: integer;
begin
Stock.Debut; ZL_Stock.Clear;
libelle := Stock.ArticleSuivant ;
While libelle <> 'FIN' do
begin
ZL_Stock.Items.Add(libelle+': '
+IntToStr(Stock[libelle]));
libelle := Stock.ArticleSuivant;
end;
end;

Tant que l'on n'est pas à la fin du stock (libelle <> 'FIN'), on affiche l'article courant puis on passe à l'article suivant.

Si le stock n'est pas vide, les instructions Stock.Debut et Stock.ArticleSuivant figurant avant la boucle placent le curseur sur le premier élément.

S'il est vide, libelle contiendra la chaîne 'FIN' dès le départ et la boucle d'affichage ne sera pas exécutée.

Propriétés graphiques

Revenons à présent sur les composants graphiques et leurs « attributs étranges ». Tout peut à présent s'expliquer grâce aux propriétés. Par exemple, pour une zone de texte ZT, l'écriture ZT.Text, ne représente pas un attribut, mais une propriété. Cela explique comment une affectation comme ZT.Text := 'Truc' provoque l'affichage de « Truc » à l'écran : c'est l'accesseur en écriture de la propriété ZT qui fait ceci !

En fait, lors de la compilation, l'instruction ZT.Text := 'Truc' est traduite en un appel de l'accesseur en écriture de ZT et non pas en une affectation.

On peut expliquer de la même manière comment la modification de la propriété width d'un contrôle va immédiatement modifier sa largeur à l'écran. Ainsi, toutes les affectations à des « attributs » de composant qui provoquent des affichages sont en fait des affectations à des propriétés.

Héritage, surcharge et polymorphisme

La notion d'héritage a déjà été abordée dans le cours I sur la programmation objet, mais simplement du point de vue de l'utilisation.

Dans cette partie du cours, nous allons voir comment définir explicitement une relation d'héritage entre deux classes, puis comment exploiter de la meilleure manière cette relation par la surcharge et le polymorphisme.

Présentation de l'exemple

L'exemple de projet qui nous servira à introduire ces notions se trouve dans le dossier Exemple-ProgObjet2/Formes.

Il contient la définition d'une classe Cercle comme sous-classe d'une autre classe nommée Forme.

Toute forme possède une aire. Un cercle est une forme spéciale qui possède également un rayon.

Voici l'interface graphique de ce projet :

Le bouton Nouvelle Forme permet de générer une forme d'aire aléatoire.

Le bouton Nouveau Cercle permet de générer un cercle de rayon aléatoire.

Chaque forme générée (cercle ou forme quelconque) est stockée dans un même tableau TF, dont les éléments sont de type Forme. Le nombre de formes générées est stocké dans un entier NF. TF et NF sont des variables globales dont voici la déclaration :

Notez qu'il est impossible, en principe, de stocker dans un même tableau des données de types différents. Nous verrons un peu plus loin ce qui permet ce miracle.

Le bouton Afficher permet d'afficher les attributs d'une forme donnée en donnant son numéro (l'indice de l'élément du tableau dans lequel elle est rangée).

La figure précédente montre l'affichage de la forme numéro 3 : c'est une instance de la classe Forme dont l'aire est égale à 71.

Voici l'affichage de la forme numéro 2 :

Il s'agit cette fois-ci d'un cercle.

Interfaces des deux classes

Voici les interfaces des deux classes :

La classe Forme possède deux méthodes :

- Create est le constructeur de la classe. Il permet de créer une forme d'aire donnée (paramètre A) ;

- AjouterForme range une forme dans le tableau TF, incrémente le nombre de formes NF et affiche sa valeur ;

- AfficherAttributs permet d'afficher les attributs d'une forme : son aire et sa classe (Forme).

La classe Cercle est définie comme une sous-classe de la classe Forme. Elle possède trois méthodes :

- Create est le constructeur de la classe. Il permet de créer un cercle de rayon donné (paramètre R) ;

- AfficherAttributs permet d'afficher les attributs d'un cercle : son aire, sa classe (Cercle) et son rayon ;

- CalculAire permet de calculer l'aire d'un cercle.

Implémentation de la classe Forme

Voici l'implémentation de la classe Forme :

Dans le corps de la méthode AjouterForme apparaît un mot-clé self.

Ce mot-clé peut être utilisé dans toute méthode pour désigner l'instance à laquelle la méthode s'applique. Il représente plus précisément son adresse.

Ici, self représente l'adresse de la forme à laquelle on applique la méthode AjouterForme.

L'affectation TF[NF]:=Self ajoute donc la forme (en fait son adresse) dans le tableau TF, à la suite des formes déjà mémorisées.

Dans la méthode AfficherAttributs figure l'appel de la méthode ClasseName. Cette méthode retourne le nom de la classe à laquelle appartient un objet quelconque. C'est en fait une méthode de la classe TObject dont, rappelons-le, toute classe Pascal est une sous-classe. Elle peut donc être appliquée à une instance de la classe Forme.

Implémentation de la classe Cercle

Voici l'implémentation de la classe Cercle :

L'affichage des attributs d'un cercle est assez similaire à l'affichage des attributs d'une forme. Il suffit de rajouter l'affichage du rayon.

Pour éviter d'écrire deux fois le même code, nous avons utilisé ici la surcharge.

Dans le corps de la procédure AfficherAttributs figure l'instruction inherited AfficherAttributs. Le mot-clé inherited permet de préciser qu'il s'agit de la méthode AfficherAttributs de la classe mère.

En appelant cette méthode, on obtient donc l'affichage de l'aire du cercle et de sa classe.

De manière générale, une méthode est surchargée si elle appelle une méthode de même nom de la classe mère. C'est un mécanisme qui permet de spécialiser une méthode sans écrire deux fois le même code.

Utilisation des deux classes

Voici les procédures évènementielles associées aux boutons de l'interface graphique :

Affichage d'une forme

L'affichage d'une forme donnée est réalisé par la procédure Bt_AfficherClick.

Elle commence par lire le numéro (num) de la forme à afficher, c'est-à-dire l'indice de l'élément du tableau TF dans lequel cette forme est stockée.

TF[num] contient donc un objet de la classe Forme ou un objet de la classe Cercle.

Pour distinguer les deux cas, nous avons utilisé l'opérateur is, qui permet de savoir si un objet appartient à une classe donnée.

Si un objet appartient à une classe, il appartient également à sa classe mère !

Si TF[num] est une instance de la classe Cercle, nous ne pouvons pas appliquer directement la méthode AfficherAttributs pour l'afficher.

En effet, bien que TF[num] contienne un objet de la classe Cercle, il est considéré par le compilateur comme un objet de la classe Forme car le tableau TF a été déclaré comme un tableau de Forme.

Pour forcer le compilateur à interpréter TF[num] comme un objet de la classe Cercle, nous avons utilisé le polymorphisme, qui se traduit en Pascal par l'opérateur as.

TF[num] as Cercle signifie que le compilateur doit interpréter TF[num] comme un cercle. De ce fait, la méthode AfficherAttributs appliquée à cet objet sera bien celle de la classe Cercle.

De manière générale, le polymorphisme est le fait de pouvoir forcer le compilateur à interpréter un objet d'une certaine classe (à la déclaration) comme un objet d'une classe fille de cette classe, de manière à pouvoir lui appliquer les méthodes de la classe fille.

Création d'un cercle

La génération aléatoire d'un cercle et son stockage dans le tableau TF est réalisée par la procédure Bt_NouveauCercleClick.

Elle illustre un autre aspect du polymorphisme. En effet, lorsque l'on applique la méthode AjouterForme au cercle généré, cela provoque (voir le code de la méthode AjouterForme) son stockage dans un élément du tableau TF.

Or ces éléments sont a priori tous des objets de la classe Forme !

En fait, cela ne pose pas de problème au compilateur car TF ne contient que des pointeurs.

L'instruction TF[NF]:=self ne fait rien d'autre que stocker l'adresse de l'objet dans le tableau TF.

Exercices et exemples

Téléchargez les exercices :

- Programmation objet - Cours II.

Pour obtenir les corrigés ainsi que les exemples, le téléchargement est possible via un login et un mot de passe, que vous pouvez obtenir en envoyant un mail à l'adresse suivante :

en précisant un peu qui vous êtes et les raisons pour lesquelles ce cours vous intéresse.

Note de la rédaction de Developpez.com

Nous remercions FRANOUCH, f-leb et Claude Leloup pour leur relecture.