Steuerelemente

Einführung

Nun soll das vorhandene Programm schließlich und endlich um ein paar Steuerelemente erweitert werden, die die Benutzerfreundlichkeit erhöhen soll. Dazu soll zunächst eine Schaltfläche ergänzt werden, die die zu zeichnende Figur ändert und ein Textfeld, welches den gegenwärtigen zu zeichnenden Typ anzeigt. Im Anschluss sollen diese beiden Steuerelemente durch ein "RadioButton"-Steuerelement ersetzt werden.

Die Schaltfläche

Zunächst aber zu der Schaltfläche: Wie bereits zuvor erwähnt handelt es sich bei einer Schaltfläche ebenfalls um ein Fenster, dessen zugehörige Fensterklasse bereits von Windows unter dem Namen 'button' registriert ist. Eine Erstellung könnte wie folgt aussehen:

hWndBtn = CreateWindow(TEXT("button"), TEXT("Change Figure"), 
			WS_CHILD | WS_VISIBLE, 
			0, 0, 0, 0, hWnd, NULL, NULL, NULL);

Hier wird eine Schaltfläche als "Child"-Fenster von 'hWnd' erstellt, die mit "Change Figure" beschriftet ist. Um die Schaltfläche komplett in unsere Anwendung zu integrieren ist in der Funktion 'MainWndProc' zunächst die entsprechende statische Variable 'hWndBtn' zu deklarieren. Bei der Behandlung der Nachricht 'WM_CREATE' kann der obige Aufruf komplett übernommen werden. Der vorletzte Parameter (die Instanz der Anwendung) ist bei vordefinierten Fensterklassen wie 'button' auf 'NULL' zu setzen.

Bei der Nachricht 'WM_DESTROY' ist auch hier dieses Fenster zu zerstören:

if (hWndBtn) 
{DestroyWindow(hWndBtn); hWndBtn = 0;}

Schließlich bleibt die Bearbeitung der Nachricht 'WM_SIZE'. Dort sollte man sich zunächst überlegen, wo die Schaltfläche zu platzieren ist, wie groß sie sein soll und was dies für das Fenster 'hWndDraw' bedeutet. Zunächst sollte man zwei Konstanten 'BTN_WIDTH' und 'BTN_HEIGHT' definieren um Breite und Höhe einmalig festzulegen. Ist dies erfolgt, dann ist die Behandlung der Nachricht 'WM_SIZE' wie folgt abzuändern:

	case WM_SIZE: //Fenstergröße wird geändert (unverändert)
		//neue Fensterdimensionen bestimmen
		lWndWidth = LOWORD(lParam); //Breite
		lWndHeight = HIWORD(lParam); //Höhe

		//Childfenster anzeigen
		MoveWindow(
			hWndDraw, 		//zu verschiebendes Fenster
			0, 			//X-Position
			0, 			//Y-Position
			lWndWidth - BTN_WIDTH, 	//Breite des Fensters
			lWndHeight, 		//Höhe des Fensters
			TRUE); 			//Fenster neu zeichnen

		MoveWindow(
			hWndBtn, 		//zu verschiebende Schaltfläche
			lWndWidth - BTN_WIDTH,	//X-Position
			0, 			//Y-Position
			BTN_WIDTH, 		//Breite des Steuerelements
			BTN_HEIGHT, 		//Höhe des Steuerelements
			TRUE); 			//Schaltfläche neu zeichnen

		return 0;

Egal was man nun für 'BTN_WIDTH' und 'BTN_HEIGHT' definiert, sollte nun das Fenster entsprechend aufgebaut sein.

Die Schaltfläche lässt sich bereits anklicken, allerdings passiert nichts (abgesehen von der Animation, die Windows komplett übernimmt). Irgendwie müsste man die Nachricht abfangen, wenn ein Benutzer die Schaltfläche auswählt. Mit der 'button'-Fensterklasse kann man sich viel Arbeit ersparen, wenn man bei der Erstellung der Schaltfläche ein paar Änderungen vornimmt. Zunächst sollte man für jede Schaltfläche eine eindeutige ID festlegen. Dazu definieren wir erneut eine Konstante 'IDC_BTNCF' und geben ihr den Wert '1'. Diese Konstante übergeben wir der Funktion 'CreateWindow' als drittletztes Argument. Normalerweise ist dieses Argument für Fenstermenüs vorgesehen, die aber bei Steuerelementen wie Schaltflächen nur wenig Sinn machen. Dennoch muss man an dieser Stelle ein Typecasting durchführen. Zusätzlich wird der Fensterstil um die Konstante 'BS_PUSHBUTTON' erweitert. Diese Konstante bewirkt nämlich, dass das "Parent"-Fenster eine Nachricht vom Typ 'WM_COMMAND' inkl. der angegebenen ID zugeschickt bekommt. Der komplette Aufruf der Funktion 'CreateWindow' sieht dann in Gänze wie folgt aus:

//Schaltfläche erstellen
hWndBtn = CreateWindow(TEXT("button"), TEXT("Change Figure"), 
		WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 
		0, 0, 0, 0, hWnd, (HMENU)IDC_BTNCF, NULL, NULL);

Nun bekommt das Hauptfenster jedes Mal, wenn die Schaltfläche angeklickt wird eine Nachricht 'WM_COMMAND' zugeschickt, wobei die im Parameter 'wParam' (genauer: 'LOWORD(wParam)') die ID mitgeschickt wird. Es muss also lediglich dieses Ereignis abgefangen werden. Die Behandlung der Nachricht könnte vorerst wie folgt aussehen:

	case WM_COMMAND: 	//Kommando vom Benutzer
		if (LOWORD(wParam) == IDC_BTNCF) //Schaltfläche
		{
			//Drücken der rechten Maustaste vorgeben
			SendMessage(hWndDraw, WM_RBUTTONDOWN, 0, 0);
		}
		return 0;

Zugegeben ist das nicht ganz die ideale Lösung. Sobald man die Schaltfläche anklickt, wird dem Fenster 'hWndDraw' vorgegeben, dass jemand die rechte Maustaste gedrückt hat (ohne dass sie jemand danach je wieder loslässt). Man bedient sich hier also der Tatsache, dass die Funktionalität bereits vorhanden ist. Wir werden uns gleich allerdings einer eleganteren Möglichkeit bedienen. Dennoch sei hier schon einmal aufgezeigt, wie man selber mit 'SendMessage' Fensternachrichten verschickt.

Auf jeden Fall hat der Benutzer nun die Möglichkeit die Figur mit Hilfe der Schaltfläche zu ändern und das Programm sieht in etwa so aus:

Die eben angesprochene unelegante Variante, die Figur zu ändern kann man auf vielerlei Weisen beheben. Die aus meiner Sicht einfachste (abgesehen von der Verwendung einer globalen Variablen) ist die Definition einer eigenen Fensternachricht, die vom Hauptfenster an das "Child"-Fenster gesendet wird und eine Änderung der Figur veranlassen soll. Zu diesem Zweck ist in der WIN32-API die Konstante 'WM_USER' definiert worden. Man kann diese als Grundlage nehmen eigene Fensternachrichten zu definieren ohne befürchten zu müssen unerwartete Reaktionen zu veranlassen. Wir definieren also eine eigene Nachricht 'WM_CHANGEFIGURE' in der Form:

#define WM_CHANGEFIGURE	(WM_USER + 1)

Diese Nachricht wird dann anstatt der 'WM_RBUTTONDOWN'-Nachricht in der Behandlung der 'WM_COMMAND'-Nachricht in der Methode 'MainWndProc' versendet:

	case WM_COMMAND: 	//Kommando vom Benutzer
		if (LOWORD(wParam) == IDC_BTNCF) //Schaltfläche
		{
			//Drücken der rechten Maustaste vorgeben
			SendMessage(hWndDraw, WM_CHANGEFIGURE, 0, 0);
		}
		return 0;

Nun muss natürlich auch diese neue Nachricht behandelt in der Methode 'DrawWndProc' werden. Da das Ändern der Figur mit dem Drücken der rechten Maustaste nur eine Notlösung war, ersetzen wir die Behandlung der 'WM_RBUTTONDOWN'-Nachricht durch die Behandlung der 'WM_CHANGEFIGURE'-Nachricht. Nun lässt sich die Figur nur noch durch das Drücken der Schaltfläche ändern.

Das Textfeld

Nun soll natürlich der Benutzer auch wissen, was er gegenwärtig für eine Figur ausgewählt hat. Darum soll er in einem Textfeld diesbezüglich informiert werden. Das Textfeld ist wiederum ein Fenster der vordefinierten Fensterklasse "static". Angenommen man hat eine statische Variable 'hWndTxtFig' in der Fensterprozedur 'MainWndProc' deklariert, dann ließe sich die Erstellung des Fensters bei der Behandlung der 'WM_CREATE'-Nachricht wie folgt realisieren:

	//Textfeld erstellen
	hWndTxtFig = CreateWindow(TEXT("static"), TEXT("Rectangle"), 
				WS_CHILD | WS_VISIBLE , 
				0, 0, 0, 0, hWnd, NULL, NULL, NULL);

Dieses Textfeld ist vorerst mit "Rectangle" beschriftet. Nun muss man sich noch um die Zerstörung des Fensters bei der Behandlung der 'WM_DESTROY'-Nachricht und um die Verschiebung des Fensters in der 'WM_SIZE'-Nachricht kümmern.

Bei 'WM_SIZE':

	MoveWindow(
		hWndTxtFig, 		//zu verschiebendes Textfeld
		lWndWidth - BTN_WIDTH,	//X-Position
		BTN_HEIGHT, 		//Y-Position
		BTN_WIDTH, 		//Breite des Textfeld
		BTN_HEIGHT, 		//Höhe des Textfeld
		TRUE); 			//Textfeld neu zeichnen

Bei 'WM_DESTROY':

	if (hWndTxtFig) 
	{DestroyWindow(hWndTxtFig); hWndTxtFig = 0;}

Eine Gegebenheit erweist sich als durchaus störend. Wenn der Benutzer auf die Schaltfläche klickt, dann erhält die Prozedur 'MainWndProc' durch die Behandlung der Nachricht 'WM_COMMAND' Kenntnis darüber, dass er eine Änderung wünscht nicht aber, wie diese aussieht. Das ist insofern ungünstig da ja nun die aktuelle Figur im Textfeld angezeigt werden soll, auf das eben nur die Prozedur 'MainWndProc' Zugriff hat. Aus diesem Grunde sollte künftig 'MainWndProc' die aktuelle Figur verwalten und 'DrawWndProc' nicht nur mitteilen, dass die Figur geändert wird sondern auch, welches die neue Figur ist.

Dazu sollte eine neue statische Variable 'lCurrFig' des Datentyps 'static long' innerhalb der Prozedur 'MainWndProc' deklariert werden, die den aktuellen Status speichert. Dafür werden zwei Konstanten definiert:  'CF_ELLIPSE' (Wert 0) und 'CF_RECTANGLE' (Wert 1). Es bietet sich an den Wert bei der Behandlung der Nachricht 'WM_CREATE' gleich entsprechend mit 'CF_RECTANGLE' kurz vor der Erstellung des Textfeldes zu initialisieren, da so eine Übereinstimmung von Textfeld und Variablen sichergestellt ist.

Der Wert der Variablen 'lCurrFig' wird künftig bei jeder Änderung mit der Nachricht 'WM_CHANGEFIGURE' mitgeschickt. Mitsamt der Änderung des Textfeldes sähe die Behandlung der 'WM_COMMAND'-Nachricht nun wie folgt aus:

	case WM_COMMAND: 	//Kommando vom Benutzer
		if (LOWORD(wParam) == IDC_BTNCF) //Schaltfläche
		{
			if (lCurrentFig == CF_RECTANGLE)
			{
				lCurrFig = CF_ELLIPSE;
				SetWindowText(hWndTxtFig, TEXT("Ellipse"));
			}
			else 
			{
				lCurrFig = CF_RECTANGLE;
				SetWindowText(hWndTxtFig, TEXT("Rectangle"));
			}

			//Figur wurde geändert neue Figur ist in 'lCurrFig' gespeichert
			SendMessage(hWndDraw, WM_CHANGEFIGURE, lCurrFig, 0);
		}
		return 0;

Nun ist natürlich auch die Behandlung der Nachricht 'WM_CHANGEFIGURE' in der Prozedur  'DrawWndProc' anzupassen:

	case WM_CHANGEFIGURE:
		//Status ändern
		if (wParam == CF_ELLIPSE) bEllipse = true;
		else bEllipse = false;

		return 0;

Bei einer Ausführung des Programms kommt es zu einer kleinen Überraschung, denn obwohl die Variable 'lCurrFig' den Wert 'CF_RECTANGLE' enthält und entsprechendes auch im Textfeld angezeigt ist, zeichnet das Programm zunächst Ellipsen. Sobald man einmal die Schaltfläche drückt ist dieses Problem behoben. Der Grund für diese Eigenart ist die Tatsache, dass wir die Variable 'bEllipse' in der Prozedur 'DrawWndProc' mit 'true', die Variable 'lCurrFig' in der Prozedur 'MainWndProc' mit 'CF_RECTANGLE' initialisiert haben. Um beides zu "synchronisieren" sollte man nach der Erstellung des "Zeichenfensters" eben diesem mitteilen, welches der aktuelle Status ist. Dazu wird einfach wieder die Nachricht 'WM_CHANGEFIGURE' versendet. Aufgrund der zahlreichen Änderungen hier noch einmal die entscheidenden Zeilen aus der Behandlung der 'WM_CREATE'-Nachricht:

		//Fenster erstellen
		hWndDraw = CreateWindow(wc.lpszClassName, 0, WS_CHILD | WS_VISIBLE, 
		0, 0, 0, 0, hWnd, NULL, wc.hInstance, NULL);

		//Schaltfläche erstellen
		hWndBtn = CreateWindow(TEXT("button"), TEXT("Change Figure"), 
					WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 
					0, 0, 0, 0, hWnd, (HMENU)IDC_BTNCF, NULL, NULL);

		//Textfeld erstellen
		lCurrFig = CF_RECTANGLE;
		hWndTxtFig = CreateWindow(TEXT("static"), TEXT("Rectangle"), 
						WS_CHILD | WS_VISIBLE , 
						0, 0, 0, 0, hWnd, NULL, NULL, NULL);

		//Werte anpassen
		SendMessage(hWndDraw, WM_CHANGEFIGURE, lCurrFig, 0);

		return 0;

Zugegeben, besonders schön sehen die Steuerelemente nicht aus. Sie sind an den Rand gequetscht, die Schrift ist nicht zentriert, aber sie sind funktional. Hier sei aber gesagt, dass durch die Änderung der Fensterstile beim Aufruf der Funktion 'CreateWindow' einige Verbesserungen möglich sind. Entsprechendes lässt sich aus der MSDN entnehmen.

Hier der Quelltext dieses Programms: Winprog_v1_3a_src.exe (Quelltext, gepackt)

Das "RadioButton"-Steuerelement

Mir ist leider keine deutsche Übersetzung geläufig, aber sie werden schnell erkennen worum es sich handelt. Mit eben einem solchen Steuerelement lassen sich leicht Optionen auswählen. Das Gute: Es sind sämtliche Optionen auf der Stelle erkennbar. Das wiederum schränkt natürlich auch die Anzahl der möglichen Optionen ein, da wir aber nur zwei Optionen haben (Rechteck oder Ellipse) ist dieses Steuerelement ideal. Einmal implementiert kann es auch leicht erweitert werden.

Zunächst legen wir einen zweielementigen Array vom Datentyp 'HWND' an und nennen diesen 'hWndFigs', der im Prinzip jedes anwählbare Element (Rechteck oder Ellipse) repräsentiert. (Man hätte auch zwei gesonderte Variablen anlegen können, aber auf diese Weise lässt sich eine Erweiterung leicht durchführen.)

Da wir die alten Steuerelemente gänzlich ersetzen wollen, kann man die Deklaration der Variablen 'hWndBtn' und 'hWndTxtFig', deren Erstellung in dem 'WM_CREATE'-Block, deren Zerstörung im 'WM_DESTROY'-Block, deren Verschiebung im 'WM_SIZE'-Block sowie die Bearbeitung der 'WM_COMMAND'-Nachricht gänzlich löschen.

Im 'WM_CREATE'-Block legen wir zwei neue Schaltflächen an, die im Bezug auf den Fensterstil eine Besonderheit aufweisen, sich sonst aber von der bekannten Schaltfläche bzgl. der Erstellung kaum unterscheiden:

	//RadioButton erstellen
	hWndFigs[0] = CreateWindow(TEXT("button"), TEXT("Ellipse"), 
		WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON, 
		0, 0, 0, 0, hWnd, (HMENU)CF_ELLIPSE, NULL, NULL);

	hWndFigs[1] = CreateWindow(TEXT("button"), TEXT("Rectangle"), 
		WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON, 
		0, 0, 0, 0, hWnd, (HMENU)CF_RECTANGLE, NULL, NULL);

Deren Zerstörung im 'WM_DESTROY'-Block ist auch nichts besonderes:

	if (hWndFigs[0]) 
	{DestroyWindow(hWndFigs[0]); hWndFigs[0] = 0;}

	if (hWndFigs[1]) 
	{DestroyWindow(hWndFigs[1]); hWndFigs[1] = 0;}

oder besser (Die Variable 'iFig' vom Datentyp 'int' wird zuvor deklariert):

	for (iFig = CF_ELLIPSE; iFig <= CF_RECTANGLE; iFig++)
	{
		if (hWndFigs[iFig]) 
		{DestroyWindow(hWndFigs[iFig]); hWndFigs[iFig] = 0;}
	}

Besser ist diese Variante deswegen, da man so leichter eine Erweiterung durchführen kann, ohne viel ergänzen oder ändern zu müssen.

Wenn man nun den 'WM_SIZE'-Block noch anpasst ist man fast fertig:

	MoveWindow(
		hWndFigs[0], 		//zu verschiebende Schaltfläche
		lWndWidth - BTN_WIDTH,	//X-Position
		0, 			//Y-Position
		BTN_WIDTH, 		//Breite der Schaltfläche
		BTN_HEIGHT, 		//Höhe der Schaltfläche
		TRUE); 			//Schaltfläche neu zeichnen

	MoveWindow(
		hWndFigs[1], 		//zu verschiebende Schaltfläche
		lWndWidth - BTN_WIDTH, 	//X-Position
		BTN_HEIGHT, 		//Y-Position
		BTN_WIDTH, 		//Breite der Schaltfläche
		BTN_HEIGHT, 		//Höhe der Schaltfläche
		TRUE); 			//Schaltfläche neu zeichnen

oder auch hier:

	for (iFig = CF_ELLIPSE; iFig <= CF_RECTANGLE; iFig++)
	{
		MoveWindow(
			hWndFigs[iFig], 	//zu verschiebende Schaltfläche
			lWndWidth - BTN_WIDTH,	//X-Position
			iFig * BTN_HEIGHT, 	//Y-Position
			BTN_WIDTH, 		//Breite der Schaltfläche
			BTN_HEIGHT, 		//Höhe der Schaltfläche
			TRUE); 			//Schaltfläche neu zeichnen

	}

Natürlich funktioniert die Schleifenvariante so gut, weil die erste Konstante 'CF_ELLIPSE' '0' ist, aber auch wenn nicht müsste man lediglich jeweils den Wert von 'CF_ELLIPSE' von 'iFig' abziehen. Spätestens, wenn Figuren hinzukommen sollen, wird man diese Variante zu schätzen wissen.

Führt man das Programm nun aus hat man in der rechten oberen Ecke zwei Radiobuttons, von denen sich jeweils nur eines auswählen lässt. Allerdings ist zu Programmstart noch keines ausgewählt. Da die IDs der Steuerelemente mit den Konstanten identisch sind, die in der Variablen 'lCurrFig' gespeichert sind lässt sich die Vorauswahl (nach Initialisierung der Variablen) wie folgt durchführen:

	//Initialisierung der Form
	lCurrFig = CF_RECTANGLE;
	CheckRadioButton(hWnd, CF_ELLIPSE, CF_RECTANGLE, lCurrFig);

Diese funktioniert wie folgt: Sie stellt sicher, dass alle Schaltflächen (des "Parent"-Fensters 'hWnd') deren IDs zwischen dem zweiten Argument (hier: 'CF_ELLIPSE') und dem dritten Argument (hier: 'CF_RECTANGLE') deaktiviert werden und nur die Schaltfläche mit der ID des vierten Arguments (hier: 'lCurrFig') aktiviert wird.

Der Aufruf der Nachricht 'WM_CHANGEFIGURE' hat natürlich weiterhin Bestand

Nun brauch man sich nur noch um die Behandlung der Nachricht 'WM_COMMAND' kümmern. Da mit jedem Klick auf eines der RadioButtons per 'WM_COMMAND'-Nachricht die ID mitgeschickt wird, die gleichzeitig einer der Konstanten entspricht, die angeben welche Figur gezeichnet werden soll ('CF_ELLIPSE' oder 'CF_RECTANGLE') stellt sich die Behandlung als äußerst einfach heraus:

	case WM_COMMAND: //Kommando vom Benutzer

		//Nachricht kommt von einem der RadioButtons
		if (LOWORD(wParam) >= CF_ELLIPSE && 
		LOWORD(wParam) <= CF_RECTANGLE)
		{
			//Konstante entspricht der ID des Steuerelements
			lCurrFig = LOWORD(wParam);

			//Figur wurde geändert neue Figur ist in 'lCurrFig' gespeichert
			SendMessage(hWndDraw, WM_CHANGEFIGURE, lCurrFig, 0);
		}
		return 0;

Aufgrund des angegebnen Fensterstils 'BS_AUTORADIOBUTTON' muss man sich nicht darum kümmern, dass der angeklickte RadioButton auch ausgewählt wird. Das macht Windows hier automatisch. Wenn Sie darauf bestehen es doch selber zu machen, dann verwenden Sie stattdessen 'BS_RADIOBUTTON'.

Hier der Quelltext dieses Programms: WinProg_v1_3b_src.exe (Quelltext, gepackt)

Zusammenfassung

Hier sollte die Verwendung von den ersten Standardsteuerelementen bekannt werden. Nebenbei sind Inhalte der Windowsprogrammierung, wie das Definieren von eigenen Fensternachrichten aufgetaucht. Auch die 'WM_COMMAND'-Nachricht ist hier erstmalig behandelt worden.

Die nächsten Lektionen

In der nächsten Lektion wird noch ein weiteres Steuerelement vorgestellt, welches ein wenig komplexer ist.

Zurück Nach oben