6.2.3 Développement d’une interface utilisateur graphique
Toute calculatrice se
compose d’au moins un écran d’affichage, de dix touches numériques et d’un
nombre plus ou moins grand de touches fonctionnelles. Cet ensemble
d’écrans et de touches doit être integré visuellement dans un cadre et
fonctionnellement de manière telle que chaque activation d’une touche fait
changer l’affichage.
La figure 6.16 donne l’image d’une calculatrice postfixe pour les quatre opérations
arithmétiques élémentaires. Cette calculatrice est dans une fenêtre qui a la même
allure externe que les fenêtres standard de SQUEAK: une bande en haut affiche des
icônes pour détruire la fenêtre, pour avoir un menu, pour changer sa taille et pour
iconifier la fenêtre; au milieu de cette bande est affiché le nom de la
fenêtre.
Une telle fenêtre est une instance de la classe SystemWindow. La classe SystemWindow est une
sous-classe de Morph10.
Elle peut donc, comme tout autre morph, posseder des sous-morphs, comme des
touches ou des écrans.
En haut de notre calculatrice se situe, sur toute la largeur un grand écran destiné
à afficher les nombres qu’on entre et les résultats des calculs. À la droite de la
calculatrice se situe un deuxième écran qui affiche l’historique des opérations
effectuées et des résultats obtenus. Ces deux fenêtres sont des instances de la
classe PluggableTextMorph, des morphs prévus pour afficher (et éventuellement
éditer) du texte.
Le touches sont, naturellement, des instances de la classe PluggableButtonMorph
dont nous venons voir le fonctionnement à travers nos boutons de téléviseur de la
section précédente.
| FIG. 6.16:
| Notre
première
calculatrice |
|
|
Revenons un instant sur les PluggableTextMorphs. Un tel morph doit connaître
au minimum son modèle, la classe, qui lui livre le texte à afficher, et le sélecteur
de la méthode permettant d’importer ce texte (cela doit être une méthode qui
retourne une chaîne de caractères). Souvent on lui donne également le
sélecteur de la méthode permettant de modifier le texte. Les deux sélecteurs
doivent réferer à des méthodes de la classe modèle, puisque le modèle est
propriétaire du texte et possède des méthodes d’accès et de modification de ce
texte.
Une des méthodes de classe pour la création d’un PluggableTextMorph est
on:text:accept: qui crée un tel morph sur la classe donnée comme argument à
on:, avec le sélecteur d’accès au texte donné en argument à text: et le
sélecteur de modification de texte donné en argument à accept:. Ainsi, le
PluggableTextMorph que nous créons pour l’écran principal de notre calculatrice
est créé par la transmission:
PluggableTextMorph
on: self
text: #printString
accept: nil.
|
|
où self désigne l’instance de la calulatrice pour laquelle nous construisons une
vue, le sélecteur printString retourne la chaîne de caractères bufferEcran,
puisque nous avons redéfini la méthode printOn: comme ci-dessous:
Calculatrice>>printOn: aStream
aStream nextPutAll: bufferEcran
|
|
et, puisque l’écran de toute calculatrice n’existe que pour afficher du texte et
jamais pour changer de texte, le sélecteur de modification de texte, l’argument de
accept:, est initialisé à la valeur indéfinie.
Pour la fenêtre de l’historique du calcul, le deuxième PluggableTextMorph de
notre calculatrice, nous utilisons la méthode de création:
on:text:accept:readSelection:menu:
Les premiers trois arguments sont du même type: un objet responsable du texte
et possédant des méthodes pour livrer ce texte au PluggableTextMorph
et, éventuellement, une méthode pour receveoir du texte de ce morph.
L’argument readSelection: attend une instance de la classe Interval
comprenant les indices des caractères qui seront sélectionnées lors de
l’affichage du texte. Par la suite nous allons sélectionner toujours le
caractère suivant le dernier caractère du texte affiché. De cette manière
nous garantissons que la fenêtre rendra le dernier texte ajouté toujours
visible11.
Finalement, l’argument menu: attend un menu contextuel.
Commençons par créer en plus des classes Touche, Calculatrice
et CalculatricePostfixe une classe Ecran représentative de l’écran
historique de notre calculatrice. Elle possède une variable d’instance,
hist, qui accumule l’ensemble des interactions entre l’utilisateur et la
calculatrice:
Model subclass: #Ecran
instanceVariableNames: ’hist’
classVariableNames: ’’
poolDictionaries: ’’
category: ’Cours-Smalltalk’
|
|
Les deux méthodes d’accès et de modification de la variable d’instance
nous serviront également d’interface avec le morph responsable d’afficher
l’écran:
et
Ecran>>hist: aString
hist := aString.
|
|
Pour imprimer une instance d’écran, il suffit d’afficher le contenu de la variable
d’instance hist:
Ecran>>printOn: aStream
hist printOn: aStream
|
|
Après ces travaux préparatoires, commençons à mettre en place les vues pour
chacune des parties de notres calculatrice. Les vues seront construites par la
méthode openInWorld dont voici le début de définition:
Calculatrice>>openInWorld
1 | window view c l couleur a1 a2 a3 |
2 window := SystemWindow labelled: self class name.
3 a1 := AlignmentMorph newRow.
4 a2 := AlignmentMorph newRow.
5 a3 := AlignmentMorph newRow.
|
|
Pour nous simplifier le travail nous commençons par déclarer quelques variables
temporaires:
-
- window contiendra le morph principal de la calculatrice. Tous les autres
morphs que nous utiliserons seront des sous-morphs de window. Dans
la ligne 2 nous initialisons cette variable à une instance de
SystemWindow avec le nom de la classe comme en-tête de la fenêtre.
Au début, le nom sera CalculatricePostfixe, puisque nous créons la
calculatrice par la transmission
CalculatricePostfixe new openInWorld
-
- view sera utilisé pour garder temporairement des morphs en constructions.
-
- c et l sont des variables temporaires qui nous aideront à savoir dans quelle
colonne et dans quelle ligne d’un tableau de morphs nous nous
trouvons à un instant donné.
-
- couleur nous sert juste à garder une fois pour toutes la couleur des fonds
pour nos sous-morphs.
-
- a1, a2 et a3 garderont temporairement des AlignmentMorphs comme
montré dans les lignes 3 à 5 de la méthode openInWorld ci-dessus.
Examinons la suite de notre méthode:
6 view := PluggableTextMorph
7 on: self
8 text: #printString
9 accept: nil.
10 view
11 font: (StrikeFont familyName: ’Atlanta’ size: 22).
|
|
Dans les lignes 6 à 11 nous créons la fenêtre d’affichage de la calculatrice: elle est
une instance de la classe PluggableTextMorph possédant comme modèle
l’instance de la calculatrice que nous sommes en train de créer. Elle affiche le
texte que la méthode printString retourne quand elle est transmise au modèle
et elle exclut de changer le texte du modèle (l’argument accept: est indéfini,
nil).
Ce PluggableTextMorph affiche son texte en caractères relativement grands, taille
22 pixels, dans le jeux de caractères Atlanta. Tout morph pouvant afficher du
texte possède l’attribut font: déterminant le jeu de caractères utilisé pour
l’afficher.12
Voici les trois lignes suivantes de la méthode Caculatrice»openInWorld:
12 window
13 addMorph: view
14 frame: (0 @ 0 extent: 1 @ (1 / 5)).
|
|
Avec la méthode addMorph:frame: nous ajoutons la vue juste que nous venons de
construire, l’écran de la calculatrice, au morph principale de notre calculatrice. Le
rectangle le représentant commence au point supérieur gauche (le point 0@0) et
s’étendra horizontalement sur toute la largeur et prendra verticalement 1
5-ième de
la fenêtre principale.
Pour placer des sous-morphs à des endroits précis d’un morph, soit on utilise des
AlignmentMorphs, et le placement se fait proportionnellement avec la méthode
addMorph:, soit on donne, avec la méthode addMorph:frame: le placement avec
un rectangle dont tous les points sont donnés en mesures relatives par rapport à
la taille du morph receveur. Le rectangle:
0 @ 0 extent: 1 @ (1 / 5)
peut se lire comme:
0 × largeur-morph-receveur @ (0 × hauteur-morph-receveur)
extent: 1 × largeur-morph-receveur @ (1 /
5 × hauteur-morph-receveur )
L’écran occupe le haut de la calculatrice sur un cinquième de la hauteur.
Ci-dessous alors la partie se chargeant de mettre en place l’écran historique de
notre calculatrice. Cet écran, également un PluggableTextMorph, a comme
modèle une instance de la classe Ecran, qui lui a comme modèle la calculatrice.
Cette dernière dépendance est réalisée lors de sa création dans la méthode on: de
la classe Ecran que voici:
Ecran class>>on: aCalc
| aux |
aux := Ecran new.
aux hist: ’’.
aux addDependent: aCalc.
^ aux
|
|
Mais revenons vers la suite de notre méthode openInWorld:
15 view := PluggableTextMorph
16 on: (c := Ecran on: self)
17 text: #hist
18 accept: nil
19 readSelection: #selectTextInterval
20 menu: #myMenu:.
21 self addDependent: c.
22 view setTextColor: Color blue.
23 window
24 addMorph: view
25 frame: (4 / 5 @ (1 / 5) extent: 1 / 5 @ (4 / 5)).
|
|
Répétons-le: la vue sur l’écran historique affiche le texte obtenu par le message
hist, donc le contenu de la variable d’instance hist et n’accepte, comme l’écran
principal de la calculatrice, aucune modification: le texte affiché ne sera
accessible qu’en lecture. Pour forcer le morph à dérouler le texte tel que sa fin
soit toujours visible dans la fenêtre, la méthode selectTextInterval
choisit la fin du texte comme partie du texte à sélectionner, donc aussi à
afficher.
Voici cette méthode selectTextInterval:
Ecran>>selectTextInterval
^Interval
from: hist size + 1
to: hist size + 1.
|
|
L’argument de menu:, dans la création du PluggableTextMorph, est le sélecteur
d’une méthode du modèle, donc ici de la classe Ecran, qui retourne le
menu à activer si l’on clique le bouton jaune de la souris. Notre méthode
myMenu:
Ecran>>myMenu: evt
| menu |
menu := MenuMorph new.
menu addTitle: ’Ecran-Menu’;
add: ’inspect’
action: #calculatriceInspect.
^ menu
|
|
propose un menu avec une seule entrée: inspect. Bien entendu nous voulons
inspecter notre calculatrice et non pas le PluggableTextMorph à partir
duquel nous pouvons obtenir ce menu. Pour cela, nous donnons la méthode
calculatriceInspect que nous devons ajouter à la classe PluggableTextMorph
comme suit:
PluggableTextMorph>>calculatriceInspect
self model dependents
do:[:obj | (obj isKindOf: Calculatrice)
ifTrue:[^obj inspect]]
|
|
L’écran, instance de la classe PluggableTextMorph, possède comme modèle une
instance de la classe Ecran. Une telle instance possède deux dépendants: le
morph qui la représente graphiquement et la calculatrice dont elle est
l’écran historique. Dans la méthode ci-dessus la transmission self model
retourne donc l’instance de la classe Ecran associée au PluggableTextMorph
receveur. Cette instance de la classe Ecran recoit le message dependents qui
retourne le tableau de ses dépendants. Ce tableau est ensuite parcouru,
avec le message do: jusqu’au rencontre d’un objet qui est une sorte
de13
Calculatrice. La méthode isKindOf: de la classe Object répond true si le
receveur est une instance de la classe ou d’une des sous-classes de la classe donnée
en argument. Ici isKindOf: répond true si le receveur est une instance de
Calculatrice ou CalculatricePostfixe. Cette instance reçoit ensuite le
message standard inspect.
Les lignes 15 à 20 de notre méthode Calculatrice»openInWorld créent donc
cette instance de PluggableTextMorph et la garde temporairement dans la
variable view.
La ligne 21 de cette méthode ajoute l’instance nouvellement créée de la classe
Ecran aux dépendants de ce la calculatrice. Ainsi la calculatrice connaîtra, elle
aussi, son écran historique.
La ligne 22:
view setTextColor: Color bleu
spécifie que tout texte affiché dans le morph view, l’écran historique, doit être
écrit en caractères de couleur bleu.
Finalement, les lignes 23 à 25:
23 window
24 addMorph: view
25 frame: (4 / 5 @ (1 / 5) extent: 1 / 5 @ (4 / 5)).
|
|
ajoutent notre nouveau morph au morph principal de la calculatrice. Comme
tout à l’heure, les mesures pour la dimension de ce sous-morph, donc
pour son placement à l’intérieur du morph propriétaire, sont exprimées
en proportions des mesures du morph propriétaire. L’écran historique
occupe le dernier cinquième de la partie droite de la calculatrice, et ceci sur
toute la hauteur libre sous l’écran d’affichage des résultats. Une telle
calculatrice, composée juste des deux écrans, est montrée dans la figure
6.17.
| FIG. 6.17: | Notre
calculatrice au stade
actuel
du programme: un
écran d’affichage du
résultat en haut et
un écran pour
l’historique à droite |
|
|
L’ajout des touches numériques est réalisé dans les lignes 26 à 49 (montrées
ci-dessous) de notre méthode openInWorld. Nous devons créer neuf touches,
donc neuf instances de la classe PluggableButtonMorph, alignées en trois
lignes horizontales, chacune se composant de trois touches. Chacune de
ces lignes de touches est formée par un AlignmentMorph sur lequel trois
PluggableButtonMorph sont disposés. Chaque ligne de touches forme donc la
même structure que notre rangée de boutons de la section précédente – c’est bien
naturel: une touche de la calculatrice remplit un rôle sensiblement identique à un
bouton de notre téléviseur.
Dans la ligne 26 nous initialisons la variable temporaire l à la valeur 0. Elle
nous sert de compteur pour savoir à quel endroit une touche doit être
insérée sur son AlignmentMorph: en première, deuxième ou troisième
position.
La ligne 27 garde dans la variable temporaire couleur la couleur de fond du
fenêtre représentant notre calculatrice. Nous mettrons chacune de nos touches à
cette même couleur afin d’avoir une représentation unicolore des diverses touches
de notre calculatrice.
26 l := 0.
27 couleur := window color.
28 #(#(9 8 7) #(6 5 4) #(3 2 1) )
29 with: {a1. a2. a3}
30 do: [:ro :morph |
31 l := l + 1.
32 ro
33 do: [:co |
34 view := PluggableButtonMorph
35 on: (Touche new
36 label: co asString
37 fonction: #activeNumber:
38 on: self)
39 getState: #etat
40 action: #activer.
41 view := self
42 initalise: view
43 with: couleur
44 and: co.
45 morph addMorph: view].
46 window
47 addMorph: morph
48 frame: (0 @ (l * (1 / 5))
49 extent: 3 / 5 @ (1 / 5))].
|
|
L’initialisation de l’ensemble des touches se fait dans deux boucles imbriquées:
une boucle extérieure, formée par la transmission du message with:do: allant de
la ligne 28 à la ligne 49, et une boucle intérieure, formée par la transmission du
message do: et allant de la ligne 32 à la ligne 45.
Le receveur du message with:do: est un tableau composé des trois sous-tableaux
correspondant aux étiquettes des touches des trois lignes de touches à construire
(ligne 28). L’argument with: est le tableau des trois AlignmentMorph
représentant les trois lignes de touches (ligne 29).
La boucle intérieure parcourt les trois étiquettes de touches d’un sous-tableau (lignes
32 et 33) pour créer pour chacune d’elles une instance de PluggableButtonMorph
(ligne 34) associée à une nouvelle instance de la classe Touche (ligne 35) ayant
l’étiquette actuelle (ligne 36), la fonctionnalité activeNumber: (ligne 37) et elle
même associée à la calculatrice que nous visualisons avec cette méthode
openInWorld (ligne 38). Le nouveau PluggableButtonMorph trouve l’état
de son modèle, donc de sa touche, avec la méthode Touche»etat (ligne
39) et lancera, quand il est activé, la méthode Touche»activer (ligne
40).
Tout PluggableButtonMorph demande la connaissance de la méthode livrant
l’état du modèle, soit true, soit false – même s’il ne doit pas distinguer entre ces
deux états possibles. C’est pourquoi nous ajoutons à la classe Touche la méthode
etat que voici:
Clairement notre touche ne change jamais d’état; ce n’est pas nécessaire.
En conséquence: le PluggableButtonMorph associé à une touche affiche
toujours la couleur offColor, celle correspondant à l’état false de son
modèle.
Revenons vers notre méthode openInWorld: les lignes 41 à 44 contribuent à
l’initialisation du PluggableButtonMorph qui vient d’être créé. Voici la
méthode initialise:with:and: qui est en charge de ces initialisations
supplémentaires:
Calculatrice>>initalise: view with: couleur and: co
^ view offColor: couleur;
borderWidth: 1;
borderColor: Color black;
hResizing: #spaceFill;
vResizing: #spaceFill;
label: co asString;
useRoundedCorners;
yourself
|
|
| FIG. 6.18:
| Notre
calculatrice
au stade actuel
du programme:
deux écrans
et neuf touches
numériques |
|
|
Cette méthode initialise la couleur offColor du morph à la couleur de la fenêtre
calculatrice, la couleur de notre instance de SystemWindow, donne au morph une
bordure d’une largeur de un pixel (la transmission de borderWidth:) de couleur
noire (la transmission borderColor:), lui dit de prendre tout l’espace hoprizontal
et vertical disponible (les transmissions hResizing: et vResizing: avec
l’argument #spaceFill), donne une étiquette au morph (la transmission
de label:) et indique au morph de s’afficher comme un rectangle avec
des coins arrondis (la transmission useRoundedCorners). Cette méthode
retourne le PluggableButtonMorph ainsi initialisé (avec la transmission
yourself).
Après ces initialisations, le PluggableButtonMorph est ajouté (ligne 45) à
l’instance courante des trois AlignmentMorph disponibles.
Une fois la boucle intérieure terminée, nous avons crée trois vues de touches, la
boucle extérieure ajoute l’AlignmentMorph contenant ces trois vues à la fenêtre
calculatrice (lignes 46 à 49). La variable temporaire l, qui est incrémentée à
chaque itération, nous permet de connaître la position verticale, l
5, de ce
morph.
Comme on peut voir dans la figure 6.18, qui montre une vue de notre calculatrice
à l’instant actuel de sa programmation, elle commence à ressembler à une vraie
calculatrice. Les quelques touches qui y manquent encore seront construites
ci-dessous.
50 a1 := AlignmentMorph newRow.
51 view := PluggableButtonMorph
52 on: (Touche new
53 label: ’C’
54 fonction: #activeClear:
55 on: self)
56 getState: #etat
57 action: #activer.
58 view := self
59 initalise: view
60 with: couleur
61 and: ’C’.
62 a1 addMorphBack: view.
63 view := PluggableButtonMorph
64 on: (Touche new
65 label: ’0’
66 fonction: #activeNumber:
67 on: self)
68 getState: #etat
69 action: #activer.
70 view := self
71 initalise: view
72 with: couleur
73 and: ’0’.
74 a1 addMorphBack: view.
75 view := PluggableButtonMorph
76 on: (Touche new
77 label: ’=’
78 fonction: #activeEgal:
79 on: self)
80 getState: #etat
81 action: #activer.
|
|
82 view := self
83 initalise: view
84 with: couleur
85 and: ’=’.
86 a1 addMorphBack: view.
87 window
88 addMorph: a1
89 frame: (0 @ (4 / 5) extent: 3 / 5 @ (1 / 5)).
|
|
L’extrait de la méthode Calculatrice»openInWorld ci-dessus peut se lire comme
une succession de 3 blocs de 12 lignes. Chacun de ces blocs définit la vue d’une
touche particulière: les lignes 51 à 62 définissent la touche de réinitialisation C, les
lignes 63 à 74 la touche numérique 0 et les lignes 75 à 86 la touche d’entrée
=.
La première ligne, la ligne 50, crée une nouvelle instance de la classe
AlignmentMorph qui sert de conteneur pour ces trois touches. Les lignes 62, 74 et
86 ajoutent chacune des touches à ce morph. Nous utilisons ici le message
addMorphBack: qui est identique à addMorph:, à la différence près que le premier
ajoute le morph argument après les morphs déjà dans le morph conteneur
pendant que le deuxième l’ajoute avant les morphs déjà présents dans le morph
conteneur.
Les lignes 87 à 89 ajoutent le morph conteneur de ces trois touches,
l’AlignementMorph crée dans la ligne 50, à la vue de la calculatrice. La figure 6.19
montre la vue de la calculatrice au stade actuel du programme: il n’y manque
qu’une rangée verticale de touches d’opérateurs arithmétiques.
| FIG. 6.19: | Notre
calculatrice
au stade actuel du
programme: deux
écrans, dix touches
numériques et les
touches C et = |
|
|
Terminons alors la vue de notre calculatrice en ajoutant ces touches des
opérateurs arithmétiques. Elles seront alignées verticalement: nous utilisons donc
un AlignmentMorph à insertion verticale des sous-morphs. Ce qui se fait dans la
ligne 91 grâce à la méthode de création newColumn.
Ensuite, comme nous l’avons fait pour les touches numériques, nous allons
parcourir un tableau contenant les étiquettes de chacune des quatres touches
d’opérations (les touches +, -, * et /, ligne 91 ci-dessous) pour construire un
nouveau PluggableButtonMorph pour chacune d’elles. Si les instances de la classe
Touche correspondant aux touches numériques ont comme label: la valeur
numérique et comme fonction: le sélecteur activeNumber:, les instances des
opérateurs ont comme label: le symbol correspondant à leur étiquette et comme
fonction: le sélecteur activeFonction: (lignes 94 à 97). Pour le reste, la
définition des vues de ces touches d’opérations se fait exactement de la même
manière que la définition des autres touches.
La ligne 108, la dernière ligne de notre méthode openInWorld, lance l’affichage de
notre calculatrice avec la transmission openInWorld. Notez bien que cette
méthode openInWorld est transmise à une instance de la classe SystemWindow et
n’a donc rien à faire avec la méthode openInWorld de notre classe Calculatrice
que nous venons d’écrire.
90 a1 := AlignmentMorph newColumn.
91 #(’+’ ’-’ ’*’ ’/’ )
92 do: [:label |
93 view := PluggableButtonMorph
94 on: (Touche new
95 label: label asSymbol
96 fonction: #activeFonction:
97 on: self)
98 getState: #etat
99 action: #activer.
100 view := self
101 initalise: view
102 with: couleur
103 and: label.
104 a1 addMorph: view].
105 window
106 addMorph: a1
107 frame: (3 / 5 @ (1 / 5) extent: 1 / 5 @ (4 / 5)).
108 window openInWorld
|
|
Clairement notre méthode openInWorld est trop longue: 108 lignes, c’est
vraiment trop! Mon conseil pour le style d’écriture de programmes SMALLTALK
est qu’une méthode ne doit pas dépasser la taille d’un écran d’affichage standard.
Nous laissons la factorisation qui s’impose comme une exercice.