Il est nécessaire de reprendre cette déclaration dans toutes les sous-classes qui peuvent faire appel aux abréviations qu'elle autorise : les dictionnaires partagés ne sont jamais hérités des classes mères. Nous verrons plus loin que le dictionnaire *CharacterConstants* est de nouveau mentionné dans la définition de la classe *FinancialStream*.

```k language="k language="k language="k language="k
Stream subclass: #WriteStream
instanceVariableNames:
    'writeLimit
classVariableNames: ''
poolDictionaries:
    'CharacterConstants
```

<caption>Figure 5.25 : définition de la classe *WriteStream*</caption>

## Particularité du retour à la ligne dans la classe *FinancialStream*

La méthode *cr* est redéfinie dans la classe *FinancialStream* pour tenir compte des codages particuliers de la marque de fin de ligne dans les systèmes UNIX et MS-DOS (figure 5.26).

Le changement de ligne correspond à deux opérations mécaniques :

- ramener le curseur en début de ligne (*Cr*);

- déplacer le curseur sur la ligne suivante (*Lf*).

Certains systèmes d'exploitation interprètent automatiquement le caractère *Lf* (retour ligne) comme une paire *Cr-Lf*. D'autres exigent au contraire la paire *Cr-Lf* pour passer à la ligne suivante. Il est important de savoir quel est le standard sur lequel on travaille pour gérer correctement les fins de ligne. La classe *FinancialStream* propose donc une variable d'instance, *lineDelimiter*, dans lequel est mémorisé le caractère reconnu comme marque de fin de ligne par le système d'exploitation. Les méthodes de même nom permettent d'obtenir ou de modifier la valeur de cette variable.

```
cr "méthode de la classe FinancialStream"
```

```
"Write the line terminating character (carriage-return line-feed pair or line-feed) to the
receiver stream."
```

```
self lineDelimiter == Cr ifTrue: [self nextPut: Cr].
```

```
self nextPut: Lf
```

<caption>Figure 5.26 : définition de la méthode *cr* dans la classe *FinancialStream*</caption>

La méthode *cr* vérifie si le caractère de fin de ligne est *Cr* (message *lineDelimiter*). Si c'est le cas, alors le système d'exploitation impose la paire *Cr-Lf* comme marque de fin de ligne. La méthode *Cr* envoie alors le caractère *Cr*. Dans tous les cas, elle envoie ensuite le caractère *Lf*.
## Les variables d'instance de la classeinde la classe *FEleStream*

La variable *LineDelimiter* n'est pas la seule qui soit définie au niveau de *FEleStream* (figure 5.27). Les instances de cette classe manipulant des fichiers, elles doivent s'adapter à la manière dont sont gérés ces fichiers en informatique.

FEleStream subclass: #FEleStream

instanceVariableNames:

'file pageStart writtenOn lastByte lineDelimiter '

classVariableNames: ''

poolDictionaries:

'CharacterConstants '

<caption>Figure 5.27 : définition de la classe *FEleStream*</caption>

Une imprimante reçoit les textes à imprimer caractère par caractère. Par contre, un disque communique avec le processeur de l'ordinateur en échangeant des blocs de caractères (granule, secteur, tampon). De cette façon, les pertes de temps induites par les accès mécaniques au disque sont réduites. En contrepartie, cela implique quelques contraintes pour le système d'exploitation :

- la gestion du disque doit se faire en tenant compte de ce partage en secteurs;

- les opérations de lecture d'un fichier doivent être capables de trouver son premier secteur et d'enchaîner les suivants;

- lors d'une écriture, il faut attendre que la zone tampon correspondant en mémoire à un secteur soit pleine avant de pouvoir l'écrire sur le disque;

- si la lecture d'un seul octet est demandée, il faut pouvoir retrouver le secteur dans lequel se situe cet octet, le charger en mémoire dans une zone tampon (« buffer »), puis calculer sa position relative;

- il faut faire de même pour modifier un caractère, sans oublier de réécrire ensuite la totalité du secteur sur le disque.

La plupart du temps, ces opérations sont transparentes et réalisées par le système d'exploitation. Ce n'est pas le cas pour Smalltalk V sous MS-DOS qui gère lui-même la zone tampon correspondant au secteur lu sur le disque (512 octets).

Pour repérer sa position dans le fichier, la classe *FEleStream* dispose de quatre variables d'instance héritées ou définies à son niveau :

*lastByte* correspond à la position du dernier octet du fichier;

*writeLimit* contrairement à la signification qu'elle avait dans les autres classes, cette variable d'instance représente la position du dernier caractère accessible *dans le secteur* en cours de lecture (ou d'écriture);

*pageStart* repère la position dans le fichier du premier caractère du secteur en cours de lecture (ou d'écriture);

*position* représente la position relative du flux dans le secteur en cours de lecture ou d'écriture.
## La sauvegarde sur disque du fichier

Cette opération n'est pas automatiquement faite par Smalltalk V : si l'utilisateur reste dans le même secteur (au sens MS-DOS du terme), les modifications qu'il peut écrire ne seront prises en compte qu'à réception de deux instructions :

* `close` qui écrit le secteur courant et ferme le fichier

* `flush` qui écrit le secteur courant sans fermer le fichier.

Les opérations d'écriture sur disque étant relativement longues, les conceptrons de Smalltalk V ont rajouté une nouvelle variable d'instance, `writtenOn`, qui signale si le secteur courant a été modifié ou non depuis la dernière écriture sur disque. Ce booléen est systématiquement testé et remis à jour lors de la sauvegarde sur disque du secteur courant.

La dernière variable d'instance qu'il nous reste à voir est *file*, qui désigne l'instance de la classe *File* sur laquelle le flux se déplace (c'est en quelque sorte la carte d'identité du fichier pour Smalltalk). L'envoi du message *close* (comme *flush*) à une instance de *FileStream* provoque une instruction *file close* (ou *file flush*) qui referme le fichier désigné par cette variable d'instance.
<figure><img src="image_33.png" /></figure>
# 6 - Réaliser une application avec IBM Smalltalk

Nous allons maintenant, à travers un exemple, montrer comment on peut, avec IBM Smalltalk, construire une application complète. Notre objectif est de montrer la puissance et le confort apportés par l'environnement de la programmation orientée objets. Il est aussi d'illustrer la nouvelle approche de cet environnement qui favorise une programmation incrémentale.

## 6.1 - Quelle application ?

Nous souhaitons ici réaliser une application qui ne se limite pas à un exercice pédagogique et qui correspondra à un logiciel qui pourrait réellement être utilisé, même si, dans le cadre de ce chapitre, nous n'en construirons qu'une maquette. Nous allons reprendre l'étude du dictionnaire analogique, que nous avions abordée en 3.5.4, et construire une application qui permette à un utilisateur de consulter et de mettre à jour un tel dictionnaire. Cette application pourrait par exemple être couplée à l'utilisation d'un traitement de texte, permettant ainsi de modifier la rédaction des textes traités, grâce aux indications fournies par le dictionnaire.

IBM Smalltalk est un logiciel qui fonctionne sous plusieurs environnements : Windows, OS/2, AIX. Dans la suite de ce chapitre, nous avons choisi de présenter une application sous Windows, l'un des environnements les plus courants. Le lecteur pourra ainsi expérimenter facilement cette application.

### 6.1.1 - Le cahier des charges

Précisons ici les objectifs de l'application. La première contrainte sera de permettre à l'utilisateur, entre deux sessions de travail, de conserver son dictionnaire. Il faut donc que celui-ci soit enregistré dans un fichier et l'application pourra charger, dans un objet, le contenu d'un tel fichier et, bien entendu, effectuer l'opération inverse. Ainsi, on sera en mesure de charger le dictionnaire en début
de session et le sauvegarder en fin de travail. On pourra aussi, en cours de session, changer de dictionnaire, si besoin est<fnref n="1" />.

Une autre contrainte sera que l'application puisse être utilisée à travers une interface graphique. On peut ainsi imaginer que l'application, une fois lancée avec l'indication d'un fichier-dictionnaire de départ, charge ce fichier dans un objet et ouvre ensuite une fenêtre à l'écran. Dans cette fenêtre, l'utilisateur pourra consulter la liste des analogies d'un mot donné, passer d'une analogie à la suivante<fnref n="2" />, modifier le dictionnaire, changer de dictionnaire, etc. Mais nous ne réaliserons ici qu'une maquette de l'application, et dans la suite, nous laisserons volontairement de côté les détails indispensables à l'obtention d'un produit fini, comme par exemple la vérification d'existence des fichiers. Attirons cependant l'attention du lecteur sur le fait que ces opérations sont avec IBM Smalltalk, moins fastidieuses qu'avec un langage classique : nous en donnerons un exemple dans le paragraphe 6.2.3.

## 6.1.2 - La structure des fichiers-dictionnaires

Un fichier-dictionnaire sera un fichier dans lequel on conservera de manière permanente les informations d'un dictionnaire analogique. Nous choisirons d'y enregistrer les informations en mode « texte ». Ainsi, ce fichier pourra éventuellement être modifié directement par un éditeur de texte, à condition de respecter les conventions de présentation des informations.

Nous choisirons ces conventions pour que le fichier soit lisible. Une analogie étant représentée par un mot-clé *m* et une liste de mots considérés comme proches de *m*, on la représentera par deux lignes dans le fichier, la première donnant le mot *m* précédé par le libellé *analogies pour*, la seconde, commençant par un retrait de deux espaces pour améliorer la lisibilité, donnera la liste des mots proches de *m*.

analogies pour amour
passion tendresse
analogies pour tendresse
amour
analogies pour rage
colère
analogies pour colère
fureur rage
analogies pour fureur
colère
analogies pour passion
amour

<caption>Figure 6.1 : un exemple de fichier-dictionnaire</caption>

<footnote n="1">1 On peut en effet imaginer qu'un utilisateur se serve de plusieurs dictionnaires, en regroupant des analogies par thèmes, ou encore utilise alternativement, avec le logiciel, un dictionnaire analogique et un dictionnaire des synonymes.</footnote>

<footnote n="2">2 Si « rage » est une analogie pour « colère », on peut souhaiter consulter les analogies de « rage » et passer ainsi de proche en proche, etc.</footnote>
La figure 6.1 donne un exemple de fichier. Une telle structure permet, bien évidemment, de créer le fichier avec un éditeur quelconque. Nous n'imposerons pas à un tel fichier de présenter les informations dans l'ordre alphabétique des mots-clés. En effet, le chargement du fichier dans un objet de la classe *DicoAnalogies* s'effectuera par hachage des mots-clés (cf. 4.9.2) et ne serait donc pas facilité par le fait que le fichier soit ordonné. Cela ne nous empêchera pas, au niveau de l'interface graphique, de présenter la liste ordonnée des mots-clés.

## 6.2 - Une classe, comme support de l'application

En Smalltalk, une application est toujours représentée par un objet qui est l'instance d'une classe qui intervient dans les traitements réalisés par l'application. Cet objet sera considéré comme le « propriétaire » de l'application, *model* ou *application model* dans la terminologie anglaise de Smalltalk. Ici, cet objet sera une instance de la classe *DicoAnalogies* et il représentera le dictionnaire manipulé. Nous avons déjà défini cette classe en 3.5.4. Reprenons cette définition, en ajoutant la variable d'instance *nomDuFichier* dont le rôle sera de désigner le fichier permanent associé au dictionnaire<fnref n="3" /> :

```
Dictionary subclass: #DicoAnalogies
instanceVariableNames: 'nomDuFichier'
classVariableNames: ''
poolDictionaries: ''
```

Un objet de la classe *DicoAnalogies* est donc désormais un dictionnaire (par héritage de *Dictionary*) disposant d'une variable d'instance nommée. Examinons de plus près l'impact de l'adjonction d'une nouvelle variable d'instance.

### 6.2.1 - La nouvelle variable d'instance et l'héritage

Définissons tout d'abord une méthode pour attribuer une valeur à la variable d'instance *nomDuFichier* :

```
nomDuFichier: uneChaine
    "affecte uneChaine à la variable d'instance nomDuFichier"
nomDuFichier := uneChaine
```

puis évaluons, avec *Execute*, la séquence de la figure 6.2 :

```
|d| d := DicoAnalogies new.
    d nomDuFichier: 'E:\chap6\dico1.txt'.
    d ajouteAnalogieEntre: 'fragile' et: 'frêle'.
    d inspectionStandard
```

<caption>Figure 6.2 : une séquence pour analyser la création d'un dictionnaire d'analogies</caption>

<footnote n="3">³ Une première définition de cette classe a été donnée en 3.5.4. Pour la modifier, on utilisera une fenêtre d'examen des classes (Classes Browser en IBM Smalltalk) dans laquelle on aura sélectionné la classe *DicoAnalogies*. En cliquant sur le nom de cette classe, on fera apparaître, dans le panneau du bas la définition de la classe. Il suffira alors, de modifier cette définition puis de sélectionner l'option « save » de ce panneau.</footnote>
On pourra se reporter au paragraphe 3.5.5 pour la description de la méthode *inspectionStandard* qui permet d'examiner complètement<fnref n="4" /> l'objet *d*. La figure 6.3 montre deux fenêtres d'inspection obtenues par l'évaluation de la séquence précédente.

Dans la figure 6.3, la première fenêtre est la fenêtre d'inspection sur l'objet *d*, ouverte par l'évaluation de la séquence précédente. La deuxième fenêtre est la fenêtre d'inspection de la variable *associationSet*, obtenue en double cliquant sur la variable *associationSet* dans le panneau d'inspection précédent.

<figure><img src="image_34.png" /></figure>

<figure><img src="image_35.png" /><figcaption>Figure 6.3 : les variables *associationSet* et *nomDuFichier* de l'objet *d*</figcaption></figure>

La première fenêtre indique que la valeur de la variable d'instance *nomDuFichier* est 'E:\chap6\dico1.txt'. C'est bien ce que nous pouvions attendre puisque la séquence évaluée affecte la chaîne 'E:\chap6\dico1.txt' à cette variable.

Dans la deuxième fenêtre, la variable d'instance *elementCount* est sélectionnée. Sa valeur indique que le dictionnaire a enregistré deux analogies. Ce sont les analogies 'frêle' ↔ 'fragile' et 'fragile' ↔ 'frêle'. On pourrait vérifier qu'elles sont

<footnote n="4">4 L'utilisation de inspect, héritée de Dictionary, ne montrerait que les associations du dictionnaire et ne ferait pas apparaître ses variables d'instances nommées. En particulier, on ne pourrait examiner la variable *nomDuFichier*.</footnote>
enregistrées respectivement dans les variables d'instance indexées 6 et 9 : il suffirait, pour cela, de cliquer successivement sur les numéros de ces deux variables dans le panneau de gauche.

On peut être surpris que cette fenêtre d'inspection fasse apparaître onze variables d'instance indexées alors que la méthode *new* utilisée pour créer le dictionnaire, crée un objet sans variables d'instance indexées. Pour comprendre l'origine du problème, il faut étudier de plus près les classes *Dictionary* et *EsLinearvíHashSet*.

Pour conserver au hachage une efficacité suffisante, chaque ajout d'une association à une instance de la classe *Dictionary* par la méthode *add*: (respectivement par la méthode *at:put:*) déclenche, l'envoi du message *dictAdd*: (respectivement du message *dictAt:put:*) à la collection des clés du dictionnaire, instance de la classe *EsLinearvíHashSet*. Ce message appelle, si nécessaire, la méthode *growEmptyBy*: de la classe *EsLinearHashSet*, qui provoque un agrandissement de la capacité du dictionnaire. Or, dans la séquence de la figure 6.2, le message :

```
d ajouteAnalogieEntre: 'fragile' et: 'frêle'.
```

fait deux ajouts successifs au dictionnaire. Lors du premier ajout, la taille du dictionnaire est fixée à onze éléments. Puis lors des ajouts suivants, la condition qui, dans la méthode *dictAdd*, déclenche l'envoi du message *growEmptyBy*: est :

```
 elementCount := elementCount + 1) > (elementsSize // 2)
```

on peut donc affirmer que :

- après le premier ajout et le deuxième ajout, le nombre d'associations dans le dictionnaire (2 respectivement 4 associations) sera inférieur à 11//2 (c'est à dire 5), la taille de d restera de onze éléments,

- après le troisième ajout, le nombre d'associations dans le dictionnaire (6 associations) sera supérieur à 5 et le dictionnaire s'agrandira.

### 6.2.2 - L'interface avec les fichiers

Nous avons expliqué, en 6.1.2, comment nous souhaitions pouvoir conserver un dictionnaire dans un fichier permanent et nous avons précisé la structure d'un tel fichier. Ajoutons maintenant à la classe *DicoAnalogies* les deux méthodes d'instance permettant de charger et conserver un dictionnaire (*chargeDuDisque* et *sauveSurDisque*). Elles sont présentées aux figures 6.4 et 6.5.

Comme son nom l'indique, la première de ces méthodes ouvre un fichier-dictionnaire dont le nom est obtenu en interrogeant l'utilisateur (dialogue via une fenêtre *CwTextPrompter*). Ce fichier est ensuite lu, ligne par ligne, et, conformément à la structure choisie (cf. l'exemple de la figure 6.1), les informations contenues dans chaque groupe de deux lignes sont enregistrées sous la forme d'une association dans le dictionnaire-récepteur. On remarquera que :

- le nom du fichier est conservé dans la variable *nomDuFichier* (il sera réutilisé pour la sauvegarde),
- un flux sur le fichier est ouvert en envoyant le message *athyName*: à la classe *EstdCodeStream*. Cette classe possède des fonctionnalités similaires à la classe *Stream* et ses descendantes de *Smalltalk/V* (cf. chapitre 5).

- des contrôles élémentaires sont effectués sur la structure du fichier : y-a-t-il bien trois mots sur la première ligne de chaque groupe (car la clé de l'association est le troisième mot) ? La deuxième ligne existe-t-elle (si le fichier n'a pas un nombre pair de lignes, il est incorrect) ? En cas d'échec de ces contrôles, le chargement est interrompu et la méthode renvoie *false*, indiquant ainsi que l'opération n'a pas pu être correctement effectuée.

chargeDuDisque

"détermine le nom du fichier de chargement et garnit le dictionnaire récepteur;
renvoie true si tout s'est bien passé, false si le fichier était de format incorrect"

|fichier ligne1 ligne2|

nomDuFichier := CwTextPrompter
prompt: 'nom du fichier de chargement ?'
answer: 'd:\chap6\analogie.txt'.

fichier := EstdCodeStream pathname: nomDuFichier.

"Chaque association est décrite par deux lignes dans le fichier"

[fichier atEnd] whileFalse:

["première ligne: la clé de l'association est le troisième mot"

"la méthode subStrings de la classe String en IBM Smalltalk est analogue
à la méthode asAoOfSubstrings de la classe String en Smalltalk/V"

lignel := fichier fileStream nextLine subStrings.

(lignel size < 3) ifTrue: [^false].

fichier atEnd ifTrue: [^false]. "car il manque la deuxième ligne"

"deuxième ligne: elle contient la chaîne des analogies"

ligne2 := fichier fileStream nextLine trimBlanks.

self at: (lignel at: 3) put: ligne2].

^ true

<caption>Figure 6.4 : interface avec les fichiers (chargement)</caption>

La méthode présentée à la figure 6.5 est la méthode *sauveSurDisque*. Le fichier de sauvegarde est celui indiqué par la variable d'instance *nomDuFichier*. Si celle-ci est encore indéterminée, la méthode *l'initialise* en demandant à l'utilisateur d'indiquer un nom de fichier<fnref n="5" />. Chaque association est sauvegardée dans ce fichier, sous la forme de deux lignes, conformément à la structure choisie. Le lecteur qui aurait déjà une large pratique de l'utilisation de la méthode *athyName*: de la classe *EstdCodeStream* pourra objecter que, si la taille du fichier diminue entre deux sauvegardes, la réécriture du fichier sera incorrecte car elle laissera subsister, en fin de fichier, des informations de la version précédente. Nous remédierons à cet inconvénient en améliorant la gestion des fichiers de sauvegarde, au paragraphe suivant.

<footnote n="5">5 On peut en effet imaginer qu'un dictionnaire soit sauvegardé sans avoir été chargé lorsqu'il s'agit d'une première création par l'utilisateur.</footnote>
### sauveSurDisque

```
"recopie le dictionnaire récepteur dans le fichier désigné par nomDuFichier"
| sauvegarde "représentera le fichier de sauvegarde" |
"on vérifie tout d'abord que l'utilisateur a bien choisi un fichier"
nomDuFichier isNil
ifTrue:
    [nomDuFichier := CwTextPrompter
        prompt: 'nom du fichier de sauvegarde ?'
        answer: 'd:\chap6\analogie.txt'.]
sauvegarde := EstdCodeStream pathname: nomDuFichier.
self associationsDo:
    [:couple "désignera chaque analogie enregistrée" |
        (sauvegarde fileStream)
        nextPutAll: 'analogies pour ', couple key; cr;
        nextPutAll: ' ', couple value; cr].
sauvegarde close.
```

<caption>Figure 6.5 : interface avec les fichiers (sauvegarde)</caption>

Pour l'instant, si l'on fait abstraction de la remarque précédente, on peut vérifier le fonctionnement de ces deux méthodes. Supposons que le fichier traité est le fichier *dicol.txt*, dont l'état initial est représenté sur la partie gauche de la figure 6.6 et évaluons la séquence S :

```
"Séquence S"
|d| d:= DicoAnalogies new.
    d chargeDuDisque.
    d ajouteAnalogieEntre: 'ire' et: 'colère'.
    d sauveSurDisque
```

A l'issue de l'évaluation, le contenu du fichier *dicol.txt* sera celui qui est représenté sur la partie droite de la figure 6.6.

<table><caption>Figure 6.6 : le fichier *dicol.txt* avant et après l'évaluation de la séquence S</caption><thead><tr><th>fichier dicol.txt (état initial)</th><th>fichier dicol.txt (état final)</th></tr></thead><tbody><tr><td>analogies pour amour</td><td>analogies pour colère</td></tr><tr><td>passion tendresse</td><td>ire</td></tr><tr><td>analogies pour passion</td><td>analogies pour amour</td></tr><tr><td>amour</td><td>passion tendresse</td></tr><tr><td>analogies pour tendresse</td><td>analogies pour tendresse</td></tr><tr><td>amour</td><td>amour</td></tr><tr><td>analogies pour passion</td><td>analogies pour passion</td></tr><tr><td>amour</td><td>amour</td></tr><tr><td>analogies pour ire</td><td>analogies pour colère</td></tr><tr><td>colère</td><td></td></tr></tbody></table>
### 6.2.3 - L'environnement objets facilite la programmation incrémentale

Dans les deux méthodes construites au paragraphe précédent, nous n'avons pas pris en considération toutes les précautions de manipulation possibles lors du chargement ou de la sauvegarde du fichier-dictionnaire. Sans vouloir les traiter ici de manière exhaustive, nous allons montrer sur un exemple comment la programmation orientée objets facilite un enrichissement progressif du développement.

Intéressons-nous à la méthode *sauveSurDisque* et supposons que nous désirions, par mesure de précaution, si le fichier de sauvegarde existe déjà, conserver sa version antérieure dans un fichier de même désignation, mais dont le suffixe sera *bak*. Si nous supposons que le nom du fichier de sauvegarde (indiqué par la variable *d'instance nomDuFichier*) n'a pas, lui-même, le suffixe *bak*, les opérations à effectuer sont les suivantes :

- s'il existe déjà un fichier de même désignation et de suffixe *bak*, détruire ce fichier,

- renommer le fichier de sauvegarde en lui donnant le suffixe *bak*.

On pourra ensuite recopier le contenu du dictionnaire dans le fichier désigné par la variable *nomDuFichier*.

Nous allons supposer que la variable *nomDuFichier* donne toujours le nom complet du fichier c'est-à-dire qu'elle a pour valeur une chaîne de caractères qui indique le chemin d'accès au fichier, la désignation du fichier et son suffixe<fnref n="6" />. Par exemple la valeur *d:\chap6\analogie.txt* indiquera, par le chemin *d:\chap6*, que le fichier est sur le répertoire *chap6* du disque *d:*, qu'il a la désignation *analogie* et que son suffixe est *txt*.

Pour effectuer la modification souhaitée de la méthode *sauveSurDisque*, il suffit d'ajouter, dans cette méthode, après l'établissement de la valeur correcte de la variable *d'instance nomDuFichier*, l'évaluation de :

```
nomDuFichier nomDeFichierChangeEnBak
```

Dans cette expression, la méthode *nomDeFichierChangeEnBak* est une méthode *d'instance de la classe String* qui, adressée à la chaîne représentant le nom du fichier, effectue le changement de suffixe. Bien entendu, cette méthode n'est pas prédéfinie dans l'environnement de Smalltalk mais il est simple de la construire. Nous la présentons à la figure 6.7.

<footnote n="6">6 Ceci suppose que la saisie du nom du fichier, lors de l'évaluation de *chargeDuDisque* ou *sauveSurDisque*, assure que cette condition est réalisée. Cela constitue encore une amélioration possible de ces deux méthodes, que nous n'étudierons pas ici.</footnote>
<figure><img src="image_36.png" /><figcaption>Figure 6.7 : une nouvelle méthode d'instance de la classe String</figcaption></figure>

Commentons maintenant les opérations effectuées par cette méthode.

- La première expression qui sera évaluée enverra à la classe *EtFileNamePrompter* un message *splitPath*: avec, comme argument, la chaîne réceptrice (c'est-à-dire un nom complet de fichier). En réponse, la classe *EtFileNamePrompter* décomposera ce nom en séparant le chemin d'accès et le nom du fichier qu'elle placera respectivement dans les deux éléments d'un tableau.

- Ensuite, les variables chemin et désignation sont initialisées à partir de ce tableau pour désigner respectivement le nom complet du répertoire et la désignation du fichier (la méthode nomDeFichier est décrite dans la suite de ce chapitre).

- L'opération suivante examine le répertoire désigné par la variable chemin et supprime, s'il existe sous ce répertoire, le fichier de même désignation que le fichier de départ, mais dont le suffixe est bak. Cet examen s'effectue en deux temps. Tout d'abord, en adressant le message *opendir:pattern:mode*: à la classe *CfsDirectoryDescriptor*: le premier paramètre correspond au répertoire de recherche, le deuxième paramètre est le nom du fichier ou du répertoire recherché et le troisième paramètre (*FREG*) indique que l'on recherche un fichier (si l'on avait recherché un repertoire, on aurait passé la constante *FDIR* en troisième paramètre). Le message précédent renvoie un objet de type *CfsDirectoryDescriptor* qui reçoit le message *noMatch* ; ce message renvoie *true* si aucun fichier n'a été trouvé sous le répertoire spécifié.
- La dernière opération envoie enfin un message *rename:new*: la classe *CfsFileDescriptor*. Ce message correspond une méthode de classe qui permet de changer le nom d'un fichier. Ici, seul le suffixe est modifié : il devient *bak*.

Il nous reste indiquer cependant que la méthode *nomDeFichier*, permet partir du nom complet d'un fichier (désignation + suffixe), de récupérer la désignation du fichier. Cette nouvelle méthode de la classe *String* est présentée ci-après :

```
nomDeFichier "méthode d'instance de String"
"renvoie la désignation"
| chaine |
chaine := self trimBlanks.
(chaine = (String with: $.) or:
  [chaine = (String with: $. with: $.)])
  ifFalse: [
    chaine := (ReadStream on: chaine)
    upTo: $..
    chaine size = 0
    ifTrue: [self error: 'file name missing'].
    chaine size > 8
    ifTrue: [self error: 'file name too long']].
  ^ chaine
```

<caption>Figure 6.8 : la méthode d'instance *nomDeFichier* pour la classe *String*</caption>

Les exemples que nous venons de traiter montrent qu'il est facile, avec la programmation orientée objets, d'enrichir un logiciel en programmant par étapes. Le lecteur peut par exemple imaginer, que dans la recherche d'une gestion de fichiers toute épreuve, la prochaine étape serait de s'assurer, dans la méthode *chargeDuDisque*, que le fichier de chargement indiqué par l'utilisateur est bien un fichier existant<fnref n="7" />. Avec ce que nous venons de voir, on peut aisément concevoir comment définir une méthode de classe de la classe *String*, que l'on pourrait appeler *nomDeFichierExistant* et qui, travers un dialogue éventuellement répétitif avec l'utilisateur, renverrait une chaîne représentant le nom d'un fichier existant.

En conclusion de ce paragraphe, nous souhaitons dégager quelques particularités du développement d'applications dans un environnement orienté objets :

- la programmation orientée objets apporte une nouvelle approche incrémentale de la programmation, qu'il ne faut pas confondre avec l'analyse descendante de la programmation classique.

<footnote n="7">7 En effet, si l'utilisateur frappe par erreur un nom de fichier qui ne correspond pas à un fichier existant, l'évaluation, dans la méthode *chargeDuDisque*, de :

fichier := EstdCodeStream pathname: nomDuFichier.
renverra l'objet nil.</footnote>
- Sans exclure une conception descendante, la programmation orientée objetspermet aussi une démarche ascendante qui va *habiller* progressivement une application.

- Le très haut degré de réutilisation des composants (méthodes ou classes) facilite cette programmation incrémentale. Chaque nouveau composant construit devient partie intégrante de l'environnement de développement, ce qui augmente encore les possibilités de réutilisation.

- Les méthodes sont, de par la puissance d'expression des mécanismes de la programmation orientée objets, toujours concises et très lisibles. Le code d'une méthode excède très rarement la vingtaine de lignes.

## 6.3 - L'interface graphique de l'application

Définissons maintenant les outils qui sont nécessaires à l'application pour permettre à l'utilisateur de consulter un dictionnaire analogique à travers une interface graphique.

### 6.3.1 - Faire apparaître une fenêtre dédiée à l'application

La plupart des applications sous Windows sont multi-fenêtres, c'est-à-dire qu'elles gèrent plusieurs fenêtres à la fois. Dans de telles applications, on peut distinguer trois types de fenêtres :

- la fenêtre principale, également appelée fenêtre de base ou fenêtre mère, elle représente le cadre de l'application ;

- les fenêtres filles ou fenêtres documents, elles sont toutes rattachées à la fenêtre mère ;

- la fenêtre client ou surface de travail, elle représente l'espace dans lequel sont confinées les fenêtres filles.

En Smalltalk, une fenêtre est un objet et IBM Smalltalk dispose de plusieurs classes dont les instances représentent des objets d'interface (widgets dans la terminologie IBM Smalltalk) tels que des fenêtres ou des boîtes-listes. Nous examinerons, à la fin de ce paragraphe, la hiérarchie de ces classes.

Pour l'instant, notons que, si nous souhaitons disposer d'une fenêtre pour l'application, il est nécessaire que cette fenêtre soit associée au dictionnaire : elle sera donc désignée par une variable d'instance de la classe *DicoAnalogies*. Nous sommes donc amenés à modifier la définition de la classe *DicoAnalogies* pour lui ajouter la variable d'instance *fenetre*. La définition de cette classe est la suivante :

```
Dictionary subclass: #DicoAnalogies
instanceVariableNames:
    'nomDuFichier
    fenetre '
classVariableNames: ''
poolDictionaries: ''
```
Il convient également, comme nous l'avons fait remarquer en 6.2.1, de définir la méthode d'instance qui permet d'affecter une valeur cette variable d'instance :

```
fenetre: uneFenetre
```

```
"affecte la valeur uneFenetre à la variable d'instance fenetre"
```

```
fenetre := uneFenetre
```

Nous pouvons maintenant définir la méthode qui fera apparaître la fenêtre l'écran. Cette méthode correspondra au message envoyer au dictionnaire proprietary (cf. début du paragraphe 6.2) de l'application. En réponse ce message, le dictionnaire créera une fenêtre (qui sera désignée par la variable d'instance *fenetre*) et la fera apparaître l'écran. Une première version, très rudimentaire, de la méthode nécessaire (*ouvrirFenetre*) est présentée la figure 6.9.

```
ouvrirFenetre
```

```
"ouvre une fenêtre pour l'application (version 1)"
```

```
| form |
```

```
fenetre :=
```

```
CwTopLevelShell createApplicationShell:'fenetrePrincipale'
```

```
argBlock:[:w |
```

```
w title:'Utiliser le dictionnaire ', nomDuFichier].
```

```
form := fenetre createForm: 'fenetreTravail' argBlock:nil.
```

```
form manageChild.
```

```
fenetre realizeWidget
```

<caption>Figure 6.9 : faire apparaître une fenêtre</caption>

Cette méthode crée un objet de classe *CwTopLevelShell*, dans la variable *fenetre*. Un tel objet représente le cadre d'une fenêtre Windows. Celle-ci possède une barre de titre laquelle est associé un menu système, une icône de minimisation et une icône de restauration de taille. Elle ne peut contenir qu'une seule fenêtre fille.

Dans la méthode *ouvrirFenetre*, le cadre de la fenêtre est créé par l'évaluation de

```
fenetre := CwTopLevelShell
```

```
createApplicationShell:'fenetrePrincipale'
```

```
argBlock:[:w |
```

```
w title:'Utiliser le dictionnaire ', nomDuFichier].
```

qui crée l'objet de classe *CwTopLevelShell* sans toutefois le faire apparaître l'écran. Cet objet reçoit ensuite le message *title*: qui fixe l'intitulé qui figurera dans la barre de titre de cette fenêtre. On définit également sous la barre de titre, une fenêtre fille unique, qui sera un objet de la classe *CwForm*, elle est obtenue en envoyant le message *createForm:argBlock*: la variable d'instance *fenetre*. Cette fenêtre est l'espace de travail de l'application, elle pourra elle-même être divisée en plusieurs sous-fenêtres comme nous le verrons dans les paragraphes suivants.

La méthode *manageChild* permet de spécifier que la géométrie de la fenêtre fille est gérée en fonction de la géométrie de la fenêtre mère : toute modification de taille ou de position de la fenêtre mère est répercutée sur la fenêtre fille.
Enfin, la dernière ligne de la méthode *ouvrirFenetre* affiche la fenêtre à l'écran. C'est le message *realizeWidget* qui est envoyé à la fenêtre mère qui déclenche l'affichage de la fenêtre complète. On peut vérifier maintenant le fonctionnement de la méthode en évaluant, avec *Execute*, la séquence :

```
|x| x:= DicoAnalogies new.
x chargeDuDisque.
x ouvrirFenetre
```

### 6.3.2 - Les différentes classes de fenêtres

Nous venons de voir comment ouvrir une fenêtre à l'écran. Cependant, nous ne pouvons rien faire dans cette fenêtre. Nous ne pouvons même pas introduire du texte sans rapport avec l'application. Avant d'améliorer, dans le paragraphe suivant, le couplage entre l'application et sa fenêtre, présentons brièvement les différentes classes de fenêtres proposées.

Tout composant d'interface ou *widget* est une instance de l'une des sous-classes des classes suivantes :

**CwShell** Les instances de cette classe ne possèdent qu'une seule fenêtre fille.

**CwComposite** Les instances de cette classe peuvent gérer plusieurs *Widgets* filles à la fois.

**CwPrimitive** Les instances de cette classe sont de « simples blocs de construction », elles ne possèdent pas de *Widgets* filles.

<figure><img src="image_37.png" /><figcaption>Figure 6.10 : la classe *CwWidget* et des classes descendantes</figcaption></figure>
Avec IBM Smalltalk, implémenter une interface graphique revient élaborer un arbre de *widgets*. Chaque *widget*, sauf celui qui est placé au niveau le plus haut dans la hiérarchie, a un parent. La construction de cet arbre se fait de haut en bas. On met d'abord en place un objet de type *CwShell*, puis on crée un enfant de ce *widget* de type *CwComposite*, enfin on insère dans le *widget* composite un ou plusieurs objets de type *CwComposite* ou *CwPrimitive*.

Par exemple, la fenêtre de la figure 6.11 contient deux panneaux : une boîte-liste gauche et une fenêtre d'édition de texte droite.

<figure><img src="image_38.png" /><figcaption>Figure 6.11 : Une application multi-fenêtres</figcaption></figure>

Cette application a été élaborée partir de l'arbre de *widgets* représenté ci-après la figure 6.12 :

<figure><img src="image_39.png" /><figcaption>Figure 6.12 : l'arbre de widgets</figcaption></figure>

### 6.3.3 - Faire apparaître des informations dans la fenêtre

Avec la méthode *ouvrirFenêtre* définie en 6.3.1 (version 1, figure 6.9), la fenêtre ouverte pour l'application ne présentait pas d'informations. Modifications-la pour y faire apparaître les informations du dictionnaire. Cette méthode devient alors :
## ouvrirFenetre

"ouvre une fenêtre pour l'application (version 2)"

| form chaine |

fenetre ::=

CwTopLevelShell createApplicationShell: 'fenetrePrincipale'
  argBlock: [:w |

w title: 'Utiliser le dictionnaire ', nomDuFichier].

form := fenetre createForm: 'fenetreTravail' argBlock:nil.

form manageChild.

fenetreTexte := form createText: 'panneauTexte'

argBlock: [: w |

w editMode: XmMULTILINEEDIT;

width: 300; height: 120;

borderWidth: 1].

fenetreTexte manageChild.

chaine := self montreDico.

fenetreTexte setString: chaine.

fenetre realizeWidget

Par rapport à la version 1, on définit une fenêtre fille de l'objet *form* de type *CwText*, c'est-à-dire un panneau dans lequel on pourra éditer du texte. Pour cela, le message *createText:argBlock*: est envoyé à l'objet *form*. Le premier paramètre indique le nom de la fenêtre texte (*panneauTexte*) et le deuxième paramètre spécifie les caractéristiques de la fenêtre texte (le mode d'édition, la largeur et la hauteur de la fenêtre, la largeur de la bordure de la fenêtre). La fenêtre d'édition est désignée dans une nouvelle variable d'instance de la classe *DicoAnalogies*: *fenetreTexte*.

Puis, l'évaluation de

chaine := self montreDico.

permet de récupérer dans la variable temporaire *chaine* les informations contenues dans le dictionnaire. Pour afficher ces informations dans la fenêtre d'édition, il suffit d'envoyer le message *setString:chaine* de la classe *CwText* à l'objet *fenetreTexte*. Nous définissons la méthode *montreDico* de la manière suivante :

## montreDico

"message envoyé à l'application pour initialiser le contenu du panneau texte avec les analogies du dictionnaire"

|chaine|

chaine := String new.

self associationsDo: [ :ass |

chaine := chaine, ass key,
    LineDelimiter, ass value,
    LineDelimiter].

^chaine
A présent, si l'on évalue la séquence :

```
|x| x:=DicoAnalogies new.
    x chargeDuDisque.
    x ouvrirFenetre
```

en indiquant, pour le chargement du fichier, le fichier *dico1.txt* dans son état initial, on verra apparaître la fenêtre présentée la figure 6.13.

<figure><img src="image_40.png" /><figcaption>Figure 6.13 : la fenêtre présente le contenu du dictionnaire</figcaption></figure>

### 6.3.4 - Gérer un menu pour la fenêtre

Pour pouvoir utiliser la fenêtre, il faut lui associer un menu. En l'état actuel de l'application, le panneau *fenetreTexte* ne dispose pas de menu. Etudions maintenant la manière de définir un menu adapté aux besoins de l'application.

L'utilisation d'un menu est réalisée dans une troisième version de la méthode *ouvrirFenetre*, qui est présentée la figure 6.14.

```perl
ouvrirFenetre
    "ouvre une fenêtre pour l'application (version 3)"
| form |
fenetre :=
    CwTopLevelShell createApplicationShell:'fenetrePrincipale'
        argBlock:[:w |
            w title:'Utiliser le dictionnaire ', nomDuFichier].
    form := fenetre createForm: 'fenetreTravail' argBlock:nil.
    form manageChild.
fenetreText := form createText:'texte'
        argBlock:[: w |
            w editMode: XmMULTILINEEDIT;
            width:300; height:120;
            borderWidth: 1].
fenetreText manageChild.
fenetreText setString: self montreDico.
self openMenu.
fenetre realizeWidget
```

<caption>Figure 6.14 : le panneau *fenetreText* gère un menu</caption>
La seule différence, par rapport à l'ancienne version, est l'envoi du message `openMenu` à l'instance de `DicoAnalogies`. Il nous faut donc définir cette nouvelle méthode d'instance de la classe `DicoAnalogies`. L'exécution de `openMenu` associera un objet menu à la fenêtre texte qui apparaîtra à l'écran et présentera une liste d'options parmi lesquelles l'utilisateur devra choisir. La figure 6.15 montre le menu affiché dans la fenêtre ; il se présente sous la forme d'un cadre comprenant une ligne par option.

<figure><img src="image_41.png" /><figcaption>Figure 6.15 : Un menu pour la fenêtre</figcaption></figure>

```java
openMenu "méthode d'instance de la classe DicoAnalogies"
"associe un menu à la fenêtre fenetreTexte"
|tableau|
"prise en compte de l'événement déclenché par le clic du bouton droit de la souris"
fenetreTexte addEventHandler: ButtonMenuMask
        receiver: self
        selector:#button:clientData:event:
        clientData: nil.
fenetreTexte manageChild.
"initialisation d'un tableau de types"
tableau := Array new:5.
1 to: 3 do: [ :w | tableau at:w put:XMUSHBUTTON].
tableau at:4 put:XMSEPARATOR.
tableau at:5 put:XMUSHBUTTON.
"création du menu de la fenêtre fenetreTexte"
menu := fenetreTexte createSimplePopupMenu:'menu'
        argBlock:[:w | w buttons:
                  #('copier' 'couper' 'coller' 'separator' 'sauvegarde');
                  buttonType: tableau].
"déclaration des méthodes correspondantes aux options du menu"
menu addCallback: XmNsimpleCallback
        receiver: self
        selector:#menuItem:clientData:callData:
        clientData: nil.
```

<caption>Figure 6.16 : La méthode d'ouverture du menu</caption>
La méthode `addEventHandler:receiver:selector:clientData:` associe un gestionnaire d'événements la fenêtre texte. En particulier, l'événement `ButtonMenuMask` qui correspond l'ouverture d'un menu contextuel (clic du bouton droit de la souris), déclenchera l'envoi l'objet `self`, du message `button:clientData:event` défini ci-après.

```
button:widget clientData:clientData event:event
"déroule le menu contextuel"
"teste si le bouton droit de la souris a été pressé"
event button = 3 ifFalse:[^self].
"positionne le menu en fonction de l'événement et l'affiche dans la fenêtre"
menu menuPosition: event;
manageChild
```

La création du menu contextuel est effectuée par l'envoi du message `createSimplePopupMenu:argBlock:` la fenêtre d'édition. Le premier paramètre indique le nom du menu et le deuxième paramètre spécifie :

- La liste des options du menu sous la forme d'un tableau de chaînes de caractères.

- Les types des options du menu sous la forme d'un tableau de constantes prédéfinies. La constante `XmPUSHBUTTON` définit une option de base du menu. La constante `XmSEPARATOR` définit un filet de séparation qui apparaîtra sous chacune des lignes qui sont indiquées par ce tableau (et sous celles-l seulement). Ces filets permettent ainsi de séparer visuellement des groupes d'options dans un menu. Par exemple, dans le menu affiché par la méthode `openMenu`, un filet est tracé sous la troisième ligne, séparant ainsi les options d'édition de texte de l'option de sauvegarde.

Le menu contextuel est désigné par une nouvelle variable d'instance de la classe `DicoAnalogies`: la variable `of instance menu`.

Enfin, l'envoi du message `addCallback:receiver:selector:clientData:` la variable `menu` introduit le sélecteur de la méthode qui sera exécutée lors de la sélection d'une option dans le menu : l'événement `XmSimpleCallback` déclenchera l'appel de la méthode `of instance menuItem:clientData:callData`.

```
menuItem:widget clientData:clientData callData:callData
"associe des méthodes aux options du menu"
clientData < 3
ifTrue:[ fenetreText perform: (#(copySelection #cutSelection #paste) at: clientData + 1)]
ifFalse:[ self perform: (#(sauvegarder) at: 1)].
```

Cette méthode associe des traitements aux options du menu. Le lecteur peut s'étonner de voir que l'on a distingué deux cas avec deux messages `perform:`. Le premier cas correspond aux traitements usuels d'un éditeur de texte ; les méthodes correspondantes (`copySelection`, `cutSelection`, `paste`) sont définies dans la classe `CwText`, elles seront donc adressées l'objet `fenetreTexte`. Le deuxième cas correspond un traitement spécifique de la classe `DicoAnalogies` ; la méthode utilisée (`sauvegarder`) est définie dans la classe `DicoAnalogies`, elle sera donc
appliquée à l'objet receveur self. Ici, ces messages ne sont pas envoyés directement aux objets, c'est le message perform: qui est utilisé. Le paramètre du message perform: est le sélecteur de la méthode à exécuter.

Pour le menu créé par la méthode `menuItem:clientData:callData:`, les options définies et les méthodes correspondantes sont répertoriées dans le tableau 6.17.

<table><caption>Tableau 6.17 : options et méthodes du menu créé par `menuItem:clientData:callData`:</caption><thead><tr><td>option du menu</td><td>méthode correspondante</td><td>origine et rôle</td></tr></thead><tbody><tr><td>copier</td><td>copySelection</td><td>méthode prédéfinie de la classe CwText, elle correspond à la copie, dans le tampon de transfert, du texte sélectionné dans le panneau.</td></tr><tr><td>couper</td><td>cutSelection</td><td>méthode prédéfinie de CwText ; elle correspond à la copie de la sélection dans le tampon de transfert puis à la suppression de cette sélection, qui disparaît de l'écran.</td></tr><tr><td>coller</td><td>paste</td><td>méthode prédéfinie de CwText ; elle correspond au remplacement du curseur ou de la sélection par le texte présent dans le tampon de transfert.</td></tr><tr><td>sauvegarder</td><td>sauvegarde</td><td>méthode à définir dans la classe DicoAnalogies ; elle correspondra à la sauvegarde, dans un fichier, du texte présenté dans le panneau.</td></tr></tbody></table>

Une seule méthode reste à définir : la méthode sauvegarder de la classe DicoAnalogies. Si l'on souhaite pouvoir rapidement expérimenter le menu, on peut par exemple réduire l'action de cette méthode à l'affichage d'une fenêtre qui informera l'utilisateur que l'option reste inopérante :

### sauvegarder

"méthode provisoire appelée par l'option 'sauvegarde' du menu pour l'instant, on se contente d'indiquer qu'on ne fait rien !"

CwTextPrompter
prompt: 'Cette option n''est pas encore implementée'
answer: 'Valider pour abandonner'.

Nous n'irons pas plus loin dans la définition de cette méthode. En effet, telle qu'elle est présentée ici, l'application est trop rudimentaire :

- l'accès aux informations du dictionnaire n'est pas aisé,

- la grande liberté offerte par les options copier, couper et coller permet à l'utilisateur des opérations qui mettent en danger la structure du fichier de sauvegarde.
Nous allons maintenant étudier les moyens de structurer la fenêtre en plusieurs panneaux entre lesquels nous définirons des relations de dépendance qui nous permettront de mieux répondre au cahier des charges défini en 6.1.1.

## 6.4 - Structurer la fenêtre de l'application

Nous pouvons maintenant aborder une nouvelle étape qui nous familiarisera avec la gestion, dans une même fenêtre, de plusieurs panneaux. Nous souhaitons que l'application puisse disposer d'une fenêtre telle que celle de la figure 6.18.

<figure><img src="image_42.png" /><figcaption>Figure 6.18 : la fenêtre souhaitée pour l'application</figcaption></figure>

Cette fenêtre est formée de quatre panneaux. Trois d'entre eux sont verticaux et adjacents, et apparaissent au dessus du quatrième qui est disposé horizontalement sur toute la largeur de la fenêtre. Les trois panneaux verticaux sont des panneaux de la classe *CwList*, dans lesquels on peut passer en revue une liste d'informations. Une telle liste apparaît, sur la figure 6.18, dans les deux panneaux verticaux les plus à gauche. Le panneau horizontal est de type *CwText* : il est donc de même nature que l'unique panneau de la fenêtre gérée aux paragraphes précédents (6.3.3 et 6.3.4).

Le panneau vertical de gauche présente la liste des mots-clés du dictionnaire. En cliquant dans le panneau sur l'un de ces mots, on fait apparaître, dans le panneau vertical central, la liste des mots proches enregistrés, pour ce mot-clé, dans le dictionnaire. Sur l'exemple de la figure, on a sélectionné à gauche le mot-clé *amour* et on a fait apparaître, dans le panneau central, la liste des deux mots proches *passion* et *tendresse*.

Puisque ce panneau central est, lui aussi, de la classe *CwList*, on pourrait également y sélectionner un des deux mots *passion* et *tendresse* pour faire apparaître en regard, dans le panneau de droite, la liste des mots proches correspondants. L'utilisateur peut ainsi procéder par analogies successives, pour trouver le mot qui lui convient.
Le panneau horizontal, de la classe *CwText*, servira d'aide-mémoire à l'utilisateur qui procède par analogies successives : il indique le point de départ de la recherche et la succession des mots sélectionnés. Sur la figure 6.18, ce panneau indique que la recherche est partie du mot *amour* et qu'elle n'est pas encore allée plus loin. Si, dans le panneau vertical central, on sélectionnait le mot *tendresse*, alors on verrait apparaître à la fois la liste des mots proches de *tendresse* dans le troisième panneau vertical et l'enchaînement *amour tendresse* dans le panneau horizontal<fnref n="8" />.

On constate bien que tous les panneaux sont interdépendants dans la mesure où la sélection d'une information dans l'un d'entre eux entraînera la modification de l'affichage d'au moins l'un des autres panneaux. Nous allons maintenant étudier les opérations à mettre en oeuvre pour structurer la fenêtre en plusieurs panneaux, exprimer les dépendances entre les panneaux et attribuer, à chaque panneau, un menu spécifique.

### 6.4.1 - Une nouvelle classe pour l'application

L'application qui s'appuyait sur la classe *DicoAnalogies* avait surtout un objectif pédagogique : il s'agissait de proposer au lecteur un premier contact avec la mise en oeuvre d'une interface graphique. Nous voulons maintenant construire une application plus réaliste qui va nécessiter la définition de plusieurs variables d'instances pour représenter, à chaque instant, l'état exact de l'application : nom du fichier, fenêtre, information sélectionnée ou présente dans chacun des panneaux. Plutôt que de modifier à nouveau la classe *DicoAnalogies*, en introduisant de nouvelles variables d'instances qui n'ont aucun rapport avec la classe-mère *Dictionary*, nous allons définir, comme support de l'application, la classe *ConsulteDico* dont le dictionnaire sera une variable d'instance. Nous pouvons définir cette classe comme le montre la figure 6.19.

```python
Object subclass: #ConsulteDico
    instanceVariableNames:
        'dictionnaire' "désigne le dictionnaire examiné"
        nomDuFichier "désigne le fichier qui contient le dictionnaire"
        fenetre' "désigne la fenêtre ouverte pour l'utilisateur"
    classVariableNames: ''
    poolDictionaries: ''
```

<caption>Figure 6.19 : la classe ConsulteDico, propriétaire de l'application</caption>

Cette classe est une sous-classe de la classe *Object*. Est-ce à dire que nous renonçons au travail déjà fait avec la classe *DicoAnalogies* ? Pas du tout : on peut immédiatement ajouter à la nouvelle classe *ConsulteDico* toutes les méthodes de *DicoAnalogies* qui concernaient la mise à jour du dictionnaire, à la seule condition de remplacer, dans ces méthodes, toutes les apparitions du mot self (qui re-

<footnote n="8">8 Ce panneau horizontal n'apportera des informations utiles que si on enchaîne plus de trois analogies successives : alors que les panneaux supérieurs ne conservent que les trois dernières analogies, le panneau horizontal présentera toujours l'enchaînement complet.</footnote>
présentait le dictionnaire dans *DicoAnalogies*) par le mot *dictionnaire* (qui, dans *ConsulteDico*, représente le même dictionnaire). On peut donc ainsi reporter très facilement, dans la nouvelle classe, les méthodes *ajouteRelationEntre:Et*; *ajoute:Pour*; *chargeDuDisque* et *sauveSurDisque*. Nous supposerons, dans la suite de ce chapitre, que ce report a été fait.

Avant d'étudier plus en détail la classe ConsulteDico, nous allons présenter l'un des concepts de base de la programmation d'interface graphique sous Smalltalk, le schéma Model/View/Controller ou schéma MVC.

### 6.4.2 - Le schéma Model/View/Controller

En 1970, lorsque démarre à Xerox PARC, en Californie, le projet de recherche pour la conception d'un nouveau mode de dialogue homme/machine, les chercheurs constatent non seulement que les langages et les outils disponibles sont insuffisants, mais aussi que les concepts sont inappropriés. En effet, avec une application classique, le dialogue est simple et l'application dirige les opérations. Elle peut appeler le système d'exploitation, mais inversement elle ne peut pas être appelée par le système d'exploitation. Par contre, avec une application sous interface graphique, la programmation devient événementielle car l'application doit réagir à des événements qu'elle n'a pas provoqués (des événements provenant de l'utilisateur par exemple).

Plutôt que d'utiliser un langage classique, qui nécessite pour dessiner un objet graphique plusieurs centaines d'instructions, les chercheurs imaginent une nouvelle approche de programmation et inventent le langage Smalltalk. De plus, pour gérer la complexité de la programmation événementielle, l'équipe de Xerox PARC organise celle-ci sous forme d'un scénario à trois acteurs que l'on appelle le schéma *Model/View/Controller*. Ce modèle, est encore aujourd'hui, à la base de toutes les interfaces graphiques :

- Le modèle est l'objet responsable de l'application, il dispose des traitements spécifiques de l'application.

- La vue est l'interface à travers laquelle le modèle va gérer le dialogue avec l'utilisateur.

- Le contrôleur est le gestionnaire d'événements (par exemple Windows). Il gère les actions de l'utilisateur (clic de souris, activation d'un menu, etc.), il réagit à ces actions en coordonnant les interactions entre le modèle et la vue.

Le schéma MVC (cf. figure 6.20) permet de séparer les problèmes applicatifs proprement dits des problèmes d'interface. Il est donc bien évident qu'un modèle unique peut être représenté par plusieurs vues, mais celui-ci n'a pas besoin de savoir quelles sont les vues qui lui sont associées. Par contre, les vues doivent connaître précisément le modèle auxquelles elles sont attachées. Il en va de même pour le contrôleur qui doit savoir sur quels modèles et sur quelles vues les actions de l'utilisateur doivent s'exécuter.
<figure><img src="image_43.png" /><figcaption>Figure 6.20 : Les liaisons entre Modèle, Vue et Contrôleur</figcaption></figure>

Reprenons maintenant la figure 6.21 pour détailler les variables d'instance de la nouvelle classe. Comme dans la classe *DicoAnalogies*, *nomDuFichier* et *fenetre* désigneront respectivement le nom du fichier de sauvegarde et la fenêtre ouverte pour l'application.

### 6.4.3 - Créer une instance de la classe ConsulteDico

Si nous évaluons la séquence suivante :

```
ldl d:= ConsulteDico new inspect
```

nous constatons que, ayant fait appel à la méthode *new* (héritée de *Object*) pour créer une instance de *ConsulteDico*, les variables d'instance ont été créées et qu'elles désignent toutes l'objet *nil*. Pour *nomDuFichier* et *fenetre*, cette initialisation nous convient bien dans la mesure où, tant que l'on n'a pas entamé le dialogue avec l'utilisateur, ces variables doivent rester indéterminées. En revanche, il est souhaitable que, dès la création d'une instance de *ConsulteDico*, la variable d'instance *dictionnaire* prenne la structure d'un objet de la classe *Dictionary*, vide au départ. C'est ce que nous obtenons, en ajoutant à la classe *ConsulteDico* une redéfinition de la méthode de classe *new* qui fait appel à la méthode d'instance privée *initialise* pour donner une première valeur à la variable d'instance *dictionnaire*. Nous représentons ces méthodes à la figure 6.21.
new "méthode de classe redéfinie dans ConsulteDico"
"crée une instance de la classe ConsulteDico : la variable dictionnaire devient un
dictionnaire

vide, les autres variables d'instance restent à nil"

|d| d := super new.

"la méthode new utilisée ici est celle de Object, pour éviter un appel récursif"

d initialise.

^d

initialise "méthode d'instance privée de ConsulteDico appelée par new"
initialise la variable d'instance dictionnaire comme un dictionnaire vide,
laisse toutes les autres variables d'instance à nil"

dictionnaire := Dictionary new

<caption>Figure 6.21 : les méthodes new et initialise de la classe ConsulteDico</caption>

### 6.4.4 - Partager la fenêtre en plusieurs panneaux

Utilisons la méthode *ouvrirFenetre*, telle qu'elle est définie à la figure 6.23, pour évaluer la séquence :

|d| d := ConsulteDico new.

d chargeDuDisque.

d ouvrirFenetre

Nous obtenons la fenêtre représentée à droite du présent texte (figure 6.22). Etudions les nouveautés apportées par cette dernière version de la méthode *ouvrirFenetre*.

<figure><img src="image_44.png" /><figcaption>Figure 6.22 : les différents panneaux</figcaption></figure>

Par rapport à la version 3 (cf. figure 6.13), nous définissons quatre panneaux, en envoyant à la fenêtre, représentée par la variable *form*, quatre messages *createList:argBlock*: Ces panneaux sont des variables d'instance de la classe ConsulteDico, ils se nomment respectivement :

- *listeGauche*: panneau vertical de gauche,

- *listeCentrale*: panneau vertical central,

- *listeDroite*: panneau vertical de droite,

- *listeBas*: panneau horizontal du bas de la fenêtre.
ouvrirFenetre

"ouvre une fenêtre pour l'application (version 4)"

[...] "cf. figure 6.10 pour le début du code de la méthode"

listeGauche := form createList:'listel' argBlock: [ : w |
    w leftAttachment: XmATTACHFORM;
    rightAttachment: XmATTACHPOSITION;
    topAttachment: XmATTACHFORM;
    bottomAttachment: XmATTACHPOSITION;
    rightPosition: 33; bottomPosition: 80;
    selectionPolicy: XmSINGLESELECT;
    items: self initialiseListeGauche].

listeGauche manageChild.

listeGauche addCallback: XmNsingleSelectionCallback
    receiver: self
    selector: #selectListe1:clientData:callData:
    clientData: nil.

listeCentrale := form createList:'liste2' argBlock: [ : w |
    w leftAttachment: XmATTACHPOSITION;
    rightAttachment: XmATTACHPOSITION;
    leftPosition: 33; rightPosition: 66;
    topAttachment: XmATTACHFORM;
    bottomAttachment: XmATTACHPOSITION;
    bottomPosition: 80;
    selectionPolicy: XmSINGLESELECT].

listeCentrale manageChild.

listeCentrale addCallback: XmNsingleSelectionCallback
    receiver: self
    selector: #selectListe2:clientData:callData:
    clientData: nil.

listeDroite := form createList:'list3' argBlock: [ : w |
    w leftAttachment: XmATTACHPOSITION;
    leftPosition: 66; bottomPosition: 80;
    rightAttachment: XmATTACHFORM;
    topAttachment: XmATTACHFORM;
    bottomAttachment: XmATTACHPOSITION;
    selectionPolicy: XmSINGLESELECT].

listeDroite manageChild.

listeDroite addCallback: XmNsingleSelectionCallback
    receiver: self
    selector: #selectListe3:clientData:callData:
    clientData: nil.

listeBas := form createText:'texte' argBlock: [ : w |
    w editMode: XmMULTILINEEDIT;
    width: 300; topPosition: 80;
    leftAttachment: XmATTACHFORM;
    rightAttachment: XmATTACHFORM;
    topAttachment: XmATTACHPOSITION;
    bottomAttachment: XmATTACHFORM].

fenetre realizeWidget.

<caption>Figure 6.23 : la méthode ouvrirFenetre crée plusieurs panneaux</caption>
Pour répartir les panneaux dans la fenêtre, plusieurs méthodes héritées de la classe `CwWidget`, sont utilisées :

- `leftAttachment: XmATTACHFORM` indique que le côté gauche de la liste adhère à la bordure gauche de la fenêtre mère

- `rightAttachment: XmATTACHPOSITION` indique que le côté droit de la liste adhère au coté gauche de la liste centrale

- `rightPosition: 33` indique que la largeur de la liste correspond à 33% de la largeur de la fenêtre mère.

- `topAttachment: XmATTACHFORM` indique que le côté supérieur de la liste adhère à la bordure supérieure de la fenêtre mère

- `bottomAttachment: XmATTACHPOSITION` indique que le côté inférieur de la liste adhère au coté supérieur du panneau du bas

- `bottomPosition: 90` indique que la hauteur de la liste correspond à 90% de la hauteur de la fenêtre mère.

- `selectionPolicy: XmSINGLESELECT` indique que le panneau `listeGauche` est une boîte-liste à sélection simple, c'est à dire une boîte-liste dans laquelle on ne peut sélectionner qu'un élément.

Ainsi, le coin supérieur du panneau `listeGauche` coïncidera avec le coin supérieur gauche de la fenêtre, ce panneau occupera le tiers de la largeur et les neuf dixièmes de la hauteur de la fenêtre. Cela correspond bien aux proportions de ce panneau dans la figure 6.22.

Intéressons-nous maintenant aux contenus des panneaux. Lors de l'ouverture de la fenêtre, le panneau `listeGauche` contient la liste des analogies du dictionnaire, les panneaux `listeCentrale` et `listeDroite` sont vides. L'initialisation du contenu du panneau de gauche est effectuée dans la méthode *ouvrirFenetre* par le message *items*: de la classe *CwList*. Ce message prend en paramètre un tableau de chaînes de caractères. C'est bien ce que renvoie la méthode *initialiseListeGauche* de la classe *ConsulteDico* (cf. ci-après), qui obtient d'abord, avec la méthode *keys* de la classe *Dictionary*, une instance de la classe *Set* répertoriant tous les mots-clés du dictionnaire consulté puis envoie à cet objet le message *asArray* pour obtenir un tableau (chaque élément du tableau correspond à un élément de la liste).

### `initialiseListeGauche`

```
"message envoyé au panneau de gauche pour initialiser
son contenu avec la liste des mots-clés du dictionnaire"
```

```
^ dictionnaire keys asArray
```

Pour les panneaux `listeCentrale` et `listeDroite`, le contenu dépendra du mot-clé qui sera sélectionné par l'utilisateur dans le panneau précédent, leur initialisation n'est donc pas effectuée dans la méthode *ouvrirFenetre*. Nous verrons par la suite, comment définir des méthodes d'initialisation de ces panneaux.

### 6.4.5 - Synchroniser les panneaux

Il nous reste maintenant à synchroniser les panneaux, c'est-à-dire à spécifier les enchaînements d'opérations provoqués par les actions de l'utilisateur. Si, par exemple, celui-ci effectue la sélection d'un mot dans le panneau `listeGauche`, on
doit voir immédiatement apparaître les mots proches correspondants dans le pan-
neau **listeCentrale**. Il faut donc que le fait de cliquer sur un mot du panneau
**listeGauche** déclenche l'exécution d'une méthode qui fasse apparaître dans le
panneau **listeCentrale**, les analogies correspondant au mot sélectionné. Comment
définir cette synchronisation entre les deux panneaux ?

L'envoi du message addCallback:e receiver:self selector:#m clientData:nil à un panneau indique le sélecteur de la méthode m à exécuter quand le panneau est affecté par l'événement e.

Si nous revenons à la méthode *ouvrirFenetre* de la figure 6.23, nous constatons que pour le panneau gauche, l'événement correspondant à la sélection d'un élément (*XmNsingleSelectionCallback*) déclenche l'exécution de la méthode *selectListe1:clientData:callData:*. Examinons une manière de définir cette méthode :

selectListe1:widget clientData:clientD callData:callD
" cette méthode est activée à chaque changement de sélection du panneau listeGauche.
Elle déclenche la mise à jour du panneau listeCentrale"
" widget représente le panneau-liste dans lequel on a sélectionné un élément
clientData est un objet de type CwListCallBackData qui décrit l'événement
callData représente des données complémentaires d'appel qui ne sont pas utilisées ici"
self panneauCentral items:
(dictionnaire at: (widget selectedItems at:1)) subStrings

Le contenu du panneau **listeCentrale** dépendra du mot-clé qui sera sélectionné par l'utilisateur dans le panneau précédent. Ce mot-clé est accessible par la méthode *selectedItems* de la classe *CwList*, qui renvoie une liste ordonnée des éléments sélectionnés dans le panneau. Le contenu du panneau est obtenu à partir du dictionnaire consulté (méthode *at:*) puis transformée en un tableau de mots.

Le raisonnement effectué pour la synchronisation des panneaux *listeGauche* et *listeCentrale*, est également appliqué aux panneaux *listeCentrale* et *listeDroite* ainsi qu'aux panneaux *listeDroite* et *listeGauche*.

6.4.6 - Améliorer la synchronisation

En l'état actuel du développement de notre application, il manque à notre
synchronisation la mise à jour du panneau **listeBas** dont le contenu doit,
rappelons-le, indiquer la chaîne des mots successivement sélectionnés.

Nous allons, dans le paragraphe suivant, finaliser l'application, en assurant
cette synchronisation et en gérant un nombre non limité de sélections consécu-
tives.

6.5 - L'application finale

Examinons la figure 6.24. Elle représente une étape du déroulement de l'ap-
plication, que l'on peut reconstituer en examinant le panneau du bas :

- L'utilisateur a sélectionné au départ le mot *ire*, puis dans les mots proches de ce premier mot, le mot *colère*.
- Parmi les mots proches de *colère*, il a sélectionné le mot *fureur*.

- Enfin, parmi les mots proches de *fureur*, il a choisi le mot *rage*.

- Il est alors « revenu en arrière » d'un mot-clé, sans modifier les sélections et les trois panneaux verticaux montrent respectivement la deuxième, troisième et quatrième sélection d'une liste de quatre sélections présentée dans le panneau du bas.

<figure><img src="image_45.png" /><figcaption>Figure 6.24 : une étape du déroulement de l'application</figcaption></figure>

Cet exemple nous montre que l'application doit gérer non pas trois sélections successives (dans les trois panneaux verticaux) mais une suite variable de sélections, dont les trois panneaux verticaux présenteront, à chaque instant trois éléments contigus. Le panneau du bas indiquera, lui, l'état complet de cette suite. Cette suite est variable car toute nouvelle sélection la modifie : par exemple si, partant de l'état représenté à la figure 6.24, on sélectionne dans le panneau vertical central le mot *ire*, les sélections *fureur* et *rage* (de rangs 3 et 4) disparaissent et la fenêtre se modifie de la manière suivante :

- le panneau gauche conserve la même liste de mots ;

- le contenu du panneau central conserve la même liste de mots mais la sélection passe de *fureur* à *ire* ;

- le panneau droit est complètement remis à jour, pour afficher les mots proches de *ire*, parmi lesquels aucune sélection n'est encore effectuée ;

- le panneau du bas indique la suite de sélections : *ire colère ire*.

Pour prendre en considération ces contraintes de l'application, il nous faut définir de nouvelles variables d'instances pour la classe *ConsulteDico*.
### 6.5.1 - Les nouvelles variables d'instance de la classe ConsulteDico

La figure 6.25 présente une redéfinition de la classe ConsulteDico. Par rapport à la précédente définition de la figure 6.19, nous pouvons remarquer :

- que les variables *dictionnaire*, *nomDuFichier* et *fenetre* sont toujours présentes ;

- que la variable *indexSelectionGauche* n'est pas la seule nouvelle variable. Cinq autres variables sont définies : elles se nomment *suiteDeSuivi*, *listeGauche*, *listeCentrale*, *listeDroite* et *listeBas*. Nous décrirons également leur utilisation.

```
Object variableSubclass: #ConsulteDico
instanceVariableNames:
    'dictionnaire nomDuFichier fenetre
    suiteDeSuivi "liste des sélections en cours"
    indexSelectionGauche "index de la sélection du panneau gauche dans
                         suiteDeSuivi"
    listeGauche "désigne le panneau vertical gauche"
    listeCentrale "désigne le panneau vertical central"
    listeDroite  "désigne le panneau vertical droit"
    listeBas     "désigne le panneau horizontal bas"

classVariableNames: ''
poolDictionaries: ''
```

<caption>Figure 6.25 : la nouvelle définition de la classe ConsulteDico</caption>

La variable *suiteDeSuivi* désignera, à chaque instant, la suite des sélections effectuées. Nous lui donnerons la forme d'une instance de la classe *OrderedCollection* et nous conviendrons que, relativement à cette suite, la variable d'instance *indexSelectionGauche* désignera toujours le rang de l'élément qui correspond à la sélection du panneau *listeGauche*. Ainsi, la sélection du panneau *listeCentrale* sera obtenue en consultant, dans *suiteDeSuivi*, l'élément de rang *indexSelectionGauche+1* et celle de *listeDroite* correspondra au rang *indexSelectionGauche+2*.

C'est par abus de langage que nous avons indiqué que les éléments de *suiteDeSuivi* correspondent à des sélections. Ils sont, en fait, associés à des panneaux (visibles ou non à l'écran) dont chacun peut être dans l'un des trois états suivants :

1. panneau vide,

2. panneau contenant une liste de mots dont aucun n'est sélectionné,

3. panneau contenant une liste de mots, dont l'un a été sélectionné et doit apparaître, s'il est visible, en inversion vidéo.

Nous conviendrons, dans ces conditions, que tout élément de *suiteDeSuivi* indique, par sa valeur si le panneau qu'il représente contient ou non une sélection effective :

*valeur nil* : le panneau est dans l'état 1 ou l'état 2,

*valeur unMot* : le panneau est dans l'état 3 et le mot *unMot* y est sélectionné.
Conformément à ces conventions, nous devons redéfinir la méthode *initialise* qui sert à préparer une instance de *ConsulteDico* nouvellement créée. Nous le faisons de la manière suivante :

### *initialise*

```
"méthode privée, appelée par new ou lors du chargement d'un nouveau fichier;
initialise la variable d'instance dictionnaire comme un dictionnaire vide;
initialise suiteDeSuivi pour indiquer que les panneaux ne comporteront, au départ,
aucune sélection;
initialise indexSelectionGauche pour indiquer que le premier élément de suiteDeSuivi désigne le panneau gauche;
laisse toutes les autres variables d'instance à nil"
dictionnaire := Dictionary new.
suiteDeSuivi := OrderedCollection
  with: nil with: nil with: nil.
indexSelectionGauche := 1
```

En effet, quand on ouvre la fenêtre, le panneau de gauche contiendra la liste de toutes les clés du dictionnaire, sans aucune sélection. La variable d'instance *indexSelectionGauche* doit donc désigner le premier élément de *suiteDeSuivi* et cet élément a la valeur *nil* (aucune sélection). La variable *suiteDeSuivi* a deux éléments supplémentaires qui correspondent aux panneaux central et gauche, vides au départ. Ces deux éléments ont donc aussi la valeur *nil*.

## 6.5.2 - Des variables d'instance pour désigner les panneaux

La figure 6.25 introduit également quatre nouvelles variables d'instance : *listeGauche*, *listeCentrale*, *listeDroite* et *listeBas*, que nous n'avons pas encore présentées. Ces variables vont désigner chacun des quatre panneaux et elles seront initialisées au moment de l'ouverture de la fenêtre. En effet, pour assurer la synchronisation entre les panneaux, il nous faudra adresser directement des messages à ces derniers. Il est donc nécessaire que l'objet propriétaire de l'application « connaisse » chacun des panneaux de la fenêtre.

A propos de la fenêtre, remarquons que, jusqu'ici, nous n'avons pas utilisé la variable d'instance *fenetre* qui la désigne. Nous le ferons pour pouvoir modifier le libellé de la barre de titre, à chaque changement de fichier, au cours du déroulement de l'application. La figure 6.26 montre comment on peut procéder. Sur cette figure, on trouve d'abord le début des nouvelles versions des méthodes *chargeDuDisque* et *sauveSurDisque* (déjà étudiées en 6.2.2). On constate que chacune d'entre elles fait appel à la méthode *nomDuFichier*: qui permet d'affecter une valeur à la variable d'instance *nomDuFichier* (cf. 6.2.1). Comme le nom du fichier est affiché, dans la barre de titre de la fenêtre, il convient, à chaque exécution de la méthode *nomDuFichier*, de mettre à jour la fenêtre. C'est ce que fait la méthode *nomDuFichier*, présentée au bas de la figure 6.26, en adressant à la variable d'instance *fenetre* successivement deux messages:

- le message *title*: initialise le libellé de la barre de titre,

- le message *updateTitle*: provoque la mise à jour de la barre de titre à l'écran.
### chargeDuDisque

"détermine le nom du fichier de chargement et garnit la variable d'instance dictionnaire renvoie true si tout s'est bien passé, false si le fichier était de format incorrect"

|fichier ligne1 ligne2|

self nomDuFichier:

(CwTextPrompter prompt: 'nom du fichier de chargement?' answer: 'd:\chap6\sentimts.txt').

... "la suite de la méthode n'est pas décrite ici"

### sauveSurDisque

"recopie le dictionnaire dans le fichier désigné par nomDuFichier"

|sauvegarde "représentera le fichier de sauvegarde" |

"on demande d'abord à l'utilisateur d'indiquer ou de confirmer le nom du fichier de sauvegarde"

self nomDuFichier:

(CwTextPrompter prompt: 'nom du fichier pour la sauvegarde?' default: nomDuFichier).

... "la suite de la méthode n'est pas décrite ici"

### nomDuFichier: uneChaine

"affecte uneChaine à la variable d'instance nomDuFichier change la barre de titre de la fenêtre pour indiquer le nouveau fichier"

nomDuFichier := uneChaine.

fenetre notNil

"si on a ouvert une fenêtre pour l'application"

ifTrue: [fenetre title: 'Utilisation du dictionnaire ', nomDuFichier; updateTitle]

<caption>Figure 6.26 : la gestion des noms de fichiers</caption>

### 6.5.3 - La version finale de la méthode ouvrirFenetre

Dans la version finale de la méthode *ouvrirFenetre*, on remarquera que chaque panneau créé est affecté à la variable d'instance qui lui correspond (*listeGauche*, *listeCentrale*, *listeDroite* et *listeBas*).

On peut constater également que chaque panneau, sauf le panneau du bas, indique une méthode pour la synchronisation (*selectListe1.clientData:callData:*; *selectListe2:clientData:callData:*; *selectListe3:clientData:callData:*;) ainsi qu'un menu. Les trois menus sont stockés dans trois nouvelles variables d'instances de la classe *DicoAnalogies* (*menuGauche*, *menuCentral*, *menuDroit*).

Nous n'étudierons pas ici la totalité des méthodes mises en oeuvre pour cette version finale de l'application. Nous nous contenterons ici de donner deux exemples, en présentant les opérations de synchronisation associées à un changement global dans le panneau droit, puis la gestion d'un menu. La totalité du code de l'application est présentée en annexe.
```ruby
ouvrirFenetre
  "ouvre une fenêtre pour l'application (version 5)"
| tableau |
[...] "le début de la méthode n'est pas décrit ici"
listeGauche addCallback: XmNsingleSelectionCallback
        receiver: self
        selector: #selectList1:clientData:callData:
                  clientData: nil.
listeGauche addEventHandler: ButtonMenuMask
        receiver: self
        selector: #buttonGauche:clientData:event:
                  clientData:nil.
listeGauche manageChild.
[...]
tableau := Array new:8.
1 to: 2 do: [ : w | tableau at:w put:XmPUSHBUTTON].
tableau at:3 put:XmSEPARATOR.
4 to: 5 do: [ : w | tableau at:w put:XmPUSHBUTTON].
tableau at:6 put:XmSEPARATOR.
7 to: 8 do: [ : w | tableau at:w put:XmPUSHBUTTON].
menuGauche := listeGauche createSimplePopupMenu: 'menuG'
                          argBlock: [ : w ]
                          w buttons: #('Charger' 'Sauvegarder' 'separator'
                                          'Ajouter' 'Modifier' 'separator'
                                          '<--' '-->');
                          buttonTypes: tableau].
menuGauche addCallback: XmNsimpleCallback
        receiver: self
        selector: #menuGaucheItem:clientData:callData:
                  clientData: nil.
[...] "la suite de la méthode n'est pas décrite ici"
```

<caption>Figure 6.27 : dernière version de la méthode ouvrirFenetre</caption>

### 6.5.4 - Effet d'une sélection dans le panneau droit

Quand on sélectionne un mot dans le panneau droit, on veut faire apparaître la liste des mots proches correspondant à cette sélection. Conformément à l'esprit de l'application, cette liste doit apparaître à droite de la sélection. Or, le panneau droit étant le dernier des trois panneaux verticaux, il est nécessaire de faire « glisser » vers la gauche les informations des panneaux selon le principe suivant :

- l'ancien affichage du panneau gauche disparaît ;

- l'ancien affichage du panneau central apparaît dans le panneau gauche ;

- l'ancien affichage du panneau droit apparaît dans le panneau central ;

- le panneau droit montre la liste des mots proches correspondant à la sélection qui venait d'être effectuée dans ce même panneau ;
- le panneau du bas complète l'affichage de la liste des sélections en cours avec le dernier mot sélectionné.

On constate donc que l'effet d'une sélection dans le panneau droit doit entraîner une modification de tous les autres panneaux. La méthode *changeDroit*: sera chargée d'assurer les modifications. Cette méthode est présentée à la figure 6.28.

```perl
changeDroit: motSelectionne
    "méthode exécutée avec un changement global dans le panneau droit"
suiteDeSuivi at: (indexSelectionGauche + 2)
    put: motSelectionne.
    "on supprime les éléments de la liste qui sont au delà du panneau droit"
    [(suiteDeSuivi size) > (indexSelectionGauche + 2)]
    whileTrue: [suiteDeSuivi removeLast].
    "on ajoute un élément qui représentera le nouveau panneau droit après le décalage vers la gauche"
suiteDeSuivi add: nil.
    "on fait maintenant glisser les sélections vers la gauche :
    l'ancien panneau central deviendra le nouveau panneau gauche"
    indexSelectionGauche := indexSelectionGauche + 1.
self ajustePanneaux
```

<caption>Figure 6.28 : la méthode exécutée après une sélection dans le panneau droit</caption>

Commentons les différentes étapes de l'exécution de la méthode *changeDroit*:

- Le mot correspondant à la sélection dans le panneau droit est transmis en argument à cette méthode. La première opération consiste à enregistrer ce mot dans l'élément de *suiteDeSuivi* qui correspond au panneau droit (élément dont l'index est *indexSelectionGauche+2*).

- Ensuite, il faut éventuellement tronquer *suiteDeSuivi*. En effet, si le panneau droit ne représentait pas le dernier élément de *listeDeSuivi* (cf. exemple de la figure 6.24), la sélection doit annuler toutes les sélections antérieures au delà du panneau droit.

- La prochaine opération ajoute un nouvel élément à la fin de *suiteDeSuivi* pour représenter le nouveau panneau qui doit apparaître à droite de la sélection.

- On fait alors glisser les sélections en augmentant d'une unité la valeur de *indexSelectionGauche*.

- Il reste à mettre à jour tous les panneaux. Cette opération s'effectue en adressant à l'objet propriétaire de l'application le message *ajustePanneaux*, qui correspond à la méthode décrite à la figure 6.29. Cette méthode déclenchera, bien entendu, les méthodes d'initialisation / mise à jour de chaque panneau : *listeGauche*, *listeCentrale*, *listeDroite* et *listeBas* mais, elle devra également, pour chacun des panneaux verticaux, rétablir si nécessaire la dernière sélection.
### ajustePanneaux

"met à jour tous les panneaux en conservant les sélections existantes"

"mise à jour du panneau gauche"

self panneauGauche.

self remetSelection: indexSelectionGauche

pour: listeGauche.

"mise à jour du panneau central"

self panneauCentral.

self remetSelection: (indexSelectionGauche + 1)

pour: listeCentrale.

"mise à jour du panneau droit"

self panneauDroit.

self remetSelection: (indexSelectionGauche + 2)

pour: listeDroite.

"mise à jour du panneau du bas"

self panneauBas

Figure 6.29 : une méthode pour remettre à jour tous les panneaux

Etudions par exemple les opérations effectuées par la méthode de la figure 6.29 pour le panneau gauche. Pour ce panneau, cette méthode évalue :

self panneauGauche.

self remetSelection: indexSelectionGauche

pour: listeGauche.

La méthode panneauGauche correspond au protocole suivant :

### panneauGauche

"message envoyé au panneau gauche pour initialiser ou mettre à jour son contenu"

(self contenuCorrespondantA: indexSelectionGauche) notNil

ifTrue: [ listeGauche items:

(self contenuCorrespondantA: indexSelectionGauche)]

dans lequel la méthode contenuCorrespondantA: est une méthode qui sera utilisée par chacun des panneaux verticaux en fonction de l'argument transmis (de indexSelectionGauche à indexSelectionGauche+2 pour les trois panneaux). La méthode contenuCorrespondantA: est présentée à la figure 6.30. Nous laisserons au lecteur le soin de l'étudier, en s'aidant des commentaires qui l'accompagnent.

Après avoir mis à jour le panneau gauche, il nous reste à rétablir, si nécessaire la sélection qui figurait dans la liste qui vient d'être affichée. Une sélection est à rétablir, si l'élément correspondant au panneau dans suiteDeSuivi n'a pas la valeur nil. Dans ce cas, la valeur de cet élément est le mot qui doit apparaître dans le panneau comme étant sélectionné. Pour rétablir une telle sélection, il est nécessaire d'adresser, au panneau lui-même, le message selectItem:notify: qui correspond à une méthode d'instance de la classe CwList.
```java
contenuCorrespondantA: indexSelection
"renvoie, pour mettre à jour un panneau, le contenu correspondant
à l'élément désigné par indexSelection dans suiteDeSuivi,
ne met pas à jour la sélection du panneau"
|selectionPrecedente|
"si le panneau à remplir n'est pas le premier, selectionPrecedente
désignera la sélection dans le panneau à gauche du panneau à remplir"
indexSelection = 1
ifTrue: ["c'est le panneau le plus à gauche : on renvoie le dictionnaire entier"
^ self initialiseListeGauche]
ifFalse: ["s'il n'y a aucune sélection à gauche, le panneau doit rester vide"
selectionPrecedente :=
suiteDeSuivi at: (indexSelection - 1).
selectionPrecedente = nil
ifTrue: [^ nil]
ifFalse:
    [^ (dictionnaire at: selectionPrecedente)
    subStrings]]
```

<caption>Figure 6.30 : déterminer les informations à afficher dans un panneau</caption>

Dans la figure 6.29, pour le panneau gauche, l'opération qui rétablit la sélection est effectuée avec l'évaluation de :

```java
self remetSelection: indexSelectionGauche
pour: listeGauche
```

et la méthode remetSelection:pour: correspond à :

```java
remetSelection: indexSelection pour: unPanneau
"remet, dans le panneau unPanneau, la sélection indiquée par indexSelection"
|cle|
cle := suiteDeSuivi at: indexSelection.
cle notNil
ifTrue: [unPanneau selectItem:cle notify:false]
```

### 6.5.5 - Le menu des panneaux verticaux

Dans chaque panneau vertical le même menu est disponible (cf. figure 6.31). Les deux premières options permettent la gestion de fichier et déclenchent les méthodes chargeDuDisque et sauveSurDisque, que nous avons déjà étudiées. Les trois options suivantes correspondent à la mise à jour du dictionnaire et permettent d'ajouter, de supprimer ou de modifier des analogies. Nous ne les décrirons pas ici.

<table><caption>Figure 6.31 : le menu</caption><tr><td>Charger</td></tr><tr><td>Sauvegarder</td></tr><tr><td>Ajouter</td></tr><tr><td>Supprimer</td></tr><tr><td>Corriger</td></tr><tr><td>&lt;--</td></tr><tr><td>--&gt;</td></tr></table>
Nous étudierons seulement les deux dernières options qui permettent le glissement des informations vers la gauche (option <--) ou bien vers la droite (option -->). Elles correspondent respectivement aux méthodes *allerAGauche* et *allerADroite* de la figure 6.32.

**allerAGauche**
"cette méthode permet de revenir en arrière d'un panneau "
indexSelectionGauche = 1
ifTrue: [CwMessagePrompter warn:'Impossible'. ^nil].
indexSelectionGauche := indexSelectionGauche - 1.
self ajustePanneaux

**allerADroite**
"cette méthode permet d'avancer d'un panneau vers la droite "
((indexSelectionGauche +2) = suiteDeSuivi size)
ifTrue: [CwMessagePrompter warn:'Impossible'. ^nil].
indexSelectionGauche := indexSelectionGauche + 1.
self ajustePanneaux

<caption>Figure 6.32 : les méthodes pour le « gissement » des informations</caption>

La méthode *allerAGauche* vérifie que l'opération demandée est possible : si le panneau de gauche (désigné dans *suiteDeSuivi* par *indexSelectionGauche*) affiche déjà les informations correspondant au premier élément de *suiteDeSuivi*, il est impossible de glisser vers la gauche. Si le glissement est possible, il est obtenu simplement, en diminuant d'une unité la valeur de *indexSelectionGauche* puis en envoyant à l'objet propriétaire de l'application le message *ajustePanneaux*.

De même, la méthode *allerADroite* n'autorisera le glissement que si le panneau de droite ne correspond pas encore au dernier élément de *suiteDeSuivi*. A l'inverse de *allerAGauche*, pour effectuer le glissement, elle augmentera d'une unité la valeur de *indexSelectionGauche*. Et, bien entendu, elle enverra également, à l'objet propriétaire de l'application, le message *ajustePanneaux*.
# Annexe - utilisation d'un dictionnaire analogique : l'application

Nous présentons ici l'implémentation complète proposée pour l'application étudiée au chapitre 6. La définition de la classe et les méthodes sont celles qui ont été obtenues avec l'option *file out* du « Classes Browser », qui produit, avec IBM Smalltalk, le fichier *cnsltdco.st*. Toutefois, les méthodes ne sont pas présentées ici dans l'ordre alphabétique. L'ordre que nous avons choisi est celui qui nous semble permettre au lecteur une meilleure compréhension des mécanismes de l'application.

## Définition de la classe et création d'une instance

### Object subclass: #ConsulteDico

<table><tr><td>instanceVariableNames:</td><td>''</td></tr><tr><td>dictionary</td><td>"désigne le dictionnaire examiné"</td></tr><tr><td>nomDuFichier</td><td>"nom du fichier contenant le dictionnaire examiné"</td></tr><tr><td>fenetre</td><td>"désigne la fenêtre ouverte pour l'utilisateur"</td></tr><tr><td>suiteDeSuivi</td><td>"liste des sélections en cours"</td></tr><tr><td>indexSelectionGauche</td><td>"index de la sélection du panneau gauche dans suiteDeSuivi"</td></tr><tr><td>listeGauche</td><td>"désigne le panneau vertical gauche"</td></tr><tr><td>listeCentrale</td><td>"désigne le panneau vertical central"</td></tr><tr><td>listeDroite</td><td>"désigne le panneau vertical droit"</td></tr><tr><td>listeBas</td><td>"désigne le panneau du bas"</td></tr><tr><td>menuGauche</td><td>"désigne le menu du panneau vertical gauche"</td></tr><tr><td>menuCentral</td><td>"désigne le menu du panneau vertical central"</td></tr><tr><td>menuDroit</td><td>"désigne le menu du panneau vertical droit"</td></tr><tr><td>classVariableNames:</td><td>''</td></tr><tr><td>poolDictionaries:</td><td>'CwConstants CldtConstants'</td></tr></table>

### new "new est la seule méthode de classe définie pour ConsulteDico"

"crée une instance de la classe ConsulteDico

les variables d'instance sont initialisées par appel de la méthode d'instance initialise"

idl d := super new.

"la méthode new utilisée ici est celle de Object, pour éviter un appel récursif"

d initialise.

^d
## initialise

```
"méthode privée, appelée par new et à chaque chargement d'un nouveau fichier;
initialise la variable d'instance dictionnaire comme un dictionnaire vide;
initialise suiteDeSuivi pour indiquer que les panneaux ne comporteront, au départ,
aucune sélection;
initialise indexSelectionGauche pour indiquer que le premier élément de suiteDeSuivi désigne, au départ, le panneau gauche;
laisse toutes les autres variables d'instance à nil"
dictionnaire := Dictionary new.
suiteDeSuivi := OrderedCollection with: nil with: nil with: nil.
indexSelectionGauche := 1
```

# Lancement de l'application

## ouvrirFenetre

```
"ouvre une fenêtre pour l'application"
lform |
"création du cadre de l'application"
fenetre := CwTopLevelShell createApplicationShell: 'MaFenetre' argBlock: [ : w |
    w title: 'Utilisation d"un dictionnaire analogique', nomDuFichier].
"création de l'espace de travail de l'application"
forme := fenetre createForm: 'MaForme' argBlock: nil.
forme manageChild.
"mise en place des quatre panneaux de l'application"
listeGauche := forme createList: 'MaListeGauche' argBlock: [ : w |
    w leftAttachment: XmATTACHFORM;
    rightAttachment: XmATTACHPOSITION;
    rightPosition: 33; bottomPosition: 90;
    topAttachment: XmATTACHFORM;
    bottomAttachment: XmATTACHPOSITION;
    selectionPolicy: XmSINGLESELECT;
    items: (dictionnaire keys asArray) ].
listeCentrale := forme createList: 'MaListeCentrale' argBlock: [ : w |
    w leftAttachment: XmATTACHPOSITION;
    leftPosition: 33; rightPosition: 66;
    rightAttachment: XmATTACHPOSITION;
    topAttachment: XmATTACHFORM;
    bottomAttachment: XmATTACHPOSITION;
    bottomPosition: 90;
    selectionPolicy: XmSINGLESELECT].
listeDroite := forme createList: 'MaListeDroite' argBlock: [ : w |
    w leftAttachment: XmATTACHPOSITION;
    leftPosition: 66; bottomPosition: 90;
    rightAttachment: XmATTACHFORM;
    topAttachment: XmATTACHFORM;
    bottomAttachment: XmATTACHPOSITION;
    selectionPolicy: XmSINGLESELECT].
```
listeBas := forme createText: 'MaListeBas' argBlock: [ : w | w
    editMode: XmMULTILINEEDIT;
    width: 300;
    leftAttachment: XmATTACHFORM;
    rightAttachment: XmATTACHFORM;
    topAttachment: XmATTACHPOSITION;
    topPosition: 90;
    bottomAttachment: XmATTACHFORM :].

"traitement à effectuer lorsque l'on sélectionne un élément dans la liste de gauche"

listeGauche addCallback: XmNsingleSelectionCallback receiver: self
    selector: #selectListe1:clientData:callData:
    clientData: nil.

"mise en place du menu contextuel de la liste gauche"

listeGauche addEventHandler: ButtonMenuMask receiver: self
    selector: #buttonGauche:clientData:event:
    clientData:nil.

listeGauche manageChild.

listeCentrale addCallback: XmNsingleSelectionCallback receiver: self
    selector: #selectListe2:clientData:callData:
    clientData: nil.

listeCentrale addEventHandler: ButtonMenuMask receiver: self
    selector: #buttonCentral:clientData:event:
    clientData:nil.

listeCentrale manageChild.

listeDroite addCallback: XmNsingleSelectionCallback receiver: self
    selector: #selectListe3:clientData:callData:
    clientData: nil.

listeDroite addEventHandler: ButtonMenuMask receiver: self
    selector: #buttonDroit:clientData:event:
    clientData:nil.

listeDroite manageChild.

listeBas manageChild.

"création des menus de l'application"

menuGauche := listeGauche createSimplePopUpMenu: 'menu gauche' argBlock: [ : w | w
    w buttons: #('Charger' 'Sauver' 'Ajouter' 'Modifier' '<----' '---->')].

menuGauche addCallback: XmNsimpleCallback receiver: self
    selector: #menuGaucheItem:clientData:callData:
    clientData: nil.

menuCentral := listeCentrale createSimplePopUpMenu: 'menu central' argBlock: [ : w | w
    w buttons: #('Charger' 'Sauver' 'Ajouter' 'Modifier' '<----' '---->')].

menuCentral addCallback: XmNsimpleCallback receiver: self
    selector: #menuCentralltem:clientData:callData:
    clientData: nil.

menuDroit := listeDroite createSimplePopUpMenu: 'menu droit' argBlock: [ : w | w
    w buttons: #('Charger' 'Sauver' 'Ajouter' 'Modifier' '<----' '---->')].

menuDroit addCallback: XmNsimpleCallback receiver: self
    selector: #menuDroitItem:clientData:callData:
    clientData: nil.

fenetre realizeWidget.
## Affichage dans les panneaux

Le contenu d'un panneau vertical est la liste complète des mots-clés du dictionnaire, si ce panneau est le premier décrit dans *suiteDeSuivi*. Sinon, ce panneau donne les mots proches de la sélection du panneau précédent. A défaut de sélection, le panneau affiche une liste vide. Le contenu du panneau du bas doit reproduire les mots de *suiteDeSuivi*.

### contenuCorrespondantA: indexSelection

"renvoie, pour mettre à jour un panneau, le contenu correspondant à l'élément désigné par indexSelection dans suiteDeSuivi. Cette méthode ne met pas à jour la sélection du panneau"

### indexSelectionPrecedente

"si le panneau à remplir n'est pas le premier, selectionPrecedente désignera la sélection qui a été effectuée dans le panneau situé à gauche du panneau à remplir"

### indexSelection = 1

ifTrue: ["c'est le panneau le plus à gauche : on renvoie le dictionnaire entier"

^ (dictionnaire keys asArray)]

ifFalse: ["s'il n'y a aucune sélection à gauche, le panneau doit rester vide"

selectionPrecedente := suiteDeSuivi at: (indexSelection - 1).

selectionPrecedente = nil

ifTrue: [^ nil]

ifFalse: [^ (dictionnaire at: selectionPrecedente) subStrings]]

### selectListe1: widget clientData: clientData callData: callData

"méthode exécutée lorsque l'on sélectionne un élément dans le panneau de gauche"

"mise à jour de la variable suiteDeSuivi"

suiteDeSuivi at: indexSelectionGauche put: (widget selectedItems at:1).

"mise à jour des panneaux central, droit et bas en fonction de la sélection"

self changeGauche: (widget selectedItems at:1)

### selectListe2: widget clientData: clientData callData: callData

"méthode exécutée lorsque l'on sélectionne un élément dans le panneau central"

"mise à jour de la variable suiteDeSuivi"

suiteDeSuivi at: (indexSelectionGauche + 1) put: (widget selectedItems at:1).

"mise à jour des panneaux droit et bas en fonction de la sélection"

self changeCentral: (widget selectedItems at:1)

### selectListe3: widget clientData: clientData callData: callData

"méthode est exécutée lorsque l'on sélectionne un élément dans le panneau de droite"

"mise à jour des panneaux en fonction de la sélection"

self changeDroit: (widget selectedItems at:1)

self panneauBas

### panneauGauche

"message envoyé au panneau gauche pour initialiser ou mettre à jour son contenu"

(self contenuCorrespondantA: indexSelectionGauche) notNil

ifTrue: [listeGauche items:

(self contenuCorrespondantA: (indexSelectionGauche))]
### panneauCentral

"message envoyé au panneau gauche pour initialiser ou mettre à jour son contenu"

(self contenuCorrespondantA: indexSelectionGauche + 1) notNil

if True: [listeCentrale items:

(self contenuCorrespondantA: (indexSelectionGauche + 1))]

### panneauDroit

"message envoyé au panneau gauche pour initialiser ou mettre à jour son contenu"

(self contenuCorrespondantA: indexSelectionGauche + 2) notNil

if True: [listeDroite items:

(self contenuCorrespondantA: (indexSelectionGauche + 2)))

### panneauBas

"message envoyé au panneau du bas pour initialiser ou mettre à jour son contenu"

l'affichage chaine1

chaine := "".

(suiteDeSuivi at: 1) = nil

if True: ['Aucune sélection']

if False: [suiteDeSuivi do: [ :element |
                element = nil if False: [chaine := chaine, ' ', element]]].

listeBas setString: chaine

## Synchronisation des panneaux

Une sélection dans le panneau gauche entraîne une modification de chacun des autres panneaux. Une sélection du panneau central entraîne une modification du panneau droit et du panneau du bas. Une sélection dans le panneau droit fait « glisser » les panneaux vers la gauche : on doit modifier tous les panneaux et rétablir les sélections adéquates (traitement réalisé par la méthode ajustePanneaux).

### changeGauche: cleChoisie

"méthode exécutée avec un changement global dans le panneau gauche"

suiteDeSuivi at: indexSelectionGauche put: cleChoisie.

"mise à jour du panneau gauche"

suiteDeSuivi at: indexSelectionGauche put: motSelectionne.

"mise à jour du panneau central"

suiteDeSuivi at: (indexSelectionGauche + 1) put: nil.

self panneauCentral.

"mise à jour du panneau droit"

suiteDeSuivi at: (indexSelectionGauche + 2) put: nil.

self panneauDroit.

"suppression des éléments inutiles de suiteDeSuivi"

[suiteDeSuivi size > (indexSelectionGauche + 2)]

while True: [suiteDeSuivi removeLast].

"mise à jour du panneau du bas"

self panneauBas
**changeCentral: cleChoisie**

"méthode exécutée avec un changement global dans le panneau central"

suiteDeSuivi at: (indexSelectionGauche + 1) put: cleChoisie.

"mise à jour du panneau droit"

suiteDeSuivi at: (indexSelectionGauche + 2) put: nil.

self panneauDroit.

"suppression des éléments inutiles de suiteDeSuivi"

[suiteDeSuivi size > (indexSelectionGauche + 2)]

whileTrue: [suiteDeSuivi removeLast].

"mise à jour du panneau du bas"

self panneauBas

**changeDroit: motSelectionne**

"méthode exécutée avec un changement global dans le panneau droit"

suiteDeSuivi at: (indexSelectionGauche + 2) put: motSelectionne.

"on supprime les éléments de la liste qui sont au-delà du panneau droit"

[(suiteDeSuivi size) > (indexSelectionGauche + 2)]

whileTrue: [suiteDeSuivi removeLast].

"on ajoute un élément qui représentera le nouveau panneau droit après le décalage vers la gauche"

suiteDeSuivi add: nil.

"on fait maintenant glisser les sélections vers la gauche :

l'ancien panneau central deviendra le nouveau panneau gauche"

indexSelectionGauche := indexSelectionGauche + 1.

self ajustePanneaux

**ajustePanneaux**

"met à jour tous les panneaux en conservant les sélections existantes"

"mise à jour du panneau gauche"

self panneauGauche.

self remetSelection: indexSelectionGauche pour: listeGauche.

"mise à jour du panneau central"

self panneauCentral.

self remetSelection: (indexSelectionGauche + 1) pour: listeCentrale.

"mise à jour du panneau droit"

self panneauDroit.

self remetSelection: (indexSelectionGauche + 2) pour: listeDroite.

"mise à jour du panneau du bas"

self initialisePanneauBas

**remetSelection: indexSelection pour: unPanneau**

"remet, dans le panneau unPanneau, la sélection indiquée par indexSelection"

lclel

cle := suiteDeSuivi at: indexSelection.

cle notNil

ifTrue: [unPanneau selectItem:cle notify:false]
## Appel des menus

Les méthodes d'interface avec les fichiers (*chargeDuDisque* et *sauveSurDisque*) sont communes à tous les menus. Il en va de même des méthodes de « glissement » (*allerAGauche* et *allerADroite*). Comme on le verra dans les paragraphes suivants, les méthodes de mise à jour du dictionnaire, pour l'addition et la modification, sont propres à chaque panneau mais font toutes appel aux mêmes méthodes de base : *ajouteAPartirDe:avecIndex* et *modifieAPartirDe:avecIndex*:

**buttonGauche:widget clientData:clientData event: event**

*"affichage du menu dans le panneau de gauche"*

event button = 3 ifFalse:[ ^self].

menuGauche menuPosition: event;

manageChild

**buttonCentral:widget clientData:clientData event: event**

*"affichage du menu dans le panneau central"*

event button = 3 ifFalse:[ ^self].

menuCentral menuPosition: event;

manageChild

**buttonDroit:widget clientData:clientData event: event**

*"affichage du menu dans le panneau de droite"*

event button = 3 ifFalse:[ ^self].

menuDroit menuPosition: event;

manageChild

**menuGaucheItem:widget clientData:clientData callData:callData**

*"associe les options du menu à des traitements"*

self perform: ( #( #chargeDuDisque #sauveSurDisque #ajoutG #modificationG #allerAGauche #allerADroite) at: clientData + 1)

**menuCentrallItem:widget clientData:clientData callData:callData**

*"associe les options du menu à des traitements"*

self perform: ( #( #chargeDuDisque #sauveSurDisque #ajoutG #modificationG #allerAGauche #allerADroite) at: clientData + 1)

**menuDroitItem:widget clientData:clientData callData:callData**

*"associe les options du menu à des traitements"*

self perform: ( #( #chargeDuDisque #sauveSurDisque #ajoutG #modificationG #allerAGauche #allerADroite) at: clientData + 1)

## Interface avec les fichiers

On notera que la méthode qui charge un dictionnaire à partir d'un fichier doit mettre à jour la fenêtre (barre de titre et panneaux). On rappelle (cf 6.1.2) que le format choisi pour les fichiers-dictionnaires est celui d'un fichier-texte, avec la convention suivante. Une analogie étant représentée par un mot-clé *m* et une liste de mots considérés comme proches de *m*, on la représentera par deux lignes dans le fichier, la première donnant le mot *m* précédé par le libellé « analogies pour », la seconde, commençant par un retrait de deux espaces pour améliorer la lisibilité, donnera la liste des mots proches de *m*.
### nomDuFichier: uneChaine

"affecte uneChaine à la variable d'instance nomDuFichier
si une fenêtre est ouverte, on change son label pour indiquer le nouveau fichier"

nomDuFichier := uneChaine.

fenetre notNil

ifTrue: [ fenetre title: 'Utilisation du dictionnaire ', uneChaine; updateTitle]

### chargeDuDisque

"détermine le nom du fichier de chargement et garnit la variable d'instance dictionnaire
renvoie true si tout s'est bien passé, false si le fichier était de format incorrect"

lfileir ligne1 ligne2!

self nomDuFichier: (CwTextPrompter prompt: 'nom du fichier de chargement ?'
answer: 'd:\chap6\sentimts.txt').

fichier := EstdCodeStream pathname: nomDuFichier.

[fichier atEnd]

whileFalse: [ ligne1 := fichier fileStream nextLine subStrings.
(ligne1 size < 3)
ifTrue: [ ^false].
fichier atEnd
ifTrue: [ ^false].
ligne2 := fichier fileStream nextLine trimBlanks.
dictionnaire at: (ligne1 at: 3) put: ligne2].

self panneauGauche.

self panneauCentral.

self panneauDroit.

self panneauBas.

^true

### sauveSurDisque

"recopie le dictionnaire dans le fichier désigné par nomDuFichier"

Isauvegarde "représentera le fichier de sauvegarde" |

"on demande d'abord à l'utilisateur d'indiquer ou de confirmer le nom du fichier"

nomDuFichier isNil

ifTrue:[self nomDuFichier:
(CwTextPrompter prompt: 'nom du fichier pour la sauvegarde ?'
answer: 'd:\chap6\dico.txt')].

sauvegarde := EstdCodeStream pathname: nomDuFichier.

dictionnaire associationsDo:

[: couple "désignera chaque analogie enregistrée" |
sauvegarde fileStream nextPutAll: 'analogies pour ', couple key; cr;
nextPutAll: ' ', couple value; cr].

self panneauGauche.

self panneauCentral.

self panneauDroit.

self panneauBas.

sauvegarde close.
# Glissement latéral des panneaux

## allerADroite

"cette méthode avance d'un panneau vers la droite"

((indexSelectionGauche +2) = suiteDeSuivi size)

ifTrue: [CwMessagePrompter warn: 'Impossible' ^nil].

indexSelectionGauche := indexSelectionGauche + 1.

self ajustePanneaux

## allerAGauche

"cette méthode revient un panneau en arrière"

indexSelectionGauche = 1

ifTrue: [CwMessagePrompter warn: 'Impossible' ^nil].

indexSelectionGauche := indexSelectionGauche - 1.

self ajustePanneaux

# Ajout d'analogies

Les méthodes de base sont ajoute:pour:., qui permet d'ajouter un mot proche d'un mot-clé du dictionnaire et ajouteRelationsEntre:et:., qui enregistre une relation symétrique entre deux mots (en faisant appel deux fois à ajoute:pour:.). Les méthodes ajoutG, ajoutC et ajoutD correspondent aux options « Ajouter » des menus des trois panneaux verticaux. Elles font toutes les trois appel à la même méthode ajouteAPartirDe:avecIndex:

## ajoute: motAssocie pour: motCle

"ajoute la relation motCle/motAssocie dans le dictionnaire récepteur

crée l'association si motCle n'était pas répertorié

renvoie true en cas de succès et false si la relation était déjà enregistrée"

listeMotAssocies!

(dictionnaire includesKey: motCle)

ifTrue: ["motCle est déjà une clé enregistrée dans le dictionnaire"

listeMotAssocies := dictionnaire at: motCle.

"listeMotAssocies reçoit la chaîne des mots en relation avec motCle"

"maintenant, on regarder si motAssocie est enregistré avec de motCle"

(listeMotAssocies subStrings includes: motAssocie)

ifTrue: [^false "car on a trouvé motproche"]

ifFalse: [dictionnaire at: motCle

put: listeMotAssocies, '', motAssocie.

"ajoute motAssocie aux mots associés déjà existants"

^ true]]

ifFalse: ["motCle n'est une clé déjà enregistrée dans le dictionnaire"

dictionnaire add: (Association key: motCle value: motAssocie).

^ true]
ajouteRelationsEntre: mot1 et: mot2

"ajoute au dictionnaire, si elles n'y sont pas déjà, les relations mot1/mot2 et mot2/mot1,
renvoie false si l'une des deux relations était déjà présente, sinon renvoie true"
reponse "indiquera si tout s'est bien passé"
reponse := self ajoute: mot1 pour: mot2.
^ (self ajoute: mot2 pour: mot1) & reponse

ajoutC

"opération d'ajout déclenchée dans le panneau gauche"
self ajouteAPartirDe: listeCentrale avecIndex: (indexSelectionGauche + 1)

ajoutD

"opération d'ajout déclenchée dans le panneau gauche"
self ajouteAPartirDe: listeDroite avecIndex: (indexSelectionGauche + 2)

ajoutG

"opération d'ajout déclenchée dans le panneau gauche"
self ajouteAPartirDe: listeGauche avecIndex: indexSelectionGauche

ajouteAPartirDe: unPanneau avecIndex: indexSelection

"méthode chargée d'enregistrer un ajout dans le dictionnaire; son exécution peut être
déclenchée par l'un des trois panneaux verticaux qui se transmet lui-même en argument
et communique son index dans suiteDeSuivi"
icle motsProposes motsAjoutes texte motsExistants!
cle := suiteDeSuivi at: indexSelection.
cle isNil
ifTrue: [ texte := 'pour : (tape le mot de départ ci dessous)'. motsExistants := #()
ifFalse: [ texte := ' confirme ou modifie le mot de départ').
motsExistants := (dictionnaire at: cle) subStrings].
cle := CwTextPrompter prompt: ('ajout d"une analogie', texte) answer: cle.
motsProposes :=
(CwTextPrompter prompt: 'tape le(s) mot(s) proches séparés par des espaces'
answer: ").
motsProposes notNil
ifTrue: [ motsProposes := motsProposes subStrings asSet.
motsAjoutes := motsProposes reject: [ candidat I
motsExistants includes: candidat].
motsAjoutes do: [ mot I self ajouteRelationsEntre: cle et: mot].
self ajustePanneaux]
## Modification du dictionnaire (ajout et suppression)

Pour les suppressions, les opérations de base correspondent aux deux méthodes `supprime:pour:` et `supprimeRelationsEntre:et:`. Les méthodes `modificationG`, `modificationC` et `modificationD` correspondent aux options « Modifier » des menus des trois panneaux verticaux. Elles font toutes les trois appel à la même méthode `modifieAPartirDe:avecIndex:`.

### supprime: motAssocie pour: motCle

"supprime la relation motCle/motAssocie dans le dictionnaire récepteur.
supprime l'association complète si motAssocie était la seule analogie de motCle
renvoie true en cas de succès et false si la relation n'était pas enregistrée"

listeMotSAssocies nouvelleListe
(dictionnaire includesKey: motCle)

ifFalse: [^ nil]

ifTrue: ["motCle est déjà une clé enregistrée dans le dictionnaire"

listeMotSAssocies := dictionnaire at: motCle.

"listeMotSAssocies reçoit la chaîne des mots en relation avec motCle"

"maintenant, il faut regarder si motAssocie est enregistré pour motCle"

listeMotSAssocies := listeMotSAssocies subStrings.

(listeMotSAssocies includes: motAssocie)

ifFalse: [^false "car on n'a pas trouvé motproche"]

ifTrue: ["s'il n'y a plus que motAssocie, on supprime l'association"

listeMotSAssocies size = 1

ifTrue: [dictionnaire removeKey: motCle.

^ true]

ifFalse: [ nouvelleListe :=".

listeMotSAssocies do:

:[mot1 mot = motAssocie

ifFalse:[ nouvelleListe := nouvelleListe,mot, ']]].

dictionnaire at: motCle put: nouvelleListe.

^ true]]]

### supprimeRelationsEntre: mot1 et: mot2

"supprime, si elles existent dans le dictionnaire, les relations mot1/mot2 et mot2/mot1 renvoie false si l'une des deux relations était déjà présente, sinon renvoie true"

reponse "indiquera si tout s'est bien passé"!"

reponse := self supprime: mot1 pour: mot2.

^ (self supprime: mot2 pour: mot1) & reponse

### modificationG

"opération de modification déclenchée dans le panneau gauche"

self modifieAPartirDe: listeGauche avecIndex: indexSelectionGauche

### modificationC

"opération de modification déclenchée dans le panneau central"

self modifieAPartirDe: listeCentrale avecIndex: (indexSelectionGauche + 1)

### modificationD

"opération de modification déclenchée dans le panneau droit"

self modifieAPartirDe: listeDroite avecIndex: (indexSelectionGauche + 2)
## modifieAPartirDe: unPanneau avecIndex: indexSelection

```
"méthode chargée d'enregistrer une modification dans le dictionnaire;
son exécution peut être déclenchée par l'un des trois panneaux verticaux
qui se transmet lui-même en argument et communique son index dans suiteDeSuivi"

icle motsProches motsChoisis motsSupprimes motsAjoutes textel
cle := suiteDeSuivi at: indexSelection.
cle isNil ifTrue: [ texte := 'pour :(tape le mot de départ ci dessous)']
                  ifFalse: [ texte := '(confirme ou modifie le mot de départ)'.
cle := CwTextPrompter prompt: ('modification d"analogie ', texte) answer: cle.
motsProches := dictionnaire at: cle ifAbsent: [motsProches := "].
motsChoisis := (CwTextPrompter prompt: 'modifie le ou les mots proches'
                  answer: motsProches).
motsChoisis notNil
ifTrue: [motsChoisis := motsChoisis subStrings asSet.
motsProches := motsProches subStrings.
motsSupprimes := motsProches reject:[: candidat |
motsChoisis includes:candidat].
motsAjoutes := motsChoisis reject:[: candidat | motsProches includes:candidat].
motsAjoutes do:[: mot I self ajouteRelationsEntre: cle et:mot].
motsSupprimes do:[: mot I self supprimeRelationsEntre: cle et:mot].
(motsSupprimes size > 0)
ifTrue:[self metAJourSuiteDeSuivi].
self ajustePanneaux]
```
### metAJourSuiteDeSuivi

"après des suppressions de relations, cette méthode annule, s'il y en a, les sélections qui correspondent aux mots supprimés; elle renvoie true si suiteDeSuivi a été modifiée"

lindex motCourant motPrecedent modificationl

"on parcourt suiteDeSuivi en valiant ou invalidant chaque mot rencontré"

index := 1.

[(suiteDeSuivi at: index) notNil] whileTrue:
    [motCourant := suiteDeSuivi at: index. "on est sûr que motCourant n'est pas nil"
        index = 1 ifTrue:[ "on est au début de suiteDeSuivi : la sélection
doit faire partie du dictionnaire"
        modification := (dictionnaire includesKey: motCourant) not]
    ifFalse:[ "on n'est pas au début de suiteDeSuivi : motCourant doit
    être abandonné s'il n'est plus un mot proche de la sélection
    précédente"
        motPrecedent := suiteDeSuivi at: (index - 1).
        modification := ((dictionnaire at: motPrecedent) subStrings
    includes: motCourant) not].
    "ici, si modification est vraie, c'est que l'on avait une sélection à invalider"
    modification
    ifTrue: [suiteDeSuivi at: index put: nil "et on sort de l'itération"]
    ifFalse: [index := index + 1 "et on continue l'itération"]
].

"sortie de l'itération : si index est inférieur à la taille de suiteDeSuivi, il faut tronquer suiteDeSuivi tout en conservant au moins trois éléments" (index = suiteDeSuivi size) ifTrue: [^ modification].
[index < 3] whileTrue: [index := index + 1. suiteDeSuivi at: index put: nil].
"suppression des éléments inutiles de suiteDeSuivi"
[suiteDeSuivi size > index] whileTrue: [suiteDeSuivi removeLast].
"on doit encore ajuster indexSelectionGauche"
indexSelectionGauche > (suiteDeSuivi size - 2)
ifTrue: [indexSelectionGauche := suiteDeSuivi size - 2].
^ modification
<figure><img src="image_46.png" /></figure>
Bibliographie

[Bro 87] Fred Brooks.
No Silver Bullet - Essence and Accidents of Software Engineering
IEEE Computing, April 1987

[Dig 86] Smalltalk/V : Tutorial and Programming Handbook
Digitalinc., 1986

[IBM 94] IBM Smalltalk Prorammer's Reference
IBM Corporation, 1994

[GoR 89] A. Goldberg & D. Robson
Smalltalk-80 : The Language
Addison Wesley, 1989

[LaP 90] R. Lalonde & JR Pugh
Inside Smalltalk (volumes 1 et 2)
Prentice Hall

[Mas 89] G. Masini et al
Les langages à objets
InterEditions 1989

[PiW 88] L.J Pinson & R.S. Wiener
An Introduction to Object Oriented Programming and Smalltalk
Addison Wesley, 1988
<figure><img src="image_47.png" /></figure>
# Index alphabétique

$ (méthode) 22
* (méthode) 28-29, 54
+ (méthode) 53
/ (méthode) 53, 55
// (méthode) 55
\\ (méthode) 55
= (méthode) 70, 132
== (méthode) 106, 132
@ (méthode) 25
, (méthode) 106
add: (méthode) 97, 101, 114
add:after: (méthode) 114
add:afterIndex: (méthode) 114
add:before: (méthode) 114
add:beforeIndex: (méthode) 114
addAll: (méthode) 121
addAllFirst: (méthode) 114
addAllLast: (méthode) 114
addFirst: (méthode) 114
addLast: (méthode) 114
adjustSize (méthode) 128
adresse 126
after: (méthode) 114
after:ifNone: (méthode) 114
argument
d'un bloc 33
d'une méthode 35, 72
Array (classe)
créer une instance 58
manipuler une instance de 56

sous-classe de Collection 57
tableau 11, 23
asArray (méthode) 55, 100
asArrayOfSubstrings (méthode) 20, 66, 88-89, 172
asBag (méthode) 100
ASCII (code) 67, 75-77, 162
asFloat (méthode) 51-53
asIndexedCollection (méthode) 100
asInteger (méthode) 42-43
asLowercase (méthode) 175
asSet (méthode) 100
associationAt:ifAbsent: (méthode) 102
associationDo: (méthode) 102-104
asSortedCollection (méthode) 100
asString (méthode) 71
Astre (classe) 109
asUpperCase (méthode) 21, 66, 68
at: (méthode) 12, 22, 30, 102, 114, 118
at:ifAbsent: (méthode) 102
at:put: (méthode) 12, 30, 57, 62, 101, 114
atAllPut: (méthode) 107
atAll:Put: (méthode) 107
atEnd (méthode) 142, 172
Bag (classe)
classe 74, 95
manipuler une instance de 97
méthodes 118
basicAt: (méthode) 127
basicAt:put: (méthode) 127
basicSize (méthode) 127
become: (méthode) 74, 129
before: (méthode) 114
before:ifNone: (méthode) 114
between:and: (méthode) 2, 16
blocs 21, 41
Boolean (classe) 42-43
browse (méthode) 17
Browse Disk 153
browser 17, 59, 71, 73, 76-77
buffer 164
ByteArray (classe) 74
caractères
  classe Character 74
  manipuler une chaîne 66
  stockage en mémoire 126
cascade 26, 58
CfsDirectoryDescriptor 175
CfsFileDescriptor 175-176
chaîne 2-4, 7
CharacterConstants (dictionnaire partagé) 157, 163
checkIndex: (méthode) 134
class (méthode) 10, 17-18, 51-52, 58-59, 69, 71, 73, 76-77, 92-93
Class Hierarchy Browser 59, 74
classe
  abstraite 13, 42, 47, 96, 105, 108
  créer 74-75
  généralités 2, 8-10
  sauver les méthodes d'une 77
classVariableNames: 38
clé 95
cliquer 5, 6, 18
close (méthode) 78, 165, 173
collect (méthode) 108
Collection (classe)
  ajouter des éléments 121
  classe 95
  généralités 46, 56-57, 65, 78, 81, 86
  méthodes 100, 118

sous-classes 95-96
stockage en mémoire 126, 135
contents (méthode) 150
convertir
  des instances de collection 100
  nombres en caractères 139
  octets en nombres 161
  méthode de hachage 126
copie
  d'un fichier 156
  d'une collection 107, 140
copy (méthode) 107
copyFrom:to: (méthode) 62, 107
copyFrom:to:with: (méthode) 107
copySelection (méthode) 184-185
copyWith: (méthode) 107
copyWithout: (méthode) 107
cr (méthode) 158
Cr (abréviation) 163
createForm: 178
createText: 181
crochets 8, 21, 33
curseur 5-7
cutSelection (méthode) 184-185
CwComposite 179-180
CwForm 178-180
CwList 179-180
CwPrimitive 179-180
CwShell 179-180
CwText 179-180
CwTextPrompter 171
CwTopLevelShell 178
CwWidget 179
degreesToRadians (méthode) 52
denominator 27-29, 49, 53-54
denominator: (méthode) 28, 49-50
detect (méthode) 113
dictAdd: 171
dictAt:put: 171
Dictionary (classe)
  classe 78, 96, 100-101
  sous-classe 130, 132
elements (variable d'instance) 119
méthodes 131
le dictionnaire Smalltalk 113
différence avec la classe
IdentityDictionary 132
dictionnaires partagés 162
Directory (classe) 155

Display 6-7, 20

do it 20

do: (méthode) 32, 50, 83, 146, 183, 198

drive: (méthode) 36

edit (méthode) 67

éditeur 4, 6, 12, 63, 67, 169

élément 95

elementCount 86, 170-171

énumération 95

encapsulation 15

equal (méthode) 16, 82

error: (méthode) 62, 64

errorInBounds (méthode) 134

errorNotIndexable (méthode) 118

EsLinearHashMap (classe) 171

EstdCodeStream (classe) 172

EtFileNamePrompter (classe) 175

expressions 2-4

false 16, 42-44

fenêtre

d'une application 181, 195-197
généralités 3-7
menu 182
ouverture d'un flux sur une
collection 140

fichier

copier 156
créer 153
généralités 80, 83
lire 155
modifier 157
nom 66
sauvegarder 163
sauvegarde Smalltalk 77
utiliser 168-169, 172, 175-176

file d'attente (simulation) 116

File (classe)

classe 155, 163

variables d'instance

lastByte 163

writeLimit 164

pageStart 164

position 164

file (méthode) 155

FileControlBlock (classe) 155

fileExtension 66

fileIn (méthode) 78

filename 66

FileStream (classe) 38, 77-78, 153, 155

find:ifAbsent: (méthode) 124

findElementIndex: (méthode) 124, 127

findFirst (méthode) 106

findLast (méthode) 106

first (méthode) 106, 116

firstIndex (méthode) 62

FixedSizeCollection (classe)

classe 57, 66, 77, 105, 108

méthodes 135

sous-classe 108, 135

Float (classe) 13-14, 22, 29, 46-49, 51-55

floatError (méthode) 52

flush (méthode) 165

flux 138

ouvrir 140

parcourir 142

fonction de hachage 125-127

fraction 13-15, 22, 27-29, 31, 47-51, 53-55

from:to: (méthode) 55

from:to:by: (méthode) 55

fromInteger: (méthode) 51, 53

grow (méthode) 128

growEmptyBy: 171

guillemets 8

hachage

fonction 126

gestion des collisions 127

notion 81-82, 124, 169

hash (méthode) 128
héritage 11-12, 14-16, 68
hiérarchie des classes 12, 17
IdentityDictionary (classe) 132
ifFalse: (méthode) 15, 41, 43
ifTrue: (méthode) 8-9, 15, 41-43
ifTrue:ifFalse: (méthode) 15, 41
implementedBySubclass (méthode) 121
includes: (méthode) 98, 100
includesAssociation: (méthode) 100
includesKey: (méthode) 100
index 132
IndexedCollection (classe)
  classe 57, 66, 69, 96, 105
  méthodes 134
indexOf: (méthodes) 106
indexOf:ifAbsent: (méthode) 106
indice 95
initialize (méthode) 119
inject:into: (méthode) 105
inspect (méthode) 50-51
inspection 50-51, 83-87
Integer (classe) 74
Interval (classe) 96
isEmpty (méthode) 77, 146
isKindOf: (méthode) 92
isNil (méthode) 27, 173
itération 9-10, 34, 44-46, 81
key:value: (méthode) 78
keyAtValue: (méthode) 102
keys (méthode) 134
keys: (méthode) 102
keysDo: (méthode) 103-104
last (méthode) 106
Lf (abréviation) 157, 163
lineDelimiter (méthode) 38, 157, 163
lineDelimiter: (méthode) 157
listes instances de collection 113, 135
Magnitude (classe) 13-19, 47, 79, 92-93
mantisse 51
matrices 59-65

mémoire (utilisation)
  accès aux objets 126
  hachage 124, 126-127
menu 67, 73, 182
message
  binaire 2, 25-26, 44, 48, 53, 73
  unaire 2, 8, 25-26, 43, 45
  à mots-clés 8, 15, 25-26, 44
méthode 7-9
  d'instance 11
  de classe 11
  privée 119
model 169
Model/View/Controller 188
MouseButton 36, 38
MVC 188
name (méthode) 176
new (méthode) 97
new: (méthode) 57, 75, 126
next (méthode) 142
next: (méthode) 144, 150
next:put: (méthode) 149
nextBytePut: (méthode) 160
nextLine (méthode) 160, 172
nextMatchfor: (méthode) 144
nextPut: (méthode) 148, 152
nextPutAll: (méthode) 3-4, 7, 12, 26, 39, 56, 80-81, 88, 148, 173
nextTwoBytesPut: (méthode) 160
nextWord (méthode) 158
nil 127
notNil (méthode) 27, 197, 200-201
Number (classe) 14, 18, 21, 27, 29, 46-48, 51-56, 93, 121
numerator 27-29, 49, 53-54
numerator: (méthode) 28, 49-50
numerator:denominator: (méthode) 28, 49, 51
Object (classe) 13, 16-17, 27, 30, 38, 42, 50, 57, 66, 79, 84, 187, 189-190, 195
occurrencesOf: (méthode) 98, 102, 120
souris 153, 188
globales 36
space (méthode) 77
partagées 35
splitPath: (méthode) 175
temporaires 33
squared (méthode) 14, 47-48
variableSubclass 59, 75
Stream (classe)
  classe 71, 137, 172
  méthodes 136
  variable d'instance
    readLimit 138, 151
    writeLimit 148, 151
    collection 149, 151
  sous-classes 146, 151
String (classe) 8, 10-11, 15, 20-21, 30, 35, 45, 65, 68, 172, 174-176, 181
subclass: (méthode) 27, 38
super 85-87, 190
Symbol 21, 58
symbole 5, 8-9, 21, 36, 38, 65, 92
synchronisation 193, 196-197
System Transcript 99
tab (méthode) 158
tableau 56
tampon (mémoire) 139, 185
template 19
timesRepeat: (méthode) 46
title: (méthode) 178
to:by: (méthode) 55
transferts 139
trim: (méthode) 67
trimBlanks (méthode) 67, 172, 175-176
truncated (méthode) 24, 26
UndefinedObject (classe) 11, 27, 31, 33
upTo: (méthode) 144, 176
value (méthode) 34, 40, 43, 46, 78, 82, 88, 92
value: (méthode) 34, 42, 78, 85, 89
value:value: (méthode) 34, 42
values 102-103
variableByteSubclass 68, 75
variables 3-4, 8-9
de classe 38
dictionnaires partagés 37

variableSubclass: 38, 59, 77, 83, 195
whileFalse: (méthode) 44-45
whileTrue: (méthode) 9, 44-46, 71, 80
widget 179-180
Windows 167, 177-178, 188
with: (méthode) 11, 57, 64-65, 69, 83, 85-86, 91, 176, 196
with:with: (méthode) 57-58, 65, 100, 135
with:with:with: (méthode) 11, 57, 65, 84
with:with:with:with: (méthode) 65, 68
withCrs (méthode) 67
withDo: (méthode) 107
with:from:to: (méthode) 147
WriteStream (classe) 146
writtenOn 38
XmPUSHBUTTON 184
XmSEPARATOR 184
XmSINGLESELECT 191-192
yourself (méthode) 122
zeroDivisor (méthode) 24
MASSON Éditeur
120, Bd Saint-Germain
75280 Paris Cedex 06
Dépôt légal : mars 1996

Normandie Roto Impression s.a.
61250 Lonrai
N° d'imprimeur : 960155
Dépôt légal : février 1996
Manuels Informatiques Masson

<figure><img src="image_48.png" /></figure>

# Programmer objets avec Smalltalk

Gilles Clavel • Nathalie Lopez • Luc Veillon

La programmation orientée objets reste difficile à aborder pour un débutant. Cet ouvrage en fournit une approche progressive et pédagogique qui s'appuie sur des exemples pratiques. Pour ces exemples, les auteurs ont utilisé l'environnement Smalltalk qui présente deux avantages : il ne nécessite pas de connaissances techniques préalables et il n'est pas non plus un prototype de laboratoire accessible aux seuls universitaires.

Depuis la première édition de cet ouvrage, la situation industrielle a sensiblement évolué. Smalltalk ne sert plus à illustrer les concepts objets : il est devenu un outil à part entière pour le développement de logiciel. Désormais, trois implémentations de Smalltalk se partagent le marché, mais la plupart des caractéristiques du langage leur sont communes. Elles sont présentées dans les cinq premiers chapitres de la présente édition, avec des exemples en Smalltalk V et IBM Smalltalk. Quant à l'interface graphique, qui diffère au sein de chaque implémentation, les auteurs ont retenu celle d'IBM Smalltalk.

Le livre s'articule en trois parties :

- découverte des concepts de base que l'on retrouvera dans la plupart des environnements orientés objets ;

- approfondissement des concepts précédents, avec une explication des mécanismes internes, en s'appuyant sur l'architecture de Smalltalk ;

- construction d'une application disposant d'une interface graphique. On montre ainsi la facilité d'emploi des langages orientés objets dès que l'on veut manipuler des objets complexes tels que des fenêtres, menus, listes, dictionnaires...

Cet ouvrage s'adresse plus particulièrement aux étudiants en informatique, aux élèves des écoles d'ingénieurs et à leurs enseignants. Il est également accessible à tout utilisateur de micro-ordinateur désireux d'apprendre un langage d'une conception radicalement différente de celle de langages comme Basic ou Pascal. Il pourra servir de base et d'aide-mémoire aux programmeurs expérimentés, se formant aux environnements orientés objets.

Professeur à l'Institut national agronomique, Gilles Clavel est directeur consultant de la société IMA-informatique. Nathalie Lopez y est responsable des formations méthodes objet. Luc Veillon est chargé de recherches à l'ORSTOM et responsable informatique du laboratoire ERMES.

ISBN : 2-225-85157-3

<figure><img src="image_49.png" /></figure>

9 782225 851575

<figure><img src="image_50.png" /></figure>

<figure><img src="image_51.png" /></figure>