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:

Ausgabe der Animation

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:

Animation nach Korrektur von Invalidate(...)

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.

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).

Kombinationen von Regionen

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:

Kombinierte Regionen und Clipping

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:

Farbläufe mit Pathanweisungen

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.

Rasteroperationen

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



Copyright © 2004

Senden Sie Emails mit Fragen oder Kommentaren zu dieser Website an: mailto:info@cpp-tutor.de
 Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein. Tel: +49 7129 6470