IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Comment gérer les messages système depuis Delphi

Ce tutoriel explique comment gérer les messages système avec Delphi à travers trois projets.

Article lu   fois.

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Les applications Windows sont du type évènementiel, c'est-à-dire qu'elles attendent patiemment que le système leur transmette des messages et réagissent en conséquence. Un message est un ensemble d'informations qu'un émetteur envoie à un ou plusieurs destinataires pour que ces derniers y réagissent (mais ils sont libres de ne rien faire). Ils peuvent être envoyés par le système (signaler l'appui sur une touche…), par une autre application, ou tout simplement au sein même du processus (typiquement un contrôle génère un nouveau message en réponse à un autre message). C'est donc un mécanisme de communication interprocessus, qui permet de communiquer avec le système, mais aussi avec une autre application, comme nous allons le voir dans cet article.

Pour télécharger les sources de ce tutoriel, cliquez ici ou ici

Pour le tutoriel version PDF : cliquez ici

II. Objectif de ce tutoriel

Apprendre comment intercepter un mouvement de souris, une touche enfoncée au clavier, etc. Mais également apprendre à faire communiquer deux applications distinctes grâce à des messages définis par le programmeur. On ne s'attardera pas sur la théorie, mais plutôt sur la pratique. En fin de chapitre, nous construirons un système dans lequel plusieurs applications échangent des messages, cet exemple nous permettra de découvrir quelques subtilités de l'utilisation des messages système.

III. Structure générale d'un message système

 
Sélectionnez
Type
    Tmsg = packed record
        hwnd: HWND; // Identificateur de la fenêtre destinataire
        message: Uint; // Identificateur du message
        wParam: WPARAM; // Variable contenue dans le message ( Du type pointeur ou entier signé sur 32 bits )
        lParam: LPARAM; // Variable contenue dans le message (Du type pointeur ou entier signé sur 32 bits)
        time: DWORD; // Moment de la création du message
        pt: Tpoint; // Position du pointeur souris à la création du
                    // message
end;

Dans le cadre de ce tutoriel, on n'utilisera que les variables hwnd, message, wParam et Lparam.

IV. Détection d'un évènement clavier

Une manière simple de réaliser cette opération serait d'utiliser la méthode :

 
Sélectionnez
TVotreForm.OnKeyPress(Sender: TObject; var Key: Char)

Dans le code source du composant TForm l'évènement OnKeyPress est en fait géré grâce aux messages système. (Le message wm_char pour être précis.) Dans le cas où on gère la pression sur une touche du clavier grâce au composant VCL TForm, le code ressemblera à ceci :

 
Sélectionnez
procedure TMainFrame.FormKeyPress(Sender: TObject; var Key: Char);
begin
showmessage('Vous avez appuyé sur une touche du clavier');
end;

On peut également afficher la touche pressée grâce à la variable Key. Dans notre cas nous allons utiliser nos propres messages système. (Nous faisons ceci à but didactique.) Avant de nous attaquer au programme, il convient de connaître la structure de l'entité TWMCHAR dont vous trouverez une description générale :

 
Sélectionnez
Type
    TWmChar = packed record
        msg: Cardinal; // Code ( Integer ) du message ( pour le message
                        // WM_CHAR : 258 ), ce code restera constant.
        CharCode: Word; // Code ASCII du caractère frappé ou touche
                        // virtuelle ( EX. VK_CANCEL ).
        Result: Integer; // Valeur renvoyée après réception du message
                        // par l'application traitant le message.
        KeyData: Integer; // La description de l'aide Borland étant
                            // obscure à ce sujet, je ne fais pas de
                            // description sur cette variable.
end;

Voici le code source de l'unité Main disponible dans le fichier en téléchargement :

 
Sélectionnez
unit Main;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TMainFrame = class(TForm)
    Label1: TLabel;
    Label2: TLabel;
  private
    { Déclarations privées }
  procedure WMCHAR(var Msg: TWMCHAR); message WM_CHAR;
  public
    { Déclarations publiques }
  end;

var
  MainFrame: TMainFrame;

implémentation

{$R *.dfm}

procedure TMainFrame.WMCHAR(var Msg: TWMCHAR);
var c: string; // Message figurant dans le Label2 une fois le message reçu
begin
c := 'Message reçu, la touche sur laquelle vous avez appuyé est : ' + chr(Msg.CharCode);
MainFrame.Label2.Caption := c;

end;



end.

Étudions les déclarations une à une :

 
Sélectionnez
private
{ Déclarations privées }
procedure WMCHAR(var Msg: TWMCHAR); message WM_CHAR;

On déclare donc dans la partie private une procédure que l'on nommera en fonction de l'identificateur du message (ici WM_CHAR), mais sans le caractère '_'. On suivra la même règle de nommage pour le type de la variable Msg, en le préfixant par T : TWMChar. On indique le mot réservé « message » suivi de l'identificateur du message ciblé, ici WM_CHAR.
Toutes ces directives sont des conventions d'écriture.
Voici la méthode qui traite les messages 'WM_CHAR' lorsqu’ils surviennent :

 
Sélectionnez
procedure TMainFrame.WMCHAR(var Msg: TWMCHAR);
var c: string;
begin
c := 'Message reçu, la touche sur laquelle vous avez appuyé est : ' +
chr(Msg.CharCode);
MainFrame.Label2.Caption := c;
end;

La déclaration de la procédure dans la partie implémentation est semblable, il y a juste deux points qui diffèrent :
- on ajoute le nom de la fiche précédé du caractère T ;
- on ne doit plus indiquer la directive « message WM_CHAR » après la déclaration de la procédure. (Le compilateur devine de quoi il s'agit.)
La variable Msg.charcode contient le code ASCII du caractère frappé. Pour pouvoir affecter sa valeur à une variable de type string, on utilisera la fonction chr(). Pour rappel, la fonction chr() attend un argument de type integer qui correspond au code ASCII d'un caractère et retourne une valeur de type char. Notez que la variable Msg.charcode ne sera pas toujours valide (en ce sens qu'elle n'aura pas d'existence pour certains messages Windows), cela dépend du type de message qu'on souhaite gérer. Par exemple cette variable n'existera pas pour un message de type WM_MouseMove.

Vous savez maintenant comment détecter si l'utilisateur presse une touche du clavier sans utiliser le gestionnaire d'évènement inclus dans un composant TForm.

V. Communication élémentaire entre applications

Dans ce point important du tutoriel, nous allons apprendre comment faire dialoguer deux applications distinctes à l'aide d'un message système défini par vous-même. Voici le code de l'application qui va envoyer un message vers toutes les autres applications en cours d'exécution :

 
Sélectionnez
unit Appli1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Envoyer: TButton;
    EnvoyeretFermer: TButton;
    procedure EnvoyeretFermerClick(Sender: TObject);
    procedure EnvoyerClick(Sender: TObject);
  private
    { Déclarations privées }
  public
    { Déclarations publiques }
  end;


var
  Form1: TForm1;
  MessageSys: UINT;


implementation

{$R *.dfm}

procedure TForm1.EnvoyerClick(Sender: TObject);
begin
MessageSys := RegisterWindowMessage('MessageUB45'); // On crée un message valide
SendMessage(HWND_BROADCAST,MessageSys, 0, 0); // On envoie ce message aux applications
                                              // qui tournent.

end;

procedure TForm1.EnvoyeretFermerClick(Sender: TObject);
begin
MessageSys := RegisterWindowMessage('MessageUB45');
SendMessage(HWND_BROADCAST,MessageSys, 1, 0);
end;

end.

Étudions la première procédure (méthode en toute rigueur) :

 
Sélectionnez
procedure TForm1.EnvoyerClick(Sender: TObject);
begin
MessageSys := RegisterWindowMessage('MessageUB45');
SendMessage(HWND_BROADCAST,MessageSys, 0, 0);
if MessageSys = 0 then
      begin
      raise Erreur.Create('Erreur lors de l''envoi du message');
      end;
end;

On utilise la variable MessageSys déclarée comme suit :

 
Sélectionnez
MessageSys: UINT;

Cette variable va en fait contenir notre message. La fonction RegisterwindowMessage() permet de créer un message qui pourra être envoyé à toutes les applications (process) en cours d'exécution. L'argument de cette fonction est une variable de type string qui va contenir le « nom » (l'identificateur) de notre message. Cette fonction garantit l'unicité, elle retourne une variable de type integer. Si cette variable vaut 0, la fonction a échoué, pour plus d'informations utilisez l'aide Delphi en recherchant la fonction RegisterwindowMessage. On utilise un nom de message un peu spécial pour réduire les chances qu'un autre programme l'utilise et ainsi on évite de compromettre le bon fonctionnement des applications extérieures aux nôtres. Une fois notre message créé on le diffuse à toutes les applications en cours d'exécution grâce à la fonction SendMessage() ( ou PostMessage() ) définie comme suit :

 
Sélectionnez
function SendMessage(hwnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM) : LRESULT; stdcall;

Si vous désirez en apprendre plus sur les fonctions SendMessage() et Postmessage(), consultez l'aide Delphi.

- hwnd est l'identificateur de la fenêtre concernée ou une valeur spéciale pour un broadcast.
- Msg est l'identificateur du message.
- wParam et lParam sont des données supplémentaires que nous pouvons ajouter avec le message, elles sont des données de type integer.

On passe comme argument pour hwnd HWND_BROADCAST pour faire en sorte que le message soit envoyé à toutes les applications en cours d'exécution.
Étudions maintenant la seconde procédure :

 
Sélectionnez
procedure TForm1.EnvoyeretFermerClick(Sender: TObject);
begin
MessageSys := RegisterWindowMessage('MessageUB45');
SendMessage(HWND_BROADCAST,MessageSys, 1, 0);
if MessageSys = 0 then
      begin
      raise Erreur.Create('Erreur lors de l''envoi du message');
      end;
end;

Elle est pratiquement identique à la première étudiée sauf pour un point : on passe comme argument pour la variable wParam l'entier 1.

Pour comprendre l'intérêt de cette différence, étudions le programme qui va analyser le message ainsi reçu.
Voici le code source du programme récepteur :

 
Sélectionnez
unit Appli2;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm2 = class(TForm)
    Label1: TLabel;
    procedure FormCreate(Sender: TObject);
  private
    { Déclarations privées }
  procedure DefaultHandler(var Msg); override;
  public
    { Déclarations publiques }
  end;


var
  Form2: TForm2;
  MessageSys : UINT; // Message recherché

implementation

{$R *.dfm}

procedure TForm2.DefaultHandler;
begin
inherited DefaultHandler(Msg);
if (TMessage(Msg).Msg=MessageSys) then
  begin
  if (TMessage(Msg).WParam) = 1 then
    begin
    Application.Terminate; // On ferme l'application
    end
    else
    Form2.Label1.Caption := 'Message reçu';
  end;
end;

procedure TForm2.FormCreate(Sender: TObject);
begin
MessageSys := RegisterWindowMessage('MessageUB45');

end;

end.

Pour la réception du message, on utilise la méthode defaulthandler(), qui traite tous les messages qui ont pour destination votre application.
Regardons la procédure Defaulthandler :

 
Sélectionnez
procedure TForm2.DefaultHandler;
begin
inherited DefaultHandler(Msg);
if (TMessage(Msg).Msg=MessageSys) then
    begin
    if (TMessage(Msg).WParam) = 1 then
        begin
        Application.Terminate; // On ferme l'application
        end
        else
        Form2.Label1.Caption := 'Message reçu';
    end;
end;

Vous constatez qu'on mentionne inherited; cela permet une propagation du message au gestionnaire éventuel de l'ancêtre de l'entité en question.
On vérifie si le message reçu est bien le message attendu que l'on a défini lorsque la fiche a été créée.
Si oui, on effectue une seconde vérification visant à connaître la valeur de wParam transmise avec le message, si celle-ci égale 1 on termine l'application.
Vous avez maintenant en main tous les outils pour faire dialoguer vos applications entre elles.

VI. Communication évoluée entre applications

Dans ce chapitre, on va réaliser un système composé de deux applications qui vont communiquer entre elles de manière un peu plus « raffinée » que nos programmes précédents. Cet exemple montre bien l'utilité des messages système et la manière de les utiliser.

 
Sélectionnez
unit Emetteur;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TMain = class(TForm)
    Console: TMemo;
    btnDemarrer: TButton;
    Message: TEdit;
    procedure btnDemarrerClick(Sender: TObject);
  private
    { Déclarations privées }
  public
    { Déclarations publiques }
  procedure DefaultHandler(var Msg); override;
  end;

var
  Main: TMain;
  i : integer = 1; // Variable utilisée pour l'incrémentation des messages console
  HandleEdit : THandle; // Handle du composant TEdit qui contient le message
                        // à afficher
  MonHandle: THandle; // Handle de l'application émettrice
  HandleAppliExt: THandle; // Handle d'une application extérieure
  Message1: UINT; // Premier message envoyé ( En mode broadcast )
  Message2: UINT; // Premier message reçu
  Message3: UINT; // Second message envoyé

implementation

{$R *.dfm}


procedure EcrireConsole(MyText: string); // Procédure qui va écrire dans la console
begin
Main.Console.Lines[i] := MyText;
i := i + 1;
end;

procedure TMain.DefaultHandler; // Méthode qui intercepte les messages
begin
inherited DefaultHandler(Msg);
if (TMessage(Msg).Msg=Message2) then
           begin
       HandleAppliExt := (TMessage(Msg).WParam);
       EcrireConsole('Un message a été reçu'); // Premier message reçu
       Message3 := RegisterWindowMessage('SX_MessageSys4591'); // On enregistre le
                                                              // troisième message.
       EcrireConsole('3e message enregistré');
       SendMessage(HandleAppliExt, Message3, HandleEdit, 0); // On envoie le troisième message
           end;
end;



procedure TMain.btnDemarrerClick(Sender: TObject); // Méthode de démarrage
begin
Main.Console.Enabled := true;
HandleEdit := Main.Message.Handle; // On définit le handle de l'Edit qui contient
                                    // le message affiché par l'application réceptrice.
MonHandle := Main.Handle; // On encode le handle de la fenêtre de l'application
                          // émettrice.
{ On encode les messages }
Message1 := RegisterWindowMessage('SX_MessageSys4589');
EcrireConsole('1er message enregistré');
Message2 := RegisterWindowMessage('SX_MessageSys4590');
EcrireConsole('2e message enregistré');
EcrireConsole('Message en attente de réception encodé');
SendMessage(HWND_BROADCAST,Message1, MonHandle, 0); // Envoi du premier
                                                             // message.
end;

end.

Le programme émetteur envoie à toutes les applications en cours d'exécution un message qui contient le handle de sa fenêtre.
Si l'application émettrice reçoit un message venant d'une des applications réceptrices (le message en question étant contenu dans la variable « Message2 »), il en extrait le handle de l'application émettrice en question (de la fenêtre en réalité) et renvoie un message contenant le handle du champ TEdit qui contient un message qui va être affiché par les applications réceptrices, mais cette fois-ci, en envoyant le message uniquement à la fenêtre concernée et non en mode broadcast.

Notez que l'on peut passer dans les variables WParam et LParam un argument de type THandle (le handle étant finalement une variable entière).

 
Sélectionnez
unit Recepteur;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;


type
  TEditTest = class(TEdit)
  end;
type
  TMain = class(TForm)
    Label1: TLabel;
    Label2: TLabel;
    procedure FormCreate(Sender: TObject);
  private
    { Déclarations privées }
  procedure DefaultHandler(var Msg); override;
  public
    { Déclarations publiques }
  end;

var
  Main: TMain;
  Message1: UINT; // Premier message reçu
  Message2: UINT; // Premier message renvoyé
  Message3: UINT; // Second message reçu
  HandleAppliExt: THandle; // Handle de l'application émettrice.
  HandleRecepteur: THandle; // Handle de l'application réceptrice.
  MonEdit: THandle; // Edit de l'application émettrice contenant le message à
                    // afficher
  MonTexte: Array [0..255] of Char; // Message à afficher

implementation

{$R *.dfm}

procedure TMain.DefaultHandler;
begin
inherited DefaultHandler(Msg);
if (TMessage(Msg).Msg) = Message1 then
  begin
  Main.Label1.Caption := 'Message reçu'; // on indique que le premier message est
                                         // reçu.
  HandleAppliExt := (TMessage(Msg).WParam); // On encode le handle de la fenêtre
                                            // émettrice

  SendMessage(HandleAppliExt, Message2, HandleRecepteur, 0); 

  end;
if (TMessage(Msg).Msg) = Message3 then
  begin
  MonEdit := (TMessage(Msg).WParam); // On récupère le handle de l'Edit contenant
                                     // notre message.
  SendMessage(MonEdit,WM_GETTEXT,255,integer(@MonTexte)); // On récupère le message
                                                          // à afficher
  Main.Label2.Caption := Main.Label2.Caption + MonTexte; // On affiche le message
  end;
end;

procedure TMain.FormCreate(Sender: TObject);
begin
// On encode tout les messages.
HandleRecepteur := Main.Handle;
Message1 := RegisterWindowMessage('SX_MessageSys4589');
Message2 := RegisterWindowMessage('SX_MessageSys4590');
Message3 := RegisterWindowMessage('SX_MessageSys4591');
end;

end.

Le programme récepteur attend la réception d'un message (Message1) qui contient le handle de la fenêtre émettrice, il retourne ensuite un autre message contenant le handle de sa fenêtre à l'application émettrice et cela sans utiliser le mode broadcast (c.-à-d. en utilisant le handle de la fenêtre émettrice récupéré auparavant).

Notre application réceptrice reçoit ensuite un autre message venant de l'application émettrice, dans ce message est inclus le handle du champ TEdit qui contient notre message. On renvoie ensuite un message prédéfini par Windows (normal : on observe le suffixe WM_NomDuMessage) au handle du champ TEdit, ce message place dans la string « MonTexte » le message à afficher, pour des questions de compatibilité, la string est définie comme une array, mais si on y réfléchit, une variable de type string a la même structure que notre array. Si vous désirez plus d'informations sur le message WM_GETTEXT, consultez l'aide Delphi.

Sachez également que si vous développez des composants VCL, une maitrise des messages système s'impose pour gérer les évènements tels qu'une pression sur une touche au clavier.

Cliquez ici pour laisser vos commentaires sur cet article.

VII. Remerciements

Laurent Dardenne pour ses conseils et la relecture de cet article.
Bernard Determe pour ses conseils en matière de style.
Yann Marec alias ElMilouse pour la relecture de cet article.
Aurelien.regat-barrel pour sa définition des messages système.
Tous les membres de developpez.com qui au travers de leurs posts m'ont aidé à résoudre certains problèmes.
Un grand merci à la rédaction de developpez.com pour m'avoir fourni cet espace web et m'avoir aidé à rédiger mon tutoriel.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Copyright © 2019 Jean-François Determe. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.