[Tutor] API-Aufrufe in vb.net/2005/2008 + Kommunikation über Windows-Messages

    • VB.NET

      [Tutor] API-Aufrufe in vb.net/2005/2008 + Kommunikation über Windows-Messages

      Verfasst von Gaga.
      Nochmal ein großes Dankeschön :)


      API-Aufrufe mit NET / Kommunikation über Windows-Messages


      Intuition:

      Eigentlich wollte ich ja nur ein kleines Tutorial zum Thema API-Aufrufe mit Net (VB.net, VB.net 2003, VB 2005, VB 2008)
      schreiben. Da man ja in Net viele Fälle, bei denen in VB6 APIs unerläßlich waren, mit den Bordmitteln lösen kann, braucht
      man bei Net in den seltensten Fällen APIs. Da hier aber in letzter Zeit häufig Fragen dazu aufgetreten sind (weil die
      VB6-Declares in Net nicht funktionierten etc) habe ich beschlossen. dieses Tutorial zu schreiben.



      Einleitung:

      Also machte ich mich auf die Suche nach einem Beispiel, bei dem man in Net wirklich APIs verwenden muß um das Gewünschte
      zu erreichen. Ziemlich alles innerhalb der eigenen Anwendung und bei der Kommunikation mit dem Betriebssystem kann man
      ja bei Net mit Bordmitteln erreichen, dazu braucht man - im Gegensatz zu VB6 - keine APIs.
      Was aber (meines Wissens nach) nach wie vor nicht mit Bordmitteln geht ist der Zugriff auf fremde Programme.

      Die dabei wohl am häufigsten verwendete API ist SendMessage. Also die API habe ich gefunden, aber welche Message schicke
      ich in dem Beispiel und vor allem an wen? Da fiel mir ein, dass man ja selbst eigene Windows-Messages definieren kann
      und solche selbst definierten Messages natürlich auch empfangen kann. Also definiere ich einfach ein paar Messages und
      schicke die an eine andere Instanz des Testprogramms.

      Herausgekommen ist ein Beispiel, wie zwei in Net geschriebene Programme mit Hilfe von Windows-Messages kommunizieren können
      Selbstverständlich können dabei mit den Parametern auch Daten übergeben werden.
      Der Parameter wParam ist eigentlich bei jeder Windows-Message vom Typ ByVal As Integer, aber der Parameter lParam kann bei
      den Windows-Messages die verschiedensten Sachen beinhalten, oft einen Zeiger auf irgendwas. Also kann man mit diesem
      Parameter so ziemlich alles übergeben was man will. Man muß es nur richtig machen.

      Ich habe im Beispiel drei Messages definiert, eine ByVal wParam as Integer und ByVal lParam As Integer,
      eine mit ByVal wParam As Integer und ByRef lParam As Integer und die dritte mit ByVal wParam as Integer
      und ByVal lParam As String.

      Mit der ersten Message kann man also zwei Zahlen übergeben, mit der zweiten kann das aufgerufene Programm eine der Zahlen
      ändern und wieder zurückgeben, mit der dritten wird ein String übergeben.

      Mir ist natürlich klar, dass sich auch mit Bordmitteln zwei Net-Programme unterhalten können aber hier gehts mir ja
      hauptsächlich um die Verwendung von SendMessage. Ich schicke also an ein anderes Programm eine Message das die auch
      versteht und entsprechend handelt. Dass es sich hier zufällig zweimal um das gleiche Programm handelt hat keinen Einfluß
      auf die Handhabung der APIs.

      Da die Kommunikation über Windows-Messages läuft ist es völlig egal, mit welcher Programmiersprache die entsprechenden
      Programme geschrieben sind. Man kann damit problemlos ein Net-Programm mit einem VB6-Programm oder mit einem C++-Programm
      oder mit einem Delphi-Programm etc unterhalten lassen.
      Wichtig ist nur, dass die Programme die jeweilige Message kennen und dass der Sender das Windowhandle des Empfängers kennt.
      Und für diese Fälle (Message registrieren, Windowhandle suchen, Message senden/auswerten) braucht man auch bei Net die APIs.


      Vorbemerkungen:

      Wenn man für ein Net-Problem eine Lösung sucht und auf VB6-Code stößt in dem APIs verwendet werden, kann man sich da
      natürlich den grundsätzlichen Lösungsansatz ansehen. Bevor man allerdings dann anfängt einfach den VB6-Code nach Net
      umzuschreiben, sollte man sich darüber Gedanken machen ob nicht die Funktionalität die die APIs liefern bereits im
      Framework enthalten ist. Wenn das der Fall ist kommt man mit Bordmitteln meißt schneller ans Ziel und erreicht besseren
      Code wie mit APIs.
      Also: Alles was zB mit Registry, Multithreading, Betriebssysteminformationen, Subclassing, Steuerelemente (auf der
      eigenen Form) etc zu tun hat kann normalerweise mit Bordmitteln gelöst werden. Der Grund warum man dazu viel Code im
      Internet findet ist einfach der, dass man bei VB6 hier oft APIs verwenden muss weil es in VB6 dafür keine Funktionen gibt.


      Bevor ich mit dem Beispiel anfange gibts erstmal graue Theorie damit die Hintergründe und Zusammenhänge klar sind.



      Allgemeines zu API-Funktionen

      Da die APIs ja bei Net und VB6 die gleichen sind, gelten natürlich die gleichen Regeln.
      In diesen beiden Beiträgen habe schon mal für VB6 beschrieben, worauf man bei der Verwendung von APIs achten muß.
      API Aufrufe, warum das so "schwierig" ist
      String-Rückgabe
      Man kann jetzt diese Beispiele nicht einfach so in ein Net-Projekt kopieren, die werden dort nicht das Gewünschte tun.
      Was aber nach wie vor wichtig und gültig ist:
      Entscheidend für den Erfolg ist, daß die richtige Anzahl an Bytes mit dem richtigen Inhalt übergeben wird.



      VB6-Declares für Net ändern

      Da es ja für Net keinen API-Viewer gibt, die im Internet verfügbaren Viewer eigentlich auch alle auf VB6 basieren
      und der größte Teil der API-Beispiele und Deklarationen im Internet VB6-Code ist, muss man den eben an Net anpassen
      wenn man in Net APIs verwenden will.

      Woher erkennt man nun, ob man einen Declare für VB6 hat oder ob der bereits für Net umgeschrieben wurde?
      Die einfachste Erkennungsart ist der Datentyp der Rückgabe.
      Bei VB6 ist der Rückgabewert immer ein Long, bei Net immer ein 4 Byte Typ wie zB Integer, IntPtr, UInt32...
      Es gibt in der gesamten 32bit-Windows-API keine einzige Funktion die etwas anderes zurückliefert wie void (= nichts,
      also Sub) oder irgendeinen 4 Byte-Datentyp.


      Unbedingt zu Beachten:
      NIE die Declares direkt vom Browser oder API-Viewer in die Net-IDE kopieren.
      Bei VB6 wird ein Parameter bei dem kein Modifikator wie ByVal oder ByRef angegeben ist mit ByRef übertragen.
      Wenn man allerdings einen Declare in die Net-IDE kopiert macht Net bei diesen Parametern ein ByVal davor.
      Deshalb: Vom Browser oder API-Viewer in einen Editor kopieren und bei jedem Parameter ein ByRef davorsetzen
      bei dem nichts angegeben ist. Wenn das gemacht ist kann man die Declares vom Editor in die Net-IDE kopieren.


      Da bei VB6 der Datentyp Long 4 Byte groß ist, in Net hingegen 8 Byte und der Integer 4 Byte, muß man das entsprechend
      anpassen.

      Unbedingt zu Beachten:
      Alle Parameter und Rückgabewerte die mit Long deklariert sind durch einen passenden Net-4 Byte-Datentyp ersetzen.
      Im Zweifelsfall kann man immer Integer verwenden aber oft kann man auch einen spezielleren Typ nehmen.
      Wichtig ist in diesem Zusammenhang immer, wie die API-Funktion definiert ist, im Zweifelsfall in der Doku zur API
      nachsehen.

      Hier eine Gegenüberstellung der numerischen Wertetypen

      Brainfuck-Quellcode

      1. API (C) VB6 Net alternativ
      2. -------------------------------------------------------------------------------------------------
      3. int, INT ByVal As Long ByVal As Integer
      4. UINT ByVal As Long ByVal As UInt32 ByVal As Integer
      5. BOOL ByVal As Long ByVal As Integer ByVal As UInt32
      6. WORD ByVal As Integer ByVal As UInt16 ByVal As Int16
      7. DWORD ByVal As Long ByVal As UInt32 ByVal As Integer ByVal As IntPtr
      8. WPARAM ByVal As Long ByVal As Integer
      9. LRESULT ByVal As Long ByVal As IntPtr ByVal As Integer
      10. COLORREF ByVal As Long ByVal As Integer
      11. ATOM ByVal As Integer ByVal As UInt16 ByVal As Int16
      12. HANDLE (und Verwandte) ByVal As Long ByVal As IntPtr ByVal UInt32 ByVal As Integer
      13. BYTE ByVal As Byte ByVal As Byte
      14. char ByVal As Byte ByVal As Byte
      15. LPARAM ByVal As Long ByVal As Integer ByVal As IntPtr ByVal As UInt32

      Bei einigen Typen kann man auch andere Datentypen verwenden, je nach dem was in der Doku steht.
      Wenn ein Parameter nämlich eine Speicheradresse von einem String oder einem bestimmten Struct beinhaltet, kann
      man diesen Parameter auch mit ByVal As String oder ByRef As [spezieller Typ] definieren.


      Hier eine Gegenüberstellung der numerischen Referenztypen

      Brainfuck-Quellcode

      1. API (C) VB6 Net alternativ
      2. -------------------------------------------------------------------------------------------------
      3. LPINT, int* ByRef As Long ByRef As Integer
      4. LPUINT, UINT* ByRef As Long ByRef As UInt32 ByRef As Integer
      5. LPBOOL, BOOL* ByRef As Long ByRef As Integer ByRef As UInt32
      6. LPBYTE, BYTE* ByRef As Byte ByRef As Byte
      7. LPWORD, WORD* ByRef As Integer ByRef As Uint16 ByRef As Int16
      8. LPDWORD, DWORD* ByRef As Long ByRef As UInt32 ByRef As Integer ByRef As IntPtr
      9. LPHANDLE, HANDLE* ByRef As Long ByRef As IntPtr ByRef UInt32 ByRef As Integer
      10. LPARAM ByRef As Long ByRef As Integer ByRef As IntPtr ByRef As UInt32

      Den Typ LPARAM habe ich hier absichtlich bei beiden Tabellen aufgenommen. Dieser Typ wird nur in den
      APIs SendMessage und PostMessage verwendet und kann ganz unterschiedliche Daten beinhalten.
      Hier ist entscheidend, was in der Doku zur jeweiligen Message steht.

      Alle hier aufgeführten Referenztypen können in VB6 auch mit ByVal As Long deklariert sein. In diesem Fall
      muß man dann beim Aufruf mit der VB6-Fkt VarPtr die richtige Speicheradresse übergeben.
      In Net sollte man bei solchen Fällen den Parameter mit dem richtigen Typ deklarieren. Wenn man eine API
      öfters mit unterschiedlichen Parametern braucht kann man die ja mehrmals mit dem gleichen Namen deklarieren
      (was in VB6 nicht möglich ist).
      Da es die VarPtr-Fkt in Net nicht gibt, kann man sich in diesen Fällen auch mit der GCHandle-Technik behelfen:
      Variablenadressen a la ObjPtr mit VB.NET

      Zusätzlich gibt es bei APIs noch den Typ LPVOID, void* der einfach nur angibt dass es sich um eine
      Speicheradresse handelt. In VB6 wird dieser Typ oft mit ByRef As Any deklariert oder mit ByVal As Long.
      Bei der Any-Deklaration sorgt (hoffentlich) der Compiler dafür, dass das richtige übergeben wird, bei der Long-Deklaration
      der Programmierer (mit Hilfe von VarPtr). Den Any-Typ (den VB6 nur in Declares zulässt) gibt es in Net nicht mehr.
      Hier kann man wieder genauso mehrere Declares mit den genauen Typen schreiben oder den VarPtr-Ersatz
      GCHandle (mit ByVal As IntPtr/UInt32/Integer) verwenden.



      Strings

      Bei Strings verlangt eine API grundsätzlich die Speicheradresse an der das erste Zeichen ist.
      In VB erreicht man das dadurch, dass man den Parameter mit ByVal As String übergibt. Da auch in VB6 und Net der
      Inhalt der Stringvariablen die Speicheradresse des Strings ist, funktioniert das. Ebenso können solche Parameter in
      VB6 wieder mit ByVal As Long deklariert sein, dann wird beim Aufruf mit StrPtr die richtige Speicheradresse übergeben.
      Als dritte Möglichkeit gibt es noch die Deklaration mit ByRef As Byte bei der dann das erste Element eines Bytearrays
      übergeben wird.
      In Net hat man hier im Prinzip genau die gleichen Möglichkeiten, man muß beim Umschreiben nur darauf achten, für weche
      dieser Möglichkeit man sich entscheidet weil davon der Aufruf der API entscheidend abhängt.

      Wenn eine API einen String zurückliefert muß man vorher dafür sorgen, dass dieser String auch genügend Platz bietet.
      Man muß den String also vor dem Aufruf zB mit der Space-Fkt auf die benötigte Größe setzen. Daran hat sich von VB6
      auf Net nichts geändert weil diese Forderung von der API kommt.
      Wie man die benötigte Größe ermittelt ist je nach API unterschiedlich und steht normalerweise in der zugehörigen Doku.
      Auch in diesem Fall wird der Parameter mit ByVal As String deklariert weil dann ja die Speicheradresse übergeben wird.



      Benutzerdefinierte Typen

      Bei der Übergabe von Typen (API: struct, VB6: Type, Net: Structure) gilt im Prinzip das gleiche, man muß dafür sorgen
      dass die Datentypen im Structure passen und darauf achten wie der entsprechende Parameter deklariert ist.
      (idR ByRef As StructureName, man kann aber auch die erwähnte ByVal As Long/IntPtr-Lösung nehmen)



      Jetzt gehts endlich los, die Umsetztung des Beispiels

      Wie ich ja am Anfang bereits angesprochen habe, gibt es bei diesem Beispiel mehrere Bereiche in denen APIs verwendet
      werden müssen. Ich habe das alles in der Form gemacht, für eine professionelle Anwendung kann man die Funktionalitäten
      natürlich in entsprechende Klassen auslagern.
      Dieses Beispiel ist mit VB 2005 geschrieben.


      Teil 1: Message registrieren

      Damit man eine Windows-Message versenden und empfangen kann muß diese Message sowohl dem Betriebssystem als auch dem
      Programm bekannt sein. Mein Beispiel verwendet drei Messages die im Konstruktor der Form registriert werden.

      Die Deklaration der Messages

      VB.NET-Quellcode

      1. Private WM_MESSAGEINT As Integer
      2. Private WM_MESSAGESTRING As Integer
      3. Private WM_MESSAGEREF As Integer

      und die Registrierung

      VB.NET-Quellcode

      1. Public Sub New()
      2. '// zuallererst die Messages registrieren
      3. WM_MESSAGEINT = RegisterWindowMessage("My_WM_Message_int")
      4. WM_MESSAGESTRING = RegisterWindowMessage("My_WM_Message_string")
      5. WM_MESSAGEREF = RegisterWindowMessage("My_WM_Message_ref")
      6. '// Windows Form-Designer
      7. InitializeComponent()
      8. End Sub

      Die API RegisterWindowMessage meldet die Message bei Windows an und liefert den zugehörigen Identifer. Diese Identifer
      werden vom Betriebssystem vergeben und können sich bei verschiedenen Programm-/Rechnerstarts unterscheiden. Deshalb müssen
      diese Messages bei jedem Start angemeldet werden und können nicht als Konstante definiert werden.
      Über den Parameter wird dem Betriebssystem mitgeteilt, wie die Message heißen soll (für das Betriebssystem, welche
      internen Bezeichnungen die einzelnen Programme dafür haben ist für Windows irrelevant).
      Wenn eine Message einmal registriert wurde und ein anderes Programm die gleiche Message (gleicher Paramter) registrieren
      will bekommt es den Identifer der bereits registrierten Message. Somit haben also zwei verschiedene Programme den
      gleichen Identifer für die gleiche Message (sonst würde das ganze ja nicht funktionieren).

      So, jetzt zur RegisterWindowMessage

      In VB6 sieht die Deklaration so aus

      VB.NET-Quellcode

      1. Private Declare Function RegisterWindowMessage Lib "user32" Alias "RegisterWindowMessageA" _
      2. (ByVal lpString As String) As Long

      für Net braucht man hier nur den Long ersetzen

      VB.NET-Quellcode

      1. Private Declare Function RegisterWindowMessage Lib "user32" Alias "RegisterWindowMessageA" _
      2. (ByVal lpString As String) As Integer



      Teil 2; Windowhandle suchen

      Damit man eine Message zum richtigen Empfänger schicken kann braucht man dazu das Windowhande. Ich habe es mir hier
      relativ einfach gemacht, ich suche einfach nach einem Window das den gleichen Windowtext und ein anderes Windowhandle
      hat wie ich selbst. Für professionelle Anwendungen ist dieser Teil verbesserungswürdig.


      Die Suchfunktion

      VB.NET-Quellcode

      1. Private Function AppPrevInstance() As IntPtr
      2. '// Windowhandle der anderen Anwendung holen
      3. Dim hwndOther As IntPtr = FindWindow(vbNullString, Me.Text)
      4. If hwndOther = Me.Handle Then
      5. '// das bin ich selbst -> weitersuchen
      6. Do Until hwndOther = CType(0, IntPtr)
      7. hwndOther = GetWindow(hwndOther, GW_HWNDNEXT)
      8. If WindowText(hwndOther) = Me.Text Then
      9. Return hwndOther
      10. End If
      11. Loop
      12. Else
      13. Return hwndOther
      14. End If
      15. Return CType(0, IntPtr)
      16. End Function

      Hier wird einfach FindWindow ohne Klassenname aufgerufen. Es gibt an dieser Stelle ja zwei Windows mit dem gleichen
      Windowtext (eben ich und der Empfänger). Da nicht sichergestellt ist, welches Window zuerst gefunden wird muß man
      dann manuell weitersuchen wenn FindWindow mein Windowhandle zurückliefert.

      Zum Ermitteln des WindowTexts wird die Fkt WindowText verwendet die anschließend besprochen wird.

      Hier die APIs

      VB6

      VB.NET-Quellcode

      1. Private Const GW_HWNDNEXT As Long = 2
      2. Private Declare Function GetWindow Lib "user32" _
      3. (ByVal hwnd As Long, ByVal wCmd As Long) As Long
      4. Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
      5. (ByVal lpClassName As String, _
      6. ByVal lpWindowName As String) As Long


      Net

      VB.NET-Quellcode

      1. Private Const GW_HWNDNEXT As Integer = 2
      2. Private Declare Function GetWindow Lib "user32" _
      3. (ByVal hwnd As IntPtr, ByVal wCmd As Integer) As IntPtr
      4. Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
      5. (ByVal lpClassName As String, _
      6. ByVal lpWindowName As String) As IntPtr

      Hier wurden die VB6-Longs durch verschiedene Net-Typen ersetzt. Da FindWindow und GetWindow jeweils ein Windowhandle
      zurückliefern kann man den Typ entsprechend als IntPtr definieren.
      Der erste Parameter von GetWindow will das Windowhandle von dem Window, bei dem die Suche begonnen werden soll und ist
      demzufolge auch ein IntPtr. Der zweite Parameter übernimmt die Konstante nach was gesucht werden soll. Hier muß nur der
      Typ der Konstanten zu dem im Declare angegebenen Typ passen und ein 4 Byte Typ sein (hier Integer).


      Bei der Hilfsfunktion WindowText kommen wir auf den Fall, dass die API einen String zurückliefert.
      Das übernimmt die API GetWindowText.

      VB.NET-Quellcode

      1. Private Function WindowText(ByVal hwnd As IntPtr) As String
      2. '// gibt den Text des Windows zurück
      3. Dim strBuffer As String
      4. Dim intLen As Integer
      5. intLen = GetWindowTextLength(hwnd) + 1
      6. strBuffer = Space$(intLen)
      7. GetWindowText(hwnd, strBuffer, intLen)
      8. strBuffer = Replace(strBuffer, Chr(0), "")
      9. Return strBuffer
      10. End Function

      Wie ja bereits angesprochen, muß vor dem Aufruf der String in entsprechender Größe angelegt werden. Wie groß dieser
      String sein soll liefert in diesem Fall die API GetWindowTextLength. Der Korrekturwert +1 ist deshalb da damit auch
      sicher noch für das abschließende 0-Byte Platz ist.
      Solche Konstruktionen, dass eine API den benötigten Wert für eine andere API zurückliefert gibt es nicht oft.
      Viele APIs die Strings zurückliefern verwenden andere Techniken mit denen man die benötigte Größe ermitteln kann.
      In der Doku zur jeweiligen API steht das idR dabei wie man die Größe ermitteln kann oder ob es eine Maximalgröße gibt.

      Zu den Declares gibts eigentlich nicht viel zu sagen, Long zu Integer oder IntPtr

      VB6

      VB.NET-Quellcode

      1. Private Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" _
      2. (ByVal hwnd As Long, ByVal lpString As String, _
      3. ByVal cch As Long) As Long
      4. Private Declare Function GetWindowTextLength Lib "user32" Alias "GetWindowTextLengthA" _
      5. (ByVal hwnd As Long) As Long

      Net

      VB.NET-Quellcode

      1. Private Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" _
      2. (ByVal hwnd As IntPtr, ByVal lpString As String, _
      3. ByVal cch As Integer) As Integer
      4. Private Declare Function GetWindowTextLength Lib "user32" Alias "GetWindowTextLengthA" _
      5. (ByVal hwnd As IntPtr) As Integer



      Teil 3: Messages verschicken

      Jetzt haben wir sowohl die Message als auch den Empfänger, also können wir dem eine Message schicken.
      Die zu verschickenden Daten (Parameter) werden über Textboxen eingegeben und die verschickten Daten
      werden zur Kontrolle in ein ListView ausgegeben. Für jede der drei Messages gibts einen eigenen Button
      mit dem diese Message verschickt wird.

      Hier die Routine mit der die erste Message (ByVal wParam as Integer und ByVal lParam As Integer) gesendet wird

      VB.NET-Quellcode

      1. Private Sub btnInt_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnInt.Click
      2. Dim wParam As Integer = 1
      3. Dim lParam As Integer = CInt(Me.txtZahl.Text)
      4. Dim hwndOther As IntPtr = AppPrevInstance()
      5. If hwndOther = CType(0, IntPtr) Then Exit Sub
      6. Dim intReturn As Integer = SendMessage(hwndOther, WM_MESSAGEINT, wParam, lParam)
      7. Dim litNew As ListViewItem = Me.lvwSended.Items.Add(intReturn.ToString)
      8. litNew.SubItems.Add("WM_MESSAGEINT")
      9. litNew.SubItems.Add(wParam.ToString)
      10. litNew.SubItems.Add(lParam.ToString)
      11. End Sub

      Die Routine für die zweite Message (ByVal wParam As Integer und ByRef lParam As Integer) sieht genauso aus nur dass
      dort eben die Deklaration SendMessageRef und die Message WM_MESSAGEREF verwendet und ein anderer Wert beim wParam
      übergeben wird. Die großen Unterschiede zwischen WM_MESSAGEINT und WM_MESSAGEREF sind auf der Empfängerseite.

      Bei der Routine für die dritte Message (ByVal wParam as Integer und ByVal lParam As String) ist eigentlich nur der
      Wert des wParams wirklich anders und der lParam natürlich ein String. In diesem Fall übergebe ich im wParam die Länge
      des Strings, näheres dazu bei der Beschreibung des Empfängers.

      VB.NET-Quellcode

      1. Private Sub btnString_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnString.Click
      2. '// im wParam die Länge des Strings übergeben
      3. Dim wParam As Integer = Me.txtString.Text.Length
      4. '// im lParam den String
      5. Dim lParam As String = Me.txtString.Text
      6. Dim hwndOther As IntPtr = AppPrevInstance()
      7. If hwndOther = CType(0, IntPtr) Then Exit Sub
      8. Dim intReturn As Integer = SendMessage(hwndOther, WM_MESSAGESTRING, wParam, lParam)
      9. Dim litNew As ListViewItem = Me.lvwSended.Items.Add(intReturn.ToString)
      10. litNew.SubItems.Add("WM_MESSAGESTRING")
      11. litNew.SubItems.Add(wParam.ToString)
      12. litNew.SubItems.Add(lParam.ToString)
      13. End Sub


      Mit den verwendeten APIs ist's jetzt nicht mehr so einfach wie bisher, hier wären durchaus auch andere Declares
      möglich (die dann auch andere Aufrufe benötigten)

      VB6

      VB.NET-Quellcode

      1. Private Declare Function SendMessageI Lib "user32" Alias "SendMessageA" _
      2. (ByVal hwnd As Long, ByVal wMsg As Long, _
      3. ByVal wParam As Long, ByVal lParam As Long) As Long
      4. Private Declare Function SendMessageS Lib "user32" Alias "SendMessageA" _
      5. (ByVal hwnd As Long, ByVal wMsg As Long, _
      6. ByVal wParam As Long, ByVal lParam As String) As Long
      7. Private Declare Function SendMessageR Lib "user32" Alias "SendMessageA" _
      8. (ByVal hwnd As Long, ByVal wMsg As Long, _
      9. ByVal wParam As Long, ByRef lParam As Long) As Long

      Net

      VB.NET-Quellcode

      1. Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" _
      2. (ByVal hwnd As IntPtr, ByVal wMsg As Integer, _
      3. ByVal wParam As Integer, ByVal lParam As Integer) As Integer
      4. Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" _
      5. (ByVal hwnd As IntPtr, ByVal wMsg As Integer, _
      6. ByVal wParam As Integer, ByVal lParam As String) As Integer
      7. Private Declare Function SendMessageRef Lib "user32" Alias "SendMessageA" _
      8. (ByVal hwnd As IntPtr, ByVal wMsg As Integer, _
      9. ByVal wParam As Integer, ByRef lParam As Integer) As Integer

      Wie man sieht wird dreimal die gleiche API verwendet (gleicher Alias-Name) allerdings immer mit einer anderen
      Deklaration. In VB6 muß man den einzelnen Versionen verschiedene Namen geben, in Net muß das nur bei den beiden
      lParam As Integer sein. Eigentlich ist es nicht wichtig ob die nun verschiedene Namen haben oder nicht, das wichtige
      in dem Fall ist das, dass die gleiche API in der gleichen Klasse mehrmals unterschiedlich deklariert ist.
      Wie ich ja anfangs bereits gesagt habe, kann beim lParam irgendwas übergeben werden. Genau das trifft hier zu.
      Einmal ein Integer als Wert, einmal ein Integer als Referenz und einmal ein String. In solchen Fällen muß man bei
      der Deklaration genau darauf achten, dass man die Parameter so deklariert wie man sie braucht und die API damit
      auch das richtige tut. Die Unterschiede zwischen den drei Messages kommen genau von diesen unterschiedlichen
      Deklarationen. Beim Versenden einer Message wird durch die verwendete Deklaration festgelegt, was beim Empfänger
      ankommt.

      In VB6 würde ich in diesem Fall genau eine Deklaration verwenden, nämlich die mit ByVal lParam As Long und bei
      den anderen Aufrufen eben mit VarPtr und StrPtr arbeiten. In Net hingegen würde ich genau die hier gezeigten
      Deklarationen verwenden. Das sollte man bei Umschreiben von VB6-Declares nach Net auch beachten wenn mehrere
      verschiedene Declares möglich sind. Eine Deklaration die in VB6 Vorteile bietet kann in Net durchaus Nachteile
      haben. Je nach dem kann es besser sein, in Net andere Declares zu verwenden wie in VB6.



      Teil 4: Messages empfangen

      Jetzt gehts ums Empfangen der Messages. Das muß man in der Windowprocedure machen die alle Messages bekommt, also
      auch unsere selbst definierten. Die Technik, eine eigene Windowprocedure zu schreiben nennt man Subclassing.
      In VB6 muß man dazu mit APIs arbeiten um die eigene Windowprocedure zum arbeiten zu bekommen.
      Eine detailierte Anleitung dazu gibts hier:
      Was ist SubClassing
      Die gleiche Technik funktioniert natürlich auch in Net wenn man die Datentypen entsprechend anpasst.
      Aber das muß nicht sein! Hier haben wir ein schönes Beispiel dafür, dass Sachen in Net mit Bordmitteln gemacht werden
      können für die in VB6 APIs notwendig sind. In Net überschreiben wir einfach die Sub WndProc und das wars dann.

      VB.NET-Quellcode

      1. Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
      2. Dim ptrReturn As IntPtr
      3. Select Case m.Msg
      4. '//
      5. '// Die eigenen Messages behandeln
      6. '//
      7. Case WM_MESSAGEINT
      8. '//...
      9. Case WM_MESSAGESTRING
      10. '//...
      11. Case WM_MESSAGEREF
      12. '//...
      13. '// alle anderen Messages von der ursprünglichen WndProc behandeln lassen
      14. Case Else
      15. MyBase.WndProc(m)
      16. End Select
      17. End Sub

      Auch hier machen wir wieder eine Ausgabe in ein Listview damit man sieht, was alles empfangen wurde.
      Die einzelnen Parameter die bei SendMessage angegeben wurden bekommen wir als Eigenschaften des Message-Objekts

      Die erste Message (ByVal wParam as Integer und ByVal lParam As Integer)

      VB.NET-Quellcode

      1. Case WM_MESSAGEINT
      2. '// Rückgabewert vorbelegen
      3. ptrReturn = CType(2, IntPtr)
      4. '// Listvieweintrag machen
      5. Dim litNew As ListViewItem = Me.lvwReceived.Items.Add(ptrReturn.ToString)
      6. litNew.SubItems.Add("WM_MESSAGEINT")
      7. litNew.SubItems.Add(m.WParam.ToString)
      8. litNew.SubItems.Add(m.LParam.ToString)
      9. '// Rückgabewert zurückgeben
      10. m.Result = ptrReturn

      Hier ist die Sache einfach, da alle Parameter ByVal übergeben wurden bekommen wir hier direkt die Werte
      Im m.Result kann man einen Wert an den Sender zurückgeben.


      Die zweite Message (ByVal wParam As Integer und ByRef lParam As Integer)

      VB.NET-Quellcode

      1. Case WM_MESSAGEREF
      2. ptrReturn = CType(4, IntPtr)
      3. Dim litNew As ListViewItem = Me.lvwReceived.Items.Add(ptrReturn.ToString)
      4. litNew.SubItems.Add("WM_MESSAGEREF")
      5. litNew.SubItems.Add(m.WParam.ToString)
      6. Dim intValue As Integer = GetIntFromProcess(m.LParam)
      7. litNew.SubItems.Add(intValue.ToString)
      8. m.Result = ptrReturn
      9. '// ByRef-Parameter ändern
      10. intValue += 5
      11. SetIntToProcess(m.LParam, intValue)

      Der wParam ist wieder klar, hier bekommt man direkt den Wert. Aber beim lParam sieht die Sache jetzt anders aus.
      Da der lParam ByRef übergeben wurde und die Eigenschaft m.LParam diesen ByVal übernimmt bekommt man hier als Wert
      die Speicheradresse an der der Wert gespeichert ist.
      Das Umwandeln in den Wert übernimmt die anschließend beschriebene Fkt GetIntFromProcess. Da man bei einem ByRef-Parameter
      auch den Wert ändern und zurückgeben kann machen wir das hier auch. Das übernimmt die Fkt SetIntToProcess.


      Die dritte Message (ByVal wParam as Integer und ByVal lParam As String)

      VB.NET-Quellcode

      1. Case WM_MESSAGESTRING
      2. ptrReturn = CType(3, IntPtr)
      3. Dim litNew As ListViewItem = Me.lvwReceived.Items.Add(ptrReturn.ToString)
      4. litNew.SubItems.Add("WM_MESSAGESTRING")
      5. litNew.SubItems.Add(m.WParam.ToString)
      6. litNew.SubItems.Add(GetStringFromProcess(m.LParam, CInt(m.WParam)))
      7. m.Result = ptrReturn

      Beim lParam ist es wieder so wie bei der zweiten Message. Hier bekommen wir die Speicheradresse des Strings.
      Im wParam bekommen wir die Länge des Strings die die Fkt GetStringFromProcess benötigt um den String richtig
      zurückzugeben.


      Bei den Messages WM_MESSAGEREF und WM_MESSAGESTRING haben wir jedesmal den Fall, dass man eine Speicheradresse
      bekommt und daraus den Wert ermitteln muß. Das wäre jetzt nicht unbedingt die Schwierigkeit, man könnte das mit
      der API CopyMemory lösen. Aber: Die Speicheradresse die wir bekommen ist die Speicheradresse in dem Programm das
      die Nachricht verschickt hat und die ist beim Empfänger nicht gültig bzw irgendwas anderes, mit CopyMemory gehts also nicht.
      Es gibt aber die APIs ReadProcessMemory und WriteProcessMemory mit denen man aus dem Speicher eines anderen Process
      lesen und schreiben kann.


      Dazu wird zuerst einmal ein Handle für den entsprechenden Process benötigt. Das liefert die Hilfsfunktion GetProcessHandle

      VB.NET-Quellcode

      1. Private Function GetProcessHandle(ByVal hwnd As IntPtr) As IntPtr
      2. '// ProcessID des anderen Programms holen
      3. Dim intProcID As IntPtr
      4. GetWindowThreadProcessId(hwnd, intProcID)
      5. '// Processhandle holen
      6. Return OpenProcess(PROCESS_VM_OPERATION Or PROCESS_VM_READ Or PROCESS_VM_WRITE, 0, intProcID)
      7. End Function

      Die API GetWindowThreadProcessId liefert die Process-ID von dem Process, dessen Windowhandle übergeben wird.
      Mit dieser Process-ID liefert dann die API OpenProcess ein Processhandle.

      Die Declares:
      VB6

      VB.NET-Quellcode

      1. Private Const PROCESS_VM_OPERATION As Long = &H8
      2. Private Const PROCESS_VM_READ As Long = &H10
      3. Private Const PROCESS_VM_WRITE As Long = &H20
      4. Private Declare Function GetWindowThreadProcessId Lib "user32" Alias "GetWindowThreadProcessId" _
      5. (ByVal hwnd As Long, ByRef lpdwProcessId As Long) As Long
      6. Private Declare Function OpenProcess Lib "kernel32" _
      7. (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, _
      8. ByVal dwProcessId As Long) As Long

      Net

      VB.NET-Quellcode

      1. Private Const PROCESS_VM_OPERATION As Integer = &H8
      2. Private Const PROCESS_VM_READ As Integer = &H10
      3. Private Const PROCESS_VM_WRITE As Integer = &H20
      4. Private Declare Function GetWindowThreadProcessId Lib "user32" Alias "GetWindowThreadProcessId" _
      5. (ByVal hwnd As IntPtr, ByRef lpdwProcessId As IntPtr) As Integer
      6. Private Declare Function OpenProcess Lib "kernel32" Alias "OpenProcess" _
      7. (ByVal dwDesiredAccess As Integer, ByVal bInheritHandle As Integer, _
      8. ByVal dwProcessId As IntPtr) As IntPtr

      Außer dem mittlerweile bekannten Long-Integer/IntPtr gibt es keine Unterschiede


      Da wir jetzt ein Processhandle haben können wir uns an die Arbeit machen und die Daten aus diesem Process lesen.
      Fangen wir mal mit dem ByRef lParam As Integer an:

      VB.NET-Quellcode

      1. Private Function GetIntFromProcess(ByVal ptrAddr As IntPtr) As Integer
      2. Dim ptrProcHandle As IntPtr
      3. ptrProcHandle = GetProcessHandle(AppPrevInstance)
      4. If ptrProcHandle = CType(0, IntPtr) Then Return 0
      5. Dim intValue As Integer
      6. Dim intLen As Integer
      7. ReadProcessMemory(ptrProcHandle, ptrAddr, intValue, 4, intLen)
      8. CloseHandle(ptrProcHandle)
      9. Return intValue
      10. End Function

      Der Hilfsfunktion GetProcessHandle wird als Parameter das von der Fkt AppPrevInstance ermittelte Windowhandle des
      anderen Programms, also des Senders übergeben, folglich bekommen wir ein Processhandle des Senders.

      Wenn wir ein gültiges Handle haben, lesen wir die Daten aus und schließen das Handle wieder mit der API CloseHandle.
      Den Declare für CloseHandle spare ich mir hier, hier ist wieder nur der Long-Integer/IntPtr zu beachten.

      Interessanter ist an dieser Stelle die API ReadProcessMemory. Diese API ist so interresant, dass ich hier zuerst
      die C-Deklaration (API-Doku) bringe:

      Quellcode

      1. BOOL ReadProcessMemory(
      2. HANDLE hProcess, // handle of the process whose memory is read
      3. LPCVOID lpBaseAddress, // address to start reading
      4. LPVOID lpBuffer, // address of buffer to place read data
      5. DWORD cbRead, // number of bytes to read
      6. LPDWORD lpNumberOfBytesRead // address of number of bytes read
      7. );

      Man sieht hier, dass diese API zwei LPVOID-Parameter hat, also dass an diesen Stellen eine Speicheradresse erwartet
      wird. Das C beim LPCVOID bedeutet, dass der Inhalt dieser Speicheradresse von der Funktion nicht geändert wird, hat
      aber ansonsten für uns keine Auswirkungen.
      Durch die Deklaration mit LPVOID kann diese API die verschiedensten Datentypen bearbeiten, es werden ja nur
      Speicheradressen übergeben.

      Wie scheibt man nun für diese API die Declares für VB

      Bei VB6 hat man bezüglich des Datentyps keine Wahl, es gibt als 4 Byte Typ nur den Long

      VB.NET-Quellcode

      1. Private Declare Function ReadProcessMemoryInt Lib "kernel32" Alias "ReadProcessMemory" _
      2. (ByVal hProcess As Long, _
      3. ByVal lpBaseAddress As Long, _
      4. ByRef lpBuffer As Long, _
      5. ByVal nSize As Long, _
      6. ByRef lpNumberOfBytesWritten As Long) As Long

      Beachten muß man hier lediglich, welchen Parameter man ByVal und welchen man ByRef deklariert.
      HANDLE hProcess ist einfach, ByVal As Long (siehe obige Tabellen), ebenso bei
      DWORD cbRead und LPDWORD lpNumberOfBytesRead mit ByVal bzw ByRef

      Aufpassen muß man bei den beiden LPVOID-Parametern.
      In LPCVOID lpBaseAddress will die API die Speicheradresse an der gelesen werden soll, also eine Speicheradresse im
      anderen Process. Da wir diese Speicheradresse in der Variablen ptrAddr bereits als Wert vorliegen haben muß dieser
      Parameter mit ByVal deklariert sein weil der Wert übergeben werden muß damit die richtige Speicheradresse ankommt.
      In LPVOID lpBuffer will die API die Speicheradresse des Ziels, also eine Speicheradresse im eigenen Process.
      Hier deklarieren wir diesen Parameter mit ByRef, dadurch wird beim Aufruf die Speicheradresse der Variablen übergeben
      und genau das will die API ja.

      Der Declare in Net sieht eigentlich genauso aus, lediglich wieder die Long-Integer/IntPtr Unterschiede

      VB.NET-Quellcode

      1. Private Declare Function ReadProcessMemory Lib "kernel32" Alias "ReadProcessMemory" _
      2. (ByVal hProcess As IntPtr, _
      3. ByVal lpBaseAddress As IntPtr, _
      4. ByRef lpBuffer As Integer, _
      5. ByVal nSize As Integer, _
      6. ByRef lpNumberOfBytesWritten As Integer) As Integer

      Beim Aufruf dieser API übergeben wir eben das erhaltenen Processhandle, die Speicheradresse im anderen Process
      von der gelesen werden soll, eine Variable in die geschrieben werden soll und bei der Längenangabe eben 4 da wir
      einen 4 Byte Typ lesen wollen. Im letzten Parameter gibt uns die API die Anzahl der gelesenen Bytes zurück.
      Eigentlich brauchen wir diesen Parameter nicht, aber da ihn die API verlangt müssen wir eben eine entsprechende
      Variable übergeben.

      Jetzt haben wir den Wert, der mit ByRef lParam As Integer übergeben wurde.


      Für den ByVal lParam As String wird die Hilfsfunktion GetStringFromProcess verwendet

      VB.NET-Quellcode

      1. Private Function GetStringFromProcess(ByVal ptrAddr As IntPtr, ByVal intLen As Integer) As String
      2. Dim ptrProcHandle As IntPtr
      3. ptrProcHandle = GetProcessHandle(AppPrevInstance)
      4. If ptrProcHandle = CType(0, IntPtr) Then Return ""
      5. Dim strText As String
      6. strText = Space(intLen)
      7. ReadProcessMemory(ptrProcHandle, ptrAddr, strText, intLen, intLen)
      8. CloseHandle(ptrProcHandle)
      9. Return strText
      10. End Function

      Der Hauptunterschied zur GetIntFromProcess ist der, dass eine andere Deklaration der ReadProcessMemory verwendet wird.

      Net

      VB.NET-Quellcode

      1. Private Declare Function ReadProcessMemory Lib "kernel32" Alias "ReadProcessMemory" _
      2. (ByVal hProcess As IntPtr, _
      3. ByVal lpBaseAddress As IntPtr, _
      4. ByVal lpBuffer As String, _
      5. ByVal nSize As Integer, _
      6. ByRef lpNumberOfBytesWritten As Integer) As Integer

      Der LPVOID lpBuffer Parameter ist hier mit ByVal As String definiert. Wie bereits erwähnt, wird dadurch die
      Speicheradresse des Strings übergeben.
      Allerdings sind hier noch ein paar Sachen zu beachten die beim Lesen des Integers nicht nötig waren.
      Wir erinnern uns, dass man bei APIs die einen String zurückliefern zuerst dafür sorgen muss, dass im Ziel
      genügend Platz ist.
      In der Fkt GetStringFromProcess bekommen wir im Parameter ptrAddr den Inhalt vom lParam der Message, also die
      Speicheradresse des Strings. Über die Länge des Strings haben wir aber noch keinerlei Informationen.
      An dieser Stelle kommt der wParam der Message ins Spiel. Beim Versenden der Message haben wir im wParam eben die
      Länge des Strings übergeben, so dass wir hier vom wParam (der im Parameter intLen steht) die benötigte Länge erfahren.
      Jetzt wird einfach der String mit der benötigten Größe angelegt und dann an ReadProcessMemory übergeben. Beim
      Parameter für die Länge müssen wir natürlich auch die Länge des Strings übergeben und schon bekommen wir den String
      zurück.


      Zum Schluß geben wir jetzt noch den geänderten ByRef lParam As Integer an das andere Programm zurück.

      VB.NET-Quellcode

      1. Private Sub SetIntToProcess(ByVal ptrAddr As IntPtr, ByVal intValue As Integer)
      2. Dim ptrProcHandle As IntPtr
      3. ptrProcHandle = GetProcessHandle(AppPrevInstance)
      4. If ptrProcHandle = CType(0, IntPtr) Then Exit Sub
      5. Dim intLen As Integer
      6. WriteProcessMemory(ptrProcHandle, ptrAddr, intValue, 4, intLen)
      7. CloseHandle(ptrProcHandle)
      8. End Sub

      Hier wird mit der API WriteProcessMemory in den Speicher des anderen Process geschrieben.
      Die Deklaration der Parameter dieser API ist mit der von der entsprechenden ReadProcessMemory identisch

      Net

      VB.NET-Quellcode

      1. Private Declare Function WriteProcessMemory Lib "kernel32" Alias "WriteProcessMemory" _
      2. (ByVal hProcess As IntPtr, _
      3. ByVal lpBaseAddress As IntPtr, _
      4. ByRef lpBuffer As Integer, _
      5. ByVal nSize As Integer, _
      6. ByRef lpNumberOfBytesWritten As Integer) As Integer

      für VB6 eben wieder Long verwenden




      Schlußbemerkungen

      So, und jetzt zum Schluß noch, wie man das ganze überhaupt testen kann.

      Wir starten das Programm einfach von der IDE aus. Dann wechseln wir im Explorer zum Debug-Ordner und starten die exe
      einfach nochmal. Jetzt haben wir das gleiche Programm zweimal laufen und können Nachrichten austauschen.
      Mit der Instanz des Programms, das aus der IDE gestartet wurde kann man das auch debuggen.

      Da die Listvieweinträge beim Senden erst nach dem Versenden gemacht werden hat man dort auch die jeweiligen Rückgabewerte
      die zurückgegeben wurden. Bei der WM_MESSAGEREF-Message hat man auch den geänderten lParam.


      Gaga
      Dateien
      • Message_Test.7z

        (10,57 kB, 248 mal heruntergeladen, zuletzt: )
      • Message_Test.zip

        (17,35 kB, 442 mal heruntergeladen, zuletzt: )

      Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „Mad Andy“ ()