Developpez.com - Delphi
X

Choisissez d'abord la catégorieensuite la rubrique :

3 types de données à échanger entre processus

Le 14/02/2003 (mise à jour)

Par Jean-Luc Mellet (Alphomega)

L'échange de données entre processus (applications) peut être réalisé de bien des façons. On peut utiliser les messages windows (à condition que les processus soient executés sur le même système), les mailslots, les sockets (nécessitent TCP/IP), le mappage de fichier, etc ..

J'ai voulu décrire ici l'utilisation des messages windows pour échanger des données entre processus. La simplicité des 3 méthodes décrites vous convaincra que tout cela est finalement très facile à réaliser.

A partir d'un exemple, nous allons voir l'échange de valeur numérique entière, de chaîne de caractères et de structure (record).

Note: Ce document sera émaillé de nombreux morceaux de code, mais vous trouverez un lien en fin de document pour charger les 2 projets exemples au format D5 et au format texte pour ceux qui auraient une version antérieure.

Les messages sous Windows



Rappelons briévement que Windows est un système basé sur le déclenchement d'événements. Il y a un échange permanent de messages divers entre le système d'exploitation et les processus actifs (applications en cours d'execution). La plus grande majorité de ces messages est déclenchée par une intervention utilisateur (mouvement souris, entrée clavier, etc.). Chaque processus est "à l'écoute" des messages qui peuvent lui parvenir. Cela s'appelle la boucle de messages (Message Loop). Plusieurs messages pouvant arriver successivement et rapidement, il existe ce que l'on appelle une "queue de message" (Message Queue). Vous aurez compris que chaque message est traité dans l'ordre d'arrivée, du plus ancien au plus récent.

Comment cela fonctionne il ? Sans entrer dans des détails complexes, disons simplement qu'une application windows traditionnelle possède une fonction winmain (le coeur) dans laquelle est codée cette fameuse boucle de message, qui continue à tourner tant que des messages sont censés lui parvenir. Chaque message reçu est traité selon sa fonction prédéfinie. Imaginez un entrepot de marchandises diverses avec une personne à la réception. Toute marchandise arrivant à la porte de l'entrepot est examinée et dirigée vers son lieu de stockage ou de fonction. Cette boucle réalise exactement la même chose.

Alors, si une application reçoit des messages du système (et des autres processus), nous allons pouvoir faire communiquer 2 processus entre eux, justement par l'envoi de messages. Dans le système, il existe une grande quantité de messages prédéfinis par des constantes. La plus connue de ces constantes se nomme WM_USER et sa valeur est de $0400 ( 1024 en décimal ). Windows se réservant les 1024 premieres valeurs pour ses messages prédéfinis, il nous reste toutes les valeurs supérieures à 1024 (WM_USER + n) pour définir nos propres constantes. En fait, pas tout à fait ... On pourrait se dire:

"Puisque Windows garde les 1024 premières valeurs, je vais utiliser la constante WM_MON_MESSAGE_A_MOI avec la valeur 1025 !"

Ce serait une erreur ! Pourquoi ? Et bien, parce que les fonctions SendMessage, PostMessage et autres peuvent envoyer un message à tous les applications en cours d'execution. C'est ce qu'on appelle le broadcast. Imaginez qu'un programmeur ait déjà utilisé cette valeur pour une application et que vous executiez cette application en même temps que la votre ! Quand vous utiliserez le message WM_MON_MESSAGE_A_MOI, votre programme le recevra, mais l'autre programme le pourrait aussi. Si la réception du message numéro 1025 par l'autre programme demande la fermeture de windows, vous imaginez le résultat ?

Nous allons utiliser une API qui va nous fournir un numéro de message unique. Il s'agit de RegisterWindowMessage. Comment l'utiliser ? Il suffit de lui passer en paramètre une chaîne de caractères pour qu'elle nous fournisse une valeur unique pour le système. Une fois cette valeur définie, elle sera utilisable jusqu'à l'arrèt du système. Par exemple, pour obtenir une valeur unique pour WM_MON_MESSAGE_A_MOI, nous pourrions écrire:

WM_MON_MESSAGE_A_MOI := RegisterWindowMessage('WM_MON_MESSAGE_A_MOI');

J'ai repris le nom de la variable sous forme de chaîne, mais cela n'est pas une obligation. Vous pourriez tout aussi bien écrire:

WM_MON_MESSAGE_A_MOI := RegisterWindowMessage('Je veux un numéro de message unique');

Cela fonctionnera tout aussi bien.

L'envoi des messages ainsi créés se fait principalement par l'intermédiaire des deux API SendMessage et PostMessage. Quelle différence entre les deux ? Souvenez-vous de notre fameuse boucle de message ! Je vous disais que le traitement se faisait de façon séquentielle. Et bien SendMessage envoie le message, l'ajoute à la queue de message, attends que le message soit transmis et ensuite seulement renvoie un résultat. PostMessage envoie le message et renvoie immédiatement un résultat sans attendre la transmission du message (son passage dans la boucle d'attente).

L'envoi de message étant expliqué, il nous reste à voir comment recevoir un message bien précis. Pour cela, deux solutions ! Soit on crèe une procédure qui ne recevra QUE le message désiré, soit on surcharge la procédure qui s'occupe de tous les messages non traités par la fiche, et on filtre les messages selon leur fonction. Il s'agit de la procédure DefaultHandler que l'on peut surcharger. Que dit l'aide delphi à ce propos ?

DefaultHandler est une procédure de TCustomForm qui gère tous les messages que la fiche n'a pas entièrement traités. procedure DefaultHandler(var Message); override;

Description

Surchargez la méthode DefaultHandler pour modifier le traitement par défaut des messages de la fiche. Ceci est rarement nécessaire car les messages peuvent être gérés en créant des méthodes de message.

DefaultHandler transmet tous les messages non gérés à la procédure de fenêtre de la fiche en appelant la fonction CallWindowProc de l'API.

Remarque : L'appel de la méthode héritée dans une méthode de gestion des messages produit l'appel de la méthode DefaultHandler de l'ancêtre si celui-ci ne spécifie pas de gestionnaire pour le message à gérer.


Pour notre exemple, elle nous sera nécessaire à cause de l'utilisation de RegisterWindowMessage. La création d'une procédure pour gérer un message unique est simple. La déclaration doit se faire dans la section public de la TForm. Par exemple, pour notre valeur précédente, on écrira:

const
  WM_MON_MESSAGE_A_MOI : integer = WM_USER + 1;

procedure WmMonMessageAmoi(var M: TMessage); message WM_MON_MESSAGE_A_MOI;

implementation

procedure WmMonMessageAmoi(var M: TMessage);
begin
  // Ici, on ne traite QUE le message WM_MON_MESSAGE_A_MOI;
end;


Vous avez remarqué que WM_MON_MESSAGE_A_MOI doit être une constante. A cause de cela, nous utiliserons la procédure DefaultHandler plutôt qu'une gestion de message au coup par coup. Pourquoi ? Tout simplement parce que la valeur de WM_MON_MESSAGE_A_MOI ne sera connue qu'au lancement du programme par l'utilisation de RegisterWindowMessage. Il est donc impossible de définir sa valeur lors de la conception du code.

Tout au long de ce document, j'utiliserai SendMessage. Alors voyons comment utiliser cette API pour un simple échange de données entre applications ! Pour illustrer ce travail, nous allons créer deux projets, l'un sera l'émetteur et l'autre le receveur.

Dans nos deux projets, nous allons définir (pour plus de clarté) les même noms pour nos variables identificateur de message. Vous noterez que l'utilisation de la même chaîne de caractères renverra la même valeur numérique à nos 2 programmes, nous permettant de connaître ces valeurs sans autre recherche.

IMPORTANT: (pour trouver le handle de chaque application)

La fiche de l'application Emetteur aura comme titre: "Cette appli envoie les infos au receveur"

La fiche de l'application Receveur aura comme titre: "Cette appli reçoit les infos"

Code Emetteur ET Receveur (similaires pour l'instant)
implementation

  {$R *.DFM}

var
WM_MESSAGE_ENVOYEUR, WM_ENVOI_ATOM: integer;

procedure TForm1.FormCreate(Sender: TObject);
begin
  WM_MESSAGE_ENVOYEUR := RegisterWindowMessage('WM_MESSAGE_ENVOYEUR');
  WM_ENVOI_ATOM := RegisterWindowMessage('WM_ENVOI_ATOM');
end;


Dans notre receveur, nous allons surcharger DefaultHandler.

Code Receveur
public
  procedure DefaultHandler(var msg); override;

begin
  inherited DefaultHandler(Msg);
end;


Voilà ! A ce stade, nos deux applications peuvent dialoguer. Ou plus exactement, notre Receveur peut récupérer des messages, y compris ceux de notre émetteur.

Envoi d'un nombre

Le cas le plus simple de besoin d'échange de données est l'échange d'une valeur entière. Les paramètres de SendMessage sont respectivement la handle de l'application qui devra recevoir le message, le numéro du message, et 2 valeurs entières.

Sur la fiche de notre Emetteur, nous allons placer un TEdit et un TButton. Bridons notre TEdit pour imposer la saisie de valeur entière uniquement en modifiant l'événement OnKeyPress, et modifions l'événement OnClick du TButton pour envoyer notre valeur numérique. Pour envoyer notre message, nous devons connaître le handle du programme Receveur. Nous le trouverons en utilisant l'API FindWindow.

Code Emetteur
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
var
  S: string;
  err, valeur: integer;
begin
  if Key in ['0'..'9', #8] then
  begin
    S := Edit2.text + Key;
    val(S, Valeur, err);
    if err <> 0 then Key := #0;
  end
  else
    Key := #0;
  end;
end;

procedure TForm1.Envoyer1Click(Sender: TObject);
var
  h: THandle;
begin
  {Envoie au receveur le nombre indiqué dans Edit1.text}
  if Edit1.text = '' then
    ShowMessage('Veuillez entrer un nombre ')
  else
   begin
    h := FindWindow(nil, PChar('Cette appli reçoit les infos'));
    if h = 0 then
      ShowMessage('Le receveur est inactif')
    else
      SendMessage(h, WM_MESSAGE_ENVOYEUR, StrToInt(Edit1.text), 0);
  end;
end;


Que va t-il se passer dans notre Receveur ? Pour mieux le montrer, rajoutons à notre fiche Receveur une un TLabel pour y inscrire le résultat.

Code receveur
procedure TForm1.DefaultHandler(var msg);
begin
  inherited DefaultHandler(Msg);
  if TMessage(msg).Msg = WM_MESSAGE_ENVOYEUR then Label1.Caption := IntToStr(TMessage(msg).WParam);
end;


Si DefaultHandler recoit le message WM_MESSAGE_ENVOYEUR, il affiche la valeur de WParam (valeur passée par le message).

Envoi d'une chaîne de caractères

Pour illustrer l'envoi d'une chaîne de caractères, nous allons parler des atom. Aucune radiation ni reste de Tchernobyl la-dedans, rassurez-vous! Un atom n'est rien qu'une string[255 {maxi}] avec un identifiant numérique stocké soit au niveau du processus, soit au niveau global du système. Chaque atom peut donc être retrouvé avec son identificateur numérique. La table est globale et peut être partagée par tous.


Pour transmettre une chaîne, il va donc nous suffire d'ajouter un atom global au système et d'envoyer son identifiant numérique au Receveur qui, à partir de ça, pourra retrouver la chaîne dans la table. Pour l'exemple, Utilisons un TEdit pour la saisie du texte et un TButton pour l'envoi du message. Pour ajouter un atom à la table globale, nous utiliserons GlobalAddAtom.

Code emetteur
procedure TForm1.Envoyer2Click(Sender: TObject); 
var 
 h: THandle; 
 atom_Envoye: Atom; 
begin 
 if length(edit2.text) = 0 then 
  ShowMessage('Vous devez saisir un texte à envoyer') 
 else 
  {Création d'un atom dans la table globale 
   Notification par message au receveur de l'envoi d'un atom} 
  begin 
   atom_Envoye := GlobalAddAtom(PChar(edit2.text)); 
   h := FindWindow(nil, PChar('Cette appli reçoit les infos')); 
   if h = 0 then 
    ShowMessage('Le receveur est inactif') 
   else 
    SendMessage(h, WM_ENVOI_ATOM, atom_Envoye, 0); {l'atom est envoyé dans WParam} 
  end; 
end; 
	  


Que va t'il se passer dans notre receveur ? Et bien nous allons filtrer le nouveau message possible et agir en conséquence.

Code receveur
procedure TForm1.DefaultHandler(var msg); 
begin 
 inherited DefaultHandler(Msg); 

  if TMessage(msg).Msg = WM_MESSAGE_ENVOYEUR  then  
  Label1.Caption := IntToStr(TMessage(msg).WParam) 

  else if TMessage(msg).Msg = WM_ENVOI_ATOM  then 
  begin 
  // le numéro identifiant l'atom se trouve dans WParam 
  atom_recu := TMessage(msg).WParam; 
  GetMem(TexteRecu, 256);   {255 maxi + #0} 
  GlobalGetAtomName(atom_recu, TexteRecu, 256); 
  label5.caption := TexteRecu; 
  GlobalDeleteAtom(atom_recu);   {Ne pas oublier de détruire l'atom puisqu'on a récupéré la valeur} 
  FreeMem(TexteRecu); 
 end;
  
end;


Envoi d'une structure

Nous allons terminer cette mini revue de l'utilisation des messages avec celui qui permet de transmettre une structure, et donc, tout et n'importe quoi. Le message utilisé sera WM_COPYDATA. Puisque ce message fait partie de ceux prédéfinis par Windows, nous allons pouvoir créer dans notre Receveur une procédure qui s'occupera principalement de lui. Pour l'exemple, j'ai ajouté sur l'Emetteur et le Receveur les éléments suivants:


L'emetteur transmettra sous forme de structure les différentes infos de ce TPanel. Là, le traitement est un peu plus compliqué. Les paramètres sont : handle de l'application Receveur, le message WM_COPYDATA, le handle de l'application emettrice, un pointeur sur une structure de type TCopyDataStruct.

TCopyDataStruct = record 
  dwData: LongInt;      {une valeur au choix de l'utilisateur} 
  cbData: LongInt;       {taille de la structure de données envoyée} 
  lpData: Pointer;         {pointeur sur la structure}
end;


Code emetteur ET receveur (parties communes)
	  
implémentation 

type 
 PCopyDataStruct = ^TCopyDataStruct; 
 TCopyDataStruct = record 
  dwData: LongInt; 
  cbData: LongInt; 
  lpData: Pointer; 
 end; 

type 
 PEnvoiFax = ^TEnvoiFax; 
 TEnvoiFax = packed record 
  Destinataire: string[255]; 
  TypeDocument: integer; 
  DateEnvoi: TDateTime; 
  EnvoiFait: boolean; 
 end;


Code emetteur
procedure TForm1.Envoyer3Click(Sender: TObject); 
var 
 h: THandle; 
 CopyDataStruct: TCopyDataStruct; 
 Envoi: TEnvoiFax; 
begin 
 {Envoyer une structure de données avec WM_COPYDATA} 
 h := FindWindow(nil, PChar('Cette appli reçoit les infos')); 
 if h = 0 then 
  ShowMessage('Le receveur est inactif') 
 else 
  begin 
   with Envoi do 
    begin 
     Destinataire := Edit1.text; 
     TypeDocument := RadioGroup1.ItemIndex; 
     DateEnvoi    := DateTimePicker1.DateTime; 
     EnvoiFait    := CheckBox1.Checked; 
    end; 
   with CopyDataStruct do 
    begin 
     dwData := 1; 
     cbData := sizeof(Envoi); 
     lpData := @Envoi; 
    end; 
   SendMessage( (h, WM_COPYDATA, Form1.Handle, LongInt(@CopyDataStruct)); 
  end; 
end; 



Code receveur
public 
 procedure WmCopyData(var M: TMessage); message WM_COPYDATA; 

implémentation 

procedure TForm1.WmCopyData(var M: TMessage); 
begin 
 with PEnvoiFax(PCopyDataStruct(m.LParam)^.lpData)^ do 
  begin 
   Edit1.text := Destinataire; 
   RadioGroup1.ItemIndex := TypeDocument; 
   DateTimePicker1.DateTime := DateEnvoi; 
   CheckBox1.Checked := EnvoiFait; 
  end; 
end; 


Conclusion

Comme vous le voyez, l'échange de données est une chose simple. Comme je le précisais au début de cet exposé, il existe bien d'autres méthodes, mais les 3 décrites ici suffisent le plus souvent à régler tous les problèmes d'échange.

A bientôt pour une autre aventure !
Responsables bénévoles de la rubrique Delphi : Gilles Vasseur - Alcatîz -