Splash-Screens mit der Win32 selbst erstellen

von Marco Leithold

 

 

Einleitung

 

Dieses Tutorial soll euch zeigen, wie man, für sein Spiel, ein Begrüßungsfenster erstellt und wir werden uns zum Schluss auch mit ungewöhnlichen Fensterformen beschäftigen. Diese Dinge sollten in keinem modernen Spiel fehlen. Ich werde hier nichts Vorraussetzen, sondern Schritt für Schritt mit euch solch ein Fenster erstellen. Es werden einige Techniken vorgestellt, die für die meisten von euch neu sein wird. Unter anderem werden wir uns mit der Threadprogrammierung beschäftigen. Keine Angst wir brauchen uns nicht mit Synchronisation von Threads beschäftigen, da unser neuer Thread für sich selbst arbeiten wird und unser Hauptthread diesen entweder erzeugt bzw. mitteillt das der erzeugte Thread sich wieder beenden soll. Als Grundlage werde ich Zerbies Tutorial  benutzen, dabei ist es egal ob ihr Kapitel 3 oder bis Kapitel 8 den Quellcode nimmt.

 

 

Threads

 

Jetzt wird es erst mal ein bisschen theoretisch. Wie schon erwähnt werden wir uns bei den Threads weder mit der Synchronisation, noch mit der Thread-Ablaufsteuerung oder mit Prioritäten beschäftigen. Dies werden wir alles nicht benötigen und wird somit Thema eines anderen Kapitels sein.

Wie ihr wisst können alle modernen Betriebssystemen Multitasking, d.h. es ist möglich mehre Anwendung, bzw. Prozesse, nebeneinander ablaufen zulassen, ohne das die Anwendungen sich gegenseitig behindern. Dies wird unter Windows so erreicht das jeder Prozess sein eigenen virtuellen Arbeitsspeicher besitzt, der derzeit bei 4 GB liegt. Des weiteren besitzt jeder Prozess ein Kernel-Objekt, das vom Betriebssystem zur Verwaltung des Prozesses eingesetzt wird. Prozesse sind träge und führen an sich keinen Code aus. Sie dienen nur als Container und zur Verwaltung der Threads. Threads sind kurz gesagt verantwortlich für die Ausführung des im Prozessadressraum abgelegten Programmcode. Somit wir jedes Mal, wenn ein Prozess gestartet wird, ein Thread mit gestartet. Dieser wird auch als Hauptthread bezeichnet. Jeder dieser Hauptthreads muss entweder eine Funktion namens WinMain oder main besitzen, da diese Funktionen Einsprunkspunkte zu eurer Anwendung sind.

Da ein Prozess mehrere Threads besitzen kann, die Programmcode im Adressraum gewissenermessen gleichzeitig ausführen können, wird vom Betriebssystem pro Thread ein Stack und eine Menge CPU-Register angelegt und verwaltet, d.h. schaltet das Betriebsystem zwischen zwei Threads um, werden, die CPU-Register und der Stack des einen Threads gespeichert und vom neuen geladen. Damit ist sichergestellt, das ein Thread nach seiner (Zwangs)Wartepause immer noch weis wo er stehen geblieben ist. Bitte behaltet im Hintergrund, das ein Stack Thread abhängig ist und ein Heap zum Prozess gehört. Dies werde ich in späteren Tutorial’s noch näher erläutern.

Wie bereits erwähnt besitzt jeder Prozess ein Hauptthread. Dieser Thread besitzt eine Callback-Funktion die ihr alle kennt, WinMain bzw. main. Werden diese Funktionen von  euch beendet, wird auch automatisch dieser Thread zerstört und damit auch der Prozess vom Betriebssystem eliminiert. Denn ein Prozess ist ohne mindesten einen Thread nicht überlebensfähig und wird deswegen vom Betriebssystem zerstört.

Vom Hauptthread haben wir die Möglichkeit eigene Threads zu erzeugen, was wir jetzt auch machen werden. Dazu dient die Funktion CreateThread

 

HANDLE CreateThread(  LPSECURITY_ATTRIBUTES lpThreadAttributes, 
                                       DWORD dwStackSize,
                                       LPTHREAD_START_ROUTINE lpStartAddress,
                                       LPVOID lpParameter,                      
                                       DWORD dwCreationFlags,                    
                                       LPDWORD lpThreadId

);

 

Im folgenden Abschnitt werden wir uns ein bisschen mit den Parametern beschäftigen, jedoch nur soweit wie wir sie für unser Programm auch benötigen werde.

 

lpThreadAttributes

Dieser Parameter enthält einen Zeiger auf eine SECURITY_ATTRIBUTES Struktur. Dies braucht uns hier aber nicht weiter zu interessieren, da wir die Standardsicherheitsattribute verwenden werden. Dazu müssen wir hier nur den Wert NULL übergeben.

 

dwStackSize

Mit diesem Parameter geben wir an, wie viel Adressraum des Prozesse der Thread für seinen eigenen Stack verwenden kann. Auf diesen Parameter werde ich auch ein anderes mal genauer eingehen, hier reicht es vollkommen aus den Wert 0 zu übergegeben. Mit dem Wert 0 wird nicht etwa ein Stack der Größe 0 angelegt, sondern wir überlassen das dem Compiler. Der richtige Wert wir vom Linker in der .EXE Datei abgelegt und dieser Wert wird letztendlich dann auch benutzt. Standardmäßig beträgt die Stack-Größe 1 MB.

 

lpStartAddress

Dieser Parameter interessiert uns schon wesentlich mehr. Er ist auch der wichtigste, hier wird nämlich die Startadresse der Thread-Funktion angegeben, die beim Start des neuen Threads ausgeführt werden soll. Diese Funktion ist eine Callback-Funktion. Bevor wir uns der Funktion widmen, müssen wir uns noch den nächsten Parameter anschauen, da dieser zu dieser Funktion dazu gehört.

 

lpParameter

Über diesen Parameter könne wir Werte an die Funktion, auf die lpStartAddress zeigt, übergeben.

 

Und so schaut die Callback-Funktion aus:

 

DWORD WINAPI ThreadProc(  LPVOID lpParameter  ); 
 
dwCreationFlags
Mit diesem Paramter steuern wir unseren Thread. Es gibt zwei mögliche Werte, der erste ist der Wert 0 und bewirkt eine sofortige Ausführung des Threads im Anschluss an dessen Erzeugung. Der zweite Wert ist das Flag CREATE_SUSPENDED. Dieser Wert bewirkt, das das Betriebssystem zwar den Thread ordnungsgemäß erzeugt, ober sofort die automatische CPU-Zeitzuteilung entzieht. Damit wird der Thread schlafen gelegt, bis wir in auswecken.
 
 

lpThreadId        

Hier wird die Thread Kenziffer (ID) zurückgeliefert, die das Betriebssystem dem Thread zugewiesen hat. Intern arbeitet das Betriebssystem mit dieser ID.

Es ist zwar möglich hier den Wert NULL zu übergeben, allerdings unterstützt dies nur Windows NT/2000. Damit euer Programm korrekt auf allen Windows Betriebssystem läuft, solltet ihr hier eine korrekte Variable von Typ DWORD übergeben.

 

Obwohl ich keine Threadsteuerung hier erklären möchte, kommt alles in späteren Tutorials, möchte ich aber doch noch eine Funktion kurz erklären.

 

VOID Sleep ( DWORD dwMilliseconds );   

 

Diese Funktion veranlasst den Thread sich selbst für eine gewisse Zeit, in Millisekunden, anzuhalten. Dieser Aufruf wird sofort ausgeführt, d.h egal wie viel Zeit der Thread noch auf der Zeitscheibe zu Verfügung hat,  er wird sofort Still gelegt. Der Wert 0 hat hier wieder mal eine Besonderheit, und zwar gibt der Thread nur seine restliche Zeit die auf der Zeitscheibe hat frei und gibt damit anderen Threads die Möglichkeit an die Reihe zukommen. Ist kein weiterer Thread arbeitsfähig bekommt dieser Thread wieder die Möglichkeit weiter zu arbeiten. 

 

So, für gerade mal zwei Funktion gibt es ja schon viel zu verdauen, deswegen zeig ich euch Ersteinmahl ein Pseudo-Code.

 

...

DWORD dwThreadID;

 

HANDLE hThread = CreateThread( NULL, 0, ThreadProc, NULL, 0, &dwThreadID );

 

Sleep (0);          // Bremse damit der zweite Thread sofort mit seiner Arbeit beginnt,

            // diese Funktion legt diesen Thread für eine Millisekunde lahm.

 

// Da der Thread nicht weiter mehr benötigt wird, schließen wir ihn wieder.

CloseHandle(hThread);

 

...

 

DWORD WINAPI ThreadProc(PVOID lpParameter)

{

            cout << “ Ich bin ein neuer Thread!\n“;

 

            return 0;

}

 

 

Da alle jemals erzeugten Handle auch wieder geschlossen werden müssen, rufen wir die Funktion CloseHandle auf und übergeben das Handle des erzeugten Threads.

Kleine Anmerkung noch, ich werde hier nicht auf die unterschiedlichen Möglichkeiten des beenden von Threads eingehen, sondern die einzig Richtige. Nämlich das normale Beenden, siehe WinMain(main). Dies hat auch ein Grund, denn nur in diesem Fall werden auch alle Ressourcen ordnungsgemäß freigegeben. Wie dies Funktioniert brauch uns hier aber nicht weiter zu interessieren.

 

 

Splash-Screen

 

So, das schwierigste, nämlich das Verständnis über Thread, haben wir geschafft. Doch wozu haben wir uns dies alles angetan? Nun, ein Splash-Screen dient ja nur dazu um den Anwender zuzeigen, das unser Programm gestartet wurde und nicht abgestürzt ist, nur weil es eine halbe Ewigkeit dauert bis das Hauptfenster erscheint. Damit  unser Hauptthread nicht mit Code belastet wird, der nur der Verschönerung dient, legen wir unseren Splash-Screen in einen extra Thread.

Es gibt viele Gestaltungsmöglichkeiten für diese Fenster. Diese Gestaltungsmöglichkeiten sind unter ME und 2000 getestet worden. Gut, dan lasst uns mal Zerbies-Code Aufmottseen, keine Angst Tim war hier nicht am Werk. Unser Thread werden wir gleich als erste nach WinMain einfügen, so das dann sehr schnell unser Fenster erscheinen wird. Kleiner Hinweis es kann nie genau Vorhergesagt werden, wann unser Splash-Screen erzeugt wird, denn die Threads werden vom Betriebssystem verwaltet. Das einzige was wir machen können ist, denn Hauptthread kurzzeitig schlafen zu legen. Damit geben wir anderen Threads die Möglichkeit ihr Arbeit zumachen. Allerdings ist dann immer noch nicht gesagt das unser zweiter Thread sofort an die Reihe kommt, den Grund werde ich euch ein anderes mal genauer erläutern.

 

Neuen Thread anlegen

 

So, jetzt habe ich euch die ganze Zeit eine API-Funktion erklärt die wir gar nicht benutzen werden, oh ich glaube ich sehe erboste gesiechter.  Nein, nein die Funktion die ich oben ausführlich erklärt habe, werden wir auch benutzen nur eben nicht direkt, sondern dessen Container der uns von der C/C++ Runtime Libary zur Verfügung gestellt wird. Der Grund ist einfach: Die C/C++ Laufzeitblibliothek hat mehrer globale Variablen die Thread sicher gemacht werden müssen, so das diese Daten als Datenblock an jeden Thread übergeben wird. Somit kann es hier nicht zu Datenverfälschung kommen.  Die Funktion der C/C++ Laufzeitblibliothek sieht wie folgt aus:

 

unsigned long _beginthreadex(

void *security,                                                   //== lpThreadAttributes

unsigned stack_size,                                         //== dwStackSize

unsigned ( __stdcall *start_address )( void * ), //== lpStartAddress

void *arglist,                                                     //== lpParameter

unsigned initflag,                                             //== dwCreationFlags

unsigned *thrdaddr );                                        //== lpThreadId

 

Den Rückgabewert müssen wir dann nur noch Casten und dann haben wir das Handle auf unseren neuen Thread.

Bevor wir aber entgültig beginnen, müssen wir noch eine kleine Änderung an unserem Projekt durchführen und zwar muss  unserem Compiler bekannt geben werden, das wir keine Single-Thread-Code haben wollen, sondern eine Multithreaded-Anwendung. 

 

 

 

 

 

 

So jetzt steht überhaupt nichts mehr im Weg und lasst uns damit unseren Code in das bestehende Projekt einfügen. Nach WinMain werden wir unsere Funktionen einfügen:

 

 

#include <process.h>    // Für _beginthreadex

 

HINSTANCE hInstance; // Globale Variable für den

 

typedef unsigned(__stdcall *PTHREAD_START) (void*);   // Vereinfacht das Casten

 

DWORD WINAPI SplashScreen(PVOID lpParameter);    

// Thread-Funktion, dies ist eine Callback-Funktion die nach dem Anlegen unseres Thread

// vom Betriebssytem aufgerufen wird. Es spiel sabei keine Rolle wie diese Funktion heist, nur die Parameteranzahl mit ihrem Typ und der

// Rückgabewert müssen stimmen.

 

LRESULT CALLBACK WindowProc2(HWND hwnd, UINT message, WPARAM wparam,  LPARAM lparam); // 2. Callback-Funktion fürs unser Fenster

 

 

/**

 * Die Startfunktion eines jeden Windows Programms. Hier wird ein

 * leeres Fenster ohne jede Menüs o.a. erzeugt und die Haupt-

 * schleife des Spiels gestartet.

 */

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hprevinst,

                                                               LPSTR lpcmdline, int ncmdshow)

{

            DWORD dwThreadID;

           

 

            HANDLE hThread = (HANDLE) _beginthreadex(

 NULL,             // Standardsicherheit

                                                            0,                    // Standard-Stack-Größe

                                                            SplashScreen, // unsere Thread-Funktion

                                                            (void*)hinst,      // als Beispiel übergeben wir hier HINSTANCE

                                                            0,                     // Unser Thread soll sofort ausgeführt werden

                                                            &dwThreadID);//  gelieferte ID übernehmen

 

            // Als nächste könnten wir überprüfen ob, die Funktion erfolgreich war.

// Ist hTread ==  0 ist irgendwas schief gegangen.  Allerdings ist ein korrekt

// laufender Thread nicht überlebenswichtig für unser Game und es sollte

// deswegen nicht gleich das ganze Programm beendet werden.

 

            Sleep (0); // Thread kurzeitig anhalten, damit unser neuer Thread sofort

    // anfangen kann zu arbeiten.

                       

            // Da der Thread nicht weiter mehr benötigt wird, schließen wir ihn wieder.

            CloseHandle ( hThread );                                                                 

 

WNDCLASSEX  winclass;                                            // Objekt vom Typ Fenster

            MSG                   message;                              // Windows Nachricht

            const char  chKlassenname[] = "Unwichtig";       // Name für das Objekt

            DWORD       dwStartzeit;                                  // Schleifenzeit

            ....

 

 

Bevor wir jedoch zu der Funktion komme, sollten wir noch dafür sorgen dass unser Thread an der richtigen Stelle auch wieder beendet wird. Dies werden wir am besten mit der Funktion „PostThreadMessage“ bewerkstelligen. Mit dieser Funktion können wir anderen Threads eine Nachricht mitteilen, allerdings nur wenn der jenige Thread auch Nachrichtenstruktur aufgebaut hat, wie es z.B Fenster besitzen. Diese Funktion scheint mir aber nicht korrekt zu funktionieren, deswegen werde ich nicht weiter auf diese Funktion eingehen. Als alternative gibt es noch die Funktion SendMessage, allerdings braucht diese Funktion das Fenster-Handle. Da aber die Nachricht WM_QUIT mit der Funktion PostThreadMessage funktioniert, werden wir sie trotzdem benutzen. Am besten schreibt ihr die Funktion nach dem Aufruf der Funktion „Spiel_Initialisieren“, denn ab hier hat unsere Anwendung in den Fullscreen umgeschaltet und unsere Begrüßungsbildschirm wird dann nicht mehr benötigt.

 

         ...

 

// PHASE 1:

if ( Spiel_Initialisieren () )

{

                        PostThreadMessage(dwThreadID,          // THREAD_ID

        WM_QUIT,           // MSG:                     Nachricht

         0,                                   // WPARAM: Thread soll mit Wert 0 beenden

         0 );                     // LPARAM:   Unwichtig

}

 

 

Ansonsten würde unser Thread mit –1 terminiert, wenn wir ihn nicht selbst beenden. Dies hat folgenden Grund stirbt der Hauptthread, werden auch alle anderen Thread die dem Prozess des Hauptthreads zugeordnet sind ebenfalls beendet. Dies wird verursacht durch PostQuitMessage(0), was häufig im WM_DESTROY-Aufruf steht.

 

Fenster anlegen

 

So, das Anlegen eines neuen Thread’s habe wir jetzt geschafft, keine Angst das sah nur viel aus. Im Grunde dauert dies nicht länger als 5 - 10 Minuten. Was wir jetzt noch machen müssen ist unser Begrüßungsbildschirm zu erzeugen und hier gehen wir genauso vor wie beim Hauptfenster, d.h. wir legen uns erst mal eine WNDCLASSEX-Struktur an und rufen dann die Funktion CreateWindow auf. Dies alles werden wir in unserer neuen Funktion SplashScreen erledigt:

 

DWORD WINAPI SplashScreen(PVOID lpParameter)

{

            HWND hWnd;

            WNDCLASSEX  winclass;

 

            HINSTANCE hinst = (HINSTANCE) lpParameter;

            // lokale Variable für unser HINSTANCE anlegen, dies erspart uns das dauernte Casten

 

            // Initialisiere die Eigenschaften des Fensters

            winclass.cbSize            = sizeof(WNDCLASSEX);          // Größe der Struktur ermitteln und übergeben

            winclass.style               = 0;                                         // Benötigen wir nicht

            winclass.lpfnWndProc   = (WNDPROC)WindowProc1;    // Callback-Funktion für unser Fenster

            winclass.cbClsExtra      = 0;                                         // Benötigen wir nicht

            winclass.cbWndExtra    = 0;                                         // Benötigen wir nicht

            winclass.hInstance        = hinst;                                    // HINSTANCE

            winclass.hIcon              = NULL;                                   // Benötigen wir nicht

            winclass.hCursor           = LoadCursor(NULL, IDC_ARROW); 

            winclass.hbrBackground= (HBRUSH)GetStockObject(WHITE_BRUSH);

            winclass.lpszMenuName= NULL;                                  // Benötigen wir nicht

            winclass.lpszClassName= "Test";                                 // Hier kann irgendwas stehen

            winclass.hIconSm         = NULL;                                   // Benötigen wir nicht

 

            // Brav bei Windows registrieren lassen

            if (!RegisterClassEx(&winclass))

                        return 0;

 

            hWnd = CreateWindow("Test",                          // ist Identisch mit winclass.lpszClassName

    "",                                       // Benötigen wir nicht

   WS_BORDER | WS_POPUP, // Mehr brauchen wir nicht

                                                  CW_USEDEFAULT,               // Unwichtig, wird spatter geändert

   CW_USEDEFAULT,              // Unwichtig, wird spatter geändert

   550,                                      // Breite des Fensters

   300,                                      // Höhe des Fensers

   NULL,                                   // Benötigen wir nicht

   NULL,                                   // Benötigen wir nicht

   hInst,                                    // HINSTANCE

   NULL);                                  // Benötigen wir nicht

 

            // Rückgabewer von CreateWindow überprüfen, wen schiefgegangen Thread beenden

            if (!hWnd)

            {

                           return FALSE;

            }

 

            // Fenster sichtbar machen

            ShowWindow(hWnd, SW_SHOW);

            UpdateWindow(hWnd);

 

            MSG msg;

 

// Main message loop:

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

            {

                        if (!TranslateAccelerator(msg.hwnd, NULL, &msg))

                        {

                                   TranslateMessage(&msg);

                                   DispatchMessage(&msg);

                        }

            }

           

            return msg.wParam;

}

 

  

Damit haben wir unsere SplashScreen-Funktion geschrieben. Ich bin deswegen nicht auf die einzelnen Funktionen eingegangen, da ich hier davon ausgehe, das ihr wisst wie man ein Hauptfenster erstellt. Kommen wir aber jetzt zu einem schwierigeren Thema, nämlich das Laden von Bitmap’s. Wenn wir dies geschafft haben werden wir uns mit einer anderen Möglichkeit zum Manipulieren von Fenster beschäftigen, was uns noch mehr Gestaltungsmöglichkeiten bieten wird. Allerdings bin ich kein Grafiker, deswegen werde ich ein Bitmap von Zerbie nehmen. Dies alles werden wir in unsere Fenster-Callback-Funktion schreiben, die wir für unser SplashScreen als Prototyp schon angelegt haben.

 

LRESULT CALLBACK WindowProc2(HWND hwnd, UINT message, WPARAM wparam,  LPARAM lparam)

{

            // Variablen die wir später benötigen werden

            static HBITMAP hBitmap ;

            static int           cxClient, cyClient;

            BITMAP            bitmap ;

            HDC                 hdc, hdcMem ;

            HINSTANCE     hInstance ;       

            PAINTSTRUCT  ps ;

 

            // Dies wird alles später ausführlich erklärt

            switch ( message )

{

case WM_CREATE:

case WM_SIZE:

case WM_PAINT:

case WM_DESTROY:

}

 

return ( DefWindowProc ( hwnd, message, wparam, lparam ) );

}

 

 

WM_CREATE

 

Beginnen werden wir mit dem Laden des Bitmaps aus einer Datei. Da diese Funktion sehr mächtig ist, mit ihr können auch Icon und Cursor geladen, werden wir uns nur auf die Besonderheiten für das Laden von Bitmaps aus einer Datei beschäftigen.

 

HANDLE LoadImage(  HINSTANCE hinst,     
                                   LPCTSTR lpszName,  
                                   UINT    uType,
                                   int       cxDesired,
                                   int       cyDesired,     
                                   UINT    fuLoad        
                                );

 

hinst

Dieser Parameter bekommt die Instanz des Moduls, dessen ausführbare Datei das Bild enthält. Um HINSTANCE nicht Global anlegen zu müssen, werden wir einen kleinen Trick anwenden, schließlich wird uns die Variable  über LPARAM in einer Struktur übergeben. Wir müssen nur noch herausfinden wie wir HINSTANCE aus der Struktur auslesen. Die Geheimnisvolle Struktur  ist  CREATESTRUCT, was für ein toller Name. Na gut, in dieser Struktur ist auch unser HINSTANCE abgelegt, was wir also nur auslesen brauchen und an unsere Funktion übergeben.

 

hInstance = ((LPCREATESTRUCT) lParam)->hInstance ;

 

 

lpszName

Hier wird die einzulesende Datei angegeben.

 

uType

Dieser Parameter ist schon etwas interessanter, denn hier wird bekannt gegeben ob wir ein Bitmap, ein Icon oder ein Cursor laden möchten. Dies sind die Flags die ihr setzen könnt:

 

-                      IMAGE_BITMAP             0

-                      IMAGE_ICON                  1

-                     IMAGE_CURSOR             2

 

cxDesired, cyDesired

Benötigen  wir nicht, deswegen werden wir bei beiden Parameter 0 übergeben.

 

fuLoad   

Dieser Parameter ist einer der wichtigsten. Denn hier geben wir bekannt, das wir aus einer Datei lesen wollen. Dazu dient uns das Flag:

 

-                      LR_LOADFROMFILE      10h

 

 

Damit sind alle Parameter und alle wichtigen Einstellungen, die wir für unser Programm brauchen, erklärt. Es gibt noch weiter Einstellungsmöglichkeiten, dies würde aber den Rahmen dieses Tutorial’s sprengen.  Als nächstes folgt der Funktionsaufruf, achtet drauf das ihr den Rückgabewert, HANDLE, richtig castet.

 

hBitmap =  (HBITMAP) LoadImage ( hInstance, "textur.bmp", IMAGE_BITMAP, 0,0,LR_LOADFROMFILE);

 

 

Als nächstes folgt die Funktion GetObject, mit der wir die Information über unser Bitmap ermitteln. Diese Funktion kann auch Informationen über eine Palette, einem logischen Stift, einem Pinsel, einem Font und einem DIB-Abschnitt ermitteln.

 

GetObject (   hBitmap,                 // Handle des Bitmaps

sizeof (BITMAP),      // Größe des Buffers, für den dritten Parameter, in Byte

&bitmap) ;              // Unsere Bitmap-Struktur

 

// Abspeichern der Länge und Breite des Bitmap’s

cxSource = bitmap.bmWidth ;

cySource = bitmap.bmHeight ;

 

// Fenster auf dem Bildschirm zentrieren

CenterWindow (hwnd);

 

Die Funktion CenterWindow müssen wir erst noch schreiben, aber hier wird sie später aufgerufen. Mit dieser Funktion zentrieren wir unser Fenster auf dem Desktop. Danach müssen wir nur noch dafür sorgen, dass unser Fenster sichtbar ist und vor allen anderen Fenster ist. Dies übernimmt die Funktion  SetForegroundWindow. Als einzigen Parameter müssen wir nur unser Fenster-Handle übergeben.

 

SetForegroundWindow(hwnd);

 

return 0;

 

 

WM_SIZE

 

Jedes Mal wenn die Größe des Fenster sich verändert, wird WM_SIZE aufgerufen. Ebenfalls, wenn das Fenster neu Initialisiert wird und dies machen wir uns zu nutze um die Breite und Höhe des Fensters zu ermitteln. Unsere Werte werden über die Variable lParam zur Verfügung gestellt. Wir müssen nur noch mit den Makros „LOWORD“ und „HIWORD“ die Werte auslesen.

        

// Höhe und Breite des Fenster ermitteln

cxClient = LOWORD (lParam);   // niederwertiges Wort von lParam

cyClient = HIWORD (lParam);    // höherwertiges Wort von lParam

 

 

WM_PAINT

 

Damit sind wir einen großen Schritt weitergekommen. Das einzige was wir jetzt noch machen müssen, ist unser Bitmap auf unserem Begrüßungsbildschirm zu zeichnen. Dies wird in der Nachricht „WM_PAINT“ realisiert. WM_PAINT wird immer dann aufgerufen, wenn irgendwas neu gezeichnet werden muss. Dies hat mehrere Ursachen, erstens wenn das Fenster neu erstellt wird und zweitens wenn das Fenster von einem anderen Fenster verdeckt wurde.

Um auf dem Fenster zeichnen zu können benötigen wir einen Handle auf den Gerätekontext, kurz HDC. Der Gerätekontext („Device Context“, kurz DC) ist letztlich eine interne Datenstruktur des GDI und mit einem bestimmten Ausgabegerät wie ein Bildschirm oder einem Drucker verbunden. Ups, schon wieder was neues. GDI. Graphics Device Interface (kurz GDI) ist eine Bibliothek von Funktionen die uns ermöglicht graphisch unser Fenster zu verändern. Das dieses Thema sehr komplex ist, werde ich nicht weiter auf GDI und HDC eingehen. Nur auf die Funktionen dir wir hier wirklich brauchen werde ich hier vorstellen. Da sind zu erst die Funktionen „BeginPaint“ und „EndPaint“. Diese zwei Funktionen treten immer paarweise auf und ermöglichen uns das Zeichnen auf unserem Fenster.

 

HDC BeginPaint(  HWND hwnd,           
                            LPPAINTSTRUCT lpPaint );
 
BeginPaint erwatet als ersten Parameter das Handle auf unser Fenster, als zweiten Paramter einen Zeiger auf die Struktur des Typs  PAINTSTRUCT. PAINTSTRUCT enthält eine Reihe von Informationen, über die eine Window-Prozedur einen Bereich eines Fensters neu zeichnen kann.
 
typedef struct tagPAINTSTRUCT 
{ 
               HDC  hdc;                              // Handle des Device Kontext 
               BOOL fErase;                         // erkläre ich gleich genauer
               RECT rcPaint;                        // Gibt das Rechteck in das Gezeichnet werden kann
               BOOL fRestore;                      // Resservier, wird vom System benutzt
               BOOL fIncUpdate;                  // Resservier, wird vom System benutzt
               BYTE rgbReserved[32];          // Resservier, wird vom System benutzt
} PAINTSTRUCT;
 
Eine Besonderheit stellt das Feld fErase da. Ist dies Feld 0, was in dem allermeisten Fällen auch der Fall ist, wird der Hintergrund mit einem Farbwert gelöscht, den ihr in der WNDCLASS(EX) im Feld hbrBackground angegeben habt. Diese Löschen wird von der BeginPaint Funktion durchgeführt, diese Funktion liefert als Rückgabewert das HDC der PAINTSTRUCT. 
Wenn alle gezeichnet worden ist, muss der Gerätekontext mit 
 
BOOL EndPaint(  HWND hWnd,                  
                          CONST PAINTSTRUCT *lpPaint );
 
wieder freigegeben weden.
Als nächstes müssen wir mit der Funktion CreateCompatibleDC einen speicherbasiertenKontext für unser Bitmap anlegen. Dieser Kontext ist damit kompatibel zu einem exisitierenden GeräteKontext:
 
HDC CreateCompatibleDC(  HDC hdc   );
 
Danach müssen wir unser Bitmap in dem angelegten speicherbasierten Gerätekontext einsetzen (selektieren). 
 
HGDIOBJ SelectObject(  HDC hdc,                        // Handle zu einem Gerätekontext
                                       HGDIOBJ hgdiobj );        // Handle zu einem Objekt
 
Ich möchte hier nicht weiter auf diese Funktion eingehen, nur kurz erwähnen das mit dieser Funktion viele GDI-Objekte wie z.B Schriften  selektiert werden können. Da aber von jedem Objekttyp nur einer selektiert werden kann. Wird der alte herausgeschmissen und als Rückgabewert zurückgeliefert. Dies nennt man auch auswechseln von GDI-Objekte. Diese Objekte solltet ihr zwischenspeichern und nach dem Ihr euren GDI-Objekt, den ihr ja mit SelectObjekt übergeben habt, nicht mehr benötigt , wieder zurück geben. Ansonsten kann es zu unschönen Effekten kommen. Bei Bitmaps ist das unnötig, weil die GDI beim Anlegen von Speicherkontexten immer wieder dasselbe Standard-Bitmap (mit 1x1 Pixeln) verwendet und deshalb auch bei der Freigabe eines solchen Kontext nicht versucht, das momentan eingesetzte Bitmap zu löschen. 
Nach unser Arbeit mit dem speicherbasierten Gerätekontext, müssen wir ihn auch wieder freigeben. Dies macht für uns die Funktion
 
BOOL DeleteDC(  HDC hdc ); 
 
Damit haben wir alles eingestellt um endlich unser Bitmap zu zeichnen. Dafür gibt es zwei Funktionen
 
BOOL BitBlt(           HDC hdcDest,          
                              int nXDest,  
                              int nYDest,  
                              int nWidth,  
                              int nHeight, 
                              HDC hdcSrc,  
                              int nXSrc,   
                              int nYSrc,   
                              DWORD dwRop  
                              );
 
BOOL StretchBlt(    HDC hdcDest,      
                               int nXOriginDest, 
                               int nYOriginDest, 
                               int nWidthDest,   
                               int nHeightDest,  
                               HDC hdcSrc,       
                               int nXOriginSrc,  
                               int nYOriginSrc,  
                               int nWidthSrc,    
                               int nHeightSrc,   
                               DWORD dwRop       
                               );
 
Der einzige Unterschied zwischen diesen beiden Funktionen ist, das BitBlt eins zu eins das Bitmap kopiert und StretchBlt das Bitmap in der Größe verändert und zwar auf unsere angegebene Größe. Da beide Funktionen nahezu identisch sind, werde ich nur die Funktion StretchBlt erklären und diese auch im Quellcode verwenden. 
Die ersten fünf Parameter geben den Zielbereich an, d.h. hier wird angegeben in welches Geratekontext ( erster Parameter), wo auf dem Gerätekontext begonnen werden soll (Parameter zwei und drei) und wie groß das Bitmap sein soll. BitBlt ignoriert alles was großer, als das zu kopierende Bitmap, ist. Wenn längen und breiten Angaben kleiner als das Bitmap sind, wird das Bitmap an dieser Stelle abgeschnitten.
Die letzten fünf Parameter sind für die Quelle gedacht. Mit Parameter sechs wird das zu kopierende Kontext angegeben. Parameter 7 und 8 legen die linke obere Ecke des zu kopierenden Bereichs innerhalb des Quell-Kontext fest. Die Parameter 9 und 10 geben dann nur noch die Länge und Breite des zu kopierenden Bereichs an, diese zwei Parameter sind in BitBlt nicht vorhanden. Parameter 11 werden wir uns nur mit einem Flag beschäftigen „SRCCOPY“. Es steht für „source copy“ – also eine einfache Kopie des Quellgebiets.
 
So wir haben es geschafft, als nächstes zeige ich euch den gesamten Quellcodeblock für die Nachrichtenverarbeitung. Hier ist dann auch der Quellcode von WM_PAINT: 

 

switch(message)

{

case WM_CREATE:                             // Das Fenster wird erzeugt                              

 

                        // Instance ermitteln und in lokale Variable zischenspeichern

                        hInstance = ((LPCREATESTRUCT) lParam)->hInstance ;

 

                        // Bitmap aus Datei laden

                        hBitmap =  (HBITMAP) LoadImage ( hInstance, "textur.bmp", IMAGE_BITMAP, 0,0,LR_LOADFROMFILE);

 

                        // Informatione über das Bitmap ermitteln

GetObject (hBitmap, sizeof (BITMAP), &bitmap) ;

 

                        // Breite und Höhe des Bitmap’s in statische Variablen speichern

                        cxSource = bitmap.bmWidth ;

                        cySource = bitmap.bmHeight ;

 

                        // Fenster auf dem Bildschirm zentrieren

                        CenterWindow(hwnd);

 

                        // Fenster in den Vordergrund schalten.

                        SetForegroundWindow(hwnd);

 

                        return 0 ;

           

case WM_SIZE:                                   // Die Größe des Fenster hat sich verändert

 

                        // Höhe und Breite des Fenster ermitteln

                        cxClient = LOWORD (lParam);   // niederwertiges Wort von lParam

                        cyClient = HIWORD (lParam);    // höherwertiges Wort von lParam

 

return 0;

 

case WM_PAINT:                                 // Das Fenster wird gezeichnet

                        // Gerätekontext fürs zeichnen ermitteln alles für Zeichnen vorbereiten

                        hdc = BeginPaint (hwnd, &ps) ;

 

                        // speicherbasiertes Gerätekontext erstellen und Bitmap darin selektieren

                        hdcMem = CreateCompatibleDC (hdc) ;

                        SelectObject (hdcMem, hBitmap) ;

 

                        // Bitmap kopieren und in seiner Größe dem Fenster anpassen

                        StretchBlt (hdc,        0,  0, cxClient, cyClient,

                                        hdcMem, 0,  0, cxSource, cySource, SRCCOPY) ;

 

                        // Aufräumarbeiten durchführen

                        DeleteDC (hdcMem) ;

                        EndPaint (hwnd, &ps) ;

       

                        return 0 ;

                       

 

case WM_DESTROY:                           // Das Fenster wurde beendet

                       

                        // Bitmap-Handle löschen

                        DeleteObject(hBitmap);

 

                        PostQuitMessage(0);

 

                        return 0;

}

 

CenterWindow

 

Jetzt bin ich euch nur noch einer Funktion schuldig. Ich habe ja weiter oben darauf hingewiesen das es keine Funktion gibt, mit der man ein Fenster zentrieren kann. Das müssen wir nun selbst machen. Ich werde euch jetzt erst mal denn gesamten Code für CenterWindow zeigen und dann nur zu bestimmten Dingen etwas sagen, da ja der Code im Grunde selbsterklärend ist.

 

BOOL CenterWindow(   HWND hWnd,                           // Zu zentrieredes Fenster

HWND hWndParent = NULL)     // Elternfenster, bei NULL ist es der Desktop

{

            RECT rectScreen;

 

            if(hWndParent == NULL)

            {

                        // Momentane Desktopgröße ermitteln

                        MONITORINFO mi;

                        mi.cbSize = sizeof(MONITORINFO);

 

                        GetMonitorInfo( MonitorFromWindow( hWnd, MONITOR_DEFAULTTOPRIMARY ) , &mi );

 

                        // Koordinaten des Arbeitsbereiches speichern.

                        rectScreen = mi.rcWork;

            }

            else

            {

                        // Fenstergröße des Elternfenster ermitteln

                        GetWindowRect(hWndParent, &rectScreen);

            }

 

            RECT rectWindow;

 

            // Fenstergröße des zu zentrierenden Fenster ermitteln

            GetWindowRect(hWnd, &rectWindow);

 

            // Fensterposition berechnen, damit es zentriert ist. Dabei aufpassen, den der User kann einen Virtuellen Arbeitsplatz

// haben. D.H. der Arbeitsplatz ist Größer als der derzeit dargestellte Bildschirm.

            int xLeft = (rectScreen.left + rectScreen.right) / 2 - (rectWindow.right- rectWindow.left)/2;

            int yTop  = (rectScreen.top  + rectScreen.bottom ) / 2 - (rectWindow.bottom- rectWindow.top)/2;

 

            // Fensterposition setzen

            return SetWindowPos(hWnd, NULL, xLeft, yTop, -1, -1, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);

           

}

 

So, diese Funktion kann im Grunde für alles einsetzen, wo man irgendwas zentrieren möchte. Dabei spielt es keine Rolle ob wir unser Fenster auf dem Desktop zentrieren wollen oder nur zu einem anderen Fenster. Diese Funktion ist jedoch nur für Betriebssystem ab Win98 bzw. Win2000 geeignet. Das liegt daran, das ich einen Funktion verwendet habe, die zwar das Auslesen der Desktopgröße vereinfacht aber erst sehr spät dazugekommen ist. Diese Funktion heißt GetMonitorInfo. Wer sich mehr dafür Interessiert, wie man Anwendungen programmiert die auf mehreren Bildschirmen bzw. Virtuelle Desktop laufen, möchte ich auf die MSDN verweisen.

 

So, wir habe es geschafft. Was jetzt noch fehlt ist sind die besonderen Fensterformen, Transparenz usw.

 

 

Fortsetzung folgt!

 

Scania V8