vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 2

Tag 8

Bilder, Zeichnungen und Bitmaps

Vielleicht ist Ihnen schon aufgefallen, daß viele Anwendungen Zeichnungen und Bilder anzeigen. Das verleiht einer Anwendung einen gewissen Pfiff und Schliff. Bei einigen Anwendungen sind Grafiken ein integraler Bestandteil der gebotenen Funktionalität. Wenn man unter Windows erfolgreich programmieren will, sind gute Kenntnisse unerläßlich, wie man die grafischen Fähigkeiten in einer Anwendung umsetzt. Bisher haben Sie gelernt, wie man Linien zeichnet und wie sich eine Folge von Linien zu einer geschlossenen Zeichnung verbinden läßt. Heute gehen wir über diese Möglichkeiten hinaus. Sie lernen, wie sich kompliziertere Grafikfähigkeiten in eine Anwendung aufnehmen lassen. Insbesondere beschäftigen wir uns heute damit, wie ...

Die grafische Geräteschnittstelle (GDI)

Das Betriebssystem Windows stellt eine Reihe von Abstraktionsebenen bereit, die sich auf das Erstellen und den Einsatz von Grafiken in einer Anwendung beziehen. Während der Tage der DOS-Programmierung mußten Sie sich eingehend mit der Ansteuerung der Grafikhardware beschäftigen, um Zeichenoperationen in einer Anwendung zu realisieren. Das erforderte umfangreiches Wissen und Kenntnisse der verschiedenartigsten Grafikkarten, die ein Benutzer in seinen Computer einbauen konnte, wozu noch die verschiedensten Optionen für Monitore und Bildschirmauflösungen kamen. Es gab zwar einige Grafikbibliotheken, die man für die Anwendungsprogrammierung erwerben konnte, aber alles in allem war es ziemlich mühsam, diese Fähigkeit in eine Anwendung einzubauen.

Mit Windows hat Microsoft das Ganze vereinfacht. Zuerst einmal stellt Microsoft ein virtuelles Grafikgerät für alle Windows-Anwendungen bereit. Dieses virtuelle Gerät ändert sich nicht mit der Hardware, sondern bleibt für die gesamte Palette der Grafikhardware, die man eventuell beim Benutzer vorfindet, gleich. Aufgrund dieser Einheitlichkeit lassen sich beliebige Grafikaufgaben in einer Anwendung realisieren, da man sich nicht um die Konvertierung der Ausgaben in Befehle, die die Hardware versteht, kümmern muß.

Gerätekontexte

Bevor man irgendeine Grafik erstellen kann, muß man über den Gerätekontext verfügen, in dem die Grafiken angezeigt werden. Der Gerätekontext enthält Informationen über das System, die Anwendung und das Fenster, in dem die Ausgabe der Zeichnungen und Bilder erfolgt. Das Betriebssystem entnimmt dem Gerätekontext, in welchem Kontext eine Grafik zu zeichnen ist, wie groß der sichtbare Bereich ist, und wo sich der Zeichenbereich momentan auf dem Bildschirm befindet.

Eine Grafik geben Sie immer im Kontext eines Anwendungsfensters aus. Dieses Fenster kann jederzeit als Vollbild, minimiert, teilweise überdeckt oder vollständig unsichtbar sein. Um den jeweiligen Zustand brauchen Sie sich nicht zu kümmern, weil Sie Ihre Grafiken mit Hilfe des Gerätekontextes in das Fenster zeichnen. Windows verwaltet alle Gerätekontexte und ermittelt daraus, wieviel und welcher Teil der gezeichneten Grafiken tatsächlich für den Benutzer anzuzeigen ist. Praktisch stellt der Gerätekontext, in dem Sie Ihre Grafiken zeichnen, den visuellen Kontext des Fensters dar, in dem Sie die Grafiken ausgeben.

Die meisten Funktionen in bezug auf Zeichnungen und Bilder führt der Gerätekontext mit zwei Ressourcen aus: Stift und Pinsel. Wie ihre Entsprechungen in der realen Welt führen Stifte und Pinsel zwar ähnliche, aber dennoch unterschiedliche Aufgaben aus. Der Gerätekontext verwendet Stifte, um Linien und Figuren zu zeichnen, während Pinsel die Flächen auf dem Bildschirm ausfüllen. Das entspricht völlig der Vorgehensweise, wenn man auf Papier zunächst die Umrisse eines Bildes zeichnet und dann einen Pinsel in die Hand nimmt, um die Flächen zwischen den Linien mit Farben auszufüllen.

Die Gerätekontextklasse

In Visual C++ stellt die MFC-Gerätekontextklasse (CDC) zahlreiche Zeichenfunktionen für Kreise, Quadrate, Linien, Kurven usw. bereit. Diese Funktionen gehören zur Gerätekontextklasse, da sie alle auf Informationen des Gerätekontextes zurückgreifen, um in den Anwendungsfenstern zu zeichnen.

Die Instanz einer Gerätekontextklasse erstellt man mit einem Zeiger auf die Fensterklasse, die man mit dem Gerätekontext verbinden möchte. Damit läßt sich in den Konstruktoren und Destruktoren der Gerätekontextklasse der gesamte Code unterbringen, der sich mit der Zuweisung und Freigabe eines Gerätekontextes befaßt.

Gerätekontextobjekte gehören genau wie alle Arten von Zeichenobjekten zu den Ressourcen im Betriebssystem Windows. Das Betriebssystem verfügt nur über eine begrenzte Zahl dieser Ressourcen. Obwohl die Gesamtzahl der Ressourcen in den neueren Versionen von Windows recht groß ist, kann dennoch ein Mangel an Ressourcen entstehen, wenn eine Anwendung die Ressourcen zwar reserviert, aber nicht wieder korrekt freigibt. Diesen Verlust bezeichnet man als Ressourcenlücke, die analog zu einer Speicherlücke das System des Benutzer blockieren oder lahmlegen kann. Es empfiehlt sich also, die Ressourcen in Funktionen zu erzeugen, wo sie zum Einsatz kommen, und sie sobald wie möglich wieder freizugeben, wenn die Arbeit mit den betreffenden Ressourcen abgeschlossen ist.

Dementsprechend nutzt man Gerätekontexte und deren Zeichenressourcen fast ausschließlich als lokale Variablen innerhalb einer einzigen Funktion. Die einzige echte Ausnahme liegt vor, wenn das Gerätekontextobjekt von Windows erzeugt und in eine ereignisverarbeitende Funktion als Argument übergeben wird.

Die Stiftklasse CPen

Die Stiftklasse CPen haben Sie bereits eingesetzt, um Farbe und Breite für die auf dem Bildschirm zu zeichnenden Zeichen festzulegen. CPen ist das hauptsächliche Ressourcenwerkzeug, mit dem man Linien aller Art auf dem Bildschirm darstellt. Erzeugt man eine Instanz der Klasse CPen, kann man den Typ, die Farbe und die Dicke der Linie spezifizieren. Nachdem der Stift erzeugt ist, läßt er sich als aktuelles Zeichenwerkzeug für den Gerätekontext auswählen und damit für alle Zeichenbefehle an diesen Gerätekontext verwenden. Der folgende Code erzeugt einen neuen Stift und wählt ihn dann als aktuellen Zeichenstift aus:

// Den Gerätekontext erzeugen
CDC dc(this);
// Den Stift erzeugen
CPen lPen(PS_SOLID, 1, RGB(0, 0, 0)));
// Den Stift als aktuellen Zeichenstift auswählen
dc.SelectObject(&lPen);

Es stehen mehrere Stiftstile zur Verfügung, die verschiedene Muster der gezeichneten Linien ergeben. Abbildung 8.1 zeigt die grundlegenden Stile, auf die man mit jeder Farbe in einer Zeichenanwendung zurückgreifen kann.

Abbildung 8.1:
Stiftstile in Windows

Alle diese Linienstile ergeben bei einer Stiftdicke größer als 1 durchgehende Linien. Wenn Sie einen anderen Stil als PS_SOLID verwenden möchten, müssen Sie eine Stiftbreite von 1 angeben.

Zusammen mit dem Linienstil, den der Stift zeichnen soll, kann man auch die Breite und Farbe des Stifts festlegen. Die Kombination dieser drei Variablen legt die Erscheinung der resultierenden Linien fest. Die Linienbreite kann Werte ab einschließlich 1 erhalten, obwohl sich bei einer Breite über 32 kaum noch eine vernünftige Linie zeichnen läßt.

Die Farbe ist als RGB-Wert anzugeben, der sich aus drei separaten Werten für die Helligkeit der roten, grünen und blauen Komponenten der Pixel auf dem Computerbildschirm zusammensetzt. Die einzelnen Komponenten können Werte im Bereich zwischen 0 und 255 annehmen. Die Funktion RGB faßt die Einzelwerte in einem Format zusammen, das Windows für die Ausgabe benötigt. Einige der gebräuchlicheren Farben sind in Tabelle 8.1 aufgeführt.

Tabelle 8.1: Gebräuchliche Windows-Farben

Farbe

Rot

Grün

Blau

Schwarz

0

0

0

Blau

0

0

255

Dunkelblau

0

0

128

Grün

0

255

0

Dunkelgrün

0

128

0

Cyan

0

255

255

Dunkelcyan

0

128

128

Rot

255

0

0

Dunkelrot

128

0

0

Magenta

255

0

255

Dunkelmagenta

128

0

128

Gelb

255

255

0

Dunkelgelb

128

128

0

Dunkelgrau

128

128

128

Hellgrau

192

192

192

Weiß

255

255

255

Die Pinselklasse CBrush

Mit der Pinselklasse CBrush lassen sich Pinsel erzeugen, die definieren, wie Bereiche ausgefüllt werden. Bei einer geschlossenen Figur zeichnet man den Umriß mit dem aktuellen Stift und füllt den Innenbereich mit dem aktuellen Pinsel aus. Pinsel können durchgängige Farben (mit den gleichen RGB-Werten wie die Stifte festzulegen), Linienmuster oder sogar wiederholte Muster, die sich aus einem kleinen Bitmap aufbauen, liefern. Wollen Sie einen Pinsel mit durchgehender Farbe erzeugen, geben Sie dazu die entsprechende Farbe an:

CBrush lSolidBrush(RGB(255, 0, 0));

Um einen gemusterten Pinsel zu erzeugen, müssen Sie nicht nur die Farbe festlegen, sondern auch das Muster:

CBrush lPatternBrush(HS_BSDIAGONAL, RGB(0, 0, 255));

Nachdem Sie einen Pinsel erzeugt haben, können Sie ihn mit dem Gerätekontextobjekt auswählen, genau wie es bei Stiften geschieht. Nach der Auswahl eines Pinsels dient er als aktueller Pinsel, wenn man irgendein Objekt zeichnet, das einen Pinsel verwendet.

Wie bei Stiften kann man Pinsel mit einer Reihe von Standardmustern erzeugen, die Abbildung 8.2 zeigt. Neben diesen Mustern gibt es einen zusätzlichen Pinselstil, HS_BITMAP, der ein Bitmap als Muster zum Füllen des angegebenen Bereichs verwendet. Das Bitmap ist auf die Größe von 8 mal 8 Pixel begrenzt und damit sogar kleiner, als die normalerweise für Symbolleisten und andere kleine Bilder verwendeten Bitmaps. Gibt man ein größeres Bitmap an, nimmt es nur die 8 mal 8 Punkte ab der oberen linken Ecke ein. Man kann auch einen Bitmap-Pinsel erzeugen, indem man eine Bitmap-Ressource für die Anwendung anlegt und ihr eine Objekt-ID zuweist. Danach können Sie einen Pinsel mit Hilfe des folgenden Codes erzeugen:

// Das Bild laden
m_bmpBitmap.LoadBitmap(IDB_MYBITMAP);
// Den Pinsel erzeugen
CBrush lBitmapBrush(&m_bmpBitmap);

Wenn Sie Ihre eigenen benutzerdefinierten Muster für einen Pinsel erzeugen möchten, können Sie das Muster als Bitmap der Größe 8 mal 8 Pixel erstellen und den Bitmap-Pinsel verwenden. Damit läßt sich die Zahl der Pinselmuster weit über die begrenzte Anzahl der Standardmuster erweitern.

Abbildung 8.2:
Standardpinselmuster

Die Klasse CBitmap

Es gibt verschiedene Möglichkeiten, um Bilder in einer Anwendung anzuzeigen. Feststehende Bitmaps nimmt man als Ressourcen mit zugewiesenen Objekt-IDs in die Anwendung auf und zeigt sie mit statischen Bildsteuerelementen oder ActiveX-Steuerelementen an. Man kann auch die Bitmap-Klasse CBitmap einsetzen, um die Anzeige der Bilder weitgehend beeinflussen zu können. Mit Hilfe der Bitmap-Klasse lassen sich Bitmaps dynamisch aus Dateien des Systemlaufwerks laden, die Bilder bei Bedarf in der Größe ändern und sie an den zugewiesenen Platz anpassen.

Wenn man das Bitmap als Ressource hinzufügt, erzeugt man eine Instanz der Klasse CBitmap und verwendet die Ressourcen-ID des Bitmap für das zu ladende Bild. Über die API-Funktion LoadImage läßt sich ein Bitmap aus einer Datei laden. Nachdem Sie das Bitmap geladen haben, können Sie das Bild über dessen Kennummer (Handle) mit der Klasse CBitmap verbinden, wie es der folgende Beispielcode zeigt:

// Die Bitmap-Datei laden
HBITMAP hBitmap = (HBITMAP)::LoadImage(AfxGetInstanceHandle(),
m_sFileName, IMAGE_BITMAP, 0, 0,
LR_LOADFROMFILE | LR_CREATEDIBSECTION);
// Das geladene Bild dem CBitmap-Objekt zuweisen
m_bmpBitmap.Attach(hBitmap);

Nachdem Sie das Bitmap in das CBitmap-Objekt geladen haben, können Sie einen zweiten Gerätekontext erzeugen und das Bitmap in diesen Gerätekontext selektieren. Wenn Sie den zweiten Gerätekontext erzeugt haben, müssen Sie ihn mit dem ersten Gerätekontext kompatibel machen, bevor das Bitmap in ihn selektiert wird. Da Gerätekontexte durch das Betriebssystem für ein bestimmtes Ausgabegerät (Bildschirm, Drucker usw.) erzeugt werden, müssen Sie sicherstellen, daß der zweite Gerätekontext ebenfalls mit demselben Ausgabegerät wie der erste verbunden ist.

// Einen Gerätekontext erzeugen
CDC dcMem;
// Neuen Gerätekontext zum originalen DC kompatibel machen
dcMem.CreateCompatibleDC(dc);
// Bitmap in den neuen DC selektieren
dcMem.SelectObject(&m_bmpBitmap);

Wenn Sie das Bitmap in einen kompatiblen Gerätekontext selektieren, können Sie das Bitmap in den normalen Anzeigegerätekontext mit Hilfe der Funktion BitBlt kopieren:

// Das Bitmap in den Anzeige-DC kopieren
dc->BitBlt(10, 10, bm.bmWidth,
bm.bmHeight, &dcMem, 0, 0,
SRCCOPY);

Sie können das Bild auch mittels der Funktion StretchBlt kopieren und in der Größe ändern:

// Bitmap in den Anzeige-DC kopieren und dabei
// in der Größe ändern
dc->StretchBlt(10, 10, (lRect.Width() - 20),
(lRect.Height() - 20), &dcMem, 0, 0,
bm.bmWidth, bm.bmHeight, SRCCOPY);

Mit der Funktion StretchBlt läßt sich das Bild an jeden beliebigen Bereich auf dem Bildschirm anpassen.

Abbildungsmodi und Koordinatensysteme

Wenn Sie die Grafikausgabe in ein Fenster vorbereiten, können Sie die Skalierung und den Zielbereich in weiten Grenzen festlegen. Diese Faktoren lassen sich über den Abbildungsmodus und den Zeichenbereich spezifizieren.

Mit dem Abbildungsmodus bestimmen Sie, wie die spezifizierten Koordinaten in Positionen auf dem Bildschirm zu übersetzen sind. Für die einzelnen Abbildungsmodi sind dabei verschiedene Maßeinheiten definiert. Die Abbildungsmodi legen Sie mit der Gerätekontextfunktion SetMapMode fest:

dc->SetMapMode(MM_ANSIOTROPIC);

Die verfügbaren Abbildungsmodi sind in Tabelle 8.2 aufgeführt.

Tabelle 8.2: Abbildungsmodi

Modus

Beschreibung

MM_ANISOTROPIC

Logische Einheiten werden auf beliebige Einheiten mit frei skalierbaren Achsen abgebildet.

MM_HIENGLISH

Eine logische Einheit wird auf 0.001 Zoll abgebildet. Positive x-Werte gehen nach rechts, positive y-Werte nach oben.

MM_HIMETRIC

Eine logische Einheit wird auf 0.01 Millimeter abgebildet. Positive x-Werte gehen nach rechts, positive y-Werte nach oben.

MM_ISOTROPIC

Logische Einheiten werden auf beliebige Einheiten mit gleich skalierten Achsen abgebildet.

MM_LOENGLISH

Eine logische Einheit wird auf 0.01 Zoll abgebildet. Positive x-Werte gehen nach rechts, positive y-Werte nach oben.

MM_LOMETRIC

Eine logische Einheit wird auf 0.1 Millimeter abgebildet. Positive x-Werte gehen nach rechts, positive y-Werte nach oben.

MM_TEXT

Eine logische Einheit wird auf 1 Pixel abgebildet. Positive x gehen nach rechts, positive y nach unten.

MM_TWIPS

Eine logische Einheit wird auf 1/20 Punkt (etwa 1/1440 Zoll, auch als »Twip« bezeichnet) abgebildet. Positive x laufen nach rechts, positive y nach oben.

Wenn Sie die Abbildungsmodi MM_ANISOTROPIC oder MM_ISOTROPIC verwenden, können Sie den Zielbereich für die Grafik entweder mit der Funktion SetWindowExt oder mit SetViewportExt festlegen.

Eine Grafikanwendung erstellen

Um einen Einblick von den genannten Möglichkeiten zu bekommen, erstellen Sie heute eine Beispielanwendung, die einen Großteil des bisherigen Stoffes in der Praxis umsetzt. Die Anwendung verfügt über zwei unabhängige Fenster. Das erste enthält eine Anzahl von Optionen, über die man die Figur, das Werkzeug und die Farbe zur Anzeige auswählen kann. Das andere Fenster dient als Leinwand, wo alle ausgewählten Optionen gezeichnet werden. Der Benutzer kann wählen, ob er Linien, Quadrate, Kreise oder ein Bitmap in das zweite Fenster zeichnen will. Außerdem kann er die Farbe spezifizieren und wählen, ob der Stift oder der Pinsel für die Kreise und Quadrate anzuzeigen ist.

Das Anwendungsgerüst erstellen

Wie Sie bisher gelernt haben, erstellen Sie im ersten Schritt das Gerüst einer Anwendung mit der grundlegenden Funktionalität. Es zeigt das erste Dialogfeld der Anwendung an und verfügt über den gesamten Code, der erforderlich ist, um die Anwendung zu starten und zu beenden.

Für die heutige Beispielanwendung beginnen Sie mit dem Gerüst einer dialogfeldbasierenden Anwendung. Dazu legen Sie mit dem Anwendungs-Assistenten ein neues Projekt an, dem Sie einen passenden Namen wie etwa Grafik geben. Im ersten Schritt des Anwendungs-Assistenten legen Sie fest, daß Sie eine dialogfeldbasierende Anwendung erstellen wollen. Prinzipiell können Sie dann alle weiteren Standardeinstellungen übernehmen, obwohl die Unterstützung für ActiveX-Steuerelemente nicht erforderlich ist. Außerdem können Sie bei Bedarf einen anderen Titel für das Anwendungsfenster eingeben.

Das Hauptdialogfeld entwerfen

Nachdem Sie sich durch den Anwendungs-Assistenten gearbeitet haben, können Sie an den Entwurf des Hauptdialogfelds gehen. Dieses Fenster enthält drei Gruppen von Optionsfeldern: eine Gruppe zur Auswahl des Zeichenwerkzeugs, die nächste zur Festlegung der Zeichenfigur und das dritte zur Auswahl der Farbe. Neben diesen Gruppen von Optionsfeldern plazieren Sie noch zwei Schaltflächen im Fenster: über die eine Schaltfläche rufen Sie das Dialogfeld Öffnen auf, um ein anzuzeigendes Bitmap auszuwählen, und mit der zweiten Schaltfläche schließen Sie die Anwendung.

Die Steuerelemente können Sie gemäß Abbildung 8.3 anordnen. Tabelle 8.3 listet die zugehörigen Eigenschaften der Steuerelemente auf.

Tabelle 8.3: Eigenschaftseinstellungen der Steuerelemente

Objekt

Eigenschaft

Einstellung

Gruppenfeld

ID

Titel

IDC_STATIC

Werkzeug

Optionsfeld

ID

Titel

Gruppe

IDC_RTPEN

&Stift

Eingeschaltet

Optionsfeld

ID

Titel

IDC_RTBRUSH

&Pinsel

Optionsfeld

ID

Titel

IDC_RTBITMAP

Bit&map

Gruppenfeld

ID

Titel

IDC_STATIC

Figur

Optionsfeld

ID

Titel

Gruppe

IDC_RSLINE

&Linie

Eingeschaltet

Optionsfeld

ID

Titel

IDC_RSCIRCLE

&Kreis

Optionsfeld

ID

Titel

IDC_RSSQUARE

&Quadrat

Gruppenfeld

ID

Titel

IDC_STATIC

Farbe

Optionsfeld

ID

Titel

Gruppe

IDC_RCBLACK

Schwar&z

Eingeschaltet

Optionsfeld

ID

Titel

IDC_RCBLUE

Bla&u

Optionsfeld

ID

Titel

IDC_RCGREEN

Gr&ün

Optionsfeld

ID

Titel

IDC_RCCYAN

&Cyan

Optionsfeld

ID

Titel

IDC_RCRED

&Rot

Optionsfeld

ID

Titel

IDC_RCMAGENTA

M&agenta

Optionsfeld

ID

Titel

IDC_RCYELLOW

&Gelb

Optionsfeld

ID

Titel

IDC_RCWHITE

&Weiß

Schaltfläche

ID

Titel

IDC_BBITMAP

B&itmap

Schaltfläche

ID

Titel

IDC_BEXIT

&Beenden

Abbildung 8.3:
Das Layout des Hauptdialogfelds

Nachdem Sie das Hauptdialogfeld entworfen haben, weisen Sie jeder Gruppe der Optionsfelder jeweils eine Variable zu. Öffnen Sie dazu den Klassen-Assistenten, und weisen Sie eine Integer-Variable an jede der drei Objekt-IDs der Optionsfelder zu. Beachten Sie dabei, daß nur die Objekt-IDs für die Optionsfelder, bei denen die Option Gruppe eingeschaltet ist, im Klassen-Assistent erscheinen. Alle darauffolgenden Optionsfelder bekommen dieselbe Variable zugewiesen und erhalten aufeinanderfolgende Werte in der Reihenfolge der Objekt-IDs. Aus diesem Grund ist es wichtig, alle Optionsfelder der einzelnen Gruppen in der Reihenfolge zu erstellen, in der die Werte aufeinanderfolgen sollen.

Um die erforderlichen Variablen an die Gruppen der Optionsfelder der Beispielanwendung zuzuweisen, öffnen Sie den Klassen-Assistenten und fügen die Variablen gemäß Tabelle 8.4 für die Objekte des Dialogfelds hinzu.

Tabelle 8.4: Variablen der Steuerelemente

Objekt

Name

Kategorie

Typ

IDC_RTPEN

m_iTool

Wert

int

IDC_RSLINE

m_iShape

Wert

int

IDC_RCBLACK

m_iColor

Wert

int

Da der Klassen-Assistent geöffnet ist, können Sie gleich zur ersten Registerkarte wechseln und eine Behandlungsroutine für die Schaltfläche Beenden hinzufügen. Im Code für die Schaltfläche rufen Sie wie gewohnt die Funktion OnOK auf. Jetzt können Sie Ihre Anwendung kompilieren und ausführen. Überzeugen Sie sich davon, daß Sie alle Gruppen der Optionsfelder korrekt definiert haben, daß Sie nur jeweils ein Optionsfeld in einer Gruppe auswählen können und daß Sie eine Option in der einen Gruppe wählen können, ohne eine der anderen Gruppen zu beeinflussen.

Das zweite Dialogfeld hinzufügen

Nachdem Sie das Hauptdialogfeld erstellt haben, fügen Sie das zweite Fenster hinzu, auf dem Sie in der Art einer Leinwand Ihre Grafiken zeichnen. Es handelt sich um ein nichtmodales Dialogfeld, das die ganze Zeit, während die Anwendung läuft, geöffnet bleibt. In dieses Dialogfeld nehmen Sie keine Steuerelemente auf und stellen damit eine vollkommen leere Leinwand zum Zeichnen bereit.

Um das zweite Dialogfeld zu erstellen, gehen Sie im Arbeitsbereich auf die Registerkarte Ressourcen. Klicken Sie im Ressourcenbaum mit der rechten Maustaste auf den Ordner Dialog. Wählen Sie aus dem Kontextmenü den Befehl Dialog einfügen. Wenn das neue Dialogfeld im Dialog-Editor geöffnet wird, entfernen Sie alle Steuerelemente aus dem Fenster. Anschließend öffnen Sie das Dialogfeld Eigenschaften für das Fenster und schalten das Kontrollkästchen Systemmenü auf der zweiten Registerkarte des Eigenschaftsdialogfelds aus. Damit verhindert man, daß der Benutzer das Dialogfeld schließt, ohne die Anwendung zu beenden. Weiterhin geben Sie diesem Dialogfeld eine Objekt-ID, die seine Funktion beschreibt, beispielsweise IDD_PAINT_DLG.

Haben Sie den Entwurf des zweiten Dialogfelds abgeschlossen, erstellen Sie für dieses Fenster mit dem Klassen-Assistenten eine neue Klasse. Wenn Sie den Klassen-Assistenten öffnen, erscheint eine Frage, ob Sie eine neue Klasse für das zweite Dialogfeld erstellen möchten. Übernehmen Sie die Standardeinstellung, und klicken Sie auf die Schaltfläche OK. Im nächsten Dialogfeld geben Sie den Namen der neuen Klasse ein, beispielsweise CPaintDlg. Stellen Sie sicher, daß die Basisklasse auf CDialog gesetzt ist. Nachdem Sie in diesem Dialogfeld auf OK geklickt und die neue Klasse erstellt haben, können Sie den Klassen-Assistenten schließen.

Das neue Dialogfeld muß markiert sein, wenn Sie den Klassen-Assistenten öffnen. Wenn das Dialogfeld nicht markiert ist und Sie zu einem anderen Objekt oder sogar zu Code in Ihrer Anwendung gewechselt haben, weiß der Klassen-Assistent nicht, daß Sie eine Klasse für das zweite Dialogfeld in Ihrer Anwendung benötigen.

Für das erste Dialogfeld fügen Sie nun Code hinzu, um das zweite Dialogfeld zu öffnen. Das erreichen Sie mit zwei Codezeilen in der Funktion OnInitDialog in der Klasse des ersten Fensters. Zuerst erstellen Sie das Dialogfeld mit der Methode Create der Klasse CDialog. Diese Funktion übernimmt zwei Argumente: die Objekt-ID des Dialogfelds und einen Zeiger auf das übergeordnete Fenster, bei dem es sich um das Hauptdialogfeld handelt. Die zweite Funktion ist ShowWindow, der Sie den Wert SW_SHOW als einziges Argument übergeben. Diese Funktion zeigt das zweite Dialogfeld neben dem ersten an. Schließlich kommen noch einige Zeilen hinzu, um die Variablen zu initialisieren. Die Funktion OnInitDialog sollte dann Listing 8.1 entsprechen.

Listing 8.1: Die Funktion OnInitDialog

1: BOOL CGrafikDlg::OnInitDialog()
2: {
3: CDialog::OnInitDialog();
4:
.
.
.
27:
28: // ZU ERLEDIGEN: Hier zusätzliche Initialisierung einfügen
29:
30: ///////////////////////
31: // EIGENER CODE, ANFANG
32: ///////////////////////
33:
34: // Variablen initialisieren und Dialogfeld aktualisieren
35: m_iColor = 0;
36: m_iShape = 0;
37: m_iTool = 0;
38: UpdateData(FALSE);
39:
40: // Das zweite Dialogfeld erzeugen
41: m_dlgPaint.Create(IDD_PAINT_DLG, this);
42: // Zweites Dialogfeld anzeigen
43: m_dlgPaint.ShowWindow(SW_SHOW);
44:
45: ///////////////////////
46: // EIGENER CODE, ENDE
47: ///////////////////////
48:
49: return TRUE; // Geben Sie TRUE zurück, außer ein Steuerelement soll den ÂFokus erhalten
50: }

Bevor Sie die Anwendung kompilieren und ausführen können, müssen Sie die Header-Datei für die zweite Dialogfeldklasse in den Quellcode des ersten Dialogfelds einbinden. Weiterhin ist die zweite Dialogfeldklasse als Variable in die erste aufzunehmen. Zu diesem Zweck müssen Sie lediglich eine Member-Variable in die erste Dialogfeldklasse hinzufügen, wobei Sie als Variablentyp den Klassentyp - in diesem Fall CPaintDlg - spezifizieren, den in Listing 8.1 verwendeten Variablennamen m_dlgPaint angeben und den Variablenzugriff als Privat festlegen. Um die Header- Datei in das erste Dialogfeld einzubinden, gehen Sie an den Beginn des Quellcodes für das erste Dialogfeld und fügen eine #include-Anweisung wie in Listing 8.2 hinzu.

Listing 8.2: Die #include-Anweisung des Hauptdialogs

1: // GrafikDlg.cpp : Implementierungsdatei
2: //
3:
4: #include "stdafx.h"
5: #include "Grafik.h"
6: #include "PaintDlg.h"
7: #include "GrafikDlg.h"
8:

Umgekehrt müssen Sie die Header-Datei für das Hauptdialogfeld in den Quellcode für das zweite Dialogfeld einbinden. Bearbeiten Sie die Datei PaintDlg.cpp, so daß die #include-Anweisungen denen in Listing 8.2 entsprechen.

Wenn Sie Ihre Anwendung kompilieren und ausführen, sollte das zweite Dialogfeld zusammen mit dem ersten geöffnet werden. Schließt man das erste Dialogfeld und beendet damit die Anwendung, wird das zweite Dialogfeld ebenfalls geschlossen, selbst wenn Sie bisher keinerlei Code hinzugefügt haben, der das bewirkt. Das zweite Dialogfeld ist ein untergeordnetes Fenster zum ersten Dialogfeld. Wenn Sie das zweite Dialogfeld - in Zeile 41 des Listings - erstellen, übergeben Sie einen Zeiger auf das erste Dialogfeld als übergeordnetes Fenster für das zweite Fenster. Damit wird eine Beziehung zwischen übergeordnetem und untergeordnetem Fenster eingerichtet. Wird das übergeordnete Fenster geschlossen, schließt auch das untergeordnete. Es handelt sich hier um die gleiche Beziehung, die das erste Dialogfeld mit allen darauf plazierten Steuerelementen hat. Jedes dieser Steuerelemente ist ein untergeordnetes Fenster des Dialogfelds. Praktisch haben Sie das zweite Dialogfeld zu einem weiteren Steuerelement des ersten Dialogfelds gemacht.

Grafikfunktionen realisieren

Da alle Variablen für die Optionsfelder als öffentlich (public) deklariert sind, kann sie das zweite Dialogfeld sehen und referenzieren. Die gesamte Funktionalität zum Zeichnen läßt sich in der zweiten Dialogfeldklasse unterbringen. Allerdings müssen Sie einen Teil der Funktionen im ersten Dialogfeld realisieren, um die Variablen zu synchronisieren und das zweite Dialogfeld anzuweisen, die Grafiken zu zeichnen. Das Ganze ist aber einfacher, als Sie vielleicht annehmen.

Sobald ein Fenster neu zu zeichnen ist (etwa weil es hinter einem anderen Fenster verborgen war und nun in den Vordergrund kommt, oder weil es minimiert war, oder weil es außerhalb des sichtbaren Bereichs gelegen hat), löst das Betriebssystem die Funktion OnPaint des Dialogfelds aus. Die gesamte Funktionalität zum Zeichnen der Grafiken können Sie in dieser Funktion unterbringen und die angezeigten Grafiken dauerhaft machen.

Es ist nun klar, wo der Code für die Anzeige der Grafiken hingehört. Wie kann man aber das zweite Dialogfeld veranlassen, seine OnPaint-Funktion aufzurufen, wenn der Benutzer eine der Auswahlen im ersten Dialogfeld verändert? Nun, man kann das zweite Dialogfeld verbergen und dann wieder anzeigen, aber das dürfte dem Benutzer etwas seltsam vorkommen. Eigentlich »überzeugt« eine einzige Funktion das zweite Fenster, daß es sein gesamtes Dialogfeld neu zu zeichnen hat. Diese Funktion, Invalidate , erfordert keine Argumente und ist eine Elementfunktion der Klasse CWnd, so daß man sie für jedes Fenster oder Steuerelement einsetzen kann. Die Funktion Invalidate teilt dem Fenster - und dem Betriebssystem - mit, daß der Anzeigebereich des Fensters nicht mehr gültig und neu zu zeichnen ist. Man kann die Funktion OnPaint im zweiten Dialogfeld nach Belieben aufrufen, ohne daß man auf irgendwelche ausgefallenen Tricks oder Hacks zurückgreifen müßte.

Wie Sie mittlerweile wissen, können alle Optionsfelder die gleiche Funktionalität für ihre Klickereignisse verwenden. Somit genügt eine einzige Behandlungsroutine für das Klickereignis aller Optionsfelder. In dieser Behandlungsfunktion synchronisieren Sie die Klassenvariablen mit den Steuerelementen des Dialogfelds durch Aufruf der Funktion UpdateData und weisen dann das zweite Dialogfeld mit Hilfe seiner Funktion Invalidate an, sich selbst neu zu zeichnen. Mit dem Code in Listing 8.3 erstellen Sie eine einzige Behandlungsroutine, die diese beiden Dinge erledigt.

Listing 8.3: Die Funktion OnRSelection

1: void CGrafikDlg::OnRSelection()
2: {
3: // TODO: Code für die Behandlungsroutine der Steuerelement- ÂBenachrichtigung hier einfügen
4:
5: // Daten synchronisieren
6: UpdateData(TRUE);
7: // Zweites Dialogfeld neu zeichnen
8: m_dlgPaint.Invalidate();
9: }

Linien zeichnen

Wenn Sie die Anwendung jetzt kompilieren und ausführen, zeichnet sich das zweite Dialogfeld neu, wenn Sie ein anderes Optionsfeld im Hauptdialogfeld wählen. Davon bemerken Sie allerdings gar nichts. Sie haben zwar das Neuzeichnen initiiert, aber dem zweiten Dialogfeld noch nicht mitgeteilt, was zu zeichnen ist. Das erledigen wir nun im nächsten Schritt für die Beispielanwendung.

Am einfachsten lassen sich Linien in verschiedenen Stilen im zweiten Dialogfeld zeichnen, weil Sie bereits etwas Erfahrung damit haben. Für die einzelnen Stiftstile ist jeweils ein Stift zu erzeugen, der die momentan ausgewählte Farbe verwendet. Nachdem Sie alle Stifte erstellt haben, gehen Sie in einer Schleife durch die einzelnen Stifte, selektieren sie nacheinander und zeichnen mit dem jeweiligen Stift eine Linie über das Dialogfeld. Bevor Sie diese Schleife starten können, müssen Sie ein paar Berechnungen anstellen, um die Lage der Linien - die Anfangs- und Endpunkte - im Dialogfeld zu bestimmen.

Zunächst erstellen Sie eine Farbtabelle, die je einen Eintrag für die Optionen des Gruppenfelds Farben im ersten Dialogfeld hat. Dazu fügen Sie dem zweiten Dialogfeld (CPaintDlg) eine Elementvariable hinzu. Legen Sie den Variablentyp als static const COLORREF, den Namen als m_crColors[8] und den Zugriff als Public fest. Öffnen Sie den Quellcode der Datei für die zweite Dialogfeldklasse, und fügen Sie die Farbtabelle aus Listing 8.4 am Beginn der Datei vor dem Konstruktor und Destruktor der Klasse ein.

Listing 8.4: Die Farbtabelle

1: const COLORREF CPaintDlg::m_crColors[8] = {
2: RGB( 0, 0, 0), // Schwarz
3: RGB( 0, 0, 255), // Blau
4: RGB( 0, 255, 0), // Grün
5: RGB( 0, 255, 255), // Cyan
6: RGB( 255, 0, 0), // Rot
7: RGB( 255, 0, 255), // Magenta
8: RGB( 255, 255, 0), // Gelb
9: RGB( 255, 255, 255) // Weiß
10: };
11: /////////////////////////////////////////////////////////////////////////////
12: // Dialogfeld CPaintDlg
.
.
.

Die Farbtabelle befindet sich nun am richtigen Platz, und Sie können eine neue Funktion hinzufügen, um die Linien zu zeichnen. Um die Funktion OnPaint nicht übermäßig kompliziert und unüberschaubar zu machen, ist es sinnvoller, nur einen kleinen Teil des Codes in dieser Funktion unterzubringen. Mit diesem Code ermittelt man lediglich, was im zweiten Dialogfeld zu zeichnen ist, und ruft dann spezialisierte Funktionen auf, die die verschiedenartigen Figuren zeichnen. In diesem Sinne fügen Sie eine neue Member-Funktion für die zweite Dialogfeldklasse zum Zeichnen von Linien hinzu. Legen Sie den Typ der Funktion als void, die Deklaration als DrawLine(CPaintDC *pdc, int iColor) und den Zugriff als Privat fest. Schreiben Sie den Code aus Listing 8.5 in diese Funktion.

Listing 8.5: Die Funktion DrawLine

1: void CPaintDlg::DrawLine(CPaintDC *pdc, int iColor)
2: {
3: // Stifte deklarieren und erzeugen
4: CPen lSolidPen (PS_SOLID, 1, m_crColors[iColor]);
5: CPen lDotPen (PS_DOT, 1, m_crColors[iColor]);
6: CPen lDashPen (PS_DASH, 1, m_crColors[iColor]);
7: CPen lDashDotPen (PS_DASHDOT, 1, m_crColors[iColor]);
8: CPen lDashDotDotPen (PS_DASHDOTDOT, 1, m_crColors[iColor]);
9: CPen lNullPen (PS_NULL, 1, m_crColors[iColor]);
10: CPen lInsidePen (PS_INSIDEFRAME, 1, m_crColors[iColor]);
11:
12: // Zeichenbereich ermitteln
13: CRect lRect;
14: GetClientRect(lRect);
15: lRect.NormalizeRect();
16:
17: // Abstand zwischen den Linien berechnen
18: CPoint pStart;
19: CPoint pEnd;
20: int liDist = lRect.Height() / 8;
21: CPen *lOldPen;
22: // Anfangspunkte festlegen
23: pStart.y = lRect.top;
24: pStart.x = lRect.left;
25: pEnd.y = pStart.y;
26: pEnd.x = lRect.right;
27: int i;
28: // Schleife durch die verschiedenen Stifte
29: for (i = 0; i < 7; i++)
30: {
31: // Bei welchem Stift sind wir?
32: switch (i)
33: {
34: case 0: // durchgehend
35: lOldPen = pdc->SelectObject(&lSolidPen);
36: break;
37: case 1: // Punkte
38: pdc->SelectObject(&lDotPen);
39: break;
40: case 2: // Striche
41: pdc->SelectObject(&lDashPen);
42: break;
43: case 3: // Strichpunkt
44: pdc->SelectObject(&lDashDotPen);
45: break;
46: case 4: // Strich Punkt Punkt
47: pdc->SelectObject(&lDashDotDotPen);
48: break;
49: case 5: // Unsichtbar
50: pdc->SelectObject(&lNullPen);
51: break;
52: case 6: // Innenseite
53: pdc->SelectObject(&lInsidePen);
54: break;
55: }
56: // Nach unten zur nächsten Position
57: pStart.y = pStart.y + liDist;
58: pEnd.y = pStart.y;
59: // Linie zeichnen
60: pdc->MoveTo(pStart);
61: pdc->LineTo(pEnd);
62: }
63: // Originalstift selektieren
64: pdc->SelectObject(lOldPen);
65: }

Jetzt brauchen Sie noch die Funktion OnPaint, die die Funktion DrawLine im Bedarfsfall aufruft. Fügen Sie die Funktion OnPaint über den Klassen-Assistenten als Behandlungsfunktion für die Nachricht WM_PAINT hinzu. Es fällt auf, daß der generierte Code für diese Funktion eine CPaintDC-Variable statt der normalen CDC-Klasse erstellt hat. Die Klasse CPaintDC ist von der Gerätekontextklasse CDC abgeleitet. Sie ruft automatisch die API-Funktionen BeginPaint und EndPaint auf, die alle Windows-Anwendungen vor dem Zeichnen während der Verarbeitung der WM_PAINT-Nachricht aufrufen müssen. Man kann sie genau wie ein normales Gerätekontextobjekt behandeln und auch die gleichen Funktionen aufrufen.

In der Funktion OnPaint ist ein Zeiger auf das übergeordnete Fenster zu ermitteln, damit man die Werte der Variablen überprüfen kann, die mit den Gruppen von Optionsfeldern für die Festlegung von Farbe, Werkzeug und der im zweiten Dialogfeld zu zeichnenden Figur verbunden sind. Aus diesen Informationen leiten Sie ab, ob die Funktion DrawLine oder eine andere Funktion, die Sie noch schreiben, aufzurufen ist.

Um diese Funktionalität in die Anwendung aufzunehmen, fügen Sie eine Behandlungsroutine für die Nachricht WM_PAINT in die zweite Dialogfeldklasse ein und schreiben den Code aus Listing 8.6 in diese Funktion.

Listing 8.6: Die Funktion OnPaint

1: void CPaintDlg::OnPaint()
2: {
3: CPaintDC dc(this); // device context for painting
4:
5: // TODO: Code für die Behandlungsroutine für Nachrichten hier einfügen
6:
7: // Zeiger auf übergeordnetes Fenster ermitteln
8: CGrafikDlg *pWnd = (CGrafikDlg*)GetParent();
9: // Ist der Zeiger gültig?
10: if (pWnd)
11: {
12: // Bitmap als Werkzeug gewählt?
13: if (pWnd->m_iTool == 2)
14: {
15: }
16: else // Nein, Figur zeichnen
17: {
18: // Wird Linie gezeichnet?
19: if (pWnd->m_iShape == 0)
20: DrawLine(&dc, pWnd->m_iColor);
21: }
22: }
23: // Kein Aufruf von CDialog::OnPaint() für Zeichnungsnachrichten
24:}

Wenn Sie die Anwendung jetzt kompilieren und ausführen, sollten Sie Linien über das zweite Dialogfeld zeichnen können, wie es Abbildung 8.4 demonstriert.

Abbildung 8.4:
Linien im zweiten Dialogfeld zeichnen

Kreise und Quadrate zeichnen

Die grundlegenden Strukturen sind nun realisiert, und Sie wissen, wie man die Ausgaben im zweiten Dialogfeld nach Belieben ändern kann. Jetzt können Sie den Code in das zweite Dialogfeld aufnehmen, um die Kreise und Quadrate zu zeichnen. Für diese Figuren kommen die Gerätekontextfunktionen Ellipse und Rectangle zum Einsatz. Diese Funktionen verwenden den momentan ausgewählten Stift und Pinsel, um die Figuren an der angegebenen Position zu zeichnen. Bei beiden Funktionen kann man ein CRect-Objekt übergeben, um das Rechteck zu definieren, in dem die spezifizierte Figur darzustellen ist. Die Funktion Rectangle füllt den gesamten angegebenen Bereich, während die Funktion Ellipse einen Kreis oder eine Ellipse zeichnet, wobei die Mittelpunkte der Rechteckseiten vom Umfang der Ellipse berührt werden.

Da diese Funktionen sowohl den Stift als auch den Pinsel verwenden, müssen Sie einen unsichtbaren Stift bzw. einen unsichtbaren Pinsel erzeugen und selektieren, um dem Benutzer zu ermöglichen, entweder den Stift oder den Pinsel zu wählen. Beim Stift können Sie zu diesem Zweck auf den Null-Stift zurückgreifen, während Sie für den Pinsel einen durchgehenden Pinsel in der Hintergrundfarbe des Fensters (Hellgrau) erzeugen müssen.

Die Positionen für die Figuren berechnen Sie nach einem anderen Verfahren als für die Linien. Bei den Linien konnte man einfach die Höhe des Fensters durch 8 teilen und dann eine Linie in jedem Abschnitt vom linken Rand zum rechten ziehen. Bei Ellipsen und Rechtecken teilen Sie das Dialogfenster in 8 gleich große Rechtecke. Am einfachsten läßt sich das erreichen, indem man zwei Reihen mit je vier Figuren erzeugt. Zwischen jeder Figur bleibt etwas Platz, damit der Benutzer die verschiedenen Stifte für die Umrisse der einzelnen Figuren erkennen kann.

Um diese Funktionalität in die Beispielanwendung einzubauen, fügen Sie für die zweite Dialogfeldklasse eine neue Funktion hinzu. Legen Sie den Funktionstyp als void, die Deklaration als DrawRegion(CPaintDC *pdc, int iColor, int iTool, int iShape) und den Zugriff als Privat fest. In die Funktion schreiben Sie den Code aus Listing 8.7.

Listing 8.7: Die Funktion DrawRegion

1: void CPaintDlg::DrawRegion(CPaintDC *pdc, int iColor, int iTool, int iShape)
2: {
3: // Stifte deklarieren und erzeugen
4: CPen lSolidPen (PS_SOLID, 1, m_crColors[iColor]);
5: CPen lDotPen (PS_DOT, 1, m_crColors[iColor]);
6: CPen lDashPen (PS_DASH, 1, m_crColors[iColor]);
7: CPen lDashDotPen (PS_DASHDOT, 1, m_crColors[iColor]);
8: CPen lDashDotDotPen (PS_DASHDOTDOT, 1, m_crColors[iColor]);
9: CPen lNullPen (PS_NULL, 1, m_crColors[iColor]);
10: CPen lInsidePen (PS_INSIDEFRAME, 1, m_crColors[iColor]);
11:
12: // Pinsel deklarieren und erzeugen
13: CBrush lSolidBrush(m_crColors[iColor]);
14: CBrush lBDiagBrush(HS_BDIAGONAL, m_crColors[iColor]);
15: CBrush lCrossBrush(HS_CROSS, m_crColors[iColor]);
16: CBrush lDiagCrossBrush(HS_DIAGCROSS, m_crColors[iColor]);
17: CBrush lFDiagBrush(HS_FDIAGONAL, m_crColors[iColor]);
18: CBrush lHorizBrush(HS_HORIZONTAL, m_crColors[iColor]);
19: CBrush lVertBrush(HS_VERTICAL, m_crColors[iColor]);
20: CBrush lNullBrush(RGB(192, 192, 192));
21:
22: // Größe der Zeichenbereiche berechnen
23: CRect lRect;
24: GetClientRect(lRect);
25: lRect.NormalizeRect();
26: int liVert = lRect.Height() / 2;
27: int liHeight = liVert - 10;
28: int liHorz = lRect.Width() / 4;
29: int liWidth = liHorz - 10;
30: CRect lDrawRect;
31: CPen *lOldPen;
32: CBrush *lOldBrush;
33: int i;
34: // Schleife durch alle Pinsel und Stifte
35: for (i = 0; i < 7; i++)
36: {
37: switch (i)
38: {
39: case 0: // Durchgehend
40: // Position für diese Figur bestimmen.
41: // Die erste Reihe beginnen
42: lDrawRect.top = lRect.top + 5;
43: lDrawRect.left = lRect.left + 5;
44: lDrawRect.bottom = lDrawRect.top + liHeight;
45: lDrawRect.right = lDrawRect.left + liWidth;
46: // Passenden Stift und Pinsel auswählen
47: lOldPen = pdc->SelectObject(&lSolidPen);
48: lOldBrush = pdc->SelectObject(&lSolidBrush);
49: break;
50: case 1: // Punkt - Diagonal von rechts oben nach links unten
51: // Position für diese Figur bestimmen.
52: lDrawRect.left = lDrawRect.left + liHorz;
53: lDrawRect.right = lDrawRect.left + liWidth;
54: // Passenden Stift und Pinsel auswählen
55: pdc->SelectObject(&lDotPen);
56: pdc->SelectObject(&lBDiagBrush);
57: break;
58: case 2: // Strich - Pinsel kreuzweise
59: // Position für diese Figur bestimmen.
60: lDrawRect.left = lDrawRect.left + liHorz;
61: lDrawRect.right = lDrawRect.left + liWidth;
62: // Passenden Stift und Pinsel auswählen
63: pdc->SelectObject(&lDashPen);
64: pdc->SelectObject(&lCrossBrush);
65: break;
66: case 3: // Strichpunkt - Diagonal kreuzweise
67: // Position für diese Figur bestimmen.
68: lDrawRect.left = lDrawRect.left + liHorz;
69: lDrawRect.right = lDrawRect.left + liWidth;
70: // Passenden Stift und Pinsel auswählen
71: pdc->SelectObject(&lDashDotPen);
72: pdc->SelectObject(&lDiagCrossBrush);
73: break;
74: case 4: // Strich Punkt Punkt - Diagonal von links oben nach rechts Âunten
75: // Position für diese Figur bestimmen.
76: // Zweite Reihe beginnen
77: lDrawRect.top = lDrawRect.top + liVert;
78: lDrawRect.left = lRect.left + 5;
79: lDrawRect.bottom = lDrawRect.top + liHeight;
80: lDrawRect.right = lDrawRect.left + liWidth;
81: // Passenden Stift und Pinsel auswählen
82: pdc->SelectObject(&lDashDotDotPen);
83: pdc->SelectObject(&lFDiagBrush);
84: break;
85: case 5: // Null - Horizontal
86: // Position für diese Figur bestimmen.
87: lDrawRect.left = lDrawRect.left + liHorz;
88: lDrawRect.right = lDrawRect.left + liWidth;
89: // Passenden Stift und Pinsel auswählen
90: pdc->SelectObject(&lNullPen);
91: pdc->SelectObject(&lHorizBrush);
92: break;
93: case 6: // Innenseite - Vertikal
94: // Position für diese Figur bestimmen.
95: lDrawRect.left = lDrawRect.left + liHorz;
96: lDrawRect.right = lDrawRect.left + liWidth;
97: // Passenden Stift und Pinsel auswählen
98: pdc->SelectObject(&lInsidePen);
99: pdc->SelectObject(&lVertBrush);
100: break;
101: }
102: // Welches Werkzeug wird verwendet?
103: if (iTool == 0)
104: pdc->SelectObject(lNullBrush);
105: else
106: pdc->SelectObject(lNullPen);
107: // Welche Figur wird gezeichnet?
108: if (iShape == 1)
109: pdc->Ellipse(lDrawRect);
110: else
111: pdc->Rectangle(lDrawRect);
112: }
113: // Auf ursprünglichen Stift und Pinsel zurücksetzen
114: pdc->SelectObject(lOldBrush);
115: pdc->SelectObject(lOldPen);
116:}

Nachdem Sie nun die Kreise und Quadrate im zweiten Dialogfeld zeichnen können, müssen Sie noch diese Funktion aufrufen, wenn der Benutzer die Figuren entweder mit einem Stift oder einem Pinsel ausgewählt hat. Dazu fügen Sie die Zeilen 21 und 22 von Listing 8.8 in die Funktion OnPaint ein.

Listing 8.8: Die modifizierte Funktion OnPaint

1: void CPaintDlg::OnPaint()
2: {
3: CPaintDC dc(this); // device context for painting
4:
5: // TODO: Code für die Behandlungsroutine für Nachrichten hier einfügen
6:
7: // Zeiger auf übergeordnetes Fenster ermitteln
8: CGrafikDlg *pWnd = (CGrafikDlg*)GetParent();
9: // Ist der Zeiger gültig?
10: if (pWnd)
11: {
12: // Bitmap als Werkzeug gewählt?
13: if (pWnd->m_iTool == 2)
14: {
15: }
16: else // Nein, Figur zeichnen
17: {
18: // Wird Linie gezeichnet?
19: if (m_iShape == 0)
20: DrawLine(&dc, pWnd->m_iColor);
21: else // Ellipse oder Rechteck zeichnen
22: DrawRegion(&dc, pWnd->m_iColor, pWnd->m_iTool,
ÂpWnd->m_iShape);
23: }
24: }
25: // Kein Aufruf von CDialog::OnPaint() für Zeichnungsnachrichten
26:}

Jetzt können Sie die Anwendung kompilieren und ausführen und sollten nicht nur Linien, sondern auch Quadrate und Kreise anzeigen können, wobei Sie auch zwischen der Anzeige der Umrisse und der ausgefüllten Figuren ohne Randlinien wählen können, wie es Abbildung 8.5 darstellt.

Abbildung 8.5:
Rechtecke im zweiten Dialogfeld zeichnen

Bitmaps laden

Mit der Anwendung lassen sich jetzt verschiedene Grafiken im zweiten Dialogfeld zeichnen. Es ist noch die Funktionalität zu ergänzen, um Bitmaps zu laden und anzuzeigen. Die Bitmaps lassen sich in einfacher Weise zu den Ressourcen in der Anwendung hinzufügen, mit eigenen Objekt-IDs versehen und dann mit der Funktion LoadBitmap und dem Makro MAKEINTRESOURCE in ein Objekt der Klasse CBitmap laden. Allerdings ist das nicht sehr nützlich, wenn man eigene Anwendungen erstellt. Sinnvoll ist es eigentlich nur, wenn man Bitmaps aus Dateien vom Systemlaufwerk laden kann. Das Bitmap laden Sie mit der API-Funktion LoadImage in den Speicher und verbinden dann das geladene Bild mit dem CBitmap-Objekt.

In Ihrer Anwendung können Sie zu diesem Zweck eine Funktion mit der Schaltfläche Bitmap auf dem ersten Dialogfeld verbinden. Die Schaltfläche ruft das Dialogfeld Öffnen auf, in dem der Benutzer ein anzuzeigendes Bitmap auswählen kann. Für das Dialogfeld Öffnen erstellt man einen Filter, um aus den verfügbaren Dateien nur die Bitmaps aufzulisten, die sich im zweiten Dialogfeld anzeigen lassen. Nachdem der Benutzer ein Bitmap ausgewählt hat, ermitteln Sie die Namen für die Datei und den Pfad aus dem Dialogfeld und laden das Bitmap mit der Funktion LoadImage. Mit einem gültigen Handle auf das Bitmap, das in den Speicher geladen wurde, löschen Sie das aktuelle Bitmap aus dem CBitmap-Objekt. Wurde ein Bitmap in das CBitmap-Objekt geladen, lösen Sie das CBitmap-Objekt von dem jetzt gelöschten Bild. Nachdem Sie sichergestellt haben, daß im CBitmap-Objekt noch kein Bild geladen ist, verbinden Sie das eben in den Speicher geladene Bitmap mit Hilfe der Funktion Attach. Jetzt machen Sie das zweite Dialogfeld mit der Funktion Invalidate ungültig, so daß ein noch angezeigtes altes Bitmap durch das neuere ersetzt wird.

Um diese Funktionalität zu unterstützen, fügen Sie gemäß Tabelle 8.5 in die erste Dialogfeldklasse eine Stringvariable für den Namen des Bitmaps und eine CBitmap-Variable für das Bitmap selbst hinzu.

Tabelle 8.5: Bitmap-Variablen

Name

Typ

Zugriff

m_sBitmap

CString

Public

m_bmpBitmap

CBitmap

Public

Anschließend fügen Sie mit dem Klassen-Assistenten eine Behandlungsroutine für das Klickereignis der Schaltfläche Bitmap hinzu. In die Funktion schreiben Sie den Code aus Listing 8.9.

Listing 8.9: Die Funktion OnBbitmap

1: void CGrafikDlg::OnBbitmap()
2: {
3: // TODO: Code für die Behandlungsroutine der Steuerelement- ÂBenachrichtigung hier einfügen
4:
5: // Filter für Dialogfeld Öffnen erstellen
6: static char BASED_CODE szFilter[] = "Bitmap-Dateien (*.bmp)|*.bmp||";
7: // Dialogfeld Öffnen erzeugen
8: CFileDialog m_ldFile(TRUE, ".bmp", m_sBitmap,
9: OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, szFilter);
10:
11: // Dialogfeld Öffnen anzeigen und Ergebnis übernehmen
12: if (m_ldFile.DoModal() == IDOK)
13: {
14: // Gewählten Dateinamen ermitteln
15: m_sBitmap = m_ldFile.GetPathName();
16: // Gewählte Bitmap-Datei laden
17: HBITMAP hBitmap = (HBITMAP) ::LoadImage(AfxGetInstanceHandle(),
18: m_sBitmap, IMAGE_BITMAP, 0, 0,
19: LR_LOADFROMFILE | LR_CREATEDIBSECTION);
20:
21: // Ist Handle für das geladene Bild gültig?
22: if (hBitmap)
23: {
24: // Aktuelles Bitmap löschen
25: if (m_bmpBitmap.DeleteObject())
26: // War Bitmap vorhanden, lösen
27: m_bmpBitmap.Detach();
28: // Aktuell geladenes Bitmap mit Bitmap-Objekt verbinden
29: m_bmpBitmap.Attach(hBitmap);
30: }
31: // Zweites Dialogfeld ungültig machen
32: m_dlgPaint.Invalidate();
33: }
34: }

Bitmaps anzeigen

Nachdem Sie nun Bitmaps in den Arbeitsspeicher laden können, müssen Sie die Bitmaps noch anzeigen. Dazu kopieren Sie das Bitmap mit Hilfe der Funktion GetBitmap aus dem CBitmap-Objekt in eine BITMAP-Struktur. Die Funktion GetBitmap ermittelt die Breite und Höhe des Bitmaps. Als nächstes erzeugen Sie einen neuen Gerätekontext, der mit dem Bildschirmgerätekontext kompatibel ist. Selektieren Sie das Bitmap in den neuen Gerätekontext, und kopieren Sie es dann von diesem zweiten Gerätekontext in den originalen Gerätekontext, wobei Sie während des Kopiervorgangs die Größe mit Hilfe der Funktion StretchBlt anpassen.

Um diese Funktionalität in der Beispielanwendung zu realisieren, fügen Sie eine neue Member-Funktion in die zweite Dialogfeldklasse ein. Legen Sie den Funktionstyp als void, die Funktionsdeklaration mit ShowBitmap(CPaintDC *pdc, CWnd *pWnd) und den Zugriff als Privat fest. In die Funktion übernehmen Sie den Code aus Listing 8.10.

Beachten Sie, daß der übergebene Fensterzeiger als Zeiger auf ein CWnd- Objekt deklariert ist und nicht als Klassentyp Ihres Hauptdialogfelds. Um ihn als Zeiger auf den Klassentyp des ersten Dialogfelds zu deklarieren, müssen Sie die Klasse für das erste Dialogfeld vor der Klassendeklaration für das zweite Dialogfeld deklarieren. Unterdessen erfordert das erste Dialogfeld, daß die zweite Dialogklasse zuerst deklariert wird. Das hat Einfluß auf die Reihenfolge, in der die Include-Dateien im Quellcode am Beginn jeder Datei eingebunden werden. Man kann nicht beide Klassen vor der jeweils anderen deklarieren, eine muß die erste sein. Obwohl man das Problem umgehen kann, indem man einen Platzhalter für die zweite Klasse vor der Deklaration der ersten Klasse deklariert, ist es in diesem Fall einfacher, den Typ des Zeigers als Zeiger auf die erste Dialogfeldklasse umzuwandeln. Im Anhang A finden Sie weitere Hinweise dazu, wie man einen Platzhalter für die zweite Klasse deklariert.

Listing 8.10: Die Funktion ShowBitmap

1: void CPaintDlg::ShowBitmap(CPaintDC *pdc, CWnd *pWnd)
2: {
3: // Zeiger in einen Zeiger auf die Dialogklasse des Hauptfensters Âumwandeln
4: CGrafikDlg *lpWnd = (CGrafikDlg*)pWnd;
5: BITMAP bm;
6: // Geladenes Bitmap holen
7: lpWnd->m_bmpBitmap.GetBitmap(&bm);
8: CDC dcMem;
9: // Gerätekontext erzeugen, in den Bitmap geladen wird
10: dcMem.CreateCompatibleDC(pdc);
11: // Bitmap in den kompatiblen Gerätekontext selektieren
12: CBitmap* pOldBitmap = (CBitmap*)dcMem.SelectObject(lpWnd->m_bmpBitmap);
13: CRect lRect;
14: // Anzeigebereich verfügbar machen
15: GetClientRect(lRect);
16: lRect.NormalizeRect();
17: // Bitmap in Dialogfeld kopieren und in Größe anpassen
18: pdc->StretchBlt(10, 10, (lRect.Width() - 20),
19: (lRect.Height() - 20), &dcMem, 0, 0,
20: bm.bmWidth, bm.bmHeight, SRCCOPY);
21: }

Jetzt können Sie das momentan ausgewählte Bitmap im Dialogfeld anzeigen. Es ist noch die Funktion OnPaint im zweiten Dialogfeld anzupassen, um die Funktion ShowBitmap aufzurufen. Ob ein Bitmap spezifiziert wurde, läßt sich anhand der Variablen m_sBitmap im ersten Dialogfeld testen. Wenn der String leer ist, gibt es kein Bitmap, das anzuzeigen wäre. Ist der String nicht leer, rufen Sie die Funktion ShowBitmap auf. Um dieses letzte Stück der Funktionalität in Ihrer Anwendung zu realisieren, fügen Sie die Zeilen 15 bis 18 aus Listing 8.11 in die Funktion OnPaint ein.

Listing 8.11: Die modifizierte Funktion OnPaint

1: void CPaintDlg::OnPaint()
2: {
3: CPaintDC dc(this); // device context for painting
4:
5: // TODO: Code für die Behandlungsroutine für Nachrichten hier einfügen
6:
7: // Zeiger auf übergeordnetes Fenster ermitteln
8: CGrafikDlg *pWnd = (CGrafikDlg*)GetParent();
9: // Ist der Zeiger gültig?
10: if (pWnd)
11: {
12: // Bitmap als Werkzeug gewählt?
13: if (pWnd->m_iTool == 2)
14: {
15: // Ist ein Bitmap ausgewählt und geladen?
16: if (pWnd->m_sBitmap != "")
17: // Bitmap anzeigen
18: ShowBitmap(&dc, pWnd);
19: }
20: else // Nein, Figur zeichnen
21: {
22: // Wird Linie gezeichnet?
23: if (m_iShape == 0)
24: DrawLine(&dc, pWnd->m_iColor);
25: else // Ellipse oder Rechteck zeichnen
26: DrawRegion(&dc, pWnd->m_iColor, pWnd->m_iTool,
27: pWnd->m_iShape);
28: }
29: }
30: // Kein Aufruf von CDialog::OnPaint() für Zeichnungsnachrichten
31:}

Jetzt sollten Sie in der Lage sein, ein Bitmap von Ihrem System auszuwählen und es im zweiten Dialogfeld anzuzeigen (siehe Abbildung 8.6).

Abbildung 8.6:
Ein Bitmap im zweiten Dialogfeld anzeigen

Zusammenfassung

Was für ein Tag, um die Woche zu beginnen! Heute haben Sie eine Menge gelernt. Vor allem ging es darum, wie Windows die Gerätekontextobjekte verwendet, damit Sie Grafiken immer in der gleichen Weise zeichnen können, ohne sich um die konkrete Hardware kümmern zu müssen, die der Benutzer auf seinem Computer installiert hat. Am Beispiel der grundlegenden GDI-Objekte Stift und Pinsel wurde gezeigt, wie man diese Objekte zum Zeichnen von Figuren in Fenstern und Dialogfeldern einsetzt. Weiterhin haben Sie erfahren, wie man Bitmaps vom Systemlaufwerk lädt und sie auf dem Bildschirm für den Benutzer anzeigt. Zum Zeichnen von Figuren mit Stiften und Pinseln stehen verschiedene Stile zur Verfügung. Außerdem haben Sie gelernt, wie man Farben für Stifte und Pinsel festlegt, so daß man steuern kann, wie sich Bilder dem Benutzer präsentieren.

Fragen und Antworten

Frage:
Warum muß ich sowohl einen Stift als auch einen Pinsel spezifizieren, wenn ich eigentlich nur den einen oder den anderen verwenden möchte?

Antwort:
Wenn Sie ein Objekt zeichnen, das ausgefüllt ist, zeichnen Sie immer mit beiden Werkzeugen. Der Stift ist für den Umriß verantwortlich, während der Pinsel den Innenbereich ausfüllt. Man kann nicht nur den einen oder den anderen verwenden, sondern immer nur beide. Wenn man nur den einen oder anderen anzeigen möchte, sind spezielle Schritte zu unternehmen.

Frage:
Warum werden alle Stiftstile durchgängig, wenn ich die Breite des Stifts über 1 erhöhe?

Antwort:
Bei einem breiteren Stift erhöht man auch die Größe der Punkte, die zum Zeichnen verwendet werden. Wie Sie noch aus Lektion 3 wissen, erhalten Sie nur verstreute Punkte, wenn Sie die von der Maus ausgelösten Ereignisse zum Zeichnen von Punkten einzeln auffangen. Sobald Sie die Größe der Punkte für die zu zeichnende Linie erhöhen, werden die Lücken zwischen den Punkten von beiden Seiten aufgefüllt, so daß eine durchgängige Linie entsteht.

Workshop

Kontrollfragen

1. Welche drei Werte faßt man zu einem Farbenwert zusammen?

2. Mit welchem Instrument zeichnet man in ein Fenster, ohne daß man die vom Benutzer eingesetzte Grafikkarte kennen muß?

3. Welche Größe kann man für ein Bitmap verwenden, um einen Pinsel daraus zu erstellen?

4. Welche Nachricht sendet Windows an ein Fenster, um es anzuweisen, sich selbst neu zu zeichnen?

5. Wie kann man erreichen, daß sich ein Fenster selbst neu zeichnet?

Übungen

1. Machen Sie das zweite Dialogfeld in der Größe veränderbar, und stellen Sie sicher, daß sich die im Fenster vorhandenen Zeichnungen ebenfalls in der Größe anpassen, wenn man die Größe des Dialogfelds verändert.

2. Nehmen Sie einen Bitmap-Pinsel in die Gruppe der Pinsel auf, um Rechtecke und Ellipsen zu zeichnen.



vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbackKapitelanfangnächstes Kapitel


Ein Imprint des Markt&Technik Buch- und Software-Verlag GmbH.
Elektronische Fassung des Titels: Visual C++ 6 in 21 Tagen, ISBN: 3-8272-2035-1