Vensters

Pengo © 2002-2003, Joost Ronkes Agerbeek

Toen Microsoft in 1995 het besturingssysteem Windows 95 uitbracht, riepen alle spelprogrammeurs: 'Windows sucks! Lang leve DOS.' Maar de tijden veranderen. DOS staat nu voor Dead Operating System en één van de eerste vragen die een beginnende programmeur zich stelt is: hoe krijg ik een venster op het scherm? Die vraag zal ik in deze les beantwoorden.

Windows-applicaties

'Hoe moeilijk kan het nou zijn om een venster op het scherm te zetten?' hoor ik je al vragen. Tenzij je een gedegen kennis hebt van pointers, callback functies, event messages en type casting is het antwoord: lastiger dan je denkt. Ik zal het je niet aandoen om al deze begrippen te verklaren. Zolang je ongeveer weet wat de code doet, heb je genoeg kennis om Pengo om te zetten naar Windows.

[ Naar boven | Terug naar Pengo ]

Nieuw project

Pengo voor Windows is een geheel nieuw project, dat gebruik maakt van de Pengo-versie die we al hebben. Mocht het nodig zijn om onderscheid te maken dan zal ik spreken van (ik bedoel: schrijven over) WinPengo respectievelijk Console Pengo.

Allereerst moeten we dus een nieuw project aanmaken. Dit keer maken we geen Console Application, maar een Windows Application. Zorg ervoor dat je je project in een nieuwe map opslaat. Hieronder staan de stappen die je moet nemen om het juiste project aan te maken. Voer alleen de stappen uit voor de IDE die jij gebruikt. (Jazeker, er zijn mensen die dat fout zouden doen. Nee, daar bedoel ik jou natuurlijk niet mee. :-P)

Visual C++ 6.0

  1. Klik op File > New....
  2. Selecteer het tabblad Projects.
  3. Klik op Win32 Application.
  4. Vul bij Project name in: WinPengo.
  5. Geef bij Location de map op waarin je je project wilt opslaan.

  6. Klik op OK.
  7. Selecteer An empty project
  8. Klik op Finish.
[ Naar boven | Terug naar Pengo ]

Main

Alle C++-programma's beginnen met de functie main... behalve Windows-applicaties. :-s In een Windows-applicatie vervangen we main door de functie WinMain. Bovendien krijgt die functie een reeks exotische parameters mee. Maak een bestand aan met de naam Main.cpp; daar zetten we WinMain in. Daar gaan we.

Main.cpp
/******************************************************************************
	Bestand:   Main.cpp
	Project:   WinPengo

	Copyright: (c) 2003 Joost Ronkes Agerbeek

	Auteur:    Joost Ronkes Agerbeek <joost@ronkes.nl>
	Datum:     30 maart 2003
******************************************************************************/

#include <windows.h>

/**
 * Het programma begint hier.
 *
 * @param	instance        	identificeert de huidige instantie van dit
 *       	                	programma
 * @param	previousInstance	wordt niet meer gebruikt
 * @param	commandLine     	de command line die dit programma startte
 * @param	show            	geeft aan hoe het programmavenster getoond moet
 *       	                	worden
 *
 * @return	altijd 0
 */
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previousInstance, LPSTR
				   commandLine, int show)
{
	return 0;
}

Oké, een paar opmerkingen.

Als je een Windows-applicatie programmeert, heb je altijd het headerbestand windows.h nodig, vandaar de include. Merk op dat je using namespace std; niet nodig hebt.

Je ziet dat de Win32 API allerlei datatypes definieert zoals HINSTANCE en LPSTR (en WINAPI, maar dat is strikt genomen geen datatype). Deze mag je negeren. Als het belangrijk is om te weten wat een datatype precies is, dan meld ik het wel. Aardig, hè? :-)

Van de vier parameters die WinMain meekrijgt, zullen we alleen de eerste nog een paar keer terugzien. Deze parameter wordt namelijk door Windows gebruikt om bij te houden over welk programma we het hebben.

Het kan zijn dat je IDE automatisch laat zien wat het prototype van WinMain is als je bezig bent met typen (je hebt het toch zeker wel handmatig overgetypt?). Het prototype gebruikt andere namen voor de parameters, bijvoorbeeld hPrevInstance in plaats van previousInstance en nCmdShow in plaats van show. Ik heb de namen aangepast aan onze gebruikelijke manier van naamgeving.

Deze code doet niets. Je kunt 'm compileren en linken en dat levert geen fouten op. (Bij jou wel. Dan heb je het nu al fout gedaan. Dat belooft niet veel goeds. :-P) Dit is dus de code die we nodig hebben om niets te laten doen. Probeer je eens voor te stellen hoeveel code je nodig hebt om een venster op het scherm te zetten. ;-)

[ Naar boven | Terug naar Pengo ]

Venster maken

Tijd om een venster aan te maken. Als je denkt dat je dit kunt doen door simpelweg een functie aan te roepen, dan heb je het mis. Je moet namelijk twee functies aanroepen.

[ Naar boven | Terug naar Pengo ]

Window class

Stel je bent koekjes aan het bakken (wat krijgen we nu? waarom heeft hij het nou ineens over koekjes bakken?) en je wilt ze allemaal maken in de vorm van een ster. Je kunt ze dan allemaal apart in de juiste vorm snijden, maar daar krijg je al snel genoeg van. Het is makkelijker als je een stervorm koopt waarmee je je koekjes in de juiste vorm kunt snijden.

Een stervormige koekjesvorm.

Datzelfde principe kun je toepassen op vensters. (Stervormige venster... mmm.) Als je meerdere vensters maakt die allemaal op elkaar lijken, dan is het handig om een soort mal te hebben voor die vensters. Dit noemen we een window class - een vensterklasse, zo je wilt. Windows heeft een window class nodig waarop hij het venster voor onze applicatie kan baseren. De eigenschappen van zo'n window class staan in een structure die WNDCLASS heet. De structure is al gedefinieerd, wij hoeven 'm alleen maar in te vullen.

Main.cpp
// maak window class
WNDCLASS myWindowClass;

Bovenstaande code maakt een variabele aan waarin we de gegevens van de window class kunnen opslaan. Dit zijn gegevens als het pictogram van het venster, de gebruikte muiscursor, de achtergrondkleur. Ik zal ze stuk voor stuk doornemen.

We zetten de achtergrondkleur op zwart. Daarvoor moeten we de functie GetStockObject aanroepen. De parameter BLACK_BRUSH geeft aan dat we een zwarte achtergrond willen hebben. Als we een witte achtergrond willen, dan geven we de waarde WHITE_BRUSH mee.

Main.cpp
myWindowClass.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);

Als we met de muis over het venster heen bewegen, moet de cursor veranderen in een pijl. We vragen de cursor op die we willen hebben met de functie LoadCursor. De parameter IDC_ARROW geeft aan dat we een pijl willen hebben.

Main.cpp
myWindowClass.hCursor = LoadCursor(NULL, IDC_ARROW);

Bij elk venster hoort een pictogram We zetten dit op het standaardpictogram voor applicaties.

Main.cpp
myWindowClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);

We moeten de parameter instance doorgeven aan de window class, zodat Windows weet bij welke applicatie het venster hoort.

Main.cpp
myWindowClass.hInstance = instance;

Iets verderop in deze les schrijven we een functie die allerlei berichten afhandelt voor ons venster. Zonder nu precies uit te leggen waar dat voor nodig is, kan ik je wel vertellen dat je in de window class aan moet geven hoe deze functie heet. Wij noemen hem WindowProc. Later meer hierover.

Main.cpp
myWindowClass.lpfnWndProc = WindowProc;

Een venster kan een menubalk hebben, maar ons venster heeft dat niet. Om dat aan te geven zetten we lpszMenuName op 0.

myWindowClass.lpszMenuName = 0;

De window class moet een naam hebben, zodat we hem straks makkelijk kunnen gebruiken. Omdat wij maar één window class hebben, maakt het niet zoveel uit welke naam we kiezen. Ik noem de window class hier StandardWindowClass.

Main.cpp
myWindowClass.lpszClassName = "StandardWindowClass";

Vensters hebben een bepaalde stijl. De stijl bepaald hoe een venster wordt weergegeven. Een aantal van die stijlattributen moeten we opgeven in de window class. We kunnen meerdere stijlattributen tegelijk opgeven door ze samen te voegen met het |-teken (de bitwise OR-operator). Vraag maar niet hoe dat werkt, want dan maak ik er meteen een toetsvraag van. :-) Door de stijlattributen CS_HREDRAW en CS_VREDRAW mee te geven, vertellen we dat we willen dat de inhoud van het venster opnieuw getekend wordt als de gebruiker het venster in horizontale of verticale richting verplaatst.

Main.cpp
myWindowClass.style = CS_VREDRAW | CS_HREDRAW;

Tot slot kunnen we nog extra gegevens opslaan in de window class. Wij gebruiken deze gegevens niet en zetten ze dus op 0.

Main.cpp
myWindowClass.cbClsExtra = 0;
myWindowClass.cbWndExtra = 0;

Nu we alle gegevens van de window class hebben opgegeven, moeten we de window class registreren bij Windows. De stap is nodig omdat Windows moet weten over welke window class we het hebben als we straks een venster gaan maken. Een window class registreren doe je met de functie RegisterClass.

Main.cpp
// registreer window class
RegisterClass(&myWindowClass);

De ampersand voor myWindowClass is geen typefout, maar heeft iets te maken met pointers en geheugenadressen. De meeste mensen hebben er minstens een jaar voor nodig om pointers te snappen en aangezien de eerstvolgende toets over een maand of twee is, zullen we daar maar niet meer aan beginnen. Daarentegen is het niet zo moeilijk om van pointers gebruik te maken: gewoon knippen en plakken. ;-)

Op dit moment wil de code nog niet compileren, omdat we verwijzen naar de functie WndProc, terwijl die nog niet bestaat. Om ervoor te zorgen dat de compiler geen fouten meer geeft, zullen we de functie alvast aanmaken, maar we laten hem vooralsnog leeg. Straks zullen we zien waar de functie toe dient en welke code er in moet staan. Dan zal ik 'm ook van commentaar voorzien. Let op, deze functie moet boven WinMain staan.

Main.cpp
LRESULT CALLBACK WindowProc(HWND windowHandle, UINT message, WPARAM wParam,
							LPARAM lParam)
{
	return DefWindowProc(windowHandle, message, wParam, lParam);
}

Je kunt de code nu compileren. Starten levert nog steeds geen zichtbaar resultaat op.

[ Naar boven | Terug naar Pengo ]

Venster

Nu we een window class hebben, kunnen we een venster maken dat daarop gebaseerd is. Dit doen we door de functie CreateWindow aan te roepen. Deze functie verwacht nogal wat parameters. Ik zal de parameters omschrijven met kort commentaar in de code. Daarna zal ik ze stuk voor stuk uitleggen.

De functie CreateWindow geeft ook een waarde terug. Deze waarde is een zogenaamde window handle. Hiermee kunnen we Windows straks vertellen over welk venster we het hebben. We slaan de window handle dus op in een variabele.

Main.cpp
// maak venster
HWND myWindowHandle = CreateWindow(
	"StandardWindowClass",       	// naam van de window class
	"Pengo",						// titel van het venster
	WS_OVERLAPPED | WS_VISIBLE | WS_SYSMENU,	// vensterstijl
	CW_USEDEFAULT, CW_USEDEFAULT,	// positie van het venster
	CW_USEDEFAULT, CW_USEDEFAULT,	// grootte van het venster
	0, 0,                        	// deze parameters gebruiken wij niet
	instance,                    	// de instantie van de applicatie
	0);                          	// deze parameter gebruiken wij niet

De eerste parameter vertelt wat de naam is van de window class die we willen gebruiken.

De tweede parameter is de titel van het venster. Deze titel komt in de titelbalk te staan.

De derde parameter bepaalt de stijl van het venster. WS_OVERLAPPED geeft aan dat we een venster willen met een rand eromheen en met een titelbalk. WS_VISIBLE zorgt ervoor dat ons venster zichtbaar is zodra het wordt aangemaakt. WS_SYSMENU voorziet ons venster van een systeemmenu (linksboven) en een sluitknop (rechtsboven).

De volgende vier parameters bpalen de positie en de grootte van het venster. We gebruiken hiervoor de standaardwaarden van Windows en geven daarom CW_USEDEFAULT mee.

De twee parameters daarna gebruiken we niet. De eerste is de zogenaamde parent window. Die heb je nodig als je vensters in vensters wilt creëren. De tweede is de naam van de menubalk, maar we hebben geen menubalk.

De tiende parameter vertelt weer aan Windows bij welke applicatie dit venster hoort.

De laatste parameter kun je gebruiken om extra informatie bij het venster op te slaan, maar daar maken wij geen gebruik van.

Als je dit programma compileert en start, zie je een venster op het scherm verschijnen en daarna meteen weer sluiten. Je bent misschien geneigd om system("pause") of cin.get() te gebruiken, maar dat werkt nu niet. Geeft niet, we lossen het wel op.

[ Naar boven | Terug naar Pengo ]

Berichten

Elk venster in Windows kan reageren op bepaalde gebeurtenissen; events in het Engels en ik zal in het vervolg ook spreken over een event in plaats van over een gebeurtenis. Zo'n event kan bijvoorbeeld zijn dat de gebruiker het venster verplaatst, dat hij op een toets drukt of dat hij het venster sluit. Als er een event optreedt, stuurt Windows een bericht, oftwel message, naar het venster. Het is onze taak als programmeur om de berichten te onderscheppen en erop te reageren.

[ Naar boven | Terug naar Pengo ]

Event loop

Nadat we ons venster hebben gemaakt, moeten we controleren of er berichten zijn voor ons venster. We maken hierbij gebruik van de functie PeekMessage. Controleren op messages moeten we niet één keer doen, maar telkens weer, zolang als ons programma draait. Daarom zetten we alles in een lus. Dit noemen we de event loop.

Main.cpp
// event loop
MSG msg;

do
{
	// is er een bericht naar het venster gestuurd?
	if (PeekMessage(&msg, myWindowHandle, 0, 0, PM_REMOVE))
	{
	}
} while (true);

PeekMessage accepteert vijf parameters. De eerste is een variabele van het type MSG. Als er een bericht beschikbaar is, dan slaat Windows hier de informatie over dat bericht op. In de tweede parameter geven we aan van welk venster we de berichten willen ontvangen. Met de volgende twee parameters kunnen we bepalen in welke berichten we geïnteresseerd zijn. Door twee keer een 0 op te geven, krijgen we alle berichten binnen. Met de laatste parameter geven we aan wat Windows met het bericht moet doen. In dit geval halen we het bericht uit de buffer (vergelijkbaar met de manier waarop we omgaan met toetsen in de Console API).

Oké, we hebben het bericht en nu? Nu sturen we het bericht door naar de zogenaamde window procedure. Voordat we dat doen moeten we het bericht vertalen met TranslateMessage. Hiermee zorgen we ervoor dat toetsaanslagen op de juiste manier worden doorgegeven. Daarna sturen we het bericht naar de window procedure door DispatchMessage aan te roepen. De volledige event loop ziet er nu als volgt uit.

Main.cpp
// event loop
MSG msg;

do
{
	// is er een bericht naar het venster gestuurd?
	if (PeekMessage(&msg, myWindowHandle, 0, 0, PM_REMOVE))
	{
		// vertaal bericht
		TranslateMessage(&msg);

		// verstuur bericht naar window procedure
		DispatchMessage(&msg);
	}
} while (true);

Als je je programma nu start, blijft het venster keurig op je scherm staan. Mooi hè. :-) Oké, genoeg. Sluit het maar weer. Klik en... huh? O jee. Het venster wil niet sluiten! Wat nu?

Ctrl-Alt-Delete. Inderdaad.

Dit probleem lossen we straks wel op, maar op dit moment heb je natuurlijk een veel dringendere vraag (niet dan?): wat is een window procedure?

[ Naar boven | Terug naar Pengo ]

Window procedure

De window procedure is een functie die alle berichten van een venster afhandelt. Herinner je je nog de functie die we moesten maken om te zorgen dat de code kon compileren? WndProc? Nee? O. Nou eh, dat is in ieder geval de window procedure. (Onderstaande code heb je dus al.)

Main.cpp
LRESULT CALLBACK WindowProc(HWND windowHandle, UINT message, WPARAM wParam,
							LPARAM lParam)
{
	return DefWindowProc(windowHandle, message, wParam, lParam);
}

De functie DispatchMessage zorgt er dus voor dat WndProc wordt aangeroepen met de juiste parameters. Op dit moment doet onze WndProc niets anders dan DefWndProc aanroepen met exact dezelfde parameters als dat WndProc meekrijgt. DefWndProc zorgt voor alle berichten die wij niet zelf afhandelen. Op dit moment zijn dat dus alles berichten.

WndProc krijgt vier parameters mee. De eerste parameter is de window handle van het venster dat het bericht ontvangt. Wij hebben maar één venster, dus windowHandle is altijd hetzelfde. De tweede parameter identificeert het bericht dat we hebben ontvangen. De laatste twee parameters geven meer informatie over het bericht, indien nodig.

Alle berichten hebben een bepaalde code gekregen. Bijvoorbeeld, als de gebruiker het venster verplaatst, is message gelijk aan WM_MOVE en als de gebruiker de muis beweegt in het venster, is message gelijk aan WM_MOUSEMOVE. Wij zijn op dit moment geïnteresseerd in het bericht dat aangeeft dat de gebruiker het venster wil sluiten: WM_CLOSE.

We kunnen controleren welk bericht er wordt doorgestuurd met behulp van een switch. Dat gaat als volgt.

Main.cpp
/**
 * Verwerkt de berichten die naar een venster gestuurd worden.
 *
 * @param	windowHandle	identificeert het venster
 * @param	message			het bericht dat naar het venster gestuurd is
 * @param	wParam			parameter van het bericht
 * @param	lParam			parameter van het bericht
 *
 * @return	hangt van het bericht af
 */
LRESULT CALLBACK WindowProc(HWND windowHandle, UINT message, WPARAM wParam,
							LPARAM lParam)
{
	// welk bericht heeft het venster ontvangen?
	switch (message)
	{
	case WM_CLOSE:
		{
			// de gebruiker sluit het venster
			// beëindig het programma
			PostQuitMessage(0);

			return 0;
		}
	}

	return DefWindowProc(windowHandle, message, wParam, lParam);
}
[ Naar boven | Terug naar Pengo ]

Afsluiten

Met de functie PostQuitMessage vertellen we Windows dat we de applicatie willen stoppen. Windows stuurt nu het bericht WM_QUIT naar ons venster. Echter, als je het programma start, kun je het venster nog steeds niet sluiten. Dat komt doordat we nog steeds in de event loop zitten. We moeten de event loop dus stoppen zodra ons venster WM_QUIT ontvangt. Kleine aanpassing.

} while (msg.message != WM_QUIT);

Nu heb je een venster dat je (houd je vast...) kunt sluiten. Dat was al de moeite toch zeker waard. :-D

[ Naar boven | Terug naar Pengo ]

Conclusie

Je hebt het overleefd! We hebben gezien hoe we een venster op het scherm kunnen zetten. Bovendien hebben we ervoor gezorgd dat je datzelfde venster ook weer van het scherm af krijgt.

Tussen neus en lippen door hebben we de volgende onderwerpen genegeeerd:

Er is dus nog genoeg te leren. Met een aantal onderwerpen heb ik jullie wel lastig gevallen, namelijk:

De volgende keer houden we ons bezig met bitmaps (plaatjes, dus). Dan zijn we eindelijk verlost van programmeren met ASCII-art en kunnen we gaan tekenen.

Programmeren is leuk. :-)

[ Naar boven | Terug naar Pengo ]

Downloads

De volgende bestanden horen bij deze les.


Valid XHTML 1.0! Correct CSS! Laatst bijgewerkt: dinsdag 15 april 2014