Die WinAPI Plattform

Tutorials

Tutorial Nummer 02

(02.) Ein eigenes Fenster

Die MessageBox Methode, um mit dem Benutzer in Interaktion zu treten, ist zwar ziemlich einfach, aber (fast) genauso einfach ist dann auch die Ebene der Kommunikation. Was wir also brauchen ist ein eigenes Fenster, das wir so gestalten können, wie wir das wollen und wie wir das brauchen. In diesem Tutorial lernst du zwar nur das Grundgerüst eines Fensters, also ein leeres, zu schreiben, aber glaube mir, das ist schon schwer genug. In den folgenden Tutorials lernst du dann, wie man das Fenster mit Informationen füllt. Jedoch ist an unserem Fenster schon alles 'dran, was ein Fenster ausmacht. Das Fenster hat eine Titelleiste, ein Systemmenü, den Anwendungsbereich und besitzt die Möglichkeit vergrößert oder verkleinert zu werden. Als erstes definieren wir wieder den Bezeichner STRICT. Danach müssen wir die windows.h Headerdatei einbinden, damit dem Compiler alle Prototypen und Konstanten Definitionen zur Verfügung stehen.

#define STRICT

#include <windows.h>

Das Betriebssystem Windows arbeitet ereignisorientiert, das heißt, dass die Programme nicht nach Informationen bezüglich Systemänderungen oder Benutzerereignissen fragen, sondern sie bekommen diese Informationen bei Bedarf frei Haus geliefert. Etwas technischer Beschrieben, ruft Windows eine Funktion des Programms auf, wenn eines dieser Ereignisse eingetreten ist. Diese Funktion ist vom Programm definiert und bestimmt so den Ablauf des Programms. So eine Funktion (auch Window Procedur oder kurz WndProc genannt) ist jedoch nicht an ein Programm gebunden, sondern an ein Fenster. Es kommt daher häufig vor, dass ein Programm mehrere solcher Funktionen unterhält. Über die Parameter der WndProc Funktion bekommt man die Art (also, was passiert ist) der Nachricht übergeben. Denn, falls es noch nicht deutlich geworden ist, eine WndProc Funktion ist für alle Ereignisse eines Fensters zuständig. Die Palette reicht vom skalieren des Fensters über einen Mouseklick bis hin zum Empfangen von Daten.

Die WndProc Funktion gibt einen LRESULT Wert zurück. LRESULT ist ein typedef auf einen long Typ. CALLBACK ist genau das gleiche, wie WINAPI (in Visual Studio läuft das wieder auf __stdcall hinaus), jedoch sollte man zu Dokumentationszwecken diese beiden Konstanten unterscheiden. CALLBACK sagt aus, dass diese Funktion von Windows aufgerufen wird. Und vielleicht ist in einer folgenden Windows Version wieder ein Unterschied zwischen diesen Aufrufkonventionen. Der Name der WndProc Funktion ist frei wählbar. Der erste Parameter ist der Handle zu dem Fenster, für das die Nachricht bestimmt ist. Ein Handle ist eine Kennziffer, ein Verweis auf ein Objekt. Mit der nackten Zahl kann man nichts anfangen. Man gibt den Handle an Windows Funktionen weiter, die sich dann das Objekt aus dem Speicher laden können. Die Kennziffer ist in einer Tabelle dem Speicherplatz zugeordnet. So können die Daten auf der Festplatte ausgelagert werden oder die Daten werden aus defragmenierungs Gründen verschoben. HWND steht also für Handle [to a] Window. Der zweite Parameter message enthält die Kennziffer der Nachricht. Für alle Nachrichten sind in winuser.h Konstanten deklariert. Die nächsten beiden Parameter enthalten je nach Nachricht verschiedene Daten. WPARAM ist vom Typ unsigned int. Das W stammt noch aus alten Tagen, als ein int noch 16 Bit breit war, damals war WPARAM also noch ein Word.

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

Den Programmnamen speichern wir in einer globalen Konstante. Dies hat den Vorteil, dass wir im kompletten Programm nur einmal dafür Speichern benötigen und wir den Programmnamen ganz einfach ändern können.

const char szAppName[] = "Ein eigenes Fenster";

Wie in jedem Windows API Programm brauchen wir die WinMain Funktion. Der erste Parameter HINSTANCE ist ein Handle auf diese Programminstanz. Mit dieser Kennziffer kann man das ausführende Programm identifizieren. Der zweite Parameter ist vom gleichen Typ. Jedoch wird dieser Wert in Win 32 immer Null sein. In den alten 16 Bit Programmen teilten sich mehrere Instanzen eines Programms den Adressraum, deshalb war in hPrevInstance immer der Handle zu der vorherigen Instanz gespeichert. In Win 32 sind die Adressräume streng von einander getrennt, weswegen ein Handle auf die vorherige Instanz wenig Sinn haben würde. Der dritte Parameter enthält die Kommandozeilen Parameter. Jedoch sind sie in Windows noch nicht so schön aufgeteilt, wie in Dos (argc und argv). Man muss sich bei Bedarf einen kleinen Parser schreiben. Mit dem vierten Parameter wird übergeben, wie das eventuelle Fenster angezeigt werden soll (maximiert, minimiert usw.). Mehr über die Funktion im MSDN: WinMain.

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 
                   PSTR szCmdLine, int iCmdShow)
{

Wer C++ programmiert, kann die Variablen auch erst bei dem ersten Auftreten deklarieren und dann gleich initalisieren. Die erste Variable hWnd soll später den Handle für unser Fenster speichern. Die Struktur msg wird gebraucht, um Windows Nachrichten abzuholen und zu bearbeiten (im MSDN: MSG). Die WNDCLASS Struktur benutzen wir, um den Typ der Fensterklasse festzulegen (im MSDN: WNDCLASS).

   HWND       hWnd;
   MSG        msg;
   WNDCLASS   wc;

Windows ist in Bezug auf die Art der Fenster sehr vielfältig. Man muss also bevor man ein Fenster erstellt ersteinmal genau festlegen, wie das Fenster aussehen soll. Man erstellt also eine Fensterklassen. Dazu füllt man die WNDCLASS Struktur mit Werten und übergibt diese dann der RegisterClass Funktion, die die Fensterklasse registriert. Die Membervariable style legt das technische Verhalten der Fensterklasse fest. Wir geben zum Beispiel an, dass das Fenster bei einer Änderung der Größe sowohl in horizontaler als auch in vertikaler Richtung neu gezeichnet werden soll. In lpfnWndProc (Long Pointer [to a] Function) wird die Adresse der Funktion gespeichert, die die Nachrichtenbearbeitung übernehmen soll. In C/C++ ist der Name einer Funktion ohne den Klammeroperator() die Adresse der Funktion.

   wc.style         =  CS_HREDRAW | CS_VREDRAW;
   wc.lpfnWndProc   =  WndProc;

Mit diesen Variablen kann man den zusätzlichen Speicher für die Fensterklasse bzw. das Fenster reservieren. In diesem Tutorial brauchen wir keinen extra Speicher.

   wc.cbClsExtra    =  0;
   wc.cbWndExtra    =  0;

hInstance müssen wir den Handle zu unserer Programminstanz übergeben, damit nur unser Programm diese Fensterklasse benutzen kann. Mit hCursor kann man den Typ des Cursors bestimmen. Wenn man hier Null angibt, erscheint auf unserem Fenster kein Cursor. Wir laden mit der Funktion LoadCursor (im MSDN: LoadCursor) den standard Pfeilcursor. Die hIcon Membervariable speichert einen Handle auf ein Icon. Auch hier laden wir ein standard Icon mit der Funktion LoadIcon (im MSDN: LoadIcon). Dann müssen wir noch über die Variable hbrBackground (hbr ist die Abkürzung für Handle [to a] Brush) die Hintergrundfarbe des Fensters festlegen. Diesmal laden wir einen weißen Hintergrund über die GetStockObject Funktion (im MSDN: GetStockObject). In C++ muss man den Rückgabetyp noch auf HBRUSH casten.

   wc.hInstance     =  hInstance;
   wc.hCursor       =  LoadCursor(NULL,IDC_ARROW);
   wc.hIcon         =  LoadIcon(NULL,IDI_APPLICATION);
   wc.hbrBackground =  (HBRUSH)GetStockObject(WHITE_BRUSH);

Die lpszClassName Membervariable soll den Fensterklassennamen speichern. Über diesen Namen kann man dann später Fenster dieser Klasse erzeugen. Die Hauptfensterklasse bekommt üblicherweise den Namen des Programms. Da unser Fenster (noch) kein Menü haben soll, geben wir bei der lpszMenuName Variablen Null an.

   wc.lpszClassName =  szAppName;
   wc.lpszMenuName  =  NULL;

Die RegisterClass Funktion (im MSDN: RegisterClass) registriert bzw. meldet unsere Fensterklasse beim Fenstermanager von Windows an.

   RegisterClass(&wc);

Mit der CreateWindow Funktion (im MSDN: CreateWindow) erstellen wir ein Fenster nach unserer registrierten Fensterklasse. Dem ersten Parameter lpClassName wird der Name der Fensterklasse als Zeichenkette übergeben. Über den zweiten Parameter können wir den Text in der Titelleiste beeinflussen. Im dritten Parameter speichern wir den Stil des Fensters. Die nächsten vier Parameter sind für die räumliche Position des Fensters verantwortlich. Der vorletzte Parameter muss wieder der Handle auf unsere Programm Instanz sein, damit man das Fenster unserem Programm zuordnen kann. Die anderen drei Parameter sind im Moment unwichtig, daher belassen wir sie bei Null. Die CreateWindow Funktion liefert als Rückgabewert den Handle auf das Fenster zurück.

   hWnd = CreateWindow(szAppName,
                       "Titelleiste",
                       WS_OVERLAPPEDWINDOW,
                       CW_USEDEFAULT,          /* X-Position auf dem Monitor */
                       CW_USEDEFAULT,          /* Y-Position auf dem Monitor */
                       CW_USEDEFAULT,          /* Fensterbreite              */
                       CW_USEDEFAULT,          /* Fensterhoehe               */
                       NULL,
                       NULL,
                       hInstance,
                       NULL);

Unser Fenster ist nun erstellt, es erscheint jedoch noch nicht auf dem Bildschirm. Dazu müssen wir erst das Startzeichen geben. Dies tuen wir mit der ShowWindow Funktion (im MSDN: ShowWindow). Der erste Parameter ist der Handle auf unser Fenster. Den zweiten Parameter iCmdShow übernehmen wir aus den Übergabeparametern der WinMain Funktion. Man kann an der Stelle von iCmdShow auch Konstanten einsetzen, die dann festlegen, wie das Fenster angezeigt werden soll (z.B. SW_SHOW). Die UpdateWindow Funktion (im MSDN: UpdateWindow) lässt den Anwendungsbereich, also den freien Fensterbereich, sofort nach dem Start neu zeichnen.

   ShowWindow(hWnd, iCmdShow);
   UpdateWindow(hWnd);

Das Fenster ist nun erstellt und wird angezeigt. Nun warten wir auf die Eingabe bzw. Aktivitäten des Benutzers. Wenn etwas passiert, informiert uns Windows über die entsprechende Nachricht. Jedoch werden uns die Nachrichten nicht aufgezwängt, sonder sie werden in einer Warteschleife abgelegt, von der wir sie erst abholen müssen. Dies tut die GetMessage Funktion (im MSDN: GetMessage) solange sie einen Wert ungleich Null zurückliefert. Und Null wird nur zurückgegeben, wenn wir das Programm durch einen Aufruf von PostQuitMessage beenden wollen. Wenn keine Nachricht vorhanden ist, dann bleibt unser Programm in der Funktion stehen und wartet auf die nächste Nachricht. In dem ersten Parameter wird die Nachricht gespeichert. Im zweiten Parameter können wir uns auf das Abholen von Nachrichten für nur ein Fenster beschränken. Wenn wir dies wollten, müssten wir hier den Handle des Fensters eintragen. Jedoch würde dann unser Programm nicht korrekt beenden, da zum Beispiel die Nachricht, die PostQuitMessage sendet nicht für unser Fenster bestimmt ist, da dies ja schon zerstört wurde. Mit dem dritten und vierten Parameter kann man die Art der Nachrichten beschränken. Es werden nur Nachrichten abgeholt, die zwischen diesen beiden Werten liegen (bedenke, dass die Nachrichten als int dargestellt werden). Eine Ausnahme ist, wenn beide Parameter Null sind, dann werden alle Nachrichten abgeholt.

   
   while (GetMessage(&msg, NULL, 0, 0))
   {

Den ersten Bearbeitungsschritt macht die TranslateMessage Funktion, die jedoch nur für die Verarbeitung von Tastaturnachrichten gebraucht wird. Die Funktion wird dann in dem entsprechenden Tutorial erklärt. Den letzten und wichtigsten Schritt macht jedoch die DispatchMessage Funktion (im MSDN: DispatchMessage), sie verteilt die Nachrichten an die jeweilige Windows Prozedur, also auch an unsere WndProc Funktion.

      
      TranslateMessage(&msg);
      DispatchMessage(&msg);
   }

Wie oben schon gesagt, muss das Programm mit der Rückgabe eines Integer Wertes beendet werden. Dieser Wert steht im wParam der letzten Nachricht, die GetMessage bearbeitet hat.

   return msg.wParam;
}

Die WndProc Funktion bearbeitet alle Nachrichten, die für unsere Fensterklasse bestimmt sind.

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{

Als erstes müssen wir herausfinden, welche Nachricht uns Windows (oder ein anderes Fenster) geschickt hat. Dies machen wir am besten mit einer switch/case Konstruktion, denn wir wollen normalerweise eine Variable mit viele Konstanten vergleichen.

   switch (message)
   {

Wenn die Nachricht vom Typ WM_DESTROY ist, dann senden wir mit Hilfe der PostQuitMessage Funktion (im MSDN: PostQuitMessage) die Nachricht WM_QUIT, die die GetMesssage Funktion veranlasst, dass Programm zu beenden. Der Parameter wird später als Rückgabewert der WinMain Funktion benutzt. Wenn wir eine Nachricht bearbeitet haben, beenden wir die WndProc Funktion mit der Rückgabe von Null.

   case WM_DESTROY:
      {
         PostQuitMessage(0);
         return 0;
      }
   }

Da man bei den meisten Nachrichten nichts spezielles machen möchte, kann man diese Nachrichten an die DefWindowProc Funktion (im MSDN: DefWindowProc) weitergeben, die alle Nachrichten mit einer standard Bearbeitung behandelt. Die Parametervariablen, die wir durch die WndProc Funktion erhalten haben, geben wir einfach an die DefWindowProc Funktion ("Default Window Procedur") weiter.

   return DefWindowProc(hWnd, message, wParam, lParam);
}

Dieses Programm bringt ein einfaches und leeres Fenster auf den Bildschirm (Screenshot).

In den folgenden Tutorials wird die WinMain Funktion zum Großteil immer gleich bleiben. Das eigentliche geschehen findet ja auch in der WndProc Funktion statt, obwohl das in diesem Tutorial noch nicht so deutlich werden mag. Windows ist ein Ereignis gesteuertes System, das heißt, dass ein Programm nur aufgrund eines Ereignis' (zum Beispiel ein Mouseklick) zum Leben erwacht. Wenn nichts zu tun ist, verbraucht ein Programm keine Rechenzeit, da es in der GetMessage Funktion stecken geblieben ist und nicht eine ständig abfragende Schleife durchläuft. Denn Windows ist ein Multitask fähiges Betriebssystem, das bedeutet, dass mehrere Programme (fast) gleichzeitig laufen können. Wenn also ein Programm gerade nichts zu tun hat, kann die Rechenzeit einem anderen Programm zugeschoben werden. Die neueren Windows Versionen (Win 32) unterstützen auch noch Multithreating. Dies heißt wiederum, dass ein Programm aus mehreren Prozessen bestehen kann, die auch praktisch gleichzeitig laufen.

Übungen

webmaster@win-api.de