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