Sonstiges
Mit dieser Lektion werden wir das Thema GDI abschließen. Es folgt im Anschluss
nur noch die fast obligatorische Tipps&Tricks Seite.
Doch sehen wir uns jetzt noch einige Fälle an die Sie zwar nicht alltäglich benötigen
werden, die Ihnen aber trotzdem nochmals einen tieferen Einblick in die Arbeitsweise
des GDI geben. So erfahren Sie hier über folgenden Themen näheres: Animationen,
Regionen, Path-Funktionen und Raster-Operationen.
Anfangen wollen wir mit dem Thema Animationen. Ziel der Animation soll es sein,
eine größere Anzahl von Kreisen, die ihre Farbe wechseln, durch ein Fenster wandern
zu lassen.
 |
Damit wir nicht unnötig viel Zeit mit der Erstellung der Kreise
und deren Farbänderung verbringen, wurde bereits das Grundgerüst des Programms
fertig erstellt. Kopieren Sie sich nun das Projekt
99Templates\Animation in Ihr Arbeitsverzeichnis und öffnen Sie
es dann.
Bevor wir uns der Funktionsweise des Programms zuwenden, übersetzen Sie das
Programm einmal und lassen es einfach laufen. Sie sollten etwa folgende Ausgabe
erhalten:

Je nach vorhandener Rechenleistung flackern jedoch die Kreise mehr oder weniger
beim Verschieben. Und genau dieses Flackern wollen wir im Verlaufe dieser Übung
beseitigen.
|
Doch sehen wir uns jetzt grob die Funktionsweise des Programms an.
Im Konstruktor der Ansichtsklasse wird zunächst das Feld für die Positionen der
einzelnen Kreise erstellt und ein Zeiger m_pMemDC mit NULL initialisiert.
Auf diesen Zeiger kommen wir gleich noch zu sprechen. Danach wird in der Methode
OnInitialUpdate(...) das Feld mit 'zufälligen' Kreispositionen gefüllt.
Anschließend wird ein Timer (Zeitgeber) aufgesetzt. Dieser Timer sendet im Abstand
von ca. 100 ms eine WM_TIMER Nachricht an das Fensters (zu der wir auch gleich kommen).
Am Schluss der OnInitialUpdate(...) Methode wird noch die Startfarbe für
alle Kreise auf schwarz gesetzt. Die Nachricht vom Timer wird innerhalb des Nachrichtenbearbeiters
OnTimer(...) abgearbeitet. Innerhalb von OnTimer(...) wird lediglich
die aktuelle Farbe aller Kreise neu berechnet und anschließend der gesamte Fensterinhalt
als ungültig markiert. Dies führt dazu, dass eine WM_PAINT Nachricht ausgelöst wird
über die wiederum die OnDraw(...) aufgerufen wird. In OnDraw(...)
wird der Pinsel zum Ausfüllen der Kreise mit der aktuellen Farbe (berechnet in
OnTimer(...))erstellt und ausgewählt. Anschließend wird die Größe des Fensters
ausgelesen; schließlich sollen die Kreise, die rechts aus dem Fenster hinauswandern
auch links wieder neu erscheinen. In der nachfolgenden for-Schleife werden
alle Kreise noch in X-Richtung um einen bestimmten Betrag verschoben und dann neu
gezeichnet. Wird das Fenster geschlossen, so erfolgt zunächst in der Methode
OnDestroy(...) die Freigabe des reservierten Timers. Im Destruktor des Ansichtobjekts
wird dann zum Schluss der für die Kreispositionen reservierte Speicher noch freigegeben.
So viel zur Funktionsweise des Programms.
Doch gehen wir nun ans eigentliche Problem, dem Beseitigen des Flackerns.
Dazu müssen wir zuerst herausbekommen, warum die Kreise beim Verschieben flackern.
Sehen Sie sich nochmals die Methode OnTimer(...) an. Dort wird durch den
Aufruf von Invalidate(...) der Inhalt des Fensters als ungültig gekennzeichnet.
Wenn Sie in der Online-Hilfe zu dieser Methode einmal nachschlagen werden Sie feststellen,
dass diese Methode eigentlich einen Parameter besitzt. Dieser Parameter steuert
das Zeichnen des Hintergrunds. Wird der Parameter weggelassen oder auf TRUE
gesetzt, so wird vor jedem Neuzeichnen des Fensters dessen Hintergrund neu 'gemalt',
d.h. der bestehende Fensterinhalt wird komplett gelöscht. Und dieses 'Neuzeichnen'
des Hintergrunds (in der Regel mit der Farbe weiß) verursacht das Flackern.
 |
Passen Sie den Aufruf von Invalidate(...) in der
OnTimer(...) Methode wie folgt an, damit der Fensterhintergrund nicht
jedes Mal neu gezeichnet wird.
void CAnimationView::OnTimer(UINT
nIDEvent)
{
// TODO: Code für die Behandlungsroutine für Nachrichten hier
einfügen und/oder Standard aufrufen
CView::OnTimer(nIDEvent);
m_ActColor += 0x00020200;
m_ActColor &= 0x00FFFFFF;
Invalidate(FALSE);
} |
Übersetzen und starten Sie das Programm
|
Doch was passiert nun? Sie werden eine Ausgabe in der folgenden Art erhalten:

Auch nicht schlecht, oder? Aber doch nicht ganz das, was wir uns vorgestellt
haben. Da der Hintergrund nun nicht mehr gelöscht wird bleiben 'Reste' der Kreise
an ihren alten Positionen stehen und die Kreise werden an den neuen Positionen einfach
darüber gemalt.
Sehen wir uns jetzt die Lösung des Problems an. Wenn Sie sich die Eigenschaften
unserer Ansichtsklasse einmal genau angesehen haben, so werden Sie dort eine Membervariable
m_pMemDC vom Typ CDC-Zeiger entdecken. Wie unschwer aus dem Variablennamen
abgeleitet werden kann, werden wir hier einen Speicher-DC zur Lösung einsetzen.
Fangen wir mit dem Anpassen der OnInitialUpdate(...) Methode an.
 |
Erstellen wir nun in der OnInitialUpdate(...) den
benötigten Speicher-DC. Die einzufügenden Code-Zeilen sollten in der Zwischenzeit
für Sie nichts Neues mehr enthalten.
void CAnimationView::OnInitialUpdate()
{
CView::OnInitialUpdate();
// TODO: Speziellen Code hier einfügen und/oder Basisklasse aufrufen
CRect CClientRect;
GetClientRect(&CClientRect);
// Speicher-DC erstellen
CDC *pViewDC = GetDC();
m_pMemDC = new CDC;
m_pMemDC->CreateCompatibleDC(NULL);
// Farbaufloesung auslesen
int nBitsPerPixel = pViewDC->GetDeviceCaps(BITSPIXEL);
int nPlanes = pViewDC->GetDeviceCaps(PLANES);
// DC des Views wird nun
nicht mehr benoetigt
ReleaseDC(pViewDC);
// Jetzt entsprechende Bitmap
erstellen
CBitmap ClientBmp;
ClientBmp.CreateBitmap(CClientRect.right, CClientRect.bottom,
nPlanes, nBitsPerPixel, NULL);
// Bitmap dem Speicher-DC
zuweisen
m_pMemDC->SelectObject(&ClientBmp);
for (int iIndex=0; iIndex<m_nNOOFCIRCLES; iIndex++)
....
} |
Der so erstellte Speicher-DC bleibt solange bestehen, bis das Programm beendet
wird. Am Ende des Programms löschen wir den Speicher-DC innerhalb der Methode
OnDestroy(...). Fügen Sie die dafür notwendige Zeile in dieser Methode
ein.
void CAnimationView::OnDestroy()
{
CView::OnDestroy();
// TODO: Code für die Behandlungsroutine für Nachrichten hier
einfügen
KillTimer(1);
delete m_pMemDC;
} |
|
Damit hätten wir die Erstellung und das Löschen des Speicher-DC abgeschlossen
und es kann ans Anpassen der Methode OnDraw(...) gehen. Bisher wurden in
dieser Methode die Kreise direkt ins Fenster gezeichnet was zum Flackern beim Aktualisieren
des Fensters führte. Anstelle nun direkt ins Fenster zu zeichnen, zeichnet die Methode
zuerst in die Bitmap des Speicher-DC und kopiert dann, wenn alles fertig gezeichnet
wurde, diese Bitmap ins Fensters. Da das Zeichnen in die Bitmap nicht sichtbar ist
und das Kopieren der Bitmap (mit den neu positionierten Kreisen) auf einen Schlag
erfolgt, ist kein Flackern mehr sichtbar.
 |
Passen Sie die OnDraw(...) nun wie folgt an:
void CAnimationView::OnDraw(CDC*
pDC)
{
CAnimationDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// ZU ERLEDIGEN: Hier Code zum Zeichnen der ursprünglichen Daten
hinzufügen
// Pinsel im Speicher-DC
auswaehlen
CBrush CActBrush(m_ActColor);
CBrush *pCOldBrush = m_pMemDC->SelectObject(&CActBrush);
CRect CClientArea;
GetClientRect(&CClientArea);
// Bitmap mit weissem Hintergrund
versehen
m_pMemDC->FillSolidRect(&CClientArea,RGB(255,255,255));
// Kreise in Bitmap einzeichnen
for (int iIndex=0; iIndex<m_nNOOFCIRCLES;
iIndex++)
{
m_pMemDC->Ellipse(m_pCCircles[iIndex].x,m_pCCircles[iIndex].y,
m_pCCircles[iIndex].x+m_nCIRCLESIZE,
m_pCCircles[iIndex].y+m_nCIRCLESIZE);
m_pCCircles[iIndex].x += 2;
if (m_pCCircles[iIndex].x> CClientArea.right)
m_pCCircles[iIndex].x
= 0;
}
// alten Pinsel wieder auswaehlen
m_pMemDC->SelectObject(pCOldBrush);
// und Bitmap ins Fenster
kopieren
pDC->BitBlt(0,0,CClientArea.right,CClientArea.bottom,
m_pMemDC,0,0,SRCCOPY);
} |
Übersetzen und starten Sie nun das Programm.
Beachten Sie bitte, dass die komplette Bitmap zuerst weiß ausgefüllt wird
(FillSolidRect(...) Aufruf). Sie können zu Testzwecken diesen Aufruf
auch einmal entfernen und sich dann das Ergebnis ansehen.
|
Wenn alles richtig funktioniert erhalten Sie eine flackerfreie Animation. Doch
einen 'kleinen' Fehler hat das Programm noch. Vergrößern Sie einmal das Fenster
zum Vollbild und lassen es eine Zeitlang laufen. Was beobachten Sie? Nun, wenn die
Kreise die Position erreichen, die ursprünglich den rechten Fensterrand markierte,
so verschwinden Sie für eine gewisse Zeit bevor sie links wieder auftauchen. Die
Ursache dafür liegt in der Größe der Bitmap des Speicher-DC. Diese Bitmap wurde
in der OnInitialUpdate(...) Methode genau so groß erstellt, wie der Client-Bereich
des Fensters am Beginn war. Wenn Sie nun das Fenster zum Vollbild vergrößern so
ist die Bitmap kleiner als der aktuelle Client-Bereich. Dass das Programm in der
OnDraw(...) Methode beim Ausführen der Methode FillSolidRect(...)
nicht abstürzt, obwohl dort die Bitmap weit über ihre Grenzen hinaus beschrieben
wird, ist reines Glück. Sie sehen, nicht immer wenn ein Programm ohne Absturz läuft
ist es auch fehlerfrei. Was uns hier noch fehlt ist die entsprechende Anpassung
der Bitmap beim Verändern der Fenstergröße.
 |
Damit Sie nach solanger Zeit mal wieder selbst etwas tun müssen,
versuchen Sie einmal selbst die OnSize(...) Methode hierfür entsprechend
anzupassen. Wie die für den Speicher-DC notwendige Bitmap im Prinzip zu erstellen
ist, können Sie sich in der OnInitialUpdate(...) Methode weiter vorne
ansehen.
 |
Wenn Sie genug geschwitzt haben kann ich Ihnen auch meine Lösung anzeigen. |
Lösung zur OnSize(...) Anpassung
void CAnimationView::OnSize(UINT
nType, int cx, int cy)
{
CView::OnSize(nType, cx, cy);
// TODO: Code für die Behandlungsroutine für Nachrichten hier
einfügen
// Falls noch kein
Speicher-DC vorhanden, fertig
// (OnInitialUpdate(...) wurde noch nicht ausgefuehrt)
if (m_pMemDC == NULL)
return;
// Farbaufloesung auslesen
CDC* pViewDC = GetDC();
int nBitsPerPixel = pViewDC->GetDeviceCaps(BITSPIXEL);
int nPlanes = pViewDC->GetDeviceCaps(PLANES);
ReleaseDC(pViewDC);
// und entsprechende
Bitmap erstellen
CBitmap ClientBmp;
ClientBmp.CreateBitmap(cx, cy, nPlanes, nBitsPerPixel, NULL);
// Bitmap dem Speicher-DC
zuweisen
CBitmap *pOldBitmap
= m_pMemDC->SelectObject(&ClientBmp);
// alte Bitmap loeschen
pOldBitmap->DeleteObject();
} |
So leicht Ihnen vielleicht diese Übung am Anfang erschien, sie hat doch
zwei kleine Fallen.
So müssen Sie am Anfang der Methode immer abfragen, ob ein entsprechender
Speicher-DC überhaupt schon existiert. Die Methode OnSize(...)
wird nämlich noch vor der Methode OnInitialUpdate(...) aufgerufen
in der der Speicher-DC das erstemal erstellt wird. Außerdem muss zum Schluss
die alte Bitmap selbstverständlich ebenfalls gelöscht werden.
Ende der Lösung
|
Das fertig Beispiel finden Sie unter 05GDI\Animation.
|
Damit verlassen wir das Thema Animation und wenden uns den Regionen (Regions)
zu. Regionen sind zunächst einmal nichts anderes als mehr oder weniger frei definierbare
Bereiche. Mehrere dieser Bereiche können Sie zu einem neuen Bereich zusammenfassen,
auf Gleichheit vergleichen, ausfüllen usw. Doch sehen wir uns erst einmal an, wie
man einen Bereich erstellt. Um einen Bereich zu erstellen ist zunächst ein Objekt
der Klasse CRgn zu erstellen. Diesem Objekt ist aber noch kein Bereich
zugeordnet. Um dem Objekt einen Bereich zuzuordnen stehen diverse CRgn-Methoden
zur Verfügung. Die wichtigsten davon sind CreateRectRgn(...), CreateEllipticRgn(...)
und CreatePolyRgn(...). Die Bedeutungen der Methoden dürfte aus ihren Namen
hervorgehen. Und nochmals: dies sind nur die wichtigsten, es stehen noch eine ganze
Reihe weiterer Methoden zur Verfügung um Bereiche zu erstellen.
Ist ein Bereich erst einmal erstellt so kann er mit den folgenden Methoden bearbeitet
werden:
|
Methode
|
Auswirkung
|
| CombineRgn(...) |
Verknüpft zwei Bereiche
zu einem neuen Bereich (mehr dazu weiter unten). |
| EqualRgn(...) |
Vergleicht zwei Bereiche
auf Gleichheit. |
| FillRgn(...) |
Füllt den Bereich mit einem
zu übergebenden Pinsel aus. Der Füllmodus kann mit der bekannten Methode
SetPolyFillMode(...) eingestellt werden (Stichwort: WINDING oder ALTERNATE). |
| PaintRgn(...) |
Wie FillRgn(...),
nur dass hier der aktuelle Pinsel verwendet wird. |
| InvertRgn(...) |
Invertiert den Inhalt des
Bereichs. |
| FrameRgn(...) |
Zeichnet einen Rahmen um
den Bereich mit dem zu übergebenden Pinsel. |
| GetRgnBox(...) |
Ermittelt die Ausdehnung
des Bereichs. |
| OffsetRgn(...) |
Verschiebt den Bereich. |
| PtInRgn(...) |
Ermittelt ob ein bestimmter
Punkt innerhalb des Bereichs liegt. |
Wie Sie sehen sind Bereich vielfältig einsetzbar. Auf die Methode CombineRgn(...)
soll noch kurz näher eingegangen werden. Wie erwähnt dient diese Methode dazu, zwei
Bereiche zu einem neuen Bereich zusammenzufassen. Wie diese Bereiche zusammengefasst
werden, wird über einen Parameter der Methode gesteuert. Das nachfolgende Bild zeigt
die Kombinationsmöglichkeiten sowie den Wert des entsprechenden Parameters auf.
Die erste Region besteht hierbei aus einem rechteckigen Bereich und die zweite Region
aus einer Ellipse (Kreis).

Was Bereiche aber so interessant macht ist die Tatsache, dass der Clipping-Bereich
eines Fensters über einen solchermaßen definierten Bereich eingestellt werden kann.
Dies erfolgt durch den Aufruf der CDC-Methode SelectClipRgn(...).
Mit Hilfe dieser CDC-Methode und der CRgn-Methode CombineRgn(...)
können damit sehr komplexe Clipping-Bereiche erstellt werden.
 |
Kopieren Sie sich nun einmal das Programm
05GDI\Region in Ihren Arbeitsbereich,
übersetzen es dort und starten es. Sie erhalten dann eine Ausgabe in folgender
Form:

Hier wurden zwei elliptische Region so kombiniert, dass nur der Bereich zwischen
den Ellipsen übrig bleibt. Anschließend wurde dieser Bereich als Clipping-Bereich
gesetzt und das gesamte Fenster mit Linien-Befehlen ausgefüllt.
|
Kommen wir jetzt zu den Path-Funktionen. Paths, oder auch Pfade, haben hier nichts
mit Verzeichnispfaden zu tun. Vielmehr besteht ein Path aus einer oder mehreren
Figuren die gemeinsam ausgefüllt und/oder umrandet werden können. Außerdem lassen
über Paths ebenfalls Clipping-Bereiche festlegen, fast genauso wie mit den vorher
erwähnten Regionen. Der Unterschied zu den Regionen liegt nun darin, dass sich mit
Hilfe von Paths beliebig komplexe Gebilde erstellen lassen. Um einen Path zu erstellen
wird zunächst die CDC-Methode BeginPath(...) aufgerufen. Sie öffnet
sozusagen den Path. Alle (oder besser fast alle) dann folgenden Zeichenanweisung
zeichnen nicht mehr auf dem Bildschirm sondern jetzt innerhalb des geöffneten Paths,
d.h. ab dem Zeitpunkt, an dem Sie BeginPath(...) aufgerufen haben, sehen
Sie keine Zeichenanweisungen mehr auf dem Bildschirm. Um den Path am Schluss wieder
zu schließen ist die CDC-Methode EndPath(...) aufzurufen. Nach
der Ausführung dieser Methoden besitzt der DC einen abgeschlossenen Path den Sie
dann mit einer der folgenden Methoden bearbeiten können.
|
Methode
|
Auswirkung
|
| FillPath(...) |
Füllt den Path mit dem aktuellen
Pinsel aus. |
| PathToRegion(...) |
Konvertiert einen Path in
eine Region. |
| StrokeAndFillPath(...) |
Zeichnet die Umrandung des
Paths mit dem aktuellen Stift und füllt ihn mit dem aktuellen Pinsel aus. |
| StrokePath(...) |
Zeichnet die Umrandung des
Paths mit dem aktuellen Stift. |
Da ein Path nur zum Zeichnen von Figuren verwendet werden kann sind innerhalb
des Blockes BeginPath(...) und EndPath(...) nicht mehr alle Zeichenanweisungen
gültig sondern nur noch diejenigen, die auch tatsächlich etwas zeichnen. Ein Aufruf
von SetBkColor(...) zum Beispiel wird innerhalb des Blockes ignoriert und
liefert einen Fehler zurück. Welche Funktionen innerhalb des Blockes zulässig sind,
können Sie in der Online-Hilfe zu BeginPath(...) nachsehen.
Interessant ist ebenfalls die Methode SelectClipPath(...), die den Clipping-Bereich
auf den zuletzt erstellen Path setzt. Als Parameter erhält die Methode eine Konstante
vom oben aufgeführten Typ RGN_xxx der bestimmt, wie der aktuelle Clipping-Bereich
mit dem Path verknüpft wird.
 |
Kopieren Sie sich nun das Programm
05GDI\Path in Ihren Arbeitsbereich, übersetzen es dort und starten
es. Sie erhalten dann ein Ausgabe in der folgenden Form:

Sieht doch gut aus, oder? Die farbige Ellipse im Hintergrund wurde mittels
einer elliptischen Region erstellt in die dann ein Farbverlauf eingezeichnet
wurde. Der farbige Text wurde mittels eines Path-Blocks erstellt. Innerhalb
des Blocks wurde mittels einer TextOut(...) der Path definiert. Der
so erstellte Path wurde als Clipping-Bereich selektiert und dann, wie bei der
Ellipse im Hintergrund, der Farbverlauf eingezeichnet. Im Programm sind auch
einige Hinweise zum 'Spielen' mit dem Programm enthalten.
|
Zum Schluss des Kapitels GDI sollen noch die Rasteroperationen (ROPs) erwähnt
werden. ROPs haben Sie im Prinzip schon bei den BLT-Methoden kennen gelernt. Dort
wurde über ROPs die Art und Weise gesteuert, wie Bereiche kopiert werden. Ganz allgemein
ausdrückt bestimmen ROPs wie zu zeichnende Figuren mit dem bestehende Fensterinhalt
(oder einer Bitmap im Falle eines Speicher-DCs) verknüpft werden. Um die Rasteroperation
zu setzen wird die CDC-Methode SetROP2(...). aufgerufen. Der Parameter
der Methode gibt den zu verwendenden ROP-Modus an. Für häufig benötigte ROPs definiert
WINDOWS eine Reihen von Konstanten die alle den Präfix R2_xxx besitzen. Ein Aufzählung
dieser Konstanten wollen wir uns hier sparen. Sehen Sie dazu bitte in der Online-Hilfe
zur oben angegebenen Methode nach. Insgesamt kennt WINDOWS 255 (größtenteils nicht
in der Online-Hilfe enthaltene) ROPs.
 |
ROPs sind immer Bitoperationen auf alle drei RGB-Farben! Ist z.B. der Hintergrund
eines Fensters blau ausgefüllt (Farbwert RGB (0x00,0x00,x0ff)) und wird die
ROP-Operation R2_NOT (~d) ausgeführt erhält man als Ergebnis die neue Farbe
gelb (Farbwert RGB(0xff,0xff,0x00)) für den Hintergrund. |
 |
Da Sie im täglichen Leben nur selten mit ROPs zu tun haben
verzichten wir an dieser Stelle auf weitere Ausführungen zu diesem Thema. Sie
sollten sich jedoch einmal das Beispiel zu diesem Thema ansehen, das. Sie es
unter 05GDI\ROPs finden. Versuchen Sie
auch einmal andere Farbwerte für den zum Zeichnen der Linien verwendeten Stift.

|
Damit ist dieses Kapitel im Prinzip beendet. Zum Schluss können Sie sich nun
noch einige Tipps&Tricks zum Thema GDI ansehen.
|