Implementierung der Maus

Einführung

Hier soll wie bereits erwähnt der Kern eines Programms entstehen, welches die Interaktion zwischen Benutzer und Programm über die Maus realisiert. Doch zuvor soll eine weitere Nachricht eingeführt werden, welche das  Gegenstück zu der 'WM_DESTROY'-Nachricht darstellt. Diese Nachricht, nämlich 'WM_CREATE', wird kurz nach der Fenstererstellung also zuallererst aufgerufen. Dort können statische Variablen initialisiert oder das Aussehen des Fensters angepasst werden. Bei der Behandlung dieser Nachricht sollen nämlich die Stifte und Pinsel angelegt werden, welche dann bei Zeichenoperationen zur Verfügung stehen. Diese müssen dann selbstverständlich statisch sein, und am Ende, bei der Behandlung der 'WM_DESTROY'-Nachricht wieder freigegeben werden. Auf die Parameter der 'WM_CREATE'-Nachricht werde ich hier allerdings nicht eingehen, da sie hier nicht benötigt werden. Wir greifen dabei auf die Fensterprozedur der zuletzt entwickelten Anwendung zurück und verändern diese wie folgt:

//Implementierung der Fensterprozedur
LRESULT CALLBACK WndProc(HWND hWnd, UINT uiMessage, WPARAM wParam, LPARAM lParam)
{
	//Variablen für WM_PAINT-Zweig
	PAINTSTRUCT ps;
	RECT rc;

	//Grafikobjekte
	HDC hDC;						//Gerätekontext
	static HBRUSH hOldBrush, hRedBrush, hGreenBrush;	//Pinsel		//Änderung
	static HPEN hOldPen, hRedPen, hGreenPen;		//Stifte		//Änderung

	//Variablen für Mausnachrichten							//Ergänzung
	static bool bLButtonDown = false;						//Ergänzung
	static signed int X1, X2, Y1, Y2;						//Ergänzung


	switch (uiMessage)
	{
	case WM_CREATE:									//Ergänzung...
		//Erstellen der Pinsel
		hRedBrush = CreateSolidBrush(RGB(255, 0, 0));
		hGreenBrush = CreateSolidBrush(RGB(0, 255, 0));
		
		//Erstellen der Stifte
		hRedPen = CreatePen(PS_SOLID, 0, RGB(255, 0, 0));
		hGreenPen = CreatePen(PS_SOLID, 0, RGB(0, 255, 0));
	
		return 0;
	case WM_PAINT:									//Änderung...
		//Koordinaten des Anwendungsbereiches ermitteln
		GetClientRect(hWnd, &rc);

		//Zeichenvorgang beginnen
		hDC = BeginPaint(hWnd, &ps);
		
		
		
		//Zeichenvorgang beenden
		EndPaint(hWnd, &ps);
		
		return 0;
	case WM_CLOSE:
		DestroyWindow(hWnd);
		return 0;
	case WM_LBUTTONDOWN:								//Ergänzung...
		//Koordinaten für den ersten Punkt ermitteln (zweiten Punkt initialisieren)
		X2 = X1 = LOWORD(lParam);
		Y2 = Y1 = HIWORD(lParam);
		
		//linke Maustaste gedrückt
		bLButtonDown = true;
		
		return 0;
	case WM_LBUTTONUP:								//Ergänzung...
		//Koordinaten für den zweiten Punkt ermitteln
		X2 = LOWORD(lParam);
		Y2 = HIWORD(lParam);
		
		//linke Maustaste nicht mehr gedrückt
		bLButtonDown = false;
		
		return 0;

	case WM_DESTROY:								//Ergänzung...
		//Freigeben der Stifte
		DeleteObject(hGreenPen); hGreenPen = NULL;
		DeleteObject(hRedPen); hRedPen = NULL;
		
		//Freigeben der Pinsel
		DeleteObject(hGreenBrush); hGreenBrush = NULL;
		DeleteObject(hRedBrush); hRedBrush = NULL;

		//Programmende
		PostQuitMessage(0);
		return 0;
	default:
		break;
	}
	return DefWindowProc(hWnd, uiMessage, wParam, lParam);
}

Die Änderungen bzw. Ergänzungen sollten nun  nachvollziehbar und vollständig hinzugefügt worden sein. 

gewünschte Features des Programms

Das Programm soll in der Lage sein wahlweise rote Quadrate und grüne Ellipsen zu zeichnen. Dabei soll der Benutzer durch Drücken der rechten Maustaste zwischen den beiden Objekten wählen können. Diese sollen dann mit der Maus an beliebigen Stellen gezeichnet werden. Zunächst sollte daher die Wahl des Benutzers in einer entsprechenden Variable festgehalten werden. Nennen wir sie 'bEllipse' mit dem Datentyp 'bool' deklarieren sie als statisch und initialisieren sie in der 'WM_CREATE'-Nachricht mit 'true':

	/* --- Variablendeklarationen ---
	...
	*/

	//Status des zu zeichnenden Objektes						//Ergänzung...
	static bool bEllipse;						


	switch (uiMessage)
	{
	case WM_CREATE:									
		//standardmäßig Ellipsen zeichnen					//Ergänzung...
		bEllipse = true;
		
		/* --- Erstellen der Grafikobjekte ---
		...
		*/
		
		return 0;
	case WM_RBUTTONDOWN:								//Ergänzung...
		//Status ändern
		bEllipse = !bEllipse;
		
		return 0;
	/* --- weitere Nachrichtenbehandlung ---
	...
	*/
	}

In der 'WM_LBUTTONUP'-Nachricht sollen die Objekte dann schließlich gezeichnet werden, d.h. es muss der Gerätekontext ermittelt und die entsprechenden Pinsel und Stifte eingesetzt werden. Für den Gerätekontext wird dazu das bereits bekannte Funktionspaar 'GetDC' und 'ReleaseDC' verwendet. Die Behandlung der Nachricht muss wie folgt ergänzt werden:

	case WM_LBUTTONUP:
		//Koordinaten für den zweiten Punkt ermitteln
		X2 = LOWORD(lParam);
		Y2 = HIWORD(lParam);

		//Gerätekontext ermitteln						//Ergänzung...
		hDC = GetDC(hWnd);

		//Status überprüfen (was soll gezeichnet werden?)
		if (bEllipse)			//grüne Ellipse
		{
			//Stift und Pinsel einsetzen
			hOldBrush = (HBRUSH)SelectObject(hDC, hGreenBrush);
			hOldPen = (HPEN)SelectObject(hDC, hGreenPen);

			//Ellipse zeichnen
			Ellipse(hDC, X1, Y1, X2, Y2);

			//Stift und Pinsel zurücksetzen
			SelectObject(hDC, hOldPen);
			SelectObject(hDC, hOldBrush);
		}
		else				//rotes Rechteck
		{
			//Stift und Pinsel einsetzen
			hOldBrush = (HBRUSH)SelectObject(hDC, hRedBrush);
			hOldPen = (HPEN)SelectObject(hDC, hRedPen);

			//Ellipse zeichnen
			Rectangle(hDC, X1, Y1, X2, Y2);

			//Stift und Pinsel zurücksetzen
			SelectObject(hDC, hOldPen);
			SelectObject(hDC, hOldBrush);
			
		}

		//Gerätekontext freigeben
		ReleaseDC(hWnd, hDC);
		
		//linke Maustaste nicht mehr gedrückt
		bLButtonDown = false;
		
		return 0;

Das Programm ist nun soweit fertig und lässt sich kompilieren. Probieren Sie die gewünschten Features aus und Sie stellen fest, dass alles so funktioniert wie es soll. Wenn Sie bei einem Zeichenvorgang allerdings über den Rand des Fensters hinausgehen, dann wird das gewählte Objekt nicht gezeichnet. Aus diesem Grund sollten zwei kleine Ergänzungen in den Mausnachrichten hinzugefügt werden. Mit dem Funktionspaar 'SetCapture(hWnd)' und 'ReleaseCapture()' kann die Maus eingefangen werden und das Fenster erhält auch Mausnachrichten, wenn sich der Mauszeiger aus dem Anwendungsbereich hinausbewegt. Sie sollten also folgende Ergänzungen machen:

	case WM_LBUTTONDOWN:
		//Maus einfangen							//Ergänzung...
		SetCapture(hWnd);

		/* --- Nachrichtenbehandlung ---
		...
		*/
		
		return 0;
	 case WM_LBUTTONUP:
		/* --- Nachrichtenbehandlung ---
		...
		*/

		//Maus wieder "loslassen"						//Ergänzung...		
		ReleaseCapture();
		
		return 0;

Nun ist der gewünschte Effekt erzielt. Wenn man die Maus während eines Zeichenvorgangs über den Rand hinausbewegt empfängt das Fenster trotzdem die entsprechenden Mausnachrichten: Die Oberfläche könnte in etwa wie folgt aussehen:

Das Programm hat allerdings noch allerhand Nochholbedarf. So wäre u.a. eine Vorschau wünschenswert, bei der während des Zeichnens, also beim Bewegen der Maus das gewünschte Objekt schon einmal angezeigt wird. 

Zur Ergänzung: eine Echtzeitvorschau

Es gibt eine Möglichkeit dies in gewissem Maße einfach zu realisieren. Man könnte das jeweiligen Objekts mit einer Farbe zeichnen, welche nicht einheitlich ist, sonder vom Hintergrund abhängig ist. Außerdem sollte die Operation wieder rückgängig zu machen sein. Dies ließe sich z.B. erreichen wenn man die Pixel des Hintergrundes, die innerhalb des zu zeichnenden Objektes liegen invertiert, denn dann kann man mit nochmaligem Zeichnen diese noch einmal invertieren und man erhält das ursprüngliche Bild. Man kann so einen Modus mit der Funktion 'SetROP2(hDC, DrawMode)' ändern. Die zahlreichen Konstanten für den 2. Parameter sind der MSDN zu entnehmen. Wir brauchen den Parameter, der die Farben invertiert, dies entspricht der Konstanten 'R2_NOT'. 

Um diese Vorschau zu implementieren muss zunächst einmal die Behandlung der Nachricht 'WM_MOUSEMOVE' in der Fensterprozedur aufgenommen werden. Ihre Implementierung sollte wie folgt aussehen:

	case WM_MOUSEMOVE:								//Ergänzung...	
		//linke Maustaste gedrückt
		if (!bLButtonDown) return 0;

		//Koordinaten für den zweiten Punkt ermitteln
		X = LOWORD(lParam);
		Y = HIWORD(lParam);

		//Gerätekontext ermitteln
		hDC = GetDC(hWnd);

		//ROP2-Modus setzen
		iROP2 = SetROP2(hDC, R2_NOT);

		//Status überprüfen (was soll gezeichnet werden?)
		if (bEllipse)			//grüne Ellipse
		{
			//Alte Vorschau aufheben
			Ellipse(hDC, X1, Y1, X2, Y2);

			//Koordinaten übernehmen
			X2 = X;
			Y2 = Y;
			
			//neue Vorschau
			Ellipse(hDC, X1, Y1, X2, Y2);

		}
		else					//rotes Rechteck
		{
			//Alte Vorschau aufheben
			Rectangle(hDC, X1, Y1, X2, Y2);

			//Koordinaten übernehmen
			X2 = X;
			Y2 = Y;
			
			//neue Vorschau
			Rectangle(hDC, X1, Y1, X2, Y2);

			
		}

		//ROP2-Modus zurücksetzen
		SetROP2(hDC, iROP2);


		//Gerätekontext freigeben
		ReleaseDC(hWnd, hDC);
		
		return 0;

Auch hier muss selbstverständlich zwischen Rechteck und Ellipse unterschieden werden. Diesmal brauchen wir allerdings nicht die Pinsel und Stifte einzusetzen. Zunächst wird allerdings die Mausposition in einem neuen Koordinatenpaar ('X' und 'Y') festgehalten, die zuvor noch deklariert (allerdings nicht unbedingt statisch) werden müssen. Der Zweck dieser Koordinaten wird gleich ersichtlich. Nach der Ermittlung des Gerätekontexts wird sofort der ROP2-Modus geändert und der alte in der Variablen 'iROP2' gespeichert, die zuvor allerdings noch deklariert werden müssen. Wir benötigen bei der Behandlung zwei Zeichenaufrufe. In dem ersten wird die gezeichnete Vorschau, d.h. die invertierte Fläche, die während der letzten Behandlung entstanden ist  noch einmal invertiert, d.h. also aufgehoben. Bei dem zweiten Aufruf wird die neue Vorschau gezeichnet, die dann beim nächsten Aufruf wieder aufgehoben wird etc. Zu diesem Zweck ist es auch notwendig ein weiteres Koordinatenpaar einzuführen, weil die letzten Koordinaten zunächst noch benötigt werden und nicht einfach überschrieben werden dürfen. Nach dem Aufheben der Vorschau werden sie dann aktualisiert. Bevor die Behandlung abgeschlossen und der Gerätekontext freigegeben wird, wird konsequenterweise der ROP2-Modus zurückgesetzt. 

Wird die Maustaste losgelassen ist eigentlich noch eine letzte Vorschau vorhanden, die während der 'WM_LBUTTONUP'-Nachricht noch aufgehoben werden müsste. Aufgrund der sehr kurzen Zeitdauer zwischen der letzten 'WM_MOUSEMOVE'- und der 'WM_LBUTTONUP'-Nachricht ist davon auszugehen, dass sie Vorschau vollständig von dem Endobjekt übermalt wird. Zur Übung könnten Sie diese Feinheit ja dennoch einbauen. Dann wäre allerdings auch zu überlegen, beide Nachrichten zusammenzufassen, da die meisten Aufrufe identisch sind. Unterschiedliche Aufrufe sollten, dann nur durch 'if' unterschieden werden. Diesen Gedankengang überlasse ich allerdings dem Leser.

Zum Schluss noch eine Anregung: Es gibt auch einen ROP2-Modus, mit dem man erreicht, dass nur die Linienfarbe invertiert wird, sofern die Füllfarbe des Objekts weiß und die Linienfarbe schwarz ist, was ja den Standardfarben entspricht. Der entsprechende Modus heißt 'R2_NOTXORPEN'. Dieser Modus führt dazu, dass die Farbe des Objekts mit der Farbe des Hintergrundes durch ein 'XOR'  verknüpft und anschließend invertiert ('NOT') wird. Wenn die Farbe des Objekts schwarz ist ('0x00000000'), dann erreicht man genau dann eine Invertierung des Hintergrundes wenn man sie mit 'XOR' verknüpft. Durch das 'NOT' wird die Invertierung wieder aufgehoben. Wenn die Farbe des Objekts weiß (0x00FFFFFF) ist, dann erreicht man mit der 'XOR'-Verknüpfung mit dem Hintergrund keine Veränderung. Durch die anschließende Invertierung durch das 'NOT' erreicht man dann das gewünschte Ergebnis. Ich überlasse es Ihnen den ROP2-Modus zu ändern. Sie müssen lediglich die Konstante verändern.

Zu dem Programm ist noch etwas anzumerken: Die 'WM_PAINT'-Nachricht bleibt praktisch unbehandelt. Der Fensterinhalt, wird (z.B. nach Änderung der Fenstergröße) nicht aktualisiert, d.h. gezeichnete Objekte sind nach dem Neuzeichnen futsch. Normalerweise müsste entweder jedes Objekt in einem Array gespeichert und der Array dann im Zuge der 'WM_PAINT'-Nachricht durchlaufen werden oder der gesamte Fensterinhalt wird in Form eines Bitmaps gespeichert. Ersteres ließe sich mit dem bisher Gelernten bereits realisieren. Dies überlasse ich wiederum dem Leser.

Zusammenfassung

Hier wurden die Erkenntnisse der letzten Lektion noch einmal ausgedehnt. Neben der  'WM_CREATE'-Nachricht sollten Ihnen jetzt die Mausbehandlung vollständig klar sein. Die Vorschau und der eingeführte ROP2-Modus ist als Ergänzung dieser Lektion zu verstehen. Allerdings ist dies kein elementares Wissen für die Windowsprogrammierung. Sie bilden eine nützliche Funktionalität, sollten allerdings nicht überbewertet werden. Es hängt auch davon ab, was Sie primär programmieren wollen. Näheres ist außerdem der MSDN zu entnehmen. 

Hier noch einmal der komplette Quelltext dieses Programms (mit Vorschau):

Winprog_v1_2_src.exe (Quelltext, gepackt)

Die nächsten Lektionen

In der nächsten Lektion wird das Konzept von "Child"-Fenstern vorgestellt, bevor in den Folgelektionen die Interaktion zwischen Benutzer und Programm mittels Steuerelementen aufgegriffen wird.

Zurück Nach oben Weiter