Comment gérer les messages système depuis Delphi.

Ce tutoriel explique comment gérer les messages système avec Delphi à travers 3 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 emetteur 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 du 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 inter-processus, 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 2 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 de 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 fait 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;

implementation

{$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ée est : ' + chr(Msg.CharCode);
MainFrame.Label2.Caption := c;

end;



end.

Etudions 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' lorsque 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ée est : ' +
chr(Msg.CharCode);
MainFrame.Label2.Caption := c;
end;
		

La déclaration de la procédure dans la partie implementation est semblable, il y a juste 2 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 2 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 procedure ( 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 le 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 2 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('3ème 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('2eme 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'information 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.


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 fournit cet espace web et m'avoir aidé à rédiger mon tutoriel.

  

Copyright © Jean-François Determe. Aucune reproduction, même partielle, ne peut être faite de ce site et 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.