Anmerkung:
Die Inhalte dieses Kapitels sind auch für die
MFC-Programmierung geeignet, denn dort wird der "handle to device
context" nur gekapselt. Die Funktionen können in MFC entsprechend als
Member-Funktionen eingesetzt werden. Die Systematik wurde jedoch
komplett übernommen.
Einer der Besonderheiten von MS Windows ist der Einsatz einer grafischen Benutzeroberfläche mit feinst abgestuften Farben. Diese grafische Schnittstelle zwischen Mensch und Maschine nennt man Graphical User Interface (GUI). Kann man sich bei der Konsolenprogrammierung noch auf die eigentliche Aufgabenstellung konzentrieren, so muß man sich bei der Windows-Programmierung zum großen Teil mit der Gestaltung und Funktion grafischer Elemente auseinandersetzen.
Damit der Programmierer sich zumindest über den inneren Mechanismus individueller Bildschirm- und Druckerausgaben keine tieferen Gedanken machen muß, steht hier eine Schnittstelle zur Verfügung, die unter der Bezeichnung Graphics Device Interface (GDI) bekannt ist. Das Betriebssystem nutzt die GDI auch intensiv für die Darstellung seiner eigenen grafischen Elemente.
Wenn
Sie auf den Bildschirm eines Rechners
schauen, dann blicken Sie auf eine große Menge sogenannter Pixel
(Bildpunkte). Dieser Begriff leitet sich von "picture element" her.
Stellen Sie sich ein Pixel nicht als physikalischen Punkt, sondern als
logischen Punkt vor. Während in der Mathematik ein Punkt keinerlei
Ausdehnung hat, kann man sich dies auf einem Computermonitor als
kleines Rechteck vorstellen. Bei einer typischen Auflösung von z.B.
1024 * 768 Pixel sind dies schon 786432 solcher Punkte. Jedem dieser
Punkte kann man bei einer 24-bit-Einstellung
der Grafikkarte einen aus
16777216 (2 hoch 24) Farbwerten
zuordnen. Ein beliebiges Bild auf dem Monitor
entspricht unter diesen Randbedingungen 16777216 hoch 786432 Möglichkeiten.
Wir
könnten die Grafikprogrammierung somit prinzipiell unter
Zuhilfenahme einer Funktion SetPixel( x, y, color ) erledigen. Eine
Win32API-Funktion, die an der Stelle P(x,y) ein farbiges Rechteck der
Farbe color darstellt, gibt es tatsächlich.
Die korrekte Syntax
in Win32API ist:
COLORREF
SetPixel
(
HDC hdc, // handle to device context
int
X,
// x-coordinate of pixel
int
Y,
// y-coordinate of pixel
COLORREF crColor // pixel color
)
Die Parameter int X und int Y sind die logischen Bildschirmkoordinaten P ( x | y ), die in der linken oberen Ecke bei P ( 0 , 0 ) beginnen und in der unteren rechten Ecke bei P ( 1023 , 767 ) enden.
Der Farbwert wird häufig als RGB-Wert dargestellt. RGB bedeutet red-green-blue und stellt für jede dieser Farbkomponenten einen Bereich von 0 bis 255 zur Verfügung. Der Wert 255 entspricht hierbei jeweils 100% Farbtiefe.
Was
bedeutet aber „device context“? Dies heißt
übersetzt Gerätekontext. Es handelt sich um eine Datenstruktur,
die
Informationen über die Eigenschaften eines Gerätes, z. B. eines
Bildschirms oder Druckers enthält. Diese Gerätekontexte ermöglichen die
geräteunabhängige Ausgabe, einer der ganz großen Vorteile, die MS
Windows gegenüber MS DOS brachte.
Die
Funktionsanweisung SetPixel(hdc,100,100,RGB(255,0,0))
kann somit verwendet werden, um sowohl auf einen Bildschirm als auch
auf einen Drucker einen roten Punkt an der Stelle P ( 100 , 100 )
auszugeben. Man muß nur den richtigen Gerätekontext hdc als ersten
Parameter angeben. Völlig hardwareunabhängig ist man nicht, da man z.B.
auf einem Monochrom-Drucker keine verschiedenen Farben ausgeben kann.
Ohne rote
Farbe gibt es eben keinen roten Klecks. Farbige Punkte auf einem
Bildschirm kann man natürlich auch nur auf einem Farbmonitor mit einer
passenden 24-Bit-Grafikkarte und richtig eingerichteter Treibersoftware
darstellen. Nehmen wir nun an, dass alle diese Gerätschaften bestens
funktionieren und wir uns nur um die Grafikausgabe kümmern müssen.
Beginnen wir mit der Ausgabe von Punkten auf dem Bildschirm. Zunächst
setzen wir genau einen roten Punkt an die Stelle P ( 100 , 100 ):
#include
<windows.h>
LRESULT
CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC
hdc;
PAINTSTRUCT ps;
switch (message)
{
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps); //
Gerätekontext
SetPixel(hdc, 100, 100, RGB(255,0,0)); // Punkt setzen
EndPaint (hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc (hwnd, message, wParam, lParam);
}
int
WINAPI WinMain( HINSTANCE hI, HINSTANCE hPrI, PSTR szCmdLine, int
iCmdShow )
{
static TCHAR szName[] = TEXT("Fensterklasse");
HWND hwnd ;
WNDCLASS wc;
wc.style = CS_HREDRAW |
CS_VREDRAW | CS_DBLCLKS;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hI;
wc.hIcon = LoadIcon
(NULL, IDI_WINLOGO);
wc.hCursor = LoadCursor (NULL,
IDC_ARROW);
wc.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = szName;
RegisterClass (&wc);
hwnd = CreateWindow (szName, TEXT("Punkte setzen"), WS_OVERLAPPEDWINDOW,
0, 0, 200, 200, NULL, NULL, hI,
NULL);
ShowWindow (hwnd, iCmdShow);
UpdateWindow (hwnd);
MSG msg;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage
(&msg);
DispatchMessage (&msg);
}
return msg.wParam;
}
Die bezüglich GDI entscheidende Stelle im Programm findet man hier:
case WM_PAINT:
hdc = BeginPaint( hwnd, &ps ); // Gerätekontext
SetPixel( hdc, 100, 100, RGB(255,0,0) ); // Punkt setzen
EndPaint( hwnd, &ps );
return 0;
Mit der Funktion BeginPaint(...) erzeugen wir einen Gerätekontext
HDC BeginPaint
(
HWND hwnd,
LPPAINTSTRUCT &ps // Adresse der Struktur PAINTSTRUCT
)
Der zweite Parameter ist ein Zeiger auf eine Struktur, die das Zeichnen in den Anwendungsbereich („client area“) ermöglicht. Der Rückgabewert der Funktion BeginPaint(...) ist ein Gerätekontext für die Ausgabe innerhalb des durch hwnd spezifierten Fensters. BeginPaint(...) erstellt den Gerätekontext, und EndPaint(...) zerstört ihn. Im Bereich zwischen diesen beiden Anweisungen kann mit hdc gezeichnet werden.
Führen Sie das Programm nun aus. Sie haben sicher Mühe, den einzelnen Punkt zu erkennen. Das liegt an der Größe des Bildschirms. Aber genau dies ist beabsichtigt. Das Pixel ist ein grafischer Grundbaustein und soll daher möglichst klein sein. Wenn man einen größeren Bildschirm oder einen Beamer verwendet, stellt man daher eher auf eine höhrere Auflösung um, z.B. 1280 * 1024 oder 1600 * 1200, damit das Bild nicht allzu „pixelig“ wird.
Damit Sie das "Gezeichnete" besser erkennen, zeichnen wir eine Linie. Wir verwenden hierzu unsere Grundfunktion SetPixel(...) in einer Schleife:
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
Die Linie sehen Sie nun sicher besser als den
einzelnen Punkt. Nun wollen wir den gesamten Anwendungsbereich mit
Farbe ausfüllen. Dazu dient eine geschachtelte for-Schleife:
Beim
Ausführen des Programmes erkennt man gut,
dass die Fläche nun Pixel für Pixel "neu eingefärbt" wird. Dazu
benötigt auch ein schneller Rechner seine Zeit. Wenn man einen Teil des
Fensters aus dem sichtbaren Bild schiebt und wieder hereinzieht, dann
wird dieser Teil anschließend erneut eingefärbt. Wenn man das Fenster
in der Größe ändert, dann tritt dieser Effekt des Neuzeichnens
ebenfalls ein. Dies zeigt anschaulich Funktion und Wirkung der
Nachricht WM_PAINT.
Sie haben keine Auflösung von 1024 * 768 eingestellt? Kein Problem. Wir beschaffen uns derartige Systemdaten mittels der Funktion GetSystemMetrics(...):
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
Natürlich müssen Linien und gefüllten Flächen nicht allesamt mit SetPixel(...) erstellt werden. Das wäre zwar prinzipiell möglich, aber auch sehr mühsam und nicht gerade programmiererfreundlich.
Die GDI umfaßt daher eine große Zahl von Zeichenfunktionen, die man durch eigenes Ausprobieren kennenlernen sollte. Zum einfachen Bewegen zu P(x,y) verwendet man MoveToEx( hdc, x, y, LPPOINT ). Der Parameter LPPOINT ist hierbei ein Zeiger auf den vorherigen Punkt. Meistens wird hier der NULL-Zeiger gesetzt. Das aus der 16-bit-Zeit noch bekannte MoveTo(...) ist bei Win32API verschwunden. Sie werden es bei den GDI-Funktionen der MFC übrigens wieder finden. Eine gerade Linie zu P(x,y) zieht man mit LineTo( hdc, x, y ). Nachstehend findet sich ein einfaches Beispiel zum Experimentieren:
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
Bevor wir zu anderen Funktionen übergehen, wollen
wir zunächst das Thema Farbe klären. Bei SetPixel(...) konnte man die
Farbe des Punktes direkt angeben. Bei LineTo(...) ist dies nicht
möglich. Hier benötigen wir neben dem Gerätekontext noch einen
Zeichenstift namens HPEN.
Wir bauen diesen in unser Programm ein:
Die wesentlichen Schritte im Umgang mit HPEN sind:
static HPEN MyPen; // Handle für logischen Stift
MyPen = CreatePen( PS_SOLID, 2, RGB (255,0,0) ); // Stift erzeugen
SelectObject( hdc, MyPen ); // Stift dem Gerätekontext zuordnen
DeleteObject( MyPen ); // Stift zerstören
Hier
taucht auch der Begriff des Objektes auf.
Unser Zeichenstift ist eines der GDI-Objekte.
Die Vielfalt steckt in
der Funktion
CreatePen(...):
HPEN CreatePen
(
int
fnPenStyle, // PenStyle
int
nWidth, // Breite
COLORREF crColor //
Farbe
);
Hier kann
man den PenStyle, die Breite des Stiftes
und die Farbe festlegen.
Möglichkeiten für fnPenStyle:
PS_SOLID
PS_DASH PS_DOT PS_DASHDOT PS_DASHDOTDOT
PS_NULL
PS_INSIDEFRAME
Probieren Sie die Varianten selbst aus und schlagen Sie in MSDN oder einer anderen Win32API-Hilfe nach. Nur durch Ausprobieren prägt man sich die Möglichkeiten gut ein.
An diesen einfachen Beispielen haben Sie bereits die grundlegende Funktionsweise des GDI verstanden. Sie können sich nun einen Gerätekontext und diesem zugeordnet einen logischen Stift beschaffen und damit einzelne Punkte und Linien zeichnen.
Die GDI ist variantenreich. Es gibt mehr als nur einen Gerätekontext, verschiedene GDI-Objekte und eine Vielzahl teilweise recht komplizierter Funktionen. Darüber hinaus gibt es verschiedene Möglichkeiten zur Manipulation des logischen Koordinatensystems. Verschaffen wir uns hier zunächst einen Überblick.
Gerätekontexte (Auswahl):
BeginPaint(...) / EndPaint(...) client area / WM_PAINT
GetDC(...) / ReleaseDC(...) client area / andere Nachrichten als WM_PAINT
GetDCEx(...) / ReleaseDC(...) client area / Kombination mit clipping region
GetWindowDC(...) / ReleaseDC(...) gesamtes Fenster / wichtig: WM_NCPAINT
CreateDC(...) / DeleteDC(...) Fenster, Drucker ...
GDI-Objekte:
HPEN Stift
HBRUSH Pinsel
HFONT Text
HBITMAP Bild
HPALETTE Palette
HRGN Region
GDI-Funktionen:
Punkte:
SetPixel GetPixel
Linien und Kurven:
LineTo MoveToEx
Arc ArcTo * AngleArc *
SetArcDirection GetArcDirection
Polyline PolylineTo PolyPolyline
PolyBezier PolyBezierTo
PolyDraw *
LineDDA LineDDAProc * ab Windows NT
Gefüllte Flächen:
Rectangle RoundRect Ellipse
Pie Chord
Polygon PolyPolygon
Arc(...), Chord(...) und Pie(...) benutzen allesamt die gleichen Parameter, die auf der Geometrie der Funktion Arc(...) beruhen:
Arc(
hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd );
Eine Ellipse wird durch das Rechteck mit den Eckpunkten LinksOben(xLeft,yTop) und RechtsUnten(xRight,yBottom) begrenzt. Die Funktion Arc(...) zeichnet gegen den Uhrzeigersinn einen Bogen auf dieser Ellipse. Stellen Sie sich nun eine Hilfslinie vor, die den Punkt(xStart,yStart) mit dem Mittelpunkt der Ellipse verbindet. Von dem Schnittpunkt dieser Hilfslinie mit der Ellipse wird ein Bogen gezeichnet bis zum Schnittpunkt einer zweiten Hilfslinie zwischen Mittelpunkt und Punkt(xEnd,yEnd). Das ist nicht unkompliziert. Lassen Sie sich nicht verwirren, sondern probieren Sie es einfach selbst mit verschiedenen Werten aus.
Sie verstehen das einfach nicht richtig? Damit Sie mit dieser komplizierten Geometrie nicht alleine gelassen sind, benutzen wir einfach ein kleines Programm, das uns die Hilfslinien, das Rechteck, die Ellipse und den Bogen mit verschieden farbigen Stiften zeichnet. Wir überprüfen die Win32API einfach mit sich selbst:
Offenbar alles in Ordnung mit der Funktion Arc(...). Experimentieren Sie mit den Hilfslinien, damit die Wirkungsweise verständlich wird. Wenn Sie auf solche neue Funktionen mit einer Vielzahl von Parametern stoßen, basteln Sie sich ein solches Experimentierfeld und speichern es in einem speziellen Verzeichnis "Vorlagen" ab.
Die Funktion Chord(...) verbindet die Endpunkte des Bogens direkt, während Pie(...) die Endpunkte des Bogens mit dem Ellipsenmittelpunkt verbindet. Wir werden die Funktionen für gefüllte Flächen in einem Programm austesten:
#include <windows.h>
Wir haben
in dem Programm Gebrauch gemacht von den
GDI-Objekten HPEN
(Stift) und HBRUSH
(Pinsel). Der Stift erstellt die Umrandung und der Pinsel füllt füllt
das Innere der jeweils entstehenden Fläche aus. Der Pinsel MyBrush2 hat
in unserem Fall z.B. die Farbe Gelb. Die Hintergrundfarbe, die das
Muster ausfüllt ist Rot:
MyBrush2 = CreateHatchBrush(HS_CROSS, RGB(255,255,0));
SetBkColor (hdc, RGB(255,0,0)); //Hintergrundfarbe
Informieren Sie sich über die möglichen Parameter (MSDN oder andere WinAPI32-Hilfe) und experimentieren Sie, damit die Wirkung verständlich wird.
Bei der Funktion CreateHatchBrush( style, color ) können z.B. folgende Styles ausgewählt werden:
HS_BDIAGONAL HS_CROSS HS_DIAGCROSS
HS_FDIAGONAL HS_HORIZONTAL HS_VERTICAL
Kehren wir zu den Linien-Funktionen zurück. Bisher haben wir zum Erzeugen von Linien die Funktionen MoveToEx(...) und LineTo(...) kennengelernt. Prinzipiell kann man auch durch wiederholte Anwendung von SetPixel(...) eine je nach Punktdichte gepunktete oder durchgezogene Linie erzeugen.
Eine interessante Möglichkeit nutzt die Funktion Polyline(...). Hier benötigt man ein Array von Punkten (Struktur POINT), dessen Adresse als Parameter übergeben wird.
BOOL Polyline
Auf
diese Weise kann man z.B. die Berechnung oder
Festlegung der Punkte vom eigentlichen Zeichenvorgang im Sourcecode
trennen. Nachfolgend finden Sie ein praktisches Beispiel zum
Experimentieren:
Die wesentlichen Schritte beim Einsatz von Polyline(...) waren:
POINT array[ ... ]; // POINT Array
for(...)
{
array[i].x = ... ; // Array-Elemente definieren
array[i].y = ... ;
}
Polyline( hdc, array, anzahl ); // Zeichenfunktion
Die Array-Elemente hätte man z.B. auch aus einer Datei einlesen können.
Bei der Funktionsdarstellung mittels des oben aufgeführten Programms ist sicher aufgefallen, dass wir nicht einfach die Variable x in die Schleife nehmen konnten, um damit jeweils das zugehörige y = f(x) auszurechnen. Stattdessen mußten wir mit Hilfsvariablen die Umrechnung des Koordinatensystems unserer Bildschirmanzeige in ein übliches mathematisches Koordinatensystem mit vier Quadranten vornehmen.
for( i=0; i<cx; i++ )
{
x = i-cx/2; // Transformation i -à x
y = f( x,cx ); // y = f(x)
j = int( cy/2-y ); // Transformation y -à y
SetPixel( hdc, i, j, RGB(255,0,0) );
}
Dies ist eine Möglichkeit. Manchmal wünscht man aber, dass das logische Koordinatensystem unseres Gerätekontextes verändert wird. Wir wollen z.B. den Punkt P(0,0) wie in der Mathematik üblich in der Mitte des Bildes. Hierfür gibt es eine einfache Funktion:
BOOL SetViewPortOrgEx
(
HDC hdc, // Gerätekontext
int X, // neue x-Koordinate des Ursprungs
int Y, // neue y-Koordinate des Ursprungs
POINT *lpPoint // Zeiger auf POINT-Struktur mit vorherigem Urspung
)
Der Ursprung P(0,0), der beim Standard in der linken oberen Ecke des Bildschirms liegt, kann also jetzt mit der Anweisung
SetViewportOrgEx(hdc, cx/2, cy/2, NULL);
in die Mitte des Bildschirms verlegt werden. Unter "Viewport" versteht man die "physische" Fläche, auf der gezeichnet wird. Die Ausdehnungen werden dort in Pixel gemessen. Das probieren wir sofort aus. Wir packen noch eine zweite quadratische Funktion dazu, damit wir sicher sehen, wo "oben und unten" ist:
Die x-Achse ersteckt
sich nun von -cx/2 bis cx/2 und die y-Achse von
-cy/2 bis cy/2. Jetzt ist eine direkte Zuordnung zwischen x und y
innerhalb der Schleife möglich:
for(x=-cx/2; x<cx/2; x++)
{
y = f1(x,cx);
SetPixel(hdc, int(x), int(y), RGB(255,0,0));
...
}
Dies bringt für die Lesbarkeit des Programmes eine deutliche Verbesserung. Störend ist lediglich, dass der positive Teil der y-Achse nach unten zeigt. Das rührt vom Standard-Koordinatensystem mit dem mapping mode MM_TEXT her. Hier zeigt die positive Richtung der y-Achse nach unten. Bei allen anderen mapping modes ist das umgekehrt. Verändert wird der mapping mode mit der Funktion
int SetMapMode
(
HDC hdc, // Gerätekontext
int fnMapMode // mapping mode
)
// Koordinatensystem einrichten
SetMapMode(hdc, MM_LOENGLISH);
SetViewportOrgEx(hdc, cx/2, cy/2, NULL); // (0,0) in der Mitte
Nun steht unsere Funktion nicht mehr auf dem Kopf. Man erkennt, dass die logischen Einheiten von MM_LOENGLISH (eine logische Einheit entspricht 0,01 Zoll = 0,254 Millimeter) den logischen Pixel-Einheiten von MM_TEXT recht nahe kommen.
Es gibt eine ganze Reihe von Funktionen zur Beeinflussung des Koordinatensystems. Verwenden Sie diese nicht zu "bunt" gemischt, sonst ist die Konfusion total.
Nachfolgend finden Sie eine Übersicht über die Mapping Modes der Funktion SetMapMode(...):
|
|
MM_HIENGLISH |
Logische Einheit: 0,001 Zoll = 0,0254 Millimeter. Nach rechts: positives x, nach oben: positives y. |
MM_HIMETRIC |
Logische Einheit: 0,01 Millimeter. Nach rechts: positives x, nach oben: positives y. |
|
|
MM_LOENGLISH |
Logische Einheit: 0,01 Zoll = 0,254 Millimeter. Nach rechts: positives x, nach oben: positives y.
|
MM_LOMETRIC |
Logische Einheit: 0,1 Millimeter. Nach rechts: positives x, nach oben: positives y.
|
MM_TEXT |
Logische Einheit: 1 Pixel. Nach rechts: positives x, nach unten: positives y. |
MM_TWIPS |
Logische Einheit: 1/1440 Zoll ( "twip" = 1/20 * 1/72 Zoll ). Nach rechts: positives x, nach oben: positives y. |
|
|
Daneben gibt es noch MM_ANISOTROPIC und MM_ISOTROPIC, die eine eigene Gestaltung des Koordinatensystems zulassen.
Wir probieren nun in unserem Programm den Parameter MM_HIENGLISH aus:
// Koordinatensystem einrichten
Das Programm zeigt, dass die Darstellung nun um den Faktor 10 verkleinert ist. Hier kann daher das Zehnfache bezüglich x- und y-Achse dargestellt werden kann.