Ausgaben mit Hilfe der GDI

Einführung

Die GDI (Graphic Device Interface) stellt dem Programmierer eine einfache Möglichkeit  graphische Standardvorgänge (Darstellen von Bitmaps, Zeichnen von Figuren und Linien oder Ausgabe von Texten etc.) einfach zu implementieren.  Der Programmierer von Windowsanwendung braucht sich beim Umgang mit Grafikdarstellungen mit keinerlei Hardware herumschlagen. Dies nämlich wird vollständig von Windows übernommen. Auch wird im Prinzip nicht zwischen den Ausgabegeräten unterschieden. Soll beispielsweise eine Figur wie z.B. ein Kreis gezeichnet werden, so lässt sich dies sowohl bei der Bildschirmausgabe als auch bei der Ausgabe mit dem Drucker mit der gleichen Funktion und den gleichen Parametern erreichen. Erreicht wird dies durch sogenannte Gerätekontexte (Device Contexts). Hierfür definiert Windows selbstverständlich einen eigenen Datentyp:

Datentyp Erläuterung
HDC - Handle zu einem Gerätekontext (Handle to Device Context)

Zunächst soll uns erst einmal nur die Ausgabe auf dem Bildschirm interessieren. Es lässt sich zu jedem Fenster ein Gerätekontext ermitteln. Ist er einmal ermittelt worden, so kann er für Grafikvorgänge innerhalb dieses Fenster verwendet werden. Angenommen wir haben ein Fenster 'hWnd', dann lässt sich der entsprechende Gerätekontext 'hDC' wie folgt ermitteln:

HDC hDC = GetDC(hWnd);
/* --- Zeichenoperationen ---
...
*/

Der Funktionsaufruf bedarf wohl keiner näheren Erläuterung. Wird der Gerätekontext am Ende einer Operation nicht mehr benötigt so wird dieser wie folgt wieder freigegeben:

/* --- Zeichenoperationen ---
...
*/
ReleaseDC(hDC); hDC = NULL;

Dieses Funktionspaar kann jederzeit aufgerufen werden um in einem Fenster irgendwelche Zeichenoperationen durchführen zu können. Jedes Fenster bekommt allerdings immer dann eine Nachricht, nämlich 'WM_PAINT', wenn sein Inhalt neu gezeichnet werden sollte. Der Fensterbereich wird dann als ungültig erklärt und muss vom Programmierer neu gefüllt werden. Anschließend sollte dann der Bereich wieder für gültig erklärt werden. Das bereits vorgestellte Funktionspaar würde da alleine nicht ausreichen. Es gibt allerdings ein weiteres Funktionspaar welches all diese Aufgaben übernimmt. Es sollte allerdings nur aufgerufen werden wenn, ein Fenster die besagte 'WM_PAINT'-Nachricht erhält. Innerhalb der Fensterprozedur sähe dies wie folgt aus:

LRESULT CALLBACK WndProc(HWND hWnd, UINT uiMessage, WPARAM wParam, LPARAM lParam)
{
	//Variablen für die WM_PAINT-NAchricht
	PAINTSTRUCT ps; HDC hDC;
	switch (uiMessage)
	{
	/* --- Nachrichtenbehandlung ---
	...
	*/
	case WM_PAINT:
		hDC = BeginPaint(hWnd, &ps);
		/* --- Zeichenoperationen ---
		...
		*/
		EndPaint(hWnd, &ps);
		return 0;
	default:
		break;
	}
	return DefWindowProc(hWnd, uiMessage, wParam, lParam);
}

Das Funktionspaar scheint einleuchtend. Einzig und allein der 2. Parameter bedarf noch an Erklärung. Der 2. Parameter beschreibt eine Struktur in der u.a. der ungültige Bereich in Form eines  Rechtecks abgelegt ist. So kann der Programmierer auf dieses Rechteck zurückgreifen und seine Neuzeichnungen nur auf diesen Bereich beschränken. Im folgenden, wird der Inhalt dieser Struktur allerdings nicht näher betrachtet, da er hier auch keine Verwendung findet.

Erste Zeichenoperationen

Die GDI stellt unzählige Funktionen bereit um den Inhalt eines Gerätekontexts zu füllen. Betrachten wir zunächst einmal das Zeichen eines einfachen Rechtecks. Die entsprechende Funktion sieht wie folgt aus:

Rectangle(hDC, Left, Top, Right, Bottom);

Es werden beim Zeichen eines Rechtecks der Gerätekontext und die Koordinaten übergeben. Als Koordinaten dienen dabei die Koordinaten der Punkte der oberen linken Ecke und der unteren rechten. Das standardmäßige Koordinatensystem bei Fenstern (und auch bei Bitmaps) ist allerdings nicht mit dem aus der  Mathematik bekannten kartesischen im Verhältnis 1:1 zu vergleichen. Sie unterscheiden sich nämlich durch die Lage des Ursprungs und der Orientierung der "Y-Achse". Bei Fenstern ist der Ursprung also in der linken oberen Ecke, wählt man positive "Y-Werte", so bewegt man sich nach unten. Möchte man nun ein Rechteck zeichnen, welches 100 Pixel breit und 100 Pixel hoch ist (1 Pixel entspricht einer Längeneinheit) und 50 Pixel unterhalb des oberen Fensterrandes und 50 Pixel rechts vom linken Fensterrand gezeichnet werden soll, so wäre dies wie folgt zu realisieren:

Rectangle(hDC, 50, 50, 150, 150);

Soll hingegen eine Ellipse gezeichnet werden, welche genau in dieses Rechteck passt, so muss folgende Funktion aufgerufen werden:

Ellipse(hDC, 50, 50, 150, 150);

Zu den Standardzeichenoperation gehört jetzt noch das Zeichnen einer einfachen Linie. Dazu sind allerdings zwei Aufrufe nötig. Zunächst muss man den Startpunkt festlegen. Anschließend malt man dann eine Linie von diesem Punkt zu einem weiteren mit einem weiteren Funktionsaufruf. Soll ein Kreuz in das bereits bestehende Rechteck gezeichnet werden müsste dies wie folgt implementiert werden:

//Linie von oben links nach unten rechts
MoveToEx(hDC, 50, 50, NULL);
LineTo(hDC, 150, 150); 

//Linie von oben rechts nach unten links
MoveToEx(hDC, 150, 50, NULL);
LineTo(hDC, 50, 150); 

Hier noch einmal alle Funktionen im Überblick:

Funktion Erläuterung
Rectangle(hDC, Left, Top, Right, Bottom) - zeichnet ein Rechteck in dem angegebenen Rechteck
Ellipse(hDC, Left, Top, Right, Bottom) - zeichnet eine Ellipse in dem angegebenen Rechteck
MoveToEx(hDC, X, Y, &pt) - setzt die Zeichenposition und speichert die alte in 'pt'
LineTo(hDC, X, Y) - malt eine Linie von der aktuellen Zeichenposition zu      dem angegebenen Punkt

Linien- und Füllfarbe

Wenn man sich die drei bis vier Zeichenoperation näher anschaut sucht man vergeblich nach einem Parameter, der Linien- oder Füllfarbe speichert. Der einzige logische Schluss ist: Die entsprechenden Farben müssen im Gerätekontext gespeichert sein. Standardmäßig ist die Füllfarbe auf weiß, die Linienfarbe auf schwarz gesetzt. Um die Farben zu ändern muss man zwei verschiedene Objekte erstellen, eins für die Füllfarbe und eins für die Linienfarbe. Letzteres nimmt außerdem Angaben über Liniendicke etc. auf. Zunächst betrachten wir einmal das erste Objekt, dasjenige welches die Füllfarbe speichert. In der Windows-API wird es Pinsel (Brush) genannt und besitzt folgenden Datentyp:

Datentyp Erläuterung
HBRUSH - Handle zu einem Pinsel (Handle to Brush), der die Füllfarbe enthält

Es lassen sich neben einfachen Pinseln mit nur einer Füllfarbe auch Pinsel mit einem Füllmuster erstellen. Hier soll der einfache aber erst einmal reichen. Um einen Pinsel zu erstellen, der einfach nur eine konstante Farbe enthält ist folgende Funktion aufzurufen:

HBRUSH hBrush = CreateSolidBrush(cl);

Der Parameter 'cl' beschreibt dabei die Farbe, Er hat den Datentyp 'COLORREF', tatsächlich ist dieser Datentyp nichts weiter als ein 32-Bit Integer von dem allerdings nur die unteren 24, nämlich jeweils 8 für Rot, Grün und Blau, benötigt werden. Man gibt für jeden einzelnen Farbkanal somit eine Zahl zwischen 0 und 255 an. Je höher der Wert, je intensiver ist der jeweilige Farbton. Wählt man für jeden Kanal das Maximum von 255, so erhält man Weiß, wählt man jeweils das Minimum, so erhält man Schwarz. Ein Grau erhält man immer dann, wenn man für jeden Farbkanal den selben Wert wählt. Auf diese Weise lässt sich jede Grauabstufung zwischen Weiß und Schwarz erreichen. So könnte man z.B. einige Farben definieren:

COLORREF clRed, clGreen, clBlue, clBlack, clWhite, clGray;
clRed = RGB(255, 0, 0);		//0x000000FF
clGreen = RGB(0, 255, 0);	//0x0000FF00
clBlue = RGB(0, 0, 255);	//0x00FF0000
clBlack = RGB(0, 0, 0);		//0x00000000
clWhite = RGB(255, 255, 255);	//0x00FFFFFF
clGray = RGB(128, 128, 128);	//0x00808080

Das Makro wandelt die Kombination aller Farbkanäle in die entsprechende 32-Bit-Werte um. Wollen wir nun einen Pinsel erstellen, welcher die Farbe Rot enthält, so gehen wir wie folgt vor:

HBRUSH hBrushRed = CreateSolidBrush(clRed);

Natürlich lässt sich das Makro auch innerhalb des Funktionsaufrufes verwenden:

HBRUSH hBrushRed = CreateSolidBrush(RGB(255, 0, 0));

Wird der Pinsel nicht mehr benötigt wird er wieder freigegeben:

DeleteObject(hBrushRed); hBrushRed = NULL;

Bevor wir den Pinsel in Verbindung mit dem Gerätekontext bringen, befassen wir uns zuvor mit dem sogenannten Objekt, welches die Linieneigenschaften enthält. In der Windows-API wird es Stift (Pen) genannt und besitzt folgenden Datentyp:

Datentyp Erläuterung
HPEN - Handle zu einem Stift (Handle to Pen), der die Linieneigenschaften enthält

Um einen "Stift" zu erstellen geht man wie folgt vor:

HPEN hPen = CreatePen(Style, Width, cl);

Den zweiten und dritten Parameter brauche ich wohl nicht mehr erläutern. Für den ersten jedoch sind Konstanten definiert, welche den Stil angeben und die ich hier kurz vorstellen möchte:

Stil Art des Stiftes
PS_SOLID - Standardstift (Linie ist durchgezogen)
PS_DASH - Gestrichelte Linie (nur für Breiten von 0 bis 1) <-->
PS_DOT - Gepunktete Linie (nur für Breiten von 0 bis 1) <..>
PS_DASHDOT - gestrichelte und gepunktete Linie im Wechsel (nur für Breiten von 0 bis 1) <-.>
PS_DASHDOTDOT - gestrichelte und gepunktete Linie im Wechsel (nur für Breiten von 0 bis 1) <-..>
PS_NULL - keine Linie

Man kann einen Stift auch mit einer weiteren Funktion ('CreatePenIndirect') erstellen, welche sich allerdings von der bekannten nur im Aufruf unterscheidet. Dort werden nämlich die drei Parameter in einer Struktur zusammengefasst. 

Wie beim Pinsel muss auch ein Stift am Ende wieder freigegeben werden. Dies geschieht ebenfalls mit der schon bekannten Funktion: 

DeleteObject(hPen); hPen = NULL;

Die GDI bietet noch eine weitere Funktion an, die es ermöglicht von Windows vordefinierte Stifte und Pinsel zu verwenden. Der Rückgabetyp dieser Funktion muss allerdings in den gewünschten ('HPEN' bzw. 'HBRUSH') umgewandelt werden. So lässt sich ein beispielsweise ein schwarzer Pinsel oder Stift ermitteln:

hBlackBrush = (HBRUSH)GetStockObject(BLACK_BRUSH);
hBlackPen = (HPEN)GetStockObject(BLACK_PEN);

Da es sich hierbei um vordefinierte Objekte handelt dürfen diese nicht per 'DeleteObject' freigegeben werden! Sollten sie nicht mehr gebraucht werden, beschränken Sie sich darauf sie auf 'NULL' zu setzen.

Stifte und Pinsel in dem Gerätekontext aufnehmen

Sind nun die gewünschten Stifte und Pinsel für Ihre Objekte ausgesucht worden, so können sie endlich in den vorhandenen Gerätekontext eingesetzt werden. Dies geschieht jeweils mit der Funktion 'SelectObject'. Dabei ist allerdings auf ein wesentlichen Aspekt zu achten. Sie sollten, wenn Sie die Zeichenoperation beendet haben, sowohl Stift als auch Pinsel zurücksetzen. Zu diesem Zweck müssen Sie die alten Objekte jeweils speichern, um sie nachher wieder zurücksetzen zu können. Die Funktion gibt zu diesem Zweck die ausgewechselten Objekte zurück. Sie müssen dann jeweils nur in den entsprechenden Datentyp umgewandelt werden:

//Eigene Objekte einsetzen und alte speichern
HBRUSH hOldBrush = (HBRUSH)SelectObject(hDC, hRedBrush);  
HBRUSH hOldPen = (HPEN)SelectObject(hDC, hBluePen);

/* --- Zeichenoperationen ---
...
*/

//Alte Objekte zurücksetzen
SelectObject(hDC, hOldPen); hOldPen = NULL;
SelectObject(hDC, hOldBrush); hOldBrush = NULL;

Wenn Sie ein Objekt nur für eine Zeichenoperation benötigen können Sie es auch einfach gleich nach dem Erstellen in den Gerätekontext einsetzen ohne es vorher in einer eigenen Variable zu speichern. Dann müssen Sie nur das alte Objekt speichern und nachher wieder zurücksetzen. Dabei muss dann aber das eigene wiederum gelöscht werden. Dazu folgendes Beispiel:

//neues Objekt erstellen und in Gerätekontext einsetzen, und altes Objekt speichern
HBRUSH hOldBrush = (HBRUSH)SelectObject(hDC, CreateSolidBrush(clRed));

/* --- Zeichenoperationen ---
...
*/

//Erstelltes Objekt löschen und altes Objekt wieder einsetzen
DeleteObject(SelectObject(hDC, hOldBrush)); hOldBrush = NULL;

Vergessen Sie dabei nur nicht das eigene Objekt wieder zu löschen und das alte gleichzeitig wieder einzusetzen.

Zusammenfassung

Eigentlich wären jetzt einfache Textausgaben mittels 'DrawText' und 'TextOut' an der Reihe. Allerdings halte ich es für sinnvoller diese warten zu lassen, und statt dessen erst einmal diese Lektion noch einmal zu überfliegen um in der nächsten Lektion eine weitere Windowsanwendung zu schreiben, in der dann das hier Erlernte angewendet wird um erst einmal aus der Theorie herauszukommen. 

In dieser Lektion sollte nun klar sein worum es sich bei Gerätekontexten handelt, wie man sie erstellt bzw. ermittelt, wie man Pinsel und Stift erstellt und schließlich wie man Ausgaben auf dem Bildschirm erreicht. Auch sollte der Zweck der 'WM_PAINT'-Nachricht im Groben klar sein. Sollte dies alles im Laufe dieser Lektion nicht ganz einleuchtend sein, so wird dies hoffentlich im Laufe der nächsten Lektion umso einleuchtender.

Die nächste(n) Lektion(en)

Das hier Erlernte soll nun angewendet werden, indem eine Anwendung geschrieben wird, welche sich mit Ausgaben beschäftigt.

 

Zurück Nach oben Weiter