|
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
  
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
atom_recu := TMessage(msg).WParam;
GetMem(TexteRecu, 256);
GlobalGetAtomName(atom_recu, TexteRecu, 256);
label5.caption := TexteRecu;
GlobalDeleteAtom(atom_recu);
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;
cbData: LongInt;
lpData: Pointer;
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
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 !
|
|