C/C++ Programmierung: Entwicklung einer einfachen Windows-Rahmenanwendung (Game Shell)

Basis für ein unter Windows lauffähiges Spiel ist eine einfache Fensteranwendung. An dieser Stelle soll nun die Funktionsweise einer Windows-Anwendung kurz besprochen werden.

Windows ist ein fensterbasiertes und ereignisgesteuertes Betriebssystem. Das Herz einer jeden Windows-Anwendung ist die Funktion WinMain(), innerhalb derer das Programmfenster erzeugt, alle notwendigen Initialisierungschritte und Aufräumarbeiten durchgeführt und die Spielschleife wieder und wieder durchlaufen wird. Die Eigenschaften des Anwendungs-Fensters werden in einer Struktur mit dem Namen WNDCLASSEX gespeichert. Nachdem die Fenstereigenschaften festgelegt, also einer Strukturvariablen vom Typ WNDCLASSEX alle hierfür notwendigen Werte übergeben worden sind, muss das Fenster mithilfe der Funktion RegisterClassEx() registriert werden. Ist die Registrierung erfolgreich verlaufen, steht der Erzeugung des Fensters mittels der Funktion CreateWindowEx() nichts mehr im Wege. Für jedes laufende Programm erzeugt Windows einen so genannten Message-Queue. Wenn irgendein Ereignis stattfindet, das ein laufendes Programm betrifft (z. B. ein Mausklick), wird dieses Ereignis in eine Nachricht umgewandelt und im Message-Queue platziert. Zur Abfrage dieser Nachrichten können die Funktionen GetMessage() oder PeekMessage() eingesetzt werden. Aus Sicht des Spieleprogrammierers ist die zweite Funktion interessant, denn diese „schaut“ lediglich kurz nach, ob eine Nachricht zur Bearbeitung ansteht. Falls das der Fall ist, muss die Nachricht für die Weiterbearbeitung in ein bestimmtes Format übersetzt werden. Hierfür ist die Funktion TranslateMessage() verantwortlich. Jetzt kommt Windows wieder ins Spiel. Mithilfe der Funktion DispatchMessage() übergibt man Windows die Nachricht mit der Bitte um Weiterleitung an eine Callback-Funktion, welche für die Nachrichtenbearbeitung zuständig ist. Für den Aufruf dieser Funktion ist das Windows-Betriebssystem verantwortlich.

Hinweis: Eine Callback-Funktion ist eine Funktion, die zwar im eigenen Programm definiert ist, aber nur durch ein anderes Programm (z. B. dem Windows-Betriebssystem) aufgerufen wird. Der Funktionsname ist frei wählbar, die Parameterliste ist jedoch fest vorgegeben.

In der hier vorgestellten Windows-Rahmenanwendung werden lediglich die Nachrichten WM_CREATE (wird aufgerufen, wenn das Fenster erzeugt wird), WM_PAINT (wird aufgerufen, wenn das Fenster neu gezeichnet werden muss) sowie WM_DESTROY (wird beim Beenden des Programms aufgerufen) bearbeitet. Das Neuzeichnen eines Fensters wird von der Funktion BeginPaint() durchgeführt. Wenn die Anwendung beendet werden soll, muss mittels der Funktion PostQuitMessage() die Nachricht WM_QUIT abgeschickt werden. Steht diese Nachricht dann zur Bearbeitung an, so wird die while-Schleife, die innerhalb von WinMain() für die Nachrichtenweiterleitung verantwortlich ist, verlassen und die Anwendung beendet.

Sowohl die WinMain()- als auch die Callback-Funktion für die Nachrichtenbearbeitung sollen nun eingehend betrachtet werden:

#include <windows.h>

HWND      main_window_handle = NULL;
HINSTANCE hinstance_app      = NULL;

#define WINDOW_CLASS_NAME "Game Shell Window"

long g_screenwidth;
long g_screenheight;



LRESULT CALLBACK WindowProc(HWND hwnd,
                            UINT msg,
                            WPARAM wparam,
                            LPARAM lparam)
{
    PAINTSTRUCT ps;
    HDC hdc;

    // Nachrichtenverarbeitung
    switch(msg)
    {
        case WM_CREATE:
        {
            return(0);
        }
        break;

        case WM_PAINT:
        {
            hdc = BeginPaint(hwnd,&ps);
            EndPaint(hwnd,&ps);
            return(0);
        }
        break;

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

        default:
        break;
    }
    return (DefWindowProc(hwnd, msg, wparam, lparam));

}

int WINAPI WinMain(HINSTANCE hinstance,
                   HINSTANCE hprevinstance,
                   LPSTR lpcmdline,
                   int ncmdshow)
{
    WNDCLASSEX winclass;
    HWND hwnd;
    MSG msg;

    // Fenstereigenschaften festlegen:
    winclass.cbSize         = sizeof(WNDCLASSEX);
    winclass.style          = CS_HREDRAW | CS_VREDRAW;
    winclass.lpfnWndProc    = WindowProc;
    winclass.cbClsExtra     = 0;
    winclass.cbWndExtra     = 0;
    winclass.hInstance      = hinstance;
    winclass.hIcon          = LoadIcon(NULL, IDI_APPLICATION);
    winclass.hCursor        = LoadCursor(NULL, IDC_ARROW);
    winclass.hbrBackground  = (HBRUSH)GetStockObject(BLACK_BRUSH);
    winclass.lpszMenuName   = NULL;
    winclass.lpszClassName  = WINDOW_CLASS_NAME;
    winclass.hIconSm        = LoadIcon(NULL, IDI_APPLICATION);

    hinstance_app = hinstance;

    if(!RegisterClassEx(&winclass))
        return(0);

    // Hier könnte man die Fenstergröße, Farbtiefe, usw. festlegen:
    InitResolutionAndRenderOptions();



    if(!(hwnd = CreateWindowEx(NULL,
                               WINDOW_CLASS_NAME,
                               "My Game Shell",
                               WS_OVERLAPPEDWINDOW | WS_VISIBLE,
                               0,0,
                               g_screenwidth, g_screenheight,
                               NULL,
                               NULL,
                               hinstance,
                               NULL)))
    return(0);

    main_window_handle = hwnd;

    // Hier könnte man Initialisierungsarbeiten durchführen:
    Game_Init();

    // Nachrichtenweiterleitung
    while(TRUE)
    {
        if(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
        {
            if(msg.message == WM_QUIT)
               break;

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

        // Hier läuft das Spiel ab:
        Game_Main();
    }

    // Hier könnte man Aufräumarbeiten durchführen:
    Game_Shutdown();

    return(msg.wParam);
}