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▲
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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) :
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 :
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 :
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 :
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 :
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 :
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.
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).
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.