start

WinApi 32 - Messages
Nachrichtenbehandlung

Beispiel: window_click

[images/window_click.gif] Nachdem es in dem letzten Teil um die Erstellung eines einfachen Fensters ging, mit einer einzigen Funktionalität in DefWindowProc(). Wird sich in diesem Teil alles rund um das Thema Nachrichten drehen. Es wird gezeigt, wie das Beispiel Programm zu verändern ist, um auf weitere Ereingisse reagieren zu können. Als Voraussetzung dafür sollte das Beispielprogramm aus dem vorangegangenen Teil vorliegen, fehlerfrei kompilierbar sein und wie erwartet arbeiten. Dieser QUelltext wird im Folgenden als Grundlage dienen.

Als erstes werden wir dem Programm beibringen, seinen Namen anzuzeigen, wenn der Benutzer auf das Fester klickt. Das ist keine besonders aufregende Funktion, aber ein gutes Beispiel für eine Nachrichtenbehandlung. Dazu werfen wir zunächst nochmal einen Blick auf die WndProc() Funktion:

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
        case WM_CLOSE:
            DestroyWindow(hwnd);
        break;
        case WM_DESTROY:
            PostQuitMessage(0);
        break;
        default:
            return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}
Wenn wir Maus-Links-Klicks auswerten wollen, muß zunächst die entsprechende Nachricht WM_LBUTTONDOWN in den message handler eingefügt werden. Alternativ können Rechts-Klicks mit WM_RBUTTONDOWN oder Mittel-Klicks mit WM_MBUTTONDOWN ausgewertet werden. Genaugenommen werden diese Nachrichten immer dann gesendet, wenn die Taste runter gedrückt wird. Man spricht bei dem Hinzufügen einer Nachricht in den Handler auch vom behandeln der Nachricht (handling a message).
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
   switch(msg)
   {
      case WM_LBUTTONDOWN:    // <-
                              // <-     we just added this stuff
      break;                  // <-
      case WM_CLOSE:
         DestroyWindow(hwnd);
      break;
      case WM_DESTROY:
         PostQuitMessage(0);
      break;
      default:
         return DefWindowProc(hwnd, msg, wParam, lParam);
   }
   return 0;
}

Die Reihenfolge in der die Nachrichten behandelt werden ist selten wichtig. Es kommt hingegen viel mehr darauf an sicher zu stellen, dass das break; nach jeder zu behandelnden Nachricht vorhanden ist. Wie in dem Quelltextauszug dargestellt wurde zunächst ein case in der switch() Umgebung hinzugefügt. Dieser Zweig ist unser neuer event handler für das Ereignis Linke-Maus-Taste gedrückt.

Im nächsten Schritt wird die Funktionalität (das Anzeigen des Programmnamens) hinzugefügt. Zunächst mal das Stück Programmcode welches hinzugefügt werden soll und die Stelle, an der es eingefüt werden muß. (Sollte es an dieser Stelle zu Problemen kommen kann der vollständige Queltext hier geladen werden : Example03.c)

GetModuleFileName(hInstance, szFileName, MAX_PATH);
MessageBox(hwnd, szFileName, "This program is:", MB_OK | MB_ICONINFORMATION);

Nach dem Einfügen des Code-Fragmentes gilt auch hier wieder: Erst mal ausprobieren, kompilieren und experimentieren und dann die Erklärung dafür lesen.

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
        case WM_LBUTTONDOWN:
        // BEGIN NEW CODE
        {
            char szFileName[MAX_PATH];
            HINSTANCE hInstance = GetModuleHandle(NULL);

            GetModuleFileName(hInstance, szFileName, MAX_PATH);
            MessageBox(hwnd, szFileName, "This program is:", MB_OK | MB_ICONINFORMATION);
        }
        // END NEW CODE
        break;
        case WM_CLOSE:
            DestroyWindow(hwnd);
        break;
        case WM_DESTROY:
            PostQuitMessage(0);
        break;
        default:
            return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

Hinweis : Die beiden zusätzlichen geschweiften Klammern {} sind notwendig, dass innerhalb einer switch() Umgebung in der Programiersprache C Variablen deklariert werden dürfen. Im Gegensatz zu C++ (und C#) dürfen in C Variablen nur am Anfang eines Blockes oder am Anfang einer Funktion deklariert werden.

Es war notwendig zwei zusätzliche Variablen hinzuzufügen, hInstance und szFileName. Wenn wir die Funktion GetModuleFileName() nachschlagen sehen wir, dass der erste Parameter ein HINSTANCE ist, welcher auf das Ausführbare Modul (in dem Fall die .exe) verweist. Woher bekommt man diesen Parameter? Die Antwort auf diese Frage liefert die FunktionGetModuleHandle(). Laut API-Dokumentation liefert die Funktion GetModuleHandle() aufgerufen mit dem Parameter NULL, "a handle to the file used to create the calling process", was genau dem entspricht, was wir an diser Stelle brauchen, der HINSTANCE. Alles zusammen ergibt dann die folgende Zeile:

HINSTANCE hInstance = GetModuleHandle(NULL);

Zu dem 2. Parameter, schauen wir in unsere vertraute API-Dokumentation, sehen wir dass dieser " a pointer to a buffer that receives the path and file name of the specified module" ist mit dem Datentype LPTSTR (oder LPSTR in alten API-Dokumenten). Da LPSTR identisch ist zu char*, kann diser als Array von char's wie folgt deklariert werden:

char szFileName[MAX_PATH];

MAX_PATH ist eines der praktischen vordefinierten Windows MACROS in <windows.h>. Dieses ist auf die maximale länge für einen Puffer definiert, welcher benötigt wird einen PATH maximaler Länge unter WIN32 zu speichern. Dieser Parameter (MAX_PATH) wird auch an GetModuleFileName() übergeben, so daß auch diese Funktion die Größe des Puffers kennt.

Nachdem Aufruf von GetModuleFileName()enthält der Puffer szFileName einen NULL terminierten String mit dem Namen und dem Path der .EXE Datei. Dises Variable wird an eine MessageBox() übergeben. Das ist der einfachste Weg etwas anzuzeigen.

Hinweis : Es wäre grundsätzlich auch möglich gewesen eine Optimierung des Codes durchzuführen, indem zwei Funktionsaufrufe ineinander verschachtelt worden werden. Somit wäre eine temporäre Variable eingespart worden, weil in C der Rückgabeparameter der einen Funktion gleich als Eingabeparameter der anderen Funktion dienen kann/darf. Von Optimierungen dieser Art wird jedoch absichtlich in diesem Tutorial kein Gebrauch gemacht, weil es weder dem Verständnis dient noch von einem "guten Programmierstiel" zeugt. Es bleibt jedoch jedem selbst überlassen auch diese Code-Variante einmal auszuprobieren ;-)

szFileName = GetModuleFileName(GetModuleHandle(NULL), szFileName, MAX_PATH);

Hier nochmal der komplette neune Teil des Beispiel-Programms:

#include <windows.h>

const char g_szClassName[] = "myWindowClass";

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
        case WM_LBUTTONDOWN:
        {
            char szFileName[MAX_PATH];
            HINSTANCE hInstance = GetModuleHandle(NULL);

            GetModuleFileName(hInstance, szFileName, MAX_PATH);
            MessageBox(hwnd, szFileName, "This program is:", MB_OK | MB_ICONINFORMATION);
        }
        break;
        case WM_CLOSE:
            DestroyWindow(hwnd);
        break;
        case WM_DESTROY:
            PostQuitMessage(0);
        break;
        default:
            return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wc;
    HWND hwnd;
    MSG Msg;

    wc.cbSize        = sizeof(WNDCLASSEX);
    wc.style         = 0;
    wc.lpfnWndProc   = WndProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInstance;
    wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = g_szClassName;
    wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);

    if(!RegisterClassEx(&wc))
    {
        MessageBox(NULL, "Window Registration Failed!", "Error!",
            MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

    hwnd = CreateWindowEx(
        WS_EX_CLIENTEDGE,
        g_szClassName,
        "The title of my window",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 240, 120,
        NULL, NULL, hInstance, NULL);

    if(hwnd == NULL)
    {
        MessageBox(NULL, "Window Creation Failed!", "Error!",
            MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    while(GetMessage(&Msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }
    return Msg.wParam;
}
Die Nachrichtenschleife (Message Loop)

Es ist notwendig den message loop und das ganze message handeling von Windows zu verstehen um erforgreich Programme entwickeln zu können, ganz besonders bei scheinbar einfachen Programmen. Nachdem wir das message handling ein wenig ausprobiert haben, werden wir etwas tiefer in den gesamten Prozess dahinter eindringen. Beim Programieren kann es ganz schön verwirrend sein, wenn man nicht weiß was passiert und vor allem wie es passiert.

Was sind Nachrichten ?

Eine Nachricht ist ein Integer-Wert. Wenn man diese aus dem Header heraussucht (das ist übrings ein durchaus üblicher und sehr einfacher Weg herauszufinden wie die API's funktionieren) findet man folgende Definitionen:
#define WM_INITDIALOG                   0x0110
#define WM_COMMAND                      0x0111
...
#define WM_LBUTTONDOWN                  0x0201
... und so weiter. Nachrichten werden verwendet um einfach alles auf einem einfachen Weg zu komunizieren und auszulösen. Will man ein Fenster oder ein Control (welches auch nichts weiter als ein spezielles Fenster ist) dazu bewegen eine bestimmte Aktion auszuführen, muß man dem Fenster nur die entsprechende Nachricht senden. Wird dies von einem anderen Fenster gewünscht sendet dieses unserem Fenster eine entsprechende Nachricht. Tritt ein Ereigniss auf (z.B. eine Taste wird gedrückt, die Maus wird bewegt, ein Button wird gecklickt) werden eine oder mehrere entsprechende Nachrichten an alle betroffenen Fenster gesendet. Wenn unser Fenster eines davon ist erhalten wir die Nachricht und verhalten uns dem entsprechend.

Jede Windows Nachricht kann bis zu zwei Parameter haben, wParam und lParam. Ursprünglich war wParam 16 bit und lParam war 32 bit, aber in Win32 wurden beide 32 bit. Nicht jede Nachricht verwendet beide Parameters, und jede Nachricht verwendet diese Parameter unterschiedlich. Ein Beispiel: Die WM_CLOSE Nachricht benutzt gar keinen der beiden, und man sollte einfach beide Parameter ignorieren. Hingegen die WM_COMMAND Nachricht verwendet beide, wobei wParam sogar zwei Werte enthält, HIWORD(wParam) ist die notification message (wenn verfügbar) und LOWORD(wParam) ist die ID des Control oder Menu's, welches die entsprechende Nachricht gesendet hat. lParam ist das HWND (window handle) welches die Nachricht gesendet hat. Es ist NULL falls diese Nachricht nicht von einem Control kommt.

Hinweis : HIWORD() und LOWORD() sind Makros die von WIndows bereits gestellt werden, um die zwei high bytes (High Word) eines 32 Bit Wertes (0xFFFF0000) und dem low word (0x0000FFFF) zu extrahieren. Unter Win32 ist ein WORD immer 16bit lang, sowie ein DWORD (or Double Word) ein 32bit Wert ist.

Um eine Nachricht zu verschicken kann man entwerder PostMessage() oder SendMessage() benutzen. PostMessage() legt die Nachricht in der Message Queue ab und fährt unverzüglich mit der Programm abarbeitung fort. Das bedeutet nachdem man PostMessage() aufgerufen hat kann die Nachricht schon behandelt worden sein oder nicht. Möglicherweise wartet die Nachricht in der Nachrichten-Warte-Schlange (message queue).

SendMessage() sendet die Nachricht sofort an das Fenster ohne Umweg über die message queue, und blockiert die weitere Abarbeitung, bis das Fenster diese Nachricht abgearbeitet hat. Wenn wir ein Fenster schließen wollen, könnten wir eine WM_CLOSE Nachricht etwa wie folgt senden: PostMessage(hwnd, WM_CLOSE, 0, 0);. Das hätte genau den gleichen Effekt wie ein Klick auf den CLose Button [x] des Fensters. Man beachte dabei, dass wParam und lParam beide 0 sind, weil diese für WM_CLOSE nicht verwendet werden.

Dialoge

Wenn man Dialogeprogrammiert ist es notwendig Nachrichten an die Control-Elemente zu senden um mit diesen Informationen und Daten austauschen zu können. Dies kann man mit Hilfe der Funktion GetDlgItem()machen, indem man sich mit der ID ein Handle auf das Control holt und mit der Funtion SendMessage() die entsprechende Nachricht verschickt. Alternativ kann man aber auch die Funktion SendDlgItemMessage() benutzen, die beide Schritte in einem verwendet. Dieser Funktion wird ein Fenster-Handle und eine Child-ID übergeben sowie die zusendende Nachricht. SendDlgItemMessage() und vergleichbare API-Funktionen wie z.B. GetDlgItemText() funktionieren mit allen Fenstern, nicht nur mit Dialogen.

Was ist eine Message Queue

Nehmen wir an unser Programm ist mit der Behandlung von der WM_PAINT Nachricht ausgelastet plötzlich tippt der Anwender eine ganze Reihe von Zeichen auf der Tastatur ein. Was soll jetzt passieren ? Sollte die Zeichenroutine unterbrochen werden um die Tasten auszuwerten oder sollten die Tasten verworfen werden ? Die Antwort lautet NEIN ! Glücklicher Weise gibt es noch eine andere Lösung: die message queue (zu Deutsch die Nachrichten-Warte-Schlage). Sobald Nachrichten ankommen werden diese zwischen gespeichert, bis das Fenster wieder in der Lage ist eine neue Nachricht zu behandeln. Dabei wird die Reihenfolge der nachrichten beibehalten, in der sie in der message queue angekommen sind. Dies stellt sicher, dass keine Nachrichten verlohren gehen, solange gerade eine andere nachricht behandelt wird.

was ist die Message Loop

while(GetMessage(&Msg, NULL, 0, 0) > 0)
{
    TranslateMessage(&Msg);
    DispatchMessage(&Msg);
}
  1. Die message loop (Nachrichten-Schleife) ruft die Funktion GetMessage() auf, welche ihrerseits in der message queue nachschaut ob eine neue Nachricht vorliegt. Für den Fall das die message queue lerr ist hält das rogramm erst mal an. (Es Blockiert).
  2. Tritt ein Ereigniss auf, welches das Ablegen einer Nachricht in die message queue zur Folge hat (z.B. ein maus click) GetMessages() gibt einen positiven Wert zurück. Dieser sagt aus, dass eine Nachricht zur Bearbeitung vorliegt, in der Struktur MSG. Eine 0 wird zurückgegeben wenn WM_QUIT empfangen wurde, und ein negativer Wert wenn ein Fehler auftrat.
  3. Die empfangene Nachricht wird an die Funktion TranslateMessage() übergeben, hier werden zusätzliche (notwendige) Konvertierungen vorgenommen. Dieser Schritt ist optional, aber es können ziemlich komsche Seiteneffekte entstehen, wenn man ihn wegläßt ;-)
  4. Als nächstes wird die Funktion DispatchMessage() mit der Nachricht aufgerufen. DispatchMessage() leitet die Nachricht an der betreffende Fenster weiter und spürt die zugehörige window procedure auf. Dabei werden die Parameter ein handle des Fensters, die Nachricht, und wParam sowie lParam mit übergeben.
  5. In der window procedure wird die Nachricht und die Parameter geprüft, wie es weitergeht hängt von Programierer ab. ;-) Wenn nichts spezielles bei dieser nachricht passieren soll wird immer die Funktion DefWindowProc() aufgerufen. Diese ruft die Standart-Funktion des Fensters für diese Nachricht auf. (Das heißt meistens: es passiert nichts).
  6. Nachdem die Nachricht komplett verarbeitet wurde, kehrt zuerst die window procedure zurück, DispatchMessage() endet. Jetzt sind wir in der Schleife wieder am Anfang und warten auf eine neue Nachricht.

Das ist ein wirklich wichtiges Konzept für Windows Programme. Die eigene window procedure wird magisch von System aufgerufen, um diese selbst aufzurufen ist es notwendig DispatchMessage() indirekt wie folgt zu ersetzen. Wer möchte kann dazu GetWindowLong() benutzen um das window handle zu bekommen, für welches die Nachricht bestimmt ist. Dann suchen wir uns das Fenster und rufen die window procedure direkt auf!

while(GetMessage(&Msg, NULL, 0, 0) > 0)
{
    WNDPROC fWndProc = (WNDPROC)GetWindowLong(Msg.hwnd, GWL_WNDPROC);
    fWndProc(Msg.hwnd, Msg.message, Msg.wParam, Msg.lParam);
}

Ich habe das einfach mal mit dem obigen Beispiel ausprobiert. Es funktioniert, aber es gibt Zusammenhänge wie Unicode/ANSI übersetzugen, timer callbacks und solche Sachen, womit es NICHT praktikabel ist. Also Proboert es ruhig auch mal aus, aber verwendet es bitte nicht in Eurer Software. Diese Beispiel dient NUR der Veranschaulichung.

Hinweis : Man kann mit GetWindowLong() die window procedure empfangen, die dem Fenster zugeordnet ist. Warum ruft man nicht gleich WndProc() direkt? Nun ein Grund ist, das der Message-Loop für alle Fenster zuständig ist, , einschlißlich Buttons, List Boxen, die eigene window proceduren haben, so sollten wir sicher stellen, das die richtige procedure gerufen wird. Für dasn Fall, das mehrere Fenster die gleiche procedure verwenden, ist der erste Parameter ein handle auf das Fenster, für welches die Nachricht gedacht ist.

Wie man hier gut sehen kann verbringt ein Windows Programm die msiste Zeit damit, sich in der message loop im Kreise zu drehen. Wo freudig Nachrichten an glückliche Fenster versendet werden, die diese dann bearbeiten. Was ist jetzt aber zu tun um ein Programm zu beenden ? Solange wir eine while() Schleife verwenden brauchen wir eine Abbruchbedinung. Diese liedert GetMessage() mit einem Rückgabewert FALSE (aka 0), wenn das Programm beendet werden soll und WinMain() verlassen wird. Und genau das macht die Funktion PostQuitMessage(). Eine WM_QUIT Nachricht wird in die message queue gelegt, und anstelle eines positiven Rückgabewertes wird FALSE zurückgegeben, GetMessage() füllt die MSG Struktur aus und kehrt mit 0 zurück. An dieser Stelle, wird der Parameter wParam mit dem Wert gefüllt der an PostQuitMessage() übergeben wurde. Diesen kann man entwerde ignorieren oder auch an WinMain() zurückgeben.

Wichtig: GetMessage() wird eine -1 zurückgeben wenn ein Fehler auftritt. Daran sollte man unbedingt denken, oder man fällt drauf rein ... selbst wenn GetMessage() mit einem Rückgabewert von BOOL definiert ist, kann es andere Werte als NUR TRUE oder FALSE enthalten, denn BOOL ist definiert als UINT (unsigned int). Die folgenden Zeilen enthalten CODE der erst mal so aussieht als würde er funktionieren, sollte aber nie benutzt werden, weil es nicht funktionieren wird oder vielleicht doch. ;-)

    while(GetMessage(&Msg, NULL, 0, 0))
    while(GetMessage(&Msg, NULL, 0, 0) != 0)
    while(GetMessage(&Msg, NULL, 0, 0) == TRUE)
ALLE DREI BIESPIELE SIND FALSCH! Fassen wir noch mal zusammen, alle diese Beispiele können funktionieren, solange GetMessage() keinen Fehler meldet. Aus genau diesem Grund sollte immer der folgende Code verwendet werden.
    while(GetMessage(&Msg, NULL, 0, 0) > 0)

Das war's erst mal zum message loop, mehr gibt es bis hier her erst mal nicht dazu zu sagen.

Weiter mit : Wie werden Resourcen, Menus und Icons verwendet ? Zurück : Einleitung - Ein einfaches Fenster


---[ letzte Änderung : 19.04.2002 ]---