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 ...
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ß.
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.
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.
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 |
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.
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);
Abbildung 8.2:
Standardpinselmuster
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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: }
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.
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
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
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.
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: }
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.
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
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.
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.
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?
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.