Das Folgende bezieht sich nicht auf die 'geheime' Datensammelei von Microsoft und anderen bekannten 'Global Playern', obwohl es die aktuellen Zustände nahe legen würden. Es soll lediglich eine Beschreibung von internen Abläufen innerhalb des beliebten Betriebssystems 'Microsoft Windows' liefern.
Hinter einigen der im folgenden Text als eingebetteter Code markierten Bereiche verbergen sich weiterführende Links auf die Online-Hilfe oder das MSDN (Microsoft Developer Network). Man erkennt sie daran, dass der Mauszeiger wechselt, wenn sich die Maus darüber befindet, und der Text unterstrichen angezeigt wird.
Allgemeines
Alle Elemente der graphischen Benutzeroberfläche (GUI) werden in Windows durchgängig als Fenster bezeichnet, daher auch der Name (Fenster = windows). Im Gegensatz zu AHK, das die Oberflächenelemente begrifflich in Fenster (Gui) und Controls teilt und dabei den Begriff Fenster normalerweise nur für das/die Hauptfenster der Anwendung verwendet, unterscheidet Windows zwischen Eltern- (parent window) und Kindfenstern (child window). Kindfenster werden mit dem Stil WS_CHILD (0x40000000) gekennzeichnet. Der sorgt u.a. dafür, dass dem Fenster bestimmte Eigenschaften wie z.B. eine Titelleiste oder ein Fenstermenü verweigert werden und das Fenster nur innerhalb des Elternfensters angezeigt werden kann. Als eindeutiges Identitätsmerkmal wird dem Kindfenster bei seiner Erstellung das systemweit eindeutige Handle (HWND) des Elternfensters übergeben.
Fenster werden in Windows und auch in anderen Betriebssystemen über Nachrichten gesteuert. Sobald das System selbst oder auch untergeordnete Elemente eine Veränderung feststellen, die möglicherweise von einem Element der GUI verarbeitet bzw. zur Kenntnis genommen werden muss, wird eine Nachricht generiert, die die Veränderung nach Auffassung von Microsoft ausreichend genau beschreibt, und an das/die betroffene/n Element/e geschickt. Für die Abarbeitung dieser Nachrichten müssen alle Anwendungen mit GUI eine Nachrichtenschleife enthalten, in der in regelmäßig kurzen Abständen geprüft wird, ob eine neue Nachricht eingetroffen ist. Für AHK-Anwendungen wird diese Nachrichtenschleife bequemerweise automatisch vom Interpreter erstellt. Darüber hinaus stellt AHK die Funktion OnMessage() zur Verfügung, die es einer Anwendung ermöglicht, den Nachrichtenverkehr zu belauschen und ggf. anders als die Standardschleife auf diese Nachrichten zu reagieren.
Windows unterscheidet dabei zwischen zwei Nachrichtentypen:
- Message: Nachricht / Botschaft
Nachrichten sind in der Regel Handlungsanweisungen, die dem Fenster mitteilen, dass eine bestimmte Reaktion erwartet wird. - Notification: Benachrichtigung / Information
Benachrichtigungen dienen in der Regel der Übermittlung von Informationen. Dabei bleibt es dem Fenster überlassen, ob es auf diese Informationen reagiert oder auch nicht.
Für den Versand der Nachrichten werden hauptsächlich zwei API-Funktionen (API = Application Programming Interface / Programmierschnittstelle für Anwendungen) genutzt: Die Funktionen unterscheiden sich darin, dass SendMessage wartet, bis die Anwendung die Nachricht verarbeitet hat und eine entsprechende Bestätigung zurückgibt, während PostMessage die Nachricht einfach abschickt und nicht wartet. Daraus ergibt sich, dass SendMessage immer dann verwendet werden muss, wenn die Nachricht einen Wert zurückliefern soll.
Beide Funktionen haben vier Parameter:
- hWnd : Das Handle (in AHK sowohl als Hwnd als auch als ID bezeichnet) des Empfängerfensters.
- Msg : Die Nummer der Nachricht.
- wParam : Nachrichtenabhängiger Inhalt.
- lParam : Nachrichtenabhängiger Inhalt.
In AHK-Anwendungen muss man die API-Funktionen nicht per DllCall() aufrufen, obwohl das durchaus Vorteile haben kann. AHK hat dafür die Anweisungen mit den Namen (wen wundert's) SendMessage und PostMessage. Ein wichtiger Unterschied im Verhalten der API-Funktionen und der eingebauten Anweisungen besteht darin, dass die Anweisungen den aktuellen Einstellungen für das Erkennen versteckter Fenster (DetectHiddenWindows) unterliegen, während die API-Funktionen die Nachrichten unabhängig davon zustellen. Außerdem kann man bei Aufruf über DllCall() direkt auf den Rückgabewert der Nachricht zugreifen, ohne den Umweg über die interne Variable ErrorLevel gehen zu müssen, in der die Anweisungen den Rückgabewert verpacken.
Für die Parameter von Anweisungen und API-Funktionen gilt folgende Zuordnung:
- Msg <-> Msg
- wParam <-> wParam
- lParam <-> lParam
- WinTitle <-> hWnd, wenn mit ahk_id %VarContainingID% das Handle des Fensters bzw. des Controls übergeben wird.
Die Parameter wParam und lParam werden von AHK wie folgt versorgt:
- Numerische Werte werden unverändert in den Parameter übernommen.
- Für Zeichenfolgen (strings) wird ein Pointer auf den zugehörigen Speicherbereich erzeugt und in den Parameter gestellt.
Sonderfall Controls
Die weitaus meisten AHK bzw. Windows Controls informieren ihre Elternfenster bei bestimmten Ereignissen per Benachrichtigung (notification). Dafür werden ausschließlich zwei Nachrichten genutzt: Wenn einem Control ein gLabel zugewiesen ist, ruft AHK diese Routine bei einigen Benachrichtigungen automatisch auf. Die Meisten bleiben aber unter der Decke und werden von der internen Nachrichtenschleife abgearbeitet, sofern AHK das für erforderlich hält. Wenn man sich etwas genauer anschauer will, was die Controls so alles mitzuteilen haben, muss man dafür die Funktion OnMessage() bemühen. Die so zugewiesenen Funktionen erwarten bis zu vier Parameter. Und wenn man sich die Namen anschaut, wird schnell klar, was sie wohl beinhalten: Ja, es sind die vier Parameter der API-Funktionen SendMessage()/PostMessage().
Wenn man sich dazu entschließt, den Nachrichtenverkehr per OnMessage() zu belauschen, muss man sich darüber im Klaren sein, dass alle Controls und dazu auch noch Menüs dieselben Nachrichten WM_COMMAND bzw. WM_NOTIFY als Hülle nutzen. Man kann deshalb nicht direkt eine Benachrichtigung eines Edit Controls wie z.B. EN_SETFOCUS als Nachrichtennummer übergeben, sondern muss stattdessen für diesen Fall die Nummer der Nachricht WM_COMMAND := 0x0111 verwenden. Sobald man das getan hat, sammelt die in OnMessage() angegebene Funktion aber die WM_COMMAND Nachrichten aller Controls ein. Deshalb sind in solchen Funktionen zwei Dinge zu beachten:
- Die Funktion muss möglichst schnell abgearbeitet werden, um die Nachrichtenverarbeitung für andere Controls nicht zu blockieren oder schlimmstenfalls ganz zu verhindern.
- Die Funktion muss zuallererst klären, ob die Nachricht eine passende Benachrichtigung eines passenden Controls enthält.
Die Benachrichtigung per WM_COMMAND scheint die ältere der beiden Methoden zu sein. Bei dieser Nachricht enthalten die Parameter wParam und lParam nur recht dürftige Informationen:
- wParam:
Dieser Parameter enthält einen Integerwert (4 Bytes), der in zwei Bereiche a 2 Byte aufgeteilt ist.
Der 'höherwertige' Bereich (high word) enthält- 0 : wenn der Absender ein Menü ist
- 1 : wenn der Auslöser ein Tastaturkürzel ist
- die Nummer der Benachrichtigung, wenn der Absender ein Control ist (Bingo!)
Der 'niederwertige' Bereich (low word) enthält Informationen, die normalerweise nur für Menüs und Tastaturkürzel interessant sind:Code: Select all
Value := (wParam & 0xFFFF0000) >> 16 ; oder Value := (wParam >> 16) & 0xFFFF
- für Menüs : die ID des Menüeintrags
- für Tastaturkürzel : die ID des Kürzels
Seinen Wert erhält man durch eine einfache Und-Verknüpfung:Code: Select all
Value := wParam & 0xFFFF
- lParam:
Dieser Parameter ist einfacher zu handhaben. Er enthält für Menüs und Tastaturkürzel nichts (0) und für Controls das Handle (HWND) des Controlfensters und damit genau die Information, die man braucht, um das Control eindeutig zu identifizieren.
Die Benachrichtigung per WM_NOTIFY bietet wesentlich mehr Möglichkeiten zur Übergabe von Informationen. Das ist einerseits gut, weil man mit der Benachrichtigung Informationen erhält, die man sonst selbst mühselig einsammeln müsste, andererseits macht es die Auswertung der Informationen etwas komplizierter. Auch hier gibt es nur die bekannten zwei Parameter wParam und lParam:
- wParam:
Dieser Parameter enthält die - wie bereits oben gesagt - für AHK recht nutzlose interne ID des sendenden Controls. Sie ist auch noch einmal in lParam enthalten, und Microsoft empfiehlt, sich bei Bedarf lieber auf diese zu verlassen. - lParam:
Dieser Parameter ist ein Pointer auf eine 'eierlegende Wollmilchsau'. Er zeigt auf einen zusammenhängenden Speicherbereich, der beliebige Informationen enthalten kann, und das leider auch oft tut. Weil die Informationen nachrichtenabhängig sind, führt in der Regel kein Weg daran vorbei, erst einmal im MSDN oder anderen Quellen nachzuschlagen, was da geliefert wird. Und weil nur eine Adresse geliefert wird, muss man die Funktionen NumGet() und oder StrGet() nutzen, um die im jeweiligen Einzelfall interessanten Informationen auszulesen.
Eine Gemeinsamkeit haben die Benachrichrigungen aber dann doch. Der Speicherbereich, dessen Adresse lParam enthält, beginnt immer mit einer NMHDR (notify message header) Struktur mit folgenden Aufbau:Die beiden Informationen, die man benötigt, um das sendende Control und die gesendete Nachricht zu idenfizieren, finden sich damit immer auf denselben Positionen. Um sie auszulesen, braucht es NumGet(). Dazu muss man überlegen, was man hat und wo das steht, was man will.Code: Select all
typedef struct tagNMHDR { HWND hwndFrom; <- das systemweit eindeutige Handle des sendenden Controls UINT_PTR idFrom; <- die interne ID des Controls UINT code; <- die Nummer der Benachrichtigung } NMHDR;
Die NumGet() Funktion erwartet 3 Parameter:- VarOrAddress : den Namen einer Variablen oder eine Adresse
- Offset : die Distanz zwischen dem Beginn des Speicherbereichs und dem gesuchten Wert
- Type : der Datentyp - er ist entscheidend dafür, in welcher Länge die / wieviele Bytes an Daten ausgelesen werden
Wir haben in der Variablen lParam die Adresse des Speicherbereichs. Wenn man nun aber einfach NumGet(lParam, ..., ...) aufruft, wird man sich wundern, was da zurückkommt. Das liegt daran, das die Funktion für den Zugriff in AHK 1.1 die Adresse der übergebenen Variablen nutzt, und nicht ihren Inhalt (Achtung: Das wird sich mit v2 ändern!). An der Adresse der Variablen sind die Informationen aber nicht zu finden. Die Lösung besteht darin, die Adresse in Form des Ausdrucks lParam + 0 zu übergeben, d.h.Numget(lParam + 0, ..., ...), und schon liegen wir richtig.
Wollen:
Wollen wollen wir zuerst einmal das Handle des Controls und die Nachrichtennummer. Dafür brauchen wir deren Offset und Type.
Das Handle liegt als erster Wert am Anfang des Speicherbereichs, Offset ist also 0. Ein Adresszeiger ist ein Pointer. An sich bräuchten wir deshalb Type nicht anzugeben, weil das der Standardrückgabetyp ist. Falls sich das wieder einmal ändert und weil es deutlich macht, dass man sich über den Typ im Klaren ist, setze ich allerdings auch in diesem Fall den Typ UPtr ein.
Die Nachrichtennummer ist der dritte Wert. Die beiden ersten Werte sind Pointer. Die Länge von Pointern ist in 32-Bit und 64-Bit Anwendungen unterschiedlich. Deshalb gibt es die interne Variable A_PtrSize, die die aktuelle Länge enthält (4 Bytes für 32-Bit, 8 Bytes für 64-Bit). Der Offset beträgt damit zwei Pointerlängen, oder anders: 2 * A_PtrSize. Type ist eindeutig UInt. Wenn man mit negativen Nachrichtennummern arbeiten muss (die gibt es wirklich), kann es aber bequemer sein, hier Int zu benutzen.
Damit haben wir Alles, was gebraucht wird, um das Handle und die Nachrichtennummer auszulesen:Diese Funktion zeigt aber ein weiteres Problem. Die Handles der Controls werden in der Regel außerhalb der Funktion versorgt. Auf die Variable MeinControlHandle kann deshalb in der Funktion nicht ohne weiteres zugegriffen werden, weil alle Variablen in Funktionen per se funktionslokale Variablen sind. Als Abhilfe kann man alle Variablen innerhalb der Funktion per Global als global erklären, mit Global MeinControlHandle die spezielle Variable innerhalb der Funktion als global erklären oder die Variable außerhalb der Funktion per Global MeinControlHandle zu einer super-globalen machen, auf die in allen Funktionen zugegriffen werden kann.Code: Select all
MeineOnMessageFunktion(wParam, lParam) { Static DieNachrichtenNummer := 0xABCD Handle := NumGet(lParam + 0, 0, "UPtr") Msg := NumGet(lParam + 0, 2 * A_PtrSize, "UInt") ; oder "Int" If (Handle = MeinControlHandle) && (Msg = DieNachrichtenNummer) { ... }
Die weiteren möglichen Inhalte des per lParam addressierten Speicherbereichs müssen hier leider ausgeklammert werden. Zum Einen sind sie nachrichtenabhängig, zum Anderen erfordert der Zugriff auf diese Bereiche einiges Wissen über die Windows Datentypen und vor allem -strukturen. Das sollte aber besser in einem eigenen Tutorial untergebracht werden.
Einen Hinweis kann ich aber trotzdem geben. Der Offset des ersten auf die Struktur NMHDR folgenden Feldes ist zwar - wie eigentlich rechnerisch logisch - in 32-Bit Anwendungen 12 = (2 * A_PtrSize) + 4, in 64-Bit Anwendungen aber nicht 20 = (2 * A_PtrSize) + 4. Das liegt an der Ausrichtung von Strukturen. Wenn man ein in beiden Versionen lauffähiges Skript schreiben will, muss man deshalb als Offset des ersten Feldes 3 * A_PtrSize verwenden.
Die Arbeit mit Nachrichten, sei es per SendMessage oder OnMessage() Funktionen ist keine unlösbare Aufgabe, erfordert aber einigen Aufwand. Die AHK-Hilfe hilft da nur in einigen wenigen Fällen, normalerweise muss man andere Informationsquellen finden, die Erklärungen für die im Einzelfall benötigten Daten liefern. Für viele dieser Daten muss man Variablen passender Größe mit VarSetCapacity() selbst einrichten, per NumPut()/StrPut() befüllen, und die Informationen im Erfolgsfall mit NumGet()/StrGet() auslesen. Der Mühe Lohn sind Steuerungsmöglichkeiten, die ohne die Nachrichten eben nicht möglich sind.
Auch für die Kennzeichnung des Absenders oder Empfängers von Nachrichten nutzt Windows nahezu ausschließlich die Handle (HWNDs) der Fenster/Controls. Es ist deshalb naheliegend, sie für die eigene Nachrichtenverarbeitung ebenfalls zu nutzen, indem man die hwnd Option der Gui-Anweisungen nutzt. Das Handle kann auch in Gui-Anweisungen anstelle des Namens verwendet werden. Statt den Namen einfach als Text zu übergeben, muss man das Handle allerdings mit %MeinControlHandle% referenzieren.