Splash-Screens
mit der Win32 selbst erstellen
von Marco Leithold
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.
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.
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.
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.
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
) );
}
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
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;
}
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