|
|||||||||||||||||||||||||||||||||||||||||||||||
|
Avec l'article de ce mois-ci, on termine la partie la plus pénible de cette série. En effet, après avoir parlé de la gestion des messages dans cet article, nous aurons toutes les bases nécessaires pour attaquer le développement de vraies applications. Les messages Bon, les messages. D'abord, il s'agit de démystifier un peu la chose. Contrairement à ce que beaucoup de débutants pensent, surtout s'ils développent aussi sous m$-Windows, ça n'a rien de vraiment compliqué. Eh oui, le système de "guillaume porte" use et abuse d'un mécanisme similaire, mais avec bien évidemment beaucoup plus de lourdeur. Mais revenons à notre Amiga préféré... Prenons le listing suivant :
Au début, on trouve les habituelles #includes. Je vous laisse les décortiquer vous-même : vous vous y reconnaîtrez facilement, car nous les avions déjà vues. Il n'y a rien de nouveau depuis le dernier article. On trouve juste derrière les initialisations. On oublie les définitions des gadgets pour le moment, ce n'est pas le sujet de cet article et on y consacrera un autre : il y a tellement à dire ! Pour le moment, utilisez ces définitions telles quelles... En ce qui concerne la déclaration des fenêtres, depuis le mois dernier, nous sommes maintenant en terrain connu. Remarquez simplement que nous avons renseigné le fameux "IDCMPFlags". C'est dans ce champ que nous devons indiquer au système les événements que nous attendons. Dans notre cas, il n'y en a que deux :
Dans notre exemple, nous n'utilisons que des boutons de type booléen (ils sont relâchés dès que l'utilisateur arrête d'appuyer dessus), donc une seule méthode est nécessaire. On utilise les deux simultanément lorsque l'on doit gérer des boutons de type "toggle" qui nécessite un deuxième clic pour être relâché. On recevra donc un GADGETDOWN lors du premier clic et un GADGETUP lors du second. Il existe encore bien d'autres drapeaux que nous aurions pu utiliser :
Les drapeaux suivants sont apparus avec le Kickstart 2.0 (version == 36) et sont donc ignorés par les systèmes 1.x :
Avant de rentrer plus en détail dans le sujet, juste un petit aparté sur les ????VERIFY. Ces messages nous arrivent lorsque quelque chose doit altérer notre fenêtre (par exemple, l'ouverture d'une requête ou d'un menu). Intuition attendra que nous répondions à ce message avant de continuer. C'est utile, par exemple, si l'on doit sauvegarder le contenu de notre fenêtre. Il faut éviter qu'un message ????VERIFY reste en attente pendant un long moment. Proscrire aussi les appels au DOS. Par exemple, nous sommes en train de traiter un REQVERIFY (mais ce serait pareil avec un SIZEVERIFY ou un MENUVERIFY) et nous voulons ouvrir un fichier. Malheureusement, il se trouve sur un volume qui n'est pas encore "monté". Le système devrait donc nous afficher une requête "Veuillez insérer le volume ???? dans un lecteur". Oui, mais voilà, il ne peut afficher quoi que ce soit tant que nous n'avons pas répondu au message... DeadLock ! On attend la réponse à une requête qui ne peut s'afficher et qui ne viendra donc... jamais ! Problème de notation L'exemple qui accompagne cet article doit pouvoir se compiler sur n'importe quel système. La notation utilisée est donc celle du 1.3. Avec le 2.0, Commodore avait décidé de modifier les noms des objets définis dans ses includes ; ceci afin d'éviter l'utilisation de noms génériques qui pouvaient être écrasés par d'autres noms de l'application. C'est ainsi que le nom des drapeaux d'activation des boutons gagne le préfixe GACT_ (donc RELVERIFY devient GACT_RELVERIFY). On notera le cas particulier de GADGIMMEDIATE (transformé en GACT_IMMEDIATE) alors que les drapeaux IDCMP ont pour préfixe... IDCMP_ (GADGETDOWN devient IDCMP_GADGETDOWN). A noter que, comme d'habitude, les ingénieurs d'Amiga ont bien fait les choses : les includes utilisent automatiquement des fichiers de compatibilité, ce qui fait qu'un source prévu pour le 1.3 se compilera sans problème sous 2.0 ou plus ! Y en a qui devraient en prendre de la graine, suivez mon regard ! Bon, revenons au code ! Tout au début de la fonction main(), on trouve l'habituel "atexit()" qui nous assure que la fonction "menage()" sera appelée à la fin de l'exécution de notre programme et donc que tout ce que nous avons ouvert sera refermé. On commence bien évidemment par ouvrir les bibliothèques, puis les fenêtres. Vient ensuite la boucle principale du programme, on y reste tant que l'une au moins des fenêtres reste ouverte. Maintenant, nous arrivons à la partie la plus intéressante : la gestion des messages. La méthode bourrin serait de faire une boucle sans fin : si un message est en attente, on le traite, sinon on boucle... C'est ce qui fut employé avec l'infâme AmigaBasic bien connu des utilisateurs de machines sous Workbench 1.x : tout le processeur est utilisé, que notre programme travaille ou non. C'est propre n'est-ce pas ? Si tout le monde faisait comme ça, il faudrait au moins un Pentium à 450 MHz pour faire tourner un simple traitement de texte (toute ressemblance avec un environnement plein de fenêtres existant ne serait que fortuite). Bon, nous ne sommes pas chez Microsoft (eh oui, c'est eux qui ont pondu cette daube ! Une cochonnerie de plus à leur actif), on va faire propre ! Si vous avez déjà utilisé un gestionnaire de tâche ("task manager"), vous aurez remarqué que les programmes lancés sur le système sont classés en trois catégories :
Si un programme n'a rien à faire... eh bien, qu'il ne fasse rien ! Cette lapalissade est mise en pratique grâce à la fonction Wait(). Elle va placer notre programme en "waiting" jusqu'à l'arrivée d'un des signaux activés dans le masque passé en paramètre. Pour recevoir nos messages, Intuition a créé automatiquement un port à chacune de nos fenêtres. A chaque port est associé un signal qui sera dressé lorsqu'un message arrive sur le port. Notez bien : le signal est mis lorsqu'un message arrive. C'est très important et on en reparlera tout à l'heure... On devine tout de suite une limitation : le nombre de signaux disponibles pour une tâche est forcément limité. Ils sont stockés dans un entier long donc il n'y en que 32 de disponibles par tâche... mais 16 sont déjà alloués par le système. Ces signaux sont aussi utilisés lorsque l'on communique avec une autre tâche et donc par extension à un périphérique logique. 16 possibilités de connexion, c'est quand même un peu limité me direz-vous. Eh bien non ! Petite astuce : il est aussi possible de ne pas créer de port de message avec la fenêtre. Vous trouverez la méthode à suivre dans les autodocs, mais voici, en résumé, comment procéder : il faut d'abord créer la fenêtre sans drapeau IDCMP, ainsi aucun port n'est créé avec la fenêtre. Ensuite, vous devez associer manuellement un port à la fenêtre et activer les drapeaux des événements attendus par un ModifyIDCMP() (voir l'autodoc de cette fonction pour plus de détails). Lors de la fermeture de la fenêtre, on commence par remettre le window.UserPort à NULL, puis on ferme la fenêtre par un CloseWindow(). En regroupant ainsi le port, on peut utiliser autant de fenêtres que l'on veut ; enfin, tant qu'il y a de mémoire disponible. Wait() prend comme paramètres les signaux que nous attendons et renvoie le ou les signaux activés. Notons au passage qu'elle remet ces signaux à zéro. Grâce au "sig = SIGBREAKB_CTRL_C", nous indiquons que nous souhaitons être avertis d'un Contrôle-C. Nous le testons par la ligne :
Attention : certains compilateurs tel que le SAS ont des mécanismes internes pour tester le CTRL_C. Il faudra donc les désactiver sinon notre programme sera arrêté avant même que nous n'apercevions ce signal. Pour GCC, c'est un peu le même problème, puisque là, l'ixemul.library intercepte ce signal pour simuler l'action qu'il y aurait sous Unix. Bref, quel que soit votre compilateur, je vous conseille de jeter un oeil sur sa documentation... Les lignes...
...activent les bits correspondant à chacune des fenêtres... en ayant bien pris soin de vérifier qu'elle est ouverte. On remarque que le champ mp_SigBit du port contient le numéro du port utilisé, d'où le décalage à gauche. Avant d'arriver sur le Wait(), sig a donc les bits actifs pour la valeur correspondant au Ctrl-C, ainsi que ceux des fenêtres ouvertes. On trouve donc ensuite notre fameux Wait(), l'exécution s'arrête jusqu'à ce qu'un des signaux soit activé. Suivent les boucles de lecture des messages. Nous aurions pu utiliser la même méthode qu'avec le break, c'est-à-dire tester si le signal correspondant à chaque fenêtre est activé, ce qui aurait donné :
(les petits futés remarqueront que j'ai simplifié volontairement les tests pour rendre ce code plus clair, mais... réveil du Guru garanti si vous l'utilisez tel quel !) Ce code est simple mais ne fonctionnera pas. Eh oui, comme je vous l'ai dit, les signaux ne sont mis que lorsqu'un message arrive. Imaginons qu'un des traitements prenne plusieurs secondes. Pendant ce temps, l'utilisateur peut cliquer sur des boutons ou fermer une fenêtre. Lorsque le traitement est terminé, on revient au Wait(), qui n'arrête rien du tout vu que des messages sont arrivés depuis le dernier passage (ceux qui ont été créés par l'utilisateur pendant le traitement). Le premier message est traité et retour sur le Wait(). Ce coup-ci, notre programme est arrêté ! Eh bien oui, le Wait() précédent a remis les signaux à zéro, et il faut attendre que des nouveaux messages arrivent avant que notre tâche revienne en READY, et ce même s'il y a d'autres messages dans les queues ! Ici, la solution correspond à créer une boucle do-while dans laquelle on reste tant qu'il y a des messages (utilisation du booléen "encore"). Reste maintenant à étudier ces messages : la fonction GetMsg() renvoie un pointeur sur le premier message en attente, ou NULL s'il n'y en a pas. Regardons de plus près les champs contenus dans les messages. En premier, on trouve évidemment une structure message : les IntuiMessage sont des extensions des ExecMessage, seule structure que les fonctions de messages savent gérer. Lorsque nous aborderons ARexx, nous verrons que les messages échangés sont aussi des excroissances des ExecMessage. Le champ "Class" contient le code IDCMP du message. Dans notre exemple, ce champ est traité par un switch/case classique. Les champs "Code" et "Qualifier" dépendent du type de message. Pour un VANILLAKEY par exemple, "Code" contient le code ASCII de la touche activée alors que "Qualifier" contiendra les codes de qualificateurs activés (si par exemple Ctrl, Alt ou une touche Amiga est appuyée). "IAddress" contient un pointeur qui dépend lui aussi de la classe du message. Dans notre cas, il contient l'adresse du gadget qui a été cliqué. "MouseX" et "MouseY" contiennent les coordonnées de la souris au moment de l'événement. Ils sont relatifs au coin supérieur gauche. Si le drapeau IDCMP_DELTAMOVE est mis, ces coordonnées sont relatives à la différence par rapport au dernier message envoyé à cette fenêtre. "Seconds" et "Micros" sont des copies de l'horloge système au moment où le message a été envoyé. Enfin, "IDCMPWindow" contient le pointeur vers notre fenêtre. Voilà, comme vous le voyez, ces messages sont assez simples mais complets. Ils sont très faciles à utiliser. Une fois que nous en avons fini avec eux, il faut le signifier au système pour qu'il les libère ou qu'il les utilise à d'autres fins. C'est fait grâce à la fonction ReplyMsg() qui prend en argument l'adresse du message. Comme dit précédemment, ReplyMsg() conditionne le bon fonctionnement du système (par exemple, pour éviter un DeadLock pour les messages Verify????), et est obligatoire pour tous les messages que nous recevons ! Attention : il faut répondre le plus rapidement possible, pour éviter de monopoliser les ressources du système, mais parallèlement, il ne faut pas utiliser les données d'un message après y avoir répondu. En effet, répondre à un message indique au système qu'il peut en faire ce qu'il veut, donc les données qu'il contient peuvent (et seront sans doute) altérer si vous tentez d'y accéder après un ReplyMsg(). Une fois que toutes les fenêtres sont fermées, ou que l'utilisateur a fait un Ctrl-C, on sort de la boucle de traitement des messages et notre programme se termine... après être passé par menage() grâce au "atexit()" du début. Certains pourraient s'étonner que l'on ferme violemment les fenêtres par un CloseWindow(). Peut-être y a-t-il des messages en attente ? Il faudrait sans doute les effacer avant de fermer les fenêtres ? Pas la peine, les ingénieurs d'Amiga y ont pensé avant nous et le CloseWindow() libère automatiquement tous les messages qui restent dans les queues... Haaaa, qu'ils sont forts ! Voilà, j'espère que vous avez réussi à tenir jusqu'au bout de cet article. C'est vraiment le point le plus important de la programmation sur Amiga, car même si vous n'utilisez pas d'interface graphique pour vos programmes, vous utiliserez une méthode similaire dès que vous souhaiterez faire communiquer plusieurs tâches, et ceci inclut aussi les périphériques logiques et donc les entrées/sorties... C'en est fini des cours barbants ! La prochaine fois, on commencera vraiment à coder utile, en posant les bases d'un jeu !
|