Obligement - L'Amiga au maximum

Samedi 27 avril 2024 - 03:40  

Translate

En De Nl Nl
Es Pt It Nl


Rubriques

Actualité (récente)
Actualité (archive)
Comparatifs
Dossiers
Entrevues
Matériel (tests)
Matériel (bidouilles)
Points de vue
En pratique
Programmation
Reportages
Quizz
Tests de jeux
Tests de logiciels
Tests de compilations
Trucs et astuces
Articles divers

Articles in english


Réseaux sociaux

Suivez-nous sur X




Liste des jeux Amiga

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z,
ALL


Trucs et astuces

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Glossaire

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Galeries

Menu des galeries

BD d'Amiga Spécial
Caricatures Dudai
Caricatures Jet d'ail
Diagrammes de Jay Miner
Images insolites
Fin de jeux (de A à E)
Fin de Jeux (de F à O)
Fin de jeux (de P à Z)
Galerie de Mike Dafunk
Logos d'Obligement
Pubs pour matériels
Systèmes d'exploitation
Trombinoscope Alchimie 7
Vidéos


Téléchargement

Documents
Jeux
Logiciels
Magazines
Divers


Liens

Associations
Jeux
Logiciels
Matériel
Magazines et médias
Pages personnelles
Réparateurs
Revendeurs
Scène démo
Sites de téléchargement
Divers


Partenaires

Annuaire Amiga

Amedia Computer

Relec


A Propos

A propos d'Obligement

A Propos


Contact

David Brunet

Courriel

 


Programmation : Introduction à la programmation en MUI sur MorphOS
(Article écrit par Grzegorz Kraszewski et extrait de MorphOS Library - janvier 2011)


Note : traduction par David Brunet.

1. Introduction

Magic User Interface (MUI en abrégé) est la boîte à outils de MorphOS permettant de créer des applications avec une interface utilisateur graphique. Elle fournit un large ensemble de gadgets (contrôles) ainsi qu'un cadre complet pour la conception de programmes pilotés par événements. MUI est orienté objet, mais ne repose pas sur un langage de programmation spécifique. BOOPSI (Basic Object Oriented System for Intuition) est utilisé comme base de la conception orientée objet de MUI.

MUI offre une mise en page dynamique comme mode de fonctionnement de base. Le placement des gadgets est déterminé par leur regroupement en groupes horizontaux, verticaux ou matriciels. Les coordonnées en pixels des gadgets s'adaptent dynamiquement aux préférences de l'utilisateur, comme la taille des polices, l'espacement entre les gadgets, les cadres des objets et les arrière-plans. Les deux captures d'écran ci-dessous montrent le même exemple d'application. La seule différence est que les paramètres MUI de l'utilisateur sont différents.

DigiRoller

Ces premiers réglages sont simples et épurés. On peut même les qualifier d'un peu vieille école, car ils utilisent des cadres vectoriels simples et des arrière-plans de couleur uniforme. Un simple habillage de fenêtre, nommé Mahalaxmi, convient parfaitement à ce style.

DigiRoller

Le deuxième exemple utilise certaines caractéristiques de MUI 4, comme les cadres avec image dotés de masques de transparence. Ce style sombre est obtenu en utilisant l'habillage de fenêtre Nox. Tous les calculs de position des gadgets sont faits automatiquement, ce qui permet d'avoir une police plus grande et des cadres fantaisistes. Le programmeur n'a pas à se soucier des goûts et des préférences de l'utilisateur (un bon programmeur testera cependant l'apparence du programme avec quelques paramètres différents).

2. Bref aperçu de BOOPSI

2.1 La programmation orientée objet

La programmation orientée objet est une technique développée en réponse à deux tendances du marché informatique. La première était la complexité croissante des logiciels. La gestion d'une base de code écrite de manière traditionnelle devient plus difficile lorsque la taille du code augmente. La deuxième tendance était la popularité croissante des interfaces graphiques, ce qui signifiait la fin de l'exécution séquentielle des programmes. Au lieu de cela, les programmes modernes sont pilotés par des événements, ce qui signifie que le flux d'exécution du code est déterminé par des événements externes (comme l'entrée de l'utilisateur) et n'est pas connu au moment de l'écriture du programme. La programmation orientée objet divise un programme en un ensemble d'objets interagissant les uns avec les autres à l'aide d'interfaces bien définies. Une telle modularisation simplifie la gestion d'un projet logiciel et s'inscrit naturellement dans le concept des interfaces utilisateur graphiques modernes. Les contrôles utilisateur (appelés "gadgets" dans MUI) ne sont que des objets dans le code et ils interagissent avec d'autres objets représentant des données utilisateur.

Cette brève introduction n'a pas pour but d'être un cours complet sur la programmation orientée objet. D'autre part, aucune connaissance d'un langage de programmation orienté objet particulier n'est nécessaire pour se familiariser avec BOOPSI. Habituellement, la gestion des techniques de POO (programmation orientée objet) est fournie par un langage de programmation, qui est soit conçu pour la POO (comme C++, C# ou Java) ou auquel la gestion de la POO est ajoutée de manière plus ou moins logique (Objective C, PHP). Ce n'est cependant pas le cas pour BOOPSI et MUI. Dans ce cas, la gestion de la programmation orientée objet provient du système d'exploitation. BOOPSI et MUI peuvent être utilisés avec n'importe quel langage de programmation, comme les langages traditionnels, par exemple le C et même l'assembleur.

Le module BOOPSI est situé dans la bibliothèque intuition.library, certaines fonctions importantes étant ajoutées à partir d'une "libabox" liée statiquement. Le but premier de sa conception était de construire un cadre pour encapsuler les éléments de l'interface graphique d'Intuition dans une interface orientée objet. Cette approche n'étant malheureusement pas assez flexible, MUI utilise uniquement le cadre de base BOOPSI. Ce cadre fournit les quatre concepts de base de la programmation orientée objet : classes, objets, méthodes et attributs. Il gère également l'héritage des classes. En raison de sa simplicité, BOOPSI est facile à comprendre et à utiliser, surtout si on le compare à des cadres de travail plus sophistiqués, comme celui du langage de programmation C++.

2.2 Classes

Une classe est le terme de base de la programmation orientée objet. C'est la description complète de ses objets, de leurs attributs et de leurs méthodes. Dans le cadre de BOOPSI, une classe consiste en :
  • Une structure IClass. Un pointeur vers cette structure est utilisé comme référence à la classe. La structure IClass est définie dans le fichier d'en-tête du système <intuition/classes.h>. Il existe également le type Class, qui est le même que "struct IClass".
  • Une fonction de distribution de classe. Lorsqu'une application appelle une méthode sur un objet, le répartiteur ("dispatcher") de classe de l'objet est appelé. Le répartiteur vérifie l'identifiant de la méthode et saute au code de cette méthode. Le répartiteur est généralement implémenté sous la forme d'une grande instruction "switch". Pour les classes simples, qui n'implémentent que quelques méthodes courtes, le code de ces méthodes est souvent placé à l'intérieur d'instructions de cas ("case"). Dans les classes plus importantes, le code des méthodes est séparé en fonctions placées à l'extérieur du répartiteur. Comme chaque méthode passe par un répartiteur, toutes les méthodes BOOPSI sont virtuelles au sens du C++. Pour cette raison, l'appel d'une méthode dans BOOPSI est généralement plus lent qu'en C++.
Une classe définit un ensemble de méthodes disponibles pour ses objets (instances) par l'ensemble des déclarations de cas dans le répartiteur. Les attributs des objets sont définis à l'aide de la méthode OM_SET() et sont récupérés à l'aide de la méthode OM_GET(). Les attributs peuvent également être passés directement au constructeur de l'objet. L'ensemble des attributs d'une classe et l'applicabilité des attributs sont alors définis par le code source des méthodes OM_NEW() (le constructeur), OM_SET() et OM_GET(). Il n'y a pas de déclaration formelle de classe. Il n'y a pas non plus de division entre les méthodes et attributs publics et privés. Une certaine forme de déclaration formelle et des niveaux d'accès peuvent être imposés en plaçant chaque classe dans un fichier de code source séparé. Un fichier d'en-tête d'accompagnement contiendrait les définitions des identificateurs de méthode, des identificateurs d'attribut et des structures de paramètres de méthode, mais uniquement ceux considérés comme "publics". Les identifiants de méthodes privées doivent être définis à l'intérieur du code source de la classe, afin qu'ils ne soient pas visibles en dehors du code source de la classe.

Une classe BOOPSI peut être partagée entre applications. Toutes les classes intégrées à MUI sont des classes partagées. BOOPSI maintient une liste des classes publiques à l'échelle du système (la liste peut être parcourue avec l'outil de surveillance Scout). Les classes partagées sont identifiées par des noms. Une partie des classes standard de MUI est contenue dans la bibliothèque principale de MUI - muimaster.library. La bibliothèque ajoute ces classes à la liste du système lorsqu'elle est ouverte pour la première fois. Le reste des classes standards MUI est stocké sur la partition du disque système dans le répertoire MOSSYS:Classes/MUI/. Des classes tierces supplémentaires peuvent être placées dans le répertoire SYS:Classes/MUI/.

Les classes partagées utilisent le cadre de la bibliothèque partagée de MorphOS, en d'autres termes une classe BOOPSI partagée est juste une sorte de bibliothèque partagée. La classe s'ajoute à la liste publique des classes, lorsqu'elle est ouverte depuis le disque. En tant que telle, une classe partagée BOOPSI doit être ouverte avec OpenLibrary() avant d'être utilisée (voir détails), d'autant plus que les classes BOOPSI ne sont généralement pas incluses dans la liste des bibliothèques ouvertes automatiquement. Ce n'est cependant pas le cas pour les classes MUI. Les classes partagées MUI peuvent être utilisées sans les ouvrir. C'est expliqué ci-dessous, dans la section "2.6 Extensions MUI à BOOPSI".

2.3 Méthodes et attributs

2.3.1 Méthodes

Les méthodes sont des actions qui peuvent être exécutées sur un objet. Un ensemble de méthodes disponibles est défini par la classe de l'objet. Techniquement parlant, une méthode est une fonction appelée avec un objet comme paramètre afin de changer l'état de l'objet. Dans BOOPSI, les méthodes sont appelées en utilisant l'appel DoMethod() de libabox :

result = DoMethod(object, method_id, ... /* method parameters */);
result = DoMethodA(object, method_struct);

La première forme d'appel, la plus populaire, construit simplement la structure de la méthode à la volée, à partir des arguments qui lui sont passés. Toute structure de méthode a toujours l'identifiant de la méthode comme premier champ. L'appel DoMethodA() obtient un pointeur vers la structure de la méthode, la structure est construite par l'application. La deuxième forme est rarement utilisée. Le nombre et la signification des paramètres, ainsi que la signification du résultat sont spécifiques à la méthode. La comparaison de l'exécution d'une méthode avec les deux formes d'appel est donnée ci-dessous :

struct MUIP_SomeMethod
{
  ULONG MethodID;
  LONG ParameterA;
  LONG ParameterB;
};

DoMethod(object, MUIM_SomeMethod, 3, 7);

struct MUIP_SomeMethod mparams = { MUIM_SomeMethod, 3, 7 };
DoMethodA(object, &mparams);

La forme DoMethod() est plus pratique, c'est pourquoi elle est couramment utilisée. MUI utilise des préfixes spécifiques pour toutes ses structures et constantes :
  • MUIM_ pour les identifiants de méthode.
  • MUIP_ pour les structures de paramètres de méthode.
  • MUIA_ pour les identificateurs d'attributs.
  • MUIV_ pour les valeurs spéciales et prédéfinies des attributs.
Les types C utilisés dans la structure de la méthode ci-dessus peuvent nécessiter quelques explications. "LONG" est un entier signé de 32 bits, "ULONG" est un entier non signé. Comme la structure est généralement construite sur la pile du processeur, tous les paramètres sont étendus et alignés sur 32 bits. Ensuite, chaque paramètre de la structure doit être défini soit comme un entier de 32 bits, soit comme un pointeur. Tout paramètre d'une taille supérieure à 32 bits doit être transmis par le biais d'un pointeur (par exemple, les flottants ou les chaînes de caractères en double précision).

2.3.2 Définition d'un attribut

Les attributs d'un objet représentent ses propriétés. Ils sont écrits et lus à l'aide de méthodes spéciales, OM_SET() et OM_GET() respectivement. Ceci diffère de la plupart des langages de programmation orientés objet, où les attributs (étant implémentés comme les champs d'un objet) sont accessibles directement. La manipulation des attributs dans BOOPSI est donc plus lente, car elle implique l'exécution d'une méthode.

La méthode OM_SET() ne prend pas un seul attribut et sa valeur, mais une liste de balises ("taglist") de ceux-ci, donc on peut définir plusieurs attributs à la fois. La mise en place de deux attributs à un objet peut être faite de la manière suivante :

struct TagItem attributes[] = {
  { MUIA_SomeAttr1, 756 },
  { MUIA_SomeAttr2, 926 },
  { TAG_END, 0 }
};

DoMethod(object, OM_SET, (ULONG)attributes);

Cependant, cette méthode est lourde et le code n'est pas facilement lisible. La bibliothèque intuition.library facilite les choses en fournissant la fonction SetAttrsA(), qui est un adaptateur pour la méthode OM_SET(). En utilisant cette fonction et le tableau défini ci-dessus, on peut écrire :

SetAttrsA(object, attributes);

Il faut toujours définir une liste de balises temporaire, mais la fonction possède également une forme variadique (ce qui signifie qu'elle peut prendre un nombre variable d'arguments) SetAttrs(), qui permet de construire la liste de balises à la volée :

SetAttrs(object,
  MUIA_SomeAttr1, 756,
  MUIA_SomeAttr2, 926,
TAG_END);

Mais ce n'est pas tout. Les programmeurs sont paresseux et ont décidé que dans le cas courant de la définition d'un seul attribut, SetAttrs() est encore trop lourd à taper. Une pratique courante trouvée dans les sources utilisant MUI était de définir une macro xset() ou set(), qui est maintenant définie dans les en-têtes du système, dans le fichier <libraries/mui.h>.

#define set(object, attribute, value) SetAttrs(object, attribute, value, TAG_END)

Ensuite, la définition d'un seul attribut peut être codée comme suit :

set(object, MUIA_SomeAttr1, 756);

La méthode OM_SET() renvoie le nombre d'attributs appliqués à l'objet. Si certains attributs ne sont pas connus de la classe (et des superclasses) de l'objet, ils ne sont pas comptés. Cette valeur de retour est généralement ignorée, elle peut être utilisée pour tester l'applicabilité d'un attribut.

MUI fournit quelques méthodes supplémentaires pour définir les attributs, à savoir MUIM_Set(), MUIM_NoNotifySet() et MUIM_MultiSet(). Elles sont principalement utilisées dans les notifications (voir chapitre "3.2 Notifications dans MUI").

2.3.3 Obtenir un attribut

La méthode OM_GET() permet de récupérer un seul attribut d'un objet. Il n'existe pas de méthode d'obtention d'attributs multiples. Son premier paramètre, évident, est l'identifiant de l'attribut. Cependant, la valeur de l'attribut n'est pas renvoyée comme résultat de la méthode. Au lieu de cela, le second paramètre est un pointeur vers une zone de mémoire, où la valeur doit être stockée. Cela permet de passer des attributs de plus de 32 bits, ils sont simplement copiés dans la zone de mémoire pointée. Cela ne fonctionne que pour les attributs de taille fixe. Les chaînes de texte ne peuvent pas être transmises de cette manière, elles sont donc transmises en tant que pointeurs (un pointeur vers la chaîne est stocké à un endroit de la mémoire indiqué par le second paramètre de OM_GET()). Les trois exemples ci-dessous illustrent les trois cas :

LONG value1;
QUAD value2;    /* entier 64 bits signé */
STRPTR *value3;

DoMethod(object, OM_GET, MUIA_Attribute1, (ULONG)&value1);  /* attribut entier */
DoMethod(object, OM_GET, MUIA_Attribute2, (ULONG)&value2);  /* attribut grosse taille fixe */
DoMethod(object, OM_GET, MUIA_Attribute3, (ULONG)&value3);  /* attribut de chaîne */

Dans les cas où un attribut est renvoyé par un pointeur, les données pointées doivent être traitées comme étant en lecture seule, sauf documentation contraire.

De la même manière que pour OM_SET(), il existe une fonction d'encapsulation ("wrapper") pour OM_GET() dans la bibliothèque intuition.library, nommée GetAttr(). Cette fonction change de manière inattendue l'ordre des arguments : l'identificateur d'attribut est le premier, le pointeur d'objet est le second. Les trois exemples ci-dessus peuvent être écrits avec GetAttr() comme suit :

GetAttr(MUIA_Attribute1, object, &value1);
GetAttr(MUIA_Attribute2, object, (ULONG*)&value2);
GetAttr(MUIA_Attribute3, object, (ULONG*)&value3);

Le troisième paramètre, un pointeur de stockage, est prototypée comme pointeur vers ULONG, donc dans le premier exemple le type "casting" n'est pas nécessaire.

Le fichier d'en-tête du système <libraries/mui.h> définit une macro get(), qui inverse l'ordre des deux premiers arguments de GetAttr() et ajoute le type "casting" à ULONG*. L'ordre des arguments de get() est le même que celui de set(), ce qui permet d'éviter les erreurs. La troisième ligne de l'exemple ci-dessus peut être réécrite avec get() de cette façon :

get(object, MUIA_Attribute3, &value3);

Les attributs les plus souvent utilisés sont les entiers (32 bits ou plus courts) et les chaînes de caractères. Les deux entrent dans une variable de 32 bits, alors que les chaînes de caractères doivent être transmises par des pointeurs. En tenant compte de cela, les programmeurs MUI ont inventé une fonction (parfois définie comme une macro), qui renvoie simplement la valeur de l'attribut au lieu de la stocker à une adresse spécifique. Cette fonction s'appelle xget() et fonctionne comme indiqué ci-dessous :

value1 = xget(object, MUIA_Attribute1);
/* MUIA_Attribute2 can't be retrieved with xget() */
value3 = (STRPTR)xget(object, MUIA_Attribute3);

La fonction xget() peut être définie de la manière suivante :

inline ULONG xget(Object *obj, ULONG attribute)
{
  ULONG value;

  GetAttr(attribute, object, &value);
  return value;
}

Cette fonction est très simple et se compile en quelques instructions de processeur. C'est pourquoi elle est déclarée comme "inline", ce qui amène le compilateur à insérer le code de la fonction en place au lieu de générer un saut. Cela rend le code plus rapide, bien qu'un peu plus gros. A part le fait de ne travailler qu'avec des attributs 32 bits, xget() a aussi l'inconvénient de perdre la valeur de retour OM_GET(). Cette valeur est booléenne et est TRUE (vraie) si la classe de l'objet (ou n'importe laquelle de ses superclasses) reconnaît l'attribut, FALSE (faux) sinon. Cette valeur est généralement ignorée, mais elle peut être utile pour rechercher les attributs pris en charge dans les objets.

La fonction xget() n'est pas définie dans les en-têtes du système. Elle a été décrite ici en raison de son utilisation courante dans les applications MUI. Ses équivalents pour des arguments de plus grande taille peuvent être définis si nécessaire.

2.4 Construction d'objets

Ayant une classe, le programmeur peut créer un nombre illimité d'objets (instances) de cette classe. Chaque objet possède sa propre zone de données d'instance, qui est allouée et effacée automatiquement par BOOPSI. Bien entendu, seules les données de l'objet sont allouées pour chaque instance. Le code n'est pas dupliqué, il doit donc être réentrant (les variables statiques et l'auto-modification du code ne doivent pas être utilisées).

Les objets sont créés et éliminés avec deux méthodes spéciales : le constructeur, OM_NEW() et le destructeur, OM_DISPOSE(). Bien sûr, la méthode du constructeur ne peut pas être appelée sur un objet, car elle en crée un nouveau. Elle a besoin d'un pointeur sur la classe de l'objet, et ne peut donc pas être invoquée avec DoMethod(). La bibliothèque intuition.library fournit les fonctions NewObjectA() et NewObject() pour appeler le constructeur. La différence entre elles est que NewObjectA() prend un pointeur vers une liste de balises spécifiant les valeurs initiales des objets. NewObject() permet au programmeur de construire cette liste de balises à partir d'un nombre variable d'arguments de fonction.

NewObject[A]() a deux façons alternatives de spécifier la classe de l'objet créé. Les classes privées sont spécifiées par des pointeurs de type Class. Les classes partagées sont spécifiées par le nom, qui est une chaîne de caractères à terminaison nulle. Si le pointeur est utilisé pour la spécification de la classe, le nom doit être NULL, si un nom est utilisé, le pointeur doit être NULL. Les quatre exemples ci-dessous montrent la création d'instances de classes privées et publiques avec NewObjectA() et NewObject() :

Object *obj;
Class *private;

struct TagItem initial = {
  { MUIA_Attribute1, 4 },
  { MUIA_Attribute2, 46 },
  { TAG_END, 0 }
};

Une classe privée, NewObjectA() :

obj = NewObjectA(private, NULL, initial);

Une classe privée, NewObject() :

obj = NewObject(private, NULL,
  MUIA_Attribute1, 4,
  MUIA_Attribute2, 46,
TAG_END);

Une classe publique, NewObjectA() :

obj = NewObjectA(NULL, "some.class", initial);

Une classe publique, NewObject() :

obj = NewObject(NULL, "some.class",
  MUIA_Attribute1, 4,
  MUIA_Attribute2, 46,
TAG_END);

NewObject[A]() retourne NULL en cas d'échec de la création d'un objet. Les raisons habituelles sont : pointeur/nom de classe erroné, manque de mémoire libre, valeurs initiales des attributs erronées ou manquantes. La valeur de retour de NewObject[A]() doit toujours être vérifiée dans le code.

2.5 Destruction d'objets

La méthode OM_DISPOSE() est utilisée pour détruire un objet. Contrairement à OM_NEW(), le destructeur peut être invoqué avec DoMethod() :

DoMethod(object, OM_DISPOSE);

La bibliothèque intuition.library possède un adaptateur pour cette méthode, nommé DisposeObject() :

DisposeObject(object);

2.6 Extensions MUI à BOOPSI

MUI ne se base pas seulement sur BOOPSI mais l'étend également. En plus de fournir un large ensemble de classes, MUI modifie aussi un peu le mode de fonctionnement de BOOPSI. Deux modifications sont abordées dans ce chapitre : l'extension de la structure IClass et les fonctions propres à MUI pour la construction et la destruction des objets.

MUI utilise la structure MUI_CustomClass pour sa représentation interne des classes. Cette structure contient la structure standard Class. Il est important lors de la création d'objets à partir de classes privées MUI avec NewObject(), que la structure Class soit extraite de la structure MUI_CustomClass :

struct MUI_CustomClass *priv_class;
Object *obj;

obj = NewObject(priv_class->mcc_Class, NULL, /* ... */ TAG_END);

La deuxième modification apportée par MUI à BOOPSI consiste à utiliser ses propres fonctions pour la construction et la destruction d'objets, respectivement MUI_NewObject[A]() et MUI_DisposeObject(). Ces deux fonctions ne sont utilisées que pour les objets des classes partagées (publiques) de MUI. Les objets des classes privées sont créés avec NewObject() comme indiqué ci-dessus. Le principal avantage de MUI_NewObject() est l'ouverture et la fermeture automatique des classes partagées sur disque. Voici un exemple :

Object *text;

text = MUI_NewObject(MUIC_Text, MUIA_Text_Contents, "foobar", TAG_END);

MUIC_Text est une macro définie dans <libraries/mui.h> et elle s'étend à la chaîne "Text.mui". Toutes les classes publiques MUI doivent être référencées par leurs macros MUIC_ plutôt que par des chaînes de caractères directes. Cela permet de détecter les noms de classes mal saisis, car une faute de frappe dans une macro sera détectée lors de la compilation. MUI vérifie si une classe nommée Text.mui a été ajoutée à la liste publique des classes. Si ce n'est pas le cas, la classe est trouvée sur le disque, ouverte et utilisée pour créer l'objet demandé. La fermeture de la classe lorsqu'elle n'est plus utilisée est également gérée automatiquement. Tous les objets MUI doivent être éliminés en utilisant MUI_DisposeObject(), qui prend l'objet à éliminer comme seul argument, de la même manière que DisposeObject().

MUI_DisposeObject(text);

3. Programmation événementielle, notifications

3.1 Programmation événementielle

La programmation événementielle est la conséquence naturelle de l'invention et du développement des interfaces graphiques utilisateur. La plupart des programmes traditionnels, en ligne de commande, fonctionnent comme un tuyau : les données sont chargées, traitées et enregistrées. Il n'y a pas ou peu d'interaction avec l'utilisateur (comme l'ajustement des paramètres de traitement ou le choix d'un autre chemin). Une interface graphique change tout cela. Un programme basé sur une interface graphique s'initialise, ouvre une fenêtre avec quelques icônes et gadgets, puis attend les actions de l'utilisateur. Des fragments du programme sont exécutés en réponse à l'entrée de l'utilisateur, et une fois l'action terminée, le programme retourne en attente. De cette façon, le déroulement du programme n'est pas déterminé par le code, mais plutôt par les événements d'entrée envoyés au programme par l'utilisateur via le système d'exploitation. C'est le paradigme de base de la programmation événementielle.

DigiRoller
Fig. 1. Flux d'exécution d'un programme orienté événement

3.2 Notifications dans MUI

Il existe deux approches du décodage des événements d'entrée dans un programme événementiel : centralisée et décentralisée. Le décodage centralisé est programmé sous la forme d'une grande instruction conditionnelle (généralement une instruction "switch", ou une longue cascade d'instructions "if") à l'intérieur de la boucle principale de l'organigramme de la figure 1. En fonction de l'événement décodé, les sous-routines effectuant les actions demandées sont appelées. Le décodage décentralisé des événements d'entrée est une idée plus moderne. Dans ce cas, la boîte à outils de l'interface graphique reçoit et traite en interne les événements d'entrée entrants et les associe aux changements d'attributs des objets de l'interface graphique (par exemple, un clic de souris sur un bouton change son attribut en "pressé"). Le programmeur d'une application peut alors attribuer des actions aux changements d'attributs des objets choisis. Cela se fait en créant des notifications sur les objets.

MUI utilise un décodage décentralisé des événements d'entrée. Tous les événements d'entrée sont mis en correspondance avec les changements d'attributs de différents objets. Il s'agit généralement de gadgets visibles de l'interface graphique (contrôles), mais certains événements peuvent être mis en correspondance avec les attributs d'un objet fenêtre ou d'un objet application (ce dernier n'a pas de représentation visible). Après avoir créé l'arbre d'objets complet, mais avant d'entrer dans la boucle principale, le programme met en place des notifications, en assignant des actions aux changements d'attributs. Les notifications peuvent également être créées et supprimées dynamiquement à tout moment.

Une notification relie deux objets entre eux. L'objet source déclenche l'action après le changement d'un de ses attributs. L'action (méthode) attribuée est alors exécutée sur l'objet cible. La notification est mise en place en appelant la méthode MUIM_Notify() sur l'objet source. Les arguments de la méthode peuvent être divisés en deux parties : la partie source et la partie cible. La forme générale de l'appel MUIM_Notify() est présentée ci-dessous :

DoMethod(source, MUIM_Notify, attribute, value, target, param_count, action, /* parameters */);

Les quatre premiers arguments forment la partie source, le reste est la partie cible. L'appel complet peut être "traduit" en langage humain de la manière suivante :
Lorsque l'objet source change son attribut en valeur,
exécuter la méthode d'action sur l'objet cible avec les paramètres.
Il y a un argument non expliqué avec la phrase ci-dessus, à savoir "param_count". Il s'agit simplement du nombre de paramètres suivant cet argument. Le nombre minimum de paramètres est 1 (l'identifiant de la méthode d'action), il n'y a pas de limite supérieure autre que l'utilisation du bon sens.

Une notification est déclenchée lorsqu'un attribut est défini à une valeur spécifiée. Il est souvent utile d'avoir une notification sur tout changement d'attribut. Une valeur spéciale MUIV_EveryTime doit être utilisée comme valeur de déclenchement dans ce cas.

L'action cible d'une notification peut être n'importe quelle méthode. Il existe quelques méthodes conçues spécifiquement pour être utilisées dans les notifications :

MUIM_Set() est une autre méthode pour définir un attribut. Elle est utilisée lorsqu'une notification doit définir un attribut sur l'objet cible. OM_SET() ne peut pas être utilisée dans les notifications car elle prend une liste de balises contenant les attributs à définir. Cette liste de balises ne peut pas être construite à partir des arguments et doit être définie séparément. MUIM_Set() définit un seul attribut à une valeur spécifiée. Ils sont passés à la méthode DoMethod directement comme deux arguments séparés. L'exemple ci-dessous ouvre une fenêtre lorsqu'un bouton est pressé :

DoMethod(button, MUIM_Notify, MUIA_Pressed, FALSE, window, 3, MUIM_Set, MUIA_Window_Open, TRUE);

Ceux qui ne sont pas familiers avec MUI peuvent se demander pourquoi la valeur de déclenchement est définie comme FALSE (faux). Cela est lié au comportement par défaut des gadgets de bouton. Le gadget se déclenche lorsque le bouton gauche de la souris est relâché, donc à la fin d'un clic. L'attribut MUIA_Pressed est défini à TRUE (vrai) lorsque le bouton de la souris est enfoncé, et défini à FALSE (faux) lorsque le bouton de la souris est relâché. Maintenant, il devrait être évident de comprendre pourquoi la notification est réglée pour se déclencher lorsque MUIA_Pressed prend la valeur FALSE (faux).

MUIM_NoNotifySet() fonctionne de la même manière que MUIM_Set() avec une exception importante. Elle ne déclenche aucune notification définie sur l'objet cible lorsque l'attribut a changé. Elle est souvent utilisée pour éviter les boucles de notification, non seulement dans la notification, mais aussi de manière autonome dans le code.

MUIM_MultiSet() permet de définir le même attribut à la même valeur pour plusieurs objets. Les objets sont spécifiés comme arguments de cette méthode et le dernier argument doit être NULL. Voici un exemple de désactivation de trois boutons après la désélection d'une coche :

DoMethod(checkmark, MUIM_Notify, MUIA_Selected, FALSE, application, 7, MUIM_MultiSet,
 MUIA_Disabled, TRUE, button1, button2, button3, NULL);

Ce qui est intéressant, c'est que si l'objet de notification cible n'est absolument pas pertinent ici, il doit néanmoins être un pointeur d'objet valide. L'objet application est généralement utilisé à cette fin, ou l'objet source de la notification.

MUIM_CallHook() appelle une fonction de rappel externe appelée "hook" (crochet). Elle est souvent utilisée de manière abusive par les programmeurs qui sont réticents à effectuer une sous-classification des classes standards, et qui préfèrent implémenter les fonctionnalités du programme sous forme de nouvelles méthodes. L'appel d'une méthode à partir d'une notification est généralement plus rapide et plus facile (cependant, un "hook" nécessite la définition de structures supplémentaires).

MUIM_Application_ReturnID() renvoie un nombre entier de 32 bits à la boucle principale d'un programme MUI. Avec cette méthode, le traitement décentralisé des événements d'entrée de MUI peut être transformé en un traitement centralisé. Les débutants en programmation MUI ont tendance à abuser de cette méthode et à rediriger toute la gestion des événements vers la boucle principale, en y plaçant une grosse instruction "switch". Bien qu'assez simple, cette technique de programmation doit être évitée au profit de l'implémentation des fonctionnalités du programme dans des méthodes. L'ajout de code à l'intérieur de la boucle principale dégrade la réactivité de l'interface graphique. La seule utilisation légitime de MUIM_Application_ReturnID() est de retourner une valeur spéciale MUIV_Application_ReturnID_Quit utilisée pour terminer le programme.

3.3 Réutilisation de la valeur de déclenchement

Lorsque l'action d'une notification consiste à définir un attribut dans l'objet cible, il est souvent souhaitable de transmettre la valeur de déclenchement à l'objet cible. C'est très facile, lorsque la notification est configurée pour se produire sur une valeur particulière. Les choses changent cependant, lorsque la notification est définie pour se produire sur n'importe quelle valeur avec MUIV_EveryTime. Une valeur spéciale MUIV_TriggerValue peut être utilisée pour cela. Elle est remplacée par la valeur réelle de l'attribut de déclenchement à chaque déclenchement. Une autre valeur spéciale, MUIV_NotTriggerValue, est utilisée pour les attributs booléens et est remplacée par la négation logique de la valeur actuelle de l'attribut déclencheur.

Le premier exemple utilise MUIV_Trigger_Value, pour afficher la valeur d'une glissière dans un gadget de chaînes ("string") :

DoMethod(slider, MUIM_Notify, MUIA_Numeric_Value, MUIV_EveryTime, string, 3,
 MUIM_Set, MUIA_String_Integer, MUIV_TriggerValue);

Le deuxième exemple relie une coche à un bouton. Lorsque la coche est sélectionnée, le bouton est activé. La désélection de la coche désactive le bouton :

DoMethod(checkmark, MUIM_Notify, MUIA_Selected, MUIV_EveryTime, button, 3,
 MUIM_Set, MUIA_Disabled, MUIV_NotTriggerValue);

MUIV_EveryTime, MUIV_TriggerValue et MUIV_NotTriggerValue sont définis comme des valeurs particulières dans la plage 32 bits. De ce fait, il est impossible de définir une notification sur la valeur 1 233 727 793 (qui est MUIV_EveryTime). Il est également impossible de définir la valeur sur un nombre fixe 1 233 727 793 (MUIV_TriggerValue) ou 1 233 727 795 (MUIV_NotTriggerValue) en utilisant MUIM_Set() dans une notification.

3.4 Boucles de notification

Il peut y avoir des centaines de notifications définies dans un programme complexe. La modification d'un attribut peut déclencher une cascade de notifications. Il est possible que la cascade contienne des boucles. L'exemple le plus simple d'une boucle de notification est une paire d'objets ayant des notifications l'un sur l'autre. Supposons qu'il y ait deux glissières qui doivent être couplés ensemble. Cela signifie que le déplacement d'une glissière doit également déplacer l'autre. Un ensemble de deux notifications peut garantir ce comportement.

DoMethod(slider1, MUIM_Notify, MUIA_Numeric_Value, MUIV_EveryTime, slider2, 3,
 MUIM_Set, MUIA_Numeric_Value, MUIV_Trigger_Value); 
DoMethod(slider2, MUIM_Notify, MUIA_Numeric_Value, MUIV_EveryTime, slider1, 3,
 MUIM_Set, MUIA_Numeric_Value, MUIV_Trigger_Value); 

Lorsque l'utilisateur déplace le slider1 à la valeur 13, la première notification se déclenche et fixe la valeur du slider2 à 13. Cela déclenche la deuxième notification. Son action consiste à régler la valeur du slider1 sur 13, ce qui déclenche à nouveau la première notification. Puis la boucle se déclenche sans fin... Ou plutôt elle le ferait, si MUI n'avait pas de mesures anti-boucles. La solution est très simple : si un attribut est défini pour un objet à la même valeur que l'attribut courant, toutes les notifications sur cet attribut dans cet objet sont ignorées. Dans notre exemple, la boucle sera interrompue après que la deuxième notification ait fixé la valeur du slider1.

3.5 La boucle principale idéale d'un programme MUI

La boucle principale idéale d'un programme MUI ne doit contenir pratiquement aucun code. Toutes les actions doivent être gérées par des notifications. Voici le code de la boucle :

ULONG signals = 0;

while (DoMethod(application, MUIM_Application_NewInput, (ULONG)&signals)
 != (ULONG)MUIV_Application_ReturnID_Quit)
{
  signals = Wait(signals | SIGBREAKF_CTRL_C);
  if (signals & SIGBREAKF_CTRL_C) break;
}

La variable "signals" contient un masque de bits des signaux d'événements d'entrée envoyés au processus par le système d'exploitation. Sa valeur initiale 0 signifie simplement "aucun signal". Lorsque la méthode MUIM_Application_NewInput() est exécutée sur l'objet application, MUI définit les bits de signaux pour les événements d'entrée qu'il attend dans la variable signals. Ces signaux sont généralement des signaux des ports de messages des fenêtres d'application, où Intuition envoie les événements d'entrée. Ensuite, l'application appelle la fonction Wait() de la bibliothèque Exec. Dans cette fonction, l'exécution du processus est arrêtée. Le planificateur de processus ne donnera aucun temps processeur au processus jusqu'à ce qu'un des signaux du masque arrive. En dehors des signaux d'événements d'entrée définis par MUI, seul le signal Ctrl-C du système est ajouté au masque. Toute application MorphOS bien écrite devrait pouvoir être interrompue par l'envoi de ce signal. Il peut être envoyé via la console en appuyant sur les touches Ctrl + C, ou depuis des outils comme le TaskManager. Quand l'un des signaux du masque arrive au processus, le programme revient de Wait(). Si le signal Ctrl-C est détecté, la boucle principale se termine. Sinon, MUI décode les événements d'entrée reçus en fonction de son masque de signaux, traduit les événements en changements d'attributs d'objets pertinents et effectue les notifications déclenchées. Tout cela se passe dans la méthode MUIM_Application_NewInput(). Enfin, le masque de signal est mis à jour. Si une notification appelle la méthode MUIM_Application_ReturnID(), l'identifiant transmis est renvoyé comme résultat de MUIM_Application_NewInput(). En cas de réception de la valeur spéciale MUIV_Application_ReturnID_Quit, la boucle se termine.

Tout code supplémentaire inséré dans la boucle introduira un retard dans la manipulation et le redessinage de l'interface graphique. Si un programme effectue certains calculs intensifs pour le processeur, la meilleure façon de les traiter est de les déléguer dans un sous-processus. Si l'on charge le processus principal de tâches de calcul, le programme peut être perçu comme lent et peu réactif aux actions de l'utilisateur.

4. "hello world!" en MUI

Hello World Voici la fenêtre de l'application MUI d'exemple. Elle est très simple, elle ne contient qu'un objet texte statique et aucun gadget, à l'exception des gadgets de bordure de fenêtre. Notez que le nombre de gadgets sur le côté droit de la fenêtre dépend des paramètres MUI de l'utilisateur. Le code source de l'application est également très simple et tient en 60 lignes, en comptant l'espacement vertical approprié. Il est découpé en morceaux dans l'article pour une meilleure lecture. Une version complète, prête à compiler, est également disponible.

#include <proto/muimaster.h>
#include <,proto/intuition.h>

Les fichiers d'en-tête pour muimaster.library et intuition.library sont inclus. Notez que ces bibliothèques seront ouvertes et fermées automatiquement.

Object *App, *Win;

Pointeurs globaux pour l'objet application et l'objet fenêtre. Bien que l'utilisation de nombreuses variables globales soit considérée comme inélégante, avoir des globales pour les objets les plus importants est pratique, surtout dans un si petit projet.

Object* build_gui(void)
{
  App = MUI_NewObject(MUIC_Application,
    MUIA_Application_Author, (ULONG)"Grzegorz Kraszewski",
    MUIA_Application_Base, (ULONG)"HELLOWORLD",
    MUIA_Application_Copyright, (ULONG)"© 2010 Grzegorz Kraszewski",
    MUIA_Application_Description, (ULONG)"Hello World in MUI.",
    MUIA_Application_Title, (ULONG)"Hello World",
    MUIA_Application_Version, (ULONG)"$VER: HelloWorld 1.0 (16.11.2010)",
    MUIA_Application_Window, (ULONG)(Win = MUI_NewObject(MUIC_Window,
      MUIA_Window_Title, (ULONG)"Hello World",
      MUIA_Window_RootObject, MUI_NewObject(MUIC_Group,
        MUIA_Group_Child, MUI_NewObject(MUIC_Text,
          MUIA_Text_Contents, (ULONG)"Hello world!",
        TAG_END),
      TAG_END),
    TAG_END)),
  TAG_END);
}

La fonction ci-dessus crée l'arbre d'objets complet pour HelloWorld. L'objet maître est l'instance de la classe Application. Il possède un objet Window. La racine de la fenêtre est un objet Group contenant un objet Text. L'objet application possède six attributs fonctionnant comme des descripteurs utilisés à différents endroits du système. Ils ne sont pas nécessaires à l'exécution du programme, mais aident à l'intégrer au système. La signification de ces attributs est expliquée dans l'autodoc de la classe Application dans le SDK. Les autres attributs s'expliquent d'eux-mêmes, veuillez vous référer aux autodocs des classes MUI respectives pour plus de détails.

La fonction illustre une manière typique de créer une interface MUI. L'arbre d'objet complet est créé dans un grand appel MUI_NewObject() contenant des sous-appels imbriqués. L'ordre d'exécution du code est différent de l'ordre de lecture. Les objets les plus imbriqués sont créés en premier et passés aux constructeurs de leurs parents. L'objet d'application est créé en dernier. Cette façon de créer l'application assure également une gestion automatique des erreurs. Si l'un des constructeurs échoue, il passe NULL à un constructeur parent, ce qui le fait échouer également, puis dispose de tous les objets enfants construits avec succès. Enfin, le constructeur de l'application échoue et renvoie NULL. Dans ce cas, seuls deux états de l'application sont possibles : soit l'application est entièrement construite, soit elle n'est pas construite du tout. Ce comportement simplifie grandement la gestion des erreurs.

void notifications(void)
{
  DoMethod(Win, MUIM_Notify, MUIA_Window_CloseRequest, TRUE, App, 2,
   MUIM_Application_ReturnID, MUIV_Application_ReturnID_Quit);
}

L'étape suivante consiste à faire des notifications. Ce programme simple ne contient qu'une seule notification, qui met fin au programme après le clic sur le gadget de fermeture de la fenêtre. MUI associe un clic du bouton gauche de la souris sur le gadget à un changement de l'attribut MUIA_Window_CloseRequest. La cible de la notification est l'application, la méthode MUIM_Application_ReturnID() transmet ensuite l'identifiant de l'action de fermeture à la boucle principale.

void main_loop(void)
{
  ULONG signals = 0;

  set(Win, MUIA_Window_Open, TRUE);
			 
  while (DoMethod(App, MUIM_Application_NewInput, &signals) != MUIV_Application_ReturnID_Quit)
  {
    signals = Wait(signals | SIGBREAKF_CTRL_C);
    if (signals & SIGBREAKF_CTRL_C) break;
  }

  set(Win, MUIA_Window_Open, FALSE);
}

La boucle principale standard a été abordée dans le chapitre 3 précédent. Le seul ajout consiste à ouvrir la fenêtre avant la boucle et à la fermer après.

int main(void)
{
  App = build_gui();

  if (App)
  {
    notifications();
    main_loop();
    MUI_DisposeObject(App);
  }

  return 0;
}

Enfin, la fonction principale du programme. Elle construit d'abord l'arbre d'objets et vérifie si elle a réussi. En cas d'échec, le programme se termine immédiatement. Après la création de tous les objets, les notifications sont ajoutées et le programme entre dans la boucle principale. Lorsque la boucle se termine, l'objet d'application est éliminé. Il dispose également de tous ses sous-objets.

5. Règles générales et objectif du sous-classage

5.1 Introduction

Le sous-classage est l'une des techniques essentielles de la programmation orientée objet. Dans MUI, le sous-classage est utilisé pour les raisons suivantes :
  • Implémentation de la fonctionnalité du programme comme un ensemble de méthodes. La classe Application est généralement utilisée à cette fin.
  • Personnaliser les classes en écrivant des méthodes laissées intentionnellement sans implémentation (ou ayant des implémentations par défaut) dans les classes MUI standards. L'exemple le plus courant est la classe List, mais aussi la classe Numeric et d'autres.
  • Créer des gadgets ou des zones dessinés personnalisés. La classe Area est sous-classée dans ce cas.
Quelle que soit la raison du sous-classage, il se fait toujours de la même manière. Le programmeur doit écrire de nouvelles méthodes ou surcharger des méthodes existantes, créer une fonction de distribution, définir une structure de données d'instance (vide dans certains cas), puis créer la classe. Il convient de noter que le sous-classage des classes MUI se fait de la même manière que le sous-classage des classes BOOPSI. La seule différence est que MUI fournit ses propres fonctions pour la création et la disposition des classes.

Note : "surcharger", dans le jargon MUI, signifie que l'on va spécialiser la classe pour faire quelque chose de différent de la classe de base.

5.2 Données d'objet

Les données des objets sont stockées dans une zone de mémoire allouée automatiquement pour chaque objet créé. La zone de données de l'objet est utilisée pour stocker les valeurs des attributs et pour les variables internes et les tampons. Cette zone est généralement définie comme une structure. La taille de la zone est transmise à la fonction MUI_CreateCustomClass(). Dans une hiérarchie de classes, chaque classe peut ajouter sa propre contribution à la zone de données de l'objet. Contrairement à C++, une classe n'a pas d'accès direct à autre chose que sa propre zone de données. Elle ne peut pas accéder aux données définies dans la superclasse (en C++, il est possible qu'un champ soit déclaré comme protégé ou public). Les données des objets définis dans l'une des superclasses ne peuvent être accédées qu'en utilisant des méthodes ou des attributs fournis par ces superclasses.

En raison de la conception interne de BOOPSI, la taille de la zone d'instance des données est limitée à 64 ko. Les tampons de grande taille doivent être alloués dynamiquement dans OM_NEW() et libérés dans OM_DISPOSE(). La zone de données est toujours réinitialisée avec uniquement des zéros à la création de l'objet. Si une classe n'a pas besoin de données d'instance d'objet, elle peut passer 0 comme taille de zone à MUI_CreateCustomClass().

5.3 Écriture des méthodes

Une méthode MUI est juste une fonction C ordinaire, mais avec un prototype partiellement fixé.

IPTR MethodName(Class *cl, Object *obj, MessageType *msg);

La valeur de retour de la méthode peut être un entier ou un pointeur vers n'importe quoi. C'est pourquoi elle utilise le type IPTR qui signifie "entier assez grand pour contenir un pointeur". Dans la version actuelle de MorphOS, c'est juste un entier de 32 bits (le même que LONG). Si une méthode n'a pas de valeur significative à retourner, elle peut simplement retourner 0. Les deux premiers arguments fixes sont le pointeur vers la classe et le pointeur vers l'objet. Le dernier est un message de méthode. Lorsqu'une méthode est surchargée, le type de message est déterminé par la superclasse. Pour une nouvelle méthode, le type de message est défini par le programmeur. Certaines méthodes peuvent avoir des messages vides (contenant seulement un identifiant de méthode), dans ce cas le troisième argument peut être omis.

La plupart des méthodes doivent accéder aux données de l'instance de l'objet. Pour obtenir un pointeur vers la zone de données, on utilise la macro INST_DATA, définie dans <intuition/classes.h>. Un exemple ci-dessous montre l'utilisation de la macro :

struct ObjData
{
  LONG SomeVal;
  /* ... */
};

IPTR SomeMethod(Class *cl, Object *obj)
{
  struct ObjData *d = (struct ObjData*)INST_DATA(cl, obj);

  d->SomeVal = 14;
  /* ... */
  return 0;
}

Si une méthode est une méthode redondante d'une superclasse, elle peut vouloir exécuter la méthode de la superclasse. Il n'y a pas d'appel implicite de super méthode dans MUI. La méthode de la superclasse doit toujours être appelée explicitement avec l'appel DoSuperMethodA() :

result = DoSuperMethodA(cl, obj, msg);
result = DoSuperMethod(cl, obj, MethodID, ...);

La deuxième forme reconstruit le message de la méthode à partir des arguments variables, et est utilisée lorsque le message est modifié avant l'appel de la méthode de la superclasse. La super méthode peut être appelée à n'importe quel endroit de la méthode, ou peut ne pas être appelée du tout. Pour les classes et méthodes standards MUI, les règles d'appel des super méthodes sont décrites dans la documentation et seront abordées plus loin dans ce tutoriel. Pour les méthodes personnalisées, la question de l'appel d'une super méthode est du ressort du programmeur de l'application.

5.4 Le répartiteur

Une fonction de répartiteur ("dispatcher") est une sorte de table de saut pour les méthodes. Quand une méthode est appelée sur un objet (avec DoMethod()), BOOPSI trouve le répartiteur de la classe de l'objet et l'appelle. Le répartiteur vérifie un identifiant de méthode, qui est toujours le premier champ de tout message de méthode. En fonction de l'identifiant, une méthode est appelée. Si une méthode est inconnue de la classe, le répartiteur doit la transmettre à la superclasse avec l'appel DoSuperMethod().

Le répartiteur est une sorte de fonction d'accrochage ("hook"). Il rend sa convention d'appel indépendante du langage de programmation. L'inconvénient est que les arguments du répartiteur sont passés dans des registres virtuels du processeur 68k. Cet inconvénient permet de gérer les anciens logiciels 68k et permet également aux classes natives PowerPC d'être utilisées par les applications 68k et aux anciennes classes 68k d'être utilisées par les applications natives. Étant un "hook", un répartiteur a besoin qu'une structure EmulLibEntry soit créée et remplie au préalable. La structure est définie dans <emul/emulinterface.h> et agit comme une porte de données entre le code natif PowerPC et l'émulateur 68k.

const struct EmulLibEntry ClassGate = {TRAP_LIB, 0, (void(*)(void))ClassDispatcher};

Ensuite, le répartiteur est défini comme suit :

IPTR ClassDispatcher(void)
{
  Class *cl = (Class*)REG_A0;
  Object *obj = (Object*)REG_A2;
  Msg msg = (Msg)REG_A1;

  /* ... */

Les arguments du répartiteur sont les mêmes que ceux d'une méthode. Ils sont passés dans les registres d'adresse virtuels A0, A1 et A2 du processeur 680x0 au lieu d'être de simples arguments. La porte de données du répartiteur est passée comme argument à MUI_CreateCustomClass(). La porte de données est utilisée même lorsqu'une application native appelle un répartiteur natif. Elle introduit une certaine surcharge, mais elle est négligeable. De nombreux programmeurs préfèrent cacher ces détails derrière un ensemble de macros de préprocesseur, de telles macros n'ont cependant pas été utilisées ici, pour une meilleure compréhension.

Le type "Msg" est un type racine pour tous les messages de méthode. Il définit une structure contenant uniquement le champ de l'identifiant de la méthode (défini comme ULONG). Tous les paramètres suivants doivent garder la pile du processeur alignée, car DoMethod() compile le message sur la pile. Il faut que chaque paramètre soit défini soit comme un IPTR, soit comme un pointeur.

Après avoir reçu les arguments, le répartiteur vérifie l'identifiant de la méthode dans le message et saute à la méthode correspondante. Il est généralement implémenté sous la forme d'une instruction "switch". Si seules quelques méthodes sont implémentées, il peut également s'agir d'une cascade "if/if else". Voici un exemple typique :

switch (msg->MethodID)
{
  case OM_NEW:            return MyClassNew(cl, obj, (struct opSet*)msg);
  case OM_DISPOSE:        return MyClassDispose(cl, obj, msg);
  case OM_SET:            return MyClassSet(cl, obj, (struct opSet*)msg);
  case OM_GET:            return MyClassGet(cl, obj, (struct opGet*)msg);
  case MUIM_Draw:         return MyClassDraw(cl, obj, (struct MUIP_Draw*)msg);
  case MUIM_AskMinMax:    return MyClassAskMinMax(cl, obj, (struct MUIP_AskMinMax*)msg);
  /* ... */
  default:                return DoSuperMethodA(cl, obj, msg);
}

Pour chaque méthode, un pointeur de message est attribué à une structure de message de cette méthode particulière. Certains programmeurs placent le code de la méthode directement dans l'instruction "switch", surtout si les méthodes sont courtes et peu nombreuses. Dans l'exemple ci-dessus, certaines méthodes de la classe Area sont surchargées. Le schéma de nommage utilisé pour les fonctions de méthode n'est qu'un exemple, il n'y a aucune contrainte à ce sujet. Bien que le fait de préfixer les noms des fonctions de méthode par un nom de classe présente l'avantage d'éviter les conflits de noms entre les classes personnalisées si les fonctions de méthode ne sont pas déclarées comme statiques.

5.5 Création de classes

Une fois tous les composants réalisés (méthodes, répartiteur, porte, structure de données objet), on peut créer une classe MUI.

struct MUI_CustomClass *MyClass;

MyClass = MUI_CreateCustomClass(NULL, MUIC_Area, NULL, sizeof(struct MyClassData),
 (APTR)&MyClassGate);

Le premier argument est une base de bibliothèque si la classe créée est publique. L'écriture de classes publiques MUI sera abordée plus tard. Disons pour l'instant que les classes publiques sont implémentées comme des bibliothèques partagées, donc une telle classe publique a une base de bibliothèque. Pour les classes privées, cet argument doit toujours être NULL.

Les deux arguments suivants sont utilisés alternativement et spécifient la superclasse. La superclasse peut être soit privée (référencée par un pointeur) soit publique (référencée par un nom). Les classes publiques sont généralement sous-classées, donc le pointeur est mis à NULL comme dans l'exemple. Les projets plus complexes peuvent utiliser le sous-classage à plusieurs niveaux et sous-classer leurs propres classes privées. Dans ce cas, un pointeur vers une classe privée est passé comme premier argument et le second est NULL.

Le quatrième argument définit la taille de la zone de données de l'objet en octets. Dans la plupart des cas, la zone de données de l'objet est définie comme une structure, donc l'utilisation de l'opérateur sizeof() est le moyen évident de déterminer la taille. Si la classe n'a pas besoin de données par objet, zéro peut être indiqué ici.

Le dernier argument est une adresse de la porte de données (structure EmulLibEntry). Les programmeurs expérimentés en programmation 68k peuvent remarquer qu'il y a une différence - dans le code 68k, seule l'adresse de la fonction de répartiteur ("dispatcher") est utilisée ici. Comme mentionné ci-dessus, la prise en charge transparente du code 68k exige que l'exécution du programme passe par la porte de données lorsqu'il passe du code système au répartiteur. C'est pourquoi l'adresse de la porte de données est placée comme cet argument, alors la porte de données contient une adresse réelle du répartiteur.

5.6 Disposition des classes

Une classe MUI est éliminée par un appel à MUI_DeleteCustomClass().

if (MyClass) MUI_DeleteCustomClass(MyClass);

Certaines conditions doivent être remplies avant d'appeler cette fonction.
  • Ne l'appelez que si la classe a été créée avec succès. L'appeler avec un pointeur de classe NULL est mortel (d'où la vérification de NULL dans l'exemple).
  • Ne supprimez pas une classe s'il lui reste des sous-classes ou des objets. La meilleure façon est de créer toutes les classes avant de créer l'interface graphique de l'application et de les supprimer après la dernière MUI_DisposeObject() de l'objet principal de l'application. Les classes doivent être supprimées dans l'ordre inverse de leur création. MUI_DeleteCustomClass() renvoie une valeur booléenne. Elle est FALSE (faux) lorsqu'une classe ne peut être supprimée en raison de sous-classes ou d'objets potentiellement orphelins.
6. Méthodes générales de surcharge

6.1 Surcharge des constructeurs

6.1.1 Objets sans objets enfants

Un constructeur d'objet (méthode OM_NEW()) prend la même structure de message opSet que la méthode OM_SET(). Le message contient le champ ops_AttrList, qui est un pointeur vers une liste de balises contenant les attributs de l'objet initial. L'implémentation d'un constructeur pour un objet sans objets enfants est simple. Le constructeur de la superclasse est appelé en premier, puis, s'il réussit, le constructeur initialise les données de l'instance de l'objet, alloue les ressources nécessaires et définit les valeurs initiales des attributs à partir des balises transmises via ops_AttrList.

Une règle de base lors de la surcharge des constructeurs est de ne jamais laisser un objet à moitié construit. Le constructeur doit soit retourner un objet entièrement construit, soit échouer complètement, libérant toutes les ressources obtenues avec succès. Ceci est important si l'objet obtient plus d'une ressource et que l'une des allocations de ressources a échoué (par exemple l'allocation d'un gros morceau de mémoire ou l'ouverture d'un fichier). Un exemple d'implémentation ci-dessous obtient trois ressources : A, B et C :

IPTR MyClassNew(Class *cl, Object *obj, struct opSet *msg)
{  
  if (obj = DoSuperMethodA(cl, obj, (Msg)msg))
  {
    struct MyClassData *d = (struct MyClassData*)INST_DATA(cl, obj);

    if ((d->ResourceA = ObtainResourceA()
     && (d->ResourceB = ObtainResourceB()
     && (d->ResourceC = ObtainResourceC())
    {
      return (IPTR)obj;    /* success */
    }
    else CoerceMethod(cl, obj, OM_DISPOSE);
  }
  return NULL;
}

Si le destructeur de l'objet libère les ressources A, B et C (ce qui serait logique vu que le constructeur les alloue), le travail de nettoyage peut être délégué au destructeur. Il faut cependant que le destructeur soit préparé à la destruction d'un objet non entièrement construit. Il ne peut pas supposer que les trois ressources ont été allouées, il doit donc vérifier chaque pointeur de ressource par rapport à NULL avant d'appeler une fonction de libération. Le destructeur se charge également d'appeler le destructeur de la superclasse lorsque les ressources sont libérées. Voir chapitre "6.2 Surcharge de destructeurs" pour des exemples de destructeurs et des explications.

La seule question qui reste est de savoir ce que fait CoerceMethod() et pourquoi il est utilisé à la place d'un simple DoMethod(). L'appel CoerceMethod() fonctionne exactement de la même manière que DoMethod(), mais effectue la coercition de méthode par un appel forcé au répartiteur de la classe spécifiée comme premier argument au lieu du répartiteur de la vraie classe de l'objet. Cela fait une différence, lorsque la classe en question est ensuite sous-classée. L'organigramme ci-dessous explique le problème :

MUI coerce method

La classe B sur le diagramme est une sous-classe de la classe A et de même, la classe C est une sous-classe de B. Supposons qu'un objet de la classe C soit en cours de construction. Comme chaque constructeur appelle d'abord la superclasse, l'appel va d'abord à la classe racine ("rootclass", la racine de toutes les classes BOOPSI). Ensuite, en descendant l'arbre des classes, chaque constructeur de classe alloue ses ressources. Malheureusement le constructeur de la classe A n'a pas pu allouer une de ses ressources et a décidé d'échouer. S'il avait juste appelé DoMethod(obj, OM_DISPOSE), il aurait inutilement exécuté les destructeurs des classes B et C, alors que les constructeurs de ces classes n'ont pas encore été entièrement exécutés. Même si ces destructeurs peuvent faire face à cela, leur appel est superflu. Avec la méthode CoerceMethod() le destructeur de la classe A est appelé directement. Ensuite, le constructeur de la classe A renvoie NULL, ce qui fait que les constructeurs des classes B et C échouent immédiatement sans tentative d'allocation de ressources.

6.1.2 Objets avec des objets enfants

Tout en conservant les mêmes principes, le constructeur d'un objet avec des sous-objets est conçu un peu différemment. Les classes les plus couramment sous-classées pouvant avoir des objets enfants sont Application et Group. La classe Window est également souvent sous-classée de manière similaire. Alors qu'un objet Window ne peut avoir qu'un seul enfant, spécifié par MUIA_Window_RootObject, cet enfant a souvent plusieurs sous-objets. Le constructeur doit d'abord créer ses objets enfants, puis les insérer dans la liste des balises ops_AttrList et appeler le constructeur de la superclasse. S'il réussit, des ressources peuvent être allouées si nécessaire. Comme n'importe laquelle des trois étapes du constructeur peut échouer, la gestion correcte des erreurs devient compliquée. De plus, l'insertion d'objets créés dans la liste des balises comme valeurs de balises enfants (comme MUIA_Group_Child) est fastidieuse. Heureusement, on peut utiliser la fonction DoSuperNew(), qui fusionne la création de sous-objets et l'appel de la superclasse en une seule opération. Elle permet également de gérer automatiquement les échecs de construction d'objets enfants. L'exemple ci-dessous est un constructeur pour une sous-classe Group qui place deux objets Text dans le groupe.

IPTR MyClassNew(Class *cl, Object *obj, struct opSet *msg)
{  
  if (obj = DoSuperNew(cl, obj,
    MUIA_Group_Child, MUI_NewObject(MUIC_Text,
      /* attributes for the first subobject */
    TAG_END),
    MUIA_Group_Child, MUI_NewObject(MUIC_Text,
      /* attributes for the second subobject */
    TAG_END),
  TAG_MORE, msg->ops_AttrList)) 
  {
    struct MyClassData *d = (struct MyClassData*)INST_DATA(cl, obj);

    if ((d->ResourceA = ObtainResourceA()
     && (d->ResourceB = ObtainResourceB()
     && (d->ResourceC = ObtainResourceC())
    {
      return (IPTR)obj;    /* success */
    }
    else CoerceMethod(cl, obj, OM_DISPOSE);
  }
  return NULL;
}

Il est important d'observer que DoSuperNew() fusionne la liste de balises transmise au constructeur via le champ ops_AttrList du message et celle spécifiée dans la liste des arguments de la fonction. Cela se fait à l'aide d'une balise spéciale TAG_MORE, qui indique à un itérateur de liste de balises (comme la fonction NextTagItem()) de passer à une autre liste de balises pointée par la valeur de cette balise. La fusion des listes de balises permet de modifier l'objet en cours de construction avec les balises passées à NewObject(), par exemple en ajoutant un cadre ou un arrière-plan au groupe dans l'exemple ci-dessus.

La gestion automatique des objets enfants qui échouent fonctionne de la manière suivante : lorsqu'un sous-objet échoue, son constructeur renvoie NULL. Cette valeur NULL est alors insérée comme valeur d'une balise "enfant" (MUIA_Group_Child) dans l'exemple. Toutes les classes MUI capables d'avoir des objets enfants sont conçues de manière à ce que :
  • Le constructeur échoue si une balise "enfant" a une valeur NULL.
  • Le constructeur dispose de tout objet enfant construit avec succès avant de quitter.
Enfin, DoSuperNew() renvoie également NULL. Cette conception garantit qu'en cas d'échec lors de la compilation de l'application, tous les objets créés sont éliminés et il n'y a pas d'objets orphelins.

6.2 Surcharge des destructeurs

La seule tâche d'un destructeur est de libérer les ressources allouées par le constructeur et les autres méthodes (certaines ressources peuvent être allouées à la demande seulement). Dans tous les cas, le destructeur doit laisser l'objet dans le même état que juste après DoSuperMethod()/DoSuperNew() dans le constructeur. Après cela, le destructeur appelle un destructeur de superclasse. Le destructeur reçoit un message vide.

IPTR MyClassDispose(Class *cl, Object *obj, Msg msg)
{
  struct MyClassData *d = (struct MyClassData*)INST_DATA(cl, obj);

  if (d->ResourceA) FreeResourceA();
  if (d->ResourceB) FreeResourceB();
  if (d->ResourceC) FreeResourceC();
  return DoSuperMethodA(cl, obj, msg);
}

L'exemple de destructeur suit l'exemple du constructeur du chapitre "6.1 Surcharge des constructeurs". Trois ressources obtenues dans le constructeur sont libérées ici. Le destructeur est également préparé pour un objet partiellement construit, chaque ressource est vérifiée par rapport à NULL avant d'être libérée. Si pour un certain type de ressource NULL est un identifiant valide, un drapeau supplémentaire peut être ajouté à la zone de données de l'instance de l'objet.

6.3 Surcharge OM SET()

La méthode OM_SET() reçoit une structure opSet comme message. La structure est définie dans l'en-tête <intuition/classusr.h>.

struct opSet
{
  ULONG              MethodID;            /* always OM_SET (0x103) */
  struct TagItem    *ops_AttrList;
  struct GadgetInfo *ops_GInfo;
};

Le champ le plus important est "ops_AttrList". Il s'agit d'un pointeur vers une liste de balises contenant les attributs et les valeurs à définir. Le champ "ops_GInfo" est un héritage obsolète et n'est pas utilisé par les composants modernes comme MUI ou Reggae. L'implémentation de la méthode doit itérer la liste de balises et définir tous les attributs reconnus. L'opération de définition d'un attribut peut se résumer à la définition d'un champ dans les données d'une instance d'objet, mais elle peut aussi déclencher des actions (par exemple le redessinage de l'objet). Il est toutefois recommandé d'implémenter les actions complexes en tant que méthodes plutôt que de modifier les attributs. Une implémentation de référence de OM_SET() peut ressembler à ceci :

IPTR MyClassSet(Class *cl, Object *obj, struct opSet *msg)
{
  struct TagItem *tag, *tagptr;
  IPTR tagcount = 0;

  tagptr = msg->ops_AttrList;

  while ((tag = NextTagItem(&tagptr)) != NULL)
  {
    switch (tag->ti_Tag)
    {
      case SOME_TAG:
        /* attribute setting actions for SOME_TAG */
        tagcount++;
      break;

      /* more tags here */
    }
  }

  tagcount += DoSuperMethodA(cl, obj, (Msg)msg);
  return tagcount;
}

L'itération de la liste de balises se fait avec la fonction NextTagItem() de la bibliothèque utility.library. La fonction renvoie un pointeur vers la balise suivante à chaque fois qu'elle est appelée et conserve la position actuelle dans "tagptr". L'avantage de cette fonction est la gestion automatique des valeurs de balises spéciales (TAG_MORE, TAG_IGNORE, TAG_SKIP), elles ne sont pas renvoyées, mais leurs actions sont exécutées à la place.

La fonction OM_SET() renvoie le nombre total de balises reconnues. Elle est implémentée avec "tagcounter". Il est incrémenté à chaque balise reconnue et finalement le nombre de balises reconnues par la ou les superclasses est ajouté.

Les bogues courants dans l'implémentation de la fonction OM_SET() sont :
  • Le comptage des balises est ignoré.
  • L'appel de la super méthode dans le cas par défaut d'une instruction "switch". Cela entraîne l'appel multiple de la super méthode, une fois pour chaque balise non gérée par la sous-classe.
Dans certains cas rares, une sous-classe peut vouloir surcharger complètement un attribut, afin qu'il ne soit pas transmis aux superclasses. Cela peut être fait en remplaçant la balise (pas la valeur !) par TAG_IGNORE. Il y a cependant une mise en garde. Dans la plupart des cas en C et C++, la liste de balises est compilée dynamiquement sur la pile à partir des arguments variables d'une fonction comme SetAttrs(). Il est toutefois possible qu'une liste de balises soit un objet statique (par exemple un objet global, ou créé dans un morceau de mémoire libre alloué). Dans ce cas, changer une balise est une opération permanente, qui peut avoir des résultats inattendus. Cette remarque s'applique également à la modification de la valeur d'une balise avant de la transmettre à une superclasse. Une solution sûre consiste à cloner la liste de balises avec la fonction CloneTagItems() de la bibliothèque utility.library. Ensuite, les changements sont effectués dans la copie et cette copie est passée à la superclasse. La copie est ensuite libérée avec FreeTagItems(). L'inconvénient de cette solution est que le clonage d'une liste de balises peut échouer en raison d'un manque de mémoire libre et que cette possibilité doit être gérée d'une manière ou d'une autre.

6.4 Surcharge OM GET()

La méthode OM_GET(), utilisée pour obtenir un attribut d'un objet, reçoit une structure opGet comme message. Cette structure est définie dans le fichier d'en-tête <intuition/classusr.h> :

struct opGet
{
  ULONG  MethodID;           /* always OM_GET (0x104) */
  ULONG  opg_AttrID;
  ULONG *opg_Storage;
};

Contrairement à OM_SET(), cette méthode ne gère qu'un seul attribut à la fois. L'attribut est placé dans le champ "opg_AttrID". Le champ "opg_Storage" est un pointeur vers un endroit où la valeur de l'attribut doit être stockée. Il est défini comme un pointeur vers ULONG, mais il peut pointer vers n'importe quoi (par exemple vers une structure plus grande). Il permet de passer des attributs qui ne peuvent pas être placés dans une variable 32 bits. Comme OM_GET() n'a pas de boucle d'itération de liste de balises, son implémentation est simple :

IPTR MyClassGet(Class *cl, Object *obj, struct opGet *msg)
{
  switch (msg->opg_AttrID)
  {
    case Some_Integer_Tag:
      *msg->opg_Storage = /* value of the tag */;
    return TRUE;

    case Some_String_Tag:
      *(char**)msg->opg_Storage = "a fixed string value";
    return TRUE;
  }

  return DoSuperMethodA(cl, obj, (Msg)msg);
}

L'implémentation consiste en une instruction "switch" avec des cas pour tous les attributs reconnus. Si un attribut est reconnu, la méthode doit retourner TRUE (vrai). C'est très important, car les notifications MUI reposent sur OM_GET() et ne fonctionneront pas sur l'attribut si TRUE (vrai) n'est pas retourné. Les attributs inconnus sont transmis à la superclasse. L'appel DoSuperMethodA() peut être placé alternativement comme clause par défaut de l'instruction "switch". Il est important que msg->opg_Storage soit déréférencé lors du stockage de la valeur de l'attribut. Si le type de valeur n'est pas un entier, un "typecast" est nécessaire. Pour un type de valeur T, le déréférencement combiné au "typecast" est noté *(T*).

7. Sous-classage de la classe Application

Chaque application MUI est (ou du moins devrait être) une application événementielle. Cela signifie que l'application fournit un ensemble d'actions, qui peuvent être déclenchées par l'activité de l'utilisateur (comme l'utilisation de la souris et du clavier). La manière la plus simple d'implémenter cet ensemble d'actions est de l'implémenter comme un ensemble de méthodes ajoutées à un objet MUI. Pour les programmes simples, le meilleur candidat pour l'ajout de telles méthodes est l'objet maître Application. Les programmes plus complexes (par exemple ceux qui utilisent une interface multi-documents) peuvent ajouter des actions à d'autres classes, par exemple la classe Window.

Pourquoi des méthodes ? L'implémentation d'actions sous forme de méthodes présente de nombreux avantages :
  • Les méthodes peuvent être utilisées directement comme actions de notification. Cela évite au programmeur d'utiliser des astuces d'accrochage ("hook") ou d'encombrer la boucle principale avec de nombreuses valeurs ReturnID.
  • Les méthodes peuvent être couplées directement avec les commandes de l'interface du langage de script (anciennement appelée interface ARexx).
  • Les méthodes utilisées dans les notifications sont exécutées immédiatement en réponse aux actions de l'utilisateur. Aucun délai n'est introduit par la boucle principale (surtout si elle n'est pas vide).
  • Une valeur d'attribut de déclenchement de notification peut être passée directement à une méthode comme paramètre.
  • L'utilisation de méthodes améliore la modularité du code et l'encapsulation des objets. La fonctionnalité censée être traitée dans la portée d'un objet est traitée dans sa méthode, sans que le code ne se répande dans tout le projet.
Dans un programme MUI bien conçu, toutes les actions et fonctionnalités du programme sont implémentées sous forme de méthodes et l'état interne du programme est stocké sous forme d'un ensemble d'attributs et de champs de données d'instance d'application interne. Un exemple d'un tel programme est présenté en détail dans le tutoriel sur le sous-classage MUI : "9. Tutoriel de sous-classage MUI : le portage de SciMark2".

8. Sous-classage de la classe List

La classe List est l'une des classes MUI les plus complexes. Son but est d'afficher des données sous forme de liste ou de tableau. Les objets de la classe List peuvent être trouvés dans presque toutes les applications MUI. L'exemple ci-dessous est un objet List de l'application Media Logger, qui affiche le journal des événements de Reggae. La liste du journal des événements est l'élément principal de la fenêtre du programme. Les éléments visibles les plus importants d'un objet List sont marqués par des chiffres.

MUI classe List

  • 1. Une barre de titres de colonnes. Il s'agit d'un élément facultatif, utilisé généralement lorsqu'une liste comporte plusieurs colonnes.
  • 2. Lignes de données. Elles contiennent généralement des informations textuelles. Un formatage simple comme l'italique, le gras, le changement de couleur (montré dans l'exemple) peut être appliqué. La classe List ne permet pas un formatage plus avancé, par exemple la police ne peut pas être modifiée. Il est toutefois possible d'ajouter des images.
  • 3. Lignes de données sélectionnées. La sélection peut être effectuée à l'aide de la souris, du clavier ou de l'intérieur de l'application en utilisant les attributs de l'objet. Il existe une possibilité de multisélection facultative.
  • 4. La ligne active. En général, elle est également sélectionnée, mais en théorie, un élément peut être actif, mais non sélectionné. Il existe même un paramètre séparé dans les préférences MUI pour ce cas.
  • 5. Glissière horizontale. Utilisée lorsque les données ne tiennent pas horizontalement. Le défilement horizontal peut être désactivé, réglé sur automatique (apparaît quand c'est nécessaire), ou toujours visible.
  • 6. Glissière verticale. Utilisée lorsque les données ne tiennent pas dans le sens vertical. Le bouton de défilement peut être désactivé.
8.1 Utilisation basique

Le cas le plus simple d'utilisation de la classe List est une liste à une seule colonne contenant des chaînes de texte en clair. Dans ce cas, la manière la plus simple de définir ces textes est un tableau statique. Un tel objet List est un objet statique. Un tableau de chaînes de caractères est passé à l'objet avec l'attribut MUIA_List_SourceArray. Cet attribut ne peut être utilisé que comme argument pour un constructeur. Le tableau peut par exemple être déclaré comme une variable globale :

STRPTR ListItems[] = { "first", "second", "third", "fourth", NULL };

Le tableau doit se terminer par un pointeur NULL. L'objet est créé comme suit :

ListObj = MUI_NewObject(MUIC_List,
  MUIA_List_SourceArray, (ULONG)ListItems,
  MUIA_Frame, MUIV_Frame_ReadList,
  MUIA_Background, MUII_ReadListBack,
  MUIA_Font, MUIV_Font_List, 
TAG_END);

A part l'attribut MUIA_List_SourceArray mentionné ci-dessus, il y a trois attributs liés à l'apparence de l'objet. Ils ne doivent jamais être omis. MUIA_Frame définit le cadre de l'objet. S'il n'est pas spécifié, l'objet sera sans cadre, ce qui n'est pas beau à voir. Il existe deux types de cadre définis dans les préférences de l'utilisateur. MUIV_Frame_ReadList doit être utilisé pour les listes en lecture seule (celles qui ne peuvent pas être éditées par l'utilisateur). Les listes éditables doivent avoir le cadre MUIV_Frame_InputList. Dans la plupart des anciennes boîtes à outils de l'interface graphique Amiga, les listes en lecture seule avaient des cadres en creux, tandis que les listes éditables avaient des cadres en relief. Bien sûr, dans MUI, c'est à l'utilisateur de décider, mais du côté de l'application, la différence entre les deux types de cadres de liste doit être maintenue. L'image ci-dessous montre un exemple d'apparence d'un objet List défini avec le code ci-dessus :

MUI classe List
code source de cet exemple.

Le réglage de l'attribut MUIA_Frame d'un objet List à l'une des valeurs spéciales listées ci-dessus, a un effet secondaire inattendu et non documenté. L'arrière-plan de l'objet est automatiquement défini en fonction du type de cadre. Ce qui est étrange, c'est que ce réglage automatique ne peut pas être surchargé par MUIA_Background explicitement, l'attribut est ignoré.

Un objet List peut changer ses deux dimensions librement lorsque la fenêtre qui le contient est redimensionnée. Pour les listes statiques, dont le contenu est connu à l'avance, on peut demander que la largeur de l'objet soit verrouillée à la largeur de l'élément le plus long. Il est également possible de verrouiller la hauteur de l'objet à la hauteur totale de tous les éléments. Pour ce faire, les attributs MUIA_List_AdjustWidth et MUIA_List_AdjustHeight peuvent être spécifiés pour la construction de l'objet. Ces deux attributs sont booléens, avec la valeur par défaut FALSE (faux). Ajoutons-les à notre objet List :

  MUIA_List_AdjustWidth, TRUE,
  MUIA_List_AdjustHeight, TRUE,

MUI classe List
code source de cet exemple.

Les dimensions de l'objet list sont maintenant fixes. Comme la liste est le seul objet à l'intérieur de la fenêtre, celle-ci n'est plus redimensionnable (il n'y a plus de barre inférieure ni de gadget de dimensionnement). Comme la liste affiche toujours tous ses éléments, la glissière verticale est superflue. En théorie, elle peut être supprimée en spécifiant l'attribut MUIA_List_ScrollerPos comme MUIV_List_ScrollerPos_None, mais MUI de MorphOS 2.7 semble l'ignorer.

8.1.1 Ajout et suppression dynamique d'éléments

Il est évident qu'une liste statique n'est utile que dans de rares cas. Habituellement, les éléments sont ajoutés et supprimés par des actions de l'utilisateur ou à cause d'autres événements externes. La méthode la plus simple pour ajouter un élément de liste dynamiquement est MUIM_List_InsertSingle. Elle insère un seul élément à une position spécifiée :

DoMethod(ListObj, MUIM_List_InsertSingle, (IPTR)"fifth", MUIV_List_Insert_Bottom);

Lorsque plusieurs éléments doivent être insérés, cela peut être fait en une seule fois avec MUIM_List_Insert. Les éléments doivent être regroupés dans un tableau. Le nombre d'éléments insérés peut être soit donné explicitement, soit le tableau peut être terminé par un élément NULL (comme pour MUIA_List_SourceArray), puis -1 est passé comme quantité. Ces deux façons sont montrées dans l'exemple ci-dessous, les deux appels sont équivalents :

DoMethod(ListObj, MUIM_List_Insert, (IPTR)ListElements, 4, MUIV_List_Insert_Bottom);
DoMethod(ListObj, MUIM_List_Insert, (IPTR)ListElements, -1, MUIV_List_Insert_Bottom);

Pour le premier appel, le tableau ListItems n'a pas besoin d'avoir un élément NULL à la fin. Ensuite, la première forme peut être utilisée pour insérer n'importe quel fragment continu d'un tableau source. Insérons seulement le deuxième et le troisième élément du tableau :

DoMethod(ListObj, MUIM_List_Insert, (IPTR)&ListElements[1], 2, MUIV_List_Insert_Bottom);

Maintenant, discutons de la position d'insertion, qui est le dernier argument des deux méthodes décrites. La position peut être spécifiée explicitement comme un index (en comptant à partir de 0). Le nouvel élément est alors inséré avant celui qui est spécifié. A part cela, il y a quatre constantes prédéfinies :
  • MUIV_List_Insert_Top : insère le ou les éléments au début de la liste. Cela donne le même résultat que l'insertion à la position 0, et en effet cette constante a une valeur de 0.
  • MUIV_List_Insert_Bottom : insère des éléments en bas de la liste.
  • MUIV_List_Insert_Active : insère des éléments au-dessus de l'élément actif (le curseur de la liste).
  • MUIV_List_Insert_Sorted : insère les éléments selon l'ordre de tri de la liste.
Il y a deux choses importantes, qui doivent être observées lors de l'insertion de tableaux d'éléments. La première est que la position d'insertion n'est pas appliquée au tableau inséré dans son ensemble. Au lieu de cela, elle est appliquée à chaque élément à tour de rôle. Cela conduit à des résultats surprenants. Par exemple, lorsqu'on insère un tableau d'éléments avec MUIV_List_Insert_Top, le tableau inséré apparaîtra dans la liste dans un ordre inversé. Le premier élément du tableau sera bien sûr inséré en haut de la liste, puis le deuxième élément du tableau sera également inséré en haut, il apparaîtra donc au-dessus du premier, et ainsi de suite. Cela fonctionne également de cette manière lorsque la position d'insertion est donnée sous la forme d'un nombre. D'autre part, l'ordre d'un tableau original est préservé, lorsqu'il est inséré en bas, ou lorsqu'il est inséré au-dessus du curseur de la liste (parce que le curseur de la liste est déplacé vers le bas après avoir ajouté chaque élément).

La deuxième chose importante est le tri de la liste. Si l'on veut garder la liste triée, chaque élément doit être inséré avec MUIV_List_Insert_Sorted. En effet, l'insertion d'un élément trié ne trie pas le contenu actuel de la liste. La constante signifie simplement que la position d'insertion sera déterminée en utilisant l'ordre de tri actuel en supposant que la liste est actuellement triée. Si ce n'est pas le cas, les résultats peuvent être imprévisibles. Si, pour une raison quelconque, une liste ne peut être maintenue triée, elle peut être triée manuellement avec la méthode MUIM_List_Sort().

La méthode de tri par défaut pour les listes de chaînes de caractères en texte brut est l'ordre alphabétique, ascendant, insensible à la casse. Elle est basée sur les codes ASCII des caractères, et ne fonctionne donc de manière fiable que pour l'anglais et les autres langues n'utilisant pas de caractères au-dessus de la plage ASCII de base (jusqu'au code 127). Pour de nombreuses langues, un tel tri ne permet pas d'obtenir l'ordre du dictionnaire. Une solution à ce problème sera présentée plus tard, lorsque le sous-classage de la classe List sera abordé.

Supprimer des éléments d'une liste est beaucoup plus facile. La méthode MUIM_List_Remove() est utilisée pour cela. Comme pour l'insertion, on peut spécifier l'indice de l'élément comme un nombre explicite, ou indirectement avec une constante prédéfinie. Ces constantes sont très similaires à celles utilisées pour l'insertion, et permettent de supprimer le premier, le dernier ou l'élément actif de la liste. Elles peuvent être trouvées dans le fichier autodoc de la classe List dans le SDK de MorphOS. Une constante supplémentaire est MUIV_List_Remove_Selected, qui supprime tous les éléments sélectionnés, en supposant qu'une liste permette la multi-sélection. C'est le seul cas où MUIM_List_Remove() peut supprimer plus d'un élément.

Chaque opération d'insertion ou de suppression entraîne un redessinage de l'objet List, si l'objet est visible. Le redessinage peut ralentir l'insertion d'énormes tableaux d'éléments. La classe List fournit l'attribut MUIA_List_Quiet qui désactive temporairement le redessinage de l'objet lors des opérations d'insertion, de suppression ou de réorganisation. L'attribut peut être mis à TRUE (vrai) avant les opérations intensives sur la liste, puis mis à FALSE (faux) à la fin, de sorte que la liste ne sera rafraîchie qu'une seule fois, montrant le résultat final. Il existe également la méthode MUIM_List_Clear(), qui supprime tous les éléments en une seule fois et est bien sûr beaucoup plus rapide que de supprimer des éléments dans une boucle.

8.1.2 Lecture de listes

Le problème de la lecture d'un objet List se compose de deux parties : la lecture des éléments et la lecture de l'état de l'objet. Commençons par la lecture des éléments. La méthode MUIM_List_GetEntry() récupère un pointeur sur un élément. Dans le cas des listes simples, il s'agit simplement d'un pointeur sur une chaîne de caractères. Il est utilisé comme suit :

STRPTR item;

DoMethod(ListObj, MUIM_List_GetEntry, index, (IPTR)&item);

L'argument "index" peut être juste un nombre, ou une constante prédéfinie MUIV_List_GetEntry_Active pour lire l'élément actif. L'index compte à partir de zéro. Ce qui est important, c'est qu'un pointeur vers l'élément n'est pas le résultat de la méthode. Ce pointeur est placé dans une variable. L'adresse de cette variable est passée comme dernier argument de la méthode. Si la liste ne contient pas d'élément de l'index spécifié, NULL est placé dans la variable. Ensuite, un exemple de boucle lisant tous les éléments peut être organisé comme indiqué :

STRPTR item;
LONG i;

for (i = 0; ; i++)
{
  DoMethod(ListObj, MUIM_List_GetEntry, i, (IPTR)&item);
  if (item) Printf("Item %ld is '%s'.\n", i, item);
  else break;
}
  • MUIA_List_Active : index de l'élément actif.
  • MUIA_List_Entries : nombre total d'éléments.
  • MUIA_List_First : index du premier élément visible.
  • MUIA_List_Visible : nombre d'éléments éventuellement visibles.
  • MUIA_List_DoubleClick : est mis à TRUE (vrai) lorsqu'un double-clic du bouton gauche de la souris est effectué sur la liste.
MUIA_List_Active et MUIA_List_First sont également paramétrables, afin que le curseur de la liste puisse être déplacé et que la liste puisse défiler depuis l'application. Les trois autres sont en lecture seule pour des raisons évidentes. Les notifications peuvent être définies sur les attributs qui changent directement après les actions de l'utilisateur : MUIA_List_Active, MUIA_List_Entries et MUIA_List_DoubleClick. Les attributs MUIA_List_First et MUIA_List_Visible décrivent la géométrie de l'objet plutôt que son contenu. Les valeurs de ces attributs ne peuvent être modifiées indistinctement que par le défilement ou le redimensionnement de la fenêtre. Les notifications sur ces deux attributs ne fonctionnent donc pas. Comme MUIA_List_Visible décrit en fait la hauteur de l'objet de la liste, sa valeur peut être supérieure au nombre d'éléments de la liste, dans le cas où la liste est courte. Par exemple, si l'objet est suffisamment haut pour afficher cinq éléments, mais qu'il n'y a que trois éléments dans la liste, l'attribut sera toujours égal à "5", et sera donc supérieur à MUIA_List_Entries.

L'attribut MUIA_List_DoubleClick peut sembler inutile à première vue, car il ne fournit aucune information sur l'élément sur lequel on a double-cliqué. Cependant, un double-clic déplace également le curseur de la liste, et MUIA_List_Active est donc défini sur l'index de l'élément sur lequel on a double-cliqué. Dans le cas de listes multicolonnes, il est également possible d'obtenir l'index de la colonne cliquée, nous y reviendrons plus tard.

Il existe d'autres attributs de liste qui gèrent les caractéristiques suivantes :
  • Listes multicolonnes.
  • Barre de titre de liste.
  • Gestion de la multi-sélection.
Comme il s'agit de sujets avancés, ils seront expliqués plus tard.

8.1.3 Copies dynamiques de chaînes de caractères

On suppose depuis le début de cet article que les chaînes de texte insérées dans l'objet List sont statiques, qu'elles existent en mémoire pendant toute la durée d'exécution d'un programme. Cela peut être vrai parfois, mais dans de nombreux cas, ces chaînes sont dynamiques, par exemple saisies par l'utilisateur. Parfois, il est simplement plus confortable d'ajouter des chaînes à partir d'un tampon qui est une variable locale. L'objet List doit alors créer des copies des éléments insérés. Heureusement, cette tâche (et aussi celle de libérer ces copies plus tard) peut être automatisée. Deux attributs permettent d'activer la fonction de copie automatique :

MUIA_List_ConstructHook, MUIV_List_ConstructHook_String,
MUIA_List_DestructHook, MUIV_List_DestructHook_String,

Ces attributs doivent toujours être utilisés ensemble. Sinon, les éléments de la liste seront détruits et des fuites mémoire se produiront.

8.1.4 Un exemple plus complexe

Le troisième exemple de programme pour la classe MUI List est plus complexe. Il démontre les opérations typiques effectuées sur un objet List : insertion, suppression et édition d'éléments, notification des mouvements du curseur de la liste et d'un double-clic. L'interface de l'exemple est présentée ci-dessous. L'élément principal de l'interface graphique est une liste avec son "éditeur" - un ensemble de gadgets pour insérer, supprimer et modifier les éléments de la liste. Un groupe de gadgets situé en dessous affiche l'état de la liste et permet d'y apporter des modifications. Ces gadgets sont rarement présents dans les applications typiques, ils ont été ajoutés ici à des fins de démonstration.

MUI classe List
code source de cet exemple.

Le code source de cet exemple est contenu dans trois fichiers. Le fichier "start.c" contient un code de démarrage personnalisé. Il peut être remplacé par un code standard si nécessaire, cela aura juste pour effet d'augmenter un peu la taille de l'exécutable compilé. Le fichier "main.c" contient le code principal du programme. Le fichier "application.c" est le code d'une classe MUI personnalisée. Cet exemple utilise une technique moderne de création d'une application MUI, où les actions de l'application sont implémentées comme des méthodes d'une sous-classe Application. De cette façon, on évite de s'embrouiller avec des "hooks" de rappel et d'encombrer la boucle d'événement principale avec du code. Pour cette raison, l'interface graphique est simplement construite dans le constructeur de la sous-classe. La configuration des notifications et même la boucle d'événement principale sont également implémentées en tant que méthodes de la sous-classe. Cette approche systématique peut sembler exagérée pour un projet aussi simple, mais elle s'avère payante lorsque le projet est étendu. Ce qu'il faut noter, c'est qu'une telle conception n'ajoute ni à la taille de l'exécutable ni au temps d'exécution.

L'éditeur de liste se compose d'un gadget de chaîne, d'un bouton "Ajouter" et d'un bouton "Supprimer". Le bouton "Ajouter" insère un nouvel élément de liste (à la position du curseur) dont le contenu est extrait du gadget de chaîne de caractères. L'appui sur la touche "Entrée" dans le gadget chaîne de caractères remplace l'élément de liste actif par le contenu du gadget. Examinons de plus près cette opération. L'exemple utilise des copies de chaîne dynamiques. Il n'y a aucun moyen de remplacer le texte directement. On peut essayer de le pirater, en lisant le pointeur de la chaîne avec la méthode MUIM_List_GetEntry() et en y plaçant le nouveau texte. Ce serait cependant une erreur critique. Une zone de mémoire pour l'ancien contenu a été allouée automatiquement par l'objet. L'écraser avec un nouveau texte, peut-être plus long, provoquera un débordement de la mémoire tampon et une destruction de la mémoire. Le résultat final peut être un plantage du programme, ou même du système si la zone de mémoire endommagée est assez grande. Ensuite, lorsqu'un objet List crée lui-même des copies des éléments insérés, la seule façon correcte de modifier un élément est de le supprimer, de créer un nouvel élément et de l'insérer à la même position. C'est ce que fait la méthode APPM_UpdateItem() dans l'exemple. De plus, pour cacher l'opération à l'utilisateur, même sur un ordinateur très lent (ou occupé), tout cela se passe pendant que l'attribut MUIA_List_Quiet est réglé sur TRUE (vrai).

Les notifications sur les boutons "Add" (ajouter) et "Delete" (supprimer) sont simples. La suppression d'un élément est plus facile, car MUIM_List_Remove() peut être appelé directement depuis la notification. L'ajout d'un nouvel élément nécessite d'appeler une méthode auxiliaire APPM_AddItem(). Ceci est dû au fait qu'il est impossible dans une notification de récupérer un attribut d'un objet qui n'est pas la source de la notification. Dans cette notification, la source est le bouton "Add" (ajouter), mais il faut que le contenu du gadget de chaînes soit ajouté comme élément de liste. Il existe d'autres méthodes auxiliaires de ce type. Par exemple, les notifications sur les gadgets de chaînes "First" (premier) et "Active" (actif) en ont besoin, car l'attribut MUIA_String_Integer ne fonctionne pas comme déclencheur de notification.

A la fin de cette section, je voudrais aborder un détail du programme : la désactivation du bouton "Delete" (supprimer). Le bouton est désactivé initialement en spécifiant "MUIA_Disabled, TRUE" au moment de la construction. Ensuite, la méthode APPM_DeleteControl() réagit à tout changement de l'élément actif de la liste et désactive le bouton "Delete" (supprimer) lorsqu'aucun élément n'est actif, ou lorsque la liste est vide. Ce n'est pas si important pour la stabilité du programme, car MUIM_List_Remove() vérifie de toute façon l'index des éléments. Il est cependant très important de donner un retour visuel approprié à l'utilisateur. Très peu d'applications le font de manière cohérente, mais la désactivation dynamique des gadgets, qui sont inutilisables pour le moment, est importante car elle améliore l'expérience utilisateur.

8.2 Éléments de données composés, listes multicolonnes

Les listes composées d'éléments qui sont de simples chaînes de texte sont utiles, mais ne suffisent pas pour de nombreuses applications. Dans de nombreux cas, des listes multicolonnes sont nécessaires, où chaque colonne contient un certain type de données, qui peuvent être représentées en interne comme un champ dans une structure. Une telle structure peut être appelée un élément de données composé. La classe List de MUI peut gérer n'importe quelle structure de données arbitraire en tant qu'élément. Le concept général de traitement des données dans la classe List est illustré dans le diagramme ci-dessous.

MUI classe List

Comme la classe peut travailler avec des structures de données inconnues, l'application doit "dire" à la classe comment effectuer des opérations de base sur celles-ci. Ces opérations sont définies comme des méthodes de la classe List, avec l'intention que l'application surcharge ces méthodes dans une sous-classe. Il existe également trois types de données : les données d'entrée, les données internes et le contenu affiché.
  • Les données internes sont la structure la plus importante et sont juste une représentation interne d'un élément de données. Obtenir un élément avec MUIM_List_GetEntry() renvoie un pointeur vers la structure de données internes.
  • Les données d'entrée sont une structure qui est passée à MUIM_List_Insert() et MUIM_List_InsertSingle(). Dans de nombreux cas, elles peuvent être identiques aux données internes.
  • Le contenu affiché est un ensemble de chaînes de caractères (une par colonne de liste) qui doivent être affichées. Ces chaînes peuvent contenir des codes de contrôle pour les styles, les couleurs et les images incluses.
Quatre méthodes de la classe List peuvent être surchargées. Elles traitent les trois types de données de la manière décrite dans le diagramme et expliquée ci-dessous :
  • MUIM_List_Construct : cette méthode est appelée lorsqu'un élément est inséré dans la liste. Elle obtient un pointeur vers la structure de données d'entrée et la convertit d'une manière ou d'une autre en une structure de données internes. Ensuite, cette structure de données internes est insérée dans une liste. L'implémentation par défaut de cette méthode ne fait que transmettre le pointeur reçu. C'est peut-être la bonne chose à faire, lorsque les données sont statiques et simples. Les deux premiers exemples de cet article ont utilisé le constructeur d'éléments par défaut. Une tâche typique du constructeur est de créer des copies de données dynamiques, il peut également retirer les données inutilisées de la structure de données d'entrée ou effectuer certaines conversions.

  • MUIM_List_Destruct : cette méthode est appelée lorsqu'un élément est supprimé (ou lorsque l'objet est détruit, ce qui implique de supprimer d'abord tous les éléments). Le destructeur d'élément libère toutes les ressources allouées dans le constructeur pour un élément. Cela signifie généralement la libération des tampons mémoire alloués pour les copies de données dynamiques. L'implémentation par défaut de cette méthode ne fait rien.

  • MUIM_List_Display : convertit la représentation des données internes en un ensemble de chaînes de caractères à afficher dans des colonnes de liste. L'implémentation par défaut suppose que les données internes sont une chaîne de texte, qui est transmise pour être affichée dans la première (et seule) colonne.

  • MUIM_List_Compare : compare deux structures de données internes. Cette méthode n'est utilisée que pour le tri de listes et les insertions triées. La classe l'utilise comme un rappel ("callback") de comparaison pour l'algorithme "quicksort". La méthode ne doit pas être implémentée si le tri n'est pas nécessaire. L'implémentation par défaut suppose qu'elle obtient des pointeurs vers deux chaînes de texte et appelle stricmp() sur celles-ci.
Note : les anciennes versions de MUI implémentaient ces quatre méthodes comme des rappels utilisant des "hooks". Ces "hooks" étaient définis par des attributs documentés dans l'autodoc de la classe List. Les "hooks" fonctionnent toujours pour des raisons de compatibilité ascendante, mais ne sont pas recommandés dans le nouveau code, d'autant plus que leur utilisation sous MorphOS est un peu lourde. La seule exception utile est le constructeur et le destructeur de chaînes de caractères de copie intégrés. Nous les avons utilisés dans le troisième exemple ci-dessus.

8.2.1 Copier ou ne pas copier ?

La question de ce titre est l'une des décisions de conception les plus importantes à prendre lorsqu'on sous-classe la classe List. La règle principale et évidente est que toutes les données de l'élément doivent exister tant que l'élément est dans la liste. Si cela ne peut être garanti, le constructeur de l'élément doit créer des copies, qui seront ensuite libérées par le destructeur. Dans la plupart des cas, la "copie" est le bon choix, sauf dans certains cas particuliers comme :
  • Un ensemble fixe (par exemple codé en dur) de données, un cas rare, mais qui arrive parfois.
  • La liste n'est qu'une vue des données stockées ailleurs dans l'application, la duplication n'a aucun sens.
  • Il y a plusieurs listes ou vues de données partageant le même ensemble de données qui peuvent être mises à jour. Bien que le fait d'avoir un constructeur non copiant dans ce cas évite de réinsérer un élément dans plusieurs listes, toutes les listes doivent de toute façon être rafraîchies manuellement après une mise à jour.
  • Liste est une vue d'un très grand ensemble de données (disons plusieurs gigaoctets) stocké sur le disque. Les lignes de données sont chargées à la demande dans MUIM_List_Display(). Dans ce cas, les données ne sont en fait jamais insérées, car la structure de données internes ne contiendra que, par exemple, la position ("offset") dans un fichier.
Ces cas ne sont que des exemples, car la vie réelle est souvent encore plus compliquée...

Lorsqu'il est nécessaire de créer une copie de certaines données dans MUIM_List_Construct(), la classe List aide à la gestion de la mémoire en fournissant un pool de mémoire créé automatiquement. Son utilisation n'est cependant pas obligatoire. Sur les systèmes Amiga Classic, les allocations de mémoire en pool étaient sensiblement plus rapides, mais ce n'est pas le cas sur MorphOS. Un avantage de l'utilisation des allocations groupées est la possibilité de sauter l'implémentation du destructeur d'éléments (MUIM_List_Destruct()), puisque le pool est libéré automatiquement lorsque l'objet List est éliminé. Pour les listes très longues (des milliers d'éléments et plus), la libération du pool est également beaucoup plus rapide que la libération des allocations individuelles. D'un autre côté, le fait d'ignorer le destructeur d'éléments peut être dangereux pour les programmes qui ajoutent et suppriment continuellement des éléments, car ils consommeront de plus en plus de mémoire, qui ne sera pas libérée avant la fin du programme.

Armé de toutes ces connaissances, on peut essayer d'implémenter un exemple de constructeur d'objet. Un objet List montrera l'inventaire d'un entrepôt. La structure des données d'entrée est un ensemble de chaînes de caractères créées dynamiquement, par exemple récupérées dans une base de données distante :

struct InputData
{
  STRPTR ItemName;
  STRPTR Quantity;
  STRPTR Price;
};

Pour la représentation interne, nous préférons convertir les données numériques en nombres à virgule flottante en double précision, ce qui facilitera les calculs ultérieurs :

struct InternalData
{
  STRPTR ItemName;
  DOUBLE Quantity;
  DOUBLE Price;
 };

Le constructeur de l'élément prend un pointeur vers InputData, alloue InternalData, crée une copie d'ItemName et convertit Quantity et Price.

8.2.2 Exemple de constructeur

Nous venons d'opter pour la copie intégrale du constructeur. Le code de celui-ci est simple :

IPTR ListConstruct(Class *cl, Object *obj, struct MUIP_List_Construct *msg)
{
  struct InputData *entry = (struct InputData*)msg->entry;
  struct InternalData *idata;

  if (idata = AllocTaskPooled(sizeof(struct InternalData)))
  {
    if (idata->ItemName = AllocVecTaskPooled(strlen(entry->ItemName) + 1))
    {
      strcpy(idata->ItemName, entry->ItemName);
      idata->Quantity = atof(entry->Quantity);
      idata->Price = atof(entry->Price);
      return (IPTR)idata;
    }
    FreeTaskPooled(idata, sizeof(struct InternalData));
  }
  return NULL;
}

Ce code n'utilise pas le pool de mémoire fourni par la classe List. Au lieu de cela, il utilise des fonctions modernes spécifiques à MorphOS qui allouent la mémoire à partir d'un pool de mémoire attribué à chaque processus système. Ce pool est automatiquement éliminé lorsqu'un processus se termine. Il faut faire attention à ne pas retourner un élément de liste à moitié construit. Chaque allocation est vérifiée, et si la seconde, qui alloue de l'espace pour la chaîne du nom de l'élément, échoue, la structure de l'élément est libérée et le constructeur retourne NULL. Les valeurs numériques sont converties en nombres avec l'appel standard atof(). Si les allocations ont été réussies, une structure InternalData nouvellement créée et remplie est retournée.

8.2.3 Exemple de destructeur

La seule tâche de notre destructeur est de libérer les zones de mémoire allouées dans le constructeur. Notez l'ordre de désallocation, ItemName est libéré en premier. Si nous libérions la structure InternalData en premier, le pointeur vers le nom de l'élément serait dans la mémoire libre et pourrait être écrasé avant que nous ne parvenions à le libérer également. Le constructeur utilise l'ordre descendant de construction, il alloue d'abord l'élément et ensuite ses composants. Le destructeur est symétrique, il libère d'abord les composants, puis l'objet. Parce que je me suis assuré dans le constructeur que les données internes sont toujours entièrement construites, je n'ai pas besoin de vérifier les pointeurs vers les composants contre la valeur NULL.

IPTR ListDestruct(Class *cl, Object *obj, struct MUIP_List_Destruct *msg)
{
  struct InternalData *idata = (struct InternalData*)msg->entry;

  if (idata)
  {
    FreeVecTaskPooled(idata->ItemName);
    FreeTaskPooled(idata, sizeof(struct InternalData));
  }
  return 0;
}

Dans cet exemple, le destructeur pourrait être complètement omis. Comme la mémoire est allouée à partir du pool de processus, sauter la désallocation ne crée pas de fuite de mémoire, la désallocation est simplement reportée à la fin de l'exécution du processus. Cela ne fait une différence pratique que lorsque de grandes listes sont créées et éliminées à plusieurs reprises pendant l'exécution du programme. Dans ce cas, le destructeur doit libérer la mémoire explicitement, afin que la mémoire du système ne soit pas épuisée.

9. Tutoriel de sous-classage MUI : le portage de SciMark2

9.1 L'application

De nombreux tutoriels de programmation ont tendance à ennuyer les lecteurs avec des exemples inutiles. Dans celui-ci, une application du "monde réel" sera portée sur MorphOS et "MUI-fiée". L'application est SciMark 2. SciMark est encore un outil de tests de performances. Il effectue quelques calculs scientifiques typiques comme la transformée de Fourier rapide, la décomposition de la matrice LU, la multiplication de matrices éparses et ainsi de suite. Le test mesure principalement la vitesse du processeur pour les calculs en virgule flottante, l'efficacité du cache et la vitesse de la mémoire. Initialement écrit en Java, il a été réécrit en C (et en fait dans de nombreux autres langages). Le code source C est disponible sur la page d'accueil du projet.

Le code source utilise uniquement le standard C ANSI pur, il est donc facilement compilable sur MorphOS en utilisant le fichier makefile fourni. Il suffit de remplacer la ligne "$CC = cc" par "$CC = gcc", pour correspondre au nom du compilateur de MorphOS. En conséquence, on obtient une application typique utilisable via le Shell. Voici des exemples de résultats pour un Pegasos II avec un processeur PowerPC G4 :

MUI Scimark

Pas très impressionnant en fait. Ceci est dû au fait qu'aucun drapeau d'optimisation n'est passé au compilateur dans le fichier makefile. Ils peuvent être ajoutés en insérant la ligne "$CFLAGS = -O3" sous la ligne "$CC = gcc". Lions également avec libnix (une émulation d'environnement Unix liée statiquement, voir Bibliothèques standard C et C++) en ajoutant "-noixemul" à CFLAGS et LDFLAGS. Après avoir reconstruit le programme et l'avoir exécuté à nouveau, les résultats sont nettement améliorés (le programme a été compilé avec GCC 4.4.4 présent dans le SDK officiel).

MUI Scimark

Cela montre l'importance de l'optimisation du code, surtout pour les programmes à forte intensité de calcul. Le code optimisé est plus de quatre fois plus rapide !

9.2 Inspection du code

Le code source original est bien modularisé. Cinq fichiers : FFT.c, LU.c, MonteCarlo.c, SOR.c et SparseCompRow.c implémentent les cinq tests individuels. Les fichiers array.c et Random.c contiennent des fonctions auxiliaires utilisées dans les tests de performances. Le fichier Stopwatch.c implémente la mesure du temps. Un fichier important, kernel.c, rassemble tout ce qui précède et fournit la synchronisation pour les cinq fonctions exécutant tous les tests de performances. Enfin, scimark2.c contient la fonction main() et implémente l'interface Shell.

L'interface MUI prévue devrait permettre à l'utilisateur d'exécuter chaque test de performances séparément ou de les exécuter tous. Il existe également une option "-large", qui augmente la taille de la mémoire pour les problèmes calculés, afin qu'ils ne tiennent pas dans le cache du processeur. Une règle générale du portage est que le moins de fichiers possible doivent être modifiés. Cette règle facilite la mise à jour du portage lorsqu'une nouvelle version du programme original est publiée. Dans le cas de SciMark, un seul fichier, scimark2.c doit être remplacé. Un portage avancé peut également remplacer Stopwatch.c par du code utilisant directement timer.device pour une meilleure précision des mesures de temps ; cependant, ceci est hors de portée de ce tutoriel.

Un regard plus attentif à "scimark2.c" révèle qu'il y a un objet Random (une structure définie dans "Random.h"), qui est requis pour tous les tests. Dans le code original, il est créé avec new_Random_seed() au début du programme et éliminé avec delete_Random() à la sortie. Dans la version MUI, le meilleur endroit pour le placer est la zone de données d'instance de la classe Application sous-classée. Il peut alors être créé dans OM_NEW() de la classe et supprimé dans OM_DISPOSE(). Ces deux méthodes doivent ensuite être surchargées.

9.3 Conception de l'interface graphique

MUI Scimark

Bien sûr, il n'existe pas une seule et unique conception d'interface graphique pour SciMark. Une conception simple, utilisant un ensemble limité de classes MUI est montrée ci-dessus. Il y a cinq boutons pour les tests individuels et un pour les exécuter tous. Tous ces boutons sont des instances de la classe Text. A droite, il y a des gadgets pour afficher les résultats des tests. Ces gadgets appartiennent également à la classe Text, mais leurs attributs sont différents. Le bouton "Large Data", de la classe Text bien sûr, est un commutateur. Étonnamment, la barre d'état (affichant "Ready.") n'est pas une instance de la classe Text, mais de la classe Gauge. Ainsi, il sera possible d'afficher une barre de progression lors de l'exécution des cinq tests. Les barres horizontales espacées au-dessus du bouton "All Benchmarks" sont des instances de la classe Rectangle. Il y a également trois objets invisibles de la classe Group. Le premier est un groupe principal vertical, qui est l'objet racine de la fenêtre. Il contient deux sous-groupes. Le groupe supérieur est le groupe tableau à deux colonnes et contient tous les boutons de référence et les gadgets d'affichage des résultats. Le groupe inférieur contient le bouton commutateur "Large Data" et la barre d'état.

La façon la plus simple de commencer à concevoir une interface graphique est de copier l'exemple "Hello World". Ensuite, les objets MUI peuvent être ajoutés à la fonction build_gui(). L'exemple modifié est prêt à être compilé et exécuté. Il ne s'agit pas d'un programme complet bien sûr, juste d'un modèle d'interface graphique sans aucune fonctionnalité ajoutée.

Une vue rapide de la fonction build_gui() révèle qu'elle ne contient pas tout le code de l'interface graphique. Le code de certains sous-objets est placé dans des fonctions appelées par la fonction principale MUI_NewObject(). La division de la fonction de construction de l'interface graphique en plusieurs sous-fonctions présente quelques avantages importants :
  • Une meilleure lisibilité du code et des modifications plus faciles à réaliser. Un simple appel à MUI_NewObject() devient rapidement plus long au fur et à mesure que le projet évolue. L'édition d'une grande fonction s'étendant sur quelques écrans est inconfortable. Ajouter et supprimer des objets GUI dans une telle fonction devient un cauchemar même avec une indentation utilisée en conséquence. D'autre part, la fonction peut avoir dix niveaux d'indentation ou plus, ce qui la rend également difficile à lire.

  • Réduction de la taille du code. Au lieu d'écrire plusieurs fois un code très similaire, par exemple des boutons avec des étiquettes différentes, une sous-routine peut être appelée avec une étiquette comme argument.

  • Débogage. Il arrive parfois que MUI refuse de créer l'objet d'application à cause de balises ou de valeurs erronées qui lui sont passées. Si l'appel principal MUI_NewObject() est divisé en sous-fonctions, il est facile d'isoler l'objet défectueux en insérant des Printf()- dans les sous-fonctions.
9.4 Méthodes et attributs

L'interface graphique SciMark qui vient d'être conçue, définit six actions pour l'application. Il y a cinq actions pour exécuter des tests individuels et la sixième pour exécuter tous les tests et calculer le résultat global. Les actions seront directement mises en correspondance avec les méthodes de la sous-classe Application. Il y a aussi un attribut lié au bouton "Large Data", il détermine la taille des problèmes résolus par les tests. Les méthodes n'ont pas besoin de paramètres, il n'est donc pas nécessaire de définir des messages de méthode. Un attribut peut être applicable au moment de l'initialisation (dans le constructeur de l'objet), il peut également être paramétrable (nécessite le recouvrement de la méthode OM_SET()) et récupérable (nécessite le recouvrement de la méthode OM_GET()). Notre nouvel attribut, nommé APPA_LargeData dans le code, doit seulement être paramétrable. Dans le constructeur, il peut être implicitement mis à FALSE (faux), car le bouton est désactivé initialement. La capacité de récupération n'est pas nécessaire, car cet attribut ne sera utilisé qu'à l'intérieur de la sous-classe Application.

Il est recommandé que chaque sous-classe de l'application soit placée dans un fichier source distinct. Cela aide à garder la modularité du code et permet également de cacher les données privées des classes. Cela nécessite l'écriture d'un fichier makefile, mais il en faut un de toute façon, car le code original de SciMark est constitué de plusieurs fichiers. En implémentant les orientations de conception discutées ci-dessus, un fichier d'en-tête de classe et un code de classe peuvent être écrits. La classe ne fait toujours rien, elle implémente juste six méthodes vides et surcharge OM_SET(), OM_NEW() et OM_DISPOSE(). En fait, c'est un exemple de modèle ennuyeux et en tant que tel, il a été généré avec le générateur de modèle de ChocolateCastle. Malheureusement, ChocolateCastle est encore en version bêta, et les fichiers ont dû être modifiés manuellement après la génération (NDLR : la dernière version.0.8 règle peut-être ce problème ?).

L'étape suivante de la conception de l'application consiste à relier les méthodes et les attributs aux éléments de l'interface graphique à l'aide de notifications. Les notifications doivent bien sûr être créées après la création de l'objet source et de l'objet cible. Dans le code de SciMark, elles sont simplement mises en place après l'exécution de build_gui(). Les six boutons d'action ont tous des notifications très similaires, c'est pourquoi une seule est présentée ici :

DoMethod(findobj(OBJ_BUTTON_FFT, App), MUIM_Notify, MUIA_Pressed, FALSE,
 App, 1, APPM_FastFourierTransform);

Le bouton "Large Data" a une notification qui définit l'attribut correspondant :

DoMethod(findobj(OBJ_BUTTON_LDATA, App), MUIM_Notify, MUIA_Selected, MUIV_EveryTime,
 App, 3, MUIM_Set, APPA_LargeData, MUIV_TriggerValue);

Les objets notifiés sont accessibles par recherche dynamique (la macro findobj()), ce qui évite au programmeur de définir des variables globales pour chacun d'entre eux.

9.5 Implémentation des fonctionnalités

Les cinq méthodes d'implémentation des tests de performances SciMark sont très similaires, c'est pourquoi nous n'en avons présenté qu'une seule, l'exécution de la transformée de Fourier rapide :

IPTR ApplicationFastFourierTransform(Class *cl, Object *obj)
{
  struct ApplicationData *d = INST_DATA(cl, obj);
  double result;
  LONG fft_size;

  if (d->LargeData) fft_size = LG_FFT_SIZE;
  else fft_size = FFT_SIZE;

  SetAttrs(findobj(OBJ_STATUS_BAR, obj),
    MUIA_Gauge_InfoText, (LONG)"Performing Fast Fourier Transform test...",
    MUIA_Gauge_Current, 0,
  TAG_END);

  set(findobj(OBJ_RESULT_FFT, obj), MUIA_Text_Contents, "");
  set(obj, MUIA_Application_Sleep, TRUE);
  result = kernel_measureFFT(fft_size, RESOLUTION_DEFAULT, d->R);
  NewRawDoFmt("%.2f MFlops (N = %ld)", RAWFMTFUNC_STRING, d->Buf, result, fft_size);
  set(findobj(OBJ_RESULT_FFT, obj), MUIA_Text_Contents, d->Buf);
  set(obj, MUIA_Application_Sleep, FALSE);
  set(findobj(OBJ_STATUS_BAR, obj), MUIA_Gauge_InfoText, "Ready.");
  return 0;
}

Le code utilise la recherche dynamique dans l'arbre des objets pour accéder aux objets MUI.

La méthode définit d'abord la taille des données de référence, sur la base de la variable "switch" d->LargeData. Cette variable est définie avec l'attribut APPA_LargeData, qui à son tour est lié au bouton "Large Data" via une notification. Ensuite, la progression de la barre d'état est effacée et un texte est défini pour informer l'utilisateur de ce qui est fait. Le champ de texte du résultat du test est également effacé.

L'étape suivante consiste à mettre l'application dans l'état "occupé". Cela devrait toujours être fait, par exemple, lorsque l'application ne répond pas à l'entrée de l'utilisateur pendant plus d'une demi-seconde. En mettant MUIA_Application_Sleep à TRUE (vrai), on verrouille l'interface graphique et on affiche le pointeur de souris occupé lorsque la fenêtre de l'application est active. Bien sûr, décharger les tâches intensives du processeur vers un sous-processus est une meilleure solution dans les cas généraux, mais pour un test de performances, cela n'a pas beaucoup de sens. L'utilisateur doit de toute façon attendre le résultat du test avant de faire quoi que ce soit d'autre, comme lancer un autre test. Le seul problème d'utilisation est qu'un test ne peut pas être arrêté avant sa fin. Laissons cela tel quel pour l'instant, pour un test de performances, où l'ordinateur est censé utiliser toute sa puissance de calcul, quelques secondes d'absence de réponse de l'interface graphique ne sont pas un si gros problème.

La ligne de code suivante exécute le test, en appelant la fonction kernel_measureFFT() du code originale de SciMark. Une fois le test terminé, le résultat est formaté et affiché dans le champ de résultat à l'aide de NewRawDoFmt(), qui est une fonction de formatage de chaîne de bas niveau d'exec.library et qui, avec la constante RAWFMTFUNC_STRING, fonctionne exactement comme sprintf(). Elle utilise un tampon fixe de 128 caractères (ce qui est beaucoup plus que nécessaire, mais ajoute une marge de sécurité) situé dans les données de l'instance de l'objet. La méthode se termine par la désactivation de l'application et l'affichage du texte "Ready" dans la barre d'état.

Le code de la méthode APPM_AllBenchmarks() est plus long, il n'est donc pas répété ici. La méthode est de toute façon très similaire à la méthode du test individuel. La différence est qu'elle exécute les cinq tests en accumulant leurs résultats dans un tableau. Elle met également à jour la barre de progression après chaque test. Enfin, elle calcule un score moyen et l'affiche.

9.6 Le portage final

Voici le code source complet du portage de SciMark2 MUI : http://krashan.ppa.pl/morphzone_tutorials/scimark2_mui.lha.

Le programme peut être compilé en lançant "make" dans le répertoire source.

10. Techniques utiles

10.1 Localisation d'objets dans l'arbre des objets

Après la création de l'arbre d'objets complet, il n'y a pas d'accès direct à aucun objet, sauf à l'objet principal de l'application. Il faut donc trouver un moyen d'accéder aux autres objets. Il y a plusieurs façons de le faire :
  • Stocker les pointeurs vers les objets dans des variables globales. C'est la méthode la plus simple et elle peut fonctionner correctement dans des projets simples. L'inconvénient est qu'elle brise les principes de conception orientée objet (comme l'encapsulation des données) et crée un désordre lorsque le nombre de variables globales atteint 50, 100 ou plus.

  • Stocker les pointeurs dans les champs des données d'une instance de sous-classe (par exemple l'instance de l'application). Une bonne idée, mais un peu fastidieuse à mettre en oeuvre. La zone de données d'instance d'un objet n'existe pas jusqu'à ce que l'objet soit créé (pour être précis - jusqu'à ce que le constructeur de la classe racine soit exécuté) et l'objet Application est créé en dernier. Ensuite, les pointeurs vers les sous-objets doivent être stockés dans des variables temporaires. Cette technique nécessite également que l'objet parent de l'objet mis en cache soit une instance d'une classe personnalisée (sous-classée) et que le parent crée ses sous-objets dans le constructeur, ce qui n'est pas toujours vrai.

  • Utilisez l'attribut MUIA_UserData et la méthode MUIM_FindUData() pour trouver des objets dynamiquement. C'est la meilleure solution lorsque l'on accède rarement aux objets (par exemple une fois, juste pour définir les notifications). Pour les objets que l'on accède fréquemment (disons plusieurs fois par seconde), elle peut être combinée avec la mise en cache des pointeurs d'objets dans les données d'instance d'un objet sous-classé.
La dernière approche fonctionne comme suit : chaque objet à rechercher dispose de l'attribut MUIA_UserData défini à une valeur unique prédéfinie. Ensuite, à tout moment, l'objet peut être trouvé par cette valeur en utilisant la méthode MUIM_FindUData() sur un objet parent direct ou indirect, par exemple sur l'objet maître Application.

#define OBJ_SOME_BUTTON 36

/* Somewhere in the initial tags for the button */
MUIA_UserData, OBJ_SOME_BUTTON,
/* ... */

/* Let's get the pointer to the button now */

Object *some_button;

some_button = (Object*)DoMethod(App, MUIM_FindUData, OBJ_SOME_BUTTON);

Cette opération est si courante qu'elle est généralement encapsulée dans une macro :

#define findobj(id, parent) (Object*)DoMethod(parent, MUIM_FindUData, id)

some_button = findobj(OBJ_SOME_BUTTON, App);

La macro peut bien sûr être utilisée directement dans d'autres fonctions, comme dans cet exemple pour changer l'étiquette du bouton :

set(findobj(OBJ_SOME_BUTTON, App), MUIA_Text_Contents, "Press Me");

Notez que la macro findobj() n'est pas définie dans les en-têtes MUI du système, elle doit donc être définie dans le code de l'application.

10.2 Classe de texte : boutons, champs de texte, étiquettes

10.2.1 Introduction

La classe Text est la plus couramment utilisée pour créer des gadgets. En effet, elle permet non seulement de créer des étiquettes simples (également appelées "texte statique" dans d'autres moteurs d'interface graphique), mais aussi des gadgets textuels en lecture seule encadrés et des boutons textuels. En fait, MUI n'a pas de classe spéciale pour les boutons, un bouton est simplement un objet Text avec un cadre et un fond appropriés et une entrée utilisateur activée. Cette polyvalence peut avoir l'inconvénient de permettre la création d'interfaces non conformes au guide de style.

La classe Text utilise le moteur de rendu MUI Text pour la sortie du texte. Elle permet d'afficher du texte multiligne, d'utiliser des styles (gras, italique), des couleurs et d'intégrer des images. Ces fonctionnalités doivent être utilisées avec parcimonie afin de préserver la cohérence et le confort d'utilisation des interfaces utilisateur. Le moteur de rendu est contrôlé par l'insertion de séquences d'échappement dans le texte. Une autre fonctionnalité du moteur est l'alignement du texte à l'intérieur du rectangle de l'objet (gauche, droite, centré).

10.2.2 Attributs communs
  • MUIA_Background et MUIA_Frame sont des attributs hérités de la classe Area. Ils spécifient l'arrière-plan et le cadre utilisés pour un objet.
  • MUIA_Text_Contents spécifie le texte. Il peut contenir des séquences d'échappement du moteur de formatage. Le texte est mis en mémoire tampon en interne, donc la valeur de cet attribut peut pointer vers une variable locale ou un tampon alloué dynamiquement.
  • MUIA_Text_PreParse peut être considéré comme un préfixe fixe, qui est toujours ajouté au début du texte avant le rendu. Il est généralement utilisé pour l'application de styles et de formatages constants, par exemple en lui donnant la valeur "\33c", le texte sera toujours centré. Cet attribut simplifie la gestion du texte lorsque le texte affiché n'est pas constant (par exemple, il est chargé à partir d'un catalogue de paramètres linguistiques).
  • MUIA_Font est un autre attribut hérité de la classe Area et spécifie une police à utiliser pour le rendu du texte. Dans la plupart des cas, il s'agit d'une des polices prédéfinies par l'utilisateur dans les paramètres MUI.
Pour plus d'attributs et des descriptions détaillées, reportez-vous à l'autodoc de la classe Text dans le SDK.

10.2.3 Étiquettes

Les étiquettes ("labels") sont la forme la plus simple des instances Text. Elles n'ont pas de cadre et héritent leur arrière-plan de l'objet parent (ni MUIA_Frame ni MUIA_Background ne sont spécifiés). Elles utilisent la police MUI standard dans la plupart des cas, donc MUIA_Font n'a pas besoin d'être spécifié non plus. Un détail important (et souvent oublié) est l'alignement correct de la ligne de base du texte lorsqu'une étiquette est utilisée avec un gadget encadré contenant également du texte (un gadget String par exemple). Le comportement par défaut de MUI pour le positionnement vertical du texte est de le centrer. Si le gadget encadré utilise un remplissage vertical inégal, les lignes de base de l'étiquette et du gadget peuvent ne pas être alignées. Un attribut spécial MUIA_FramePhantomHoriz résout ce problème. Il est spécifié pour une étiquette avec une valeur TRUE (vrai). L'étiquette a également MUIA_Frame spécifié avec le même type de cadre que le gadget. L'étiquette obtient alors un cadre invisible de ce type (des parties horizontales de cadre pour être exact, d'où le nom de l'attribut) et le texte est disposé en conséquence. Par conséquent, le texte de l'étiquette et celui du gadget sont toujours alignés verticalement, en supposant qu'ils utilisent la même police.

MUI étiquette

La capture d'écran agrandie ci-dessus illustre le problème d'alignement de l'étiquette. Le gadget String utilise un remplissage inégal (le remplissage supérieur est de deux pixels, le remplissage inférieur est de zéro pixel), ce qui entraîne un désalignement de la ligne de base du texte de un pixel, comme indiqué sur la gauche. L'étiquette de droite a été définie avec deux balises supplémentaires :

MUIA_FramePhantomHoriz,  TRUE,
MUIA_Frame,              MUIV_Frame_String,

Une étiquette peut avoir un caractère souligné avec l'attribut MUIA_Text_HiChar. Il est utilisé pour créer un indice visuel pour une touche de raccourci d'un gadget étiqueté. Voir la section "10.2.5 Boutons" ci-dessous pour plus de détails.

10.2.4 Champs de texte

Un champ de texte ("textfield") est une zone encadrée en lecture seule affichant un texte (généralement modifié au moment de l'exécution). La différence entre une étiquette et un champ de texte est que ce dernier a un cadre et un fond spécifiés :

MUIA_Frame,             MUIV_Frame_Text,
MUIA_Background,        MUII_TextBack,

10.2.5 Boutons

Un bouton de texte est aussi une instance de la classe Text. Il possède cependant plus d'attributs qu'une simple étiquette, car il gère les entrées de l'utilisateur. MUI a un arrière-plan et un cadre prédéfinis pour les boutons :

MUIA_Frame,             MUIV_Frame_Button,
MUIA_Background,        MUII_ButtonBack,

Ces attributs spécifient également un cadre et un arrière-plan pour l'état "pressé". MUI a également des paramètres de police séparés pour les boutons. Oublier l'attribut MUIA_Font pour les boutons est l'une des erreurs les plus courantes dans la conception de MUI.

MUIA_Font,              MUIV_Font_Button,

De nombreux utilisateurs (et programmeurs) se contentent de définir la police par défaut pour les boutons, de sorte que le bogue n'est pas visible. Il est recommandé de toujours tester une interface graphique avec des paramètres de police inhabituels pour les boutons, afin que le problème soit facilement visible. Le texte des boutons est généralement centré, ce qui peut être fait soit en insérant la séquence "\33c" au début du libellé du bouton, soit en utilisant MUIA_Text_PreParse.

MUIA_Text_PreParse,     "\33c",

Après la définition de l'apparence du bouton, il est temps de traiter l'entrée de l'utilisateur. Le comportement du bouton est défini par l'attribut MUIA_InputMode avec trois valeurs :
  • MUIV_InputMode_None : la valeur par défaut, le bouton ne réagit à rien.
  • MUIV_InputMode_RelVerify : un simple bouton activé par un clic gauche de la souris.
  • MUIV_InputMode_Toggle : un bouton à deux états, un clic l'active, un autre le désactive.
Un autre bogue commun aux boutons MUI est d'omettre la gestion du clavier. La souris n'est pas tout. La première étape, obligatoire, est d'entrer le bouton dans la chaîne de cycles de touche de tabulation de la fenêtre :

MUIA_CycleChain,        TRUE,

Tout gadget entré dans la chaîne peut être sélectionné en appuyant sur la touche tabulation (pour les paramètres par défaut du clavier MUI). L'objet sélectionné est entouré d'un cadre spécial. Il peut ensuite être activé par une touche définie dans les préférences de MUI. Pour les boutons, la touche d'activation par défaut est la touche "Retour". Une règle de base pour le chaînage des cycles :

Chaque gadget acceptant une entrée utilisateur doit être ajouté à la chaîne de cycle de tabulation.

Une autre fonctionnalité de gestion du clavier fournie par MUI est celle des racourcis clavier. Une touche de raccourci active simplement un bouton qui lui est associé. Les touches de raccourcis ont les caractéristiques suivantes :
  • MUI fournit une indication visuelle d'une touche de raccourci dans un bouton en soulignant la lettre de la touche de raccourci dans l'étiquette du bouton. Cela implique que la lettre du raccourci doit exister dans l'étiquette.
  • Il y a un retour visuel pour l'utilisation d'une touche de raccourci, le bouton est pressé comme s'il avait été cliqué avec la souris.
Il n'est pas nécessaire d'attribuer un raccourci à chaque bouton d'une interface graphique. La meilleure pratique consiste à n'attribuer des raccourcis que pour les boutons les plus utilisés, surtout s'il y a de nombreux boutons dans une fenêtre. Une touche de raccourci est définie avec deux attributs :
  • MUIA_Text_HiChar : cet attribut spécifie une lettre à souligner dans l'étiquette. Elle est insensible à la casse.
  • MUIA_ControlChar : cet attribut spécifie une touche de raccourci. Bien sûr, elle doit être la même que la précédente. Il doit s'agir d'une lettre minuscule, car les majuscules obligent l'utilisateur à appuyer sur "Shift", ce qui rend le raccourci moins confortable à utiliser. Il n'y a pas non plus d'indication visuelle pour l'exigence "Shift". Les chiffres peuvent également être utilisés comme touches de raccourci si l'étiquette les contient. L'utilisation de la ponctuation et d'autres caractères doit être évitée. Un exemple d'utilisation :
MUIA_Text_Contents,     "Destroy All",
MUIA_Text_HiChar,       'a',
MUIA_Text_ControlChar,  'a'

Notez que ces attributs prennent un seul caractère, et non une chaîne.


[Retour en haut] / [Retour aux articles]