Autor |
Beitrag |
Xion
Beiträge: 1952
Erhaltene Danke: 128
Windows XP
Delphi (2005, SmartInspect), SQL, Lua, Java (Eclipse), C++ (Visual Studio 2010, Qt Creator), Python (Blender), Prolog (SWIProlog), Haskell (ghci)
|
Verfasst: Mi 02.05.12 18:20
Stichworte: FindWindow SendMessage PostMessage
Dieses Tutorial wurde auf Windows XP implementiert und getestet, ob dies auch in neueren Windows-Versionen funktioniert ist dem Autor unbekannt
1. Zielstellung
IPC im allgemeinen bezeichnet den Austausch von Daten zwischen Prozessen. Dies kann auf verschiedene Wege geschehen, etwa durch einen Socket oder durch eine Datei, die zunächst von einem Prozess geschrieben und anschließend vom anderen Prozess gelesen wird.
Unter Windows stellt uns das Betriebssystem zusätzlich sogenannte Messages zur Verfügung. Diese kann man sich als kleine Paketchen vorstellen, die vom Betriebssystem zugestellt werden. Da dies in Delphi gut gekapselt ist, muss sich der Entwickler nicht damit auseinandersetzen. Man kann jedoch dieses System blockieren, etwa durch eine zeitaufwändige Berechnung, so dass die Anwendung "nicht mehr reagiert". Durch den Befehl Application.ProcessMessages kann man nun von Hand veranlassen, dass die zugestellten Messages abgerufen und verarbeitet werden. All dies geschieht intern, wir bemerken davon meist nur, dass nun die Grafik neu gezeichnet oder ein Button-Klick erkannt wurde.
Wir werden nun im Folgenden verschiedene Gründe sehen, warum sich ein näherer Blick auf diese Messages dennoch lohnen kann.
_________________ a broken heart is like a broken window - it'll never heal
In einem gut regierten Land ist Armut eine Schande, in einem schlecht regierten Reichtum. (Konfuzius)
|
|
Xion
Beiträge: 1952
Erhaltene Danke: 128
Windows XP
Delphi (2005, SmartInspect), SQL, Lua, Java (Eclipse), C++ (Visual Studio 2010, Qt Creator), Python (Blender), Prolog (SWIProlog), Haskell (ghci)
|
Verfasst: Mi 02.05.12 18:22
2. Ein Image, welches auf die Maus reagiert
Dieses Kapitel soll einen kleinen Einstieg geben in das Thema. Wir wollen der TImage Komponente Events beifügen, welche es ermöglichen, Mouse-Hover-Effekte zu erzeugen.
Dazu benötigen wir zwei Events: OnMouseEnter und OnMouseLeave, wie sie von anderen Komponenten bekannt sind. Diese bietet die TImage Komponente jedoch nicht von sich aus (Stand: Delphi 2005). Die Klasse bekommt diese Messages bereits zugeschickt, wertet diese aber nicht aus. Dies wollen wir nun ändern.
Unsere neue Klase sieht wie folgt aus:
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11:
| type TAdvImage = class(TImage) private VOnMouseLeave: TNotifyEvent; VOnMouseEnter: TNotifyEvent; procedure Handle_MouseEnter(var Msg: TMessage); message CM_MOUSEENTER; procedure Handle_MouseLeave(var Msg: TMessage); message CM_MOUSELEAVE; published property OnMouseEnter: TNotifyEvent read VOnMouseEnter write VOnMouseEnter; property OnMouseLeave: TNotifyEvent read VOnMouseLeave write VOnMouseLeave; end; |
Bis auf die Funktionen Handle_MouseEnter und Handle_MouseLeave zeigt dieser Code lediglich die Definition der Events.
Die beiden neuen Funktion sind dank der klaren Delphi-Syntax auch leicht zu verstehen. Trifft die Message CM_MOUSEENTER ein, so wird die Funktion Handle_MouseEnter automatisch aufgerufen. Wir erhalten dabei die Message als Parameter, diesen brauchen wir aber hier nicht, da wir lediglich die Callback-Funktion VOnMouseEnter aufrufen müssen. Nun haben wir schon ein funktionierendes Event. Das war einfach.
Einloggen, um Attachments anzusehen!
_________________ a broken heart is like a broken window - it'll never heal
In einem gut regierten Land ist Armut eine Schande, in einem schlecht regierten Reichtum. (Konfuzius)
|
|
Xion
Beiträge: 1952
Erhaltene Danke: 128
Windows XP
Delphi (2005, SmartInspect), SQL, Lua, Java (Eclipse), C++ (Visual Studio 2010, Qt Creator), Python (Blender), Prolog (SWIProlog), Haskell (ghci)
|
Verfasst: Mi 02.05.12 18:25
3. Befehle erteilen
Im letzten Kapitel waren wir Empfänger einer Message. Nun wollen wir aktiv werden und selbst etwas mitteilen. Als Beispiel entwickeln wir nun eine ferngesteuerte Stoppuhr, die man etwa zum Messen von Berechnungszeiten nutzen könnte.
Dazu erstellen wir zunächst ein Programm, welches die Stoppuhr implementiert.
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7: 8: 9: 10:
| type TStoppUhr = class(TForm) [...] private [...] public procedure Start(var msg: TMessage); message WM_CLOCKSTART; procedure Stop(var msg: TMessage); message WM_CLOCKSTOP; procedure Reset(var msg: TMessage); message WM_CLOCKRESET; end; |
Die verwendeten Messages sind dabei natürlich erstmal undefiniert. Wir müssen diesen Konstanten nun einen Zahlenwert zuordnen. Um nicht mit den Windows-internen Messages zu kollidieren, gibt es einen eigenen Nummernraum für uns Entwickler. Dieser beginnt bei WM_USER, so dass wir unsere Messages folgendermaßen definieren dürfen:
Delphi-Quelltext 1: 2: 3:
| const WM_CLOCKSTART=WM_USER+1; WM_CLOCKSTOP =WM_USER+2; WM_CLOCKRESET=WM_USER+3; |
Schnell haben wir nun die drei Funktionen implementiert und einen Timer erstellt, welche die aktuelle Zeit darstellt. Da noch niemand unserer StoppUhr eine Message sendet, tut sich natürlich erstmal nichts. Um dies zu ändern, werden wir nun einen Sender programmieren.
Zunächst müssen wir im Sender die Konstanten mit gleichen Werten definieren, damit unsere Stoppuhr auch die empfangenen Nachrichten versteht und richtig bearbeitet. Im Anhang kann wieder der volle Code betrachtet werden, ich werde hier nur auf die wesentliche Methode eingehen. Diese Methode erhält als Parameter die Nachricht, die wir senden wollen (z.B. WM_CLOCKSTART) und sieht wie folgt aus:
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7: 8: 9:
| procedure TSender.Send(msg: cardinal); var h: THandle; begin h:=FindWindow('TStoppUhr',nil); if h<>0 then PostMessage(h,Msg,0,0) else ShowMessage('Stoppuhr wurde leider nicht gefunden. Ist das entsprechende Programm gestartet?'); end; |
Laufen beide Programme, so kann nun die Stoppuhr ferngesteuert werden. Das war auch einfach.
Einloggen, um Attachments anzusehen!
_________________ a broken heart is like a broken window - it'll never heal
In einem gut regierten Land ist Armut eine Schande, in einem schlecht regierten Reichtum. (Konfuzius)
|
|
Xion
Beiträge: 1952
Erhaltene Danke: 128
Windows XP
Delphi (2005, SmartInspect), SQL, Lua, Java (Eclipse), C++ (Visual Studio 2010, Qt Creator), Python (Blender), Prolog (SWIProlog), Haskell (ghci)
|
Verfasst: Mi 02.05.12 18:28
4. Datenaustausch
Wenn es nicht genügt, nur Befehle zu erteilen, sondern auch Daten auszutauschen, so kann dies mit Hilfe der Message WM_COPYDATA erfolgen. Delphi stellt dabei bereits die nötigen Datentypen bereit.
Erweitern wir also unsere Stoppuhr um eine Logging-Funktion. In unserem Hauptprogramm ist beispielsweise ein Fehler aufgetreten, den wir nicht lokalisieren können. Breakpoints sind ungeeignet, da die Schleife sehr oft durchlaufen wird. Der Debugger stürzt leider auch jedesmal ab (nicht unwahrscheinlich mit Delphi 2005 ). Deshalb wollen wir in unserer Stoppuhr-Anwendung vermerken, bis zu welchem Wert die Schleife ordentlich gerechnet hat, um dem Fehler auf die Spur zu kommen. Wir erweitern also den Sender um folgende Funktion:
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
| procedure TSender.Log(text: String); var h: THandle; cds: TCopyDataStruct;begin
cds.dwData:=0; cds.cbData:=StrLen(PChar(text)) + 1; cds.lpData:=PChar(text); h:=FindWindow('TStoppUhr',nil); if h<>0 then SendMessage(h,WM_COPYDATA,Sender.Handle,Integer(@cds)); end; |
Zunächst verpacken wir den String in eine Struktur. Diese senden wir dann an die Stoppuhr-Anwendung. Achtung: SendMessage blockiert den Programmfluss nun solange, bis die Empfangsfunktion abgearbeitet ist.
Als Empfangsfunktion in unserer StoppUhr-Anwendung verwenden wir eine Funktion, die durch WM_COPYDATA ausgelöst wird. Diese liest den String ein und gibt ihn in einem Memo aus.
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7:
| procedure TStoppUhr.Log(var msg: TWMCopyData ); var sText: array [0..256] of Char; begin StrLCopy(sText, Msg.CopyDataStruct.lpData, Msg.CopyDataStruct.cbData); Memo1.Lines.Add(inttostr(msg.From)+': '+sText); end; |
Im Anhang ist das Beispiel wieder als funktionsfähiges Projekt zu finden.
Einloggen, um Attachments anzusehen!
_________________ a broken heart is like a broken window - it'll never heal
In einem gut regierten Land ist Armut eine Schande, in einem schlecht regierten Reichtum. (Konfuzius)
|
|
Xion
Beiträge: 1952
Erhaltene Danke: 128
Windows XP
Delphi (2005, SmartInspect), SQL, Lua, Java (Eclipse), C++ (Visual Studio 2010, Qt Creator), Python (Blender), Prolog (SWIProlog), Haskell (ghci)
|
Verfasst: Mi 02.05.12 18:30
5. Strukturierter Datenaustausch
(Dank an Luckie für sein Code-Snippet auf seiner Website)
Wir können bereits Strings zwischen Programmen austauschen. Theoretisch könnte man damit bereits alles realisieren, auch wenn es etwas Umstände machen würde, verschiedene Datentypen in einen String zusammenzufassen und beim Empfangen wieder zu zerlegen. Allerdings bietet uns Delphi eine einfachere Möglichkeit. Denn wir übergeben ja nur einen Pointer und eine Größe. Ob sich dahinter nun ein String versteckt oder ein record, ist unerheblich. Wichtig ist nur, dass beide Gesprächspartner die selbe Sprache sprechen, d.h. das selbe Format verwenden. Deshalb können wir auch ohne weiteres ein strukturierte Datenmenge als Record übergeben. Achtung: Im übergebenen Record sollten keine Referenzen/Pointer (z.B. auch dynamische arrays und strings) gespeichert sein, da diese in der empfangenden Anwendung keine Gültigkeit besitzen, da beide Prozesse in verschiedenen Speicherräumen arbeiten und nicht auf fremden Speicher zugreifen können.
Das vorherige Beispiel können wir nun flexibler durch ein Record realisieren:
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7:
| type TLogInfo = packed record Index: integer; Sum: real; Hint: string[32]; end; PLogInfo = ^TLogInfo; |
Dieses record deklarieren wir in beiden Programmen. Wir verwenden dabei (optional) ein packed record, da dieses weniger Speicherplatz benötigt. Der Zugriff darauf ist zwar tendenziell langsamer, jedoch müssen so weniger Daten mit der Nachricht transportiert werden.
Die Sende-Funktion muss nun etwas verändert werden, da wir explizit einen Pointer auf ein Record benötigen:
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24:
| procedure TSender.Log(index:integer; Sum: real); var h: THandle; cds: TCopyDataStruct; rec: PLogInfo; begin
GetMem(rec, sizeof(TLogInfo)); try rec.Index := Index; rec.Sum := Sum;
cds.dwData := 0; cds.cbData := sizeof(TLogInfo); cds.lpData := rec;
h:=FindWindow('TStoppUhr',nil); if h<>0 then SendMessage(h,WM_COPYDATA,Sender.Handle,Integer(@cds)); finally FreeMem(rec, sizeof(TLogInfo)); end; end; |
In der Empfangsfunktion können wir nun explizit auf die einzelnen Elemente des Records zugreifen.
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13:
| procedure TStoppUhr.Log(var msg: TWMCopyData ); var rec: TLogInfo; sum: real; index: integer; hint: String[32]; begin index := PLogInfo(msg.CopyDataStruct.lpData)^.index; sum := PLogInfo(msg.CopyDataStruct.lpData)^.sum; hint := PLogInfo(msg.CopyDataStruct.lpData)^.hint;
Memo1.Lines.Add('Status: "'+hint+'" Index:"'+inttostr(index)+' Sum: "'+floattostr(Sum)+'"'); end; |
Man kann sich leicht vorstellen, dass mit dieser Methode auch komplexere Protokolle realisiert werden können. Bei all den Möglichkeiten sollte man jedoch nicht vergessen, dass Messages lediglich innerhalb des Systems transportiert werden können, weshalb eine Nutzung von Sockets flexibler ist, falls die andere Anwendung eventuell irgendwann auf einem anderen Rechner laufen soll.
Einloggen, um Attachments anzusehen!
_________________ a broken heart is like a broken window - it'll never heal
In einem gut regierten Land ist Armut eine Schande, in einem schlecht regierten Reichtum. (Konfuzius)
|
|
Xion
Beiträge: 1952
Erhaltene Danke: 128
Windows XP
Delphi (2005, SmartInspect), SQL, Lua, Java (Eclipse), C++ (Visual Studio 2010, Qt Creator), Python (Blender), Prolog (SWIProlog), Haskell (ghci)
|
Verfasst: Mi 02.05.12 18:32
6. RegisterWindowMessage
(Dank an BenBE für den Hinweis auf diese Funktion)
Wie ihr vermutlich bereits gemerkt habt, haben wir bisher keinerlei Sicherheit was unseren Gesprächspartner angeht. Ein "Angreifer" (durchaus auch unbeabsichtigt) könnte sehr leicht unsere Stoppuhr manipulieren, wenn er nur die richtigen Message-Nummern an unser Programm sendet. Die statischen Nummern führen auch zu gewissen Problemen mit der Organisation der Kommunikation. Es muss immer jedem Gesprächspartner der aktuelle Satz an Messages bekannt sein, damit eine korrekte Kommunikation stattfinden kann. In unserem Beispiel ist dies noch kein Problem, da wir beide Programme selbst schreiben, jedoch kann dies in dynamischen Szenarien durchaus problematisch werden.
Um diesen Problemen entgegenzuwirken gibt es die Funktion RegisterWindowMessage. Diese bildet uns einen String auf eine eindeutige Nummer ab. Rufen wir die Funktion mit dem selben Parameter mehrmals auf, so erhalten wir jeweils die gleiche Nummer (dies kann auch unser Gesprächspartner tun und erhält die selbe Nummer wie wir). Umgekehrt werden verschiedenen Strings auch verschiedene Nummern zugewiesen.
Delphi-Quelltext 1:
| WM_CLOCKSTART:=RegisterWindowMessage('Give me the ClockStart-Message-Number'); |
Es gibt jetzt allerdings ein kleines Problem. Da WM_LOG nun offentsichtliche keine Konstante mehr sein kann, können wir nicht die schöne Delphi-Syntax nutzen:
Delphi-Quelltext 1:
| procedure Start(var msg: TMessage); |
Wir müssen somit den Aufruf von Start per Hand auslösen. Dazu überschreiben wir die Message-Handler-Funktion, also die Funktion, bei der intern die Messages ankommen:
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16:
| type TStoppUhr = class(TForm) [...] procedure WndProc(var msg : TMessage); override; end;
procedure TStoppUhr.WndProc(var msg : TMessage); begin if msg.msg=WM_CLOCKSTART then begin Start(msg); end else inherited WndProc(msg); end; |
Wir machen uns das Leben hierbei einfach: Ist die angekommene Message die richtige, so bearbeiten wir diese. Alle anderen geben wir einfach an die alte ( inherited) Funktion weiter, diese wird sich dann darum kümmern, genau wie bisher auch.
Im Anhang findet ihr wieder den Code zu diesem Kapitel.
Einloggen, um Attachments anzusehen!
_________________ a broken heart is like a broken window - it'll never heal
In einem gut regierten Land ist Armut eine Schande, in einem schlecht regierten Reichtum. (Konfuzius)
|
|
Xion
Beiträge: 1952
Erhaltene Danke: 128
Windows XP
Delphi (2005, SmartInspect), SQL, Lua, Java (Eclipse), C++ (Visual Studio 2010, Qt Creator), Python (Blender), Prolog (SWIProlog), Haskell (ghci)
|
Verfasst: Mi 02.05.12 18:36
7. Unfreiwilliger Zugriff
Dies ist das letzte und wohl auch das spannendste Kapitel. Vielleicht kennt ihr Programme wie PantsOff, mit denen man (manche) Passwort-Felder als Klartext darstellen kann, falls man mal wieder sein Passwort vergessen hat.
Dieses Szenario ist nicht wie unsere bisherigen Anwendungen, welche dafür ausgelegt waren, miteinander zu sprechen. Oft sieht man sich dem Problem gegenüber, dass ein fremdes Programm Funktionalitäten bietet, die man für sein eigenes Programm in irgend einer Form nutzen möchte, bzw. dass man eine Erweiterung für dieses Programm entwickeln möchte, dies jedoch nicht unterstüzt wird.
Messages bieten da gewisse Möglichkeiten, in manchen Fällen doch an die gewünschten Informationen zu kommen oder Aktionen durchzuführen. Da auch User-Interaktionen über Messages verbreitet werden (wie wir bei unserem TImage gesehen haben), können wir einen User simulieren. Es gibt dort auch einige mächtigere Instrumente, wie z.B. Text aus einem Edit-Feld auslesen. Das ganze hat jedoch seine Grenzen, da wir nur Nachrichten an Komponenten senden können, welche ein Handle besitzen. Dies ist bei einem Label z.B. nicht der Fall, so dass wir von diesem leider keine Informationen gewinnen können.
In unserem Beispiel (siehe Anhang) haben wir ein Programm, welches eine Berechnung durchführt und ausgibt. Die Berechnung kann eine Weile dauern und man muss schnell hinschauen um sie zu sehen. Uns wäre es lieber, wenn das Tool die Möglichkeit bieten würde, die Ausgabe z.B. in eine Datei zu loggen, doch dies ist nicht gegeben.
Durch unsere neuen Kenntnisse haben wir aber eine Möglichkeit, das Ergebnis zu loggen.
Wir nutzen also FindWindow (Nutzen wir den zweiten Parameter, so können wir leicht den Fenstertitel verwenden) um das Handle zu bekommen. Mit FindWindowEx können wir ein "Unterfenster" ermitteln, welches auch ein Edit-Feld wie in unserem Beispiel sein kann.
Anschließend senden wir eine Nachricht an das Edit, um die Textlänge zu ermitteln und lesen anschließend den Text.
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24:
| procedure TSniffer.Timer1Timer(Sender: TObject); var h: THandle; TextLength: integer; Text: String; begin if CheckBox1.Checked then begin h := FindWindow('TDeepThought',nil); if (h<>0) then begin h:=FindWindowEx(h,0,'TEdit',nil); if (h<>0) then begin TextLength := SendMessage(h, WM_GETTEXTLENGTH, 0, 0) ; if TextLength>0 then if (TextLength<>0) then begin SetLength(Text,TextLength+1);
if SendMessage(H,WM_GETTEXT,TextLength+1,Integer(Pointer(Text)))>0 then if Pos('The Answer Is:',Text)>0 then Memo1.Lines.Add(Text); end; end; end; end; end; |
Mit folgender Zeile können wir auch etwas in das Edit einfügen, was allerdings in diesem Fall wenig Sinn macht:
Delphi-Quelltext 1:
| SendMessage(h,WM_SETTEXT,0,Integer(Pointer(Text))) |
Da man bei fremden Programmen nicht die Struktur und die Klassen kennt, kann man das kostenlose Programm WinSpector nutzen.
Mit diesen einfachen Mitteln habt ihr ein mächtiges Werkzeug an der Hand und könnt nun kreativ fremde Programme um nützliche Fähigkeiten erweitern. Ihr könnt auch Tastatur-Kommandos senden und so z.B. Aktionen starten oder Buttons drücken. In der unit Messages kann man sich von einer großen Liste Messages inspirieren lassen.
Ich hoffe euch hat dieses Tutorial weitergeholfen und zumindest etwas Spaß gemacht.
Einloggen, um Attachments anzusehen!
_________________ a broken heart is like a broken window - it'll never heal
In einem gut regierten Land ist Armut eine Schande, in einem schlecht regierten Reichtum. (Konfuzius)
Für diesen Beitrag haben gedankt: bole, DonManfred
|
|
glotzer
Beiträge: 393
Erhaltene Danke: 49
Win 7
Lazarus
|
Verfasst: Mi 02.05.12 20:36
WOW wirklich tolles Tutorial, großes Lob dafür
_________________ ja, ich schreibe grundsätzlich alles klein und meine rechtschreibfehler sind absicht
|
|
Jensen K.
Hält's aus hier
Beiträge: 1
|
Verfasst: Mo 07.05.12 15:03
|
|
uall@ogc
Beiträge: 1826
Erhaltene Danke: 11
Win 2000 & VMware
Delphi 3 Prof, Delphi 7 Prof
|
Verfasst: Mo 07.05.12 23:18
Leicht umständlich das mit dem LogInfo oder nicht?
Wie wärs mit:
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20:
| procedure TSender.Log(index:integer; Sum: real); var h: THandle; cds: TCopyDataStruct; rec: TLogInfo; begin rec.Index := Index; rec.Sum := Sum; rec.Hint := 'irgendwas';
cds.dwData := 0; cds.cbData := sizeof(rec); cds.lpData := @rec;
h:=FindWindow('TStoppUhr',nil); if h <> 0 then SendMessage(h,WM_COPYDATA,Sender.Handle,Integer(@cds)); end; |
und
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7:
| procedure TStoppUhr.Log(var msg: TWMCopyData ); var rec: PLogInfo; begin rec := PLogInfo(msg.CopyDataStruct.lpData); Memo1.Lines.Add('Status: "'+rec.hint+'" Index:"'+inttostr(rec.index)+' Sum: "'+floattostr(rec.Sum)+'"'); end; |
_________________ wer andern eine grube gräbt hat ein grubengrabgerät
- oder einfach zu viel zeit
|
|
|