Newsgroup: it.comp.lang.delphi
Titolo: Comunicazioni tra diverse istanze tramite messaggi di windows

Ciao a tutti,
avendo riscontrato diverse difficoltà ad utilizzare un observer per le
mie esigenze, ho valutato la via dei messaggi windows; in pratica ho
la mia applicazione sviluppata con un form principale che a seconda
dell'esigenza carica moduli bpl dove al loro interno è contenuto un
frame. L'esigenza era quella di aggiornare alcune label presenti nel
form principale dopo alcune operazioni in determinati package.
Funziona tutto quasi perfettamente, quasi. In pratica succede che
alcune volte senza un motivo apparente, le label nel mainform non si
aggiornano ma il messaggio risulterebbe ricevuto correttamente e non
capisco dove sta il problema.
Posto un po di codice per chiarezza:
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sorgente del MainForm per ricevere i messaggi di windows:
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
procedure TFormMain.HandleCopyDataRecord(
  copyDataStruct: PCopyDataStruct);
var
  sampleRecord : TSampleRecord;
  WhoObj : string;
  WhatObj : integer;
  WhenObj : TDateTime;
begin
  sampleRecord.s := TSampleRecord(copyDataStruct.lpData^).s;
  sampleRecord.i := TSampleRecord(copyDataStruct.lpData^).i;
  sampleRecord.d := TSampleRecord(copyDataStruct.lpData^).d;
  //
  WhoObj := sampleRecord.s;
  WhatObj := sampleRecord.i;
  WhenObj := sampleRecord.d;
  //
  if WhoObj = 'Accettazioni' then
  begin
    AggiornaAccettazioni();
    LabelAccettazioni.Invalidate;
    Application.ProcessMessages;
  end;
  if WhoObj = 'Carico' then
  begin
    AggiornaCarico();
    LabelCarico.Invalidate;
    Application.ProcessMessages;
  end;
  if WhoObj = 'Consegne' then
  begin
    AggiornaConsegne();
    LabelConsegne.Invalidate;
    Application.ProcessMessages;
  end;
  if WhoObj = 'Resi' then
  begin
    AggiornaResi();
    LabelResi.Invalidate;
    Application.ProcessMessages;
  end;
end;
procedure TFormMain.WMCopyData(var Msg: TWMCopyData);
var
  copyDataType : TCopyDataType;
begin
  copyDataType := TCopyDataType(Msg.CopyDataStruct.dwData);
  //Handle of the Sender
  //cdMemo.Lines.Add(Format('WM_CopyData from: %d',[msg.From]));
  case copyDataType of
    cdtRecord: HandleCopyDataRecord(Msg.CopyDataStruct);
  end;
  //Send something back
  msg.Result := 1;
end;
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sorgente di un package per inviare il messaggio al mainForm
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
procedure TFrameControlloInvioTracer.SendData(
  copyDataStruct: TCopyDataStruct);
var
  receiverHandle  : THandle;
  res : integer;
begin
  receiverHandle := Application.MainForm.Handle;
  if receiverHandle = 0 then
  begin
    ShowMessage('Data Receiver NOT found!');
    Exit;
  end;
  res := SendMessage(receiverHandle, WM_COPYDATA, Integer(Handle),
Integer(@copyDataStruct));
  if res <> 1 then ShowMessage('Errore durante la procedura di
aggiornamento dei controlli nel form principale.');
end;
procedure TFrameControlloInvioTracer.SendRecord();
var
  sampleRecord : TSampleRecord;
  copyDataStruct : TCopyDataStruct;
begin
  sampleRecord.s := 'Accettazioni';
  sampleRecord.i := 0;
  sampleRecord.d := Now;
  copyDataStruct.dwData := Integer(cdtRecord); //use it to identify
the message contents
  copyDataStruct.cbData := SizeOf(sampleRecord);
  copyDataStruct.lpData := @sampleRecord;
  SendData(copyDataStruct);
end;
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
Come potete vedere, il messaggio inviato al form principale si aspetta
un messaggio di conferma di ritorno per sicurezza. Questa risposta
avviene sempre quindi deduco che il messaggio arrivi sempre
correttamente anche al form principale; una volta arrivato il
messaggio al form, viene richiamata una procedura che effettua una
select SQL e aggiorna la caption della relativa label interessata.
Questo il codice dell'esempio:
procedure TFormMain.AggiornaAccettazioni;
begin
  ADOQueryAccettazioni.Active := false;
  ADOQueryAccettazioni.SQL.Clear;
  ADOQueryAccettazioni.SQL.Add('SELECT COUNT(ID) AS CountAccettazioni
FROM t_tracer WHERE Inviato = false');
  ADOQueryAccettazioni.Open;
  ADOQueryAccettazioni.Active := true;
  if ADOQueryAccettazioni.FieldByName('CountAccettazioni').AsInteger >
0 then
    BottoneInviaAccettazioni.Enabled := true;
  LabelAccettazioni.Caption :=
ADOQueryAccettazioni.FieldByName('CountAccettazioni').AsString;
end;
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
Secondo voi da cosa può dipendere questo problema? Non è logico, o
funziona sempre o non funziona, il codice è sempre lo stesso e le
operazioni che deve eseguire idem, in più se fosse un problema di
ricezione dei messaggi windows, il sistema dovrebbe rilevarlo e
mostrarmi a video l'errore mentre questo non accade mai.
Grazie mille dell'aiuto.

 mie esigenze, ho valutato la via dei messaggi windows;
[CUT]
Ciao, a prima vista il codice non dovrebbe avere problemi (a prima 
vista, sottolineo, non posso testarlo).
A questo punto ti posso suggerire una cosa: creati un log banalissimo,un 
qualcosa che va in append su un file di testo e scrive una riga dove 
dici il timestamp, chi sei, che cosa hai ricevuto.
In questo modo puoi verificare se il messaggio viene ricevuto o meno: 
stabilito questo, potrai capire dove concentrare le tue attenzioni.
Ho lavorato anche io con il copydata, ma mai nella situazione di 
concorrenza.. magari il problema viene da lì (e quindi sarebbe lato 
windows).
Inoltre, il tuo sender resta bloccato in attesa di risposta, e questo 
non è un approccio safe: rischi di dover forzare la chiusura se il 
receiver dovesse collassare.
Che versione di delphi stai usando? Ci sono possibili problemi di 
API(RTL) buggata?
Un'alternativa sarebbe quella di ingegnerizzare un pochino il protocollo 
di scambio messaggi, una cosa molto rozza, diciamo..: una classettina 
"messaggio" che contiene i tuoi dati di business, e che viene persistita 
in una tabella di questo tipo:
tabella messaggi:
ID (autoinc) ,TIMESTAMP MSG (datetime) ,RECEIVER (stringa) ,SENDER 
(stringa) ,MESSAGE (stringa o blob), READ (bool)
con questa semplice tabellina chi volesse sapere se ci sono messaggi per 
lui, fa una query filtrata per receiver pari al suo nome, prende tutto 
ciò che trova, flagga il READ a true e si disconnette, in seguito 
elabora i messaggi letti. potrebbe anche cancellare quello che legge, 
per tenere scarico il db, ma preferisco che sia una stored triggerata a 
farlo.
Questa e' un'architettura che tira in ballo il database, con i suoi pro 
e contro, ma se non ne esci dalla API di windows questa sarebbe una 
comoda soluzione (e funziona anche con il tuo programma distribuito in 
WLAN in  quanto il db è in comune).
please, keep us up to date.
ciao
-- 
morde
D5,D6,D7
QT 4.7.0 - MingW- QT Creator 2.1

 QT 4.7.0 - MingW- QT Creator 2.1
Ciao, ti ringrazio dell'attenzione.
Allora, ho pensato anche io a creare un log banale almeno per avere la
cronologia dei messaggi ricevuti e fare un confronto quando
l'aggiornamento non avviene (e magari estendere il log a tutta
l'applicazione in modo da poterlo interrogare in caso di problemi).
Per il resto, ti ringrazio per i suggerimenti che terrò sicuramente in
considerazione ma prima devo cercare di capire dove sta il problema
dei messaggi per poi eventualmente affinare il codice e la struttura.
Per darti un'info in più, sto utilizzando Delphi2010 su OS Win7 e
tutti i package vengono caricati a runtime tramite LoadPackage solo se
necessario e non preventivamente.
Grazie ancora.

 mostrarmi a video l'errore mentre questo non accade mai.
2 cose:
il mio vecchio help recita, a proposito di WM_COPYDATA: "The data
being passed must not contain pointers or other references to objects
not accessible to the application receiving the data." Sei sicuro che
SizeOf(sampleRecord) sia proprio quel che ti aspetti? ovvero la
dimensione della stringa + quella dell'intero + quella della data?
Prova a mettere un outputdebugstring(PChar('Sto per inviare ' +
inbttostr(sizeof(sampleRecord)))) prima di fare il sendmessage.
Per stare sul sicuro, invece della stringa puoi mettere un array of
char di opportune dimensioni, o allocare lo spazio necessario e
copiare li' la stringa (stralloc, strnew, getmem). Ovviamente dovrai
poi rilasciare la memoria quando sendmessage ritorna.
L'altra cosa è un po' più "architetturale": passare da
un'implementazione pattern-oriented ad una che sfrutta direttamente le
chiamate di sistema operativo mi pare drastico.
Apparentemente, dal package "vedi" MainForm (ne ricavi l'handle). A
questo punto, perchè non definire per il mainform un'interfaccia che
poi si possa invocare dal frame (eventualmente con un cast)?
Qualcosa tipo:
TMainForm
...
public
...
procedure DoFrameEvent(Sender: TObject; label2Update: string; Data:
TObject);
end;
e nel frame
procedure TSomeFrame.HaveToUpdateView(Data2Show: TSomeData)
begin
    (cast to mainform)(Application.MainForm).DoFramEvent(Self,
'someLabel', Data2Show);
end;
Saluti
Paolo Arosio


Ciao Paolo,
riguardo alla tua prima osservazione direi che se non fosse così non
funzionerebbe sempre o per la maggiorparte dei casi, non a volte si e
a volte no, in ogni caso ho inserito la stessa struttura di
sampleRecord sia sul form che nei vari package per cui sono quasi
sicuro che i puntamenti siano corretti.
Per quanto riguarda la seconda osservazione, potrebbe essere un'altra
soluzione ma anche se faccio il cast a MainForm passando per
Application non ho accesso diretto alle funzioni del mainform per cui
avrei errore già in compilazione. E' questo che intendi o sono io
svampito che non ho capito una fava?
Grazie comunque dell'intervento.

 Grazie comunque dell'intervento.
prova a dare un un sguardo 
http://tinyurl.com/6743lbo
 forse ti pu� 
essere utile

 Grazie comunque dell'intervento.
Figurati, siamo qui apposta (up to a point).
Saluti
Paolo Arosio

 Paolo Arosio
Credo di aver capito cosa intendi con la dimostrazione postata e mi
domando, secondo te l'approccio migliore è quello di utilizzare una
struttura pattern-oriented oppure rimanere sul WM_COPYDATA con
allocazione della memoria?
Per quanto riguarda il codice, se non ho capito male intendi questo?
    HandlePack := LoadPackage(ExtractFilePath(Application.ExeName)+'Bpl
\'+PackToLoad);
    //Controlla se il package è stato caricato correttamente
    if Not (HandlePack > 0) then
    begin
      ShowMessage('Impossibile caricare il modulo ' + '"'+PackToLoad
+'".');
    end
    else
    begin
      try
        //Preleva la classe desiderata fra le class registrate
        FrameClass := TFrameClass(GetClass(ClassToLoad));
        //Controlla se la classe è stata assegnata correttamente
        if not Assigned(FrameClass) then
        begin
          ShowMessage('Impossibile caricare la classe ' +
'"'+ClassToLoad+'".');
        end
        else
        begin
          //CurrentConditions.Create(FrameClass);
          //Crea e visualizza il form del packages
          FrameTo := FrameClass.Create(Application);
          FrameTo.Parent := FormMain.PagePanel;
          FrameLoaded := ClassToLoad;
        end;
      except
        on E: exception do
        begin
       //Scarica il package
       UnloadPackage(HandlePack);
       end;
      end;
    end;
Giusto per chiarirmi le idee, per seguire la via del pattern-oriented
dovrei registrare la classe TMainForm per renderla disponibile nei
vari package dopo il loro caricamento?
Grazie ancora, porta pazienza ma sono un po duro di comprendonio.

    FrameTo.Parent := FormMain.PagePanel;
// cast del frame alla classe "attuale"
    (FrameClass)(FrameTo).HaveData := MyFrameHasSomethingToShow;
...
TFormMain.MyFrameHasSomethingToShow(Sender: TObject; who: string;
what: TObject);
var label2Update: TLabel;
begin
  outputDebugString(Pchar('An instance of ' + Sender.classname + '
sent me something to show on label ' + who));
  label2Update := TLabel(FindComponent('Label' + who));
  label2Update.caption := WhatToString(what);
end;
Che non è propriamente un Observer: il subject (frame) non conosce il
proprio observer (mainForm) (che tra l'altro è un solo oggetto). Molto
semplicemente, quando l'observer riceve la notifica di cambiamento del
subject ha già anche il nuovo stato del subject e le info su dove
rappresentare tale stato (e questo è *piuttosto* strano: che il
subject dica all'observer come rappresentarlo).
Se vuoi disaccoppiare maggiormente, fai in modo che i frame espongano
un evento semplice (TNotifyEvent) (che sarà sottoscritto dal main form
alla creazione del frame), da attivare quando cambia qualcosa; in
più,  un metodo generico (getState) che consenta al Main Form di
recuperare i dati quando l'evento viene scatenato. Ma mi pare un po'
una complicazione inutile, per come hai esposto la cosa.
Saluti
Paolo Arosio

 [...]
Seguendo un po le tue direttive ho cercato di buttare giù un po di
codice di prova.
--------------------------------------------------------------------------------------
CallBack
--------------------------------------------------------------------------------------
unit Callback;
interface
type
  THaveDataToShow = procedure(Sender: TObject; who: string; what:
TObject) of object;
implementation
end.
---------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------
MainForm
---------------------------------------------------------------------------------------
unit MainForm;
interface
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,
Forms,
  Dialogs, DB, ADODB, sSkinManager, ExtCtrls, sPanel,
  ToolWin, ComCtrls, sToolBar, Menus, IniFiles, GIFImg,
  jpeg, sSkinProvider, StdCtrls, sLabel, sGroupBox, sButton,
  OnGuard, OgUtil, sEdit, acDBTextFX, CallBack;
type
  TCopyDataType = (cdtString = 0, cdtImage = 1, cdtRecord = 2);
  TSampleRecord = packed record
    s : string[50];
    i : integer;
    d : TDateTime;
  end;
type
  TFormMain = class(TForm)
    [...]
    procedure LoadPack(Cmd: string);
  private
    { Private declarations }
    [...]
  public
    { Public declarations }
    [...]
    procedure MyFrameHasSomethingToShow(Sender: TObject; who: string;
what: TObject);
end;
type
  TFrameClass = class of TFrame;
var
  FormMain: TFormMain;
  [...]
implementation
procedure TFormMain.LoadPack(Cmd: string);
Var
  ClassToLoad: string;
  PackToLoad: string;
  FrameTo: TFrame;
  FrameClass: TFrameClass;
  ConfigFile: TIniFile;
begin
  try
    Screen.Cursor := crHourGlass;
    // Procedura di carico del file INI per la gestione automatica del
pacchetto da caricare
    ConfigFile := TIniFile.Create(ExtractFilePath(Application.ExeName)
+'Config\PackToLoad.ini');
    PackToLoad := ConfigFile.ReadString(Cmd,'PackToLoad','');
    ClassToLoad := ConfigFile.ReadString(Cmd,'ClassToLoad','');
    if HandlePack > 0 then
      DoUnloadPackage(HandlePack);
    //Carica il package e inizializza tutte le unit presenti al suo
interno
    HandlePack := LoadPackage(ExtractFilePath(Application.ExeName)+'Bpl
\'+PackToLoad);
    //HandlePack := LoadPackage(PackToLoad);
    //Controlla se il package è stato caricato correttamente
    if Not (HandlePack > 0) then
    begin
      ShowMessage('Impossibile caricare il modulo ' + '"'+PackToLoad
+'".');
    end
    else
    begin
      try
        //Preleva la classe desiderata fra le class registrate
        FrameClass := TFrameClass(GetClass(ClassToLoad));
        //Controlla se la classe è stata assegnata correttamente
        if not Assigned(FrameClass) then
        begin
          ShowMessage('Impossibile caricare la classe ' +
'"'+ClassToLoad+'".');
        end
        else
        begin
          //CurrentConditions.Create(FrameClass);
          //Crea e visualizza il form del packages
          FrameTo := FrameClass.Create(Application);
          FrameTo.Parent := FormMain.PagePanel;
          FrameLoaded := ClassToLoad;
          (FrameClass)(FrameTo).HaveData := MyFrameHasSomethingToShow;
<-------------------- TFrameClass not contain a member named HaveData
        end;
      except
        on E: exception do
        begin
       //Scarica il package
       UnloadPackage(HandlePack);
       end;
      end;
    end;
    Screen.Cursor := crDefault;
  except
    on E: exception do
    begin
      Screen.Cursor := crDefault;
    end;
  end;
end;
procedure TFormMain.MyFrameHasSomethingToShow(Sender: TObject; who:
string;
  what: TObject);
var label2Update: TLabel;
begin
  outputDebugString(Pchar('An instance of ' + Sender.classname + 'sent
me something to show on label ' + who));
  label2Update := TLabel(FindComponent('Label' + who));
  label2Update.caption := Str(what);
end;
end;
---------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------
Package content
---------------------------------------------------------------------------------------
unit ControlloInvioTracer;
interface
uses
  [...], CallBack;
type
  TFrameControlloInvioTracer = class(TFrame)
    [...]
  private
    { Private declarations }
    [...]
    private fHaveData: THaveDataToShow;
    procedure doHaveData; virtual; // o virtual abstract;
  public
    { Public declarations }
    [...]
    property HaveData: THaveDataToShow read fHaveData write fHaveData;
  end;
Var
  FrameControlloInvioTracer : TFrameControlloInvioTracer;
implementation
procedure TFrameControlloInvioTracer.doHaveData;
begin
  if Assigned(fHaveData) then
    fHaveData(Self, Self.lastUpdatedItem, Self.lastUpdatedItemData);
<------ TFrameControlloInvioTracer not contain a member named
lastUpdated[...]
end;
end;
---------------------------------------------------------------------------------------
That's all. Questo è quello che ho interpretato dalle tue indicazioni
ma non ti nascondo che avrei bisogno di qualche delucidazione perchè
non sono così evoluto e non mi sono chiari diversi passaggi oltre al
fatto che così com'è non è comunque funzionante e da solo non
riuscirei a raggiungere lo scopo. Se hai ancora un po di pazienza per
darmi una spiegazione di quello che fa il codice e di come poterlo
correggere te ne sarei grato, almeno so cosa sto facendo e posso
studiarmi il codice per applicazioni future e per customizzarlo a
seconda delle esigenze.
Grazie mille di nuovo.

 riuscirei a raggiungere lo scopo.
Lo schema generale è il seguente:
frameBase: espone un'interfaccia che consente di recuperare il
cambiamento di stato
uno o più frame discendenti dal frame base, che sono contenuti nei
package
applicazione (main form) client dei package: istanzia un frame
(discendente del base), utilizza l'interfaccia resa disponibile dal
frame base.
Per chiarirmi le idee ho messo giù un esempio minimale; se mi dici
dove, ti mando lo zip.
Saluti
Paolo Arosio

 dove, ti mando lo zip.
Poiché l'argomento m'interessa proprio tanto, potrei eventualmente avere 
anch'io l'esempio?
Thanks. :-)
Fed.

 anch'io l'esempio?
Lo posto qui, visto che effettivamente potrebbe essere di qualche
utilità (e google groups non mi consente di vedere il tuo indirizzo in
chiaro).
Non ho comunque fatto altro che seguire le indicazioni trovate qui:
http://edn.embarcadero.com/article/27178
Il progetto comprende quello del package (che contiene il frame
discendente) e l'applicazione che utilizza il package.
Il frame padre mi serve in quanto espone l'evento cui si aggancia il
main form per "consumare" i dati prodotti dal frame. E' un esempio
quick & dirty (e forse per questo funzionante :-)), niente interfacce,
solo PODOs :-)
(* !!! uBaseFrame.pas: frame vuoto (Label1.caption è "frame
padre") !!! *)
unit uBaseFrame;
interface
uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs,
  StdCtrls;
type
  THaveDataEvent = procedure(Sender: TObject;
    who: string;
    what: TObject) of object;
  TbaseFrame = class(TFrame)
    Label1: TLabel;
  private    { Private declarations }
    fHaveData: THaveDataEvent;
    fWho: string;
    fWhat: TObject;
  protected
    property Who: string read fWho write fWho;
    property What: TObject read fWhat write fWhat;
    procedure doHaveData; virtual;
  public     { Public declarations }
    property OnHaveData: THaveDataEvent read fHaveData write
fHaveData;
  end;
implementation
{$R *.dfm}
{ TbaseFrame }
procedure TbaseFrame.doHaveData;
begin
  if Assigned(OnHaveData) then
    OnHaveData(Self, Who, What);
end;
end.
(* !!! uRealFrame.pas: frame con un pulsante,
   un Edit con il nome della label da aggiornare,
   un Memo con un po' di linee da trasmettere come dati.
   E' la sola unit nel package (samplePackage.dpk) !!! *)
unit uRealFrame;
interface
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,
Forms,
  Dialogs, uBaseFrame, StdCtrls;
type
  TrealFrame = class(TbaseFrame)
    Edit1: TEdit;
    Label2: TLabel;
    Memo1: TMemo;
    Label3: TLabel;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private    { Private declarations }
  public     { Public declarations }
  end;
var
  realFrame: TrealFrame;
implementation
{$R *.dfm}
procedure TrealFrame.Button1Click(Sender: TObject);
begin
  Who := Edit1.Text;
  What := Memo1.Lines;
  inherited doHaveData;
end;
// da provare senza inizializzazione e con inizializzazione  !!
initialization
  RegisterClass(TrealFrame);
end.
(* !!! ufrmMain.pas: main form dell'applicazione, ha
   un Button per attivare il caricamento,
   un Panel come parent del frame istanziato,
   un paio di Label da aggiornare,
   un Memo utilizzato come log  !!! *)
unit ufrmMain;
interface
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,
Forms,
  Dialogs, StdCtrls, ExtCtrls;
type
  TfrmMain = class(TForm)
    btnLoad: TButton;
    Memo1: TMemo;
    pnlDest: TPanel;
    Label1: TLabel;
    Label2: TLabel;
    procedure FormDestroy(Sender: TObject);
    procedure btnLoadClick(Sender: TObject);
  private     { Private declarations }
    pkgHandle: THandle;
  public      { Public declarations }
    procedure doLoadPackage;
    procedure HandleFrameData(Sender: TObject;
      Who: string;
      What: TObject);
  end;
var
  frmMain: TfrmMain;
implementation
{$R *.dfm}
uses uBaseFrame;
type
  EPackageSample = class(Exception);
procedure TfrmMain.doLoadPackage;
const pkg2Load = 'samplePackage.bpl';
      frame2Load = 'TrealFrame';
var frameClass: TPersistentClass;
    myActualFrame: TbaseFrame;
begin
  pkgHandle := LoadPackage(pkg2Load);
  try
    frameClass := GetClass(frame2Load);
    if not Assigned(frameClass) then
      raise EPackageSample.Create('Cannot find class ' + frame2Load);
    myActualFrame :=
TbaseFrame(TComponentClass(frameClass).Create(Application));
    myActualFrame.Parent := pnlDest;
    myActualFrame.OnHaveData := HandleFrameData;
  except
    on E: Exception do begin
      UnloadPackage(pkgHandle);
      pkgHandle := 0;
      raise;
    end;
  end;
end;
procedure TfrmMain.HandleFrameData(Sender: TObject; Who: string;
  What: TObject);
begin
  Memo1.Lines.Add('Got data from ' + Sender.ClassName);
  Memo1.Lines.Add('Who: ' + who);
  Memo1.Lines.Add('What is a ' + What.ClassName);
  TControl(FindComponent(Who)).SetTextBuf(PChar('What has ' +
  inttostr(TStrings(What).Count) + ' lines'));
end;
procedure TfrmMain.FormDestroy(Sender: TObject);
begin
  if pkgHandle <> 0 then
    UnloadPackage(pkgHandle);
end;
procedure TfrmMain.btnLoadClick(Sender: TObject);
begin
  doLoadPackage;
end;
end.