> Théorie
PrograZine issue #10 - http://www.citeweb.net/discase/10/windows.htm Edito Sommaire Contribution Contacts


Le multitâches sous Windows95/98
par Plouf - plouf@win.be http://www.citeweb.net/pmplayer
Initié Win9x Pascal/Delphi

    Ecrit pour Prograzine la nuit du 22 au 23 octobre 1999 :)

    Note : pour ceux qui auraient lu mes autres tuts et qui ralent bicôze l'adresse mail ne marche pas, c'est normal parce qu'elle n'est plus bonne, repérez la nouvelle à la place.

    Ce tutoriel se base sur le compilateur Delphi, mais est facilement transposable sur n'importe quel autre language, sauf bien sur en MsC++ où il est un peu sans objet vu qu'il n'y a normalement pas moyen de lancer un prog sans connaître tout ce qui va suivre...

    Une grande particularité qui passe souvent inaperçue, c'est que d'un point de vue multitâche, win95 n'a vraiment rien à voir avec win3.1. En effet, sous win3.1, il fallait dire explicitement au système que l'on désirait lui rendre un moment la main, tandis que sous win95, ce dernier ne vous pose même pas la question : il la prend...

    Ce qui fait que l'application ne doit pas continuellement rapeller l'os pour maintenir ce dernier dans un état correct. En d'autres termes, ça n'est pas parce que vous excécutez une boucle à vide que win plante. D'ailleurs, en général, il plante pour bien moins que ça :)

    C'est bien beau, cela nous permet en fait d'ignorer le système, et d'agir exactement comme si il n'était pas là. Evidement, ignorer Windows signifie également ignorer l'utilisateur qui a lancé votre programme, car ce n'est pas l'application qui se charge de gérer le clavier ou la souris, mais c'est l'OS, d'ailleurs c'est bien fait pour lui : c'est son rôle.

    Bref, vous pouvez choisir de l'ignorer, mais votre application sera, disons, peut conviviale. Sinon, il va falloir travailler avec lui. En général, vous lui donnez des ordres (ouvre une fenêtre...), et lui vous donne des messages (la fenêtre est ouverte...)

    Lui donner des ordres, c'est assez simple, il suffit d'appeler l'api correspondant, avec les paramètres bien comme il faut. Cela suppose évidemment que vous connaissiez le nom de cette api, donc la dll qui la contient ainsi que les paramètres à envoyer. Pour cela, pas de mystère, documentation, références et aide en ligne ne seront pas de trop.

    Pour les messages, c'est plus comique...
>L'expéditeur, la poste et le destinataire

    Imaginons, par exemple, que l'utilisateur clique avec sa souris sur une de vos fenêtres, que vous avez ouverte avec une belle api (ou que le compilateur à ouvert pour vous, en appelant la même api). Windows vous envoie un message vers une sorte de boite au lettre, dans une zone mémoire entre lui et vous. En fait, il met ce message dans une file d'attente, car d'autres messages sont peut-être arrivés avant ce click.

    Vous, car vous êtes un bon programmeur, vous allez régulièrement vérifier le contenu de cette boite au lettre, en prenant le message le plus ancien (selon un système FIFO : first in, first out). Vous regardez ce que dit ce message, vous agissez en conséquence, et vous prennez le message suivant, agissant en conséquence, etc etc...

    A un moment, vous lirez "l'utilisateur a pressé le bouton gauche de la souris sur la fenêtre untelle". Il y a fort à parier que quelque part dans le programme, se trouve la routine qui doit recevoir ce genre de message, une sorte de routine Gere_La_Souris. Il est clair en effet que tout caser dans le programme principal serait une erreur, nous sommes à l'ère de la programmation modulaire, voyons voyons...

    Donc, en somme, dans la boucle principale de votre programme, vous servez de relai aux messages venant de Windows, les retransferant à qui de droit (ici, envoyons le message à la routine de gestion de la souris relative à la fenêtre untelle, chaque fenêtre devrait avoir sa gestion de souris propre).

    En fait, c'est même encore plus gag, car comme on module, on module, et on module encore, on n'envoie pas directement le message à la routine souris de la fenêtre, on envoie le message "bouton pressé" à une certaine routine "fenetre untelle", qui, elle, va se charger de redistribuer le message à sa propre routine souris.

    C'est pratique ce genre de gestion, car quand on a une vingtaine de fenêtre, ça facilite la lecture du programme, chaque module se chargant de redistribuer son propre courier, un peu comme des burreaux de triages de plus en plus petits jusqu'au destinataire final.
                             Windows
                                |
                                |
    Fenêtre----------Votre programme principal--------Fenêtre
    |  |  |                     |                     |  |  |
  Clavier,souris...          Fenêtre               Clavier, souris...
                             |  |  |
                         Clavier, souris...
l

    Remarquons au passage que le programme principal ne doit pas néccessairement redistribuer le message aux fenêtre, si ce message ne concerne que lui, du genre un message de Quit, comme une fenêtre ne doit pas forcément redistribuer à des sous-contrôles, si ce qu'elle doit faire ne concerne qu'elle : Fermer...
>Le pony express

    Nous avons donc vu que Windows envoie ses messages dans une file d'attente, d'où notre programme va les rechercher. Mais que se passerait-il si il devait envoyer un message précis à une fenêtre, un message prioritaire qui n'a pas le temps de passer à travers toute cette file d'attente? Et bien il ne se gênerait pas, il l'enverrait directement à la fenêtre interessée, qui redistribuerait etc...

    Windows fait donc la distinction entre deux types de messages : les messages qui passent par la file d'attente (la plupart), et ceux qui arrivent directement aux fenêtres. Pour utiliser les termes consacrés, on parlera de "Queued Messages" et de "Nonqueued Messages", qui sont respectivement les messages normaux et les messages express qui ne passent pas par la file d'attente.
>Le callback

    Pour la boucle principale, sa programmation ne fait pas beaucoup de mystère. On tourne en rond en allant rechercher (via une api, forcément) le message le plus ancient, on le traite (en fait on ne fait rien du tout car une api fait tout pour nous, c'est à se demander a quoi sert encore notre boucle principale), et on boucle. On sortira de la boucle une fois qu'on aura recu le message "c'est fini", ce qui entrainera la fin du programme.

    Mais parlons-en, du traitement de ces messages. Car pour appeller une routine, il faut savoir où elle est, et donc connaitre son addresse en mémoire. C'est pourquoi quand on ouvre une fenêtre, il faut communiquer à Windows une adresse (un pointeur, donc) qui contient l'adresse de la routine (une fonction) associée à cette fenêtre. (bon, je simplifie un peu mais on ne va pas faire de chichi, hein? :)) Windows utilise cette adresse pour ses messages express, et également pour les messages normaux si la boucle principale lui demande de s'occuper lui-même de la distribution.

    Et en fait, ce qui se passe à l'échelle du programme, se passe également à l'échelle de la fenêtre. Chaque message (ou presque) possède une routine, et donc a une adresse (un pointeur) qui lui est associé. C'est toujours comme cela que l'on fait, on ne fait pas des conditions dans tous les sens, on sait qu'au message numéro untel correspond la routine pointée par l'addresse untelle, ces pointeurs se trouvant dans des tableaux, et roulez jeunesse.

    Et c'est excactement ce que font les compilateurs autres que MsC++, avec ces procédures associées à des Events qui semblent s'appeler toutes seules. Vous connaissez à présent tout le mécanisme qui à précédé cet appel qui n'a rien de magique. Et à présent que vous le conaissez, vous allez pouvoir le contrôler...
>Un peu de concret

    En général, vous devriez laisser la gestion de la boucle principale à votre compilateur préféré, car il fait bien ça et c'est plus pratique. Si je prends un cas concret en Delphi, le programme principal se résume souvent à :
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
l

    Il faut savoir que la boucle principale décrite plus haut réside en fait dans Application.Run. Si vous voulez reprogrammer la boucle principale, il vous faut éviter d'appeler Application.Run et vous taper toute la gestion des messages.

    Sauf que bien sur, Delphi est malin et a déjà prévu le coup, on peut faire très facilement :
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Fin:=False;
  Repeat
    Application.ProcessMessages;
  Until Fin;
l

    où Fin serait un booléen qui serait modifié dans un des Events de Form1 (par exemple : OnCloseQuery)

    Tant qu'on y est pour les précisions : Application.ProcessMessages va lire tous les messages de la "boite au lettre" jusqu'à ce qu'elle soit vide, alors que Application.HandleMessage ne va en lire qu'un (le plus ancien). C'est à vous de voir ce que vous préférez...

    Application.ProcessMessages (ou l'autre) va donc lire les messages et les redistribuer aux fonctions associées aux fenêtres, ces dernières les redistribuant aux Events définis, du genre MouseDown.

    Si vous désirez reprogrammer entièrement la boucle principale, je vous suggère de vous commenter sur les api GetMessage, TranslateMessage, et DispatchMessage, mais ceci sort du cadre de ce texte.

    Bien, nous progressons, nous sommes à présent capables de rajouter quelque chose dans la boucle principale, pratique quand vous avez besoin d'avoir une routine appelée non stop pour un refresh d'une animation, par exemple. Mais on peut faire encore mieux.
>Détourner la fonction windows

    Donc, vous savez qu'à chaque fenêtre est associée une fonction, que vous pouvez éventuellement reprogrammer. Ce qui cette fois-ci a une réelle utilité, car beaucoup d'utilitaires et gadjets windows envoient des Nonqueued Messages, qui ne sont bien sur pas gérés par le compilateur, et qui sont donc perdus, empechant l'utilisation, par exemple, du Systray.

    D'un autre côté, vous devez être en train de vous dire : "il est fou le mec, il veut qu'on reprogramme toute la fonction windows, et donc se taper les liens vers tous les events de cette fenêtre?". Rassurez-vous, nous allons utiliser un vieux trucs des programmeurs sous Dos : le patchage (whaou le franglais :)) ce qui signifie que nous allons intercepter les messages qui arrivent, si ces messages sont pour nous, nous les gérons, et sinon, nous rapellons l'ancienne routine (qui est toujours quelque part) avec ce message, et le tour est joué, le compilo n'y verra que du feu :)

    La procédure doit avoir (en Delphi) l'en-tête suivante (le nom, on s'en fiche) :
Function WinMsg(Win:HWnd;Msg:Word;wParam,lParam:LongWord):LongWord;StdCall
l

    Win contient le handle de la fenêtre (ce renseignement est inutile, je trouve) Msg contient le message en lui-même wParam et lParam contiennent des paramètres éventuels pour ce messages.

    WinMsg devrait en général être du style :
Function WinMsg(Win:HWnd;Msg:Word;wParam,lParam:LongWord):LongWord;StdCall
  Case Msg Of
    Valeur:Begin
      (*les messages qu'on intercepte*)
    End;
    Else WinMsg:=CallWindowProc(OldMsg,Win,Msg,wParam,lParam);
  End;
End;
l

    La valeur retournée par WinMsg est une sorte de 'réponse' a Windows, en général ce dernier n'en demande pas et on se contente de retourner 0.

    OldMsg se trouve être un pointeur vers l'ancienne routine fenêtre, et CallWindowProc est une api Windows qui permet de faire croire à cette routine qu'elle a été appelée par Windows alors que c'est nous qui l'appelons, hihi :)

    Et comment modifier l'addresse et comment avoir OldMsg? Facile :
OldMsg:=Pointer(SetWindowLong(Form.Handle,GWL_WNDPROC,LongWord(@WinMsg)));
l

     Form.Handle est le handle de la fenêtre dont on désire modifier l'adresse, GWL_WNDPROC signifie que cet appel de SetWindowLong est fait pour modifier cette adresse (car SetWindowLong a bien d'autres utilités), et on passe comme troisième paramètre l'adresse de notre routine à nous, pendant que cette Api nous retourne l'adresse de l'ancienne routine, ce qui est justement ce qu'il nous fallait pour terminer la résolution de notre problème.

    C'est totalement QFD...

    Heu... et à quoi ça sert, m'sieur? Vous le saurez rapidement si vous lisez mon prochain tutoriel : l'utilisation du Systray :)
Cet article est la propriété de Plouf. La copie et la diffusion sont libres sauf dans un but lucratif sans accord explicite de l'auteur.