als PDF-Datei ctutor4

Transcrição

als PDF-Datei ctutor4
Prof. Dr. J. Dankert
FH Hamburg
C und C++ für UNIX, DOS und MS-Windows, Teil 4:
C
++
Windows-Programmierung
mit "Microsoft Foundation
Classes"
Dies ist weder ein Manual noch ein "normales" Vorlesungs-Skript ("normale"
Vorlesungen über eine Programmiersprache sind wohl ohnehin langweilig). Es
soll in erster Linie eine Hilfe zum Selbststudium sein (und wird deshalb als
"Tutorial" bezeichnet).
Die im Skript abgedruckten Programme (Quelltext) können über die InternetAdresse
http://www.fh-hamburg.de/rzbt/dankert/c_tutor.html
kopiert werden.
Prof. Dr. J. Dankert
FH Hamburg
Inhalt (Teil 4)
13
Windows-Programmierung mit C++ und MFC
13.1
13.2
13.3
13.4
14
++
C oder C für die Windows-Programmierung?
Das C++-MFC-Minimal-Programm "minimfc2.cpp"
Bearbeiten von Botschaften, natürlich zuerst: "Hello World!"
Fazit aus den beiden Beispiel-Programmen
MS-Visual-C++-Programmierung mit "App Wizard",
"Class Wizard" und "App Studio"
14.1
14.2
14.3
14.4
Trennen von Klassen-Deklarationen und Methoden
Das "Document-View"-Konzept
Das vom "App Wizard" erzeugte "Hello World"-Programm
Das Projekt "fmom"
14.4.1
Die mit "fmom" zu realisierende Funktionalität
14.4.2
Erzeugen des Projektes (Version "fmom1")
14.4.3
Datenstruktur für "fmom", Entwurf der Klassen
14.4.4
Einbinden der Datenstruktur in die Dokument-Klasse,
die Klasse CObList
14.4.5
Menü mit "App Studio" bearbeiten
14.4.6
Dialog-Box mit "App Studio" erzeugen
14.4.7
Einbinden des Dialogs in das Programm
14.4.8
Bearbeiten der Ansichts-Klasse, Ausgabe erster
Ergebnisse
14.4.9
Die Return-Taste muß Kompetenzen abgeben
14.4.10
Ein zusätzlicher "Toolbar"-Button für "fmom"
14.4.11
Das Dokument als Binär-Datei, "Serialization"
14.4.12
Eine zweite Ansicht für das Dokument,
Splitter-Windows
14.4.13
GDI-Objekte und Koordinatensysteme
14.4.14
Graphische Darstellung der Flächen
14.4.15
Schwerpunkt markieren,
Durchmesser: 0,1 "Logical Inches"
14.4.16
Erweiterung der Funktionalität:
Flächenmomente 2. Ordnung
14.4.17
Listen, Ändern, Löschen
14.4.18
Dialog-Box mit "List Box"
14.4.19
Initialisieren der "List Box", die Klasse CString
14.4.20
Ändern bzw. Löschen einer ausgewählten Teilfläche
14.4.21
Sortieren in einer CObList-Klasse
14.4.22
Eine Klasse für die Berechnung von Polygon-Flächen
14.4.23
Ressourcen für die Eingabe einer Polygon-Fläche
14.4.24
Der "Dialog des Programms" mit der Dialog-Box
14.4.25
Drucker-Ausgabe
14.4.26
Optionale Ausgabe der Eingabewerte
14.4.27
Platzbedarf für Texte
261
261
263
267
270
271
271
272
274
278
278
279
283
287
291
293
304
308
317
319
322
329
335
347
354
358
361
364
367
374
377
378
382
385
391
396
400
261
J. Dankert: C++-Tutorial
"Die Riesen werden immer riesiger. Wenn Du auf
Ihren Schultern sitzt, hast Du einen tollen Überblick
und fühlst Dich unglaublich stark."
"Leider wird das Hinaufklettern immer schwieriger."
13
Windows-Programmierung mit C++ und MFC
Windows-Programmierung ist schwierig genug, so daß sorgfältig zu überlegen ist, welche
Hilfsmittel benutzt werden sollen. In die nachfolgende Betrachtung werden nur die Programmiersprachen C und C++ und die von der Firma Microsoft (mit dem Produkt Visual C++)
bereitgestellten Tools einbezogen (ernsthafte weitere Konkurrenten wären z. B. Visual-Basic
oder Borlands "Delphi").
13.1 C oder C++ für die Windows-Programmierung?
Wer mit MS-Visual C++ arbeitet, kann sowohl in C als auch in C++ programmieren und kann
Programme für DOS, Windows 3.1 und (ab Version 4.0) für Windows 95 und Windows NT
schreiben. Das Ziel, Windows-Programme zu erzeugen, kann mit folgenden Strategien
verfolgt werden:
◆
Variante 1: C-Programme und Windows-API ("Application Interface")
Der Vorteil dieser Variante ist, daß C++-Kenntnisse nicht erforderlich sind. Nachteilig
ist, daß weder die "Microsoft Foundation Classes" noch die Tools für die Programmentwicklung ("App Wizard" und "Class Wizard") genutzt werden können. Das "App
Studio" für die Entwicklung von Ressourcen kann genutzt werden, die Einbindung der
Ressourcen in das Anwendungsprogramm muß "von Hand" vorgenommen werden.
Diese Variante der Windows-Programmierung wurde in den Kapiteln 9 und 10 dieses
Tutorials behandelt. Wer nicht zur C++-Programmierung aufsteigen möchte, kann auch
auf diesem Wege alle Möglichkeiten der Windows-Programmierung erschließen. Ein
Nachteil ist, daß für Windows 3.1 geschriebene Programme nicht ohne Änderungen
in die "32-Bit-Welt" (Windows 95 oder Windows NT) portiert werden können. In
diesem Tutorial wird dieser Weg nicht weiter verfolgt. Dem Leser, der den nachfolgend beschriebenen Weg nicht mitgehen möchte, werden dringend die ganz hervorragenden Bücher von Charles Petzold ("Programmierung unter Windows 3.1" und
"Windows 95 Programmierung") als weiterführende Literatur empfohlen.
◆
Variante 2: C++-Programme und Windows-API
Natürlich kann man das Anwendungsprogramm in C++ schreiben und die WindowsAPI-Routinen aufrufen, was allerdings in höchstem Maße inkonsequent wäre. Der
Programmierer, der in der Lage ist, C++-Programme zu schreiben, sollte von den
J. Dankert: C++-Tutorial
262
Vorteilen der "Microsoft Foundation Classes" unbedingt Gebrauch machen. Dieser
Weg wird deshalb hier nicht weiter verfolgt.
◆
Variante 3: C++-Programme und MFC ("Microsoft Foundation Classes") ohne
Benutzung von "App Wizard" und "Class Wizard"
Dieser Weg hat den nicht zu unterschätzenden Vorteil, daß der Programmierer jede
Zeile seines Programms kennt, weil er sie selbst geschrieben (oder wenigstens bewußt
aus irgendeiner Quelle kopiert) hat. Das Programm ist nicht mit Code überladen, der
für die spezielle Anwendung nicht benötigt wird. Der Programmierer ist auch nicht
gezwungen, die "Document-View"-Architektur, die allen von "App Wizard" erzeugten
Programmen zugrunde liegt, bereits für einfache Programme zu verwenden. Der
Nachteil dieser Variante ist, daß die gesamte Funktionalität, die der "App Wizard"
gratis spendiert, (sofern benötigt) selbst erzeugt werden muß. Außerdem sind die
Beziehungen zwischen den Ressourcen und dem Anwendungsprogramm "von Hand"
herzustellen.
Ein wesentlicher Vorteil bei der MFC-Programmierung besteht natürlich darin, daß
die systemspezifischen Teile in den Methoden der Klassen-Bibliothek liegen, so daß
z. B. beim Umstieg von Windows 3.1 auf Windows 95 im wesentlichen nur eine
Neu-Compilierung (mit den neuen Klassen-Bibliotheken) erforderlich ist.
In den beiden nachfolgenden Abschnitten 13.2 und 13.3 werden ein MinimalProgramm und der "Hello World"-Klassiker mit dieser Programmier-Variante erzeugt.
Die Programme werden mit dem entsprechenden C-Programmen aus den Abschnitten
9.3 bzw. 9.5.1 verglichen.
◆
Variante 4: C++-Programme, deren Gerüst vom "App Wizard" erzeugt und unter
Verwendung des "Class Wizard" bearbeitet wird
Bei dieser Variante werden zwangsläufig die "Microsoft Foundation Classes" benutzt,
so daß die damit verbundenen Vorteile gegeben sind. Hinzu kommen eine sehr
komfortable Grund-Funktionalität und eine hohe Sicherheit beim Verknüpfen der mit
"App Studio" erzeugten Ressourcen mit dem Anwendungsprogramm, weil der "Class
Wizard" auf dieser Strecke wesentliche Unterstützung leistet.
Das Problem bei dieser Variante besteht darin, daß der Programmierer sich in dem
automatisch erzeugten Code zunächst zurechtfinden muß, um die Anschlußpunkte für
seinen eigenen Beitrag zum Programm zu finden. Als weiterer Nachteil mag
empfunden werden, daß man auch für einfache Programme die vom "App Wizard"
vorgegebene "Document-View"-Architektur akzeptieren muß. Andererseits unterstützt
gerade diese Architektur ein strukturiertes Programmieren und erleichtert das
Zurechtfinden im automatisch erzeugten Code. Im Kapitel 14 wird ein Projekt
bearbeitet, das mit dieser Variante erzeugt wird.
Auch zu den Varianten 3 und 4 existieren zahlreiche Bücher recht unterschiedlicher Qualität.
Empfohlen werden können z. B. "Inside Visual C++" (deutsche Übersetzung) von David J.
Kruglinski, in dem konsequent die oben beschriebene Variante 4 verwendet wird, und
"Programming Windows 95 with MFC" (englisch) von Jeff Prosise, in dem Variante 3
verwendet wird.
263
J. Dankert: C++-Tutorial
13.2 Das C++-MFC-Minimal-Programm "minimfc2.cpp"
Bei der Benutzung der "Microsoft Foundation Classes" braucht der Programmierer das
Hauptprogramm WinMain nicht selbst zu schreiben. Es wird automatisch beim Linken
hinzugefügt. Er muß nur die Deklaration von mindestens zwei Klassen, die aus den MFCBasisklassen abgeleitet werden, bereitstellen und die Methoden der Klassen codieren, die für
sein spezielles Problem benötigt werden.
Da dies allerdings recht fundierte Kenntnisse in der C++-Programmierung voraussetzt, wird
nachfolgend in sehr kleinen Schritten das Zusammenarbeiten mit den Basisklassen demonstriert. Begonnen wird wieder mit dem "Minimal-Programm" ohne eigentliche Funktionalität,
vergleichbar mit dem C-Programm miniwin.c aus dem Abschnitt 9.3, das als "WindowsSkelett-Programm" im Abschnitt 9.4 aufgelistet wurde. Auf den ersten Blick fällt auf, daß das
C++-MFC-Programm weniger umfangreich ist:
// Programm minimfc2.cpp
#include <afxwin.h>
//1
class CMinimfc2App : public CWinApp
{
public:
virtual BOOL InitInstance () ;
} ;
//2
class CMainFrame : public CFrameWnd
{
public:
CMainFrame () ;
} ;
//4
CMinimfc2App
//6
theApp ;
BOOL CMinimfc2App::InitInstance ()
{
m_pMainWnd = new CMainFrame ;
m_pMainWnd->ShowWindow (m_nCmdShow) ;
return TRUE ;
}
CMainFrame::CMainFrame ()
{
Create (NULL , "Programm MINIMFC2") ;
}
//
//
//
//
//
//3
//5
//7
//8
//9
//10
//11
Im nachfolgenden Kommentar wird das MFC-Programm minimfc2.cpp mit
dem entsprechenden C-Programm miniwin.c aus dem Abschnitt 9.3 bzw.
dem gleichwertigen Skelett-Programm winskel.c aus dem Abschnitt 9.4
verglichen, um die grundsaetzlichen Unterschiede zur objektorientierten
Programmierung zu verdeutlichen.
// MFC-Programme muessen die Header-Datei afxwin.h einbinden (//1). Es ist
// eine Datei mit mehreren tausend Zeilen, die selbst noch andere grosse
// Dateien inkludiert (z. B. auch die Riesendatei windows.h).
// Ein MFC-Programm muss mindestens zwei Klassen deklarieren und
// je ein Objekt dieser Klassen erzeugen:
//
//
* Ein Objekt der APPLIKATIONSKLASSE repraesentiert das eigentliche
//
Anwendungs-Programm, diese Klasse (hier: CMinimfc2App) muss von
//
der Basisklasse CWinApp abgeleitet werden (//2).
//
264
J. Dankert: C++-Tutorial
//
//
//
//
*
Ein Objekt einer FENSTERKLASSE fuer das Hauptfenster fungiert
als Rahmenfenster, die Klasse (hier: CMainFrame) kann z. B. (wie
in diesem Programm) von der Basisklasse CFrameWnd abgeleitet
werden (//4).
//
//
//
//
//
//
//
//
//
//
//
//
Von der Applikationsklasse (hier: CMinimfc2App) wird GENAU EINE globale
Instanz (//6) erzeugt (hier: theApp, alle Namen wurden so gewaehlt,
wie sie vom App Wizard beim automatischen Erzeugen des Rahmenprogramms
auch gewaehlt werden). Zur Erinnerung: Globale Instanzen werden vor
der Abarbeitung des Hauptprogramms erzeugt (vgl. Beispiel im Abschnitt
12.2.3), so dass der Konstruktor von CWinApp die ersten Aktionen
des Programms ausfuehrt, z. B. werden verschiedene Windows-Variablen
initialisiert, insbesondere wird einer globalen Variablen der Pointer
auf die Instanz theApp zugewiesen (ist im CWinApp-Konstruktor als
this-Pointer verfuegbar, vgl. Abschnitt 12.7.2). Auf diesen Pointer
kann bei Bedarf (wird in diesem Programm nicht explizit genutzt)
mit der ebenfalls globalen Funktion AfxGetApp () zugegriffen werden.
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
Anschliessend startet das (in den Basisklassen versteckte) WinMain.
Es kann auf die Methoden der Applikationsklasse CMinimfc2App zugreifen,
weil es ueber AfxGetApp den Pointer auf theApp ermitteln kann (bis auf
die ueberladene Methode InitInstance sind dies in diesem Fall
ausschliesslich die von CWinApp ererbten Methoden). Die wichtigsten
von WinMain zu startenden Methoden der Anwendungsklasse uebernehmen
folgende Aufgaben:
//
//
//
//
//
//
//
//
//
Die Methode InitInstance, die in der Applikationsklasse CMinimfc2App
die (im uebrigen voellig leere) Methode InitInstance der Basisklasse
CWinApp ueberdeckt, erzeugt zunaechst ein Objekt der Fensterklasse
CMainFrame (mit new in Zeile //8). Da fuer diese ab Zeile //4
deklarierte Klasse ein Konstruktor bereitgestellt wird (//5 bzw.
(//10), wird dieser abgearbeitet und erzeugt mit seiner einzigen
Anweisung (//11) ein Fenster. Der von new gelieferte Pointer auf
dieses Fenster-Objekt wird in der Variablen m_pMainWnd abgelegt,
die CMimimfc2App von CWinApp geerbt hat.
//
//
//
//
//
//
//
//
Mit dem Pointer m_pMainWnd kann man auf alle (von CFrameWnd geerbten)
Methoden der Fensterklasse zugreifen, hier wird nur ShowWindow
aufgerufen, die das Fenster auf den Bildschirm bringt. Das Argument,
das ShowWindow uebernimmt, ist der vierte an WinMain uebergebene
Parameter (Fenstertyp, vgl. Kommentar zum Programm miniwin.c im
Abschnitt 9.3). Die beiden Anweisungen (//8) und (//9) in InitInstance
entsprechen dem CreateWindow und dem ShowWindow im Windows-SkelettProgramm im Abschnitt 9.4.
*
Die Methode InitInstance sollte grundsaetzlich von der
Applikationsklasse des Programms ueberladen werden (//3 bzw.
//7), weil in der CWinApp-Version dieser Methode kein Fenster
erzeugt wird. InitInstance ist der geeignete Ort, um Parameter
der Applikation zu initialisieren (wird in diesem Programm
nicht genutzt) und um das Hauptfenster zu erzeugen und auf
den Bildschirm zu bringen (wird nachfolgend noch genauer
beschrieben).
*
Die CWinApp-Methode Run betreibt die Nachrichtenschleife, dies
entspricht der Schleife
while (GetMessage (...)) ...
im WinMain-Skelett-Programm des Abschnitts 9.4, die Nachrichtenschleife bricht bekanntlich beim Eintreffen der WM_QUIT-Botschaft
ab, danach wird noch von der Methode Run die Methode ...
*
... ExitInstance gestartet, die sich fuer "Aufraeumarbeiten"
anbietet und in einem solchen Fall ueberladen werden muesste.
// Die Methode Create (//11), die CMainFrame von CFrameWnd erbt (CFrameWnd
// hat sie uebrigens aus ihrer Basisklasse CWnd geerbt), erledigt etwa
265
J. Dankert: C++-Tutorial
//
//
//
//
//
//
//
//
//
◆
die gleiche Arbeit wie die Funktion CreateWindow im Windows-SkelettProgramm im Abschnitt 9.4. Von den insgesamt 8 Argumenten, die Create
uebernehmen kann, sind die letzten 6 mit Default-Werte vorbelegt.
In diesem Programm werden nur die beiden "Pflicht-Argumente"
uebergeben: Fuer das erste Argument, den Namen der Fensterklasse
(String), wird der NULL-Pointer uebergeben, in diesem Fall waehlt
Create eine Fensterklasse, die "am besten zu den uebergebenen
Argumenten passt". Das zweite Argument legt die Fenster-Ueberschrift
fest.
Die auffallende Ähnlichkeit der CWnd-Methode
CWnd::ShowWindow (int nCmdShow) ;
mit der aus dem Abschnitt 9.3 bekannten Funktion
ShowWindow (HWND hwnd , int nCmdShow) ;
ist typisch für einen sehr großen Teil der Methoden der "Microsoft Foundation
Classes". Das ist angenehm für denjenigen, der die Kapitel 9 und 10 dieses Tutorials
durchgearbeitet hat, weil ihm sehr viele Methoden sofort vertraut sein werden. Auch
der "kleine Unterschied" ist typisch für alle die Methoden, die man sich vorstellen
darf als "in eine Klassen-Deklaration 'eingewickelte' Funktionen" (Originalton der
Microsoft-Dokumentation: "wrapped"): Das "Objekt", auf das die Methode angewendet wird, ist die Instanz, mit der sie aufgerufen wird, während das Objekt, das die CFunktion bearbeiten soll, durch einen "Handle" identifiziert wird.
◆
Den Quellcode, der den "Microsoft Foundation Classes" zugrunde liegt, kann man
sich bemerkenswerterweise zu einem großen Teil ansehen. Wenn (wie üblich) MSVisual-C++ 1.5 in einem Verzeichnis \msvc installiert ist, sind die MFC-HeaderDateien im Verzeichnis \msvc\mfc\include und der Quellcode im Verzeichnis
\msvc\mfc\src gespeichert. im letzgenannten Verzeichnis findet man z. B. in der
Datei winmain.cpp auch die Funktion WinMain, der nachfolgend daraus gelistete
Ausschnitt zeigt einige im Kommentar des Programm minimfc2.cpp genannte
Funktionsaufrufe (Sie sollten nicht den Ehrgeiz haben, schon hier jede Einzelheit des
Code-Fragments verstehen zu wollen):
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
int nReturnCode = -1;
// AFX internal initialization
if (!AfxWinInit(hInstance, hPrevInstance, lpCmdLine, nCmdShow))
goto InitFailure;
// App global initializations (rare)
if (hPrevInstance == NULL && !AfxGetApp()->InitApplication())
goto InitFailure;
// Perform specific initializations
if (!AfxGetApp()->InitInstance())
{
nReturnCode = AfxGetApp()->ExitInstance();
goto InitFailure;
}
ASSERT_VALID (AfxGetApp());
nReturnCode = AfxGetApp()->Run();
InitFailure:
AfxWinTerm();
return nReturnCode;
}
266
J. Dankert: C++-Tutorial
Es hat also alles seine Ordnung, WinMain existiert (natürlich!) auch in MFCProgrammen, die Funktionalität entspricht den aus Kapitel 9 bekannten Aufgaben, die
diese Funktion zu erledigen hat. Daß die Profi-Programmierer von Microsoft in dieser
Funktion (sinnvollerweise) mehrere goto-Statements verwenden, mag die "Verfechter
der reinen Lehre" erschrecken, wer dieses Tutorial durchgearbeitet hat, kennt die
Meinung des Schreibers dieser Zeilen zu diesem Thema aus der Betrachtung am Ende
des Abschnitts 6.2.
◆
Interessant ist auch eine Inspektion des Files appcore.cpp (liegt im gleichen
Verzeichnis wie winmain.cpp). Dort findet man den Quellcode der CWinAppMethoden, man sollte sich speziell die im Kommentar des Programms minimfc2.cpp
erwähnten Methoden CWinApp::InitInstance und CWinApp::Run sowie den
Konstruktor CWinApp::CWinApp einmal ansehen.
Um das ausführbare Programm von minimfc2.cpp zu erzeugen, benutzt man zweckmäßigerweise die integrierte Entwicklungsumgebung von MS-Visual-C++. Mit dem Editor der "Visual
Workbench" wird das Programm geschrieben. Es wird ein neues Projekt erzeugt (New im
Menü Project), automatisch werden die Projekt-Dateien erzeugt (unter anderem auch ein
Makefile mit der Extension .mak zur Steuerung des Compilier- und Link-Prozesses, nicht
ansehen, sieht wahnsinnig kompliziert aus!). Anschließend wird der Programmierer aufgefordert, seine eigenen Dateien zum Projekt hinzuzufügen, zunächst ist das nur die Programmdatei minimfc2.cpp.
Auch für MFC-Programme wird eine Definitionsdatei (mit der Extension .def) für die Arbeit
des Linkers benötigt (vgl. Abschnitt 9.3). Es genügt in der Regel die Default-Datei, die die
Entwicklungsumgebung bereitstellt, z. B. so: Man wählt im Menü Project die Option Build
MINIMFC2.EXE und wird darauf aufmerksam gemacht, daß die .def-Datei fehlt. Die Frage,
ob man die Default-Datei verwenden möchte, wird mit OK beantwortet, auf dem Bildschirm
erscheint die nachfolgend gelistete Definitionsdatei minimfc2.def:
NAME
EXETYPE
CODE
DATA
HEAPSIZE
MINIMFC2
WINDOWS
PRELOAD MOVEABLE DISCARDABLE
PRELOAD MOVEABLE MULTIPLE
1024
EXPORTS
;
===List your explicitly exported functions here===
An dieser Datei braucht man nichts zu ändern, der nächste Versuch Build MINIMFC2.EXE
sollte gelingen. Unten rechts ist das nach Execute MINIMFC2.EXE aus dem Menü Project
erscheinende Fenster des Programms zu sehen.
Zum Tutorial gehören jeweils neben den .cpp-Dateien auch
die .mak-Datei und die .def-Datei, so daß man den Prozeß
abkürzen kann: Man kopiert die Dateien in ein beliebiges
Verzeichnis, wählt Open aus dem Menü Project und öffnet
durch Doppelklick auf die .mak-Datei das Projekt. Dann wird
man in der Regel darauf aufmerksam gemacht, daß das
Projekt an sein neues Verzeichnis angepaßt wird, anschließend kann sofort Build MINIMFC2.EXE gestartet werden.
267
J. Dankert: C++-Tutorial
13.3 Bearbeiten von Botschaften, natürlich zuerst: "Hello World!"
Für das Bearbeiten von Botschaften sind in den C-Programmen, die in den Kapiteln 9 und 10
vorgestellt wurden, die Fensterfunktionen ("call back functions") zuständig. Der Programmierer sucht sich die Botschaften heraus, auf die das Programm reagieren soll, die übrigen
Botschaften werden an die Funktion DefWindowProc weitergeleitet.
Die Programmiersprache C++ bietet mit dem Konzept der virtuellen Funktionen eigentlich
genau die Technik an, die eine elegante Lösung für das Bearbeiten von Botschaften erlaubt:
In den Basisklassen werden für die Bearbeitung aller Botschaften virtuelle Methoden
definiert, die die Botschaften dann bearbeiten, wenn sie nicht von entsprechenden Methoden
der abgeleiteten Klassen überlagert sind. Der Programmierer schreibt also für genau die
Botschaften, die sein Programm bearbeiten soll, die Behandlungs-Routinen als Methoden der
abgeleiteten Klassen.
Bis auf die letzte Aussage ("Programmierer schreibt für die Botschaften, die sein Programm
behandeln soll, eigene Methoden") wird diese schöne (von der Programmiersprache C++
unterstützte) Strategie in den "Microsoft Foundation Classes" leider nicht verfolgt. Der Grund
ist der "Overhead", der beim Arbeiten mit virtuellen Funktionen unvermeidlich ist, um einem
Aufruf die jeweils richtige Methode zuzuordnen. Bei der Unzahl von Botschaften, die ständig
gesendet werden, würde dies zweifellos zu einem beträchtlichen Geschwindigkeitsverlust
führen.
Die Zuordnung der Botschaften zu ihren Behandlungsroutinen erfolgt über ein sehr feinsinniges Konzept mit sogenannten "Message Maps", die in den Klassen angesiedelt sein
müssen, in denen die Botschaften bearbeitet werden sollen. Glücklicherweise stellt Visual C++
geeignete Makros zur Verfügung, mit denen die entsprechenden Eintragungen in den Klassen
vom Precompiler generiert werden. Der Programmierer braucht nur die Verwendung dieser
Makros zu kennen und darf darauf vertrauen, daß der entsprechende C++-Code in geeigneter
Weise erzeugt wird und auch funktioniert. Das nachfolgende Beispiel-Programm, eine weitere
Version des "Hello World"-Klassikers, demonstriert diese Technik am Beispiel der
Bearbeitung der Botschaft WM_PAINT:
// Programm hllmfc2.cpp
#include <afxwin.h>
class CHllmfc2App : public CWinApp
{
public:
virtual BOOL InitInstance () ;
} ;
class CMainFrame : public CFrameWnd
{
public:
CMainFrame () ;
protected:
afx_msg void OnPaint () ;
DECLARE_MESSAGE_MAP ()
} ;
CHllmfc2App
theApp ;
//1
//2
268
J. Dankert: C++-Tutorial
BOOL CHllmfc2App::InitInstance ()
{
m_pMainWnd = new CMainFrame ;
m_pMainWnd->ShowWindow (m_nCmdShow) ;
m_pMainWnd->UpdateWindow () ;
return TRUE ;
}
BEGIN_MESSAGE_MAP (CMainFrame , CFrameWnd)
ON_WM_PAINT ()
END_MESSAGE_MAP ()
//3
//4
//5
//6
CMainFrame::CMainFrame ()
{
Create (NULL , "Programm HLLMFC2") ;
}
void CMainFrame::OnPaint ()
{
CPaintDC dc (this) ;
CRect
rect
;
GetClientRect (&rect) ;
dc.DrawText ("Hello MFC-World!" , -1 , &rect ,
DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
}
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//7
//8
//9
//10
//11
Das Programm demonstriert das Arbeiten mit "Message Maps", mit denen
die fuer die Bearbeitung von Botschaften vorgesehenen Routinen
angesteuert werden. In diesem Programm wird die Windows-Message
WM_PAINT von der zur Klasse CMainFrame gehoerenden Methode OnPaint ()
bearbeitet. An vier Stellen im Programm muessen dafuer Eintragungen
vorgenommen werden:
a) Die Methode muss in der abgeleiteten Klasse, in der die
Botschaft behandelt werden soll, deklariert sein (//1).
b) Die Methode muss programmiert werden (//7 bis //11).
c) Deklarationen fuer Daten und Methoden der "Message Map" muessen
in der abgeleiteten Klasse untergebracht werden (//2). Dies
uebernimmt ein Makro DECLARE_MESSAGE_MAP ().
d) Der Code fuer Daten und die Methoden der "Message Map" muss
erzeugt werden (//4 bis //6). Dafuer ist eine ganze Reihe von
Makros verfuegbar.
Der tatsaechlich von den Makros erzeugte Code ist fuer den Programmierer
weitgehend uninteressant. Er muss die oben genannten Punkte a) bis d)
beachten, die noch einiger ergaenzender Bemerkungen beduerfen:
Zu a) Zur Behandlung der WM_PAINT-Botschaft muss eine Methode OnPaint
verwendet werden, die keine Argumente uebernimmt. Welche Methode
zu welcher Botschaft gehoert und welche Argumente uebergeben
werden, muss man dem Handbuch oder der On-Line-Hilfe entnehmen.
Das afx_msg vor der Deklaration der Methode hat keine
nennenswerte Funktionalitaet, dient dem Programmierer als
Erinnerung dafuer, dass der Aufruf der Funktion ueber die
"Message Map" erfolgt, wird vom Precompiler ersatzlos entfernt
und koennte auch im Programm gleich weggelassen werden.
Zu b) Fuer die meisten WM_-Botschaften gilt folgende Namenszuordnung:
Der Name der zugehoerigen Behandlungsroutine entsteht durch
Ersetzen von 'WM_' durch 'On', die nachfolgenden Grossbuchstaben
werden bis auf die Anfangsbuhstaben von Worten durch
Kleinbuchstaben ersetzt, z. B.: Zur Botschaft WM_PAINT gehoert
die Funktion OnPaint, zur Botschaft WM_RBUTTONDOWN gehoert die
Funktion OnRButtonDown.
J. Dankert: C++-Tutorial
//
//
Im Zweifelsfall sollte man schon deshalb im Handbuch oder der
//
On-Line-Hilfe nachsehen, weil man sich ueber die uebergebenen
//
Argumente informieren muss. Diese werden nicht (wie an die
//
Fensterfunktion, vgl. Abschnitt 9.3) in einem "Einheitsformat"
//
uebergeben, so dass man sich wie bei den Maus-Botschaften die
//
Koordinaten erst durch Zerlegen eines long-Wertes ermitteln
//
muss, sondern in einer aufbereiteten Form, die zur Botschaft
//
passt.
//
//
Die Aktionen, die OnPaint in diesem Programm ausfuehrt, werden
//
weiter unten ausfuehrlich kommentiert.
//
// Zu c) Das Makro DECLARE_MESSAGE_MAP () muss genau einmal in einer
//
Klassen-Deklaration stehen, auch wenn fuer mehrere Botschaften
//
Behandlungsroutinen definiert werden. Man beachte, dass die
//
Makrozeilen NICHT durch ein Semikolon abgeschlossen werden
//
duerfen.
//
// Zu d) Daten und Code der "Message Map" werden von den Makros erzeugt,
//
die von den beiden Makros
//
//
BEGIN_MESSAGE_MAP (theClass , baseClass)
//
...
//
END_MESSAGE_MAP
()
//
//
eingerahmt werden muessen.
//
//
Die beiden Argumente des Makros BEGIN_MESSAGE_MAP identifizieren
//
die Klasse, fuer die die Behandlungsroutinen geschrieben werden,
//
und deren Basisklasse. Damit wird eine Strategie des
//
Durchsuchens der Klassen nach Behandlungsroutinen fuer
//
Botschaften sichtbar: Wenn in einer "Message Map" einer Klasse
//
kein Eintrag gefunden wird, kann die "Message Map" der
//
Basisklasse durchsucht werden, die gegebenenfalls wieder auf
//
ihre Basisklasse veweist.
//
//
Zwischen BEGIN_MESSAGE_MAP und END_MESSAGE_MAP stehen auch
//
Makros, jeweils eins fuer die zu behandelnde Botschaft, die in
//
der Klasse eine eigene Behandlungsroutine besitzt. Fuer die
//
meisten WM_-Botschaften gilt, dass der Makroname durch
//
Voranstellen von 'ON_' gebildet wird und dass die Makros keine
//
Argumente erwarten, zur Botschaft WM_PAINT gehoert also das
//
Makro ON_WM_PAINT (). Diese Aussage gilt z. B. nicht fuer die
//
WM_COMMAND-Botschaft, mit der in Abhaengigkeit von
//
Identifikatoren unterschiedliche Behandlungsroutinen
//
angesteuert werden sollen. Das dazu gehoerende Makro ON_COMMAND
//
verarbeitet zwei Argumente (Identifikator und anzusteuernde
//
Methode).
//
//
//
//
//
Gegenueber dem Programm mimimfc2.cpp wurde in der Methode InitInstance
der Aufruf der CWnd-Methode CWnd::UpdateWindow ergaenzt (//3), die der
aus dem Abschnitt 9.4 bekannten Funktion UpdateWindow entspricht. Sie
erzeugt die Botschaft WM_PAINT, so dass das Neuzeichnen des FensterInhalts ausgeloest wird. Darum kuemmert sich die Methode OnPaint.
//
//
//
//
//
//
//
//
//
//
//
Aus dem Kapitel 9 ist bekannt, dass fuer das Zeichnen in einem Fenster
ein "Device Context" benoetigt wird, der dort durch den Aufruf der
Funktion BeginPaint beschafft wurde. In der objektorientierten
Programierung mit den "Microsoft Foundation Classes" ist eine
Basisklasse CPaintDC verfuegbar, und durch das Erzeugen einer Instanz
dieser Klasse (//8) werden alle Hilfsmittel bereitgestellt, ein "Device
Context" und alle Methoden der Klasse, die fuer die Zeichenoperationen
benoetigt werden, z. B. die Methode DrawText (//11), die der
gleichnamigen Funktion entspricht, die aus dem Abschnitt 9.5.1 bekannt
ist.
269
J. Dankert: C++-Tutorial
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
270
Der Konstruktor der Klasse CPaintDC erwartet einen Pointer auf
das Fenster-Objekt, fuer das der "Device Context" benoetigt wird.
Da OnPaint zur Klasse dieses Fenster-Objektes gehoert, kann der
this-Pointer uebergeben werden. Um die Freigabe des "Device Contextes"
(vgl. die im Abschnitt 9.5.1 beschriebene Funktion EndPaint) braucht
sich der Programmierer hier nicht zu kuemmern, weil das der Destruktor
der Klasse CPaintDC erledigt, der automatisch aufgerufen wird, wenn
das Objekt der Klasse seine Gueltigkeit verliert (hier also am Ende
der Methode OnPaint).
Vor dem Aufruf von DrawText werden die Abmessungen der Zeichenflaeche
("Client Area") ermittelt. Auch hierbei gibt es eine kleine Neuerung:
GetClientRect wird mit dem Pointer auf eine Instanz der Klasse CRect
aufgerufen. Die Klasse CRect entspricht der Struktur RECT (vgl.
Abschnitt 9.5.1), enthaelt allerdings noch einige sehr nuetzliche
Methoden zur Bearbeitung ihrer Daten, z. B. sind die Operatoren
== (Test auf Gleichheit), = (Zuweisung durch Kopieren), +, -, +=
und -= und weitere Operatoren sehr sinnvoll ueberladen. Die Funktion
GetClientRect kann sowohl mit einer RECT-Variablen als auch mit einer
CRect-Instanz aufgerufen werden. Als Methode der Klasse CWnd ist sie
(ueber CFrameWnd) an CMainFrame vererbt worden, kann also in
CMainFrame::OnPaint direkt aufgerufen werden.
13.4 Fazit aus den beiden Beispiel-Programmen
Die beiden in den Abschnitten 13.2 und 13.3 vorgestellten Programme lassen folgende
vorläufigen Schlußfolgerungen zu:
◆
Das Arbeiten mit den "Microsoft Foundation Classes" ist sicher eine besonders
elegante Variante der Windows-Programmierung. Es verlangt allerdings fundierte
Kenntnisse in der Programmiersprache C++. Die persönlichen Erfahrungen des
Schreibers dieser Zeilen belegen jedoch, daß gerade bei der doch recht schwierigen
Windows-Programmierung die Fehlerrate bei der objektorientierten Vorgehensweise
deutlich geringer ist.
◆
Wer die beiden Kapitel 9 und 10 dieses Tutorials durchgearbeitet hat, findet sich in
der Programmierung mit MFC sehr schnell zurecht, weil ihm hinsichtlich der
Windows-Problematik (bis hin zur Gleichheit der Namen für die Funktionen) sehr
vieles vertraut vorkommt. Wenn er trotzdem beim Durcharbeiten des Kapitels 13
Probleme hat, ist dies mit großer Wahrscheinlichkeit auf Defizite zurückzuführen, die
in der objektorientierten Denkweise zu suchen sind. Wahrscheinlich ist dann ein
Nacharbeiten der entsprechenden Abschnitte des Kapitels 12 ratsam.
Das Problem beim Entwerfen einer geeigneten Klassen-Hierarchie für das zu schreibende
Programm, das (in den beiden Beispielen dieses Kapitels noch nicht behandelte) Zusammenspiel von Ressourcen mit den Methoden der Klassen und viele andere Probleme führen erst
bei "ernsthaften" Programmen zu nicht zu unterschätzenden Schwierigkeiten. Dann ist jede
Hilfe im "handwerklichen" Bereich der Programmierung willkommen. Wer sich beim
Anlegen der "Message Maps", das im Programm des Abschnitts 13.3 demonstriert wurde,
oder beim Organisieren der Zusammenarbeit von Ressourcen mit den Behandlungsroutinen
der Botschaften einmal vom "Class Wizard" unterstützen ließ, möchte diese Hilfe kaum
wieder vermissen. Deshalb widmet sich das folgende Kapitel dem Erzeugen und Bearbeiten
von MFC-Programmen mit "App Wizard", "App Studio" und "Class Wizard".
271
J. Dankert: C++-Tutorial
Früher schrieben Fachleute und Spezialisten Programme. Heute werden "Class Objects von Wizards und
Gurus generiert".
Die deutsche Sprache wird immer ausdrucksvoller.
14
MS-Visual-C++-Programmierung mit "App Wizard",
"Class Wizard" und "App Studio"
Neben der integrierten Entwicklungsumgebung "Visual Workbench" (Editor, Projektmanagement, Compiler, Linker, Browser, Debugger) bietet MS-Visual-C++ drei starke Tools für die
Unterstützung beim Schreiben von Programmen:
◆
Der "App Wizard" generiert automatisch ein Programmgerüst (vgl. Kapitel 11).
◆
Der Ressourcen-Editor "App Studio" dient zum Erzeugen und Bearbeiten von
Menüs, Dialog-Boxen, String-Tables, Icons und Bitmaps.
◆
Mit dem "Class Wizard" werden die Klassen verwaltet, er generiert KlassenDeklarationen und Code-Gerüste für Methoden und sorgt für eine sichere Verbindung
der Windows-Botschaften über die mit dem "App Studio" generierten Identifikatoren
mit den dazugehörenden Behandlungs-Routinen.
Die Leistungsfähigkeit und das Zusammenspiel dieser Tools wird erst an etwas umfangreicheren Projekten deutlich. Deshalb wird in diesem Kapitel ein Projekt immer wieder
aufgegriffen, das schon im Kapitel 12 in verschiedenen kleinen Programmen zu finden war,
die Berechnung von Kennwerten für zusammengesetzte ebene Flächen. Vorab sind jedoch
einige Grundbegriffe zu klären.
14.1 Trennen von Klassen-Deklarationen und Methoden
Die von "App Wizard" erzeugten Programme werden in einer größeren Anzahl von Dateien
abgelegt, wobei zu einer generierten Klasse stets zwei Dateien gehören: In einer Datei mit
der Extension .h ("Header-Datei") befindet sich die Deklaration der Klasse, in einer Datei mit
der Extension .cpp befindet sich der Code für die Methoden. Diese Trennung ist ausgesprochen sinnvoll, weil andere Klassen nur auf die in den Header-Dateien untergebrachten
Informationen zugreifen (und damit nur diese Dateien einbinden) müssen.
Auch ohne Verwendung von "App Wizard" ist diese Trennung empfehlenswert, das Beispiel
aus dem Abschnitt 13.3 hätte man folgendermaßen in zwei Dateien zerlegen können (das
noch konsequentere Aufteilen in vier Dateien, für jede Klasse jeweils eine .h- und eine .cppDatei wäre bei diesem kleinen Beispiel doch übertrieben):
272
J. Dankert: C++-Tutorial
#include "hllmfc2.h"
CHllmfc2App
theApp ;
BOOL CHllmfc2App::InitInstance ()
{
m_pMainWnd = new CMainFrame ;
m_pMainWnd->ShowWindow (m_nCmdShow) ;
m_pMainWnd->UpdateWindow () ;
return TRUE ;
}
BEGIN_MESSAGE_MAP (CMainFrame , CFrameWnd)
ON_WM_PAINT ()
END_MESSAGE_MAP ()
CMainFrame::CMainFrame ()
{
Create (NULL , "Programm HLLMFC2") ;
}
#include <afxwin.h>
class CHllmfc2App : public CWinApp
{
public:
virtual BOOL InitInstance () ;
} ;
class CMainFrame : public CFrameWnd
{
public:
CMainFrame () ;
protected:
afx_msg void OnPaint () ;
DECLARE_MESSAGE_MAP ()
} ;
Header-Datei hllmfc2.h
void CMainFrame::OnPaint ()
{
CPaintDC dc (this) ;
CRect
rect
;
GetClientRect (&rect) ;
dc.DrawText ("Hello MFC-World!" , -1 , &rect ,
DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
}
Programm hllmfc2.cpp aus dem Abschnitt 13.3: Auslagern der Klassen-Deklarationen in eine Header-Datei
14.2 Das "Document-View"-Konzept
In den "Microsoft Foundation Classes" existiert der Basis-Code für alle zu schreibenden
Anwendungen. Die beiden Beispiel-Programme des Kapitels 13 zeigten, daß ihnen nur noch
das "Programm-Gerüst" gegeben werden muß, das das Zusammenwirken der Klassen
organisiert. Genau an dieser Stelle wird der Programmierer vom "App Wizard" unterstützt.
Dieser erzeugt ein Programm-Gerüst, das für zahlreiche Grundfunktionen das Zusammenarbeiten der Klassen bereits organisiert und für einige Funktionen, die der Programmierer mit
großer Wahrscheinlichkeit brauchen wird, bereits den Rahmen generiert, z. B.:
void CHllmfc1View::OnBeginPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/)
{
// TODO: add extra initialization before printing
}
... ist ein Beispiel für einen typischen leeren Rahmen für eine Methode, den der "App
Wizard" generiert hat. Dem Programmierer ist es natürlich in diesem Fall freigestellt, ob er
diesen Rahmen füllen möchte.
Wenn der Programmierer sich vom "App Wizard" ein Programm-Gerüst generieren läßt, muß
er die Programm-Architektur, die ihm vorgesetzt wird, akzeptieren. Das Gerüst ist nach dem
sogenannten "Document-View"-Konzept aufgebaut, das seit etwa Mitte der achtziger Jahre
in zahlreichen Software-Entwicklungen genutzt wird. Nach diesem Konzept wird zwischen
"Dokumenten" und "Ansichten" getrennt. Der etwas unglücklich gewählte Name suggeriert
etwas zu stark die Vorstellung vom "Textverarbeitungs-Programm mit Dokumenten und
Layout-Funktionen", ist sicherlich auch in Anlehnung an den Aufbau von TextverarbeitungsSoftware entstanden, muß aber in einem wesentlich weiteren Sinne verstanden werden. Es
J. Dankert: C++-Tutorial
273
mag für einfache Anwendungsprogramme als überzogene und für bestimmte Applikationen
als unpassende Architektur angesehen werden, bei "einigem guten Willen" kann man damit
aber fast jedes Programm recht sinnvoll strukturieren.
Zum "Objekt" Dokument gehören die Daten, die die mit dem Programm zu lösende Aufgabe
beschreiben, und die Methoden, mit denen diese Daten manipuliert werden können. Bei
einem Textverarbeitungsprogramm sind dies z. B. die Text-Datei, die Datenstruktur des
Textsegments, das gerade bearbeitet wird, und alle Funktionen, mit denen der Text verändert
werden kann.
Ein Dokument kann in verschiedenen Ansichten ("Views") präsentiert werden, im
Textverarbeitungsprogramm z. B. als Textausschnitt mit oder ohne Steuerzeichen oder als
Druckervorschau usw. Zu einer "Ansicht" gehört immer eindeutig ein Dokument, während zu
einem Dokument mehrere Ansichten gehören können.
Der "App Wizard" ermöglicht die Erstellung von Programmgerüsten für "SDI-Anwendungen"
("Single Document Interface"), die nur ein Dokument verwalten können, und "MDIAnwendungen" ("Multiple Document Interface"), die das Arbeiten mit mehreren Dokumenten
gestatten.
Das nebenstehende Bild
zeigt ein MDI-Programm,
mit dem gerade "zwei
Dokumente" völlig unabhängig voneinander
bearbeitet werden (Rechteck mit zwei Kreisausschnitten bzw. Dreieck
mit Rechteckausschnitt).
Während für das "Dokument Dreieck mit Rechteckausschnitt" eine Ansicht (graphische Darstellung) sichtbar ist, werden
für das "Dokument Rechteck mit zwei Kreisausschnitten" zwei Ansichten
(hier in einem sogenannten "Splitter-Window") gezeigt, eine graphische Darstellung und eine Ergebnisliste.
In den "Microsoft Foundation Classes" werden Dokumente z. B. durch Objekte der Klasse
CDocument repräsentiert, Ansichten z. B. durch Objekte der Klasse CView, deren Basisklasse
sinnvollerweise die Klasse CWnd ist, so daß alle Methoden dieser Klasse auch in der Klasse
CView verfügbar sind.
In der Regel kommuniziert der Benutzer über die Ansichtsklasse mit dem Programm, gibt
z. B. Daten ein, die natürlich auch in der Dokument-Klasse abgelegt und verarbeitet werden
müssen, was wiederum ein Aktualisieren der Ansichten erfordert. Die Kommunikation der
Methoden dieser beiden Klassen ist ein wichtiges Thema, das im Anschluß an eine erste
Untersuchung eines vom "App Wizard" generierten Programm-Gerüstes behandelt wird.
274
J. Dankert: C++-Tutorial
14.3 Das vom "App Wizard" erzeugte "Hello World"-Programm
Das im Abschnitt 11.2 mit dem "App Wizard" erzeugte Projekt "hllmfc1" lieferte ein
ausführbares Programm, das die gleiche Funktionalität wie das außerordentlich einfache
Programm "hllmfc2.cpp" hat, das im Abschnitt 14.1 gemeinsam mit seiner Include-Datei
"hllmfc2.h" aufgelistet wurde. Das mit dem "App Wizard" erzeugte Projekt besteht unter
anderem aus 5 .cpp-Dateien, 6 .h-Dateien, 2 .rc-Dateien (Ressourcen), einer .bmp-Datei
(Bitmap), einer .ico-Datei (Icon) und mehrere Verwaltungsdateien (Makefile, Definitions-File
für den Linker, ...). Freundlicherweise wird sogar eine README.TXT-Datei generiert, in der
man nachlesen kann, was die anderen Dateien enthalten.
In den zahlreichen Dateien ist natürlich eine große Menge an "schlafender Funktionalität"
versteckt. Glücklicherweise sind es zunächst nur wenige Dateien, mit denen man sich als
Programmierer befassen muß. Um etwas "Licht in den Dschungel" zu bringen, wird zunächst
nach dem Code gesucht, der äquivalent zu den wenigen Zeilen des Programms im Abschnitt
14.1 ist (schließlich erledigt dieses kleine Programm die gleiche Aufgabe).
Es ist sicher eine lobenswerte Absicht, sofort möglichst viel verstehen zu wollen,
aber es besteht absolut kein Grund zur Verzweiflung, wenn Sie die nachfolgenden
Ausführungen dieses Abschnitts nicht komplett verstehen. Einiges wird ohnehin
vom Programmierer nicht benötigt, weil der "App Wizard" dies zuverlässig
erledigt.
Sie sollten allerdings diesen Abschnitt nicht völlig überspringen, weil manches in
den nachfolgenden Abschnitten verständlicher wird, wenn man die
Zusammenhänge wenigstens erahnen kann.
◆
Die Datei hllmfc1.h ist der Header-File der Applikations-Klasse. Sie ist kurz und
übersichtlich und wird deshalb nachfolgend komplett aufgelistet:
// hllmfc1.h : main header file for the HLLMFC1 application
//
#ifndef __AFXWIN_H__
#error include 'stdafx.h' before including this file for PCH
#endif
#include "resource.h"
// main symbols
///////////////////////////////////////////////////////////////////////
// CHllmfc1App:
// See hllmfc1.cpp for the implementation of this class
//
class CHllmfc1App : public CWinApp
{
public:
CHllmfc1App();
// Overrides
virtual BOOL InitInstance();
J. Dankert: C++-Tutorial
275
// Implementation
//{{AFX_MSG(CHllmfc1App)
afx_msg void OnAppAbout();
// NOTE - the ClassWizard will add and remove member functions here.
// DO NOT EDIT what you see in these blocks of generated code !
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
///////////////////////////////////////////////////////////////////////
Hervorgehoben wurden genau die Zeilen, die sich auch in der Datei hllmfc2.h im
Abschnitt 14.2 finden. Man beachte, daß der "App Wizard" schon durch besondere
Kommentarzeilen eingerahmte Bereiche vorsieht, in denen später der "Class Wizard"
Code ergänzt.
◆
Der Code für die Applikationsklasse findet sich in der Datei hllmfc1.cpp, die hier
nur ausschnittweise gelistet wird:
// hllmfc1.cpp : Defines the class behaviors for the application.
// ...
///////////////////////////////////////////////////////////////////////
// The one and only CHllmfc1App object
CHllmfc1App NEAR theApp;
///////////////////////////////////////////////////////////////////////
// CHllmfc1App initialization
BOOL CHllmfc1App::InitInstance()
{
// ...
// Register the application's document templates. Document templates
// serve as the connection between documents, frame windows and views.
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CHllmfc1Doc),
RUNTIME_CLASS(CMainFrame),
// main SDI frame window
RUNTIME_CLASS(CHllmfc1View));
AddDocTemplate(pDocTemplate);
// create a new (empty) document
OnFileNew();
// ...
return TRUE;
}
Hier wurden die Teile hervorgehoben, die den Programmzeilen in der Datei
hllmfc2.cpp entsprechen: Es wird das Applikations-Objekt erzeugt ("The one and
only"). Daß dies nit dem NEAR-Attribut geschieht, sollten Sie einfach übersehen (mit
dem Übergang auf die 32-Bit-Programmierung stirbt dieses Relikt ohnehin).
Etwas komplizierter als in der Datei hllmfc2.cpp sieht die Methode InitInstance ()
aus, was mit der "Document-View"-Architektur zusammenhängt: Zunächst wird ein
276
J. Dankert: C++-Tutorial
Objekt der Klasse CSingleDocTemplate erzeugt, mit dem das "Single Document
Interface" (SDI) implementiert wird, indem vom Konstruktur die Bezüge zwischen der
Dokument-Klasse, der Ansichtsklasse und der Klasse für das Hauptrahmenfenster
hergestellt werden. Nehmen Sie das zunächst einfach mal so hin. Das Erzeugen und
Anzeigen des Hauptrahmenfensters (Klasse CMainFrame), das in der InitInstanceMethode der Datei hllmfc2.cpp zu sehen ist, wird hier tief im Programmgerüst über
den Aufruf von OnFileNew erledigt.
Zunächst sollten Sie also registrieren, daß ein Objekt der Klasse CMainFrame erzeugt und
angezeigt wird, die Deklaration der Klasse findet man in der Datei mainfrm.h, den
zugehörigen Code in der Datei mainfrm.cpp. Sie können durchaus darauf verzichten, sich
diese Dateien anzusehen, auf die Anschlußpunkte für eigene "Zutaten" wird später eingegangen.
Geklärt werden muß allerdings noch, wie mit dem vom "App Wizard" erzeugten Programm
die Ausgabe von "Hello, MFC-World!" auf den Bildschirm gebracht wird, denn auch dabei
gibt es nicht unerhebliche Unterschiede zum Programm hllmfc2.cpp, weil nicht mehr direkt
das Window (und damit die Klasse CMainFrame) der Ansprechpartner ist, sondern die
"View" (Ansichtsklasse):
◆
Die Deklaration der Ansichtsklasse CHllmfc1View, die von der Basisklasse CView
abgeleitet wird, findet man in der Datei hllmfvw.h. Die entscheidende Zeile lautet:
virtual void OnDraw (CDC* pDC);
// overridden to draw this view
Die Methode OnDraw ist das Pendant zu OnPaint, mit der im Programm
hllmfc2.cpp in das Fenster gezeichnet wird, wobei folgende Unterschiede zu beachten
sind:
Während die Zuordnung der Botschaft WM_PAINT zur zugehörigen Behandlungsroutine OnPaint über das Konzept der "Message Maps" realisiert
wird (vgl. Abschnitt 13.3), ist OnDraw in den "Microsoft Foundation Classes"
als virtuelle Methode deklariert, die in der abgeleiteten Ansichtsklasse
überschrieben werden muß (der "App Wizard" hat das bereits erledigt).
Der Programmierer darf die Vorstellung haben, daß OnPaint über die
"Message Maps" angesteuert wurde, einen "Device Context" angefordert hat
und mit diesem als Argument die Methode OnDraw aufruft.
Der Vorteil für den Programmierer liegt einmal darin, daß er den "Device
Context" bereits geliefert bekommt. Zum anderen wird OnDraw auf entsprechend modifiziertem Weg (über OnPrint) auch für Druckerausgaben
gerufen und empfängt auch in diesem Fall den geeigneten "Device Context".
◆
Der Code für die Ansichtsklasse befindet sich in der Datei hllmfvw.cpp. Dort findet
man auch das bereits vorbereitete Gerüst für OnDraw in der Form:
void CHllmfc1View::OnDraw(CDC* pDC)
{
CHllmfc1Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
}
277
J. Dankert: C++-Tutorial
Angeliefert wird der Pointer auf den "Device Context" pDC, mit dem auf alle
Methoden der Basisklasse CDC zugegriffen werden kann. Mit den (weit über 100)
CDC-Methoden wird die Ausgabe realisiert.
Die bereits vom "App Wizard" vorgesehenen beiden Programmzeilen sind (fast
immer) sinnvoll. GetDocument () liefert einen Pointer auf die Dokument-Klasse, mit
dem auf die Daten und die Methoden zugegriffen werden kann, die die eigentliche
Funktionalität des Programms bestimmen. Weil "Hello, MFC-World!" überhaupt noch
keine eigenen Daten vewaltet, können die beiden Zeilen in diesem Fall herausgenommen werden (sie stören allerdings auch nicht).
ASSERT_VALID ist übrigens ein Makro, das nur in der Debug-Version aktiv wird.
Es testet die Gültigkeit des übergebenen Arguments (würde in diesem Fall eine
Warnung ausgeben, wenn ein NULL-Pointer von GetDocument abgeliefert worden
wäre) und gehört zu den vielen Vorsichtsmaßnahmen, die der "App Wizard" in das
generierte Programm eingebaut hat.
Um das "Hello, MFC-World!"-Programm zu komplettieren, muß die Methode
OnDraw nur um die folgenden Zeilen ergänzt werden:
RECT rect ;
GetClientRect (&rect) ;
pDC->DrawText ("Hello, MFC-World!" , -1 , &rect ,
DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
Dies sind fast exakt die Zeilen, die auch das Programm hllmfc2.cpp in OnPaint hat,
nur wurde dort DrawText mit einer selbst erzeugten Instanz der CPaintDC-Klasse
aufgerufen, während in OnDraw der Aufruf mit dem Pointer auf den "Device
Context" erfolgt, der angeliefert wird.
Was muß der Programmierer vom Programmgerüst, das der "App Wizard"
erzeugt, unbedingt wissen?
◆
Der "App Wizard" bereitet eine Dokumentklasse vor, die mit einem zum
Projektnamen passenden Namen versehen wird. Im Projekt hllmfc1 findet
man die Deklaration der Klasse CHllmfc1Doc in der Datei hllmfdoc.h.
Dies sollte der (in hllmfc1 ungenutzte) Anschlußpunkt für die
Datenstruktur der Applikation sein. Die zugehörigen Methoden sollten in
der mit dem gleichen Namen (aber der Extension .cpp) erzeugten Datei
untergebracht werden.
◆
Die vorbereitete Ansichtsklasse hat einen ebenso passenden Namen, im
Projekt hllmfc1 ist es die Klasse CHllmfc1View, deren Deklaration in der
Datei hllmfvw.h steht. In der zugehörigen .cpp-Datei ist bereits ein Gerüst
für die Methode OnDraw vorgesehen, das schon eine Programmzeile
enthält, die einen Pointer auf die Dokumentklasse liefert. Da OnDraw
einen Pointer auf den "Device Context" erhält, sind die wichtigsten
Bezüge damit hergestellt.
278
J. Dankert: C++-Tutorial
14.4 Das Projekt "fmom"
Das in diesem Abschnitt beschriebene etwas umfangreichere Projekt "fmom" soll Schritt für
Schritt verschiedene Aspekte der Entwicklung eines C++-Programms unter Einbeziehung der
"Microsoft Foundation Classes" (MFC) und der Entwicklungstools "Visual Workbench",
"App Wizard", "Class Wizard" und "App Studio" aus dem MS-Visual-C++-Produkt beschreiben.
Empfehlenswert ist, die einzelnen Schritte, die nach und nach zu immer erweiterten
Versionen des Programms führen, mit dem Computer und der MS-Visual-C++-Software
tatsächlich auszuführen. Die Schritte, die dafür erforderlich sind, werden mit dem Symbol ➪
angekündigt. Da die am Ende der Unterabschnitte jeweils entstehende Version stets der
Ausgangspunkt für die Weiterentwicklung im nachfolgenden Unterabschnitt ist und die
Dateien für jede Version zum Tutorial gehören, kann man jedoch auch an beliebiger Stelle
einsteigen, um einen ganz speziellen Aspekt nachzuempfinden.
14.4.1
Die mit "fmom" zu realisierende Funktionalität
Der Name "fmom" steht für "Flächenmomente". Es soll ein Programm werden, mit dem für
ebene Flächen, die aus Teilflächen zusammengesetzt sind, zunächst die Gesamtfläche, die
statischen Momente bezüglich der Achsen eines kartesischen Koordinatensystems und daraus
die Lage des Gesamtschwerpunkts berechnet werden können. Es wird so angelegt, daß es
ohne Schwierigkeiten auf die Berechnung weiterer nützlicher Werte für Ingenieur-Aufgaben
wie die Momente 2. Grades ("Flächenträgheitsmomente"), Hauptträgheitsmomente und
Hauptzentralachsen erweitert werden kann.
Diese mit wenigen theoretischen Grundlagen zu beschreibende Aufgabenstellung wurde
bewußt gewählt, um nicht von den eigentlich zu behandelnden Programmierproblemen
abzulenken. In den ersten Versionen wird nicht mehr benötigt, als bereits in den Abschnitten
12.3 und 12.4 für die Realisierung der Programme schwerp1.cpp und schwerp3.cpp
erforderlich war. Trotzdem werden am Beginn der jeweiligen Abschnitte die benötigten
Formeln noch einmal zusammengestellt.
Das Programm wird so konzipiert, daß beliebige Flächen einzubeziehen sind, in den ersten
Versionen werden nur Kreise und Rechtecke (als Teilflächen bzw. Ausschnitte) realisiert. Die
Datenstruktur, die rechnerintern das Ensemble von Teilflächen definiert, wird unter
Verwendung verketteter Listen bei Nutzung der in den "Microsoft Foundation Classes" dafür
verfügbaren Hilfsmittel definiert. Die Daten werden über Dialog-Boxen eingegeben, die nach
der Auswahl entsprechender Menüpunkte geöffnet werden. Erst nach diesen Vorbereitungen
kann mit dem Programm etwas ausgerechnet werden.
Das komplette Programm gehört übrigens zur CAMMPUS-Software und kann über die
Internet-Adresse
http://www.fh-hamburg.de/rzbt/dnksoft/cammpus
frei kopiert werden.
J. Dankert: C++-Tutorial
14.4.2
279
Erzeugen des Projektes (Version "fmom1")
Die Version "fmom1" wird noch gar keine Funktionalität haben, an der automatisch
generierten Version werden ausschließlich "kosmetische Korrekturen" vorgenommen:
➪
Nach dem Start der "Visual Workbench" aus dem Windows-Programm-Manager
werden zunächst (vorsichtshalber) alle eventuell noch geöffneten Dateien eines
offenen Projekts geschlossen (Angebot Close All im Menü Window, wenn dieses
verfügbar ist). Danach wird im Menü Project die Option App Wizard... gewählt. Es
erscheint die Dialog-Box "MFC App Wizard".
➪
In einer sogenannten "Group Box", die mit "Project Path" beschriftet ist, werden u. a.
das aktuelle Verzeichnis und ein Ausschnitt aus der Verzeichnisstruktur angezeigt.
Man sollte hier das aktuelle Verzeichnis einstellen, unter dem das Verzeichnis für
das zu definierende Projekt angelegt werden soll. Wenn man in das Feld "Project
Name" den Namen fmom einträgt
(bitte noch nicht die Return-Taste
drücken), wird der Name des von "App
Wizard" zu erzeugenden Unterverzeichnisses im Feld "New Subdirectory"
angezeigt. Im oberen Teil der "Group
Box" erscheint der komplette Pfadname
des zu erzeugenden "Makefiles".
Die nebenstehende Abbildung zeigt die
Dialog-Box mit dem bereits eingetragenen
Projekt-Namen fmom. Als aktuelles Verzeichnis ist hier d:\manuals\c\win_prg eingestellt,
dementsprechend wird in einem darin erzeugten Unterverzeichnis fmom u. a. die Datei
d:\manuals\c\win_prg\fmom\fmom.mak
(Makefile) erzeugt werden.
➪
Der Button Options wird angeklickt, es erscheint die "Options"-Dialog-Box. Die
Voreinstellungen werden akzeptiert, nur in der "Group Box" mit der Beschriftung
"Memory Model" wird (wegen der
"Kompatibilität mit der Zukunft") die
Voreinstellung Medium in Large
geändert. Wenn die Dialog-Box wie
nebenstehend aussieht, wird der OKButton angeklickt. Es erscheint wieder
die "MFC AppWizard"-Dialog-Box.
Mit dem Akzeptieren der Einstellung "Multiple
Document Interface" wurde entschieden, daß
eine MDI-Anwendung erzeugt wird. Dies ist
für das Projekt "fmom" durchaus sinnvoll, weil
dann mehrere unterschiedliche Berechnungen
parallel bearbeitet werden können.
J. Dankert: C++-Tutorial
280
➪
In der "MFC AppWizard"-Dialog-Box wird der Button Classes... angeklickt, es
erscheint die "Classes"-Dialog-Box. In einem Listenfeld wird angezeigt, welche
Klassen erzeugt werden, in den "Edit-Feldern" darunter kann man gegebenenfalls die
Namen der Klassen und der Files, in denen die Deklarationen bzw. der Code für die
Methoden untergebracht werden, ändern. Natürlich kann auch alles akzeptiert werden,
eine kleine Ergänzung ist jedoch angebracht: Man wählt im Listenfeld die
Klasse CFmomDoc und trägt in das
Edit-Feld mit der Beschriftung File
Extension: z. B. fmo ein. Damit legt
man die Standard-Dateinamen-Extension für die Sicherung der DokumentDaten fest (wird im Abschnitt 14.4.11
behandelt). Wenn die "Classes"-DialogBox das nebenstehende Aussehen hat,
kehrt man durch Anklicken des Buttons
OK zur "MFC AppWizard"-DialogBox zurück.
➪
Damit sind alle Einstellungen für das Projekt festgelegt, auch in der "MFC AppWizard"-Dialog-Box kann nun der OK-Button angeklickt werden. Es erscheint die
nachstehend zu sehende "New Application Information". Man findet alle Einstellungen noch einmal zusammenhängend gelistet. Es ist die letzte Chance (Button
Cancel), noch einmal zu den oben beschriebenen Dialog-Boxen zurückzukehren,
wenn eine Einstellung nicht wie gewünscht angezeigt wird.
➪
Wenn in der "New Application Information"-Dialog-Box der Button Create angeklickt wird, erzeugt der "App Wizard" alle Files des Projekts.
Compiler und Linker könnten nun (vom gerade erzeugten Makefile gesteuert) in Aktion
treten, vorher sollen noch einige kleine Änderungen ausgeführt werden:
J. Dankert: C++-Tutorial
281
➪
Im Menü Tools wird App Studio
gewählt. Das Programm "App
Studio" compiliert (mit dem sogenannten "Resource Compiler") die
vom "App Wizard" erzeugte recht
umfangreiche Ressourcen-Datei und
zeigt in einer Liste (links) an, daß 6
verschiedene Ressourcen bereits
existieren und bearbeitet werden
können. Zunächst wird String Table
(durch Anklicken) gewählt, im
rechten Fenster erscheint eine Liste
der verfügbaren String-Segmente
(nebenstehendes Bild).
➪
Man kann ein beliebiges String-Segment auswählen, immer erscheint die komplette
Liste der String-Ressourcen (die Auswahl des String-Segments entscheidet nur
darüber, welcher String als erster im "Scroll Window" erscheint, erreichbar sind
immer alle Strings). Wählt man z. B. String Segment 0 (durch Anklicken und
anschließendes Klicken auf den Button Open, schneller durch Doppelklick auf String
Segment 0), dann kann man auf alle vom "App Wizard" generierten Strings zugreifen
(und z. B. mit der Übersetzung in die deutsche Sprache beginnen). Hier sollen
exemplarisch 2 Strings bearbeitet werden: Der zum Idenfikator IDR_MAINFRAME
gehörende String (ist die
Überschrift des Hauptrahmenfensters) wird durch
Doppelklick in ein Bearbeitungsfenster transportiert und dort geändert
zu: Programm FMOM
(Flächenmomente). Nach
Drücken der Return-Taste
verschwindet das Bearbeitungsfenster. Auf entsprechende Weise wird der
zu dem Identifikator
AFX_IDS_IDLEMESSAGE gehörende String
Ready durch das Wort Fertig ersetzt.
Es ist vielfach sehr nützlich, daß man in "App Studio" mit mehreren Fenstern
gleichzeitig arbeiten kann. Gegenwärtig ist das "String Table"-Fenster im
Vordergrund. Wenn man im Menü auf Window klickt, sieht man, daß das "FMOM.RC
(MFC Resource Script)"-Fenster auch noch aktiv ist. Mit Tile Horizontal (im Menü
Window) erreicht man z. B., daß beide Fenster zu sehen sind. Man braucht also ein Fenster,
in dem man die Arbeit gerade beendet hat, nicht zu schließen, bevor man das nächste Fenster
öffnet.
Tip:
J. Dankert: C++-Tutorial
➪
282
Im Fenster "FMOM.RC (MFC Resource Script)" (erreichbar gemacht z. B. mit Tile
Horizontal, wie gerade beschrieben) wird im linken mit Type: beschrifteten
Listenfeld Dialog angeklickt. Im rechten Listenfeld (mit Resources: beschriftet)
erscheint der Identifikator IDD_ABOUTBOX, der auf eine von "App Wizard"
generierte sehr einfache Dialog-Box verweist. nach Doppelklick auf IDD_ABOUTBOX landet man im Dialog-Editor, die Dialog-Box wird angezeigt und kann
bearbeitet werden. Auch wenn sich der eigene Beitrag zum Programm auf zwei
String-Änderungen beschränkt, soll das Copyright schon einmal gesichert werden:
Nach Doppelklick auf das Wort
"Copyright" in der Dialog-Box
erscheint die "Text Properties"Box, dort wird im Edit-Feld,
das mit Caption: beschriftet ist,
der gewünschte String eingetragen (nebenstehende Abbildung).
Damit sind alle für die Version 1 des Projekts vorzunehmenden Modifikationen erledigt, das
"App Studio" kann verlassen werden:
➪
Im Menü File von "App Studio" wird Exit gewählt, und das Programm fragt, ob die
Änderungen gespeichert werden sollen, was natürlich bestätigt wird. Danach ist
wieder die "Visual Workbench" aktiv, mit der nun das ausführbare Programm erzeugt
werden soll.
➪
Man wählt z. B. im Menü Project der "Visual Workbench" die Option Build
FMOM.EXE (der Toolbar-Button mit den drei abwärts gerichteten Pfeilen erledigt
das auch), Resource-Compiler, Compiler und Linker werden aktiv und erzeugen das
ausführbare Programm.
➪
Das ausführbare Programm wird
gestartet, indem man im Menü
Project auf die Option Execute
FMOM.EXE klickt. Das
Hauptrahmenfenster und das
Fenster für das erste Dokument
erscheinen. Man erkennt die
Auswirkungen der vorgenommenen Änderungen: Das Hauptrahmenfenster zeigt in seiner
Titelleiste die im "String Table"
eingetragene Überschrift, und in
der Statuszeile am unteren Fensterrand steht das deutsche Wort Fertig.
➪
Im Menü Help findet man nur
eine Option: About Fmom...,
nach Auswahl dieser Option
erscheint der geänderte "AboutDialog" (nebenstehende Abbildung).
J. Dankert: C++-Tutorial
14.4.3
283
Datenstruktur für "fmom", Entwurf der Klassen
Folgende Aspekte sind beim Entwurf der Datenstruktur für das Programm "fmom" zu
beachten:
Es müssen verschiedene Arten von Flächen (Kreise, Rechtecke, ...) verarbeitet
werden. Gerade in dieser Hinsicht soll das Programm möglichst problemlos erweitert
werden können.
Jede Fläche kann als Teilfläche oder als Ausschnittfläche definiert werden. Die
Eigenschaft "Teilfläche oder Ausschnittfläche" gehört zu jeder Fläche.
Die Anzahl der Flächen ist zunächst unbestimmt, während des Programmlaufs sollen
Flächen ergänzt und auch gelöscht werden können.
Jede Fläche wird durch einen speziellen Satz von Daten, der auf den Typ der Fläche
zugeschnitten ist, beschrieben. Die Eingabe der Daten soll ebenfalls dem Flächentyp
angepaßt sein.
Folgende Entscheidungen werden deshalb für einen ersten (erweiterbaren) Entwurf der
Datenstruktur getroffen:
◆
Es wird eine Klasse CArea deklariert, die in der ersten Version nur ein Datenelement
(Indikator, ob Teilfläche oder Ausschnitt vorliegt) enthält. Die Klasse wird einige
virtuelle Methoden enthalten, die nur in den aus CArea abgeleiteten Klassen (z. B.
CCircle für Kreisflächen und CRectangle für Rechteckflächen) definiert sind (z. B.
wird die Berechnung des Flächeninhalts nur für die abgeleiteten Klassen definiert), so
daß CArea eine abstrakte Klasse ist, für die keine Instanzen erzeugt werden können.
◆
Für alle Instanzen der abgeleiteten Klassen (CCircle, CRectangle, ...) werden Pointer
in einer Liste von CArea-Pointern verwaltet (wird im Abschnitt 14.4.4 beschrieben).
Dafür wird die Klasse CObList aus den "Microsoft Foundation Classes" verwendet,
die die Methoden für die Verwaltung dieser (doppelt verketteten) Liste bereitstellt.
Zur Erinnerung: Bei der Abarbeitung einer solchen Liste wird die (passende!)
Methode der abgeleiteten Klassen verwendet, wenn sie in der abstrakten Klasse nicht
definiert wurde (Polymorphismus, vgl. Abschnitt 12.4).
◆
Da fast alle abgeleiteten Klassen bei der Beschreibung der ebenen Flächen auch 2DPunkte benutzen, wird eine zusätzliche Klasse CPoint_xy verwendet, für die nach
Bedarf Instanzen in den übrigen Klassen definiert werden (die Basisklasse CPoint aus
den "Microsoft Foundation Classes" ist dafür nicht geeignet, weil sie nur IntegerWerte als Koordinaten zuläßt).
◆
Alle oben genannten Klassen werden in einer Datei areas.h deklariert, der Code für
die zu den Klassen gehörenden Methoden findet sich jeweils in separaten Dateien
(z. B. in area.cpp für die abstrakte Klasse CArea bzw. in circle.cpp für die
abgeleitete Klasse CCircle).
◆
Die (separat deklarierte) problembezogene Datenstruktur wird in der vom "App
Wizard" erzeugten Dokument-Klasse CFmomDoc verankert, indem die Instanz der
Klasse CObList (die oben erwähnte Liste von CArea-Pointern) dort eingefügt wird.
284
J. Dankert: C++-Tutorial
Die Datei areas.h mit den Deklarationen und die Dateien point_xy.cpp, area.cpp, circle.cpp
und rectangl.cpp werden mit dem Editor der "Visual Workbench" erzeugt, z. B.:
➪
Im Menü File der "Visual Workbench" wird Open gewählt, als File Name: wird
areas.h angegeben, nach OK (bzw. Return) merkt die "Visual Workbench" an, daß
"The file ... does not exist. Would you like to create it?". Nach erneuter Bestätigung (OK bzw. Return) wird der Text eingegeben (Sie sollten sich das Eintippen
allerdings ersparen und die Dateien aus der "fmom2-Version" des Tutorials kopieren):
#include <math.h>
const double pi_4 = atan (1.) ;
//
... fuer Kreisberechnung
class CPoint_xy
{
private:
double m_x ;
double m_y ;
public:
CPoint_xy () ;
CPoint_xy (double x , double y) ;
~CPoint_xy () ;
void
set_x (double x) ;
void
set_y (double y) ;
double get_x () ;
double get_y () ;
} ;
Header-Datei areas.h für
die Version "fmom2"
class CArea : public CObject
{
protected:
int m_area_or_hole ;
// 1 --> Flaeche, -1 --> Ausschnitt
public:
CArea
(int area_or_hole = 1) ;
virtual ~CArea
() ;
// ... virtueller Destruktor
void
set_aoh
(int area_or_hole) ;
int
get_aoh
() ;
virtual double get_a () = 0 ;
// Flaeche
virtual double get_xc () = 0 ;
// Schwerpunkt-Koordinate x
virtual double get_yc () = 0 ;
// Schwerpunkt-Koordinate y
} ;
class CCircle : public CArea
{
private:
double
m_d ;
// Durchmesser
CPoint_xy m_mp ;
// Mittelpunkt
public:
CCircle (double d , double mpx , double mpy) ;
double get_a
() ;
double get_xc
() ;
double get_yc
() ;
} ;
class CRectangle : public CArea
{
private:
CPoint_xy m_p1 ;
// Die beiden Eckpunkte muessen ...
CPoint_xy m_p2 ;
// auf einer Diagonalen liegen
public:
CRectangle (double x1 , double y1 , double x2 , double y2) ;
double get_a
() ;
double get_xc
() ;
double get_yc
() ;
} ;
285
J. Dankert: C++-Tutorial
Die Deklarationen in der Datei areas.h sind weitgehend selbsterklärend, deshalb dazu nur
zwei Anmerkungen:
◆
Der Konstruktor in CPoint_xy, der keine Argumente übernimmt, ist erforderlich, weil
Instanzen dieser Klasse in anderen Klassen ohne Initialisierung definiert werden.
◆
In den "Microsoft Foundation Classes" ist CObject gewissermaßen die "Mutter aller
Klassen". Der Overhead, den eine Klasse durch Ableitung aus CObject erbt, ist
minimal, die Vorteile dagegen sind beträchtlich. Die Klasse CArea wird aus CObject
abgeleitet, weil Pointer auf CArea-Instanzen in einer Liste der Klasse CObList
verwaltet werden sollen (wird beschrieben im Abschnitt 14.4.4). Weitere Vorteile der
Ableitung einer Klasse aus CObject werden später behandelt, die Klasse CArea gibt
natürlich alle aus dieser Ableitung gewonnenen Möglichkeiten an die aus ihr
abgeleiteten Klassen (hier bisher: CCircle und CRectangle) weiter.
Die Dateien point_xy.cpp, area.cpp, circle.cpp und rectangl.cpp inkludieren außer der
Header-Datei areas.h noch die vom "App Wizard" erzeugte Datei stdafx.h, die selbst nur die
erforderlichen Standard-Header-Dateien zusammenfaßt. Damit wird auch die für MFCProgramme benötigte Datei afxwin.h (und mit dieser auch windows.h, vgl. Abschnitt 13.2)
eingebunden. Die in den genannten Dateien definierten Methoden sind außerordentlich
einfach und ohne weitere Erläuterungen zu verstehen:
// Datei point_xy.cpp für Version "fmom2"
#include "stdafx.h"
#include "areas.h"
CPoint_xy::CPoint_xy () {}
CPoint_xy::CPoint_xy (double x , double y)
{
m_x = x ;
m_y = y ;
}
CPoint_xy::~CPoint_xy () {}
void
void
double
double
CPoint_xy::set_x
CPoint_xy::set_y
CPoint_xy::get_x
CPoint_xy::get_y
(double x) { m_x = x
(double y) { m_y = y
()
{ return m_x
()
{ return m_y
;
;
;
;
// Datei area.cpp für Version "fmom2"
#include "stdafx.h"
#include "areas.h"
CArea::CArea (int area_or_hole)
{
m_area_or_hole = area_or_hole ;
}
CArea::~CArea () {}
void CArea::set_aoh (int area_or_hole)
{
m_area_or_hole = area_or_hole ;
}
int CArea::get_aoh () { return m_area_or_hole ; }
}
}
}
}
J. Dankert: C++-Tutorial
286
// Datei circle.cpp für Version "fmom2"
#include "stdafx.h"
#include "areas.h"
CCircle::CCircle (double d , double mpx , double mpy)
{
m_d = d ;
m_mp.set_x (mpx) ;
m_mp.set_y (mpy) ;
}
double CCircle::get_a ()
{
return (pi_4 * m_d * m_d * m_area_or_hole) ;
}
double CCircle::get_xc () { return (m_mp.get_x ()) ; }
double CCircle::get_yc () { return (m_mp.get_y ()) ; }
// Datei rectangl.cpp für Version "fmom2"
#include "stdafx.h"
#include "areas.h"
CRectangle::CRectangle (double x1 , double y1 , double x2 , double y2)
{
m_p1.set_x (x1) ;
m_p1.set_y (y1) ;
m_p2.set_x (x2) ;
m_p2.set_y (y2) ;
}
double CRectangle::get_a ()
{
return (fabs (m_p1.get_x () - m_p2.get_x ()) *
fabs (m_p1.get_y () - m_p2.get_y ()) * m_area_or_hole) ;
}
double CRectangle::get_xc ()
{
return ((m_p1.get_x () + m_p2.get_x ()) / 2) ;
}
double CRectangle::get_yc ()
{
return ((m_p1.get_y () + m_p2.get_y ()) / 2) ;
}
➪
Da diese Dateien im Projekt fmom noch nicht erfaßt sind, müssen sie hinzugefügt
werden: Nach dem Speichern der Dateien (z. B. durch Schließen der Fenster) wird im
Menü Project der "Visual Workbench" Edit gewählt. Es erscheint eine Dialog-Box,
in der links in einer Liste die Files des aktuellen Verzeichnisses zu sehen sind,
während am unteren Rand eine Liste alle zum Projekt gehörenden Files anzeigt.
Durch Auswählen eines Files in der Liste links und Klicken auf den Add-Button wird
ein File zum Projekt hinzugefügt. Dies wird für die Dateien point_xy.cpp, area.cpp,
circle.cpp und rectangl.cpp ausgeführt. Nun wird der Close-Button angeklickt, und
die Entwicklungsumgebung scannt alle zum Projekt gehörenden Dateien, um
Abhängigkeiten herauszufinden, so daß auch die Header-Datei areas.h erfaßt wird.
J. Dankert: C++-Tutorial
14.4.4
287
Einbinden der Datenstruktur in die Dokument-Klasse, die
Klasse CObList
In der einleitenden Diskussion zum Abschnitt 14.4.3 wurde bereits darauf hingewiesen, daß
für jede Teilfläche und jeden Ausschnitt eine Instanz der entsprechenden Klasse (CCircle
bzw. CRectangle) erzeugt wird und die Pointer auf die Instanzen in einer Liste von CAreaPointern verwaltet werden sollen (CArea ist die Basisklasse der Klassen CCircle und
CRectangle).
Zu den "Microsoft Foundation Classes" gehört die Klasse CObList, die alle Methoden zur
effektiven Verwaltung einer (doppelt verketteten) Liste zur Verfügung stellt. Die Listenelemente müssen allerdings Pointer auf Instanzen von CObject oder von CObject abgeleiteten Klassen sein. Da vorsorglich im File areas.h (voriger Abschnitt) die Klasse CArea aus
der Klasse CObject abgeleitet wurde, ist diese Bedingung für diese und alle aus ihr
abgeleiteten Klassen erfüllt.
Eine Liste wird erzeugt durch Definition einer Instanz der Klasse CObList. Dann stehen dem
Programmierer alle erforderlichen Methoden zur Listenverwaltung zur Verfügung, z. B.:
◆
AddHead und AddTail fügen ein Listenelement am Kopf bzw. am Ende an, in
beiden Fällen kann es auch das erste Element überhaupt sein.
◆
GetHead und GetTail liefern das erste bzw. letzte Listenelement, vorher muß mit
IsEmpty (eventuell auch mit GetCount, liefert die Anzahl der Listenelemente)
überprüft werden, ob überhaupt Listenelemente existieren.
◆
RemoveHead und RemoveTail entfernen das Listenelement am Kopf bzw. Ende,
auch hier ist eine Vorabprüfung mit IsEmpty erforderlich.
◆
Eine Besonderheit stellt das Arbeiten mit Variablen vom Typ POSITION dar (der
Typname ist etwas irreführend, es ist keine "Durchnumerierung", man sollte die
Vorstellung haben, es sei ein Pointer wie der "anchor" und der "next"-Pointer der im
Abschnitt 7.3 behandelten Listenverarbeitung): Mit GetHeadPosition und GetTailPosition können die entsprechenden POSITION-Werte abgefordert werden, um dann
z. B. mit GetNext vorwärts oder mit GetPrev rückwärts durch die Liste zu wandern.
Mit den POSITION-Werten kann man dann so nützliche Methoden wie InsertAfter,
InsertBefore oder RemoveAt benutzen (die Namen erläutern die Funktionalität der
Methoden wohl ausreichend).
Vorsicht, auch die Bezeichnungen GetNext und GetPrev sind leicht irreführend, was
am Beispiel von GetNext erläutert werden soll (alles gilt sinngemäß auch für
GetPrev):
Man fordert mit GetHeadPosition den POSITION-Wert des Kopfelements (liefert
NULL, wenn die Liste leer ist), gibt diesen als Argument an GetNext, und GetNext
liefert nun NICHT DAS NÄCHSTE LISTENELEMENT, sondern das Kopfelement
selbst, ändert jedoch das POSITION-Argument auf den POSITION-Wert des
nächsten Listenelements. Man muß es nur wissen, dann ist es bequem anzuwenden,
so z. B. wird eine Liste vorwärts "traversiert":
288
J. Dankert: C++-Tutorial
COblist
CArea
// ...
m_areaList ;
*pArea
;
POSITION pos = m_areaList.GetHeadPosition () ;
while (pos != NULL)
{
pArea = (CArea*) m_areaList.GetNext (pos) ;
// ... und pos hat sich geaendert, waehrend pArea das
//
Listenelement des alten pos-Wertes ist.
//
// ...
}
Man beachte in dem Beispiel auch das "Casten" des von GetNext abgelieferten
CObject-Pointers auf den Pointertyp des Objekts, der in der Liste abgelegt wurde (ist
erforderlich, weil der Return-Wert von GetNext vom Typ CObject* ist).
Mit einer solchen Liste wird nun die Datenstruktur in der Dokument-Klasse verankert. Der
"App Wizard" hat für diese Klasse zwei Dateien erzeugt, die Header-Datei fmomdoc.h mit
der Deklaration der Dokument-Klasse CFmomDoc und die Datei fmomdoc.cpp für den
Code der Methoden. Beide Dateien sind (ohne die Ergänzungen, die der Programmierer
hinzufügt) nicht sehr umfangreich und werden nachfolgend erweitert:
➪
In der Visual Workbench" wird nach Anklicken des Toolbar-Buttons, der das FileÖffnen symbolisiert (in der Button-Leiste ganz links), die "Dokument-Header-Datei"
fmomdoc.h geöffnet, es werden die nachfolgend fett gedruckten Zeilen ergänzt:
// fmomdoc.h : interface of the CFmomDoc class
//
/////////////////////////////////////////////////////////////////////////////
#include "areas.h"
//
//
//
... ist erforderlich, um die Deklarationen der
problembezogenen Datenstruktur
bekanntzumachen
class CFmomDoc : public CDocument
{
protected: // create from serialization only
CFmomDoc();
DECLARE_DYNCREATE(CFmomDoc)
// Attributes
protected:
CObList
m_areaList ;
//
//
Anker zur Datenstruktur der Applikation
(doppelt verkettete Liste von CArea-Pointern)
public:
// Operations
public:
// Zugriffsmethoden auf die doppelt verkettete Liste m_areaList:
void
NewArea
(CArea *pArea) ;
POSITION GetFirstAreaPos ()
;
CArea*
GetNextArea
(POSITION &pos) ;
// Ueberschreiben der aus CDocument ererbten Methode DeleteContents:
void DeleteContents () ;
// Implementation
289
J. Dankert: C++-Tutorial
public:
virtual
virtual
#ifdef _DEBUG
virtual
virtual
#endif
~CFmomDoc();
void Serialize(CArchive& ar);
// overridden for document i/o
void AssertValid() const;
void Dump(CDumpContext& dc) const;
protected:
virtual BOOL OnNewDocument();
// Generated message map functions
protected:
//{{AFX_MSG(CFmomDoc)
// NOTE - the ClassWizard will add and remove member functions here.
//
DO NOT EDIT what you see in these blocks of generated code !
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
/////////////////////////////////////////////////////////////////////////////
◆
Die Klasse CFmomDoc enthält nun mit m_areaList (den Anker für) eine doppelt
verkette Liste, die CArea-Pointer aufnehmen soll. Für drei CFmomDoc-Methoden
(NewArea, GetFirstAreaPos und GetNextArea), die diese Liste bearbeiten sollen,
wurden die Deklarationen bereits eingefügt (es werden noch wesentlich mehr werden).
◆
Die Methode DeleteContents, die von der Klasse CDocument ererbt wird, tut in
ihrer Originalversion gar nichts, wird aber immer vor der Zerstörung eines Dokuments
aufgerufen. Sie ist dafür vorgesehen, daß der Programmierer die von ihm erzeugten
Instanzen löschen kann. Sie wurde deshalb auch hier überschrieben und wird das
Freigeben der Liste m_areaList realisieren.
Der Code für die drei Methoden, die hinzugefügt werden sollen, und die Methode DeleteContents, die die ererbte Methode dieses Namens überschreibt, wird in fmomdoc.cpp am
Ende angefügt. Eigentlich sind es nur "Wrapper Functions", in die die CObList-Methoden
"eingepackt" sind.
➪
In der Visual Workbench" wird nach Anklicken des Toolbar-Buttons, der das FileÖffnen symbolisiert, die Datei fmomdoc.cpp geöffnet, es werden die nachfolgend fett
gedruckten Zeilen ergänzt:
// fmomdoc.cpp : implementation of the CFmomDoc class
//
//
... Code, der vom "App Wizard" erzeugt wurde ...
////////////////////////////////////////////////////////////////////////
// CFmomDoc commands
void CFmomDoc::NewArea (CArea *pArea)
{
m_areaList.AddTail (pArea) ;
}
POSITION CFmomDoc::GetFirstAreaPos ()
{
return m_areaList.GetHeadPosition () ;
}
290
J. Dankert: C++-Tutorial
CArea* CFmomDoc::GetNextArea (POSITION &pos)
{
return (CArea*) m_areaList.GetNext (pos) ;
}
void CFmomDoc::DeleteContents ()
{
while (!m_areaList.IsEmpty ())
delete (CArea*) m_areaList.RemoveHead () ;
}
◆
Während NewArea und GetFirstAreaPos nur "Verpackungen" für die CObListMethoden AddTail bzw. GetHeadPosition sind, übernimmt GetNextArea immerhin
das "Casten" des Return-Wertes von GetNext auf den passenden Typ.
◆
Die kleine Methode DeleteContents verdient allerdings eine etwas eingehendere
Betrachtung: Solange die Liste nicht leer ist (IsEmpty liefert einen BOOL-Wert ab),
wird RemoveHead aufgerufen. In der Programmzeile
delete (CArea*) m_areaList.RemoveHead ()
werden zwei Aufgaben erledigt:
Die Methode RemoveHead entfernt das erste Listenelement aus der Liste,
liefert es aber als Return-Wert noch einmal ab. Mit dem Entfernen des
Elements von der Liste ist natürlich die CArea-Instanz, auf die das Element
pointerte, noch existent. Deshalb wird ...
... der Return-Wert (CObList-Pointer) "gecastet" auf den Typ CArea-Pointer,
mit dem dann die Instanz der Klasse CArea mit delete zerstört wird.
Die "Aufräumarbeiten", die hier in DeleteContents ausgeführt werden, könnten natürlich
auch im Destruktor der Klasse CFmomDoc stehen. Allerdings sind sie in DeleteContents
natürlich recht gut angesiedelt, außerdem hat man dann gleich eine geeignete Funktion, um
später ein Kommando im Sinne von "Clear All" zu implementieren.
➪
Die geänderten Dateien sollten auf syntaktische Richtigkeit getestet werden, indem
das Projekt aktualisiert wird (Compilierung aller Dateien, die von Änderungen
betroffen sind). Dies erreicht man z. B. durch die Wahl von Build FMOM.EXE im
Menü Project.
Obwohl das ausführbare Programm, das mit den Erweiterungen aus den Abschnitten
14.4.3 und 14.4.4 erzeugt wurde, sich nicht anders als die Version "fmom1" zeigt
(die neue Datenstruktur kann vom Programm-Benutzer noch nicht erreicht werden),
wird der Zustand der Dateien am Ende dieses Abschnitts als Version "fmom2"
bezeichnet. Sie gehört unter diesem Namen zum Tutorial.
So wird auch demjenigen, der nicht alle Änderungen selbst nachvollzogen hat, die
Möglichkeit gegeben, die Erweiterungen des folgenden Abschnitts wieder
"eingenhändig" nachzuempfinden.
J. Dankert: C++-Tutorial
14.4.5
291
Menü mit "App Studio" bearbeiten
Zwei Schritte sind erforderlich, um die Eingabe von Daten in die im vorigen Abschnitt
definierte Datenstruktur zu erreichen. In diesem Abschnitt wird die Menüleiste so modifiziert,
daß der Programm-Benutzer seine Absicht, Daten zu erzeugen, äußern kann. Die Menüoption
wird später mit einer Aktions-Routine, mit der die Auswahl bearbeitet wird, verküpft. Im
nächsten Abschnitt wird eine Dialog-Box konstruiert, mit der die Beschreibung einer Fläche
tatsächlich eingegeben werden kann.
➪
Im Menü Tools der "Visual Workbench"
wird App Studio gewählt. Es erscheint
das aus dem Abschnitt 14.4.2 bekannte
Fenster. In der Liste (links) wird diesmal
Menu gewählt, im rechten Listen-Fenster
erscheinen zwei Identifikatoren, hier wird
IDR_FMOMTYPE gewählt (nebenstehende Abbildung, der andere Identifikator
bezieht sich auf das Menü des Hauptrahmenfensters).
➪
Nach Anklicken des Buttons Open (oder Doppelklick auf IDR_FMOMTYPE)
erscheint der Menü-Editor, der das von "App Wizard" vorbereitete Menü anzeigt und
mit einem punktierten Rechteck am rechten Rand des Menüs die Bereitschaft
signalisiert, dieses zu erweitern. Wenn man einen beliebigen Menüpunkt auswählt,
öffnet sich das Popup-Menü wie später im ausführbaren Programm: Klickt man z. B.
auf View, erscheinen die Optionen dieses Menü-Angebots (Abbildung), und auch hier
ist ein leeres Rechteck für die Erweiterung
vorgesehen. Es wäre nun durchaus die
passende Gelegenheit, aus dem vorbereiteten Menü die Punkte zu löschen, die nicht
benutzt werden sollen. Das kann jedoch
auch später geschehen (erst einmal abwarten, was als sinnvoll erkannt wird), zunächst wird alles akzeptiert, ein Punkt soll
ergänzt werden:
➪
Das punktierte Rechteck am rechten Rand des Menüs wird angeklickt und bei
gedrückter linker Maustaste nach links verschoben, bis die Markierung zwischen File
und Edit steht. Nach Loslassen der Maustaste sieht man das nunmehr verschobene
Rechteck. Nach Doppelklick auf das punktierte Rechteck öffnet sich die sogenannte
"Property Page" (nebenstehende Abbildung),
mit der das Aussehen
des neuen Menü-Punktes
festgelegt wird. In das
Edit-Feld mit der Bezeichnung Caption:
wird &Standardfläche
J. Dankert: C++-Tutorial
292
eingetragen (das Ampersand & bestimmt das zu unterstreichende Zeichen), nach
Drücken der Return-Taste verschwindet die "Property Page", das Menü ist um einen
Punkt erweitert.
➪
Nun wird durch Doppelklick auf das punktierte Rechteck unter dem neuen Menüpunkt
wieder die "Property Page" geöffnet, für Caption: wird &Kreis eingetragen. Für die
Verbindung dieses Menüpunkts mit der zugehörigen Funktion im Programm wird ein
Identifikator benötigt, für den man in das mit ID: beschriftete Edit-Feld einen Namen
eintragen kann. Allerdings erledigt dies das "App Studio" auch automatisch, davon
soll Gebrauch gemacht werden. In das mit Prompt: beschriftete Edit-Feld wird der
Text Definition einer
Kreisfläche oder eines
Kreisausschnitts eingetragen (Abbildung), der
im ausführbaren Programm in der Statuszeile
erscheint. Drücken der
Return-Taste schließt die
"Property Page".
➪
Eine entsprechende Aktion wird mit dem punktierten Rechteck unterhalb des neuen
Menüangebots Kreis ausgeführt. Für Caption: wird &Rechteck, als Prompt: wird
der Text Definition einer Rechteckfläche oder eines Rechteckausschnitts eingetragen. Damit ist das Menü komplett.
➪
Wenn man abschließend noch einmal durch Doppelklick auf Kreis die zugehörige
"Property Page" öffnet, sieht man, daß "App Studio" als Identifikator den sehr
sinnvollen Namen ID_STANDARDFLCHE_KREIS gewählt hat.
➪
Die geänderten Ressourcen werden abgespeichert z. B. durch Anklicken des ToolbarButtons mit dem Disketten-Symbol. Danach wäre es durchaus sinnvoll, sofort zum
"Class Wizard" zu wechseln. Hier soll aber eine Version von fmom beendet werden,
deshalb wird "App Studio" (z. B. über File und Exit) verlassen.
➪
In der "Visual Workbench" wird das Projekt aktualisiert (z. B. über Project und
Build FMOM.EXE), das ausführbare Programm wird gestartet (z. B. über Project
und Execute FMOM.EXE) und zeigt sich mit seinem geänderten Menü. Nach
Anklicken von Standardfläche
(oder mit Alt-S) zeigt sich das
gerade erzeugte Popup-Menü
(Abbildung). Weil aber noch
keine Funktionen hinter den
Menü-Angeboten stehen, sind
diese grau geschrieben und
können nicht angewählt werden.
Der nun erreichte Zustand des
Projekts gehört zum Tutorial
als Version "fmom3".
J. Dankert: C++-Tutorial
14.4.6
293
Dialog-Box mit "App Studio" erzeugen
Zunächst soll eine Dialog-Box für die Eingabe einer Kreisfläche erzeugt werden. Die
Kreisfläche wird beschrieben durch die Koordinaten des Mittelpunktes und den Durchmesser.
Außerdem muß die Information "Teilfläche oder Ausschnitt?" eingegeben werden.
➪
Im Menü der "Visual Workbench" wird unter Tools die
Option App Studio gewählt. Es erscheint der bereits
bekannte Start-Bildschirm von "App Studio". Nach
Anklicken des New-Buttons öffnet sich die "New Resource"-Dialog-Box (nebenstehende Abbildung), in der Dialog
gewählt und mit OK bestätigt wird. Es erscheint der
nachfolgend zu sehende Dialog-Editor.
Der Dialog-Editor (erste Bekannschaft wurde bereits im Abschnitt 10.3.3 gemacht) ist ein
besonders starkes Werkzeug des "App Studio", mit dem man konsequent nach dem "WYSIWYG"-Prinzip arbeiten kann ("What you see is what you get"). Mit der "Drag and Drop"Technik (Elemente mit der Maus bei gedrückter linker Maustaste verschieben bzw. kopieren)
können die Elemente aus einer Palette ausgewählt und in die zu erzeugende Dialog-Box
transportiert bzw. innerhalb der Dialog-Box
verschoben werden.
Die nebenstehende Abbildung zeigt den
Start-Bildschirm des Dialog-Editors, in dem
bereits eine "Minimal-Dialog-Box" angelegt
ist, weil angenommen werden darf, daß mit
sehr großer Wahrscheinlichkeit die zu erzeugende Dialog-Box sowohl mit einem OKButton als auch mit einem Cancel-Button
ausgestattet sein soll. Sollte das wider Erwarten nicht der Fall sein, wird das unerwünschte Element angeklickt (ein farbiger Rahmen
weist es als "ausgewählt" aus), und nach
dem Drücken der Del(Entf)-Taste ist es
Start-Bildschirm des Dialog-Editors
verschwunden. Soll der Button mit Abbrechen an Stelle von Cancel beschriftet sein,
kann mit Doppelklick auf den Button die "Properties-Box" geöffnet werden, und die
Beschriftung wird durch Änderung des Eintrages im Feld Caption: geändert.
Vieles im Dialog-Editor ist selbsterklärend oder kann ausprobiert werden. Auf höchst
angenehme Art funktioniert die "Drag and Drop"-Technik genau so, wie es der WindowsBenutzer z. B. aus dem Datei-Manager kennt, insbesondere gilt:
◆
Bei gedrückter Ctrl(Strg)-Taste können durch Anklicken mehrere Elemente "eingesammelt" werden (alle werden durch farbige Rahmen gekennzeichnet), um diese dann
gemeinsam zu löschen, zu verschieben oder zu kopieren.
◆
"Drag and Drop" nur mit der Maus verschiebt die Elemente innerhalb der Dialog-Box,
bei gleichzeitig gedrückter Ctrl(Strg)-Taste werden sie kopiert.
J. Dankert: C++-Tutorial
294
Es ist durchaus angebracht, sich die Benutzer-Schnittstelle des Dialog-Editors einmal
anzusehen aus der Sicht des Programmierers, der selbst eine komfortable Kommunikation
des Benutzers mit dem Programm realisieren möchte. Neben der "WYSIWYG"Eigenschaft finden sich auch noch andere beachtenswerte Besonderheiten, zum Beispiel
ein mehrstufiges "Undo" und das zugehörige "Redo". Zu erreichen sind diese beiden
Funktionen
◆
durch Anklicken der Toolbar-Buttons (die gekrümmten Pfeile), die nur dann
schwarz (ansonsten grau) gezeichnet sind, wenn eine entsprechende Aktion
möglich ist,
◆
über das Menü Edit, in dem die möglichen Aktionen sogar "intelligent" angeboten
werden, also z. B. Undo Drag and Drop oder Redo Property Edit,
◆
über die "Short-Cuts" Ctrl(Strg)-z ("Undo") bzw. Ctrl(Strg)-a ("Redo").
Es gibt noch eine Reihe anderer recht komfortabler Bedienungs-Funktionen und einige
"intelligente" Reaktionen des Dialog-Editors, gelegentlich wird nachfolgend darauf
aufmerksam gemacht.
Nach dem Start des Dialog-Editors sollte auch die Palette der Dialog-Box-Elemente zu sehen
sein (wenn nicht, wird sie mit Show Control Palette im Menü Window oder mit der F2Taste hervorgeholt). In der
nebenstehenden Abbildung
wurden bewußt die englischen
Bezeichnungen für die DialogPointer ----- Static Graphic
Box-Elemente verwendet, weil
Static Text ----- Edit Box
der Dialog-Editor mit dem
Group Box ----- Pushbutton
Programmierer auch "englisch
Check Box ----- Radio Button
spricht" und diese Bezeichnungen verwendet.
Combo Box ----- List Box
Die für die nachfolgend zu
Hor. Scroll Bar ----- Vert. Scroll Bar
erzeugende Dialog-Box erUser-def. Control ----- VBX Control
forderlichen Elemente werden
bei Bedarf genauer erläutert
(ansonsten: Help anklicken
Palette der Dialog-Box-Elemente
oder das Manual "App Studio
User's Guide" konsultieren).
Nach dem Start des Dialog-Editors ist die gesamte Dialog-Box durch ein farbiges Rechteck
als "ausgewähltes Objekt" gekennzeichnet. Durch Anklicken eines Elements (z. B. des OKButtons) wird dieses als "ausgewählt" gekennzeichnet und kann bearbeitet werden.
➪
In der Statuszeile sind unten rechts die aktuellen Abmessungen der Dialog-Box zu
sehen (Breite * Höhe), angegeben in "Dialog-Box-Einheiten" (ein Viertel der
Zeichenbreite bzw. ein Achtel der Zeichenhöhe des System-Fonts, vgl. Abschnitt
10.2.2). Durch "Ziehen mit der Maus bei gedrückter linker Maustaste" an der rechten
unteren Ecke der Dialog-Box (die gesamte Dialog-Box muß dabei als "ausgewählt"
J. Dankert: C++-Tutorial
295
gekennzeichnet sein) wird deren Größe auf etwa 150 * 140 Einheiten verändert.
Sollten Sie dabei mit dem rechten Rand an den Buttons anstoßen, müssen Sie diese
vorab etwas nach links verschieben ("Drag and Drop"). Öffnen Sie danach einmal das
Edit-Menü, dort wird sofort Undo Resize angeboten.
➪
Die Überschrift der Dialog-Box wird geändert, indem durch einen Doppelklick
irgendwo in die Box (nicht allerdings auf einen der bereits vorhandenen Buttons) die
"Property Page" angefordert wird. Dort wird
in dem mit Caption:
bezeichneten Feld die
Überschrift Dialog durch
Kreis ersetzt, und in
dem ID-Feld wird
IDD_DIALOG1 mit
IDD_DIALOG_KREIS
überschrieben (Abbildung). Übrigens: Wenn man bei einer als "ausgewählt" gekennzeichneten Dialog-Box einfach zu schreiben anfängt, vermutet das kluge Programm,
daß man die Überschrift ändern will, und öffnet automatisch die "Property Page".
➪
Vor der weiteren Bearbeitung der Dialog-Box
ist es empfehlenswert, im Menü Layout die
Option Grid Settings... zu wählen. In der sich
öffenden Box kann das Spacing akzeptiert
werden, die mit Snap to Grid beschriftete
"Check Box" ist als ausgewählt zu kennzeichnen (nebenstehende Abbildung). Mit OK wird
die "Grid Settings"-Box geschlossen.
➪
Das Ziel der folgenden Aktionen ist es, der Dialog-Box etwa das Aussehen der
Abbildung unten rechts zu geben. Zunächst werden die beiden Buttons verschoben:
Anklicken des OK-Buttons, danach
wird bei gedrückter Ctrl(Strg)-Taste
der Cancel-Button angeklickt, so
daß beide gleichzeitig "ausgewählt"
sind. Die Buttons (es sind übrigens
in der Bezeichnungsweise des
Dialog-Editors "Pushbuttons")
werden durch "Drag and Drop"
verschoben, dabei wird auf das
(durch Punkte angedeutete) "Grid"
gefangen.
Die Strings Mittelpunkt: und x =
sind "Static Text", das Rechteck, in
das später der Wert eingegeben
wird, ist eine "Edit Box". Diese
werden folgendermaßen erzeugt:
J. Dankert: C++-Tutorial
296
Mittels "Drag and Drop" wird aus der Palette der Dialog-Box-Elemente ein "Static
Text"-Element (das "große A") in der Dialog-Box plaziert. Nach Doppelklick auf das
mit dem Text Static versehene Element öffnet die "Property Page". Nach Anklicken
des Feldes, das mit Caption: beschriftet ist, wird dort der neue Text Mittelpunkt:
eingetragen. Nach Drücken der Return-Taste stellt man fest, daß der Text für das Feld
zu lang ist. Man kann es durch "Ziehen an den Rändern" vergrößern, einfacher ist es,
im Menü Layout die Option Size to Content zu wählen (noch schneller: Drücken der
Funktionstaste F7), wodurch sich die Größe des "Static Text"-Feldes automatisch dem
eingegebenen Text anpaßt.
Auf entsprechende Weise wird der Text x = erzeugt.
Auch die "Edit Box" wird mit "Drag and Drop" aus der Palette erzeugt. Nach
Doppelklick zeigt die "Property Page", daß als ID: (wird später im Programm als
Bezug zu dem in dieses Feld einzugebenden Wert benötigt) IDC_EDIT1 eingestellt
ist. Es ist empfehlenswert,
diese Bezeichnung auf eine
"zum Inhalt passende" zu
ändern, z. B. hier auf
IDC_EDIT_KREIS_MPX,
wie es die nebenstehende
Abbildung zeigt.
Die Identifikatoren für die Dialog-Box-Elemente sind ganzzahlige Werte. Daß eindeutige
Namen verwendet und eindeutige Werte zugewiesen werden, liegt in der Verantwortung
des "App Studio" (auch beim Kopieren von Elementen wird "aufgepaßt", wie man bei der
nachfolgenden Aktion sehen kann).
Der Programmierer darf die Namen nach seinen Wünschen ändern, um die WertZuweisung sollte er sich nicht kümmern, obwohl er auch darauf Einfluß nehmen könnte.
➪
Weiter geht es am schnellsten mit
einer Kopieraktion, die das nebenstehende Aussehen der Dialog-Box
zum Ziel hat: Die beiden "Static
Text"-Elemente und die "Edit Box"
werden bei gedrückter Ctrl(Strg)Taste nacheinander angeklickt, so
daß alle drei als "ausgewählt" gekennzeichnet sind. Danach werden
sie (der "Kreuz-Cursor" muß dabei
zu sehen sein) mit "Drag and
Drop" bei gedrückter Ctrl(Strg)Taste kopiert. Da die Auswahl auf
die neuen Elemente übergeht, kann
die Kopieraktion sofort ein zweites
Mal ausgeführt werden.
J. Dankert: C++-Tutorial
➪
297
Das eigentliche Ziel dieser Aktion
ist aber die nebenstehend zu sehende Dialog-Box. Einmal Mittelpunkt: ist also zuviel: Anklicken,
Del(Entf)-Taste drücken, schon ist
der Text verschwunden. Die anderen
neuen Texte müssen geändert
werden: Doppelklick, "Property
Page" öffnet, Caption: ändern, und
auch dies ist erledigt.
Man sollte auch für die beiden
neuen "Edit Box"-Elemente durch
Doppelklick die "Property Page"
öffnen. Man erkennt, daß "App
Studio" den Identifikatoren die
Namen IDC_EDIT_KREIS_MPX2
bzw. IDC_EDIT_KREIS_MPX3 gegeben hat (abgeleitet aus dem Namen des
Originals der Kopieraktion). Diese Namen werden auf IDC_EDIT_KREIS_MPY
bzw. IDC_EDIT_KREIS_D geändert.
Es fehlt noch die Eingabemöglichkeit der Information "Teilfläche oder Ausschnitt?". Dafür
werden "Radio Buttons" vorgesehen, die immer dann (im Unterschied zu "Check Box"Elementen) verwendet werden sollten, wenn nur eine von mehreren Alternativen erlaubt ist.
Die beiden "Radio Buttons" werden in einer "Group Box" zusammengefaßt, die im
wesentlichen als Rahmen um andere Dialog-Box-Elemente benutzt wird.
➪
Zunächst wird ("Drag and Drop")
eine "Group Box" aus der Palette in
der Dialog-Box etwa so plaziert, wie
es die Abbildung zeigt. Sie muß
durch "Ziehen an den Ecken bzw.
Seiten" etwas vergrößert werden,
nach Doppelklick wird in der "Property Page" als Caption: der Text
Kreis ist ... eingetragen.
Zwei "Radio Buttons" werden (ebenfalls mittels "Drag and Drop") aus
der Palette geholt und etwa so
plaziert, wie es die Abbildung zeigt.
Durch Doppelklick wird jeweils die
"Property Page" geöffnet, als Caption: werden Teilfläche bzw.
Ausschnitt eingegeben (mit F7 wird die Größe angepaßt). Nur für den oberen
"Radio Button" werden in der "Property Page" zusätzlich (zu den voreingestellten
Eigenschaften "Visible" und "Auto") auch die "Check Box"-Elemente "Group" und
"Tabstop" "angekreuzt" (Klicken in die Kästchen). Auswirkungen und noch erforderliche Ergänzungen dieser Aktion werden nach dem Test der Dialog-Box erklärt.
J. Dankert: C++-Tutorial
298
Das spätere Aussehen (im Programm) und die Funktionalität der erzeugten Dialog-Box
können jederzeit getestet werden:
➪
Im Resource-Menü findet man das Angebot
Test. In die erscheinende Dialog-Box (nebenstehende Abbildung) kann man Werte eingeben,
man kann die "Radio Buttons" schalten und mit
Mausklick oder der Tab-Taste von einem Feld
zum anderen wechseln. Klicken auf OK oder
Cancel oder Drücken der Return-Taste lassen
die Dialog-Box wieder verschwinden.
Das Wechseln der aktiven Elemente mit der
Tab-Taste offenbart noch einen Mangel, der
nachfolgend beseitigt wird.
➪
Im Menü Layout wird die Option
Set Tab Order gewählt. Es erscheint das nebenstehende Bild: Die
intern gespeicherte Reihenfolge der
Elemente hängt weitgehend von der
Reihenfolge ihres Erzeugens ab.
Dies kann auf sehr einfache Weise
korrigiert werden: Mit der Maus
werden nacheinander alle DialogBox-Elemente in der gewünschten
Reihenfolge angeklickt. Dabei
ändern sich die Zahlen sofort. Sie
sollten die in der Abbildung unten
rechts zu sehende Reihenfolge
erzeugen. Mit Return wird die
Aktion beendet. Bei einem neuen
Test der Dialog-Box kann man sich
davon überzeugen, daß die "Tab
Order" tatsächlich geändert wurde.
➪
Nun wird noch die Gruppierung der
"Radio Buttons" komplettiert. Eine
Gruppe beginnt mit dem ersten
"angekreuzten" "Button" (dies ist
der "Teilfläche"-Button) und endet
vor dem nächsten "angekreuzten"
Button. Es muß also noch einmal
die "Property Page" des OK-Buttons
geöffnet werden (in der "Tab-Order"
der erste nach den "Radio Buttons"), in der dann das "Group"Kästchen "anzukreuzen" ist.
J. Dankert: C++-Tutorial
➪
299
Die Dialog-Box ist komplett. Durch Anklicken des Toolbar-Buttons mit dem
Disketten-Symbol wird sie gespeichert und muß nun in das Programm eingebunden
werden. Dies erfolgt in zwei Schritten.
Einbinden einer Dialog-Box in das Programm:
◆
Für jede Dialog-Box wird eine eigene Dialog-Klasse konstruiert, die für jeden
Eingabewert, der über die Dialog-Box abgefragt werden soll, eine Variable enthält
(dabei ist der "Class Wizard" hilfreich, der auch die Methoden für den Austausch
der Werte zwischen Dialog-Box und Klassen-Variablen einrichtet). Diese Aktion
wird zweckmäßig gleich über das "App Studio" eingeleitet (wird nachfolgend
demonstriert).
◆
An einer geeigneten Stelle im Programm muß das Erscheinen der Dialog-Box
ausgelöst werden. Im behandelten Beispiel soll die Auswahl des bereits
eingerichteten Menüpunktes Kreis im Menü Standardfläche zum Erscheinen der
Dialog-Box führen. Das Verbinden der Menü-Auswahl mit der Dialog-Box wird
im Abschnitt 14.4.7 behandelt.
➪
Aus dem Dialog-Editor des "App Studio" wird im Menü Resource die Option
ClassWizard... gewählt. Es öffnet sich eine "Add Class"-Box mit dem Vorschlag, zur
gerade erzeugten Dialog-Box mit dem Identifikator IDD_DIALOG_KREIS die
zugehörige DialogKlasse zu erzeugen
(nebenstehende Abbildung). Im Feld Class
Name wird der gewünschte Name für die
Klasse eingetragen,
z. B.: CCircleDlg, und
der "Class Wizard" leitet
daraus sofort Namensvorschläge für die
beiden Dateien ab, die er für die Klasse generieren wird. Diese Namen (im behandelten Beispiel circledl.h für die Header-Datei und circledl.cpp für die Implementierung
der Methoden) können akzeptiert oder geändert werden. Im Feld, das mit Class Type
beschriftet ist, steht die Basisklasse, aus der die neue Klasse abgeleitet wird (in
diesem Fall CDialog), diese sollte natürlich nicht geändert werden. Nach Anklicken
des Buttons Create Class werden die Dateien automatisch erzeugt, und das StandardFenster des "Class Wizard" erscheint.
➪
Der "Class Wizard" bietet an, die garade von ihm erzeugten Dateien für die Klasse
CCircleDlg zu modifizieren (der Name der Klasse steht im Feld Class Name, dort
kann man auch die anderen bereits erzeugten und vom "Class Wizard" verwalteten
Klassen einstellen). Im Listenfeld mit der Überschrift Object IDs: sind alle Identifikatoren zu sehen, die der "Class Wizard" mit dieser Klasse verbindet. Nach Anklicken
J. Dankert: C++-Tutorial
300
einer Zeile in diesem Feld werden in dem mit Messages: überschriebenen Feld alle
Windows-Botschaften gelistet, auf die im Zusammenhang mit dem entsprechenden
Objekt sinnvollerweise reagiert werden könnte (probieren Sie es aus!). Das alles ist
jedoch nicht unbedingt erforderlich, weil der "Class Wizard" die wichtigste Aufgabe
der Dialog-Box, den Datenaustausch mit der Dialog-Klasse, weitgehend selbständig
organisiert.
➪
Aus dem Angebot am
oberen Rand des "Class
Wizard"-Fensters (den
"Karteikarten-Reitern")
wird Member Variables
gewählt, es erscheint das
nebenstehende Fenster
mit einer Liste aller
Identifikatoren der
Dialog-Box-Elemente,
über die Informationen
vom Benutzer entgegengenommen werden. Man
registriert, daß dies die
beim Erzeugen der
Dialog-Box definierten
Namen sind und daß für
die "Radio Buttons" nur
ein Identifikator aufgeführt ist (alle "Radio Buttons" einer Gruppe liefern nur die eine
Information ab, welcher von ihnen angeschaltet ist). Die Identifikatoren IDOK und
IDCANCEL gehören zu den vom "App Studio" selbständig erzeugten Buttons OK
bzw. CANCEL.
Für die Informationen, die das Programm aus der Dialog-Box erhalten soll (MittelpunktKoordinaten und Durchmesser des Kreises, "Teilfläche oder Ausschnitt?") müssen nun
Klassen-Variablen der Klasse CCircleDlg angelegt werden, auf denen die Information
abgelegt werden kann. Auch hierfür gibt es nachhaltige Unterstützung vom "Class Wizard":
➪
Nach Auswahl einer Control ID, z. B. IDC_EDIT_KREIS_D, wird auf den Button
Add Variable... geklickt. Es öffnet sich das "Add Member Variable"-Fenster. In das
Feld Member Variable Name: muß man
den Namen für die Variable eingeben (es ist
m_ als Anfang voreingestellt, weil es üblich
ist, "member variables" so kenntlich zu
machen). Im Feld Variable Type: muß man
den gewünschten Typ für die Variable einstellen. Für den Kreis-Durchmesser, zu dem
der Identifikator IDC_EDIT_KREIS_D
gehört, werden m_d als Name und der Typ
double gewählt (nebenstehende Abbildung).
Nach Anklicken des OK-Buttons zeigt der
J. Dankert: C++-Tutorial
301
"Class Wizard" die entsprechend erweiterte Liste. Der Vorgang wird wiederholt für
die Identifikatoren IDC_EDIT_KREIS_MPX (Variable m_mpx vom Typ double)
und IDC_EDIT_KREIS_MPY (Variable m_mpy vom Typ double). Für den
Identifikator IDC_RADIO1 wird im "Add Member Variable"-Fenster vom "Class
Wizard" als Typ sinnvollerweise gleich int
angeboten, weil die ("0basierte") Nummer des
eingestellten Buttons
abgeliefert wird. Dies
wird natürlich akzeptiert,
als Name wird m_radio
gewählt.
Wenn die "Karteikarte"
mit der Überschrift
Member Variables das
nebenstehende Aussehen
zeigt, kann man über die
"Karteikarte" Message
Maps nach Anklicken
des Buttons Edit Code
direkt in der "Visual
Workbench" die gerade
vom "Class Wizard" erzeugte und modifizierte Datei circledl.cpp erreichen, und es
lohnt sich durchaus, den vom "App Wizard" generierten Code zu inspizieren.
Nachfolgend ist zunächst die Header-Datei circledl.h der Dialog-Klasse CCircleDlg
aufgelistet. Sie enthält die Deklaration der Klasse:
// circledl.h : header file
//
/////////////////////////////////////////////////////////////////////////////
// CCircleDlg dialog
class CCircleDlg : public CDialog
{
// Construction
public:
CCircleDlg(CWnd* pParent = NULL);
// standard constructor
// Dialog Data
//{{AFX_DATA(CCircleDlg)
enum { IDD = IDD_DIALOG_KREIS };
double
m_d;
double
m_mpx;
double
m_mpy;
int
m_radio;
//}}AFX_DATA
// Implementation
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
// Generated message map functions
//{{AFX_MSG(CCircleDlg)
// NOTE: the ClassWizard will add member functions here
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
J. Dankert: C++-Tutorial
302
In der Klassen-Deklaration erkennt man die gerade mit dem "Class Wizard" erzeugten
Klassen-Variablen und die Deklaration der sehr wichtigen Methode DoDataExchange, die
den Transfer der Werte der Klassen-Variablen zu den Elementen der Dialog-Box und auch
in die entgegengesetzte Richtung erledigt (von dieser mühsamen Angelegenheit ist also der
Programmierer befreit). In der Datei circledl.cpp findet man den Code dieser Methode und
des Konstruktors der Klasse:
// circledl.cpp : implementation file
//
#include "stdafx.h"
#include "fmom.h"
#include "circledl.h"
#ifdef _DEBUG
#undef THIS_FILE
static char BASED_CODE THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CCircleDlg dialog
CCircleDlg::CCircleDlg(CWnd* pParent /*=NULL*/)
: CDialog(CCircleDlg::IDD, pParent)
{
//{{AFX_DATA_INIT(CCircleDlg)
m_d = 0;
m_mpx = 0;
m_mpy = 0;
m_radio = -1;
//}}AFX_DATA_INIT
}
void CCircleDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CCircleDlg)
DDX_Text(pDX, IDC_EDIT_KREIS_D, m_d);
DDX_Text(pDX, IDC_EDIT_KREIS_MPX, m_mpx);
DDX_Text(pDX, IDC_EDIT_KREIS_MPY, m_mpy);
DDX_Radio(pDX, IDC_RADIO1, m_radio);
//}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CCircleDlg, CDialog)
//{{AFX_MSG_MAP(CCircleDlg)
// NOTE: the ClassWizard will add message map macros here
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CCircleDlg message handlers
◆
Im Konstruktor hat "App Wizard" für jede Klassen-Variable die Initialisierung
vorgesehen. Damit sind in jedem Fall Werte vorhanden, wenn die Dialog-Box
erscheint.
◆
Vor Erscheinen der Dialog-Box (welche Funktion das Erscheinen veranlaßt, wird auch
noch in diesem Abschnitt behandelt) wird aus der vom Programm-Gerüst aufgerufenen Methode OnInitDialog die CWnd-Methode UpdateData (mit einem Argument-
303
J. Dankert: C++-Tutorial
wert FALSE) gerufen, nach dem Drücken des OK-Buttons der Dialog-Box wird
UpdateData mit einem Argumentwert TRUE aufgerufen. Der Argumentwert
bestimmt die Richtung des Datentransfers (TRUE bedeutet Dialog-Box ---> Variablen
der Dialog-Klasse).
UpdateData ruft DoDataExchange mit einem Argument vom Typ CDataExchangePointer auf, die Variablen dieser Klasse enthalten alle für den Datentransfer erforderlichen Informationen, u. a. die "Richtung des Datentransfers". Für jede zu
übertragene Variable ist in DoDataExchange ein Aufruf der DDX-Funktion zu
codieren. Diese erhält neben dem CDataExchange-Pointer den Identifikator des
Dialog-Box-Elements und die zugehörige Klassen-Variable der Dialog-Klasse, mit
denen sie den Datenaustausch in der vorgegbenen Richtung realisiert. Sie ist mehrfach
überladen und kann so die verschiedenen Typen von Klassen-Variablen verarbeiten.
◆
Dieser gesamte Prozeß wurde vom "Class Wizard" bereits eingerichtet, der Programmierer braucht sich darum nicht zu kümmern. Wenn es einen Grund gibt, den
Datenaustausch zwischen der Dialog-Klasse und der Dialog-Box in einer bestimmten
Situation zu erzwingen, sollte der Programmierer einfach UpdateData (mit dem
Argument TRUE oder FALSE, abhängig von der gewünschten Richtung des
Datentransfers) aufrufen.
Wer die Schritte in diesem Abschnitt nachvollzogen hat, ahnt, daß es solche Gründe
sicher geben kann, wenn ihm aufgefallen ist, daß bei jedem einzelnen eingegebenen
Zeichen für die Festlegung des Namens der Dialog-Klasse sich auch die Namen für
die Files geändert haben. Es muß also ein ständiger Datenaustausch mit der "Edit
Box", in die gerade eingegeben wird, stattfinden (und nicht erst nach dem Schließen
des Dialogs). Dies wird mit dem oben beschriebenen Mechanismus realisiert.
Es bleibt noch die Frage, welche Funktion den gesamten Vorgang auslöst. Für das Starten
eines modalen Dialoges (der Unterschied zwischen modalen und nicht-modalen Dialogen
wurde im Abschnitt 10.2.1 behandelt) ist die Funktion DoModal zuständig:
Die Dialog-Klassen des Programms werden von der Basisklasse CDialog abgeleitet und
erben von dieser die Methode CDialog::DoModal, deren Deklaration
virtual int DoModal () ;
zeigt, daß sie ohne Argumente aufgerufen wird.
DoModal startet die Initialisierung der Dialog-Box, bringt sie auf den Bildschirm, die
gesamte Interaktion mit dem Benutzer wird von dieser Methode abgewickelt, und der
bereits beschriebene Datenaustausch zwischen Dialog-Box und Dialog-Klasse (in beiden
Richtungen) wird organisert. Der Return-Wert von DoModal ist -1, wenn die Dialog-Box
nicht geöffnet werden konnte, ansonsten ist es der von CDialog::EndDialog
zurückgegebene Resultatwert, der (wenn der Programmierer keine der Standardfunktionen
zum Beenden des Dialogs überschrieben hat) beim Beenden über den OK-Button den
Wert IDOK und beim Beenden über den Cancel-Button den Wert IDCANCEL hat.
Der Aufruf von DoModal im Programm fmom wird im folgenden Abschnitt demonstriert.
J. Dankert: C++-Tutorial
14.4.7
304
Einbinden des Dialogs in das Programm
Die Dialog-Box, die im Abschnitt 14.4.6 konstruiert wurde (sie existert in der RessourcenDatei und als Dialog-Klasse), soll dann erscheinen, wenn der Programm-Benutzer im Menü
Standardfläche die Option Kreis wählt. Dieser Zusammenhang wird in diesem Abschnitt
hergestellt. Dazu ist es sinnvoll, wenigstens etwas über die Behandlung der WindowsMessage WM_COMMAND (das "Command Routing") zu wissen, denn das Anwählen einer
Menü-Option löst diese Message aus.
Es gibt verschiedene Ziele ("Command Targets"), die Kommandos empfangen und verarbeiten können. Das "Routing" (in welcher Reihenfolge werden den potentiellen "Kommando-Empfängern" die Kommandos zur Bearbeitung angeboten) ist kompliziert genug, um
darüber einige Seiten zu füllen, glücklicherweise aber so sinnvoll, daß der Programmierer
selbst ohne fundiertes Wissen darüber darauf vertrauen darf, daß ein Kommando dort
"ankommt", wo er es sinnvollerweise bearbeiten möchte.
Die Klassen-Bibliothek identifiziert ein Kommando mit dem Identifikator, "interessiert
sich nicht" für den Auslöser des Kommandos. Andererseits ist der Identifikator das
Bindeglied zum Kommando-Auslöser, dies kann das Menü sein, mit dem gleichen
Identifikator kann jedoch z. B. auch ein Kommando beim Anklicken eines ToolbarButtons ausgelöst werden.
Für den Programmierer, der sich vom "App Wizard" ein Programmgerüst in der "DocumentView"-Architektur erzeugen ließ, bieten sich die Dokument-Klasse und die Ansichtsklasse an,
um die Methode zur Behandlung eines Kommandos ("Message Handler Routine") anzusiedeln. Er sollte seine Entscheidung davon abhängig machen, ob das Kommando eher das
Dokument beeinflußt (z. B. durch Änderung der Daten) oder nur die Sicht auf die Daten
ändert ("Zoom", "Highlighting", ...). In jedem Fall sollte er sich bei der Implementierung vom
"Class Wizard" unterstützen lassen.
➪
Man erreicht den "Class Wizard" von der "Visual Workbench" über das Menü
Browse, in dem die Option ClassWizard... zu finden ist. In der "Karteikarte"
Message Maps ist vermutlich noch im Feld Class Name die zuletzt behandelte
Klasse CCircleDlg eingestellt. Wenn man auf den Pfeil neben dem Feld klickt,
werden die anderen vom "Class Wizard" verwalteten Klassen sichtbar (dies ist
übrigens eine "Combo Box", vgl. die "Palette der Dialog-Box-Elemente" im Abschnitt
14.4.6).
Wählen Sie z. B. die Ansichts-Klasse CFmomView. Im Fenster der Object IDs
finden Sie dann durch "Scrollen" auch die Eintragung ID_STANDARDFLCHE_KREIS, dieser Identifikator wurde an die Menü-Option Kreis im Menü
Standardfläche vergeben (Abschnitt 14.4.5). Weil mit der Eingabe der Daten einer
Kreisfläche aber die Dokument-Daten verändert werden, soll die "Message Handler
Routine" für die Bearbeitung des Kommandos Kreis in der Dokument-Klasse
angesiedelt werden.
J. Dankert: C++-Tutorial
305
➪
In der mit Class Name gekennzeichneten "Combo Box" wird CFmomDoc gewählt.
Auch unter den Object IDs dieser Klasse findet man ID_STANDARDFLCHE_KREIS (die Object IDs stehen in einer "List Box", vgl. Abschnitt 14.4.6).
Wenn ID_STANDARDFLCHE_KREIS angeklickt wird, erscheinen in der benachbarten "List Box" die Messages, die für das ausgewählte Objekt relevant sind (das
sind bei anderen Objekten vielfach wesentlich mehr als die beiden, die in diesem Fall
erscheinen). Wenn man
nun in der Messages"List Box" COMMAND
anklickt, wechselt die
Beschriftung des Buttons
Add Funktion... ihr
Aussehen (von grau zu
schwarz). Damit signalisiert der "App Wizard",
daß er bereit ist, das
Gerüst einer "Message
Handler Function" für
die Bearbeitung dieses
Kommandos in der
ausgewählten Klasse
CFmomDoc zu erzeugen (nebenstehende
Abbildung).
➪
Nach Anklicken des Buttons Add Function... erscheint die "Add Member Function"Dialog-Box mit einem Namensvorschlag für die zu erzeugende Funktion (der Name
sollte mit On beginnen). Es gibt keinen Grund,
den zum Identifikator passenden Namen (nebenstehende Abbildung) zu ändern, deshalb wird
OK angeklickt. In der "List Box" mit der
Überschrift Member Functions: erscheinen der
Funktionsname und der Identifikator des Kommandos.
Der "App Wizard" erzeugt automatisch die Deklaration für die Methode OnStandardflcheKreis in der CFomDoc-Klassen-Deklaration (in der Datei fmomdoc.h) und das Gerüst der
Methode CFmomDoc::OnStandardflcheKreis in der Datei fmomdoc.cpp.
➪
Durch Anklicken des Buttons
Edit Code im "Class Wizard"
landet man im Editor der
"Visual Workbench" in der
Datei fmomdoc.cpp. Der
Cursor steht dort, wo der
Programmierer der Methode
OnStandardflcheKreis Leben
einhauchen muß (Abbildung).
306
J. Dankert: C++-Tutorial
Die Aufgabe der Methode OnStandardflcheKreis besteht im wesentlichen aus dem Aufruf
der Methode DoModal der Dialog-Klasse, um die Dialog-Box erscheinen zu lassen, und in
der Übertragung der eingegebenen Daten aus der Dialog-Klasse in die Dokument-Datenstruktur (eine Instanz der Dialog-Klasse wird deshalb nur für das "kurze Leben in OnStandardflcheKreis" erzeugt).
➪
Die Methode FmomDoc::OnStandardflcheKreis in der Datei fmomdoc.cpp wird um
die fett gedruckten Zeilen ergänzt:
void CFmomDoc::OnStandardflcheKreis()
{
CCircleDlg dlg ;
dlg.m_radio = 0 ;
if (dlg.DoModal () == IDOK)
{
CArea* pArea = new CCircle (dlg.m_d , dlg.m_mpx , dlg.m_mpy) ;
pArea->set_aoh ((dlg.m_radio == 0) ? 1 : -1) ;
NewArea
(pArea) ;
UpdateAllViews (NULL) ;
}
}
Da eine Instanz der Dialog-Klasse CCircleDlg erzeugt wird, muß die Header-Datei der
Dialog-Klasse circledl.h eingebunden werden (die Informationen über die ebenfalls
verwendeten Klassen CArea und CCircle werden über die Header-Datei fmomdoc.h, die
areas.h inkludiert, bezogen):
➪
Im Kopf der Datei fmomdoc.cpp wird die entsprechende #include-Anweisung
ergänzt:
// fmomdoc.cpp : implementation of the CFmomDoc class
//
#include "stdafx.h"
#include "fmom.h"
#include "fmomdoc.h"
#include "circledl.h"
◆
In der oben gelisteten Methode OnStandardflcheKreis wird zunächst eine Instanz
der Dialog-Klasse mit dem Namen dlg erzeugt. Der Konstruktor dieser Klasse
initialisiert alle Klassen-Variablen, die Mittelpunkt-Koordinaten und den Durchmesser
des Kreises mit dem Wert 0. (das ist akzeptabel), die Variable m_radio allerdings mit
dem Wert -1 (kein "Radio Button" gewählt). Dies wird auf den Wert 0 geändert
(damit gilt der erste "Radio Button" mit der Beschriftung "Teilfläche" als gewählt).
Auf die Variablen der Klasse CCircleDlg kann direkt zugegriffen werden, weil sie
vom "Class Wizard" public deklariert wurden.
◆
Die von CDialog an CCircleDlg vererbte Methode DoModal leistet die Hauptarbeit:
Sie initialisiert die Dialog-Box mit den Variablen aus der Instanz dlg (also z. B. mit
dem m_radio-Wert, der gerade gesetzt wurde), führt danach die gesamte Interaktion
mit dem Programm-Benutzer aus und aktualisert, wenn die Dialog-Box über OK
geschlossen wird, die Klassen-Variablen der Dialog-Klasse mit den eingegebenen
Werten.
J. Dankert: C++-Tutorial
◆
307
Wenn die Dialog-Box nicht durch Klicken auf den Cancel-Button verlassen wird,
liefert die Methode DoModal den Return-Wert IDOK ("Identifikator OK"). Nur in
diesem Fall wurden die Variablen der Dialog-Klasse aktualisiert, und nur in diesem
Fall wird "ein neuer Kreis" in der Dokument-Datenstruktur registriert. Dafür sind drei
Programmzeilen in OnStandardflcheKreis erforderlich:
Mit new wird eine Instanz der Klasse CCircle erzeugt, dem Konstruktor
werden die Parameter des Kreises übergeben, die in der Instanz dlg der
Dialog-Klasse gespeichert sind. Der von new abgelieferte Pointer wird auf
einer Pointer-Variablen der (abstrakten) Basisklasse CArea abgelegt.
Mit der CArea-Methode set_aoh wird die Information "Teilfläche oder
Ausschnitt?" in Abhängigkeit von der m_radio-Variablen in die CCircleInstanz eingebracht: 1 (Teilfläche), wenn "Radio Button" 0 ausgewählt war, -1
(Ausschnitt), wenn "Radio Button" 1 ausgewählt war.
Mit der CFmomDoc-Methode NewArea wird der Pointer auf die CAreaInstanz ("der neue Kreis") in der Dokument-Datenstruktur (verkettete Liste)
verankert.
◆
Der Aufruf der CDocument-Methode UpdateAllViews löst gegenwärtig im Programm noch keine erkennbare Reaktion aus, ist für die zukünftigen ProgrammErweiterungen vorausschauend schon eingebaut worden: Wenn sich die Datenstruktur
des Dokuments ändert, werden alle Ansichten, die die Daten des Dokuments
darstellen, ungültig. Da die Dokument-Klasse eine Liste aller zu ihr gehörenden
Views verwaltet, kann sie dafür sorgen, daß in allen Views die Methode OnDraw
aktiviert wird (diese ist im Programm fmom allerdings noch in dem jungfräulichen
Zustand, wie sie vom "App Wizard" erzeugt wurde). Das Argument NULL, das an
UpdateAllViews übergeben wird, veranlaßt, daß ausnahmslos alle Views aktualisiert
werden (in vielen Fällen wird die Änderung der Datenstruktur interaktiv über eine
View ausgelöst, die selbst dadurch aktuell ist, der Pointer auf diese View kann dann
als Argument an UpdateAllViews übergeben werden, wodurch eine Aktualisierung
der View vermieden wird).
➪
Nach der Aktualisierung
des Projekts fmom (Build
FMOM.EXE im Menü
Project) wird das Programm gestartet (Execute
FMOM.EXE). Man kann
nun im Menü Standardfläche das Angebot Kreis
wählen, es erscheint die
Dialog-Box, und es können
Werte eingegeben werden
(nebenstehende Abbildung).
Der nun erreichte Zustand des
Projekts gehört zum Tutorial als
Version "fmom4".
J. Dankert: C++-Tutorial
14.4.8
308
Bearbeiten der Ansichts-Klasse, Ausgabe erster Ergebnisse
Da bereits die Beschreibungen von Flächen eingegeben werden können und diese auch in der
Datenstruktur des Dokuments gespeichert werden, sollen erste Berechnungen durchgeführt
und die Ergebnisse ausgegeben werden. Dafür wird zunächst in der Dokument-Klasse eine
Methode etabliert, mit der die Gesamtfläche A und die statischen Momente der Gesamtfläche
Sx und Sy nach den Formeln
berechnet werden. Die Koordinaten des Gesamt-Schwerpunkts ergeben sich dann nach
(vgl. z. B. "Dankert/Dankert: Technische Mechanik, computerunterstützt", Seite 28).
➪
In der "Visual Workbench" wird die Datei fmomdoc.cpp geöffnet, der Code für die
Methode ASxSy wird ergänzt:
int CFmomDoc::ASxSy (double &a , double &sx , double &sy)
{
double ai ;
if (m_areaList.IsEmpty ()) return 0 ;
a = 0. ;
sx = 0. ;
sy = 0. ;
for (POSITION pos = GetFirstAreaPos () ; pos != NULL ; )
{
CArea *pArea = GetNextArea (pos) ;
ai
= pArea->get_a () ;
a
+= ai ;
sx += ai * pArea->get_yc () ;
sy += ai * pArea->get_xc () ;
}
return 1 ;
}
◆
Der Return-Wert der Methode ASxSy ist 0, wenn die Liste der Flächen leer ist (und
keine Ergebnisse abgeliefert werden), ansonsten 1.
◆
Die for-Schleife zeigt eine Alternative zu der im Abschnitt 14.4.4 verwendeten whileSchleife, um eine verkettete Liste vom Typ CObList komplett zu "traversieren". Da
die mit den CArea-Pointern aufgerufenen Methoden in der abstrakten Klasse CArea
nicht definiert sind, werden automatisch die zur "Herkunft der Pointer" passenden
Methoden aufgerufen (bisher sind das ausschließlich CCircle-Methoden).
Für die neue CFmomDoc-Methode muß noch die Deklaration in der Klassen-Deklaration
ergänzt werden:
➪
In der "Visual Workbench" wird die Header-Datei der Dokument-Klasse fmomdoc.h
geöffnet und im public-Bereich um die nachfolgend fett gedruckte Zeile ergänzt:
J. Dankert: C++-Tutorial
309
public:
// Zugriffsmethoden auf die doppelt verkettete Liste m_areaList:
void
NewArea
(CArea *pArea) ;
POSITION GetFirstAreaPos ()
;
CArea*
GetNextArea
(POSITION &pos) ;
int
ASxSy
(double &a , double &sx , double &sy) ;
Die zu berechnenden Ergebnisse sollen über die Ansichts-Klasse auf den Bildschirm gebracht
werden. Ein Gerüst für die Methode OnDraw ist bereits in der Datei fmomview.cpp vom
"App Wizard" angelegt worden und enthält bereits den Aufruf der Methode GetDocument,
die einen Pointer auf die Dokument-Klasse abliefert (vgl. Abschnitt 14.3), so daß auf deren
Methoden zugegriffen werden kann (also auch auf die gerade erzeugte Methode ASxSy).
➪
In der "Visual Workbench" wird die Datei fmomview.cpp geöffnet, im bereits
angelegten Gerüst der Methode OnDraw werden zunächst nur die nachfolgend fett
gedruckten Zeilen ergänzt:
////////////////////////////////////////////////////////////////////////
// CFmomView drawing
void CFmomView::OnDraw(CDC* pDC)
{
CFmomDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
double a , sx , sy ;
CRect clientRect ;
GetClientRect (&clientRect) ;
if (pDoc->ASxSy (a , sx , sy))
{
// Hier muss der Code fuer die Ergebnis-Ausgabe ergaenzt werden!
}
else
pDC->DrawText ("Bitte Option aus Menü wählen!" , -1 ,
&clientRect , DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
}
Die Erweiterung der Methode OnDraw liefert zwar noch keine Ergebnis-Ausgabe, zeigt aber,
wie auf eine CFmomDoc-Methode zugegriffen wird. Wenn noch keine Fläche definiert ist
(beim Programmstart), wird ein Text zentrisch in das Fenster geschrieben (vgl. das "Hello
World"-Programm im Abschnitt 14.3), der nach der Eingabe der ersten Fläche verschwindet.
➪
Nach der Aktualisierung des Projekts fmom (Build FMOM.EXE im
Menü Project) wird das Programm
gestartet (Execute FMOM.EXE).
Der in OnDraw ausgegebene Text
erscheint im Fenster (nebenstehende
Abbildung), auch nach Änderung
der Fenstergröße wieder zentriert.
Man wählt im Menü Standardfläche das Angebot Kreis und schließt
die Dialog-Box mit OK. Da nun
eine Fläche in der Datenstruktur
existiert, verschwindet der Text.
J. Dankert: C++-Tutorial
310
Bevor die Methode OnDraw um die Anweisungen für die Ausgabe der Ergebnisse ergänzt
wird, sind einige Bemerkungen zur Text-Ausgabe angebracht, obwohl zunächst weitgehend
die Standard-Voreinstellungen akzeptiert werden:
◆
Das voreingestellte Standard-Koordinatensystem MM_TEXT hat seinen Ursprung in
der linken oberen Ecke der "Client Area" ("Netto"-Fläche des Fensters ohne
Titelleiste und Ränder), die Einheiten in diesem Koordinatensystem sind bei
Bildschirm-Ausgabe "Pixel". Für die Ausgabe von Text ist dieses Koordinatensystem
besonders gut geeignet.
◆
Die bereits verwendete Methode DrawText positioniert den Text in einem Rechteck.
Sie empfiehlt sich damit z. B. für das Problem, einen Text in einem Bereich zentriert
auszugeben.
◆
Die sicher am häufigsten verwendete Methode zur Ausgabe von Text ist OutText, die
den Text mit den Koordinaten eines Punktes positioniert. Es ist ein Punkt auf dem
Rechteck, das durch Texthöhe und -länge bestimmt wird, Voreinstellung ist der Punkt
in der linken oberen Ecke diese Rechtecks, dies kann mit der Methode SetTextAlign
geändert werden.
◆
Eine Alternative zu TextOut ist die Ausgabe von Text mit TabbedTextOut. Diese
Methode berücksichtigt Tabulatorzeichen (codiert als \t) im String, wobei die
Standard-Tabulator-Positionen verwendet werden können, es ist sogar möglich, ein
Array der Tabulator-Positionen zu übergeben. Speziell bei Proportionalschriften, bei
denen der Ausgleich durch Leerzeichen nicht mehr sinnvoll ist, kann TabbedTextOut
eine wesentliche Hilfe sein, um vertikal ausgerichtete Kolonnen auszugeben.
◆
Leider gibt es (sicher historisch bedingt) einige Unterschiede bei der Übergabe der
Strings an die Text-Ausgabe-Funktionen (und auch bei anderen "String-Verarbeitern").
Die drei genannten Funktionen erwarten neben dem String auch noch die Anzahl der
Zeichen als zusätzliches Argument. Während bei DrawText dort eine -1 stehen darf,
wenn ein "normaler" (durch die ASCII-Null begrenzter) String übergeben wird,
akzeptieren TextOut und TabbedTextOut diese Variante nicht.
TextOut ist allerdings überladen, es existiert neben der Variante mit vier Argumenten
(zwei Koordinaten, String, Zeichenanzahl) noch eine Variante mit drei Argumenten,
bei der ein Objekt der Klasse CString erwartet wird, dafür wird auf die Zeichenanzahl verzichtet. Das ist deshalb eine besonders gute Variante, weil damit auch
"normale" String-Konstanten als Argumente möglich sind. Leider ist für TabbedTextOut eine solche Variante nicht verfügbar.
◆
Die Methode GetTextMetrics liefert die Abmessungen der Zeichen des "Current
Font" in einer Struktur vom Typ TEXTMETRIC ab (enthält die beachtliche Anzahl
von 20 Werten). Nachfolgend werden davon die drei wohl wichtigsten verwendet: Der
Wert tmAveCharWidth ist die "mittlere Zeichenbreite" (bei Proportionalschriften
kann nur ein Mittelwert angegeben werden), tmHeight ist die Gesamthöhe, die von
den Zeichen beschrieben wird (von der "Unterkante des g bis zur Oberkante des Ä
einschließlich der Pünktchen), tmExternalLeading ist der Abstand zwischen den
Zeilen ("Unterkante der oberen Zeile bis zur Oberkante der folgenden Zeile").
311
J. Dankert: C++-Tutorial
➪
In der "Visual Workbench" wird die Datei fmomview.cpp geöfnet, die Methode
OnDraw wird um die nachfolgend fett gedruckten Zeilen ergänzt. Auch die mehrfach
aufgerufene Methode LineOut (Ausgabe einer typischen Ergebniszeile mit zwei
Strings und einem double-Wert) wird in dieser Datei untergebracht:
////////////////////////////////////////////////////////////////////////
// CFmomView drawing
void CFmomView::OnDraw(CDC* pDC)
{
CFmomDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
double a , sx , sy ;
CRect clientRect ;
GetClientRect (&clientRect) ;
if (pDoc->ASxSy (a , sx , sy))
{
TEXTMETRIC tm ;
int
cxChar , cyChar , y , x1 , x2 , x3 ;
pDC->GetTextMetrics (&tm) ;
cxChar = tm.tmAveCharWidth ;
cyChar = tm.tmHeight + tm.tmExternalLeading ;
x1
x2
x3
y
=
=
=
=
cxChar * 3 ;
x1 + cxChar * 36 ;
x2 + cxChar * 8 ;
cyChar * 2 ;
pDC->TextOut (x1 , y , "Programm 'Flächenmomente', Ergebnisse");
y = LineOut (pDC , x1 , x2 , x3 , y , cyChar * 2 ,
"Gesamtfläche:" , "A" , a) ;
y = LineOut (pDC , x1 , x2 , x3 , y , (cyChar * 3) / 2 ,
"Statisches Moment um x:" , "Sx" , sx) ;
y = LineOut (pDC , x1 , x2 , x3 , y , cyChar ,
"Statisches Moment um y:" , "Sy" , sy) ;
if (fabs (a) > 1.e-20)
{
y = LineOut (pDC , x1 , x2 , x3 , y , (cyChar * 3) / 2 ,
"Schwerpunkt-Koordinaten:" , "xS" , sy / a) ;
y = LineOut (pDC , x1 , x2 , x3 , y , cyChar ,
"" , "yS" , sx / a) ;
}
}
else
pDC->DrawText ("Bitte Option aus Menü wählen!" , -1 ,
&clientRect , DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
}
int CFmomView::LineOut (CDC* pDC , int x1 , int x2 , int x3 , int y ,
int yoff , CString s1 , CString s2 , double v)
{
char wstring [80] ;
sprintf (wstring
y += yoff ;
pDC->TextOut (x1
pDC->TextOut (x2
pDC->TextOut (x3
return y ;
}
, "=
%g" , v) ;
, y , CString (s1)) ;
, y , CString (s2)) ;
, y , CString (wstring)) ;
J. Dankert: C++-Tutorial
➪
312
Die Methode LineOut muß noch in der Klassen-Deklaration in der Header-Datei
fmomview.h deklariert werden:
public:
virtual ~CFmomView();
virtual void OnDraw(CDC* pDC); // overridden to draw this view
int
LineOut (CDC* pDC , int x1 , int x2 , int x3 , int y ,
int yoff , CString s1 , CString s2 , double v) ;
Die gelistete Variante für die Programmierung der Ergebnis-Ausgabe ist natürlich nur eine
spezielle Möglichkeit, man könnte es (wie immer) auch ganz anders machen, deshalb einige
Erläuterungen zur vorgestellten Realisierung:
◆
Das "Layout" für die Ausgabe mit den Abmessungen des eingestellten System-Fonts
zu konzipieren, ist für die Textausgabe natürlich dann sinnvoll, wenn dieser Font für
den Text auch verwendet wird (GetTextMetrics liefert zwar die Abmessungen des
"Current Font", da im Programm der Font nicht speziell eingestellt wurde, ist es der
System-Font). Aber selbst für Graphikausgabe werden die Abmessungen des SystemFonts von vielen Programmierern gern als Bezugsgröße verwendet, weil sie unabhängig von der Bildschirmgröße ein sinnvolles Maß dafür ist, was in welcher Größe
noch "erkennbar" ist.
◆
In OnDraw werden mit den Abmessungen des Fonts drei horizontale Positionen für
die Ausgabe und die Abstände der Zeilen festgelegt. Für die Ausgabe einer Zeile ist
die spezielle Routine LineOut zuständig, die neben dem Pointer auf den "Device
Context" die Positionen für die Ausgabe von zwei Strings und eines double-Wertes
und einen vertikalen Offset übernimmt (und natürlich die beiden Strings und den
double-Wert). Die vertikale Position y wird vor der Ausgabe um den Offset
vergrößert ("Zeilensprung"), der neue Wert wird außerdem als Return-Wert abgeliefert
und steht damit für den nachfolgenden LineOut-Aufruf zur Verfügung.
◆
Für die Textausgabe wird die Methode CDC::TextOut in der Version verwendet, die
ein CString-Objekt erwartet, so daß die Zeichenanzahl nicht mit übergeben werden
muß.
◆
Die Ausgabe der Schwerpunkt-Koordinaten wird von der Größe der Gesamtfläche mit
der willkürlich gewählten Schranke 1.e-20 abhängig gemacht, um "Division durch
Null" zu vermeiden. Natürlich sollte diese Schranke gelegentlich durch eine zentral
verwaltete "EPS-Konstante" ersetzt werden.
◆
Auf eine weitere Verbesserung des Programmier-Stils wurde auch bewußt verzichtet,
damit hier deutlich bleibt, welche Auswirkungen die Programmzeilen auf die
"Ansicht" haben: Alle Strings wurden "hard-coded" in den Programmtext geschrieben.
Tatsächlich ist es nur eine kleine Mühe, Strings als "Ressourcen" zu definieren. Dies
wurde im Abschnitt 10.2 ohne Unterstützung von "App Studio" bereits demonstriert,
im Abschnitt 14.4.2 wurden Strings mit dem String-Editor von "App Studio" geändert,
ein Klick auf den Button New in diesem Editor würde das Eintragen neuer Strings
und der zugehörigen Identifikatoren ermöglichen. Dies sind Objekte der Klasse
CString, die mit der Methode CString::LoadString unter Angabe des Identifikators
aus der Ressourcen-Datei gelesen werden können.
313
J. Dankert: C++-Tutorial
➪
Nach der Aktualisierung
des Projekts fmom (Build
FMOM.EXE im Menü
Project) wird das Programm gestartet (Execute
FMOM.EXE). Wenn man
nun der Eingabeaufforderung "Bitte Option aus
Menü wählen" folgt, Standardfläche und Kreis
wählt (mehr ist ja noch
nicht möglich), einen Kreis
über die Dialog-Box definiert und mit OK abschließt, erscheinen sofort die Ergebnisse. Die Abbildung oben rechts zeigt dies,
nachdem bereits zwei Kreise eingegeben wurden:
Teilfläche,
Ausschnitt,
Mittelpunkt: x = 2 ,
Mittelpunkt: x = 1.5 ,
y = 2.5 ,
y=2,
Durchmesser: d = 4 ,
Durchmesser: d = 2 .
Der nun erreichte Zustand des Projekts gehört zum Tutorial als Version "fmom5". Mit
dieser Version können also schon (bescheidene) Berechnungen ausgeführt werden. Es lohnt
sich auch, einige der vom "App Wizard" gratis spendierten Fähigkeiten des Programms zu
inspizieren:
➪
Wenn man im Menü File das Angebot
Print Preview wählt, sieht man, daß die
Methode OnDraw auch für die Druckerausgabe aufgerufen wird. In der Vorschau
erscheint die gleiche Ausgabe wie auf
dem Bildschirm (nebenstehende Abbildung). Sicher ist daran noch einiges zu
verbessern, aber immerhin, einem geschenkten Gaul ...
Es stehen sogar die Hilfsfunktionen zur
Verfügung (Zoom in, Zoom out, ...).
Wenn man "zoomt", erkennt man, daß
möglicherweise für die Druckerausgabe
ein anderer Standard-Font eingestellt ist,
alle Berechnungen der Abstände beziehen
sich dann natürlich auf die Metrik dieses
Fonts. Auch der Button mit der Beschriftung Print... kann (wie das Angebot
Print... im Menü File) mit Erfolg angeklickt werden: Es erscheint die typische
"Drucken"-Dialog-Box von Windows
(nebenstehende Abbildung), Abbrechen
spart ein Blatt Papier.
J. Dankert: C++-Tutorial
314
Zur "Multi-Dokument-Fähigkeit" des
Programms hat der Programmierer
bisher nichts anderes beigetragen als
das Akzeptieren der Voreinstellung
der "App Wizard"-Optionen:
➪
Man wählt im Menü File das
Angebot New (schneller noch
durch Anklicken des Buttons,
der in der "Toolbar" ganz
links angeordnet ist), und ein
neues Dokument zeigt sich in
einem neuen Fenster. In
diesem Dokument kann man
(völlig unabhängig von dem
bereits existierenden Dokument) eine neue Berechnung
starten (nebenstehende Abbildung), beliebig zwischen
den Dokumenten wechseln, weitere Dokumente öffnen, geöffnete Dokumente wieder
schließen usw.
Das Projekt fmom der Version "fmom5" ist um die Möglichkeit zu
erweitern, neben den Kreisen auch Rechtecke als Teilflächen und Ausschnitte zu verarbeiten. Ein Rechteck soll definiert werden durch zwei Punkte, die auf einer
Diagonalen liegen (die Deklaration einer entsprechenden Klasse CRectangle und der Code
für die Methoden sind in den Dateien areas.h bzw. rectangl.cpp bereits vorhanden, vgl.
Abschnitt 14.4.3). Im einzelnen sind folgende Schritte zu realisieren:
Aufgabe 14.1:
a)
Mit "App Studio" ist eine Dialog-Box für die Beschreibung eines Rechtecks zu
erzeugen (beachten Sie dazu den nachfolgenden "Tip 1".
b)
Es ist eine Dialog-Klasse CRectDlg zu erzeugen, die die erforderlichen KlassenVariablen für den Datenaustausch mit der Dialog-Box enthält (analog zur Erzeugung
der Klasse CCircleDlg im Abschnitt 14.4.6).
c)
Der Dialog ist in das Programm einzubinden (analog zur Einbindung des Dialogs zur
Eingabe eines Kreises im Abschnitt 14.4.7).
d)
Da alle weiteren Anschlüsse an die Datenstruktur
und den Berechnungs-Algorithmus durch die abstrakte Klasse gegeben sind, kann das entstehende
Programm sofort mit der nebenstehend skizzierten
Fläche getestet werden (beachten Sie hierzu den
nachfolgenden "Tip 2").
Die wichtigsten Ergebnisse für das Testbeispiel
(Gesamtfläche und Schwerpunkt-Koordinaten) sind
im Abschnitt 12.4 zu finden, wo sie mit dem
Programm schwerp3.cpp berechnet wurden.
J. Dankert: C++-Tutorial
315
Bei der Erzeugung einer Dialog-Box mit dem "App Studio" kann man
häufig mehrere Elemente aus einer bereits existierenden ähnlichen Box
mittels "Drag and Drop" kopieren und damit eine erhebliche Arbeitseinsparung erzielen.
Tip 1:
Die nebenstehende Abbildung
zeigt den Dialog-Editor von
"App Studio" mit der DialogBox IDD_DIALOG_KREIS
(rechtes Fenster), die im Abschnitt 14.4.6 erzeugt wurde. Im
Fenster links unten sieht man
die für die Aufgabe 14.1 zu
erzeugende neue Dialog-Box
IDD_DIALOG_RECHTECK,
aus der die vom Dialog-Editor
automatisch eingefügten Buttons
OK und CANCEL gelöscht
wurden, um danach mittels
"Drag and Drop" alle Elemente
aus der Kreis-Dialog-Box zu
übertragen, die für die
Rechteck-Dialog-Box brauchbar
sind (dieser Zustand ist im Bild
dargestellt). Nun müssen noch der "Static Text" Mittelpunkt: editiert und zweckmäßigerweise auch die Namen der Identifikatoren für die "Edit Box"-Elemente sinnvoll geändert
werden. Schließlich sind noch (natürlich auch durch "Drag and Drop"-Kopieren) die "Static
Text"- und "Edit Box"-Elemente für den zweiten Punkt zu erzeugen.
Wer programmiert, macht Fehler, und Fehlersuche ist bei der WindowsProgrammierung nicht ganz einfach. Natürlich sollte die Debug-Option für
das Projekt während der Bearbeitung eingeschaltet sein. Darum braucht man sich bei der
Projekt-Erzeugung nicht zu kümmern, weil es die Standard-Einstellung ist.
Tip 2:
Kontrollieren kann man es über das Angebot Project... im Menü Options. In der
"Project Options"-Dialog-Box (nebenstehende Abbildung) sollte man allerdings nach
Fertigstellung des Projekts vor der letzten
Compilierung unbedingt auf Release umstellen, denn die EXE-Datei, die bei eingeschalteter Debug-Option erzeugt wird, ist
wesentlich größer.
"Debuggen" kann sehr zeitraubend sein, die außerordentlich schnellen Compiler haben
deshalb viele C-Programmierer veranlaßt, lieber mal schnell zur Kontrolle einige printfAnweisungen einzubauen und auf den Debugger weitgehend zu verzichten. Das ist natürlich
bei der Windows-Programmierung nicht möglich.
Das in MS-Visual-C++ verfügbare Makro TRACE verschafft dem Programmierer die gleichen
Möglichkeiten, wie sie die printf-Funktion bietet (bei allerdings gesteigertem Komfort, die
316
J. Dankert: C++-Tutorial
Ausgabe landet in einem anderen Fenster), wird also aufgerufen mit einem Format-String und
einer dazu passenden Anzahl von Variablen. Auch das probiert man am besten einmal aus:
➪
In der "Visual Workbench" wird die Datei fmomdoc.cpp geöffnet. In der Methode
OnStandardflcheKreis ergänzt man z. B. die nachfolgend fett gedruckte Zeile, mit
der zwei Variablen aus der Instanz der Dialog-Klasse nach dem Schließen der DialogBox ausgegeben werden sollen (das Projekt fmom muß danach aktualisiert werden
mit Build FMOM.EXE im Menü Project):
void CFmomDoc::OnStandardflcheKreis()
{
CCircleDlg dlg ;
dlg.m_radio = 0 ;
if (dlg.DoModal () == IDOK)
{
TRACE ("OnStandardflcheKreis:
CArea* pArea =
pArea->set_aoh
NewArea
UpdateAllViews
m_d = %g m_radio = %d\n" ,
dlg.m_d , dlg.m_radio) ;
new CCircle (dlg.m_d , dlg.m_mpx , dlg.m_mpy) ;
((dlg.m_radio == 0) ? 1 : -1) ;
(pArea) ;
(NULL) ;
}
}
➪
Das "Tracing" muß aktiviert werden.
Dafür verwendet man eines der
zahlreichen Utilities, die zum Lieferumfang von MS-Visual-C++
gehören (wenn es in Ihrer Installation fehlt, sollten Sie es unbedingt
nachinstallieren). Das nebenstehende
Bild zeigt einen Ausschnitt aus dem
Fenster des Programm-Managers mit
den Icons einiger Utilities.
Ein Doppelklick auf das mit "MFC Trace Options"
beschriftete Icon läßt die "MFC Trace Options"-DialogBox erscheinen, in der Enable Tracing "angekreuzt"
werden muß (nebenstehende Abbildung). Mit OK wird
die Dialog-Box geschlossen.
Nach Rückkehr zur "Visual Workbench" in MS-VisualC++ wird im Menü Debug das Angebot Go gewählt,
und der Debugger startet das Programm. Alle TRACEAufrufe erzeugen eine Ausgabe in das Output-Window
(Abbildung unten rechts
zeigt es nach Eingabe
einer Teilfläche und
eines Ausschnitts), die
man während der Programmabarbeitung verfolgen kann.
J. Dankert: C++-Tutorial
317
Neben der Annehmlichkeit, die TRACE-Ausgaben in einem gesonderten Fenster verfolgen
zu können, gibt es noch einen nicht zu unterschätzenden zusätzlichen Vorteil. Wenn die
Debug-Option (Angebot Project... im Menü Options) ausgeschaltet wird, haben die TRACEMakros keinerlei Funktion mehr, können also schadlos im Programm verbleiben.
Der nach Erledigung der Aufgabe 14.1 erreichte Zustand des Projekts
gehört zum Tutorial als Version "fmom6", auch der Quelltext, so daß Sie
natürlich "schummeln" können (und die Aufgabe nicht selbst lösen). Andererseits: Wer sich
bis zu diesem Punkt des Tutorials durchgearbeitet hat, "schummelt" entweder nicht (weil er
etwas lernen will), oder er kann es sich leisten. Beides ist akzeptabel.
Tip 3:
14.4.9
Die Return-Taste muß Kompetenzen abgeben
Möglicherweise sind Sie beim Testen der Dialog-Box auch auf die Return-Taste reingefallen,
die die Arbeit sofort abbricht (das ist "Windows-Philosophie", der Schreiber dieses Tutorials
ist Abonnent bei diesem Bedienungs-Fehler).
Die Return-Taste wirkt in der Regel auf den Button, der den Eingabefokus hat, wie ein
Mausklick auf diesen Button (der "Pünktchen-Rahmen" um die Beschriftung kennzeichnet
den Button mit dem Eingabefokus). Wenn kein Button den Eingabefokus hat (oder es wird
z. B. gerade in eine "Edit Box" eingegeben, und dabei stört die Reaktion ja gerade), sucht
Windows nach dem "Standard-Button" (das ist der mit dem dickeren Rahmen) und führt die
Funktion aus, die für das Klicken auf diesen Button zuständig ist. Wenn auch kein "StandardButton" festgelegt wurde, wird trotzdem die von CDialog ererbte Methode OnOK aufgerufen, die für die Behandlung des Klickens auf den OK-Button zuständig ist (und genau das ist
das Problem).
Wenn man nun in der abgeleiteten Dialog-Klasse eine eigene OnOK-Funktion etabliert, die
entweder nichts tut oder aber z. B. den Eingabefokus auf das Nachfolge-Element setzt, muß
man für die Botschaft, die durch das Anklicken des OK-Buttons ausgelöst wird, eine andere
Methode vorsehen (diese braucht nur die CDialog::OnOK-Methode aufzurufen, und für das
Klicken auf den OK-Button hat sich nichts geändert). Genau diese Strategie wird nun für die
Dialog-Box zur Eingabe eines Kreises in der Klasse CCircleDlg realisiert:
➪
"App Studio" wird gestartet (aus der "Visual Workbench" z. B. mit AppStudio im
Menü Tools). Im linken Listen-Fenster wird Dialog gewählt, Doppelklick auf
IDD_DIALOG_KREIS im rechten Listen-Fenster startet den Dialog-Editor mit der
gewählten Dialog-Box. Nach Doppelklick auf den OK-Button öffnet die "Property
Page", in der die ID: auf
ID_CIRCLE_OK geändert
wird, außerdem wird die
Eigenschaft Default Button
deaktiviert (nebenstehende
Abbildung). Der breitere
Rahmen um den OK-Button
ist verschwunden.
318
J. Dankert: C++-Tutorial
➪
Über Resource und ClassWizard... wird der "Class Wizard" gestartet, der sich mit
der Klasse CCircleDlg meldet. Unter den Object IDs findet man den gerade
erzeugten Identifikator ID_CIRCLE_OK (ist
sogar schon als "ausgewählt" gekennzeichnet),
im Fenster Messages: wird BN_CLICKED
gewählt, und nach Anklicken des Buttons Add
Function... erscheint in der "Add Member
Function"-Dialog-Box der vorgeschlagene
Member Function Name: OnCircleOk (nebenstehende Abbildung), der durchaus akzeptabel
ist. Mit OK wird die Dialog-Box geschlossen, über den Button Edit Code landet man
in der Datei circledl.cpp, der Cursor steht in der gerade vom "App Wizard" erzeugten
Funktion OnCircleOk, in die nun nur der Aufruf der CDialog-Methode OnOK
eingefügt wird:
void CCircleDlg::OnCircleOk ()
{
CDialog::OnOK () ;
}
Außerdem wird die Methode OnOK in dieser Klasse definiert, die die von CDialog
ererbte OnOK-Methode überdeckt, die dann nur noch über OnCircleOk erreicht
wird. In die eigene OnOK-Methode wird die Weitergabe des Eingabefokus an das (in
der "Tab-Order") folgende Element eingefügt. Das erledigt die von CDialog geerbte
Methode NextDlgCtrl:
void CCircleDlg::OnOK ()
{
NextDlgCtrl () ;
}
➪
Während die Deklaration von OnCircleOk vom "App Wizard" in die Deklaration der
Klasse CCircleDlg eingefügt wurde, muß die ohne Hilfe des "Class Wizard"
geschriebene Methode OnOK dort "von Hand" eingefügt werden: Man öffnet die
Datei circledl.h und ergänzt in der Deklaration der Klasse CCircleDlg die nachfolgend fett gedruckte Zeile:
// Implementation
public:
void OnOK () ;
➪
Das Projekt fmom muß aktualisiert werden (z. B. mit Build FMOM.EXE im Menü
Project), nach dem Starten des Programms (Execute FMOM.EXE) reagiert die
Dialog-Box für die Eingabe eines Kreises nicht mehr so nervös beim Drücken der
Return-Taste.
Der nun erreichte Zustand des Projekts gehört zum Tutorial als Version "fmom7".
In der Version "fmom7" reagiert die Dialog-Box zur Eingabe eines
Rechtecks auf das Drücken der Return-Taste anders als die Dialog-Box
für die Eingabe eines Kreises. Bearbeiten Sie die Dialog-Box für das Rechteck und die
zugehörige Dialog-Klasse so, daß auch bei Eingabe eines Rechtecks das Drücken der ReturnTaste zur gleichen Reaktion führt wie bei der Eingabe eines Kreises.
Aufgabe 14.2:
J. Dankert: C++-Tutorial
14.4.10
319
Ein zusätzlicher "Toolbar"-Button für "fmom"
Die "Toolbar-Buttons" dienen dazu, häufig gewählte Menü-Angebote schneller zu erreichen
(und sind natürlich hilfreich für die Analphabeten unter den Programm-Benutzern). Einige
sind bereits vom "App Wizard" kreiert worden, in diesem Abschnitt soll ein Button mit
einem Kreis hinzukommen, der die gleiche Reaktion auslöst wie die Auswahl des Angebotes
Kreis im Menü Standardfläche.
➪
Nach dem Start von "App Studio" wird im linken Listen-Fenster Bitmap gewählt. Im
rechten Listen-Fenster erscheint mit IDR_MAINFRAME die einzige bisher existierende Ressource dieser Art. Nach Doppelklick auf IDR_MAINFRAME öffnen sich
gleich mehrere Fenster. Zunächst sollte man aus dem Menü Image das Angebot Grid
Settings wählen: Es öffnet sich die "Grid
Settings"-Dialog-Box, in der sowohl Pixel
Grid (sollte voreingestellt sein) als auch Tile
Grid angekreuzt werden (nebenstehende
Abbildung). Die Voreinstellungen für Width
(16) und Height (15) sind zu akzeptieren.
Nach Klicken auf OK sind in der vergrößerten Darstellung der "Button-Leiste" in dem
Fenster, das mit IDR_MAINFRAME (Bitmap) überschrieben ist (befindet sich in der folgenden Abbildung im unteren Teil),
vertikale Trennstriche zwischen den einzelnen Symbolen zu sehen (das "Tile Grid").
Im linken Teil dieses "Splitter-Windows" (ein solches Window wird fmom später
auch einmal erhalten, der hier vertikale Balken, der das Window "splittet", kann mit
der Maus verschoben werden) sind die "Toolbar"-Buttons in Originalgröße zu sehen.
Das schmale Fenster rechts ist die "graphische Palette":
Zwischen "Diskette" und "Schere" soll das neue Symbol eingefügt werden
J. Dankert: C++-Tutorial
320
➪
Mit der unteren Laufleiste wird die "Bitmap"
soweit nach links verschoben, daß rechts
Platz für einen weiteren
Button entsteht (nebenstehende Abbildung).
➪
Durch "Ziehen" mit der
Maus (bei gedrückter
linker Taste) wird Platz
für einen zusätzlichen
Button erzeugt (nebenstehende Abbildung).
Das weiße Feld soll nun
an den gewünschten
Platz (zwischen "Diskette" und "Schere") transportiert werden, indem die vorhandenen
Symbole um ein Feld nach rechts verschoben werden:
➪
Das "Selektions-Rechteck" in der "graphischen Palette" (das "Pünktchen-Rechteck" in
der oberen Reihe) wird angeklickt, danach kann in der "Bitmap" ein Rechteck
"aufgezogen" werden (z. B. durch Drücken der linken Maustaste auf der linken
unteren Ecke, Maustaste während der Bewegung gedrückt halten, Loslassen der
Maustaste, wenn obere rechte Ecke erreicht ist). Sie sollten exakt den Bereich der zu
verschiebenen Symbole erfassen, wie es das nachfolgende Bild zeigt (ärgerlich ist,
daß das Selektions-Rechteck innerhalb der Grenzen des Bereichs plaziert werden muß,
der anschließend zu sehende Selektions-Rahmen liegt außerhalb des Bereichs):
➪
Wenn sich der Cursor in dem selektierten Bereich befindet, zeigt er mit seiner
"Kreuzform" an, daß der Bereich verschoben werden kann. Mittels "Drag and Drop"
wird er exakt um ein
Feld nach rechts verschoben, das weiße Feld
befindet sich danach
genau an der gewünschten Stelle zwischen
"Diskette" und "Schere"
(nebenstehende Abbildun).
J. Dankert: C++-Tutorial
➪
321
Nun wird in das weiße Feld ein Kreis gezeichnet, z. B. so: In der Farbpalette wird ein
mittleres Grau ausgewählt, anschließend wird im oberen Teil der Palette die "gefüllte
Ellipse" angeklickt. Man zeichnet die Ellipse in das weiße Feld, indem man "das
umschließende Rechteck aufzieht". Da es ein Kreis werden soll, ist ein Quadrat
"aufzuziehen" (Sie dürfen ruhig probieren, im Menü Edit existiert wieder ein
mehrstufig arbeitendes Undo, und in der
Palette gibt es einen "Radiergummi").
Anschließend wird die Farbe Schwarz
gewählt und die "nicht gefüllte Ellipse"
wird angeklickt, und durch "Aufziehen
eines Quadrats" wird der Rand des Kreises schwarz. Vielleicht sieht auch Ihr
Ergebnis etwa wie in der nebenstehenden
Abbildung aus.
Das war es eigentlich schon. Die "Bitmap" IDR_MAINFRAME, die die "Toolbar-Buttons"
definiert, ist um einen Button erweitert worden. Das Einbinden des Buttons als einen
Startpunkt des "Command Routings" ist einfach, weil das zugehörige Kommando bereits als
Menü-Kommando (mit dem Identifikator ID_STANDARDFLCHE_KREIS) existiert:
➪
Beim Verlassen von "App Studio" wird gefragt, ob die Änderungen in der
Ressourcen-Datei gespeichert werden sollen, was natürlich mit Ja zu beantworten ist.
In der "Visual Workbench" wird die Datei mainfrm.cpp geöffnet. Dort findet man
das Array buttons, das vom Programmgerüst für die Darstellung und die KommandoZuordnung der "Toolbar-Buttons" benutzt wird. Dies wird um die beiden nachfolgend
fett gedruckten Zeilen erweitert:
// toolbar buttons - IDs are command buttons
static UINT BASED_CODE buttons[] =
{
// same order as in the bitmap 'toolbar.bmp'
ID_FILE_NEW,
ID_FILE_OPEN,
ID_FILE_SAVE,
ID_SEPARATOR,
ID_STANDARDFLCHE_KREIS,
ID_SEPARATOR,
ID_EDIT_CUT,
ID_EDIT_COPY,
ID_EDIT_PASTE,
ID_SEPARATOR,
ID_FILE_PRINT,
ID_APP_ABOUT,
};
Man beachte, daß die Reihenfolge der Elemente im Array buttons exakt mit der Reihenfolge
in der gerade geänderten "Bitmap" übereinstimmen muß, ID_STANDARDFLCHE_KREIS
muß zwischen ID_FILE_SAVE ("Diskette") und ID_EDIT_CUT ("Schere") stehen.
ID_SEPARATOR sorgt für einen kleinen Abstand zwischen den "Buttons".
➪
Das Projekt fmom wird aktualisiert (z. B. mit Build FMOM.EXE im Menü Project).
Nach dem Start des Programms (Execute FMOM.EXE) sieht man den zusätzlichen
"Toolbar-Button", dessen Anklicken die "Kreis-Dialog-Box" öffnet.
Der nun erreichte Zustand des Projekts gehört zum Tutorial als Version "fmom9".
J. Dankert: C++-Tutorial
322
Das Projekt fmom ist um
einen zusätzlichen "Toolbar-Button" zu erweitern, mit dem die
Eingabe eines Rechtecks gestartet wird
(nebenstehende Abbildung).
Aufgabe 14.3:
Das Anklicken dieses Buttons soll den
gleichen Effekt haben wie die Wahl von
Rechteck im Menü Standardfläche.
Das Ergebnis der Aufgabe 14.3 entspricht der zum Tutorial gehörenden Version
"fmom10".
14.4.11
Das Dokument als Binär-Datei, "Serialization"
Die MFC-Klassen-Bibliothek unterstützt das Erzeugen und Lesen einer Binär-Datei, die die
gesamte Datenstruktur eines Dokuments repräsentiert und damit deren permanente Speicherung und Wiederverwendung in späteren Programm-Läufen ermöglicht. Dieser Prozeß wird
in den Microsoft-Manuals als "Serialization" bezeichnet.
Die Idee, die hinter der Unterstützung der "Serialization" steckt, ist einfach: Der Zustand der
Instanz einer Klasse wird beschrieben durch die Werte, die den Klassen-Variablen zugewiesen sind. Der Programmierer muß also veranlassen, daß genau diese Werte gesichert
bzw. geladen werden. Die Klassen-Bibliothek unterstützt diesen Prozeß für alle Klassen, die
von CObject abgeleitet sind. Weil die abstrakte Klasse CArea, aus der alle weiteren Klassen,
die das Dokument beschreiben, abgeleitet werden, "vorsorglich" aus CObjekt abgeleitet
wurde (Abschnitt 14.4.3), ist diese Voraussetzung für das Projekt fmom erfüllt.
Die Klassen-Bibliothek verwendet eine Instanz der Klasse CArchive als "Vermittler"
zwischen den Daten in den Dokument-Klassen und der zu erzeugenden bzw. zu lesenden
Datei. In CArchive sind die Operatoren >> und << überladen und dienen dazu, die Werte der
Variablen auf die CArchive-Instanz zu übertragen bzw. von dieser zu übernehmen (in dem
Sinne, wie mit den gleichen Operatoren in den "iostream"-Klassen von und zu den Objekten
cin und cout transferiert wird, vgl. Abschnitt 12.6).
Beim Präparieren einer Klasse für die "Serialization" kann man auf einige vordefinierte
Makros und einige Vorbereitungen zurückgreifen, die der "App Wizard" bereits erledigt hat.
Im einzelnen müssen folgende Schritte realisiert werden (natürlich sollte man dies sofort
beim Deklarieren einer neuen Klasse mit erledigen, es ist hier nur deshalb in einem
gesonderten Abschnitt angesiedelt, um den Prozeß einmal geschlossen darzustellen):
a)
Deklaration: Die Klasse ist von CObject oder einer von CObject abgeleiteten Klasse
abzuleiten.
b)
Deklaration: In der Deklaration der Klasse ist das Macro DECLARE_SERIAL
anzusiedeln.
c)
Deklaration und Implementierung: Die Klasse muß einen Konstruktor erhalten, der
keine Argumente übernimmt.
323
J. Dankert: C++-Tutorial
d)
Deklaration und Implementierung: Die von CObject geerbte Methode Serialize ist zu
überschreiben.
e)
Implementierung: Es ist das Makro IMPLEMENT_SERIAL zu implementieren, das
den erforderlichen Code erzeugt.
Dies scheint relativ aufwendig zu sein. Speziell die beiden Punkte b) und e) sind nicht gleich
verständlich, deshalb wenigstens eine kurze Erklärung dafür: Die Klassen-Bibliothek muß die
Klassen dynamisch (während der Laufzeit) erzeugen können (z. B. beim Erzeugen eines
Dokuments durch Lesen von der Binär-Datei). Um dies "typsicher" zu realisieren, müssen
einige spezielle Methoden verfügbar sein. Diese brauchen glücklicherweise nicht vom
Programmierer erzeugt zu werden. Dies wird von den Makros DECLARE_SERIAL
(Deklarationen) bzw. IMPLEMENT_SERIAL (Code) erledigt.
◆
Die komplette Datenstruktur des Dokuments wird in der aktuellen Version von fmom
in den Klassen CFmomDoc (verkettete Liste) bzw. CCircle und CRectangle
gehalten. Für diese Klassen sollen nun die oben genannten 5 Punkte abgearbeitet
werden:
Weil CFmomDoc von "App Wizard" erzeugt wurde, sind für diese Klasse die meisten
Vorkehrungen bereits getroffen worden, allerdings leicht abweichend von den oben genannten
5 Punkten, zunächst ein Auszug aus der Klassen-Deklaration in der Datei fmomdoc.h:
// fmomdoc.h : interface of the CFmomDoc class
// ...
class CFmomDoc : public CDocument
{
protected: // create from serialization only
CFmomDoc();
DECLARE_DYNCREATE(CFmomDoc)
// ...
protected:
CObList
m_areaList ;
//
//
Anker zur Datenstruktur der Applikation
(doppelt verkettete Liste von CArea-Pointern)
// ...
// Implementation
public:
virtual ~CFmomDoc();
virtual void Serialize(CArchive& ar);
// ...
};
// overridden for document i/o
/////////////////////////////////////////////////////////////////////////////
◆
Die Forderung a) ist erfüllt, weil die Basisklasse CDocument, aus der CFmomDoc
abgeleitet ist, CObject in ihrer Ahnenreihe hat. Für die Forderungen c) und d) sind
die Deklarationen bereits angelegt.
◆
An Stelle des nach b) geforderten Makros DECLARE_SERIAL wurde "nur"
DECLARE_DYNCREATE vom "App Wizard" vorgesehen. Dieses Makro ist
ausreichend, um die Unterstützung des dynamischen Anlegens von Instanzen zu
unterstützen, es fehlt die CArchive-Funktionalität (also sind z. B. die Operatoren >>
und << nicht wie oben beschrieben überladen). Diese wird in diesem Fall tatsächlich
nicht benötigt, weil das einzige zu "archivierende" Objekt eine Instanz der Klasse
J. Dankert: C++-Tutorial
324
CObList ist, die in der Lage ist, sich "selbst zu archivieren" (die Implementierung
von CObList enthält auch das Makro IMPLEMENT_SERIAL).
An der Klassen-Deklaration von CFmomDoc in der Header-Datei fmomdoc.h muß also gar
nichts ergänzt werden, auch in der Implementations-Datei wurde vom "App Wizard" fast
alles bereits eingetragen, was benötigt wird, wie folgender Ausschnitt aus fmomdoc.cpp
zeigt:
// fmomdoc.cpp : implementation of the CFmomDoc class
// ...
IMPLEMENT_DYNCREATE(CFmomDoc, CDocument)
// ...
/////////////////////////////////////////////////////////////////////////////
// CFmomDoc construction/destruction
CFmomDoc::CFmomDoc()
{
// TODO: add one-time construction code here
}
// ...
/////////////////////////////////////////////////////////////////////////////
// CFmomDoc serialization
void CFmomDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// TODO: add storing code here
}
else
{
// TODO: add loading code here
}
}
// ...
◆
Das Makro IMPLEMENT_DYNCREATE in fmomdoc.cpp ist das Pendant zu dem
Makro DECLARE_DYNCREATE in der Klassen-Deklaration. Da das Makro
IMPLEMENT_SERIAL zur Implementierung von CObList gehört, ist Forderung e)
damit erfüllt.
◆
Auch der Konstruktor, der keine Argumente erwartet, wurde von "App Studio"
erzeugt, womit Forderung c) erfüllt ist.
◆
Für die entsprechend Forderung d) zu überschreibende Methode Serialize hat der
"App Wizard" ein Gerüst angelegt, das vom Programmierer ausgefüllt werden muß.
Serialize wird mit einer Instanz von CArchive aufgerufen, die das Ziel bzw. die
Quelle des Archivierungs-Prozesses ist. Mit dieser Instanz kann die CArchiveMethode IsStoring aufgerufen werden (es gibt auch CArchive::IsLoading, aber
"Storing" und "Loading" schließen einander natürlich aus, deshalb ist die eine Abfrage
ausreichend). Diese liefert die "Richtung des Datentransfers", und der Programmierer
muß nun normalerweise im if-Zweig und im else-Zweig eintragen, was zu archivieren
bzw. zu lesen ist (und für die Klassen CCircle und CRectangle wird das nachfolgend
auch so erledigt).
325
J. Dankert: C++-Tutorial
Für die Klasse CFmomDoc ist auch diese Arbeit etwas einfacher, weil nur das
CObList-Objekt m_areaList zu speichern bzw. zu laden ist. Dafür kann die COblistMethode Serialize gerufen werden, die diese Frage ("Loading or Storing") ohnehin
selbst stellt, so daß schließlich nur eine einzige Programmzeile ergänzt werden muß:
➪
In der "Visual Workbench" wird die Datei fmomdoc.cpp geöffnet, in der die
Methode CFmomDoc::Serialize um die nachfolgend fett gedruckte Zeile ergänzt
wird:
void CFmomDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// TODO: add storing code here
}
else
{
// TODO: add loading code here
}
m_areaList.Serialize (ar) ;
}
Für die beiden Klassen CCircle und CRectangle sind die Punkte b) bis e) zu erledigen,
Punkt a) ist bereits erfüllt, weil sie aus CArea abgeleitet sind, CArea aber aus CObject
abgeleitet wurde.
➪
In der "Visual Workbench" wird die Datei areas.h geöffnet, in den KlassenDeklarationen von CCircle und CRectangle werden die nachfolgend fett gedruckten
Zeilen ergänzt:
class CCircle : public CArea
{
protected:
DECLARE_SERIAL (CCircle)
CCircle () ;
// ...
public:
CCircle (double d , double mpx , double mpy) ;
// ...
virtual void Serialize (CArchive &ar) ;
} ;
class CRectangle : public CArea
{
protected:
DECLARE_SERIAL (CRectangle)
CRectangle () ;
// ...
public:
CRectangle (double x1 , double y1 , double x2 , double y2) ;
// ...
virtual void Serialize (CArchive &ar) ;
} ;
326
J. Dankert: C++-Tutorial
◆
Das Makro DECLARE_SERIAL enthält in den Klammern den Namen der Klasse,
in der es steht. Man beachte, daß die Makros nicht mit einem Semikolon abgeschlossen werden.
➪
In der "Visual Workbench" wird die Datei circle.cpp geöffnet. Es werden der
zusätzliche Konstruktor, die Methode Serialize und das Makro IMPLEMENT_SERIAL ergänzt:
// Datei circle.cpp fuer die Version "fmom11"
#include "stdafx.h"
#include "areas.h"
CCircle::CCircle () {}
CCircle::CCircle (double d , double mpx , double mpy)
{
m_d = d ;
m_mp.set_x (mpx) ;
m_mp.set_y (mpy) ;
}
double CCircle::get_a ()
{
return (pi_4 * m_d * m_d * m_area_or_hole) ;
}
double CCircle::get_xc () { return (m_mp.get_x ()) ; }
double CCircle::get_yc () { return (m_mp.get_y ()) ; }
void CCircle::Serialize (CArchive &ar)
{
if (ar.IsStoring ())
{
ar << m_d ;
}
else
{
ar >> m_d ;
}
m_mp.Serialize
(ar) ;
Serialize_AreaVals (ar) ;
}
IMPLEMENT_SERIAL (CCircle , CObject , 1)
◆
Das Makro IMPLEMENT_SERIAL enthält in den Klammern den Namen der
Klasse, für die das Makro eingesetzt wird, den Namen der zugehörigen Basisklasse
und eine "Versions-Nummer" (postive ganze Zahl). Die Versions-Nummer sollte
geändert werden, wenn sich für die Klasse die "Serialization" ändert. Damit überwacht das Programm-Gerüst, daß keine Datei gelesen wird, die nicht mehr zur
aktuellen Programm-Version paßt.
◆
In der Methode CCircle::Serialize sieht man die beiden typischen Anweisungen zum
Speichern bzw. Lesen von Werten (ar << m_d bzw. ar >> m_d). Zur Vereinfachung
wurde für das Archivieren einer Instanz von CPoint_xy eine Methode Serialize in
deren Klassen-Definition ergänzt, mit der hier die Mittelpunkt-Koordinaten des
Kreises archiviert werden (wird nachfolgend beschrieben).
327
J. Dankert: C++-Tutorial
Die von CArea geerbte Variable m_area_or_hole könnte natürlich in CCircle direkt
archiviert werden. Das würde aber dem Prinzip der Kapselung widersprechen. Bei der
Weiterentwicklung des Programms werden zusätzliche Variablen in die Klasse CArea
aufgenommen werden (z. B. Farben zum Zeichen der Flächen), die alle abgeleiteten
Klassen erben. Deshalb wird bereits für das Archivieren der einen bisher vererbten
Variablen eine Methode Serialize_AreaVals kreiert, die von allen Erben benutzt wird
und bei Weiterentwicklung der Klasse CArea "mitwachsen" kann.
➪
In der Datei areas.h wird im public-Bereich der Deklaration der Klasse CPoint_xy
die Zeile
void Serialize (CArchive &ar) ;
ergänzt, im public-Bereich der Deklaration der Klasse CArea ist die Zeile
void Serialize_AreaVals (CArchive &ar) ;
hinzuzufügen.
➪
In der "Visual Workbench" wird die Datei point_xy.cpp geöffnet, es wird die
Methode CPoint_xy::Serialize ergänzt:
void CPoint_xy::Serialize (CArchive &ar)
{
if (ar.IsStoring ())
{
ar << m_x ;
ar << m_y ;
}
else
{
ar >> m_x ;
ar >> m_y ;
}
}
➪
In der "Visual Workbench" wird die Datei area.cpp geöffnet, es wird die Methode
CArea::Serialize_AreaVals ergänzt:
void CArea::Serialize_AreaVals (CArchive &ar)
{
if (ar.IsStoring ())
{
ar << (WORD) m_area_or_hole ;
}
else
{
WORD i ;
ar >> i ;
m_area_or_hole = i ;
}
}
◆
In der Methode CArea::Serialize_AreaVals fällt das "Casten" der int-Variablen auf
den Typ WORD vor dem Speichern und das entsprechend umständliche Lesen auf.
Das ist ein Stück "Zukunftssicherheit", die vom MS-Visual-C++-Compiler sogar
erzwungen wird. In der "16-Bit-Welt" werden in MS-Visual-C++ int-Variablen mit 2
Byte, in der "32-Bit-Welt" (Windows 95, Windows NT) mit 4 Byte gespeichert, was
bei Binär-Dateien natürlich zu Inkompatibilitäten führen würde. "WORD ist WORD",
und mit der programmierten Konstruktion ist man also aufwärtskompatibel.
J. Dankert: C++-Tutorial
328
Natürlich werden die Methoden, die in CPoint_xy bzw. CArea ergänzt wurden, mit Vorteil
auch in der Serialize-Methode von CRectangle verwendet. Da ein Rechteck nur durch zwei
Punkte (und die von CArea geerbte Variable m_area_or_hole) beschrieben wird, ist
CRectangle::Serialize besonders einfach:
➪
In der "Visual Workbench" wird die Datei rectangl.cpp geöffnet, es werden folgende
Zeilen ergänzt:
void CRectangle::Serialize (CArchive &ar)
{
m_p1.Serialize
(ar) ;
m_p2.Serialize
(ar) ;
Serialize_AreaVals (ar) ;
}
IMPLEMENT_SERIAL (CRectangle , CObject , 1)
Das war's. Nun ist gesichert, daß alle Daten des Dokuments erfaßt werden. Ihr möglicher
Eindruck, daß dies relativ kompliziert war, trügt. Es ist im Gegenteil ein sehr formaler
Prozeß, den man allerdings jeweils bei der Deklaration und Implementierung einer Klasse
gleich miterledigen sollte, auch deshalb, weil es gerade in der Testphase eines Programms
sehr nützlich ist, wenn man die Daten der Test-Beispiele in Dateien speichern kann. Und man
sollte auch deshalb nicht darauf verzichten, weil alles, was kompliziert und aufwendig bei der
Programmierung wäre, vom Programm-Gerüst als Geschenk beigesteuert wird.
➪
Das Projekt fmom wird aktualisiert (z. B. mit Build FMOM.EXE im Menü Project).
Nach dem Start des Programms (Execute FMOM.EXE) kann man nach Erzeugen
eines Modells (geben Sie das Beispiel aus der Aufgabe 14.1 ein) z. B. im Menü File
das Angebot Save As... wählen. Es erscheint (Abbildung unten rechts) die typische
Windows-Dialog-Box für das Arbeiten mit Dateien mit allem Komfort (Verzeichnis
wechseln, Laufwerk wechseln, ...).
Nach dem Speichern sollte man das Programm beenden und erneut starten. Man kann dann
z. B. mit dem Angebot Open... im Menü File wieder eine der komfortablen Dialog-Boxen
öffnen, die im Listen-Fenster
links die gerade gespeicherte
Datei zeigt. Nachdem man diese
gewählt hat, sollte das Dokument sich wieder in der gleichen Form zeigen, wie man es
in die Binär-Datei gebracht hat.
Übrigens: Die vom Programm
angebotene Extension .fmo für
die Dateien ist bereits beim
Erzeugen des Projekts (Abschnitt 14.4.2) im "ClassesDialog" festgelegt worden.
Der nun erreichte Zustand
des Projekts gehört zum
Tutorial als Version
"fmom11".
J. Dankert: C++-Tutorial
14.4.12
329
Eine zweite Ansicht für das Dokument, Splitter-Windows
Für das Projekt fmom ist eine graphische Darstellung der eingegebenen Flächen sicher eine
besonders aussagekräfte "Ansicht" auf die Daten des Dokuments. In diesem Abschnitt werden
die Vorbereitungen für die Darstellung der Elementdaten in 2 Ansichten ("Views") getroffen,
eine "Ansicht" wird die bereits existierende Ausgabe der Ergebnisse sein, die andere Ansicht
wird die im Abschnitt 14.4.14 zu realisierende graphische Darstellung werden. Das bisher für
die Ausgabe der Ergebnisse verwendete "Dokument-Fenster" wird dafür zu einem sogenannten Splitter-Window umfunktioniert.
Ein Splitter-Window
füllt die Zeichenfläche ("Client Area") eines Rahmenfensters ("Frame Window"), die
durch Teilungs-Balken ("Splitter Bars") in mehrere "Fensterscheiben" ("Panes") unterteilt
wird. Jede "Fensterscheibe" kann die Daten des Dokuments in einer anderen Ansicht
("View") darstellen.
◆
Dynamische Splitter-Windows gestatten dem Programm-Benutzer das "Splitten"
(auch "Unsplit" ist möglich) und das Verschieben der "Split Bars", die Ansichten
in den "Fensterscheiben" sind jedoch alle von der gleichen Klasse (so kann man
z. B. bei großen Text-Dokumenten verschiedene Bereiche des Textes in
verschiedenen "Panes" gleichzeitig sichtbar halten).
◆
Statische Splitter-Windows erhalten ihre Aufteilung durch den Programmierer.
Der Benutzer kann die Aufteilung nicht ändern, also auch keine weitere Teilung
vornehmen, allerdings können die "Splitter Bars" verschoben werden. In den
"Panes" von statischen Splitter-Windows können Ansichten unterschiedlicher
Klassen dargestellt werden.
Für das Projekt fmom bietet sich das Anlegen eines statischen Splitter-Windows mit 2
"Panes" an. Beim automatischen Erzeugen mit "Class Wizard" wird jedoch vorübergehend
auch ein dynamisches Splitter-Window vorhanden sein, so daß derjenige, der den Prozeß der
Entwicklung von Version "fmom11" zu "fmom12" in diesem Abschnitt schrittweise
nachvollzieht, beide Varianten zu sehen bekommt.
Zum besseren Verständnis der auszuführenden Schritte sollen einige Bemerkungen über die
bisher im Projekt fmom erzeugten Fenster und den darin dargestellten Ansichten vorangestellt werden: Es existiert ein Hauptrahmenfenster des Programms, und für jedes erzeugte
Dokument ein eigenes "Child Window", das im Programm durch eine (vom "App Wizard"
kreierte) Instanz der Klasse CMDIChildWnd ("Multi Document Interface Child Window")
repräsentiert wird. Die Methoden dieser Klasse sind für alle Operationen zuständig, die mit
dem "Rahmen des Fensters" zusammenhängen, während die Zeichenfläche ("Client Area")
von einer Instanz einer Ansichtsklasse (in fmom bisher nur die aus CView abgeleitete Klasse
CFmomView) bearbeitet wird.
Hier wird nun noch eine "Zwischendecke" eingezogen: Die Instanz der Klasse CMDIChildWnd wird durch eine Instanz einer aus CMDIChildWnd abzuleitenden Klasse ersetzt
(dieser wird für fmom der Name CFmomFrame gegeben). Diese Klasse wiederum enthält
J. Dankert: C++-Tutorial
330
ein Objekt der Klasse CSplitterWnd, von dem das Splitter-Window verwaltet wird, das die
"Client Area" des durch die CFmomFrame-Instanz repräsentierten Dokument-Rahmenfensters füllt. Die "Panes" des Splitter-Windows sind schließlich die Zeichenflächen ("Client
Areas"), für die jeweils eine Ansicht ("View") definiert wird. Jede Ansicht wird durch eine
Instanz einer Ansichtsklasse repräsentiert (bisher gibt es in fmom nur CFmomView, am
Ende dieses Abschnitts wird noch CDrawView hinzugekommen sein).
Das klingt sicher etwas kompliziert, ist es wohl auch, wird aber durch den "Class Wizard"
unterstützt, so daß nur einige Schritte "von Hand" erledigt werden müssen. Zunächst wird die
neue Klasse CFmomFrame erzeugt:
➪
Aus der "Visual Workbench" kommt man über Browse und ClassWizard... zur
bekannten "MFC Class Wizard"-Dialog-Box, in der Add Class... gewählt wird. Es
erscheint die "Add Class"-Dialog-Box, im Feld Class Name: wird CFmomFrame
eingetragen, in den Feldern Header File: und Implementation File: erscheinen
automatisch Vorschläge für die Namen der zu erzeugenden Dateien, die akzeptiert
werden können. Unter Class Type: ist splitter zu wählen, das wird den "Class
Wizard" veranlassen, eine CSplitterWnd-Instanz in die Klasse aufzunehmen. Wenn
die Dialog-Box das
nebenstehende Aussehen
hat, wird mit Create
Class das Erzeugen der
entsprechenden Dateien
beim "Class Wizard" in
Auftrag gegeben.
Mit OK wird der "Class
Wizard" verlassen.
Man sollte sich die Klassen-Deklaration, die man nun in der Datei fmomfram.h findet, ruhig
einmal ansehen. Man erkennt, daß die Klasse CFmomFrame aus der Basisklasse CMDIChildWnd abgeleitet ist und ein Objekt der CSplitterWnd-Klasse enthält:
class CFmomFrame : public CMDIChildWnd
{
// ...
// Attributes
protected:
CSplitterWnd
m_wndSplitter;
// ...
}
Außerdem ist bemerkenswert, daß die Methode OnCreateClient, die über CMDIChildWnd
von deren Basisklasse CFrameWnd geerbt wurde, überschrieben wird. Die StandardImplementierung dieser Methode erzeugt ein CView-Objekt (Ansichts-Klasse) für die "Client
Area" des Fensters.
Genau diese Methode, die während der Ausführung von OnCreate (Erzeugen eines Fensters)
gerufen wird, wenn die "Client Area" erzeugt wird, ist der Ort, wo das Splitter-Window zu
kreieren ist. Auch das ist vom "Class Wizard" bereits vorbereitet worden, in der Datei
fmomfram.cpp findet man die entsprechende CFmomFrame-Methode. Diese enthält den
Aufruf der CSplitterWnd-Methode Create (für das CSplitterWnd-Objekt in der Klasse
CFmomFrame), womit ein dynamisches Splitter-Window erzeugt wird:
J. Dankert: C++-Tutorial
331
BOOL CFmomFrame::OnCreateClient (LPCREATESTRUCT , CCreateContext* pContext)
{
return m_wndSplitter.Create(this,
2, 2,
// TODO: adjust the number of rows, columns
CSize(10, 10),
// TODO: adjust the minimum pane size
pContext);
}
Die beiden Pointer, die dieser Methode übergeben werden, zeigen auf die CREATESTRUCTStruktur, mit der das Fenster gerade erzeugt wird (hier nicht benötigt), und eine Struktur mit
wichtigen Informationen (z. B. Pointer auf das zugehörige Dokument), die an die Methode
Create "durchgereicht" wird. Create erzeugt mit diesem Aufruf ein dynamisches SplitterWindow für "dieses" Fenster (this-Pointer), mit maximal 2 "Panes" übereinander und 2
"Panes" nebeneinander, die "Panes" sollen eine Minimalgröße von jeweils 10 Pixeln
horizontal bzw. vertikal haben.
Für fmom ist dies nicht so vorgesehen, wird auch noch geändert auf das Erzeugen eines
statischen Splitter-Windows, aber es ist eine ganz gute Idee, sich zunächst einmal anzusehen,
wie diese vom "Class Wizard" erzeugte Variante aussieht. Dazu muß erst noch die neue
Klasse die bisher verwendete Klasse CMDIChildWnd ersetzen, die in der "Multi-DokumentKlasse" CMultiDocTemplate verankert ist und beim Erzeugen einer Instanz dieser Klasse
vom Konstruktor eingetragen wird:
➪
In der "Visual Workbench" wird die Datei fmom.cpp geöffnet, in der man das
Erzeugen der CMultiDocTemplate-Instanz einschließlich Konstruktor-Aufruf findet:
CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
IDR_FMOMTYPE,
RUNTIME_CLASS(CFmomDoc),
RUNTIME_CLASS(CMDIChildWnd),
// standard MDI child frame
RUNTIME_CLASS(CFmomView));
AddDocTemplate(pDocTemplate);
Hierin ist also genau eine Zeile zu ersetzen, um an die Stelle der CMDIChildWndKlasse die neue CFmomFrame-Klasse zu setzen. Natürlich muß die Deklaration
dieser Klasse bekannt sein, deshalb ist die Header-Datei fmomfram.h einzubinden.
Die beiden im folgenden fett gedruckten Zeilen sind also in fmom.cpp zu ergänzen
bzw. zu ändern:
// fmom.cpp : Defines the class behaviors for the application.
// ...
#include "fmomview.h"
#include "fmomfram.h"
// ...
CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
IDR_FMOMTYPE,
RUNTIME_CLASS(CFmomDoc),
RUNTIME_CLASS(CFmomFrame),
RUNTIME_CLASS(CFmomView));
AddDocTemplate(pDocTemplate);
332
J. Dankert: C++-Tutorial
➪
Das Projekt fmom wird aktualisiert (z. B. mit Build FMOM.EXE im Menü Project).
Nach dem Start des Programms (Execute FMOM.EXE) muß man schon ganz genau
hinsehen, um die Neuerung zu erkennen: Beide
Bildlauflisten enthalten
(über dem "Pfeil nach
oben" bzw. neben dem
"Pfeil nach links") jeweils ein kleines Rechteck, das das Vorhandensein eines "Splitter Bars"
signalisiert. Wenn man
diese Rechtecke verschiebt, "splittet" sich
das Fenster in maximal
4 "Panes" (nebenstehende Abbildung).
Das Projekt fmom soll allerdings mit einem statischen Splitter-Window ausgestattet werden.
Dafür ist an die Stelle der CSplitterWnd-Methode Create die Methode CreateStatic zu
setzen. Dies verpflichtet dazu, die "Panes" sofort mit "Views" auszustatten, indem man ihnen
Ansichtsklassen zuordnet. Die nachfolgend beschriebenen Änderungen werden im Anschluß
noch kommentiert:
➪
In der "Visual Workbench" wird die Datei fmomfram.cpp geöffnet. Die Methode
CFmomFrame::OnCreateClient wird folgendermaßen geändert:
BOOL CFmomFrame::OnCreateClient (LPCREATESTRUCT ,
CCreateContext* pContext)
{
if (!m_wndSplitter.CreateStatic (this , 1 , 2)) return FALSE ;
TEXTMETRIC tm ;
CClientDC dc (this) ;
dc.GetTextMetrics (&tm) ;
int cxChar = tm.tmAveCharWidth ;
return (m_wndSplitter.CreateView (0 , 0
CSize
&& m_wndSplitter.CreateView (0 , 1
CSize
, RUNTIME_CLASS (CFmomView) ,
(cxChar * 60 , 0) , pContext)
, RUNTIME_CLASS (CFmomView) ,
(0 , 0) , pContext)) ;
}
Mit CreateStatic wird ein "Child Window" erzeugt, erstes Argument ist der Pointer auf das
zugehörige "Parent Window" (hier: CFmomFrame, repräsentiert durch den this-Pointer). Die
beiden folgenden Argumente geben die Anzahl der "Panes" in vertikaler bzw. horizontaler
Richtung an, hier also "2 Fensterscheiben nebeneinander".
Mit CreateView werden den "Panes" Ansichten ("Views") zugeordnet. Die beiden ersten
Argumente geben die (mit 0 beginnende) Zeilen- bzw. Spaltennummer des "Panes" an, hier
also 0,0 für die linke und 0,1 für die rechte "Fensterscheibe". Das RUNTIME_CLASSMakro liefert einen Pointer auf eine CRuntimeClass-Struktur der Klasse, die in den
nachfolgenden Klammern angegeben ist, hier also wird die Ansichtsklasse eingetragen, die
in dem "Pane" dargestellt werden soll. Weil bisher nur eine Ansichtsklasse CFmomView
333
J. Dankert: C++-Tutorial
existiert, ist diese in beiden CreateView-Aufrufen eingetragen. Dies wird nach Erzeugen
einer weiteren Klasse noch geändert werden.
Das CSize-Objekt, das als viertes Argument übergeben werden muß, enthält die Breite und
die Höhe des "Panes" beim Erzeugen (Größe des "Panes" kann danach sofort vom
Programm-Benutzer durch Verschieben der "Splitter Bars" geändert werden). Nur für die
Breite des linken "Panes" wird ein sinnvoller Wert vorher berechnet. Weil dieses nach wie
vor für die Ergebnisausgabe vorgesehen ist, wird die 60-fache mittlere Zeichenbreite des
"Current Font" eingestellt, weil die in CFmomView::OnDraw programmierte Ausgabe (vgl.
Abschnitt 14.4.8) einschließlich angemessener Ränder auf beiden Seiten damit auskommt. Im
Gegensatz zu OnDraw (bekommt einen Pointer auf einen "Device Context" geliefert) muß
OnCreateClient erst einen "Device Context" für das Fenster (this-Pointer als Argument für
CClientDC) anfordern, bevor die Methode GetTextMetrics aufgerufen werden kann (der
"Device Context" wird automatisch freigegeben, wenn das CClientDC-Objekt beim
Verlassen von OnCreateClient "stirbt").
Für die Höhe der "Panes" wird 0 vorgegeben, weil bei nur einer "Zeile" ohnehin die gesamte
Höhe der "Client Area" genommen wird. Die 0 für die Breite des rechten "Panes" hat einen
ähnlichen Grund: Bei nur zwei "Panes" wird dem zweiten der verbleibende Platz zugewiesen.
Der Pointer auf die Struktur CCreateContext wird an die Methode CreateView einfach nur
"durchgereicht".
Weil in der Methode OnCreateClient die Klasse CFmomView verwendet wird, muß ihre
Deklaration bekannt sein (da diese einen Pointer auf die Klasse CFmomDoc enthält, muß
auch deren Deklaration bekanntgemacht werden). Dafür werden die entsprechenden HeaderDateien in die Datei fmomfram.cpp eingebunden:
➪
In der "Visual Workbench" wird die Datei fmomfram.cpp geöffnet, die includeStatements am Anfang werden um die beiden fett gedruckten Zeilen ergänzt:
// fmomfram.cpp : implementation file
//
#include
#include
#include
#include
#include
➪
Das Projekt fmom wird
aktualisiert (z. B. mit
Build FMOM.EXE im
Menü Project). Nach
dem Programm-Start
(Execute FMOM.EXE)
sieht man sofort die
beiden "Panes", beide
enthalten dieselbe
"View" (nebenstehende
Abbildung).
"stdafx.h"
"fmom.h"
"fmomfram.h"
"fmomdoc.h"
"fmomview.h"
334
J. Dankert: C++-Tutorial
Vorbereitend für das Erstellen einer "sinnvollen" zweiten Ansicht wird eine weitere
Ansichtsklasse erzeugt, die dann dem rechten "Pane" zugeordnet wird:
➪
Über Browse und ClassWizard... kommt man aus der "Visual Workbench" in die
"MFC Class Wizard"-Dialog-Box, dort wird Add Class... gewählt, es erscheint die
"Add Class"-Dialog-Box. Für Class Name: wird z. B. CDrawView eingetragen, als
Class Type wird CView gewählt, die vorgeschlagenen Namen für die Dateien werden
akzeptiert. Nach Wahl von Create Class erzeugt der Class Wizard die Dateien. Nach
Wahl von OK landet man wieder in der "Visual Workbench".
Die entstandenen Dateien drawview.h und drawview.cpp ähneln sehr den Dateien der
Ansichtsklasse CFmomView in ihrer ursprünglichen Form (bevor die Ausgabe der Ergebnisse ergänzt wurde). So findet man auch in drawview.cpp natürlich wieder das Gerüst der
Methode OnDraw, die auch hier der Einstiegspunkt für die weitere Arbeit sein wird.
➪
Die neue Klasse CDrawView wird nun mit dem rechten "Pane" verknüpft: In der
"Visual Workbench" wird die Datei fmomfram.cpp geöffnet. Zu den include-Dateien
wird noch die neue Datei drawview.h hinzugefügt, und in der Methode OnCreateClient wird im CreateView-Aufruf für das rechte "Pane" der Name der neuen
Ansichtsklasse eingetragen:
// fmomfram.cpp : implementation file
// ...
#include "fmomview.h"
#include "drawview.h"
// ...
BOOL CFmomFrame::OnCreateClient (LPCREATESTRUCT ,
CCreateContext* pContext)
{ // ...
return (m_wndSplitter.CreateView (0 , 0 ,
CSize (cxChar
&& m_wndSplitter.CreateView (0 , 1 ,
CSize (0 , 0)
RUNTIME_CLASS (CFmomView) ,
* 60 , 0) , pContext)
RUNTIME_CLASS (CDrawView) ,
, pContext)) ;
}
➪
Das Projekt fmom wird
aktualisiert (z. B. mit
Build FMOM.EXE im
Menü Project). Nach
dem Programm-Start
(Execute FMOM.EXE)
sieht man die beiden
"Panes", wie sie sich
zukünftig beim Programmstart präsentieren
sollen (nebenstehende
Abbildung).
Der nun erreichte
Zustand des Projekts gehört zum Tutorial als Version "fmom12".
J. Dankert: C++-Tutorial
14.4.13
335
GDI-Objekte und Koordinatensysteme
In diesem Abschnitt "ruht" das Projekt fmom, weil vor der Programmierung der graphischen
Darstellung der eingegebenen Flächen noch einige grundsätzliche Betrachtungen nützlich
sind. Eigentlich sind alle bereits realisierten Text-Ausgaben auch unter "Graphik" einzuordnen (in der Windows-Programmierung gilt: Text ist Graphik), auch die wichtigsten
Begriffe wurden zumindest schon erwähnt, noch einmal zur Erinnerung:
◆
Das "Graphics Device Interface" (GDI) stellt die Funktionen für alle Zeichenaktionen zur Verfügung.
◆
Die Ausgabe bezieht sich dabei nicht direkt auf ein physikalisches Gerät, sondern auf
einen "Device Context", der das "Gerät" (z. B.: "Client Area" eines Windows,
Drucker, ...) repräsentiert. Für den Programmierer ist ein Objekt der Basisklasse CDC
("Class of Device Context") oder einer daraus abgeleiteten Klasse der "Vermittler"
zum Ausgabegerät. In diesem Objekt sind alle Eigenschaften und Attribute des
Ausgabegerätes gespeichert bzw. beim Erzeugen des Objekts mit sinnvollen Werten
(Farbeinstellungen, Linientypen, Schriftfont, ...) vorbelegt worden.
Deshalb ist es beim Arbeiten mit Programmen, die vom "App Wizard" erzeugt
wurden, besonders einfach: Einen Pointer auf einen "Device Context" bekommt die
Methode OnDraw geliefert, und dieser ist mit sinnvollen Werten vorbelegt, so daß
man sofort mit einer Zeichenaktion starten kann (durch Aufruf von Methoden der
Klasse CDC). Dies soll an einem einfachen Beispiel demonstriert werden.
Um nicht unnötig viel "Ballast" in dem kleinen Demonstrations-Programm zu haben, das in
diesem Abschnitt entwickelt wird, sollte man entweder ein neues Projekt erzeugen (gute Idee
für den denjenigen, der das im Abschnitt 14.4.2 behandelte Erzeugen eines Projekts
wiederholen will) oder aber mit der zum Tutorial gehörenden Version "fmom1" starten (diese
Variante wird nachfolgend beschrieben).
➪
Die Files der Version "fmom1" werden in ein leeres Unterverzeichnis kopiert. In der
"Visual Workbench" wird im Menü Project das Angebot Open... gewählt, es öffnet
sich die "Open Project"-Dialog-Box, in der man das entsprechende Verzeichnis und
in diesem Verzeichnis die Datei fmom.mak auswählt. Nach dem Anklicken des OKButtons kommt wahrscheinlich der Hinweis, daß sich die Projekt-Dateien aus ihrem
ursprünglichen Verzeichnis herausbewegt haben und daß die Abhängigkeiten
korrigiert werden, was man mit OK bestätigt.
Man hat nun ein beinahe "jungfräuliches" Projekt. Es wird die Datei fmomview.cpp
geöffnet, in der man die vom "App Wizard" angelegte Methode CFmomView::OnDraw findet, die einen Pointer auf einen "Device Context" empfängt
(CDC* pDC). Mit diesem Pointer werden nun zwei (der außerordentlich zahlreichen)
CDC-Methoden gerufen (da in diesem Testprogramm nicht mit Dokument-Daten
operiert wird, wurden die beiden vom "App Wizard" bereits vorgesehenen Programmzeilen gelöscht):
void CFmomView::OnDraw(CDC* pDC)
{
pDC->Ellipse
( 20 , 50 , 120 , 150) ;
pDC->Rectangle (220 , 200 , 420 , 300) ;
}
336
J. Dankert: C++-Tutorial
Es werden eine ("gefüllte") Ellipse und ein ("gefülltes") Rechteck gezeichnet, beide erwarten
die Koordinaten von zwei Punkten (die ersten beiden Argumente sind die Koordinaten des
ersten, die letzten beiden Argumente die Koordinaten des zweiten Punktes). Bei der Ellipse
ist damit das "umschreibende Rechteck" definiert (wenn man die Koordinaten des Beispiels
nachrechnet, bemerkt man, daß ein Kreis zu erwarten ist). In dieser außerordentlich einfachen
Form erhält man die Zeichnung mit den voreingestellten Farben, die Argumente beziehen
sich auf das voreingestellte Koordinatensystem (vom Typ MM_TEXT mit dem Ursprung in
der linken oberen Ecke der "Client Area" und der Einheit "Pixel", wenn die Ausgabe auf dem
Bildschirm landet).
➪
Das ausführbare Programm wird erzeugt (z. B.:
Build FMOM.EXE im Menü Project). Nach dem
Programmstart (Execute FMOM.EXE) sieht man
das nebenstehende Ergebnis der Zeichenaktion: Ein
schwarzer "Zeichenstift" hat die Konturen gezeichnet, die Flächen wurden mit weißer Farbe
gefüllt (das fällt natürlich auf dem ebenfalls weißen
Hintergrund gar nicht auf), und man bekommt eine
Vorstellung von der Auflösung des Bildschirms
(der linke Rand des Kreises ist 20 Pixel vom Rand
der "Client Area" des Fensters entfernt).
Standard-Einstellungen
Der "schwarze Zeichenstift", der die Zeichenarbeit verrichtet hat, ist ein sogenanntes GDI-Objekt, das (natürlich!) durch eine Klasse repräsentiert wird. Der Name dieser Klasse ist CPen, weitere
Klassen anderer GDI-Objekte haben die Namen CBrush (Füllfarbe und -muster), CFont
(Schrifttyp und -größe), CBitmap (Bit-Matrix), CPalette (Farbzuordnungs-Palette) und CRgn
(Bereich, der durch eine Kontur definiert wird).
Wenn man nun ein GDI-Objekt für die Ausgabe benutzen will, das sich von dem StandardObjekt unterscheidet (z. B. einen "roten, 3 Pixel breiten und strichpunktiert zeichnenden" Stift
an Stelle des "schwarzen, eine 1 Pixel breite durchgezogene Linie zeichnenden" Standardstiftes), dann muß man ein entsprechendes GDI-Objekt erzeugen (Instanz der Klasse CPen)
und dieses Objekt in den "Device Context" einsetzen (dabei wird das vorherige Objekt aus
dem "Device Context" entfernt).
Diese immer wieder vorkommene Aktion ist ein spezielles Beispiel wert, vorab eine
Auflistung der Schritte, die erforderlich sind:
◆
Das GDI-Objekt wird erzeugt. Das kann in einem Schritt erfolgen, indem dem
Konstruktor der Klasse alle Attribute übergeben werden, z. B. bei CPen, weil der
Zeichenstift mit nur 3 Attributen (Linientyp, Breite, Farbe) erzeugt wird. Bei andern
GDI-Objekten, die komplizierter in der Beschreibung sind (z. B. CFont), muß
mindestens noch eine Methode der Klasse zur kompletten Festlegung der Eigenschaften aufgerufen werden.
◆
Das neue GDI-Objekt wird in den "Device Context" eingesetzt. Danach können
die Zeichenaktionen ausgeführt werden, für die dieses GDI-Objekt verwendet werden
soll.
337
J. Dankert: C++-Tutorial
◆
Das GDI-Objekt muß wieder gelöscht werden. Vorher muß es vom "Device
Context" getrennt werden, und das ist nur möglich, wenn ein anderes GDI-Objekt
dafür eingesetzt wird. Deshalb sollte man den Pointer auf das GDI-Objekt, das von
dem eigenen verdrängt wurde, speichern (Pointer auf das "verdrängte GDI-Objekt"
wird von der Methode, die ein GDI-Objekt einfügt, als Return-Wert abgeliefert) und
nach allen Zeichenaktionen den "alten Zustand wieder herstellen" und das eigene
Objekt löschen (nachfolgendes Beispiel demonstriert das mit einem GDI-Objekt der
Klasse CBrush).
Die Klasse CBrush besitzt mehrere Konstruktoren und einige Methoden, mit denen man die
Farbe und das Muster einer "Füllung" festlegen kann. Im nachfolgend verwendeten einfachsten Fall wird der Konstruktor verwendet, dem nur ein Argument (die Farbe) übergeben wird.
Damit wird eine einfarbige Füllung definiert ("Solid Brush"). Farben werden in Windows
durch den speziellen Datentyp COLORREF definiert (Doppelwort), wobei in je einem Byte
(Werte also von 0...255) die Mengen der Anteile "Rot", "Grün" und "Blau" gespeichert
werden. Für das Erzeugen eines Wertes vom Typ COLORREF steht das "RGB-Makro" zur
Verfügung, z. B. erzeugt man mit RGB(0,0,255) ein reines "Blau", mit RGB(255,255,255)
mischen sich die drei Grundfarben zu "Weiß".
➪
In der "Visual Workbench" wird die Datei fmomview.cpp geöffnet. Die Methode
CFmomView::OnDraw wird folgendermaßen erweitert:
void CFmomView::OnDraw(CDC* pDC)
{
CBrush brush_red (RGB (255 , 0 , 0)) ;
// ... erzeugt
// CBrush-Objekt fuer roten "Solid Brush"
CBrush* brush_old = pDC->SelectObject (&brush_red) ;
// ... setzt CBrush-Objekt in "Device Context"
//
ein, Pointer auf "altes" CBrush-Objekt
//
(Return-Wert) wird aufbewahrt
pDC->Ellipse ( 20 , 50 , 120 , 150) ;
// ... zeichnet Ellipse mit roter Fuellung
CBrush brush_green (RGB (0 , 255 , 0)) ;
pDC->SelectObject (&brush_green) ;
pDC->Rectangle (220 , 200 , 420 , 300) ;
// ... zeichnet Rechteck mit gruener Fuellung
pDC->SelectObject (brush_old) ;
// ... setzt "altes" CBrush// Objekt wieder in den "Device Context ein
}
➪
// ... und hier werden die erzeugten GDI-Objekte (brush_red und
//
brush_green) automatisch ge//
loescht, weil sie lokal in der
//
Funktion vereinbart wurden
Das ausführbare Programm wird erzeugt (z. B.:
Build FMOM.EXE im Menü Project). Nach dem
Programmstart (Execute FMOM.EXE) sieht man
das nebenstehende Ergebnis der Zeichenaktion: Die
beiden Flächen sind (infolge der unterschiedlichen
CBrush-Objekte) rot bzw. grün gefüllt, die (vom
CPen-Objekt gezeichneten) Randkonturen sind
weiterhin schwarz (Standard-"Zeichenstift" wurde
beibehalten).
"Gefüllte" Flächen
338
J. Dankert: C++-Tutorial
Für die Interpretation der an die CDC-Methoden übergebenen Koordinaten wurde bisher das
Standard-Koordinatensystem MM_TEXT verwendet mit dem Koordinatenursprung in der
linken oberen Ecke der Zeichenfläche und nach rechts bzw. unten gerichteten positiven
Koordianten, Koordinaten-Einheiten sind bei der Darstellung auf dem Bildschirm "Pixel".
Tatsächlich werden "Geräte-Einheiten" verwendet, die auf anderen Ausgabegeräten andere
Abmessungen der Darstellung ergeben. Man kann das ausprobieren:
➪
Das vom "App Wizard" erzeugte Programm hat eine (gratis gelieferte) DruckerSchnittstelle. Im Menü File des gerade erzeugten Programms wird das Angebot Print
Preview... gewählt. Man erkennt, daß die Zeichnung infolge der wesentlich höheren
Auflösung (und damit viel kleineren Geräte-Einheit) auf dem Papier erheblich kleiner
sein wird.
Das GDI stellt insgesamt 8 Koordinatensysteme zur Verfügung (ein für die Bedürfnisse von
Ingenieuren und Naturwissenschaftlern brauchbares ist leider nicht dabei). Alle Koordinatensysteme haben zunächst ihren Ursprung in der linken oberen Ecke der Zeichenfläche, dieser
kann jedoch an einen beliebigen anderen Punkt verschoben werden. Die Koordinatensysteme
können in 3 Gruppen eingeteilt werden:
◆
Das bereits bekannte MM_TEXT nimmt eine Sonderstellung ein: Die logischen
Koordinaten, die der Programmierer in seinen Aufrufen der CDC-Methoden angibt,
sind mit den physikalischen Koordinaten (Geräte-Einheiten, z. B. Bildschirm-Pixel)
identisch. Die positiven Richtungen der Koordinaten (nach rechts bzw. nach unten)
lassen sich nicht ändern.
◆
Die nachfolgend aufgelisteten Koordinatensysteme der 2. Gruppe interpretieren die
Werte der logischen Koordinaten als jeweils feste Längenangabe, Windows rechnet
diese (anhand der bekannten Gerätedaten) in die entsprechenden Geräte-Einheiten um
(eine Strecke von 60 mm hat dann auf einem Bildschirm beliebiger Auflösung und
auch auf Druckern jeweils genau diese Länge). Folgende Koordinatensysteme mit den
angegeben logischen Einheiten sind verfügbar:
MM_LOENGLISH mit 0.01 Inch,
MM_LOMETRIC mit 0.1 mm,
MM_TWIPS mit 1/1440 Inch
MM_HIENGLISH mit 0.001 Inch,
MM_HIMETRIC mit 0.01 mm,
(das merkwürdige Wort "TWIPS" steht für "Twentieth of a Point", und ein "Point" ist
mit 1/72 Inch das im Druckgewerbe übliche Maß für die Angabe von Schriftgrößen).
Für diese 5 Koordinatensystem zeigen die positiven Koordinatenachsen nach
rechts bzw. nach oben. Vorsicht, Falle: Wenn man im Programm nur die
Standard-Einstellung von MM_TEXT auf eines dieser Systeme ändert, sieht man
von seiner Zeichnung wahrscheinlich nichts mehr, weil das Koordinatensystem
nach wie vor in der linken oberen Ecke liegt. Man muß also entweder ausschließlich negative y-Koordinaten verwenden (keine gute Idee) oder den KoordinatenUrsprung verschieben. Dies wird mit einem kleinen Beispiel demonstriert.
Das gewünschte Koordinatensystem wird mit der CDC-Methode SetMapMode eingestellt,
der nur ein Argument übergeben wird (die oben verwendeten Namen für die Koordinatensysteme entsprechen den in windows.h definierten Konstanten, man sollte sie als Argumente
verwenden).
339
J. Dankert: C++-Tutorial
Für die Verschiebung des Koordinaten-Ursprungs sind die CDC-Methoden SetViewportOrg
(arbeitet mit Geräte-Koordinaten, wird nachfolgend verwendet) und SetWindowOrg (arbeitet
mit logischen Koordinaten, wird später demonstriert) verfügbar.
Im nachfolgenden Beispiel-Programm wird zunächst mit GetClientRect (liefert die
Abmessungen der Zeichenfläche in Geräte-Koordinaten) die Größe der Zeichfläche ermittelt,
um dann mit SetViewportOrg den Ursprung des Koordinatensystems in die linke untere
Ecke zu legen (die beiden Argumente sind in Geräte-Koordinaten anzugeben, gemessen von
der linken oberen Ecke der Zeichenfläche, Height und das hier nicht verwendete Width sind
übrigens Methoden der Klasse CRect):
➪
In der "Visual Workbench" wird die Datei fmomview.cpp geöffnet. Die Methode
CFmomView::OnDraw wird folgendermaßen erweitert:
void CFmomView::OnDraw(CDC* pDC)
{
pDC->SetMapMode (MM_LOMETRIC) ;
CRect
rect ;
GetClientRect
(&rect) ;
pDC->SetViewportOrg (0 , rect.Height ()) ;
CBrush
brush_red (RGB (255 , 0 , 0)) ;
// ...
// ...
}
➪
Das ausführbare Programm wird erzeugt (z. B.:
Build FMOM.EXE im Menü Project). Nach dem
Programmstart (Execute FMOM.EXE) sieht man
(nebenstehende Abbildung), daß das Bild im
Vergleich mit der MM_TEXT-Text-Darstellung
"auf dem Kopf steht", weil die y-Koordinate bei
MM_LOMETRIC nach oben zeigt. Außerdem ist
das Bild viel kleiner, obwohl MM_LOMETRIC
mit einer Einheit von 0.1 mm das "gröbste" der
oben vorgestellten Koordinatensysteme ist.
Die Abmessungen sind allerdings tatsächlich (bei Darstellung mit MM_LOMETRIC
beliebigem Bildschirm) in der erwarteten Größe,
das mit den "logischen Abmessungen" 200x100 definierte Rechteck ist 20 mm breit
und 10 mm hoch. Wenn man sich allerdings über File und Print Preview... die
Druckvorschau ansieht, ist die Zeichnung wahrscheinlich nicht komplett zu sehen (es
sei denn, Sie haben einen sehr grob auflösenden Drucker eingestellt). Der Grund ist
klar: Die Verschiebung des Koordinaten-Ursprungs wurde in den für die BildschirmDarstellung sehr bequemen Geräte-Einheiten vorgenommen, die für den Drucker sehr
viel kleiner sein können.
Um für alle Ausgabe-Geräte die gleiche Darstellung zu bekommen, muß man auch die
Verschiebung des Koordinaten-Ursprungs in logischen Koordinaten (mit SetWindowOrg)
vornehmen. Die beiden Argumente, die SetWindowOrg übergeben werden müssen, sind
allerdings ganz anders zu interpretieren als bei SetViewportOrg: Es werden die Koordinaten
des Punktes angegeben, den der durch SetViewportOrg gesetzte (bzw. ohne SetViewportAufruf in der linken oberen Ecke liegende) Punkt nach dem SetWindowOrg-Aufruf haben
340
J. Dankert: C++-Tutorial
soll. Wenn also kein SetViewportOrg-Aufruf erfolgt ist, übergibt man SetWindowOrg die
Koordinaten, die die linke obere Ecke haben soll, und SetWindowOrg legt das Koordinatensystem so, daß genau das erfüllt wird. Probieren Sie es aus:
➪
In der "Visual Workbench" wird die Datei fmomview.cpp geöffnet. In der Methode
CFmomView::OnDraw werden die drei "auskommentierten" Zeilen durch einen
SetWindowOrg-Aufruf ersetzt:
void CFmomView::OnDraw(CDC* pDC)
{
pDC->SetMapMode (MM_LOMETRIC) ;
/*
CRect
rect ;
GetClientRect
(&rect) ;
pDC->SetViewportOrg (0 , rect.Height ()) ; */
pDC->SetWindowOrg
(-200 , 500) ; // ... und die linke obere
// Ecke der Zeichenfläche hat die Koordinaten (-200,500)
CBrush
brush_red (RGB (255 , 0 , 0)) ;
// ...
// ...
}
Nach Aktualisierung des Projekts und Starten des
Programms sieht man, daß sich die Zeichnung nach
rechts verschoben hat, weil der Ursprung des
Koordinatensystems nun nicht mehr auf dem linken
Rand der Zeichenfläche liegt. In der DruckerVorschau (nebenstehende Abbildung) zeigt sich die
gleiche Darstellung wie auf dem Bildschirm.
Der erreichte Stand dieses Beispiel-Programms
gehört zum Tutorial als "gri1".
◆
Die 3. Gruppe bilden die "skalierbaren" KoordinaDrucker-Vorschau
tensysteme MM_ISOTROPIC bzw. MM_ANISOTROPIC, bei denen der Programmierer festlegt, in
welchem Verhältnis die logischen zu den physikalischen Koordinaten stehen. Dafür
sind zwei zusätzliche Methoden verfügbar: Mit SetWindowExt werden die Abmessungen eines Rechtecks in logischen Koordinaten festgelegt, mit SetViewportExt
die entsprechenden Abmessungen in Geräte-Koordinaten. Damit sind für beide
Richtungen Skalierungsfaktoren gegeben, mit denen die in den Aufrufen der CDCMethoden verwendeten logischen Koordinaten in Geräte-Koordinaten umgerechnet
werden sollen.
Mit dem Koordinatensystem MM_ANISOTROPIC wird für beide Koordinaten so
verfahren, für das Koordinatensystem MM_ISOTROPIC wird jedoch nur einer der
beiden Skalierungsfaktoren für die Umrechnung in beiden Richtungen verwendet, so
daß in beiden Richtungen gleichartig skaliert wird (es wird automatisch der Faktor
gewählt, bei dem der mit SetWindowExt definierte Bereich garantiert in dem mit
SetViewportExt definierten Bereich darstellbar ist, so daß dieser gegebenenfalls in
einer Richtung nicht voll genutzt wird).
Da über die Vorzeichen der Argumente von SetViewportExt auch noch die Richtungen der
Koordinatenachsen festgelegt werden können, klingt das beinahe so, als müßten damit auch
341
J. Dankert: C++-Tutorial
die Ingenieure und Naturwissenschaftler mit ihren immer etwas anspruchsvolleren Problemen
gut bedient werden können: Ein Weg-Zeit-Diagramm z. B. mit unterschiedlichen Dimensionen auf beiden Achsen wird mit MM_ANISOTROPIC dargestellt, für technische Zeichnungen mit Längenabmessungen in beiden Richtungen bietet sich MM_ISOTROPIC an.
Spätestens hier wird ein besonders gravierender Mangel deutlich: Alle Koordinatensysteme
arbeiten ausschließlich mit int-Werten. Für die genannten Beispiele muß also immer noch
eine Transformation der in der Regel in double-Werten vorliegenden Problemparameter auf
einen Integer-Bereich vom Programmierer vorgeschaltet werden, wobei sich bei MS-VisualC++ in der Version 1.5 für Windows 3.1 die Begrenzung der in nur zwei Byte gespeicherten
int-Werte auf den Bereich - 32768 ... + 32767 als ausgesprochen lästig erweisen kann.
Nachfolgend werden vorbereitend für die Weiterarbeit am Projekt fmom schrittweise die
Probleme der graphischen Darstellung von Flächen diskutiert und Lösungsmöglichkeiten
vorgestellt. Das Ziel soll sein, alle Flächen in der "Client Area" in optimaler Größe (unverzerrt, aber die verfügbare Zeichenfläche möglichst gut ausfüllend) darzustellen. Das kann
mit einer Modifikation des bisher behandelten Demonstrations-Programms (für die Darstellung eines Kreises und eines Rechtecks) sehr gut verdeutlicht werden. Zunächst wird das
Problem der double-Werte noch ausgeklammert, im nachfolgenden Programm werden die
gleichen durch int-Werte definierten Flächen wie bisher dargestellt, allerdings ist die
Zeichenfläche möglichst gut gefüllt.
➪
In der "Visual Workbench" wird die Datei fmomview.cpp geöffnet. In der Methode
CFmomView::OnDraw werden die Zeilen vor dem Aufruf der ersten Zeichenroutine
wie folgt geändert:
void CFmomView::OnDraw(CDC* pDC)
{
pDC->SetMapMode (MM_ISOTROPIC) ;
CRect rect ;
GetClientRect
pDC->SetWindowExt
pDC->SetViewportExt
pDC->SetWindowOrg
CBrush
(&rect) ;
(410 , 260) ;
(rect.Width () , - rect.Height ()) ;
(15 , 305) ;
brush_red (RGB (255 , 0 , 0)) ;
// ...
pDC->Ellipse ( 20 , 50 , 120 , 150) ;
// ...
pDC->Rectangle (220 , 200 , 420 , 300) ;
// ...
}
Natürlich bietet sich MM_ISOTROPIC an, weil die Flächen nicht unterschiedlich skaliert
werden sollen. Mit pDC->SetWindowExt (410,260) wird etwas mehr als der maximale
Bereich, der von den logischen Koordinaten in den Aufrufen der Methoden Ellipse bzw.
Rectangle erfaßt wird (ein Rechteck mit den Abmessungen 400x250), etwas überschritten,
so daß in jedem Fall ein kleiner Rand bleibt.
Der Aufruf von SetViewportExt enthält die Abmessungen der gesamten Zeichenfläche, so
daß der mit SetWindowExt festgelegte logische Bereich auf diese abgebildet wird. Das
Minuszeichen vor dem zweiten Argument sorgt dafür, daß die y-Achse nach oben zeigt.
Mit SetWindowOrg (15,305) wird der linken oberen Ecke der Zeichenfläche der (logische)
Punkt (15,305) zugeordnet, der Ursprung des Koordinatensystems liegt damit links außerhalb
der Zeichenfläche. Auch hier sorgt die 15 für einen kleinen Rand (kleinster logischer x-Wert
342
J. Dankert: C++-Tutorial
ist 20), entsprechend sorgt die 305 bei einer maximalen y-Koordinate von 300 für einen
kleinen oberen Rand.
➪
Das ausführbare Programm
wird erzeugt (z. B.: Build
FMOM.EXE im Menü Project) und gestartet (Execute
FMOM.EXE). Nebenstehend
ist in zwei Fenstern zu sehen,
wie die "optimale" Anpassung
an die Zeichenfläche erfolgt:
Eine Richtung ist immer voll
ausgenutzt (bis auf die "planmäßigen" Ränder), in der
anderen Richtung bleibt ein
Bereich der Zeichenfläche
ungenutzt.
Wenn in OnDraw das Koordinatensystem auf MM_ANISOTROPIC geändert wird, ist
die Anpassung an die Zeichenfläche in beiden Richtungen optimal, allerdings wird
dann aus dem Kreis eine
Ellipse (nebenstehende Abbildung). Probieren Sie es aus!
MM_ISOTROPIC
MM_ANISOTROPIC
Weil für fmom natürlich nur MM_ISOTROPIC sinnvoll ist, wird die Darstellung zunächst
noch etwas "verschönert": In der nicht voll ausgenutzten Richtung soll das Bild in der Mitte
der Zeichenfläche liegen. Das wird erreicht, indem zunächst der Koordinaten-Ursprung der
Zeichenfläche (mit SetViewportOrg) in die Mitte verlegt wird, um dann mit SetWindowOrg
die Mittelwerte der logischen "Extrem-Koordinaten" mit diesem Punkt zu verbinden:
➪
In CFmomView::OnDraw wird die Zeile, in der mit SetWindowOrg der Koordinaten-Ursprung festgelegt wird, durch die beiden folgenden Zeilen ersetzt:
pDC->SetViewportOrg (rect.Width () / 2 , rect.Height () / 2) ;
pDC->SetWindowOrg
(220 , 175) ;
Nach Aktualisierung des
Projekts und Starten des
Programms sieht man den
Erfolg (nebenstehende Abbildung): Die Zeichnung
befindet sich bei beliebigen
Seitenverhältnissen des Fenster immer in der Mitte.
Das Programm in dieser Version
gehört zum Tutorial als "gri2".
343
J. Dankert: C++-Tutorial
Nun ist noch das leidige Problem mit der Abbildung beliebiger als double-Werte gegebener
Koordinaten auf die int-Werte, die den CDC-Methoden übergeben werden müssen, zu lösen.
Dabei sollen gleich "Nägel mit Köpfen" gemacht werden, indem eine eigene Klasse
eingerichtet wird, die dann auch für die Weiterführung des fmom-Projektes verwendet
werden kann. Das Arbeiten mit dieser Klasse soll folgende Möglichkeiten bieten:
◆
Beim Erzeugen eines Objekts erhält der Konstruktor die Extremwerte der logischen
Koordinaten (als double-Werte), die Abmessungen des Rechtecks in Geräte-Koordinaten, auf den der "logische Bereich" abgebildet werden soll (int-Werte), und den
Pointer auf den "Device Context".
Im Konstruktor wird der "logische Bereich" auf den größtmöglichen int-Bereich
transformiert (unter Verwendung von INT_MAX aus limits.h), danach werden (wie
bereits demonstriert) die CDC-Methoden SetMapMode, SetWindowExt, SetViewportExt, SetViewportOrg und SetWindowOrg gerufen. Alle für die Transformation der Koordinaten erforderlichen Parameter werden als Klassen-Variablen
(private) abgelegt, so daß ...
◆
... die Klasse Methoden erhalten kann, die so einfach wie die CDC-Methoden
aufgerufen werden können, aber mit double-Argumenten. Dies wird zunächst nur
realisiert für die Methoden zum Zeichnen eines Rechtecks bzw. einer Ellipse (eine
Erweiterung auf andere Methoden ist trivial).
Die Deklaration der Klasse CGrInt findet sich in der Datei grint.h, die im Tutorial zur
Version "gri3" dieses Beispiel-Programms gehört:
// Deklaration der Klasse CGrInt, die das Arbeiten mit double-Koordinaten
// im Modus MM_ISOTROPIC unterstuetzt:
#include <afxwin.h>
#include <math.h>
class CGrInt
{
private:
int
double
double
double
double
double
double
CDC*
int
int
m_inorm ;
m_xfac ;
m_yfac ;
m_xmin ;
m_ymin ;
m_xmax ;
m_ymax ;
m_pDC
;
Conv_x (double x) ;
Conv_y (double y) ;
public:
CGrInt (CDC* pDC , double xmin , double ymin ,
double xmax , double ymax ,
int
width , int height) ;
~CGrInt () ;
void
Ellipse
(double x1 , double y1 , double x2 , double y2) ;
void
Rectangle (double x1 , double y1 , double x2 , double y2) ;
} ;
Die Implementation der Methoden ist relativ ausführlich kommentiert (eine Begründung für
die Art der Transformation wird im Anschluß an das Listing gegeben), sie findet sich in der
Datei grint.cpp:
344
J. Dankert: C++-Tutorial
// Implementation der Klasse CGrInt:
#include "stdafx.h"
#include "grint.h"
#include <limits.h>
// ... fuer INT_MAX
// Der Konstruktor bereitet den Bereich für die Ausgabe so vor, dass
// mittig und unverzerrt bei voller Ausnutzung einer Abmessung
// gezeichnet wird:
CGrInt::CGrInt (CDC*
pDC
,
double xmin ,
double ymin ,
double xmax ,
double ymax ,
int
width ,
int
height)
{
int
wi , hi ;
double ww , hw ;
//
//
//
//
//
//
//
Pointer auf "Device Context"
** Extremwerte des Rechteck** Bereichs, in den mit den
** Methoden der Klasse
** gezeichnet werden soll
++ Breite und Hoehe des Bereichs
++ in Geraete-Koordinaten
m_inorm = INT_MAX ;
pDC->SetMapMode (MM_ISOTROPIC) ;
ww = xmax - xmin ;
hw = ymax - ymin ;
// Abmessungen des Bereichs in
// double-Werten
m_xmin
m_ymin
m_xmax
m_ymax
=
=
=
=
//
//
//
//
if (ww
{
wi
hi
}
else
{
hi
wi
}
> hw)
xmin
ymin
xmax
ymax
;
;
;
;
Die Grenzen werden gespeichert, um
die Ruecktransformation und die
Kontrolle der Einhaltung der
Grenzen zu ermoeglichen
// Die groessere Abmessung wird auf
// INT_MAX transformiert, die kleinere
// wird proportional angepasst:
= m_inorm ;
= int ((hw / ww) * m_inorm) ;
= m_inorm ;
= int ((ww / hw) * m_inorm) ;
m_xfac = double (wi) / ww ;
m_yfac = double (hi) / hw ;
m_pDC
= pDC
// Faktoren fuer die Umrechnung
// der Koordinaten double --> int
;
// SetWindowExt wird mit den transformierten
// (int-)Abmessungen aufgerufen:
pDC->SetWindowExt
(wi
,
hi)
;
pDC->SetViewportExt (width , - height) ;
// Das Viewport-Koordinatensystem wird in die Mitte der
// Zeichenflaeche gelegt, ...
pDC->SetViewportOrg (width / 2 , height / 2) ;
// ... das Window-Koordinatensystem in die linke untere Ecke:
pDC->SetWindowOrg
(int (m_xfac * (m_xmax - m_xmin) / 2) ,
int (m_yfac * (m_ymax - m_ymin) / 2)) ;
}
// Destruktor:
CGrInt::~CGrInt () {}
// Gezeichnet wird nur, wenn die Koordinaten auch garantiert
// innerhalb des definierten Bereichs liegen:
345
J. Dankert: C++-Tutorial
void CGrInt::Ellipse (double x1 , double y1 , double x2 , double y2)
{
if (x1 >= m_xmin && y1 >= m_ymin &&
x2 <= m_xmax && y2 <= m_ymax)
m_pDC->Ellipse (Conv_x (x1) , Conv_y (y1) ,
Conv_x (x2) , Conv_y (y2)) ;
}
void CGrInt::Rectangle (double x1 , double y1 , double x2 , double y2)
{
if (x1 >= m_xmin && y1 >= m_ymin &&
x2 <= m_xmax && y2 <= m_ymax)
m_pDC->Rectangle (Conv_x (x1) , Conv_y (y1) ,
Conv_x (x2) , Conv_y (y2)) ;
}
// Transformation der double-Werte auf int-Werte durch Verschieben
// (weil Window-Koordinatensystem in die linke untere Ecke gelegt
// wurde) und Skalieren:
int CGrInt::Conv_x (double x) { return (int ((x - m_xmin) * m_xfac)) ; }
int CGrInt::Conv_y (double y) { return (int ((y - m_ymin) * m_yfac)) ; }
◆
Wie in der Version "gri2" dieses Beispiel-Programms wird mit SetViewportOrg der
Ursprung des Viewport-Koordinatensystems in den Mittelpunkt der "Client Area"
gelegt. Bevor dieser Punkt mit den Mittelwerten der logischen Koordinate verknüpft
wird, muß beachtet werden, daß der damit festgelegte Ursprung der logischen
Koordinaten durchaus außerhalb der Zeichenfläche liegen kann, was zu transformierten Koordinaten größer als INT_MAX führen könnte. Deshalb wird der Ursprung
dieses Koordinatensystems durch Translation um xmin bzw. ymin immer in die linke
untere Ecke des Zeichenbereichs gelegt. Dies muß natürlich bei der Interpretation der
Koordinaten in den Zeichen-Methoden rückgängig gemacht werden.
Die Klasse CGrInt wird getestet mit einer Variante der Methode OnDraw (Datei fmomview.cpp), die ein Objekt dieser Klasse erzeugt, dabei dem Konstruktor die Informationen
über den Zeichenbereich und die extremen double-Werte übergibt, um danach die Methoden
der Klasse CGrInt wie die CDC-Methoden, allerdings mit double-Argumenten, aufzurufen:
➪
In der Datei fmomview.cpp wird die Header-Datei der Klasse CGrInt inkludiert:
#include
"grint.h"
wird im Kopf hinzugefügt. Die Methode OnDraw wird z. B. wie folgt geändert:
void CFmomView::OnDraw(CDC* pDC)
{
CRect rect ;
GetClientRect (&rect) ;
CGrInt grint (pDC , 10000. , 40000. , 430000. , 310000. ,
rect.Width () , rect.Height ()) ;
CBrush brush_red (RGB (255 , 0 , 0)) ;
CBrush* brush_old = pDC->SelectObject (&brush_red) ;
grint.Ellipse (20000. , 50000. , 120000. , 150000.) ;
CBrush brush_green (RGB (0 , 255 , 0)) ;
pDC->SelectObject (&brush_green) ;
grint.Rectangle (220000. , 200000. , 420000. , 300000.) ;
pDC->SelectObject (brush_old) ;
}
346
J. Dankert: C++-Tutorial
In der Methode OnDraw wurden ausgesprochen
große (mit 2-Byte-int-Werten nicht darstellbare)
Koordinaten verwendet. Daß das Arbeiten mit der
Klasse CGrInt (natürlich auch bei Verwendung
von Koordinaten kleiner als 1) funktioniert, zeigt
die nebenstehende Abbildung.
Im Hinblick auf die weitere Verwendung der
Klasse CGrInt lohnt sich noch eine kleine
Verbesserung: Es ist für den Programmierer
bequemer, wenn er sich nicht um den Randabstand der Zeichnung selbst kümmern muß, son- Die Methoden der Klasse CGrInt arbeiten mit
dern mit einer Prozentangabe festlegt, wie breit double-Koordinaten und zeichnen bei optimaler
der Abstand vom Rand der Zeichenfläche (in der Fensterausnutzung unverzerrt und mittig
ungünstigeren Richtung) sein soll. Er kann dann
für die Minimal- und Maximalwerte der double-Koordinaten dem Konstruktor die tatsächlichen Extremwerte übergeben, und dieser soll sich um den Randabstand selbst kümmern.
Die Änderung soll so ausgeführt werden, daß Programme, die die Klasse CGrInt in der
bisher realisierten Form benutzt haben, davon nicht betroffen sind. Das kann man erreichen
durch Überladen eines zweiten Konstruktors, der ein Argument mehr erwartet, oder aber
durch Erweiterung des vorhandenen Konstruktors um einen zusätzlichen Parameter, dem in
der Klassen-Deklaration ein Default-Wert so zugeordnet wird, daß die bisher verwendeten
Aufrufe unverändert bedient werden. Dieser zweite Weg wird nachfolgend beschrieben:
➪
Die Deklaration des Konstruktors wird in der Datei grint.h wie folgt geändert:
CGrInt (CDC* pDC , double xmin , double ymin ,
double xmax , double ymax ,
int
width , int height , double p = 0.) ;
Die Implementierung des Konstruktors in der Datei grint.cpp wird erweitert:
CGrInt::CGrInt (CDC*
pDC
,
int
height,
double p
)
// Pointer ...
// ++ in Geraete-Koordinaten
// Prozentanteil fuer Rand
{
int
wi , hi ;
double ww , hw , marg ;
// ...
ww = xmax - xmin ;
// Abmessungen des Bereichs in
hw = ymax - ymin ;
// double-Werten.
if
(ww / width < hw / height)
else
ww += marg * 2 ;
hw += marg * 2 ;
m_xmin
m_ymin
m_xmax
m_ymax
=
=
=
=
xmin
ymin
xmax
ymax
+
+
marg
marg
marg
marg
marg = hw * p / 100 ;
marg = ww * p / 100 ;
// Alle Abmessungen werden mit dem
// ermittelten Rand korrigiert
;
;
;
;
//
//
//
//
Die Grenzen werden gespeichert, um
die Ruecktransformation und die
Kontrolle der Einhaltung der
Grenzen zu ermoeglichen
Nun können dem Konstruktor die tatsächlichen Extremwerte und ein weiteres Argument
(sinnvoll ist z. B. p = 5.) übergeben werden, der "alte" Aufruf funktioniert aber auch noch.
Die Klasse CGrInt in dieser Form gehört zur "gri3"-Version des Tutorials.
J. Dankert: C++-Tutorial
14.4.14
347
Graphische Darstellung der Flächen
Das Projekt fmom wird nun in dem Zustand, der am Ende des Abschnitts 14.4.12 als Version
"fmom12" erreicht war, wieder aufgegriffen. In diesem Abschnitt soll die graphische
Darstellung der Flächen realisiert werden. Bereits vorbereitend dafür enthält "fmom12"
◆
ein "Splitter-Window", von dem bisher nur ein "Pane" benutzt wird (für die andere
"Fensterscheibe" existiert aber schon eine Ansichtsklasse CDrawView, in der sich
allerdings nur eine "untätige" Methode OnDraw befindet), ...
◆
und den Aufruf von UpdateAllViews, der bei jeder Änderung der Dokument-Daten
auch zum Aufruf von CDrawView::OnDraw führt.
Im einzelnen wird in diesem Abschnitt folgendes zum Projekt fmom hinzugefügt werden:
◆
Da alle Flächen mit einer Füllfarbe und einer Randfarbe zu zeichnen sind, werden
zwei Variablen dafür in der Klasse CArea ergänzt, die dann an alle abgeleiteten
Klassen vererbt werden. Vorerst werden die Farben mit festen Werten vom Konstruktor vorbelegt und ihre Änderung an die Frage "Teilfläche oder Ausschnitt"
gekoppelt (und in der Methode CArea::set_aoh entsprechend gesetzt).
◆
Für das Zeichnen wird die Hilfe der im vorigen Abschnitt entwickelten Klasse
CGrInt genutzt. Sie wird um eine Methode zur Abfrage des "Device Context"Pointers erweitert, um auch CDC-Aufrufe ausführen zu können (z. B. zum Setzen der
Farben).
◆
In CDrawView::OnDraw wird eine Instanz der im vorigen Abschnitt entwickelten
Klasse CGrInt erzeugt. Dem Konstruktor dieser Klasse müssen die minimalen und
maximalen Koordinaten aller Flächen übergeben werden, die mit einer Methode
GetMinMaxCoord ermittelt werden.
◆
Die Methode GetMinMaxCoord ist sinnvoll in der Dokumentklasse CFmomDoc
anzusiedeln. Sie arbeitet in einer Schleife die verkettete Liste der Flächen ab, wobei
für jede Fläche eine Methode get_area_min_max aufgerufen wird, die für eine
Fläche die extremen Koordinaten abliefert.
◆
Die Methode get_area_min_max wird als virtuelle Methode in der abstrakten Klasse
CArea deklariert, aber nicht definiert, Definitionen dieser Methode erhalten nur die
aus CArea abgeleiteten Klassen (bisher: CCircle und CRectangle).
◆
Nach dem Aufruf von GetMinMaxCoord und dem Erzeugen der CGrInt-Instanz
wird in CDrawView::OnDraw die Methode DrawAllAreas aufgerufen, die nach der
gleichen Strategie wie GetMinMaxCoord realisiert wird (ansiedeln in CFmomDoc,
verkettete Liste abarbeiten, Aufruf einer Methode draw_area, die virtuell in CArea
deklariert und in den aus CArea abgeleiteten Klassen definiert wird).
Die Realisierung erfolgt nicht in der Reihenfolge dieser Liste, sondern durch sukzessives
Überarbeiten der einzelnen Klassen. Es wird "ganz unten" begonnen, indem in den
Basisklassen immer die Variablen und Methoden ergänzt werden, auf die andere Klassen
dann zugreifen werden. Wenn Sie die Schritte nacharbeiten wollen, sollten Sie folgendermaßen auf der Basis der Version "fmom12" des Tutorials starten:
348
J. Dankert: C++-Tutorial
➪
Alle Dateien der Version "fmom12" (auch das Unterverzeichnis res mit den darin
befindlichen Dateien) werden in ein leeres Verzeichnis (z. B. mit dem Namen
fmom13) kopiert. Zusätzlich sind in dieses Verzeichnis die beiden Dateien grint.h
und grint.cpp aus der Version "gri3" des im vorigen Abschnitt entwickelten BeispielProgramms zu kopieren (die Arbeit kann z. B. mit dem Windows-Dateimanager
erledigt werden).
Nach dem Start von MS-Visual-C++ wird in der "Visual Workbench" unter Project
die Option Open... gewählt. In der sich öffnenden "Open Project"-Dialog-Box wird
im Fenster Verzeichnisse: das gerade mit den Dateien des Projekts bestückte
Verzeichnis (fmom13) ausgewählt, dann dürfte im linken Fenster nur die Datei
fmom.mak aus diesem Verzeichnis angeboten werden. Nach Doppelklick auf diese
Datei meldet sich eine "Message-Box" mit dem Hinweis, daß sich das Projekt aus
seinem ursprünglichen Verzeichnis herausbewegt hat und nun die Abhängigkeiten neu
generiert werden müssen. Das wird mit OK bestätigt.
Die Dateien der Klasse CGrInt müssen noch beim Projekt angemeldet werden (sie
gehörten ja nicht zur Version "fmom12"): Im Menü Project wird Edit... gewählt. In
der sich öffnenden "Edit - FMOM.MAK"-Dialog-Box wird im linken mit File Name:
überschriebenen Fenster die Datei grint.cpp ausgewählt, nach Klicken auf den Button
Add erscheint sie unten im Fenster Files in Project. Das genügt, die Header-Datei
muß nicht gesondert angemeldet werden, weil sie über die #include-Anweisung in der
Implementations-Datei gefunden wird. Mit einem Klick auf den Button Close wird
die Dialog-Box geschlossen.
Zunächst wird die Klasse CGrInt um eine Methode erweitert, die den in der Klasse
gespeicherten "Device Context"-Pointer abliefert:
➪
In der "Visual Workbench" wird die Datei grint.h geöffnet. Im public-Bereich der
Klasse wird folgende Zeile ergänzt:
CDC* GetDC () ;
Es wird die Datei grint.cpp geöffnet, in der die Methode implementiert wird:
CDC* CGrInt::GetDC () { return m_pDC ; }
In der Header-Datei der Basisklassen, die die Flächen repräsentieren, sind die Deklarationen
der Klassen CArea, CCircle und CRectangle zu erweitern:
➪
In der "Visual Workbench" wird die Datei areas.h geöffnet. Die nachfolgend fett
gedruckten Zeilen sind zu ergänzen (einige Erläuterungen danach):
// Datei areas.h fuer die Version "fmom13"
#include "stdafx.h"
#include "grint.h"
#include <math.h>
const double pi_4 = atan (1.) ;
// ...
class CArea : public CObject
{
protected:
int
m_area_or_hole ;
COLORREF m_area_col
;
COLORREF m_cont_col
;
//
//
//
//
...
1 --> Flaeche, -1 --> Ausschnitt
Fuellfarbe
Konturfarbe
349
J. Dankert: C++-Tutorial
public:
CArea
void
int
COLORREF
COLORREF
void
virtual
virtual
virtual
virtual
virtual
(int area_or_hole = 1) ;
set_aoh
(int area_or_hole) ;
get_aoh
() ;
get_area_col () ;
get_cont_col () ;
Serialize_AreaVals (CArchive &ar) ;
double get_a () = 0 ;
// Flaeche
double get_xc () = 0 ;
// Schwerpunkt-Koordinate x
double get_yc () = 0 ;
// Schwerpunkt-Koordinate y
void
get_area_min_max (double &xmin , double &ymin ,
double &xmax , double &ymax) = 0 ;
void
draw_area (CGrInt *grint) = 0 ;
} ;
class CCircle : public CArea
{
// ...
public:
CCircle (double d , double mpx , double mpy) ;
double get_a
() ;
double get_xc
() ;
double get_yc
() ;
void
get_area_min_max (double &xmin , double &ymin ,
double &xmax , double &ymax) ;
void
draw_area (CGrInt *grint) ;
virtual void Serialize (CArchive &ar) ;
} ;
class CRectangle : public CArea
{
// ...
public:
CRectangle (double x1 , double y1 , double x2 , double y2) ;
double get_a
() ;
double get_xc
() ;
double get_yc
() ;
void
get_area_min_max (double &xmin , double &ymin ,
double &xmax , double &ymax) ;
void
draw_area (CGrInt *grint) ;
virtual void Serialize (CArchive &ar) ;
} ;
◆
Die beiden Klassen-Variablen, die die Farben der Flächen bestimmen sollen, wurden
mit dem Type COLORREF (vgl. Abschnitt 14.4.13) deklariert. Deshalb wird auch
die Header-Datei stdafx.h eingebunden (diese inkludiert afxwin.h und diese
wiederum windows.h). Beide Variablen werden wie die beiden Methoden
(get_area_col und get_cont_col) an die abgeleiteten Klassen vererbt.
◆
Die beiden virtuellen Methoden get_area_min_max und draw_area wurden mit dem
Zusatz = 0 deklariert, werden also in der abstrakten Klasse CArea nicht definiert. Sie
müssen deshalb in den beiden aus CArea abgeleiteten Klassen (CCircle und
CRectangle) deklariert und definiert werden, da diese sonst auch abstrakt wären.
◆
Die Methode draw_area soll mit der Hilfe der Klasse CGrInt die Fläche zeichnen.
Sie erwartet deshalb einen Pointer auf ein Objekt dieser Klasse. Um den Typ
bekanntzumachen, wird die Header-Datei grint.h eingebunden.
J. Dankert: C++-Tutorial
350
Nun müssen die zusätzlichen Methoden der Klassen CArea, CCircle und CRectangle
implementiert werden:
➪
In der "Visual Workbench" wird die Datei area.cpp geöffnet, es werden die
nachfolgend fett gedruckten Zeilen ergänzt:
// Datei area.cpp fuer die Version "fmom13"
#include "stdafx.h"
#include "areas.h"
CArea::CArea (int area_or_hole)
{
m_area_or_hole = area_or_hole ;
m_area_col
= RGB (255,0,0) ; // Fuellfarbe: Rot
m_cont_col
= RGB ( 0,0,0) ; // Konturfarbe: Schwarz
}
void CArea::set_aoh (int area_or_hole)
{
m_area_or_hole = area_or_hole ;
if
(area_or_hole == 1) m_area_col = RGB (255, 0, 0) ;
else
m_area_col = RGB ( 0,255,255) ;
}
int CArea::get_aoh () { return m_area_or_hole ; }
COLORREF CArea::get_area_col () { return m_area_col ; }
COLORREF CArea::get_cont_col () { return m_cont_col ; }
void CArea::Serialize_AreaVals (CArchive &ar)
{
if (ar.IsStoring ())
{
ar << (WORD) m_area_or_hole ;
ar << m_area_col
;
ar << m_cont_col
;
}
else
{
WORD i ;
ar >> i ;
m_area_or_hole = i ;
ar >> m_area_col
;
ar >> m_cont_col
;
}
}
◆
Der Konstruktor der Klasse initialisiert die beiden neuen Klassen-Variablen mit festen
Werten (RGB-Makro wurde im Abschnitt 14.4.13 erläutert).
◆
Die Methode set_aoh, mit der der Indikator "Teilfläche oder Ausschnitt" gesetzt wird,
setzt in Abhängigkeit von diesem Indikator die Farben neu, für Teilflächen wird mit
RGB(255,0,0) die Farbe "Rot" festgelegt, für Ausschnitte mit RGB(0,255,255) die
Farbe "Cyan".
◆
Auch die "Serialization" (vgl. Abschnitt 14.4.11) wurde erweitert, auch die neuen
Klassen-Variablen werden erfaßt. Damit ist die Kompatibilität zu Vorgängerversionen
nicht mehr gegeben (vgl. die nachfolgenden Änderungen für die Klassen CCircle und
CRectangle).
351
J. Dankert: C++-Tutorial
➪
In der "Visual Workbench" wird die Datei circle.cpp geöffnet, in der die beiden
Methoden CCircle::get_area_min_max und CCircle::draw_area ergänzt werden.
Der nachfolgend dafür angegebene Code dürfte selbsterklärend sein:
void
CCircle::get_area_min_max (double &xmin , double &ymin ,
double &xmax , double &ymax)
{
xmin
ymin
xmax
ymax
=
=
=
=
get_xc
get_yc
get_xc
get_yc
()
()
()
()
+
+
m_d
m_d
m_d
m_d
/
/
/
/
2
2
2
2
;
;
;
;
}
void CCircle::draw_area (CGrInt *grint)
{
grint->Ellipse (get_xc () - m_d / 2 , get_yc () - m_d / 2 ,
get_xc () + m_d / 2 , get_yc () + m_d / 2) ;
}
Außerdem wird der Aufruf des IMPLEMENT_SERIAL-Makros (am Ende der
Datei) folgendermaßen geändert:
IMPLEMENT_SERIAL (CCircle , CObject , 2)
◆
Die geänderte Versionsnummer im IMPLEMENT_SERIAL-Makro sorgt dafür, daß
keine Binär-Dateien älterer Programmversionen von diesem Programm eingelesen
werden können (und umgekehrt), was mit Sicherheit zu Fehlinterpretationen der
gelesenen Werte führen müßte, weil sich die Anzahl der zu speichernden Werte
geändert hat.
➪
In der "Visual Workbench" wird die Datei rectangle.cpp geöffnet, in der die beiden
Methoden CRectangle::get_area_min_max und CRectangle::draw_area ergänzt
werden:
void
CRectangle::get_area_min_max (double &xmin , double &ymin ,
double &xmax , double &ymax)
{
double
double
double
double
x1
y1
x2
y2
xmin
ymin
xmax
ymax
(x1
(y1
(x1
(y1
=
=
=
=
=
=
=
=
m_p1.get_x
m_p1.get_y
m_p2.get_x
m_p2.get_y
<
<
>
>
x2)
y2)
x2)
y2)
?
?
?
?
x1
y1
x1
y1
()
()
()
()
:
:
:
:
;
;
;
;
x2
y2
x2
y2
;
;
;
;
}
void CRectangle::draw_area (CGrInt *grint)
{
grint->Rectangle (m_p1.get_x () , m_p1.get_y () ,
m_p2.get_x () , m_p2.get_y ()) ;
}
Auch hier wird der Aufruf des IMPLEMENT_SERIAL-Makros (am Ende der Datei)
geändert:
IMPLEMENT_SERIAL (CRectangle , CObject , 2)
◆
Der etwas kompliziertere Algorithmus zum Ermitteln der Extremwerte eines
Rechtecks resultiert daraus, daß ein Rechteck durch zwei beliebige Punkte auf einer
Diagonalen beschrieben werden kann.
352
J. Dankert: C++-Tutorial
In der Klasse CFmomDoc werden die Methoden GetMinMaxCoord und DrawAllAreas
ergänzt, die jeweils die gesamte verkettete Liste der Flächen durchlaufen. Zunächst werden
die beiden Methoden in der Klasse CFmomDoc deklariert, anschließend in der Implementations-Datei definiert:
➪
In der "Visual Workbench" wird die Datei fmomdoc.h geöffnet, im public-Bereich
der Klassen-Deklaration werden die fett gedruckten Zeilen ergänzt:
public:
// Zugriffsmethoden auf die doppelt verkettete Liste m_areaList:
void
NewArea
(CArea *pArea) ;
POSITION GetFirstAreaPos ()
;
CArea*
GetNextArea
(POSITION &pos) ;
int
ASxSy
(double &a , double &sx , double &sy) ;
int
GetMinMaxCoord (double &xmin , double &ymin ,
double &xmax , double &ymax) ;
void
DrawAllAreas
(CGrInt *grint) ;
➪
In der "Visual Workbench" wird die Datei fmomdoc.cpp geöffnet, der Code für die
beiden neuen Methoden wird ergänzt:
int CFmomDoc::GetMinMaxCoord (double &xmin , double &ymin ,
double &xmax , double &ymax)
{
double xmn , ymn , xmx , ymx ;
int
first = 1 ;
if (m_areaList.IsEmpty ()) return 0 ;
for (POSITION pos = GetFirstAreaPos () ; pos != NULL ; )
{
CArea *pArea = GetNextArea (pos) ;
if (first)
{
pArea->get_area_min_max (xmin , ymin , xmax , ymax) ;
first = 0 ;
}
else
{
pArea->get_area_min_max (xmn , ymn , xmx , ymx) ;
if
if
if
if
(xmn
(ymn
(xmx
(ymx
<
<
>
>
xmin)
ymin)
xmax)
ymax)
xmin
ymin
xmax
ymax
=
=
=
=
xmn
ymn
xmx
ymx
;
;
;
;
}
}
return 1 ;
}
void CFmomDoc::DrawAllAreas (CGrInt *grint)
{
if (m_areaList.IsEmpty ()) return ;
CDC* pDC = grint->GetDC () ;
for (POSITION pos = GetFirstAreaPos () ; pos != NULL ; )
{
CArea *pArea = GetNextArea (pos) ;
CBrush brush (pArea->get_area_col ()) ;
CBrush* brush_old = pDC->SelectObject (&brush) ;
353
J. Dankert: C++-Tutorial
CPen
CPen*
pen
(PS_SOLID , 1 , pArea->get_cont_col ()) ;
pen_old = pDC->SelectObject (&pen) ;
pArea->draw_area (grint) ;
pDC->SelectObject (brush_old) ;
pDC->SelectObject (pen_old)
;
}
}
◆
Die Strategie des Durchlaufens der verketteten Liste der Fläche entspricht in beiden
Methoden dem bereits in der Methode ASxSy programmierten Algorithmus (vgl.
Abschnitt 14.4.8). Der Aufruf der in CArea virtuell deklarierten Methoden
get_area_min_max bzw. draw_area aktiviert nach den Regeln des Polymorphismus
immer die "passende" Methode.
◆
Bei der Methode GetMinMaxCoord erhält die erste Fläche eine Sonderbehandlung,
weil sie die Anfangswerte setzt. Diese Methode hat außerdem einen Return-Wert, der
darüber informiert, ob überhaupt Flächen existieren. Dieser Wert kann in OnDraw
genutzt werden, um eventuell die weiteren Aktionen zu stoppen.
◆
In der Methode DrawAllAreas wird zunächst der "Device Context"-Pointer über das
CGrInt-Objekt besorgt, um damit einen speziellen "Brush" und einen speziellen
"Pen" im "Device Context" zu etablieren. Das Einsetzen eines GDI-Objekts der
Klasse CBrush wurde bereits im Abschnitt 14.4.13 demonstriert, für ein Objekt der
Klasse CPen ist die Vorgehensweise identisch.
◆
Die drei Argumente, die dem Konstruktor der Klasse CPen übergeben werden,
bestimmen den Linientyp, die Linienbreite (Geräteeinheiten, also "Pixel" bei
Bildschirmausgabe) und die Farbe (vom Typ COLORREF). Nur für die Linienbreite
1 sind neben dem Linientyp PS_SOLID (durchgezogene Linie) auch noch weitere
wie z. B. PS_DOT (punktiert), PS_DASH (gestrichelt), PS_DASHDOT und
PS_DASHDOTDOT möglich.
Nun sind alle Klassen ausreichend nachgerüstet, um in der Ansichtsklasse des noch leeren
"Splitter-Window-Panes" die Methode OnDraw mit der Zeichenaktion auszustatten:
➪
In der "Visual Workbench" wird die Datei drawview.cpp geöffnet. Nach den
ausführlichen Vorbereitungen wird der Code für OnDraw nicht sehr umfangreich:
void CDrawView::OnDraw(CDC* pDC)
{
CFmomDoc* pDoc = (CFmomDoc*) GetDocument();
CRect rect ;
GetClientRect (&rect) ;
double
xmin , ymin , xmax , ymax ;
if (pDoc->GetMinMaxCoord (xmin , ymin , xmax , ymax))
{
if (rect.Width () > 0 && rect.Height () > 0 &&
fabs (xmax - xmin) > 1.e-20 && fabs (ymax - ymin) > 1.e-20)
{
CGrInt grint (pDC , xmin , ymin , xmax , ymax ,
rect.Width () , rect.Height () , 5.) ;
pDoc->DrawAllAreas (&grint) ;
}
}
}
354
J. Dankert: C++-Tutorial
Die Header-Datei der Dokument-Klasse muß zusätzlich eingebunden werden, um die
die Methoden GetMinMaxCoord und DrawAllAreas bekanntzumachen:
#include "fmomdoc.h"
wird im Kopf der Datei ergänzt. Da fmomdoc.h die Header-Datei areas.h inkludiert
und diese wiederum grint.h, ist damit auch die Klasse CGrInt bekannt (und natürlich
auch der in OnDraw verwendete Konstruktor dieser Klasse).
◆
Die CView-Methode GetDocument liefert einen Pointer vom Type CDocument*, der
hier einfach auf den Typ der von CDocument abgeleiteten Klasse FmomDoc
"gecastet" wird. Das ist der einfachere Weg im Vergleich mit dem (allerdings nach
allen Seiten "wasserdichten" und in fmomview.h und fmomview.cpp zu besichtigenden) vom "App Wizard" generierten Code.
➪
Nun kann das ausführbare Programm erzeugt werden (z. B. mit Build FMOM.EXE
im Menü Project). Nach dem Programmstart (Execute FMOM.EXE) werden die
Daten des Beispiels
eingegeben, das bereits
im Abschnitt 12.4 (dort
mit dem Programm
schwerp3.cpp) berechnet wurde, und neben
den berechneten Ergebnissen sieht man die
graphische Darstellung
der Fläche (nebenstehende Abbildung).
Der nun erreichte Zustand des Projekts gehört zum Tutorial als Version "fmom13".
14.4.15
Schwerpunkt markieren, Durchmesser: 0,1 "Logical Inches"
Auch das einzige bisher graphisch darstellbare Ergebnis der Berechnung, die Lage des
Schwerpunkts, soll nun im Graphik-"Pane" sichtbar gemacht werden. Dafür bietet sich ein
"Marker" an (hier wird ein kleiner gefüllter Kreis gewählt), der bei beliebiger Abmessung des
Fensters stets die gleiche Größe haben soll.
Dabei ergibt sich folgendes Problem: Für das Zeichnen mit vorzugebenden Abmessungen
bietet Windows zwar vier Koordinatensysteme an (mit Einheiten auf der Basis von mm bzw.
Inches, vgl. Abschnitt 14.4.13), da aber die Flächen mit dem Koordinatensystem MM_ISOTROPIC (unter aufwendiger Transformation auf die erforderlichen int-Werte) gezeichnet
werden, muß in jedem Fall noch zusätzlich umgerechnet werden, um die festen Längenabmessungen mit den eingestellten logischen Koordinaten (auf die sich die Lage des
Schwerpunkts bezieht) paßfähig zu machen. Von den (wie immer) mehreren Möglichkeiten
der Realisierung wird nachfolgend eine Variante beschrieben, mit der einige auch sonst sehr
nützliche Methoden aus den "Microsoft Foundation Classes" vorgestellt werden können.
355
J. Dankert: C++-Tutorial
Zunächst wird die Klasse CGrInt um die Methode DrawMarker erweitert, die folgende
Eigenschaften haben soll (eigentlich fehlt eine solche Methode in den MFC doch sehr):
◆
Sie übernimmt die Koordinaten des zu markierenden Punktes wie die anderen
CGrInt-Methoden als double-Werte und transformiert diese vor der Übergabe an die
CDC-Methode auf den im Konstruktor eingestellten int-Bereich. Dort wird ein
Marker der durch das Argument type vorzugebenden geometrischen Form gezeichnet
(zunächst werden die Formen "Kreuz", "Kreis" und "Quadrat" realisiert).
◆
Für die Einstellung einer festen Größe des darzustellenden Markers werden die CDCMethoden GetDeviceCaps ("Get Device Capabilities") und DPtoLP (konvertiert
"Device Units" nach "Logical Units") benutzt. Die "Standard-Marker-Größe" wird
schon im Konstruktor von CGrInt festgelegt und in einer Klassen-Variablen vom
Type SIZE gespeichert. Die fest eingestellte Standardgröße kann durch ein Argument
size beim Aufruf von DrawMarker verändert werden.
➪
In der "Visual Workbench" wird die Datei grint.h geöffnet, im private-Bereich der
Klassen-Deklaration wird eine Zeile ergänzt:
private:
SIZE
m_def_marker_offset ;
Im public-Bereich werden zwei Zeilen hinzgefügt:
public:
void DrawMarker (double x , double y , double size , int type) ;
enum
mk_type { mk_cross , mk_circle , mk_square } ;
◆
SIZE ist eine in windows.h deklarierte Struktur, die nur zwei int-Komponenten
enthält. Sie soll die Abmessungen des Markers von dem zu markierenden Punkt
(Mittelpunkt des Markers) bis zum Randpunkt in x- bzw. y-Richtung aufnehmen.
◆
Mit dem Aufzählungstyp enum werden für die drei Indikatoren, die als Argument für
den Parameter type von DrawMarker möglich sind, Namen festgelegt (der Compiler
ordnet ihnen die Werte 0, 1 und 2 zu). Da die enum-Anweisung im public-Bereich
steht, können sie über eine Instanz der Klasse CGrInt angesprochen werden.
Die beiden Komponenten von m_def_marker_offset werden schon im Konstruktor
CGrInt::CGrInt mit Werten belegt:
◆
Die CDC-Methode GetDeviceCaps gibt Auskunft über das Ausgabegerät, das dem
"Device Context" zugeordnet ist. Da man einige Dutzend Informationen abfordern
kann, wird bei einem GetDeviceCaps-Aufruf immer nur genau eine Information (als
Return-Wert) abgeliefert, als Argument wird ein Wert übergeben, der die Art der
gewünschten Information bestimmt (die möglichen Argument-Werte sind in der Datei
windows.h als Konstanten definiert).
Während z. B. die Breite bzw. Höhe der verfügbaren Ausgabefläche (überraschenderweise) in mm geliefert wird, gilt für die hier erfragten Informationen LOGPIXELSX
und LOGPIXELSY die etwas exotisch klingende Dimension 'Bildpunkte / "Logical
Inch"'. Für Drucker ist ein "Logical Inch" gleich einem "Inch" (25,4 mm), bei einem
HP-LaserJet III wird deshalb der Wert 300 geliefert (300 dpi, "Dots per Inch"). Weil
kleine Schriften, die bei der Druckerausgabe durchaus gut lesbar sind, bei den
356
J. Dankert: C++-Tutorial
wesentlich grober gerasterten Bildschirmen unleserlich wären, baut Windows hier eine
"automatische Vergrößerung" ein, indem ein "Logical Inch" für den Bildschirm etwas
größer ausfällt. Für eine 640x480-VGA-Graphikkarte wird deshalb der Wert 96
abgeliefert. In jedem Fall ist damit für das Ausgabegerät die Anzahl der Bildpunkte
für eine feste (beim Bildschirm etwas komische) Längeneinheit gegeben.
◆
Die Umrechnung von Maßen, die in Geräteeinheiten gegeben sind, auf die jeweils
eingestellten logischen Koordinaten kann man der CDC-Methode DPtoLP übertragen.
Diese ist mehrfach überladen, kann u. a. RECT-Strukturen, CRect-Objekte, und
POINT-Arrays verarbeiten, wird hier mit einem Pointer auf eine SIZE-Struktur
aufgerufen und verändert die Komponenten dieser Struktur.
➪
In der "Visual Workbench" wird die Datei grint.cpp geöfnnet. Im Konstruktor
CGrInt::CGrInt werden (an beliebiger Stelle) drei Zeilen ergänzt:
CGrInt::CGrInt (CDC*
{
pDC
,
// ...
// ...
m_def_marker_offset.cx = m_pDC->GetDeviceCaps (LOGPIXELSX) / 20 ;
m_def_marker_offset.cy = m_pDC->GetDeviceCaps (LOGPIXELSY) / 20 ;
m_pDC->DPtoLP (&m_def_marker_offset) ;
}
Der Offset (von Marker-Mitte bis zum Rand wird also auf 1/20 "Logical Inches" festgelegt,
der Durchmesser wird also 1/10 "Logical Inches" sein. Das wären bei Druckerausgabe exakt
2,54 mm, bei Ausgabe auf dem Bildschirm sind es etwas mehr als 3 mm (genau wird es
ohnehin nie, weil auf dem Bildschirm natürlich bei der Ausgabe wieder nur die PixelPositionen getroffen werden können).
In der Regel werden für beide Richtungen gleiche Werte von GetDeviceCaps abgeliefert (das
war im Zeitalter von CGA- und EGA-Graphikkarten durchaus nicht der Fall). Die CDCMethode DPtoLP ändert direkt die Struktur, deren Pointer sie übernimmt.
➪
In der Datei grint.cpp wird die Methode DrawMarker ergänzt:
void CGrInt::DrawMarker (double x , double y , double size , int type)
{
int offset_x = int (m_def_marker_offset.cx * size) ;
int offset_y = int (m_def_marker_offset.cy * size) ;
if (x >= m_xmin && y
x <= m_xmax && y
{
int x2 = Conv_x
int y2 = Conv_y
int x1 = max (0
int y1 = max (0
>= m_ymin &&
<= m_ymax)
(x) ;
(y) ;
, x2 - offset_x) ;
, y2 - offset_y) ;
// max (a,b) ist Makro
// aus windows.h
if (x2 <= m_inorm - offset_x) x2 += offset_x ;
if (y2 <= m_inorm - offset_y) y2 += offset_y ;
switch (type)
{
case mk_cross:
m_pDC->MoveTo
m_pDC->LineTo
m_pDC->MoveTo
m_pDC->LineTo
break ;
(x1 , Conv_y (y))
(x2 , Conv_y (y))
(Conv_x (x) , y1)
(Conv_x (x) , y2)
;
;
;
;
357
J. Dankert: C++-Tutorial
case mk_circle:
m_pDC->Ellipse (x1 , y1
break ;
, x2 , y2) ;
case mk_square:
m_pDC->Rectangle (x1 , y1
break ;
, x2 , y2) ;
}
}
}
Der Marker wird in der im Konstruktor festgelegten Größe gezeichnet, wenn für den
Parameter size der Wert 1.0 übergeben wird, ansonsten mit dem "size-fachen" dieser Größe.
Der Rest des Programms (einschließlich der Vorsichtsmaßnahmen zur Einhaltung der
Grenzen der Zeichenfläche) dürfte selbsterklärend sein.
➪
In der "Visual Workbench" wird die Datei fmomdoc.cpp geöffnet, in der Methode
DrawAllAreas wird am Ende der Aufruf der Methode DrawResult ergänzt (der
Name deutet beabsichtigte spätere Erweiterungen an):
void CFmomDoc::DrawAllAreas (CGrInt *grint)
{
// ...
DrawResult (grint) ;
}
➪
In der Datei fmomdoc.cpp wird die Methode DrawResult hinzugefügt:
void CFmomDoc::DrawResult (CGrInt *grint)
{
double a , sx , sy ;
if (ASxSy (a , sx , sy))
{
if (fabs (a) > 1.e-20)
{
CDC* pDC = grint->GetDC() ;
CBrush brush (RGB (255 , 255 , 0)) ;
// ... gelb mit ...
CBrush* brush_old = pDC->SelectObject (&brush) ;
CPen
CPen*
pen
(PS_SOLID , 1 , RGB (0 , 0 , 255)) ;
// ...
pen_old = pDC->SelectObject (&pen) ; // blauem Rand
grint->DrawMarker (sy / a , sx / a , 1. , grint.mk_circle) ;
pDC->SelectObject (brush_old) ;
pDC->SelectObject (pen_old)
;
}
}
}
➪
Die neue Methode muß in der Deklaration der Klasse CFmomDoc (in der Datei
fmomdoc.h) ergänzt werden:
public:
void
DrawResult (CGrInt *grint) ;
Wenn der Schwerpunkt berechnet werden kann, besorgt sich DrawResult den Pointer auf den
"Device Context" (von der CGrInt-Instanz grint), setzt die Farben und ruft DrawMarker
auf. Das Erzeugen und die Wiederfreigabe der GDI-Objekte (für die Farben) erfolgt nach der
bereits in DrawAllAreas realisierten Strategie.
J. Dankert: C++-Tutorial
358
Dem aufmerksamen Leser wird nicht entgangen sein, daß bei der Darstellung der Ergebnisse
mehrfach die gleichen Berechnungen ausgeführt wurden, so wird z. B. die Methode
CFmomDoc::ASxSy wiederholt bei unveränderter Datenstruktur aufgerufen. Das bleibt
allerdings selbst für komplizierte Probleme bei schnellen Rechnern ohne nenenswerte
Auswirkungen auf die Reaktionszeit. Aber natürlich kann jederzeit (ohne Auswirkungen auf
die Programme, die die Methoden der Klasse CFmomDoc aufrufen) "nachgerüstet" werden:
Es könnten z. B. die Ergebnisse, die ASxSy berechnet, zusätzlich und gemeinsam mit einem
Indikator, ob sie aktuell sind, in der Klasse gespeichert werden. Sie müßten dann nur neu
berechnet werden, wenn sich die Datenstruktur ändert. Von einer solchen Modifikation wären
nur die Deklaration und wenige Methoden der Klasse betroffen, "nach außen" hätte eine
solche Änderung keine Auswirkungen (außer einem minimalen Geschwindigkeitsvorteil).
➪
Nun kann das ausführbare Programm erzeugt werden (z. B. mit Build FMOM.EXE
im Menü Project). Nach dem Programmstart (Execute FMOM.EXE) und Eingabe
eines Berechnungsmodells sieht man den
Schwerpunkt auch in der
graphischen Darstellung
(nebenstehende Abbildung). Bei einer Änderung der Fenstergröße
paßt sich die Zeichnung
an, der Marker behält
seine Größe bei.
Der nun erreichte Zustand des Projekts gehört zum Tutorial als Version "fmom14".
14.4.16
Erweiterung der Funktionalität: Flächenmomente 2. Ordnung
Bisher werden von fmom nur die Koordinaten des Gesamt-Schwerpunkts der Fläche
berechnet, die durch Teilflächen (und Ausschnittflächen) beschrieben wird. Es ist eine kleine
Mühe, das Programm auf die Berechnung der Flächenmomente 2. Ordnung zu erweitern und
es damit zu einem sehr nützlichen Werkzeug für Ingenieur-Probleme zu machen. Die dafür
erforderlichen Formeln werden (entnommen aus "Dankert/Dankert: Technische Mechanik,
computerunterstützt") nachfolgend (einschließlich der bisher schon verwendeten Formeln zur
Schwerpunkt-Berechnung) zusammengestellt:
Mit den Teilflächen Ai (geliefert von den Methoden get_a) und den Schwerpunkt-Koordinaten der Teilflächen xi und yi (geliefert von get_xc bzw. get_yc) berechnen sich die Gesamtfläche A und die statischen Momente Sx und Sy (realisiert in CFmomDoc::ASxSy) nach
Die Koordinaten des Gesamt-Schwerpunkts ergeben sich dann nach
J. Dankert: C++-Tutorial
359
Für die Flächenmomente 2. Ordnung ("Flächenträgheitsmomente") Ixx,S und Iyy,S und das
"Deviationsmoment" Ixy,S (jeweils bezogen auf ein Koordinatensystem mit dem Ursprung im
Gesamt-Schwerpunkt und Achsen, die parallel zum x-y-System liegen, auf das sich die
Eingabe der Teilflächen bezog) gelten die Formeln:
Darin sind die Ixx,i, Iyy,i und Ixy,i die Flächenmomente 2. Ordnung der Teilflächen, bezogen auf
das Schwerpunkt-Koordinatensystem der jeweiligen Teilflächen. Speziell gilt
(b ist die parallel zur x-Richtung gemessene Breite, h die parallel zur y-Richtung gemessene
Höhe des Rechtecks, d der Durchmesser des Kreises).
Das maximale und das minimale Flächenträgheitsmoment (die "Hauptträgheitsmomente")
aller möglichen Schwerpunktachsen ergeben sich bezüglich einer um den Winkel ϕ zur xAchse geneigten Achse (Imax) und einer dazu senkrechten Achse (Imin). Es gelten die Formeln:
Da der Einbau der zusätzlichen Berechnungen in das Programm ausschließlich mit bereits
behandelten Programmier-Strategien realisiert werden kann, andererseits die Chance bietet,
einige wichtige Themen der objektorientierten Programmierung zu wiederholen, wird die
Erweiterung des Projekts fmom als Aufgabe formuliert:
Das Projekt fmom ist auf der Basis der Version "fmom14" zu erweitern,
so daß zusätzlich die Flächenmomente 2. Ordnung Ixx,S, Iyy,S und Ixy,S
(bezogen auf Koordinaten mit Ursprung im Gesamt-Schwerpunkt), die beiden Hauptträgheitsmomente und die Lage der Hauptzentralachsen (Winkel zwischen x-Achse und Achse des
größten Hauptträgheitsmoments) berechnet werden. Folgende Schritte sind zu realisieren:
Aufgabe 14.4:
a)
In der abstrakten Klasse CArea sind drei virtuelle Methoden (mit dem Zusatz = 0)
get_Ixx, get_Iyy und get_Ixy zu deklarieren. Zu definieren sind diese Methoden nur
J. Dankert: C++-Tutorial
360
in den aus CArea abgeleiteten Klassen CCircle und CRectangle. Sie sollen für eine
Teilfläche die Flächenmomente 2. Ordnung (bezogen auf das eigene SchwerpunktKoordinatensystem) abliefern, die in den oben angegebenen Formeln mit Ixx,i, Iyy,i und
Ixy,i bezeichnet wurden.
b)
In der Dokumentklasse ist eine Methode CFmomDoc::IminImaxPhi zu ergänzen, die
6 Ergebnisse für die Gesamtfläche abliefert: Ixx,S, Iyy,S, Ixy,S, Imax, Imin und ϕ (zu
realisieren unter Verwendung des angegebenen Formelsatzes analog zur Methode
CFmomDoc::ASxSy).
c)
In der Ansichtsklasse ist die Methode CFmomView::OnDraw um den Auruf der
Methode CFmomDoc::IminImaxPhi und die Ausgabe der damit errechneten
Ergebnisse zu erweitern.
Für das Beispiel, das
bereits im Abschnitt 12.3
mit dem Programm
schwerp1.cpp berechnet
wurde, kann die ErgebnisAusgabe wie nebenstehend dargestellt aussehen.
Und wenn Sie keine Lust
hatten, die Aufgabe
selbst zu lösen: Der mit
Aufgabe 14.4 erreichte
Zustand des Projekts
gehört zum Tutorial als
Version "fmom15".
Das Projekt fmom ist auf der Basis der Version "fmom15" zu erweitern:
Die Hauptzentralachsen, deren Lage als Ergebnis der Aufgabe 14.4
bekannt ist, sollen in das Graphik-"Pane" eingezeichnet werden. Weil sie durch die
Koordinaten des Schwerpunktes und einen Winkel bestimmt werden, Linien aber durch
Angabe von zwei Punkten gezeichnet werden, wird folgendes Vorgehen empfohlen:
Aufgabe 14.5:
a)
In der Klasse CGrInt wird eine Methode DrawLinePtAng ergänzt, die die Koordinaten eines Punktes und den Winkel als Parameter erwartet (alle Werte vom Typ
double). Sie berechnet damit die Koordinaten zweier Punkte auf den Rändern des in
den Klassen-Variablen von CGrInt abgelegten Zeichenbereichs und verbindet die
beiden Punkte mit einer geraden Linie.
b)
Die Methode CGrInt::DrawLinePtAng wird von der Methode der Dokument-Klasse
CFmomDoc::DrawResult zweimal aufgerufen (je einmal für jede Hauptzentalachse,
die sich nur durch den um 90° geänderten Winkel voneinander unterscheiden). Vorher
wird jeweils ein Zeichenstift mit einer anderen Farbe in den "Device Context"
eingesetzt, z. B. "Magenta" (RGB(255,0,255)) für die Imax-Achse und "Grün"
(RGB(0,255,0)) für die Imin-Achse.
J. Dankert: C++-Tutorial
361
Das Ergebnis der Lösung
der Aufgabe 14.5 könnte
so aussehen wie in der
nebenstehenden Abbildung.
Natürlich gehört auch
das Ergebnis der Aufgabe 14.5 zum Tutorial.
Es ist die Version
"fmom16".
14.4.17
Listen, Ändern und Löschen
In den Abschnitten 14.4.17 bis 14.4.20 sollen Änderungen der Datenstruktur ermöglicht
werden. Mit folgenden Varianten werden die Wünsche des Programm-Benutzers wohl
weitgehend erfüllt:
◆
Das Löschen der gesamten Datenstruktur ist besonders einfach zu realisieren und
wurde mit dem Schreiben der Methode CFmomDoc::DeleteContents (siehe
Abschnitt 14.4.4) bereits vorbereitet. Es ist sinnvoll, eine Rückfrage an den
Programm-Benutzer einzubauen, ob dies wirklich seine Absicht ist.
◆
Da nach jeder Eingabe einer Teilfläche die aktualisierte Datenstruktur graphisch
dargestellt wird, erkennt der Programm-Benutzer Eingabefehler in der Regel sofort.
Deshalb ist ein Löschen der letzten Fläche eine besonders schnelle KorrekturMöglichkeit und auch relativ einfach realisierbar.
◆
Etwas aufwendiger ist die Realisierung des gezielten Änderns (einschließlich
Löschens) einer bestimmten Teilfläche. Hierfür muß ein Dialog vorgesehen werden,
der alle Teilflächen listet, eine Auswahl ermöglicht, um dann die ausgewählte
Teilfläche entweder zum Ändern der Werte anzubieten (dazu können die bereits
programmierten Dialoge zur Eingabe der Teilflächen genutzt werden) oder aus der
Datenstruktur zu entfernen.
Zunächst wird das Menü für diese Aufgaben vorbereitet. Dabei wird gleich in dem vom "App
Wizard" erzeugten Angebot Edit etwas "aufgeräumt":
➪
In der "Visual Workbench" wird App Studio im Menü Tools gewählt. Nach der
Auswahl Menu im Type-Fenster und Doppelklick auf IDR_FMOMTYPE im
Resources-Fenster wird der aktuelle Stand des fmom-Menüs angezeigt. Nach einem
J. Dankert: C++-Tutorial
362
Klick auf Edit sieht man vier bisher ungenutzte Optionen, die alle durch mehrmaliges
Drücken der Del(Entf)-Taste gelöscht werden sollten. Es verbleibt ein leerer Rahmen
unter Edit. Doppelklick auf Edit öffnet die zugehörige "Property Page", in der die
Caption-Eintragung von &Edit auf &Listen/Ändern geändert wird.
Ein Doppelklick auf das leere Kästchen unter dem nun mit Listen/Ändern beschrifteten Menüpunkt öffnet eine leere "Property Page". Im Feld Caption: wird &Teilflächen listen eingetragen, was sofort auch im Popup-Menü sichtbar wird, wo sich
außerdem ein neues leeres Kästchen zeigt. In das Feld Prompt: der "Property Page"
wird der Text Listen aller eingegebenen Teilflächen und Ausschnitte geschrieben.
Nach Doppelklick auf das leere Kästchen unter Teilflächen listen wird eine entsprechende Aktion ausgeführt mit Caption: &Letzte löschen und dem Prompt:
Löscht zuletzt eingegebene(n) Teilfläche/Ausschnitt, schließlich das Ganze noch
einmal für das dritte und letzte Angebot im Menü Listen/Ändern mit Caption:
&Alle löschen und dem Prompt: Löschen aller eingegebenen Teilflächen/Ausschnitte.
Wenn man nun für die drei Angebote des Menüs Listen/Ändern noch einmal die
"Property Pages" öffnet, sieht man, daß "App Studio" durchaus sinnvolle (durch das
Weglassen der Umlaute vielleicht etwas merkwürdige) Bezeichner für die ID's
gewählt hat, die akzeptiert werden sollen.
Da die Auswahl eines der drei gerade erzeugten Menüangebote in der Regel eine Änderung
der Datenstruktur zu Folge hat, werden die zugehörigen "Behandlungs-Routinen" in der
Dokument-Klasse angesiedelt:
➪
Über ClassWizard... im Menü Resource von "App Studio" landet man in der "MFC
Class Wizard"-Dialog-Box. In der mit Class Name: bezeichneten "Combo Box" wird
CFmomDoc ausgewählt. In der mit Object IDs: überschriebenen "List Box" findet
man die drei Identifikatoren, die gerade vom "App Studio" erzeugt wurden. Für alle
drei wird die nachfolgend nur für einen Identifikator beschriebene Aktion ausgeführt:
Man wählt ID_LISTENNDERN_ALLELSCHEN und danach in der Messages-"List
Box" COMMAND, klickt auf den "Add Function"-Button und akzeptiert in der sich
öffnenden "Add Member Function"-Dialog-Box mit OK den vorgeschlagenen Namen.
Dies wird für die beiden anderen ID's sinngemäß wiederholt.
In der mit Member Functions: überschriebenen "List Box" sind die Namen der
neuen Funktionen mit den zugehörigen Identifikatoren zu sehen. Es wird die Zeile
gewählt, in der der Funktionsname OnListenndernAllelschen steht und auf den
Button Edit Code geklickt. Man landet im Editor, der die Datei fmomdoc.cpp
geöffnet hat, der Cursor steht im gerade erzeugten Gerüst der Methode CFmomDoc::OnListenndernAllelschen.
Eigentlich genügt der Aufruf der bereits vorhandenen Methode DeleteContents (und
natürlich UpdateAllViews), um die mit dem Menü-Angebot Alle löschen gewünschte Aktion
auszuführen. Bei einer so radikalen Aktion soll aber vorsichtshalber zurückgefragt werden.
Dafür bietet sich die bereits im Abschnitt 10.1 verwendete Funktion MessageBox an. In der
Klassen-Bibliothek ist eine entsprechende "globale" (nicht einer Klasse zugeordnete) Funktion
AfxMessageBox vorhanden, die hier verwendet werden soll:
363
J. Dankert: C++-Tutorial
➪
In der Datei fmomdoc.cpp wird das Gerüst der Methode CFmomDoc::OnListenndernAllelschen um die folgenden fett gedruckten Zeilen ergänzt:
void CFmomDoc::OnListenndernAllelschen()
{
if (AfxMessageBox ("Alle Teilflächen/Ausschnitte löschen?" ,
MB_ICONQUESTION | MB_YESNO) == IDYES)
{
DeleteContents () ;
UpdateAllViews (NULL) ;
}
}
◆
Die Funktion AfxMessageBox arbeitet analog zu der im Abschnitt 10.1 ausführlich
beschriebenen Funktion MessageBox, kann maximal drei Buttons und ein Icon zeigen
und liefert als Return-Wert die Information, welcher Button gedrückt wurde. Hier
wird als Icon das "Fragezeichen" dargestellt. Die beiden Buttons haben die Beschriftungen Ja bzw. Nein, abgefragt wird, ob der Ja-Button gedrückt wurde.
➪
Das Projekt wird aktualisiert
(Build FMOM.EXE im Menü
Project). Nach dem Starten
des Programms (Execute
FMOM.EXE) und Eingabe
eines Berechnungsmodells
erscheint nach Auswahl von
Alle löschen im Menü Listen/Ändern die "Message
Box" mit der Rückfrage (nebenstehende Abbildung), von
deren Beantwortung das
"Schicksal" der bisher eingegebenen Teilflächen und
Ausschnitte abhängig ist.
Das Gerüst der Methode CFmomDoc::OnListenndernLetztelschen mit Leben zu erfüllen,
ist besonders einfach. Mit der CObList-Methode RemoveTail, die analog zur Methode
RemoveHead funktioniert (vgl. Erläuterungen am Ende des Abschnitts 14.4.4), wird das
letzte Element aus der verketteten Liste entfernt, analog zum Vorgehen in DeleteContents
wird mit delete danach der von der CArea-Instanz belegte Speicherplatz freigegeben:
➪
In der Datei fmomdoc.cpp wird das Gerüst der Methode
Doc::OnListenndernLetztelschen um folgende Zeilen ergänzt:
CFmom-
void CFmomDoc::OnListenndernLetztelschen()
{
if (!m_areaList.IsEmpty ())
{
delete (CArea*) m_areaList.RemoveTail () ;
UpdateAllViews (NULL) ;
}
}
Das Projekt sollte danach aktualisiert und getestet werden.
Der nun erreichte Stand des Projekts gehört zum Tutorial als Version "fmom17".
J. Dankert: C++-Tutorial
14.4.18
364
Dialog-Box mit "List Box"
Nach der Auswahl von Teilflächen listen im Menü Listen/Ändern (wurde im vorigen
Abschnitt eingerichtet, aber noch nicht mit einer Aktion hinterlegt) soll sich eine Dialog-Box
öffnen, die in einer "List Box" in jeweils einer Zeile jede bisher eingegebene Teilfläche
(natürlich auch die Ausschnitte) beschreibt. In dieser "List Box" soll der Programm-Benutzer
eine Fläche auswählen können. Drei Buttons bieten folgende Aktionen an: "Ausgewählte
Teilfläche löschen", "Ausgewählte Teilfläche ändern" und "Aktion abbrechen". Zunächst wird
diese Dialog-Box mit "App Studio" erzeugt:
➪
In der "Visual Workbench"
wird im Menü Tools das
Angebot App Studio gewählt.
Nach Anklicken des Buttons
New und Doppelklick auf
Dialog in der sich öffnenden
"New Resource"-Dialog-Box
zeigt sich das Gerüst einer
Dialog-Box. Nach Doppelklick
(nicht auf einen der beiden
vordefinierten Buttons) ändert
man in der sich öffnenden
"Property Page" die ID: auf
IDD_LIST_DIALOG und die
Eintragung im Feld Caption:
auf Ändern/Löschen. Die
nebenstehende Abbildung
zeigt das Ziel, das schließlich
erreicht werden soll, dazu sind
noch folgende Aktionen erforderlich:
Der OK-Button wird gelöscht (Anklicken und Del(Entf)-Taste drücken), die gesamte
Dialog-Box wird durch "Ziehen" an der rechten unteren Ecke etwas vergrößert (auf
etwa 200x180 Dialog-Box-Einheiten, werden unten rechts angezeigt), der CancelButton wird ("Drag and Drop") in die linke untere Ecke der Dialog-Box verschoben.
Mit "Drag and Drop" werden zwei weitere "Pushbuttons" aus der Palette der DialogBox-Elemente am unteren Rand der Dialog-Box plaziert.
Nach Doppelklick auf den Cancel-Button wird in der "Property Page" in das
Caption-Feld Abbrechen eingetragen und die "Check Box" Default Button wird
"angekreuzt" (das war vorher der OK-Button), nach Doppelklick auf den mittleren
Button wird im Caption-Feld Ändern eingetragen, die ID: wird auf
IDC_AREA_EDIT geändert. Entsprechend wird der rechte Button mit Löschen
beschriftet (im Caption-Feld) und mit der ID: IDC_AREA_DELETE versehen.
Mit "Drag and Drop" wird eine "List Box" aus der Palette in die Dialog-Box
übertragen und angemessen vergrößert. Nach Doppelklick auf die "List Box" wird in
der "Property Page" die ID: auf IDC_AREA_LIST geändert. Rechts oben in der
J. Dankert: C++-Tutorial
365
"Property Page" steht in einer kleinen "Combo Box" das Wort General. Dort wird
nun Styles gewählt, die vorgesehenen Einstellungen werden weitgehend akzeptiert
(die Einstellung Single bedeutet übrigens, daß der Programm-Benutzer nur genau eine
Eintragung auswählen kann), nur die "Check Box" Horiz. Scroll wird zusätzlich
"angekreuzt" (weil in einer Zeile die gesamte Information über eine Teilfläche
untergebracht werden soll).
Im Menü Layout wird Set Tab Order gewählt, und die Dialog-Box-Elemente werden
in der Reihenfolge "List Box"...Abbrechen-Button...Ändern-Button...Löschen-Button
angeklickt.
Damit ist die Dialog-Box komplett. Nach Auswahl von Class Wizard im Menü
Resource öffnet sich die "Add Class"-Dialog-Box, in der schon CDialog als Class
Type: eingestellt ist. Im Feld Class Name: wird CListDlg eingetragen, die automatisch angebotenen Dateinamen sind akzeptabel. Mit Create Class gibt man das
Erzeugen der Klasse in Auftrag und landet in der "MFC Class Wizard"-Dialog-Box.
Vor der weiteren Arbeit sind einige "strategische" Überlegungen nützlich: Die Dialog-Box
soll als Folge der Auswahl von Teilflächen listen im Menü Listen/Ändern erscheinen. Dafür
ist bereits die Methode CFmomDoc::OnListenndernTeilflchenlisten eingerichtet worden.
Dort also soll die CDialog-Methode DoModal aufgerufen werden.
Zu initialisieren sind nur die Listen-Einträge, dies kann jedoch nicht wie z. B. bei "Edit
Boxes" über den (am Ende des Abschnitts 14.4.6 beschriebenen) DoDataExchangeMechanismus erfolgen, weil die Organisation dafür sehr aufwendig wäre (die Anzahl der
Einträge kann bei jedem Erzeugen der Dialog-Box anders sein). Deshalb sind in diesem Fall
keine "Member Variables" für die Dialog-Box-Elemente erforderlich, es muß nur dafür
gesorgt werden, daß die "List Box" vor dem Erscheinen der Dialog-Box initialisiert wird und
daß die Methode, die die Dialog-Box erscheinen läßt, nach dem Schließen erfährt, mit
welchem Button dies erfolgt ist und welche "List Box"-Eintragung gerade selektiert war:
◆
Für das Initialisieren der "List Box" bietet sich die Auswertung der Botschaft
WM_INITDIALOG an, die vor dem Erscheinen der Dialog-Box abgesetzt wird. In
der Klasse CListDlg wird dafür eine Methode angesiedelt.
◆
Für jeden der drei Buttons wird in CListDlg eine Methode vorgesehen, die auf die
Botschaft BN_CLICKED reagieren soll. In diesen Methoden wird gegebenenfalls
abgefragt, welche "List Box"-Eintragung selektiert ist (nur erforderlich für LöschenButton und Ändern-Button), und beide Informationen werden beim Schließen des
Dialogs an die aufrufende CFmomDoc-Methode übergeben. Dafür stehen zwei Wege
zur Verfügung: Variablen in der Dialog-Klasse können mit Werten belegt werden, die
von der aufrufenden Methode ausgewertet werden, außerdem liefert DoModal einen
int-Wert ab (wurde bisher benutzt für die Abfrage "OK-Button gedrückt?"), der in
diesem Fall ausreichend als Informationsträger ist.
Es müssen also keine "Member Variables" für die Dialog-Box-Elemente generiert werden,
dafür aber vier Methoden der Klasse CListDlg:
➪
In der "MFC Class Wizard"-"Karteikarte" Message Maps muß im Feld Class Name:
die Klasse CListDlg eingestellt sein. Im Fenster Object IDs: wird CListDlg
ausgewählt, im Fenster Messages: erscheinen alle Botschaften, die die Dialog-Klasse
J. Dankert: C++-Tutorial
366
empfangen kann. Nach Wahl von WM_INIT_DIALOG wird der Button Add
Function angeklickt, im Fenster Member Functions: wird angezeigt, daß das Gerüst
einer Methode OnInitDialog angelegt wird.
Nach Auswahl von IDC_AREA_DELETE im Fenster Object IDs: zeigen sich im
Fenster Messages: nur zwei Botschaften: Auswahl von BN_CLICKED, Klicken auf
den Button Add Function, Bestätigen des angebotenen Member Function Name
durch Klicken auf OK erzeugt ein weiteres Gerüst für eine CListDlg-Methode. Die
für IDC_AREA_DELETE ausgeführte Aktion wird in gleicher Weise für die Object
IDs der beiden anderen Buttons (IDC_AREA_EDIT und IDCANCEL) ausgeführt.
Wenn schließlich im Fenster Member Functions: die vier Funktionen OnAreaDelete, OnAreaEdit, OnInitDialog und OnCancel angezeigt werden, klickt man auf den
Button Edit Code und landet im Editor, der die vom "Class Wizard" erzeugte Datei
listdlg.cpp geöffnet hat. In dieser Datei findet man die Gerüste der vier gerade
generierten Methoden.
Folgende Entscheidungen für die Arbeit der Methoden, die zum Ende des Dialogs führen
sollen, werden getroffen:
◆
OnAreaEdit und OnAreaDelete fragen ab, welches Listen-Element gerade selektiert
ist. Dafür ist die CListBox-Methode GetCurSel verfügbar, die einen int-Wert
abliefert ("0-basiert", das erste Listen-Element hat die Nummer 0). Um eine
CListBox-Methode aufrufen zu können, braucht man einen Pointer auf das entsprechende Objekt in der Dialog-Box, den man sich mit der Methode GetDlgItem
(von CWnd über CDialog an CListDlg vererbt) verschafft, der man den Identifikator
der "List Box" übergeben muß (in "App Studio" als IDC_AREA_LIST festgelegt).
Sowohl OnAreaEdit als auch OnAreaDelete rufen dann die CDialog-Methode
EndDialog auf, der ein int-Wert übergeben wird, der an die Methode DoModal
weitergereicht wird (wer das Programm "dialog1.c" im Abschnitt 10.2.3 durchgearbeitet hat, wird feststellen, daß dort die gleiche Strategie auf dem Weg "DialogFunktion"-->EndDialog-->Dialog-Box realisiert wird).
OnAreaEdit gibt den um 1 vergrößerten Wert zurück, den GetCurSel geliefert hat,
OnAreaDelete den gleichen Wert, allerdings mit einem Minuszeichen. So kann die
Funktion, die DoModal aufruft, erkennen, wie und mit welcher Selektion der Dialog
beendet wurde.
◆
OnCancel braucht das selektierte Element nicht zu erfragen, es wird einfach
EndDialog mit dem Argument 0 aufgerufen, so daß die Information weitergegeben
wird, daß "nichts getan werden muß".
➪
in der Datei listdlg.cpp werden die Gerüste der Methoden OnAreaEdit, OnInitDialog und OnCancel folgendermaßen ergänzt (einige Erläuterungen zu den fett
gedruckten Anweisungen werden im Anschluß an das Listing gegeben):
void CListDlg::OnAreaDelete()
{
CListBox* pListBox = (CListBox*) GetDlgItem (IDC_AREA_LIST) ;
EndDialog (- (pListBox->GetCurSel () + 1)) ;
}
J. Dankert: C++-Tutorial
367
void CListDlg::OnAreaEdit()
{
CListBox* pListBox = (CListBox*) GetDlgItem (IDC_AREA_LIST) ;
EndDialog (pListBox->GetCurSel () + 1) ;
}
void CListDlg::OnCancel()
{
EndDialog (0) ;
}
◆
Man beachte, daß aus der Methode CListDlg::OnCancel der vom "Class Wizard"
generierte Aufruf von CDialog::OnCancel herausgenommen wurde. Diese Methode
würde nämlich die in windows.h mit dem Wert 2 definierte Konstante IDCANCEL
als Return-Wert von DoModal erzeugen, was natürlich nicht in das hier realisierte
Konzept paßt. Da CDialog::OnCancel sonst nichts tut, kann der Aufruf weggelassen
werden.
◆
Die CWnd-Methode GetDlgItem liefert einen Pointer auf ein CWnd-Objekt, der auf
den aktuellen Pointertyp (hier: CListBox-Pointer) "gecastet" wird.
14.4.19
Initialisieren der "List Box", die Klasse CString
Das Problem, daß die Initialisierung der "List Box" nicht nach dem DoDataExchangeMechanismus wie bei einfachen Dialog-Box-Elementen (vgl. Abschnitt 14.4.6) durchgeführt
werden kann, wird noch dadurch verschärft, daß die zu listenden Informationen verstreut in
den verschiedenen aus CArea abgeleiteten Klassen abgelegt sind. Deshalb wird folgende
Strategie gewählt:
◆
In der Dokument-Klasse CFmomDoc werden zwei Methoden bereitgestellt:
CFmomDoc::GetAreaCount () liefert die Anzahl der Objekte in der verketteten Liste
(Teilflächen und Ausschnitte), und CFmomDoc::GetAreaDesc (i) liefert für die i-te
Teilfläche die Beschreibung (String, der in die "List Box" eingetragen werden kann).
◆
Damit diese CFmomDoc-Methoden aus der Dialog-Klasse heraus aufgerufen werden
können, muß ein Pointer auf das aktuelle CFmomDoc-Objekt bekannt sein (dieses
Problem ist bereits in der OnDraw-Methode der Ansichtsklasse aufgetreten, konnte
dort allerdings sehr einfach über den Aufruf der CView-Methode GetDocument
gelöst werden). Hier wird eine Variable pDoc (CFmomDoc-Pointer) in der Klasse
CListDlg angesiedelt, die mit einem Pointer auf das aktuelle Dokument vor dem
Aufruf von DoModal zu initialisieren ist.
Zunächst wird die Pointer-Variable pDoc in die CListDlg-Deklaration eingefügt. Da mit ihr
die Methoden der Dokument-Klasse aufgerufen werden, wird mit einer Änderung des
(einzigen) Konstruktors von CListDlg erzwungen, daß ihr beim Erzeugen eines CListDlgObjektes ein Wert zugewiesen wird:
➪
In der "Visual Workbench" wird die Datei listdlg.h geöffnet. Am Anfang wird die
Deklaration von CFmomDoc eingefügt, die Pointer-Variable pDoc wird private
deklariert, und der Prototyp des Konstruktors wird erweitert:
368
J. Dankert: C++-Tutorial
class CFmomDoc ;
class CListDlg : public CDialog
{
private:
CFmomDoc* pDoc ;
// Construction
public:
CListDlg (CFmomDoc* pDocument , CWnd* pParent = NULL) ;
// standard constructor
// ...
} ;
➪
In der "Visual Workbench" wird die Datei listdlg.cpp geöffnet. Am Beginn wird mit
einer #include-Anweisung die Header-Datei der Dokument-Klasse zusätzlich
eingebunden, der vom "Class Wizard" angelegte Konstruktor wird erweitert:
// listdlg.cpp : implementation file
//
#include
#include
#include
#include
"stdafx.h"
"fmom.h"
"listdlg.h"
"fmomdoc.h"
#ifdef _DEBUG
#undef THIS_FILE
static char BASED_CODE THIS_FILE[] = __FILE__;
#endif
////////////////////////////////////////////////////////////////////////
// CListDlg dialog
CListDlg::CListDlg (CFmomDoc* pDocument , CWnd* pParent /*=NULL*/)
: CDialog(CListDlg::IDD, pParent)
{
//{{AFX_DATA_INIT(CListDlg)
// NOTE: the ClassWizard will add member initialization here
//}}AFX_DATA_INIT
pDoc = pDocument ;
}
Schließlich wird auch gleich das vom "Class Wizard" bereits angelegte Gerüst der
Methode CListDlg::OnInitDialog wie geplant ergänzt:
BOOL CListDlg::OnInitDialog()
{
CDialog::OnInitDialog();
CListBox* pListBox = (CListBox*) GetDlgItem (IDC_AREA_LIST) ;
int nr = pDoc->GetAreaCount () ;
for (int i = 1 ; i <= nr ; i++)
pListBox->InsertString (- 1 , pDoc->GetAreaDesc (i)) ;
// ... und die - 1 als erstes Argument legt fest, dass der String
//
jeweils am Ende der Eintragungen eingefuegt wird
pListBox->SetCurSel (0) ;
return TRUE;
}
// ... und die erste Zeile der "List Box"
//
ist beim Erscheinen "selektiert"
// return TRUE
unless you set the focus to a control
J. Dankert: C++-Tutorial
◆
369
Das Besorgen eines Pointers auf die "List Box" folgt der gleichen Strategie, die
bereits in CListDlg::OnAreaDelete und CListDlg::OnAreaEdit verwendet wurde.
Die beiden in CListDlg::OnInitDialog aufgerufenen CListBox-Methoden dienen zum
Einsetzen einer Zeile (String) in die "List Box" (CListBox::InsertString) und dem
Selektieren einer Zeile der "List Box" (CListBox::SetCurSel), die "0-basiert"
numeriert sind).
Nun werden die beiden Methoden der Dokument-Klasse, die aus CListDlg::OnInitDialog
gerufen werden, in CFmomDoc ergänzt:
➪
In der "Visual Workbench" wird die Datei fmomdoc.h geöffnet, im public-Bereich
der Deklaration der Klasse CFmomDoc werden die beiden Prototypen der zu
schreibenden Methoden ergänzt:
class CFmomDoc : public CDocument
{
// ...
public:
// Zugriffsmethoden auf die doppelt verkettete Liste m_areaList:
int
GetAreaCount
() ;
CString GetAreaDesc
(int nr) ;
// ...
} ;
◆
Für den Return-Wert der Methode CFmomDoc::GetAreaDesc wurde ein Objekt der
Klasse CString gewählt, um die Vorteile des Arbeitens mit diesen MFC-Objekten zu
demonstrieren.
➪
In der "Visual Workbench" wird die Datei fmomdoc.cpp geöffnet, der Code für die
beiden Methoden wird hinzugefügt (Erläuterungen findet man anschließend):
int CFmomDoc::GetAreaCount ()
{
return m_areaList.GetCount () ;
}
CString CFmomDoc::GetAreaDesc (int nr)
{
CArea *pArea ;
if (m_areaList.IsEmpty ()) return "" ;
POSITION pos = GetFirstAreaPos () ;
for (int i = 1 ; i <= nr ; i++)
pArea = GetNextArea (pos) ;
return pArea->get_area_desc () ;
}
◆
Die von CFmomDoc::GetAreaCount abzuliefernde Anzahl der Teilflächen und
Ausschnitte entspricht der Länge der verketteten Liste, die mit der Methode
CObList::GetCount erfragt werden kann.
◆
Für die Ermittlung des i-ten Pointers der Liste wird die bereits mehrfach verwendete
Strategie mit den CObList-Methoden GetFirstAreaPos und GetNextArea verwendet.
Um die Beschreibung der speziellen Fläche aus der Datenstruktur zu holen, bietet sich
wieder eine virtuelle CArea-Methode an, die nur in den von CArea abgeleiteten
370
J. Dankert: C++-Tutorial
Klassen definiert wird. Der Return-Wert dieser Methode CArea::get_area_desc ist
ebenfalls vom Typ CString.
Bevor die Methode get_area_desc in CArea deklariert und in den abgeleiteten Klassen
definiert wird, sind noch einige Bemerkungen über die Klasse CString angebracht:
◆
Ein Objekt der CString-Klasse darf man sich als ein "Array of Characters" vorstellen,
das im Gegensatz zu diesem Typ in der Sprache C (theoretisch) keine feste Grenze
hat (praktisch ist die Obergrenze mit 32766 Zeichen festgelegt). Ein CString-Objekt
"wächst" bei Operationen (wie z. B. beim Verbinden mit einem anderen CStringObjekt) entsprechend den Anforderungen, der Programmierer muß sich darum nicht
kümmern. Die meisten Operationen, die in der Programmiersprache C mit den
Funktionen der String-Library (vgl. Abschnitt 5.1) ausgeführt werden, können für
CString-Objekte mit sinnvoll überladenen Operatoren erledigt werden.
◆
Die Anweisung
CString
area_desc = "Teilfläche" ;
erzeugt ein CString-Objekt area_desc, das den String "Teilfläche" enthält, wobei ein
Konstruktor verwendet wird, der kein Argument übernimmt. Anschließend wird dem
Objekt area_desc mit dem überladenen Operator = ein Wert zugewiesen (hier ein
"normaler" String).
Auch der Konstruktor CString::CString ist mehrfach überladen. Gleichwertig mit der
oben angegebenen "Definition mit anschließender Wertzuweisung" wäre mit
CString
area_desc ("Teilfläche") ;
eine Definition, bei der die Initialisierung von einem der CString-Konstruktoren
erledigt wird.
◆
CString-Objekte dürfen mit "normalen" Strings gemischt werden, natürlich muß bei
überladenen Operatoren mindestens ein Operand ein CString-Objekt sein (sonst
würde der Compiler die besondere Bedeutung des Operators nicht erkennen, vgl.
Abschnitt 12.5).
◆
CString-Objekte dürfen als Argumente in Funktions-Aufrufen dort stehen, wo ein
Pointer auf eine Zeichenketten-Konstante erwartet wird (formaler Parameter vom Typ
const char*). Deshalb funktioniert auch die in der Methode CListDlg::OnInitDialog
programmierte Übergabe des CString-Objekts, das von CFmomDoc::GetAreaDesc
als Return-Wert geliefert wird, an die Methode CListBox::InsertString, die auf
dieser Position einen Wert vom Typ const char* erwartet.
Bitte beachten: CFmomDoc::GetAreaDesc und CArea::get_area_desc liefern
CString-Objekte als Return-Werte ab, nicht etwa Pointer auf diese Objekte (Originalton der Microsoft-Dokumentation: "CString Objects Are Values").
➪
In der "Visual Workbench" wird die Datei areas.h geöffnet, im public-Bereich der
CArea-Deklaration wird folgende Zeile ergänzt:
virtual CString get_area_desc () = 0 ;
... zeigt an, daß diese Methode in der Klasse CArea nicht definiert wird. In den
public-Bereichen der beiden aus CArea abgeleiteten Klassen CCircle und CRectan-
371
J. Dankert: C++-Tutorial
gle wird dagegen folgende Zeile eingetragen:
CString get_area_desc () ;
➪
In der "Visual Workbench" wird die Datei circle.cpp geöffnet, es wird die Methode
CCircle::get_area_desc ergänzt:
CString CCircle::get_area_desc ()
{
CString area_desc = "Kreis " ;
if (m_area_or_hole == 1) area_desc += "(Teilfläche), " ;
else
area_desc += "(Ausschnitt), " ;
area_desc += "Mittelpunkt: " + get_point_string (get_xc () ,
get_yc ()) ;
area_desc += ", Durchmesser: " ;
char
szBuffer [20] ;
sprintf (szBuffer , "%g" , m_d) ;
area_desc += szBuffer ;
return area_desc ;
}
◆
An das erzeugte CString-Objekt wird mit dem überladenen Operator += ein "normaler" String angehängt.
Das Problem, die beiden als double-Werte gegebenen Punkt-Koordinaten in einen
String umzuwandeln, wird noch mehrfach auftauchen. Deshalb wird dafür eine
Methode in CArea eingebaut (wird nachfolgend gelistet), die von allen abgeleiteten
Klassen geerbt wird. Sie liefert einen CString ab, so daß der überladene Operator +
genutzt werden kann, um an die "normale" String-Konstante "Mittelpunkt: " diesen
CString anzukoppeln. Diese Methode CArea::get_point_string wird zunächst
nachgeliefert, sie muß natürlich auch in der CArea-Deklaration angegeben werden:
➪
In der "Visual Workbench" wird die Datei areas.h geöffnet, im public-Bereich der
CArea-Deklaration wird folgende Zeile ergänzt:
CString get_point_string (double x , double y) ;
Nun wird die Datei area.cpp geöffnet, in der diese Methode implementiert wird:
CString CArea::get_point_string (double x , double y)
{
char
szBuffer [20] ;
CString point_string = "(" ;
sprintf (szBuffer , "%g" , x) ;
point_string += szBuffer ;
point_string += " ; " ;
sprintf (szBuffer , "%g" , y) ;
point_string += szBuffer ;
point_string += ")" ;
return point_string ;
}
◆
Hier wird noch einmal der Unterschied des Klassen-Objekts vom Typ CString zu
einem "normalen" String deutlich: Das CString-Objekt point_string ist zwar nur lokal
in der Methode get_point_string gültig, aber als Return-Wert wird der Wert
372
J. Dankert: C++-Tutorial
abgeliefert, kein Pointer (bei einem "normalen" C-String, der in einem lokalen "Array
of Characters" gespeichert wäre, ist das leider nicht von allen C-Compilern monierte
Abliefern des Pointers als Return-Wert natürlich mehr als kritisch, weil dann der
Pointer auf einen nicht mehr gültigen Speicherbereich zeigt).
Die für die Klasse CRectangle noch zu schreibende Methode get_area_desc kann nun auch
auf die Methode CArea::get_point_string zurückgreifen:
➪
In der "Visual Workbench" wird die Datei rectangle.cpp geöffnet, in der folgende
Methode ergänzt wird:
CString CRectangle::get_area_desc ()
{
CString area_desc = "Rechteck " ;
if (m_area_or_hole == 1) area_desc += "(Teilfläche), " ;
else
area_desc += "(Ausschnitt), " ;
area_desc +=
"Punkt 1: " + get_point_string (m_p1.get_x () ,
m_p1.get_y ()) ;
area_desc += ", Punkt 2: " + get_point_string (m_p2.get_x () ,
m_p2.get_y ()) ;
return area_desc ;
}
Damit stehen alle Funktionen bereit, die für das Initialisieren und das Beenden der im
Abschnitt 14.4.18 definierten Dialog-Box benötigt werden. Diese soll nun (wie geplant in der
Methode CFmomDoc::OnListenndernTeilflchenlisten) mit einem DoModal-Aufruf sichtbar
gemacht werden. Die Auswertung des Return-Wertes von DoModal ("Abbrechen" oder
"Ändern" oder "Löschen") wird auf den nächsten Abschnitt verschoben.
Um DoModal aufrufen zu können, benötigt man eine Instanz der Dialog-Klasse (vgl.
Abschnitt 14.4.7). Beim Erzeugen dieser Instanz wird man möglicherweise vom Compiler
daran erinnert, daß eine Methode der Dialog-Klasse (OnInitDialog) auf einen in der Klasse
abgelegten Pointer auf das Dokument (pDoc) zugreifen muß. Da der (einzige) Konstruktor
diesen Pointer benötigt, zahlt sich jetzt unter Umständen diese Sicherheitsvorkehrung aus.
➪
In der "Visual Workbench" wird die Datei fmomdoc.cpp geöffnet, im Kopf der Datei
wird die Header-Datei der Dialog-Klasse zusätzlich eingebunden:
#include "listdlg.h"
In dem bereits vorhandenen Gerüst der Methode OnListenndernTeilflchenlisten wird
eine Instanz der Dialog-Klasse CListDlg erzeugt, mit der DoModal aufgerufen wird:
void CFmomDoc::OnListenndernTeilflchenlisten()
{
CListDlg dlg (this) ;
dlg.DoModal () ;
}
Der this-Pointer (Pointer auf das aktuelle Dokument) muß dem Konstruktor übergeben werden (wer weiß, ob man noch daran gedacht hätte, wenn man z. B. pDoc in
der Dialog-Klasse im public-Bereich angesiedelt hätte, um vor dem DoModal-Aufruf
den Pointer auf das Dokument dieser Variablen zuzuweisen).
J. Dankert: C++-Tutorial
➪
373
Das Projekt wird aktualisiert (z. B. mit Build FMOM.EXE im Menü Project). Nach
dem Programmstart (Execute FMOM.EXE) und Eingabe eines Berechnungsmodells
kann man sich die Liste aller Teilflächen und Ausschnitte ansehen (Angebot
Teilflächen listen im Menü Listen/Ändern). Die drei Buttons am unteren Rand
schließen unterschiedslos
den Dialog, weil der
Return-Wert von DoModal noch nicht ausgewertet wird.
Wenn eine größere
Anzahl von Teilflächen
erzeugt wird, erscheint
der vertikale ScrollBalken am rechten Rand
der "List Box" sofort,
wenn es "zu eng" wird
(nebenstehende Abbildung). Dieser Automatismus funktioniert
leider nicht in analoger
Weise für den (der "List
Box" beim Erzeugen der Dialog-Ressource zugeordneten) horizontalen Scroll-Balken,
obwohl die in den Zeilen stehenden Texte länger sind als die "List Box"-Breite.
Der horizontale Scroll-Balken erscheint, wenn mit der CListBox-Methode SetHorizontalExtent (int cxExtent) eine "List Box"-Breite (Pixel) festgelegt wird, die größer als die
tatsächliche Breite ist. Dann wird das Scrollen über eine damit festgelegte Breite ermöglicht.
Der einfachste Weg (übrigens in vielen kommerziellen Programmen realisiert, auch in der
"Visual Workbench" von MS-Visual C++) ist ein statisches (vom Inhalt der Box unabhängiges) Festlegen eines sehr großen (praktisch kaum je nutzbaren) Scroll-Bereichs. Hier soll ein
"etwas intelligenterer" Weg beschritten werden, mit dem noch eine weitere CString-Methode
demonstriert werden kann:
➪
In der "Visual Workbench" wird die Datei listdlg.cpp geöffnet, die Methode
CListDlg::OnInitDialog wird folgendermaßen modifiziert:
BOOL CListDlg::OnInitDialog()
{
int
len , max_len = 0 ;
CDialog::OnInitDialog();
CListBox* pListBox = (CListBox*) GetDlgItem (IDC_AREA_LIST) ;
int nr = pDoc->GetAreaCount () ;
for (int i = 1 ; i <= nr ; i++)
{
CString line = pDoc->GetAreaDesc (i) ;
len = line.GetLength () ;
if (len > max_len) max_len = len ;
pListBox->InsertString (- 1 , line) ;
}
374
J. Dankert: C++-Tutorial
// ... und die - 1 als erstes Argument legt fest, dass der String
//
jeweils am Ende der Eintragungen eingefuegt wird
TEXTMETRIC tm ;
CClientDC dc (pListBox) ;
dc.GetTextMetrics (&tm) ;
pListBox->SetHorizontalExtent (max_len * tm.tmAveCharWidth) ;
pListBox->SetCurSel (0) ;
return TRUE;
// ... und die erste Zeile der "List Box"
//
ist beim Erscheinen "selektiert"
// return TRUE
unless you set the focus to a control
}
◆
Die CString-Methode GetLength wird benutzt, um die Anzahl der Characters eines
jeden Strings zu ermitteln und schließlich die Länge des längsten Strings zu kennen.
◆
Die Strategie, sich einen "Device Context" für ein
Fenster zu besorgen und mit diesem die "TextMetrik" des eingestellten Fonts zu erfragen, wurde
bereits im Abschnitt 14.4.12 beschrieben. Hier wird
die durch Scrollen erreichbare "List Box"-Breite
schließlich auf das Produkt aus "Zeichenanzahl der
längsten Zeile" und "Mittlere Breite der Zeichen"
eingestellt. Die "List Box" hat danach auch einen
horizontalen Scroll-Balken (nebenstehende Abbildung).
14.4.20
Ändern bzw. Löschen einer ausgewählten Teilfläche
In der Methode CFmomDoc::ListenndernTeilflchenlisten muß noch die Auswertung des
Return-Wertes von DoModal ergänzt werden:
◆
Der Return-Wert 0 ("Abbrechen") soll keine weitere Aktion auslösen.
◆
Ein negativer Return-Wert ("Löschen") soll eine Teilfläche (bzw. einen Ausschnitt)
aus der Datenstruktur entfernen. Dazu kann die CObList-Methode RemoveAt
verwendet werden (natürlich sollte unbedingt auch der Speicherplatz des CAreaObjekts freigegeben werden).
◆
Ein positiver Return-Wert ("Ändern") soll die Änderung der ausgewählten
Teilfläche ermöglichen. Da dafür ein zum Flächentyp "passender" Dialog beginnen
muß, ist diese Aktion wieder ein Kandidat für eine virtuelle CArea-Methode, die nur
in den aus CArea abgeleiteten Klassen definiert wird.
Zunächst wird der Code der Aktions-Routine aus CFmomDoc vorgestellt, der diese Aktionen
auslöst, Erläuterungen werden danach gegeben:
➪
In der "Visual Workbench" wird die Datei fmomdoc.cpp geöffnet. Die Auswertung
des Return-Wertes von DoModal, der durch die Methoden der Klasse CListDlg
bestimmt wird, kann z. B. folgendermaßen codiert werden:
375
J. Dankert: C++-Tutorial
void CFmomDoc::OnListenndernTeilflchenlisten()
{
CListDlg dlg (this) ;
CArea
*pArea ;
int retdlg = dlg.DoModal () ;
if
(retdlg == 0) return ;
// "Abbrechen"
POSITION pos1 , pos2 ;
pos1 = GetFirstAreaPos () ;
for (int i = 1 ; i <= fabs (retdlg) ; i++)
{
pos2 = pos1 ;
pArea = GetNextArea (pos1) ;
}
// ... und pos2 ist der POSITION-Wert der ausgewaehlten
//
Teilflaeche, pArea der Pointer auf das Objekt
if (retdlg > 0)
{
pArea->edit_area () ;
}
else
{
m_areaList.RemoveAt (pos2) ;
delete pArea ;
}
UpdateAllViews (NULL)
// "Aendern"
// "Loeschen
// Entfernen aus Liste
// Loeschen des Objekts
;
}
◆
Besonders sorgfältig muß beim Löschen eines Elements, das an beliebiger Stelle der
Liste stehen kann, vorgegangen werden: Für die CObList-Methode RemoveAt wird
der POSITION-Wert benötigt, für die Freigabe des Speicherplatzes des CAreaObjektes wird der in der Liste zu löschende Pointer ein letztes Mal gebraucht. Weil
beim Zugriff auf ein Listenelement der POSITION-Parameter immer schon auf den
Wert für den Nachfolger geändert wird, muß mit zwei Variablen gearbeitet werden.
Die für das Ändern der ausgewählten Teilfläche vorgesehene Methode edit_area wird als
virtuelle Methode in der Klasse CArea (mit dem Zusatz = 0) deklariert und in den aus
CArea abgeleiteten Klassen CCircle und CRectangle definiert:
➪
In der "Visual Workbench" wird die Datei areas.h geöffnet. Im public-Bereich der
Deklaration der Klasse CArea wird folgende Zeile eingefügt:
virtual void edit_area () ;
In den public-Bereichen der Deklarationen der Klassen CCircle und CRectangle
wird jeweils folgende Zeile eingefügt:
void edit_area () ;
➪
In der "Visual Workbench" wird die Datei circle.cpp geöffnet. Die Methode
CCircle::edit_area kann z. B. wie folgt codiert werden:
void CCircle::edit_area ()
{
CCircleDlg dlg ;
dlg.m_radio = (get_aoh () == 1) ? 0 : 1 ;
dlg.m_mpx
= get_xc () ;
dlg.m_mpy
= get_yc () ;
376
J. Dankert: C++-Tutorial
dlg.m_d
= m_d ;
if (dlg.DoModal () == IDOK)
{
set_aoh ((dlg.m_radio == 0) ? 1 : -1) ;
m_d = dlg.m_d ;
m_mp.set_x (dlg.m_mpx) ;
m_mp.set_y (dlg.m_mpy) ;
}
}
◆
In CCircle::edit_area wird ein Objekt der Dialog-Klasse CCircleDlg erzeugt, wie es
schon für die Eingabe einer Kreisfläche (in CFmomDoc::OnStandardflcheKreis)
verwendet wurde. Der Rest der Methode ist geradezu ein klassisches Beispiel für den
Aufruf eines modalen Dialogs unter Nutzung des DoDataExchange-Mechanismus:
Vor dem DoModal-Aufruf wird die Datenstruktur der Dialog-Klasse "geladen", nach
dem DoModal-Aufruf wird (nach Schließen des Dialogs mit OK) zurückgespeichert.
Weil in CCircle::edit_area ein Objekt der Dialog-Klasse CCircleDlg erzeugt wird, muß
deren Header-Datei eingebunden werden. Weil vom "Class Wizard" dort eine Konstante aus
der Header-Datei für die Ressourcen verwendet wird, muß auch diese zugänglich sein. Hier
wird der gleiche Weg gewählt, den auch der "Class Wizard" realisiert, es wird die Datei
fmom.h eingebunden, die selbst wiederum resource.h inkludiert:
➪
Im Kopf der Datei circle.cpp werden die beiden folgenden Zeilen zusätzlich
eingefügt:
#include "fmom.h"
#include "circledl.h"
Eine entsprechende Aktion wird für die Klasse CRectangle ausgeführt:
➪
In der "Visual Workbench" wird die Datei rectangle.cpp geöffnet, im Kopf der Datei
werden folgende Zeilen ergänzt:
#include "fmom.h"
#include "rectdlg.h"
Außerdem wird die folgende Methode hinzugefügt:
void CRectangle::edit_area ()
{
CRectDlg dlg ;
dlg.m_radio
dlg.m_p1x =
dlg.m_p1y =
dlg.m_p2x =
dlg.m_p2y =
= (get_aoh
m_p1.get_x
m_p1.get_y
m_p2.get_x
m_p2.get_y
()
()
()
()
()
== 1) ? 0 : 1 ;
;
;
;
;
if (dlg.DoModal () == IDOK)
{
set_aoh ((dlg.m_radio == 0) ? 1 : -1) ;
m_p1.set_x (dlg.m_p1x) ;
m_p1.set_y (dlg.m_p1y) ;
m_p2.set_x (dlg.m_p2x) ;
m_p2.set_y (dlg.m_p2y) ;
}
}
Damit stehen alle benötigten Funktionen bereit. Das Projekt kann aktualisiert werden. Der
erreichte Stand des Projekts gehört zum Tutorial als Version "fmom18".
J. Dankert: C++-Tutorial
14.4.21
377
Sortieren in einer CObList-Klasse
Die nebenstehende Abbildung zeigt
einerseits die Möglichkeiten, die sich
durch das gezielte Löschen einzelner
Teilflächen ergeben, demonstriert
allerdings auch ein Problem, das
bisher nicht auftrat, weil man sinnvollerweise wohl immer erst dann
einen Ausschnitt definiert, wenn eine
Teilfläche existiert, aus der man
etwas ausschneiden kann:
Die links oben dargestellte Kreisfläche mit 17 Ausschnitten (gehört als
Datei area1.fmo zur Version
"fmom18" des Tutorials) soll so
verändert werden, daß eine Quadratfläche mit den gleichen Ausschnitten
entsteht (als Dokument AREA4.FMO
rechts unten zu sehen). Das ist mit
der Version "fmom18" nicht schwierig zu realisieren:
Man wählt Listen/Ändern und Teilflächen listen, selektiert die erste Fläche (Kreis mit dem
Durchmesser 2000) und klickt auf den Button Löschen. Die verbleibenden "Fläche besteht
nur noch aus Löchern" (als Dokument AREA2.FMO oben rechts zu sehen). Nach Auswahl
von Standardfläche und Rechteck wird das Quadrat durch die beiden Punkte (-1000;-1000)
und (1000;1000) definiert, nach Anklicken des OK-Buttons werden zwar für die "Quadratfläche mit 17 Ausschnitten" die Ergebnisse richtig berechnet, in der graphischen Darstellung
sind allerdings die Ausschnitte nicht mehr zu sehen, weil sie von dem Quadrat verdeckt
werden (im Bildschirm-Schnappschuß unten links als Dokument AREA3.FMO zu sehen).
Der Grund für diesen Mangel ist klar: Eine neue Fläche wird immer an des Ende der Liste
gesetzt (mit CObList::AddTail in CFmomDoc::NewArea) und damit als letzte Fläche
gezeichnet. Der Mangel wäre zu beheben, indem man prizipiell Teilflächen mit AddHead
und Ausschnitte mit AddTail einfügt. Das hätte aber den erheblichen Nachteil, daß eine
falsch eingegebene Teilfläche, die innerhalb vorher eingegebener Flächen liegen würde, sofort
verdeckt wäre, z. B.: Man gibt einen Kreis mit dem Durchmesser 200 ein, aus dem man
einen Kreis mit dem Durchmesser 100 ausschneiden will, vergißt aber das Umschalten auf
"Ausschnitt". Dann ist der kleinere Kreis nur zu sehen, weil er als letzter gezeichnet wird,
was also unbedingt beibehalten werden sollte.
Hier soll dem Programm-Benutzer die Möglichkeit gegeben werden, durch Auswahl eines
entsprechenden Menü-Punktes das Umordnen der Teilflächen nur bei Bedarf selbst veranlassen zu können, so daß unverändert eine neue Fläche am Ende der Liste plaziert (und
damit als letzte gezeichnet) wird.
Das Realisieren des Menü-Angebots "Umordnen" soll als Wiederholung für bereits behandelte Themen dienen.
378
J. Dankert: C++-Tutorial
Aufgabe 14.6:
Mit dem "App Studio" ist das Menü IDR_FMOMTYPE zu erweitern, mit
dem "App Wizard" ist das Gerüst der zugehörigen Methode zu erzeugen:
a)
Das Popup-Menü Listen/Ändern ist um das Angebot Umordnen zu erweitern, als
Prompt ist folgender Text vorzusehen: Ordnet alle Ausschnitte am Ende der Liste
an. Das Erzeugen des Identifikators ID: ist dem "App Studio" zu überlassen.
b)
Mit dem "Class Wizard" ist das Gerüst einer Methode OnListenndernUmordnen in
der Klasse CFmomDoc zu erzeugen.
c)
Die Methode CFmomDoc::OnListenndernUmordnen kann z. B. folgendermaßen
codiert werden (Teilflächen werden an den Kopf der Liste verschoben, es müssen
zwei POSITION-Werte verwendet werden, weil die Methode GetNextArea das
POSITION-Argument schon auf den Nachfolgewert setzt, der aktuelle POSITIONWert aber noch für das Löschen mit RemoveAt benötigt wird):
void CFmomDoc::OnListenndernUmordnen()
{
CArea
*pArea ;
POSITION pos1 , pos2 ;
pos1 = GetFirstAreaPos () ;
while (pos1 != NULL)
{
pos2 = pos1 ;
pArea = GetNextArea (pos1) ;
if (pArea->get_aoh () == 1)
{
m_areaList.RemoveAt (pos2) ;
m_areaList.AddHead (pArea) ;
}
// Entfernen aus Liste
// und Einsetzen am
// Listenanfang
}
UpdateAllViews (NULL) ;
}
d)
Das Projekt ist zu aktualisieren und zu testen.
Der mit der Realisierung der Aufgabe 19.6 erreichte Stand des Projekts gehört zum
Tutorial als Version "fmom19".
14.4.22
Eine Klasse für die Berechnung von Polygon-Flächen
In diesem Abschnitt sollen einige Voraussetzungen geschaffen werden, um das "fmom"Projekt so zu erweitern, daß auch durch beliebige Polygone berandete Flächen berechnet
werden können. Dabei werden einige Themen wiederholt, die in vorangegangenen Abschnitten bereits behandelt wurden (deshalb werden verschiedene Schritte als Aufgaben
formuliert). Da ein Polygon durch eine (erst bei der Eingabe festzulegende) Anzahl von
Punkten definiert wird, bietet sich die Speicherung in einer verketteten Liste (Objekt der
Klasse CObList) an, so daß auch diese Strategie (bisher schon für das Speichern der Flächen
verwendet) noch einmal vertieft wird.
Dem Leser, der zur Übung alle (oder wenigstens einige) Schritte mit dem Computer selbst
nacharbeiten möchte, wird wegen der etwas umfangreicheren Programmteile, die zu schreiben
J. Dankert: C++-Tutorial
379
sind, empfohlen, auf die Bausteine zurückzugreifen, die in der jeweiligen Vorgängerversion
des "fmom"-Projektes bereits bereitgestellt werden (es werden auch nur noch die Teile hier
gelistet, die für das Verständnis besonders wichtig sind). Bereits in der (mit dem vorangegangenen Abschnitt erzeugten) Version "fmom19" finden sich einige Dateien, die mit der
Extension .p20 ("Puzzle" für Version "fmom20") gekennzeichnet sind. Wer also Version
"fmom20" selbst erzeugen möchte, sollte Version "fmom19" in ein spezielles Verzeichnis
kopieren, den nachfolgend jeweils mit ➪ eingeleiteten Anweisungen folgen und dabei
gegebenenfalls die .p20-Dateien benutzen.
Zunächst wird eine Klasse CPolygon eingerichtet, die (wie die bereits extierenden Klassen
CCircle und CRectangle) aus CArea abgeleitet wird und natürlich mit den (in CArea nicht
definierten aber deklarierten) virtuellen Methoden ausgestattet werden muß. Dazu gehören die
(sämtlich mit get_ eingeleiteten) "Berechnungs"-Methoden. Bevor der entsprechend
vorbereitete Programm-Baustein erläutert wird, soll der (für das Verständnis der C++Programmierung eigentlich unwesentliche) theoretische Hintergrund für die Berechnung von
Polygonflächen gegeben werden.
Einige Formeln wurden bereits im Abschnitt 12.3 angegeben. Dort wurde darauf aufmerksam
gemacht, daß der Umlaufsinn, mit dem die Punkte numeriert werden, beachtet werden muß
und daß die Formeln auch für zusammengesetzte Flächen und mehrfach zusammenhängende
Bereiche gelten. Diese für einen Programm-Benutzer eher verwirrenden Möglichkeiten
werden dahingehend vereinfacht, daß jede Teilfläche und jede Ausschnittfläche als eigenständiges Polygon definiert werden müssen. Um ihm auch das Beachten eines bestimmten
Umlaufsinns bei der Punkt-Eingabe nicht zumuten zu müssen, wird (wie schon für Kreise
und Rechtecke) nur der Zustand der "Radiobuttons" ("Teilfläche" bzw. "Ausschnitt") für die
Vorzeichen-Entscheidung herangezogen. Das bedeutet, daß die vorzeichenbehafteten Werte,
die von den nachfolgend angegebenen Formeln geliefert werden, nachträglich nach der
angegebenen Vorschrift korrigiert werden müssen.
Für eine Fläche, die von einem geschlossenen Polygon begrenzt wird (die Skizze zeigt ein
Polygon, das durch 6 Punkte definiert wird), gelten folgende Formeln (vgl. "Dankert/Dankert:
Technische Mechanik, computerunterstützt", Seite 219):
380
J. Dankert: C++-Tutorial
Für den Punkt n+1, dessen Koordinaten bei einem Polygon mit n Punkten in den Formeln
benötigt wird, müssen die Koordinaten des Startpunktes (Punkt 1) noch einmal verwendet
werden. Dies wird in der Klasse CPolygon so realisiert, daß nach der Eingabe eines Polygons
ein zusätzlicher Punkt mit den gleichen Koordinaten wie der erste Punkt an die Punktliste
angehängt wird.
Die Datenstruktur, die das Polygon beschreibt, besteht nur aus einer verketteten Liste der
Klasse COblist (wie die in CFmomDoc angesiedelte "Liste der Flächen"):
CObList m_pointList ;
... nimmt eine Liste von Pointern auf Objekte der Klasse CPoint_xy auf. Dafür ist es
erforderlich, daß die Klasse CPoint_xy von der Basisklasse CObject abgeleitet wird (vgl.
Abschnitt 14.4.4):
➪
In der "Visual Workbench" wird die Datei areas.h geöffnet, in der Deklaration der
Klasse CPoint_xy werden folgende Änderungen vorgenommen:
class CPoint_xy : public CObject
{
protected:
DECLARE_SERIAL (CPoint_xy)
// ...
void
Serialize (CArchive &ar) ;
}
Der Einbau des DECLARE_SERIAL-Makros dient dazu, daß die Punkte aus der Liste
m_pointList automatisch in den "Serialization"-Prozeß einbezogen werden. Mit der
Ableitung der Klasse von CObject, dem Einbau des DECLARE_SERIAL-Makros, dem
bereits vorhandenen Konstruktor, der keine Argumente erwartet, und der bereits seit der
Version "fmom11" existierenden Methode CPoint_xy::Serialize (diese überschreibt nun die
von CObject ererbte Methode gleichen Namens) sind schon 4 der im Abschnitt 14.4.11
aufgeführten 5 Bedingungen für die "Serialization" der Klassen-Objekte erfüllt, es muß nur
noch das Makro IMPLEMENT_SERIAL in der Datei point_xy.cpp untergebracht werden:
➪
Die Datei point_xy.cpp wird geöffnet, am Ende der Datei wird die Zeile
IMPLEMENT_SERIAL (CPoint_xy , CObject , 3)
eingefügt (die 3 auf der Position der "Versions-Nummer" kennzeichnet die neue
Version der zu schreibenden Binär-Dateien, die von "alten" Programmversionen
natürlich nicht gelesen werden können).
Für den Entwurf der Klasse CPolygon wird eine gegenüber den Klassen CCircle und
CRectangle geänderte Strategie verfolgt. Weil die Auswertung des Formelsatzes für die
Polygon-Berechnung doch einen erheblichen Aufwand erfordert, soll diese tatsächlich nur
dann erfolgen, wenn das Polygon erzeugt bzw. geändert wurde. Es werden deshalb für alle
Ergebnisse der Berechnung (Fläche, Schwerpunkt-Koordinaten, Flächen-Trägheitsmomente)
Klassen-Variablen vorgesehen, zusätzlich wird in einer Variablen m_uptodate vermerkt, ob
die gespeicherten Werte dem aktuellen Zustand des Polygons entsprechen, und gerechnet
wird nur dann, wenn dies nicht der Fall ist.
Die Deklaration der neuen Klasse CPolygon wird in die Datei areas.h eingefügt, wo sich ja
bereits die anderen Klassen-Deklarationen befinden, mit denen die Flächen-Objekte erzeugt
werden:
381
J. Dankert: C++-Tutorial
➪
In der "Visual Workbench" wird die Datei areas.h geöffnet. Folgende KlassenDeklaration, die man in der Version "fmom19" als Datei areas.p20 findet (auch diese
Datei sollte geöffnet werden), wird in die Datei areas.h kopiert:
class CPolygon : public CArea
{
protected:
DECLARE_SERIAL (CPolygon)
private:
CObList
m_pointList ;
double
double
double
double
double
double
m_a
m_xc
m_yc
m_ixx
m_iyy
m_ixy
;
;
;
;
;
;
int
int
void
void
m_uptodate
;
Calculate () ;
copy_points (CPolygon* source , CPolygon* target) ;
delete_polygon () ;
public:
CPolygon () ;
~CPolygon () ;
void
void
void
int
void
CString
append_point
(CPoint_xy *p_point) ;
insert_point_before (CPoint_xy *p_point , int nr) ;
close_polygon
() ;
get_point_count () ;
remove_point
(int nr) ;
get_pt_string
(int nr) ;
double
double
double
double
double
double
void
get_a
() ;
get_xc
() ;
get_yc
() ;
get_Ixx
() ;
get_Iyy
() ;
get_Ixy
() ;
get_area_min_max (double &xmin , double &ymin ,
double &xmax , double &ymax) ;
void
draw_area (CGrInt *grint) ;
CString get_area_desc () ;
void
edit_area
() ;
virtual void Serialize (CArchive &ar) ;
} ;
◆
Auch in der Deklaration der Klasse CPolygon wurden die Vorbereitungen für die
"Serialization" getroffen. Implementiert werden müssen noch der Konstruktor, das
IMPLEMENT_SERIAL-Makro und die Methode CPolygon::Serialize, um allen im
Abschnitt 14.4.11 aufgelisteten Forderungen gerecht zu werden.
◆
Die Methode Calculate ist private, sie wird nur innerhalb der Klasse zur Berechnung
der Ergebnisse benutzt, wenn ein Wert über die public-Methoden angefordert wird
und die gespeicherten Werte nicht aktuell sind. Die anderen als private deklarierten
Methoden copy_points und delete_polygon sind nur von den Methoden der Klasse
genutzte Hilfs-Funktionen.
382
J. Dankert: C++-Tutorial
◆
Neben den 10 Methoden, die in der Basisklasse CArea deklariert (aber nicht
definiert) sind, wurden noch 6 weitere public-Methoden deklariert, mit denen ein
Polygon erzeugt, verändert und gelistet werden kann.
Der Code für alle Methoden der Klasse CPolygon befindet sich in der Datei polygon.cpp,
die man bereits in der Version "fmom19" findet (dort wird sie allerdings nicht genutzt), diese
muß nur zum Projekt hinzugefügt werden. Das soll aber noch unterbleiben, weil sie mit der
erst noch zu erzeugenden Dialog-Klasse korrspondieren muß. Aber das Arbeitsprinzip einiger
Methoden soll schon kurz vorgestellt werden:
◆
In den Methoden, die ein Polygon erzeugen bzw. verändern (Konstruktor, append_point, insert_point_before, remove_point, close_polygon und delete_polygon), wird
m_uptodate = 0 ;
gesetzt.
◆
In den Methoden, die ein berechnetes Ergebnis (Fläche, Schwerpunkt-Koordinaten,
Flächen-Trägheitsmomente) zurückgeben sollen, wird zunächst die private-Methode
Calculate gerufen, danach wird der Return-Wert abgeliefert, z. B.:
double CPolygon::get_Ixx () { Calculate () ; return m_ixx ; }
Die Methode Calculate überprüft den Wert von m_uptodate und führt die komplette
Berechnung nur aus, wenn dieser gleich 0 ist.
Die Klasse für die "Basisarbeit" zur Behandlung von Polygon-Flächen existiert nun. Um sie
zu benutzen, wird im folgenden Abschnitt der Weg vom Menü über eine Dialog-Box und
eine Dialog-Klasse zum Erzeugen einer Instanz von CPolygon realisiert.
14.4.23
Ressourcen für die Eingabe der Polygon-Fläche
Die Bearbeitung des Menüs mit "App Studio" wurde im Abschnitt 14.4.5 ausführlich
beschrieben, das Erzeugen einer Dialog-Box und der zugehörigen Klasse im Abschnitt 14.4.6.
Das Einbinden des Dialogs in das Programm wurde im Abschnitt 14.4.7 behandelt. Deshalb
werden diese Schritte für die Polygon-Fläche als Aufgaben formuliert, mit denen sich der
Leser trainieren sollte (aber selbstverständlich gehört das Ergebnis als neue Version zum
Tutorial).
Aufgabe 14.7:
Mit dem "App Studio" ist das Menü IDR_FMOMTYPE zu erweitern,
mit "App Wizard" ist das Gerüst der zugehörigen Methode zu erzeugen:
a)
Das Popup-Menü Standardfläche ist um das Angebot Polygon zu erweitern, als
Prompt ist folgender Text vorzusehen: Definition eines geschlossenen Polygons. Das
Erzeugen des Identifikators ID: ist dem "App Studio" zu überlassen.
b)
Mit dem "Class Wizard" ist das Gerüst einer Methode OnStandardflchePolygon in
der Klasse CFmomDoc zu erzeugen, die beim Anklicken des unter a) eingerichteten
Menü-Angebots aufgerufen wird.
J. Dankert: C++-Tutorial
383
Mit dem "App Studio" ist eine Dialog-Box für die Eingabe und das
Ändern einer Polygon-Fläche zu erzeugen. Sie sollte etwa das Aussehen
haben wie die nebenstehende Abbildung. Die Eingabe der Punkte erfolgt
über die beiden mit x = bzw. y =
gekennzeichneten Felder (die Überschrift Punkt 1 soll sich während der
Eingabe sinngemäß ändern), alle
bereits eingegebenen Punkte erscheinen in der "List Box" (links oben).
Aufgabe 14.8:
Ein gelisteter Punkt soll selektiert und
gelöscht werden können. Der dafür
vorgesehene Button wird erst aktiv,
wenn ein Punkt selektiert ist, entsprechendes gilt für den Button, mit dem
ein Punkt an beliebiger Stelle eingefügt werden kann. Auch der Button
Polygon schließen, mit dem die
Eingabe beendet wird, ist anfangs
nicht aktiv.
Wenn Sie sich bei den Identifikatoren
an die Vorschläge der Aufgabenstellung halten, wird die Dialog-Box
"kompatibel" zu den Klassen, die in
den nachfolgenden "fmom"-Versionen
des Tutorials zu finden sind:
◆
Die Gesamt-Box sollte Polygon als Caption und IDD_POLYGON als ID: haben.
◆
Die "List Box" links oben soll den Identifikator IDD_LIST_POLYGON bekommen,
unter "Styles" ist zusätzlich zum voreingestellten vertikalen Scroll-Balken auch Horiz.
Scroll "anzukreuzen".
◆
Der mit Punkt 1: als Caption: vorgesehene "Static Text" erhält den Identifikator
IDC_STATIC_POINT, mit dem er aus der Dialog-Klasse angesprochen und
verändert werden soll.
◆
Die beiden "Edit Boxes" für die Koordinaten-Eingaben erhalten die Identifikatoren
IDC_EDIT_POLYGON_PX bzw. IDC_EDIT_POLYGON_PY.
◆
Die beiden Buttons Punkt eingeben und Abbrechen erhalten die Identifikatoren
IDC_INPUT_POINT bzw. IDCANCEL, die übrigen Voreinstellungen werden
akzeptiert. Die Buttons Polygon schließen, Selektierten Punkt löschen und Punkt
vor selektiertem Punkt einfügen erhalten die Identifikatoren ID_POLYGON_OK,
ID_POLYGON_PTDEL bzw. ID_POLYGON_PTINS, bei ihnen wird in der
"Property Page" Disabled "angekreuzt".
◆
Die "Group Box" mit der Überschrift Polygon ist ... und die beiden "Radio Buttons"
384
J. Dankert: C++-Tutorial
in dieser Box können von einer bereits existierenden Dialog-Box kopiert werden
(IDD_DIALOG_KREIS bzw. IDD_DIALOG_RECHTECK), den "Radio Buttons"
werden die Identifikatoren IDC_RADIO_PT ("Teilfläche", bei diesem Button ist
Group "anzukreuzen") bzw. IDC_RADIO_PA ("Ausschnitt") gegeben.
◆
Es ist eine sinnvolle "Tab Order" festzulegen, in der auf jeden Fall auf die beiden
"Edit Boxes" die Buttons Punkt eingeben und Polygon schließen folgen sollten. Für
das Element, das in der "Tab Order" auf den "Radio Button" IDC_RADIO_PA folgt,
ist das Kreuz bei Group in der "Property Page" zu ergänzen.
Aufgabe 14.9:
a)
b)
c)
Für die mit der Aufgabe 14.8 erzeugte Dialog-Box ist mit dem "App
Wizard" eine Dialog-Klasse CPolyDlg einzurichten.
Folgenden Control IDs sind mit dem "Class Wizard" die aufgelisteten "Member
Variables" mit den angegebenen Typen zuzuordnen:
Control IDs:
Type
Member
IDC_EDIT_POLYGON_PX
IDC_EDIT_POLYGON_PY
IDC_RADIO_PT
IDC_STATIC_POINT
double
double
int
CString
m_px
m_py
m_radio
m_ptest
Für folgende Object IDs sind mit dem "Class Wizard" für die aufgelisteten "Messages" die Gerüste der angegebenen "Member Functions" zu erzeugen:
Control IDs:
Messages
Member Functions
CPolyDlg
IDC_INPUT_POINT
ID_POLYGON_OK
ID_POLYGON_PTDEL
ID_POLYGON_PTINS
WM_INIT_DIALOG
BN_CLICKED
BN_CLICKED
BN_CLICKED
BN_CLICKED
OnInitDialog
OnInputPoint
OnPolygonOk
OnPolygonPtdel
OnPolygonPtins
In der Dokument-Klasse (Datei fmomdoc.cpp) ist in dem bereits mit der Aufgabe
14.7 vom "Class Wizard" bereitgestellten Gerüst der Methode OnStandardflchePolygon eine Instanz der Klasse CPolyDlg zu erzeugen, mit der die Methode
DoModal aufzurufen ist
void CFmomDoc::OnStandardflchePolygon()
{
CPolyDlg dlg ;
dlg.DoModal () ;
}
(dazu muß in fmomdoc.cpp die gerade vom "Class Wizard" generierte Header-Datei
polydlg.h inkludiert werden). Danach wird das Projekt aktualisiert.
Zwar steckt hinter den Dialog-Methoden noch keine Funktionalität, man kann sich
jedoch über das mit der Aufgabe 14.7 erzeugte Menü-Angebot die Dialog-Box
wenigstens schon einmal ansehen.
Der mit der Lösung der Aufgaben 14.7 bis 14.9 erreichte Stand gehört zum Tutorial als
Version "fmom20".
385
J. Dankert: C++-Tutorial
14.4.24
Der "Dialog des Programms" mit der Dialog-Box
Während sich die einfachsten Dialoge auf die Aktionen "Initialisieren der Variablen der
Dialog-Klasse" --> "DoModal aufrufen" --> "Auswerten der Variablen der Dialog-Klasse"
beschränken (und den Datentransfer von der "Klasse zur Box und zurück" dem DoDataExchange-Mechanismus überlassen), wurde im Abschnitt 14.4.19 gezeigt, daß eine "List
Box" gesondert initialisiert werden muß. Für die Polygon-Eingabe, bei der jeweils ein
Koordinaten-Paar über die "Edit Boxes" eingegeben wird, muß die Dialog-Klasse den
DoDataExchange-Mechanismus selbst auslösen, um die Koordinaten zu bekommen, diese in
die Datenstruktur einfügen und die "List Box" aktualisieren. Es müssen also Methoden in der
Dialog-Klasse vorgesehen werden, die auf die Botschaften reagieren, die während der
DoModal-Abarbeitung gesendet werden.
In der Dialog-Klasse CPolyDlg wird ein Pointer auf ein Objekt der Klasse CPolygon
ergänzt. Das mit dem Dialog zu bearbeitende Polygon soll über diesen Pointer an die DialogKlasse übergeben bzw. von ihr abgeliefert werden. Er muß vor dem Aufruf von DoModal
initialisiert werden und auf eine Polygon-Datenstruktur zeigen, in die die Methoden der
Dialog-Klasse die Polygon-Koordinaten speichern können. Damit das Initialisieren auf keinen
Fall vergessen werden kann, wird die Parameter-Liste des (einzigen) Konstruktors der Klasse
CPolyDlg um ein entsprechendes Argument ergänzt.
➪
In der "Visual Workbench" wird die Datei polydlg.h geöffnet, die nachfolgend fett
gedruckten Anweisungen werden ergänzt:
// polydlg.h : header file
//
///////////////////////////////////////////////////////////////////////
// CPolyDlg dialog
class CPolygon ;
class CPolyDlg : public CDialog
{
// Construction
public:
CPolyDlg(CPolygon* pPolygon , CWnd* pParent = NULL);
// standard constructor, modifiziert
CPolygon* m_pPolygon ;
// Dialog Data
...
} ;
In der Datei polydlg.cpp muß der Konstruktor entsprechend geändert werden:
CPolyDlg::CPolyDlg(CPolygon* pPolygon , CWnd* pParent /*=NULL*/)
: CDialog(CPolyDlg::IDD, pParent)
{
m_pPolygon = pPolygon ;
//{{AFX_DATA_INIT(CPolyDlg)
m_px = 0;
m_py = 0;
m_radio = -1;
m_ptest = "";
//}}AFX_DATA_INIT
}
386
J. Dankert: C++-Tutorial
Nun funktioniert natürlich die im Punkt c) der Aufgabe 14.9 eingerichtete Methode
CFmomDoc::OnStandardflchePolygon nicht mehr (probieren Sie es aus, der Compiler
meldet sofort, daß er keinen passenden Konstruktor findet). Weil die Polygon-Klasse
CPolygon ohnehin eingebunden werden muß, soll CFmomDoc::OnStandardflchePolygon
gleich ihre endgültige Form erhalten:
➪
In der "Visual Workbench" wird die Datei fmomdoc.cpp geöffnet, die Methode
CFmomDoc::OnStandardflchePolygon wird wie folgt geändert:
void CFmomDoc::OnStandardflchePolygon()
{
CPolygon* pPolygon = new CPolygon ();
CPolyDlg dlg (pPolygon) ;
dlg.m_radio = 0 ;
if (dlg.DoModal () == IDOK)
{
pPolygon->set_aoh ((dlg.m_radio == 0) ? 1 : -1) ;
pPolygon->close_polygon () ;
NewArea
(pPolygon) ;
UpdateAllViews (NULL) ;
}
else
{
delete pPolygon ;
}
}
◆
Nachdem in CFmomDoc::OnStandardflchePolygon zunächst ein Polygon (Instanz
der Klasse CPolygon) erzeugt wird, kann beim Erzeugen der Instanz der DialogKlasse dem Konstruktor der Pointer auf das Polygon übergeben werden. Der Rest
ähnelt dem Auswerten der Dialoge für das Erzeugen von Kreisen bzw. Rechtecken:
Beim Verlassen der Dialog-Box über den Button Polygon schließen wird das Polygon
geschlossen (Methode CPolygon::close_polygon) und in die Liste der Flächen
aufgenommen (NewArea), beim Abbruch des Dialogs wird der Speicherplatz für die
CPolygon-Instanz wieder freigegeben.
Die Methoden der Klasse CPolygon (Datei polygon.cpp) gehören seit der Version "fmom19"
(allerdings ungenutzt) zum Tutorial, die Datei muß nur noch in das Projekt eingebunden
werden:
➪
Im Menü Project wird Edit... gewählt. In der Dialog-Box "Edit - FMOM.MAK" wird
im linken Fenster die Datei polygon.cpp ausgewählt. Nach Klicken auf Add und
Close gehört sie zum Projekt.
Die Methode CPolygon::draw_area ruft eine Methode zum Zeichnen eines "gefüllten
Polygons" auf, die in der Klasse CGrInt ergänzt werden soll. Zur Version "fmom20" gehört
ein Baustein polydraw.p21, der in die Datei grint.cpp eingefügt werden kann:
➪
In der "Visual Workbench" werden die Dateien grint.cpp und polydraw.p21
geöffnet. Die komplette Methode CGrInt::Polygon wird in die Datei grint.cpp
kopiert. Natürlich muß sie auch in der Klassen-Deklaration ergänzt werden: Die Datei
grint.h wird geöffnet, im public-Bereich der Deklaration von CGrInt wird die Zeile
387
J. Dankert: C++-Tutorial
void Polygon (double x [] , double y [] , int npoints) ;
eingefügt. Wie man vermuten kann (und beim Analysieren des Codes natürlich
bestätigt findet), erwartet CGrInt::Polygon zwei double-Arrays mit den Koordinaten
der Polygon-Punkte und einen int-Wert, der die Anzahl der Punkte in diesen Arrays
bestimmt.
Nun müssen noch die als Gerüste vom "Class Wizard" in der Datei polydlg.cpp angelegten
Methoden der Dialog-Klasse mit Leben erfüllt werden. Auch dafür findet man einen
vorbereiteten Baustein, der mühsames Eintippen erspart:
➪
In der "Visual Workbench" werden die Dateien polydlg.cpp und polydlg.p21
geöffnet. Die Gerüste der Methoden OnInitDialog, OnPolygonOk, OnInputPoint,
OnPolygonPtdel und OnPolygonPtins in der Datei polydlg.cpp werden gelöscht und
durch den kompletten Code der Datei polydlg.p21 ersetzt.
Da die geänderten Methoden auf Methoden aus der Klasse CPolygon zugreifen
müssen, ist die Header-Datei areas.h zu inkludieren:
#include "areas.h"
... wird im Kopf der Datei polydlg.cpp ergänzt.
Da die geänderten Methoden in polydlg.cpp Hilfs-Funktionen benutzen, die auch mit dem
Baustein polydlg.p21 übernommen wurden (und natürlich wieder die CDialog-Methode
OnOK überschrieben wird), müssen die entsprechenden Ergänzungen in der Deklaration der
Klasse CPolyDlg vorgenommen werden:
➪
In der "Visual Workbench wird die Datei polydlg.h geöffnet, in der KlassenDeklaration wird folgender private-Bereich ergänzt:
private:
void
void
CString
void
Damit ist der Einbau der Polygon-Berechnung komplett, das
Projekt kann aktualisiert und
getestet werden.
Die nebenstehende Abbildung
zeigt die Eingabe des Beispiels,
das bereits am Ende des Abschnitts 12.3 behandelt wurde
(dort nur für die SchwerpunktBerechnung). Zwei Teilflächen
wurden bereits definiert, mit der
Dialog-Box für die PolygonEingabe sind gerade die drei
Punkte eingeben worden, die
den dreieckigen Ausschnitt
realisieren sollen.
update_listbox () ;
update_dialog () ;
point_headline (int i) ;
OnOK () ;
388
J. Dankert: C++-Tutorial
Es sollen noch einige Erläuterungen zur Realisierung des Eingabe-Dialogs gegeben werden:
◆
Die Methoden der Klasse CPolyDlg wurden so angelegt, daß eine Instanz der Klasse
sowohl von der FmomDoc-Methode OnStandardflchePolygon als auch der
CPolygon-Methode edit_area erzeugt und mit dieser DoModal gerufen werden kann.
In CPolyDlg::OnInitDialog werden zwei Funktionen aufgerufen, die die "List Box"
und den Zustand der Buttons initialisieren:
BOOL CPolyDlg::OnInitDialog()
{
CDialog::OnInitDialog () ;
update_listbox () ;
update_dialog () ;
return TRUE;
// return TRUE
unless you set the focus to a control
}
◆
In update_listbox wird (nur dann, wenn die Methode CPolygon::get_point_count
einen Wert größer 0 für die Anzahl der Punkte liefert) für jeden Punkt eine Zeile in
die "List Box" eingetragen (die Zeile wird mit CPolyDlg::point_headline und
CPolygon::get_pt_string zusammengesetzt, CString-Objekte können mit dem
überladenen Operator + zusammengefügt werden):
void CPolyDlg::update_listbox ()
{
CListBox* pListBox = (CListBox*) GetDlgItem (IDC_LIST_POLYGON) ;
pListBox->ResetContent () ;
// ... loescht den "List Box"-Inhalt
int npoints = p_polygon->get_point_count () ;
for (int i = 1 ; i <= npoints ; i++)
pListBox->InsertString (- 1 , point_headline (i) +
p_polygon->get_pt_string (i)) ;
}
◆
In update_dialog werden die Variablen der Dialog-Klasse initialisiert (auch der String
m_ptext: "Punkt ...:") und die Elemente der Dialog-Box modifiziert. UpdateData
(FALSE) löst den DoDataExchange-Mechanismus aus (Klassen-Variablen ---> Box).
GetDlgItem (aufgerufen mit dem Identifikator des Elements) liefert den Pointer auf
das jeweilige Element, das modifiziert werden soll. Mit GotoDlgCtrl wird der
Eingabefokus gesetzt (auf die "Edit Box" für die Eingabe der x-Koordinate), mit
EnableWindow werden nur die Buttons als "enabled" eingestellt, die entsprechend
der vorhandenen Punkt-Anzahl sinnvollerweise schon angeklickt werden dürfen.
void CPolyDlg::update_dialog ()
{
int npoints = m_pPolygon->get_point_count () ;
m_ptext = point_headline (npoints + 1) ;
m_px
= 0. ;
m_py
= 0. ;
UpdateData (FALSE) ;
GotoDlgCtrl (GetDlgItem (IDC_EDIT_POLYGON_PX)) ;
CWnd* pWnd = GetDlgItem (ID_POLYGON_PTDEL) ;
pWnd->EnableWindow ((npoints > 0) ? TRUE : FALSE) ;
pWnd = GetDlgItem (ID_POLYGON_PTINS) ;
pWnd->EnableWindow ((npoints > 0) ? TRUE : FALSE) ;
pWnd = GetDlgItem (ID_POLYGON_OK) ;
J. Dankert: C++-Tutorial
389
pWnd->EnableWindow ((npoints >= 3) ? TRUE : FALSE) ;
if (npoints > 0)
{
CListBox* pListBox = (CListBox*) GetDlgItem (IDC_LIST_POLYGON) ;
pListBox->SetCurSel (npoints - 1) ;
}
}
◆
Die Methode OnInputPoint ruft UpdateData mit dem Argument TRUE, so daß die
Punkt-Koordinaten auf die Klassen-Variablen übertragen werden, erzeugt ein
CPoint_xy-Objekt und läßt es mit CPolygon::append_point an das Ende der
Datenstruktur anhängen. Danach wird der Punkt in die "List Box" eingetragen,
update_dialog aktualisiert schließlich alle übrigen Elemente der Dialog-Box:
void CPolyDlg::OnInputPoint()
{
UpdateData (TRUE) ;
CPoint_xy* p_point = new CPoint_xy (m_px , m_py) ;
m_pPolygon->append_point (p_point) ;
int npoints = m_pPolygon->get_point_count () ;
CListBox* pListBox = (CListBox*) GetDlgItem (IDC_LIST_POLYGON) ;
pListBox->InsertString (- 1 , m_ptext +
m_pPolygon->get_pt_string (npoints)) ;
pListBox->SetCurSel
(npoints - 1) ;
update_dialog () ;
}
◆
Die Methode OnPolygonPtins arbeitet ähnlich wie OnInputPoint, erfragt zusätzlich
das gerade selektierte Element der "List Box" (GetCurSel), um den Punkt (mit
CPolygon::insert_point_before) mit der gewünschten Position in die Datenstruktur
einfügen zu können. Das Aktualisieren der "List Box" wird einfach mit update_listbox realisiert:
void CPolyDlg::OnPolygonPtins()
{
CListBox* pListBox = (CListBox*) GetDlgItem (IDC_LIST_POLYGON) ;
int isel = pListBox->GetCurSel () ;
if (isel != LB_ERR)
{
UpdateData (TRUE) ;
CPoint_xy* p_point = new CPoint_xy (m_px , m_py) ;
m_pPolygon->insert_point_before (p_point , isel + 1) ;
update_listbox () ;
update_dialog () ;
}
}
◆
Die Strategie des Löschens eines Punktes mit OnPolygonPtdel ist ähnlich zur Arbeit
von OnPolygonPtins, der Punkt wird aus der Polygon-Datenstruktur mit CPolygon::remove_point entfernt.
Der umfangreiche Code der Klasse CPolygon ist weitgehend selbsterklärend (zeigt noch
einmal die bereits behandelte Verwendung der als CObList-Objekt erzeugten "doppelt
verketteten Liste"). Deshalb soll hier nur auf den Destruktor und zwei andere Methoden
speziell aufmerksam gemacht werden:
390
J. Dankert: C++-Tutorial
◆
Der Destruktor CPolygon::~CPolygon ruft eine Methode delete_polygon auf, die alle
Elemente der CObList-Instanz m_pointList löscht und auch den Speicherplatz aller
CPoint_xy-Instanzen freigibt, auf denen die Punkt-Koordinaten gespeichert sind.
CPolygon::~CPolygon ()
{
delete_polygon () ;
}
void CPolygon::delete_polygon ()
{
while (!m_pointList.IsEmpty ())
delete (CPoint_xy*) m_pointList.RemoveHead () ;
m_uptodate = 0 ;
}
◆
Die Methode CPolygon::edit_area arbeitet etwas anders als die gleichnamigen
Methoden in den Klassen CCircle und CRectangle. Um bei einem Abbruch des
Dialogs das "alte Polygon" komplett behalten zu können, wird zunächst eine Kopie
des aktuellen Polygons hergestellt, und der Dialog-Klasse wird nur ein Pointer auf
diese Kopie (via Konstruktor) übergeben. Die beim Anlegen der Kopie benutzte
Hilfsfunktion copy_points erzeugt für jeden Punkt ein CPoint_xy-Objekt, diese
werden beim Löschen des Polygons (im Destruktor, siehe oben) automatisch wieder
gelöscht. Vor dem Aufruf von DoModal wird der letzte Punkt aus der "PolygonKopie" entfernt, so daß der Programm-Benutzer gar nicht merkt, daß beim Erzeugen
eines Polygons mit CPolygon::close_polygon ein zusätzlicher Punkt (mit den
Koordinaten des ersten Punktes) hinzugefügt wurde:
void CPolygon::edit_area ()
{
CPolygon* pPoly = new CPolygon () ;
CPolyDlg dlg (pPoly) ;
// ... nur fuer das Editieren
copy_points (this , pPoly) ;
pPoly->remove_point (m_pointList.GetCount ()) ;
// ... kopiert Punkte, entfernt letzten Punkt
dlg.m_radio = (get_aoh () == 1) ? 0 : 1 ;
if (dlg.DoModal () == IDOK)
{
delete_polygon () ;
// loescht "altes" Polygon
copy_points (pPoly , this) ;
// ... kopiert Punkte
set_aoh ((dlg.m_radio == 0) ? 1 : -1) ;
close_polygon () ;
// ... schliesst Polygon
}
delete pPoly ;
}
Für das Testen der errechneten Ergebnisse bietet sich hier ein für viele
andere Probleme in modifizierter Form "wiederverwendbarer Trick" an:
Man definiert mehrere (auch recht bizarr geformte) Polygone, die zusammen eine geometrisch einfache Grundfigur geben, für die das Ergebnis kontrollierbar ist. Als Alternative dazu
kann auch eine Gesamtfigur entstehen, die auf andere Weise berechnet und damit kontrolliert
werden kann. Die nachfolgenden Abbildungen zeigen beide Varianten: Im linken Bild sind
drei Polygone zu einem Rechteck mit den Kantenlängen 3 und 4 zusammengesetzt worden,
Tip:
391
J. Dankert: C++-Tutorial
die Ergebnisse sind mit den "Rechteck-Formeln" leicht nachprüfbar. Das rechte Bild zeigt
zwei identische Flächen, die einmal aus einer Rechteck-Teilfläche mit zwei RechteckAusschnitten und zum anderen aus drei Polygon-Teilflächen und vier Ausschnitten gebildet
worden sind, die auch als Polygone eingegeben wurden:
Vergleich mit einer Standardfläche
Modell aus Rechtecken bzw. Polygonen
Mit "App Studio" ist ein "Toolbar-Button" mit dem Symbol eines
Polygons zu erzeugen (die beiden Bilder oben zeigen schon die Version
mit einem solchen Button). Das Anklicken dieses Buttons soll zur gleichen Reaktion wie die
Auswahl Polygon im Menü Standardfläche führen.
Aufgabe 14.10:
Der (einschließlich der Lösung von Aufgabe 14.10) erreichte Stand des Projekts gehört
zum Tutorial als Version "fmom21".
14.4.25
Drucker-Ausgabe
Im Abschnitt 14.4.8 wurde bereits daruf hingewiesen, daß das vom "App Wizard" erzeugte
Programmgerüst die Drucker-Ausgabe (einschließlich Drucker-Vorschau) gratis spendiert. Im
Projekt "fmom" beschränkt sich dies auf die vom "App Wizard" erzeugte Klasse CFmomView und ist (ohne eigenen Beitrag des Programmierers) wie folgt realisiert:
◆
Das Programmgerüst ruft als Reaktion auf die Wahl von Print... oder Print Preview
(im Menü File) die Methode CView::OnPrint mit zwei Argumenten auf, einem
Pointer auf den "Device Context" und einem Pointer auf eine CPrintInfo-Struktur.
Die Standard-Implementation dieser Methode ruft OnDraw auf (mit dem Pointer auf
den "Device Context", den OnDraw erwartet), so daß die darin implementierten
Aktionen auch für die Drucker-Ausgabe genutzt werden.
Wenn die Ausgabe auf den Drucker von der Ausgabe der in OnDraw realisierten Ausgabe
für die "View" abweichen soll, muß in der Ansichtsklasse die von CView ererbte Methode
OnPrint überschrieben werden. In diesem Abschnitt soll dies demonstriert werden, indem die
J. Dankert: C++-Tutorial
392
Drucker-Ausgabe, die sich bisher auf die Ergebnis-Ausschriften der CFmomView-Klasse
beschränkt, um die graphische Darstellung des Berechnungsmodells ergänzt wird.
Die Aufgabe wird bewußt einfach formuliert: Der auf der Druckseite nach dem Ergebnisdruck verbleibende Platz soll mit der graphischen Darstellung in der Form gefüllt werden,
wie sie auf dem Bildschirm im rechten "Pane" des Splitter-Windows gezeichnet wird
(unverzerrt, mittig, den vorhandenen Platz ausnutzend).
Die auf der Druckseite verfügbare Fläche (entsprechend der "Client Area" eines Bildschirmfensters) kann man einer public-Variablen (vom Typ CRect) entnehmen, die zur CPrintInfoStruktur gehört, deren Pointer an OnPrint übergeben wird. Im Gegensatz zum Zeichenvorgang im rechten "Pane" des Splitter-Windows, dessen gesamte Fläche optimal gefüllt
werden soll, ist auf der Druckseite nur ein Teil der verfügbaren Fläche zu füllen. Deshalb
wird zunächst die für die graphische Ausgabe eingerichte Klasse CGrInt um eine auch für
andere Zwecke sehr nützliche Fähigkeit erweitert:
Bisher wurde dem Konstruktor von CGrInt die Abmessung (Breite und Höhe in GeräteEinheiten) des Rechteckbereichs übergeben, in den gezeichnet werden soll ("Client Area"
gleich Zeichenbereich). Dies wird nun ergänzt um die Angabe der Geräte-Einheiten des
Punktes der linken oberen Ecke, so daß ein beliebiger Teilbereich für die Zeichnung
innerhalb der "Client Area" festgelegt werden kann. Daß diese Möglichkeit erst hier
"nachgeliefert" und nicht schon bei der Einrichtung der Klasse im Abschnitt 14.4.13
vorausschauend realisiert wurde, geschieht mit voller Absicht. Es wird gezeigt, daß die
Funktionalität einer Klasse nachträglich ohne Einfluß auf die bisherige Verwendung erweitert
werden kann (kein Programm, das die Klasse benutzt, muß geändert werden).
Die Definition des Zeichenbereichs innerhalb der "Client Area" und die Festlegung des mit
double-Werten arbeitenden Koordinatensystems, mit dem die Methoden der Klasse CGrInt
arbeiten, wird durch die beiden folgenden Skizzen verdeutlicht:
In der "Client Area" wird der Zeichenbereich durch
Angabe der Geräte-Koordinaten pulx und puly des
Punktes PUL ("upper left point") und der Breite width
und Höhe height des Rechtecks (auch in GeräteEinheiten) definiert.
Das mit double-Werten arbeitende Koorinatensystem
wird durch Angabe der vier Werte xmin, ymin, xmax,
ymax definiert. Der durch die Mittelwerte bestimmte
Punkt liegt in der Mitte des Zeichenbereichs, der
Ursprung kann außerhalb des Zeichenbereichs liegen.
393
J. Dankert: C++-Tutorial
Die (recht erhebliche) Erweiterung der Funktionalität der Klasse CGrInt ist mit außerordentlich geringem Aufwand realisierbar:
➪
In der "Visual Workbench" wird die Datei grint.h geöffnet, die Deklaration des
Konstruktors CGrInt wird um die zwei Parameter pulx und puly erweitert, denen die
Default-Werte 0 gegeben werden, so daß bei einem Konstruktor-Aufruf ohne die
entsprechenden Argumente (wie in den bisherigen Versionen) die Annahme "Client
Area" gleich Zeichenbreich gilt:
public:
CGrInt (CDC* pDC , double xmin
double xmax
int
width
double p = 0.
,
,
,
,
double ymin
double ymax
int height
int pulx = 0
,
,
,
, int puly = 0) ;
In der Datei grint.cpp müssen nur der Kopf des Konstruktors CGrInt::CGrInt und
der Aufruf der Methode SetViewportOrg geändert werden:
CGrInt::CGrInt (CDC*
double
double
double
double
int
int
double
int
int
{ // ...
pDC
,
xmin ,
ymin ,
xmax ,
ymax ,
width ,
height,
p
,
pulx ,
puly )
//
//
//
//
//
//
//
//
//
//
Pointer auf "Device Context"
** Extremwerte des Rechteck** Bereichs, in den mit den
** Methoden der Klasse
** gezeichnet werden soll
++ Breite und Hoehe des Bereichs
++ in Geraete-Koordinaten
Prozentanteil fuer Rand
## Geraete-Koordinaten des
## "Upper-Left"-Punktes
// Das Viewport-Koordinatensystem wird in die Mitte der
// Zeichenflaeche gelegt, ...
pDC->SetViewportOrg (pulx + width / 2 , puly + height / 2) ;
// ...
}
Nun wird die Drucker-Ausgabe wie beabsichtigt mit folgender Strategie realisiert:
◆
Überschreiben der von CView ererbten Methode OnPrint, darin Aufruf von
CFmomView::OnDraw, um die gleiche Ergebnisausgabe wie im linken "Pane" des
Splitter-Windows auch auf dem Drucker zu erhalten.
◆
Ermitteln der Position auf dem Ausgabemedium, die nach der OnDraw-Ausgabe
erreicht wurde, und Zeichnen mit CFmomDoc::DrawAllAreas in den noch freien
Bereich (unverzerrt, mittig, den Bereich optimal füllend).
➪
In der "Visual Workbench" wird die Datei fmomview.cpp geöffnet. Die folgende
Methode OnPrint wird ergänzt (Erläuterungen nach dem Listing):
void CFmomView::OnPrint (CDC* pDC , CPrintInfo *pInfo)
{
OnDraw (pDC) ;
CFmomDoc* pDoc = (CFmomDoc*) GetDocument() ;
CPoint ptbr = pInfo->m_rectDraw.BottomRight () ;
CPoint cp
= pDC->GetCurrentPosition () ;
double
xmin , ymin , xmax , ymax ;
if (pDoc->GetMinMaxCoord (xmin , ymin , xmax , ymax))
{
394
J. Dankert: C++-Tutorial
if (ptbr.x - cp.x > 0 && ptbr.y - cp.y > 0 &&
fabs (xmax - xmin) > 1.e-20 && fabs (ymax - ymin) > 1.e-20)
{
CGrInt grint (pDC , xmin , ymin , xmax , ymax ,
ptbr.x - cp.x , ptbr.y - cp.y , 10. ,
cp.x , cp.y) ;
pDoc->DrawAllAreas (&grint) ;
}
}
}
◆
In der Struktur der Klasse CPrintInfo ist die public-Variable m_rectDraw (vom Typ
CRect) enthalten, aus der mit der CRect-Methode BottomRight die Geräte-Koordinaten des rechten unteren Punktes der Zeichenfläche erfragt werden (Ergebnis ist vom
Typ CPoint). Die CDC-Methode GetCurrentPosition liefert die aktuelle Position des
Zeichenstiftes als Return-Wert ebenfalls mit dem Typ CPoint (vgl. nachfolgende
Bemerkung). Damit ist das Rechteck bestimmt, in das die Zeichnung eingebracht
wird, wobei die erweiterte Form des Konstruktors von CGrInt benutzt wird.
Weil die "Current Position" in der Methode OnDraw nicht geändert wird (die TextausgabeRoutine erwartet Koordinaten, modifiziert aber im Gegensatz zu Methoden wie MoveTo oder
LineTo die "Current Position" nicht), ist noch folgende kleine Erweiterung der Methode
OnDraw erforderlich:
➪
In der Methode OnDraw (in der Datei fmomview.cpp) wird nach der letzten TextAusgabe ein MoveTo-Aufruf eingefügt, der die aktuelle Position auf einen Punkt
unterhalb des ausgegebenen Textes setzt:
y = LineOut (pDC , x1 , x2 , x3 , y , cyChar ,
"Achse von Imax:" , "PHI [°]" , phi * 45. / pi_4) ;
pDC->MoveTo (x1 , y + (cyChar * 3) / 2) ;
// "Zeilenvorschub"
}
➪
In der "Visual Workbench" wird die Datei fmomview.h geöffnet, im public-Bereich
der Deklaration der Klasse CFmomView wird die Methode OnPrint ergänzt:
public:
virtual void OnPrint (CDC* pDC , CPrintInfo *pInfo) ;
Damit sind die beabsichtigten und für die zusätzliche graphische Darstellung des Berechnungsmodells bei der Drucker-Ausgabe erforderlichen Erweiterungen realisiert. Das Projekt
kann aktualisiert und getestet werden. Dabei zeigt sich, daß der linke Rand, der für die
Bildschirmausgabe mit 3 mittleren Zeichenbreiten recht sinnvoll gewählt wurde, für den
Heftrand des Papiers etwas zu knapp bemessen ist.
Weil auf diese Weise eine Möglichkeit gezeigt werden kann, wie man OnDraw-Ausgaben
an das Ausgabemedium anpassen kann, soll eine kleine "kosmetische Verbesserung" noch
vorgenommen werden:
➪
In der "Visual Workbench" wird die Datei fmomview.h geöffnet, im private-Bereich
der Klassen-Deklaration werden zwei zusätzliche Variablen installiert:
class CFmomView : public CView
{
private:
int m_x1 ;
int m_y1 ;
// ...
395
J. Dankert: C++-Tutorial
➪
In der "Visual Workbench" wird die Datei fmomview.cpp geöffnet, im Konstruktor
der Klasse werden die beiden neuen Klassen-Variablen initialisiert:
CFmomView::CFmomView ()
{
m_x1 = 0 ;
m_y1 = 0 ;
}
In der Methode OnPrint werden am Anfang die beiden Variablen auf ein Achtel der
Papierbreite bzw. ein Zwölftel der Papierhöhe gesetzt, am Ende wieder auf die
Initialisierungswerte zurückgesetzt:
void CFmomView::OnPrint (CDC* pDC , CPrintInfo *pInfo)
{
m_x1 = pInfo->m_rectDraw.Width () / 8 ;
m_y1 = pInfo->m_rectDraw.Height () / 12 ;
OnDraw (pDC) ;
// ...
m_x1 = 0 ;
m_y1 = 0 ;
}
In der Methode OnDraw werden die Variablen x1 und y, die den Startpunkt der
Ausgabe festlegen, nur dann wie bisher initialisiert, wenn die Klassen-Variablen m_x1
und m_y1 die Werte 0 haben, ansonsten werden die in OnPrint gesetzten Werte
verwendet. Zwei Zeilen sind also zu modifizieren:
x1
x2
x3
y
➪
=
=
=
=
(m_x1 == 0)
x1 + cxChar
x2 + cxChar
(m_y1 == 0)
? cxChar * 3 : m_x1 ;
* 36 ;
* 8 ;
? cyChar * 2 : m_y1 ;
Das Projekt wird aktualisiert (z. B. mit Build
FMOM.EXE im Menü
Project) und gestartet
(Execute FMOM.EXE).
Nach der Eingabe eines
Berechnungsmodells
(über die Tastatur oder
durch Einlesen von
einem File) wird im
Menü File das Angebot
Print Preview gewählt.
Dann zeigt sich z. B.
das nebenstehende Bild.
Sicher lassen sich noch allerhand Verbesserungen der
Drucker-Ausgabe realisieren, einige besonders wichtige Techniken dafür wurden vorgestellt,
so daß dem Ehrgeiz des Lesers keine Grenzen gesetzt sind.
Der nun erreichte Stand des Projekts gehört zum Tutorial als Version "fmom22".
J. Dankert: C++-Tutorial
14.4.26
396
Optionale Ausgabe der Eingabewerte
Die Eingabewerte kann man sich bereits über die in den Abschnitten 14.4.17 bis 14.4.19
implementierte "List Box" ansehen (und bei Bedarf ändern). Speziell für die DruckerAusgabe wäre es sinnvoll, das durch die Eingabewerte beschriebene Berechnungsmodell auch
mit ausgeben zu können.
Hier wird, um das "Aktualisieren von Menü-Items" zu demonstrieren, folgender Weg
beschritten: In Abhängigkeit vom Status eines noch einzurichtenden Menü-Items werden
sowohl bei der Bildschirm-Ausgabe als auch bei der Drucker-Ausgabe die Eingabewerte mit
aufgelistet. Dafür sollte man folgendes wissen:
◆
Bei der Wahl eines Menü-Angebots wird vor dem Ausrollen des Popup-Menüs für
alle Menü-Items die Botschaft ON_UPDATE_COMMAND_UI gesendet. Damit wird
die Möglichkeit gegeben, das Angebot zu aktualisieren (Text ändern, "Enable"-Status
setzen, "Check"-Status setzen, ...). Hier soll ein Menü-Item mit dem "Häkchen"
("Check"-Status) versehen werden, wenn es gültig ist.
➪
Im Menü Tools wird App Studio gewählt, im Type-Fenster wird Menu angeklickt,
und nach Doppelklick auf IDR_FMOMTYPE im Resources-Fenster kann das Menü
editiert werden:
Nach einem Doppelklick auf View öffnet sich die "Property Page", in der im Feld
Caption die Eintragung &View durch &Ansicht/Optionen ersetzt wird. Das PopupMenü enthält bereits die beiden Angebote Toolbar und Status Bar. Ein Doppelklick
auf das leere Kästchen unter Status Bar öffnet die "Property Page" für das neue
Element. In das Caption-Feld wird &Eingabewerte listen eingetragen, in das
Prompt-Feld z. B.: Alle Eingabewerte bei Bildschirm- und Drucker-Ausgabe mit
ausgeben.
Im Menü Resource wird Class Wizard... gewählt. In der "Karteikarte" Message
Maps sollte als Class Name: CFmomView eingestellt sein. Im Fenster Object IDs:
findet man für das gerade eingerichtete Menü-Angebot den (von "App Studio"
erzeugten) Identifikator ID_ANSICHTOPTIONEN_EINGABEWERTELISTEN,
der ausgewählt wird. Zwei Messages: werden angeboten (COMMAND und
UPDATE_COMMAND_UI), für beide wird je eine Funktion eingerichtet (Message
anklicken, Klicken auf Add Function..., angebotenen Funktionsnamen mit OK
akzeptieren). Nach Anklicken von Edit Code landet man im Gerüst einer der gerade
vom "Class Wizard" eingerichteten Funktionen.
Folgende Strategie soll verfolgt werden: Es wird eine Variable m_input_out in der Klasse
CFmomView installiert. Diese wird im Konstruktor initialisiert (auf den Wert 1) und in der
Methode OnAnsichtoptionenEingabewertelisten (wird beim Anklicken des Menü-Angebots
Eingabewerte listen vom Programmgerüst aufgerufen) jeweils umgestellt (auf 0 bzw. 1). In
der Methode OnDraw wird sie abgefragt, um von ihrem Zustand die Ausgabe abhängig zu
machen.
Die Methode OnUpdateAnsichtoptionenEingabewertelisten, die immer vor dem Aufrollen
des Popup-Menüs gerufen wird, empfängt einen Pointer auf ein Objekt der Klasse CCmdUI,
in das der "Check"-Status (mit CCmdUI::SetCheck) eingetragen wird.
397
J. Dankert: C++-Tutorial
➪
Die beiden gerade vom "Class Wizard" erzeugten Methoden in der Datei fmomview.cpp werden folgendermaßen ergänzt:
void CFmomView::OnAnsichtoptionenEingabewertelisten ()
{
m_input_out = (m_input_out == 0) ? 1 : 0 ;
Invalidate () ;
}
void CFmomView::OnUpdateAnsichtoptionenEingabewertelisten
(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck (m_input_out) ;
}
Der Konstruktor (in der gleichen Datei) wird um eine Zeile erweitert:
CFmomView::CFmomView()
{
m_x1 = 0 ;
m_y1 = 0 ;
m_input_out = 1 ;
}
Für die Erweiterung der Ausgabe in der Methode CFmomView::OnDraw werden die in der
Dokument-Klasse bereits vorhandenen Methoden GetAreaCount und GetAreaDesc
verwendet:
➪
Die Methode CFmomView::OnDraw wird wie folgt ergänzt:
void CFmomView::OnDraw(CDC* pDC)
{
// ...
y
= (m_y1 == 0) ? cyChar * 2 : m_y1 ;
if (m_input_out)
{
pDC->TextOut
(x1 , y , "Programm 'Flächenmomente', Berechnungsmodell:") ;
y += cyChar * 2 ;
int nr = pDoc->GetAreaCount () ;
for (int i = 1 ; i <= nr ; i++)
{
CString line = pDoc->GetAreaDesc (i) ;
pDC->TextOut (x1 , y , line) ;
y += cyChar ;
}
y += cyChar ;
}
pDC->TextOut (x1 , y , "Programm 'Flächenmomente', Ergebnisse") ;
// ...
}
➪
In der "Visual Workbench" wird die Datei fmomview.h geöffnet, im private-Bereich
der Klassen-Deklaration wird die Variable m_input_out ergänzt:
class CFmomView : public CView
{
private:
int m_input_out ;
// ...
J. Dankert: C++-Tutorial
➪
398
Das Projekt wird aktualisiert, das Programm wird gestartet. Nach Öffnen des Menüs
Ansicht/Optionen zeigt sich das neue Angebot Eingabewerte listen "mit einem
Häkchen" (Abbildung), weil in OnUpdateAnsichtoptionenEingabewertelisten die
Methode SetCheck mit dem
Argument 1 aufgerufen wird
(Initialisierungs-Wert von
m_input_out). Wenn Eingabewerte listen angeklickt und das
Menü Ansicht/Optionen noch
einmal geöffnet wird, ist das
Häkchen verschwunden.
Die folgende Abbildung läßt allerdings ein neues Problem erkennen: Bei Polygonen reicht
der Platz für die Ausgabe in einer Zeile nicht aus (auch das Verschieben des "Splitter-Bars"
hat seine Grenzen), und
für etwas kompliziertere
Berechnungsmodelle
passen auch nicht alle
Ausgabezeilen in das
Fenster (auch große
Bildschirme und kleine
Fonts setzen irgendwo
eine Grenze).
E i n e g r undsätzliche
Abhilfe kann hier nur
durch eine "Scroll-View"
erzielt werden, was (bei
Verzicht auf eine "intelligente Lösung", die den
Platzbedarf des auszugebenden Textes berücksichtigen würde) als "Schnellschuß-Lösung" mit
geringem Aufwand durch Realisierung der beiden folgenden Punkte erreicht werden kann:
◆
Die Klasse CFmomView, bisher aus der Klasse CView abgeleitet, wird aus der
Klasse CScrollView abgeleitet (CScrollView ist selbst aus CView abgeleitet, so daß
alle CView-Methoden auch weiter verfügbar sind).
◆
Die Größe des Scroll-Bereichs (insgesamt erreichbarer Bereich) muß festgelegt
werden. An dieser Stelle könnte eine "intelligente Lösung" die Größe an die "aktuelle
Größe des Dokuments" (hier: Platzbedarf für die Ausgabe des Berechnungsmodells)
anpassen. Bei Verzicht darauf kann (wie nachfolgend realisiert) eine Größe angegeben
werden, die "mit an Sicherheit grenzender Wahrscheinlichkeit für alle Fälle ausreichend ist".
➪
In der "Visual Workbench" wird die Datei fmomview.h geöffnet. In der Deklaration
der Klasse CFmomView wird die Klasse nun aus der Klasse CScrollView abgeleitet.
Im public-Bereich der Deklaration wird die (von CView geerbte) Methode OnInitialUpdate überschrieben:
399
J. Dankert: C++-Tutorial
class CFmomView : public CScrollView
{ // ...
// Implementation
public:
virtual ~CFmomView();
void OnInitialUpdate () ;
// ...
}
➪
In der "Visual Workbench" wird die Datei fmomview.cpp geöffnet. In den Makros
IMPLEMENT_DYNCREATE und BEGIN_MESSAGE_MAP ist die Angabe der
Klasse, aus der CFmomView abgeleitet wird, auf CScrollView zu ändern. Die zu
ergänzende Methode OnInitialUpdate enthält nur den Aufruf der CScrollViewMethode SetScrollSizes, der (mindestens) zwei Argumente zu übergeben sind,
"Mapping Mode" und ein Argument vom Typ CSize, wobei die Einheiten vom
"Mapping Mode" (hier: "Pixel" für MM_TEXT, willkürlich mit 5000x5000 festgelegt) bestimmt werden:
IMPLEMENT_DYNCREATE(CFmomView, CScrollView)
BEGIN_MESSAGE_MAP(CFmomView, CScrollView)
//{{AFX_MSG_MAP(CFmomView) ...
void CFmomView::OnInitialUpdate ()
{
SetScrollSizes (MM_TEXT , CSize (5000 , 5000)) ;
}
Das war es schon. Das Projekt
kann aktualisiert werden. Nach
dem Programmstart zeigt sich
das linke "Pane" des "SplitterWindows" mit horizontalen und
vertikalen Scroll-Leisten (in der
nebenstehenden Abbildung mit
dem zum fmom-Projekt als
Datei kasten.fmo gehörenden
Berechnungsmodell), die einen
wohl für alle Probleme ausreichenden Bereich zugänglich
machen.
Der nun erreichte Stand des
Projekts gehört zum Tutorial
als Version "fmom23".
Das linke "Pane" des "Splitter Windows" kann "scrollen"
Die Lösung des Problems größerer Ausgabemengen ist für die Bildschirmausgabe mit
"Scroll-Windows" relativ einfach zu erreichen (und dem Ausgabemedium angemessen). Es
ist gleichzeitig ein Beispiel dafür, daß nicht für alle Ausgabemedien die gleichen Strategien
verfolgt werden können: Die Drucker-Ausgabe kann nicht "gescrollt" werden, dafür ist in
jedem Fall eine "intelligente Lösung" erforderlich, die im folgenden Abschnitt für das
typische Problem "Text paßt nicht in eine Zeile" realisiert werden soll.
400
J. Dankert: C++-Tutorial
14.4.27
Platzbedarf für Texte
Texte, die bei der Drucker-Ausgabe nicht in eine Zeile passen, müssen "umgebrochen"
werden. Um dies bei beliebig eingestelltem Font sinnvoll realisieren zu können, nuß der
Platzbedarf für einen Text, der mit dem "Current Font" im gültigen "Device Context"
ausgegeben werden soll, ermittelt werden. Dafür steht die CDC-Methode GetTextExtent zur
Verfügung.
Folgende Verbesserung der Drucker-Ausgabe soll in diesem Abschnitt realisiert werden: Bei
der Ausgabe des Berechnungsmodells (nur dabei entsteht das Problem, speziell bei Polygonen) soll immer dann nach einem Komma auf die folgende Zeile übergegangen werden, wenn
die Ausgabe bis zum folgenden Komma nicht mehr auf die Zeile passen würde. Dabei
werden noch einige recht nützliche Methoden der Klasse CString vorgestellt.
Zunächst wird eine Variable m_width in der Klasse CFmomView ergänzt, die mit 0
initialisiert wird und (nur bei Druckerausgabe) die Breite der verfügbaren Ausgabefläche (in
Geräteeinheiten) enthalten soll:
➪
In der "Visual Workbench" wird die Datei fmomview.h geöffnet, im private-Bereich
der Deklaration der Klasse CFmomView wird eine Zeile ergänzt:
class CFmomView : public CScrollView
{
private:
int m_width ;
// ...
} ;
➪
In der "Visual Workbench" wird die Datei fmomview.cpp geöffnet. Der Konstruktor
CFmomView::CFmomView wird um eine Zeile erweitert:
CFmomView::CFmomView()
{
m_x1 = 0 ;
m_y1 = 0 ;
m_width = 0 ;
m_input_out = 1 ;
}
In der Methode CFmomView::OnPrint wird auf diesen Parameter der aus der
CPrintInfo-Struktur zu entnehmende Wert übertragen, vor dem Beenden der Methode
wird er auf den Wert 0 zurückgesetzt, so daß er gleichzeitig als Indikator "Druckeroder Bildschirmausgabe?" benutzt werden kann:
void CFmomView::OnPrint (CDC* pDC , CPrintInfo *pInfo)
{
m_width = pInfo->m_rectDraw.Width () ;
// ...
m_width = 0 ;
}
In der Methode CFmomView::OnDraw wird in dem von m_input_out abhängigen
Block der in der for-Schleife zu findende TextOut-Aufruf
pDC->TextOut (x1 , y , line) ;
durch den Aufruf der noch zu schreibenden Methode PrintText ersetzt:
401
J. Dankert: C++-Tutorial
void CFmomView::OnDraw(CDC* pDC)
{ // ...
if (m_input_out)
{ // ...
for (int i = 1 ; i <= nr ; i++)
{
CString line = pDoc->GetAreaDesc (i) ;
y = PrintText (pDC , x1 , y , cyChar , line) ;
y += cyChar ;
}
y += cyChar ;
}
// ...
}
Die Methode CFmomView::PrintText, die nachfolgend aufgelistet wird, fragt den Parameter
m_width ab und ruft für die Bildschirm-Ausgabe ungeändert die Methode CDC:TextOut mit
der gesamten Textzeile auf, für die Drucker-Ausgabe wird mit Hilfe einiger CStringMethoden gegebenenfalls ein Zeilenumbruch organisiert (Erläuterungen nach dem Listing):
➪
In der Datei fmomview.cpp wird die Methode CFmomView::PrintText ergänzt, die
z. B. folgendermaßen codiert werden kann:
int CFmomView::PrintText (CDC* pDC
, int x , int y ,
int cyChar , CString line)
{
if (m_width == 0)
{
pDC->TextOut (x , y , line) ;
}
else
{
int
first = 0 , nchars , xx = x ;
CSize linesize ;
do {
nchars = (line.Mid (first)).Find (',') + 1 ;
if (nchars <= 0) nchars = line.GetLength () - first ;
linesize = pDC->GetTextExtent (line.Mid (first , nchars) ,
nchars) ;
if (xx + linesize.cx > m_width)
{
xx = x ;
y += cyChar ;
}
pDC->TextOut (xx , y , line.Mid (first , nchars)) ;
xx += linesize.cx ;
if (first == 0) x = xx ;
first += nchars ;
} while (first < line.GetLength ()) ;
}
return y ;
}
Die Methode CFmomView::PrintText muß in der Klassen-Deklaraion ergänzt
werden. Im public-Bereich der Deklaration von CFmomView in der Datei fmomview.h wird folgende Zeile hinzugefügt:
int PrintText (CDC* pDC , int x , int y , int cyChar , CString line) ;
Im else-Zweig der Methode CFmomView::PrintText, der nur bei der Drucker-Ausgabe
durchlaufen wird, befindet sich die do-while-Schleife, die bei jedem Durchlauf einen Teil des
402
J. Dankert: C++-Tutorial
Textes (bis zu einem Komma bzw. bis zum Text-Ende) ausgibt. Dabei werden vornehmlich
CString-Methoden verwendet:
◆
Ehemalige Basic-Programmierer, die in der C-Programmierung schmerzlich die
schönen Kommandos zur String-Manipulation gleichen Namens vermißt haben, finden
in der Klasse CString mit den Methoden Left, Right und Mid genau diese Funktionen wieder, die den linken bzw. rechten Teil eines Strings (bis bzw. ab einer
vorzugebenden Position) oder einen beliebigen Teil des Strings ansprechen. Als
Return-Wert wird jeweils wieder ein CString abgeliefert.
◆
Der Aufruf
line.Mid (first)
liefert einen CString ab Position first (Positionen zählen "0-basiert") bis zum StringEnde, der Aufruf
line.Mid (first , nchars)
liefert einen CString mit nchars Zeichen (wieder ab Position first).
◆
Die Methode CString::Find, aufgerufen mit einem char-Wert liefert die ("0basierte") Position des Zeichens (erstes Auftreten des Zeichens im String).
◆
Die Methode CString::GetLength liefert die Anzahl der Zeichen in einem CStringObjekt.
◆
Der CDC-Methode GetTextExtent müssen zwei Arguemte übergeben werden, ein
String (bzw. CString-Objekt) und die Anzahl der Zeichen des Strings. Sie liefert als
Return-Wert den Platzbedarf des Strings, wenn er mit dem "Current Font" im "Device
Context", der durch das CDC-Objekt bestimmt wird, ausgegeben wird (Ergebnis in
logischen Einheiten). Der Return-Wert ist ein CSize-Objekt, das in den beiden
Komponenten cx bzw. cy die horizontale bzw. vertikale Abmessung enthält.
In der Methode CFmomView::PrintText wird jeweils ein Teil-String ausgegeben,
danach wird die horizontale Position (für die Ausgabe des nächsten Teil-Strings) um
den ermittelten Platzbedarf vergrößert. Wenn der Platzbedarf größer als der noch
verfügbare Rest der Zeile ist, wird auf die nachfolgende Zeile übergegangen.
◆
Beim Übergang auf eine neue Zeile wird die horizontale Position nicht auf den
Anfangswert zurückgesetzt, sondern auf die Position "nach dem ersten Komma". So
sind Folgezeilen (durch "Einrücken") optisch als solche zu erkennen.
◆
Die vertikale Ausgabe-Position kann sich in Abhängigkeit von der Anzahl der
erforderlichen Zeilen für den auszugebenden Text in CFmomView::PrintText
unterschiedlich ändern. Deshalb wird diese Position als Return-Wert abgeliefert
(ähnlich der Strategie, die im Abschnitt 14.4.8 mit der Methode CFmomView::LineOut realisiert wurde), um für die nachfolgenden Ausgaben die passende
Anschluß-Position verfügbar zu haben.
➪
Das Projekt kann aktualisiert werden (z. B. mit Build FMOM.EXE im Menü
Project). Nach dem Starten des Programms (Execute FMOM.EXE) und Eingabe
eines Berechnungsmodells zeigt sich die Bildschirm-Ausgabe ungeändert (mit der
J. Dankert: C++-Tutorial
Möglichkeit des horizontalen Scrollens, um die
gesamte Information von
sehr langen Ausgabezeilen zu erreichen). Die
Drucker-Vorschau
(Print Preview im
Menü File) realisiert
dagegen den Zeilenumbruch, der auch bei der
Drucker-Ausgabe lange
Texte auf mehrere
Zeilen verteilt. Die
nebenstehende Abbildung zeigt die ("gezoomte") DruckerVorschau für ein Berechnungsmodell, das
die Beschreibung der
beiden Polygone auf
diese Weise ausgibt.
Der nun erreichte Stand des Projekts gehört zum Tutorial als Version "fmom24".
Das ist noch nicht das Ende des Skripts, nicht einmal das Ende des Projekts
"fmom", denn an diesem Projekt lassen sich noch sehr viel mehr
Besonderheiten der Windows-Programmierung mit "Microsoft Foundation
Classes" demonstrieren.
Fortsetzung folgt, wenn die verfügbare Zeit es erlaubt.
J. Dankert
403

Documentos relacionados