Die meisten Anwendungen bieten dem Benutzer die Möglichkeit, die eigene Arbeit zu speichern. Dabei kann es sich um ein Dokument einer Textverarbeitung, ein Tabellenblatt, eine Zeichnung oder eine Menge von Datensätzen handeln. In der heutigen Lektion untersuchen wir, welche Mittel Visual C++ bereitstellt, um diese Funktionalität in einfacher Weise zu implementieren.
Heute geht es darum, wie man ...
Die sogenannte Serialisierung zerfällt in zwei Teile. Wenn Anwendungsdaten auf dem Systemlaufwerk in Form einer Datei gespeichert werden, spricht man von Serialisierung. Wird der Anwendungszustand aus der Datei wiederhergestellt, bezeichnet man diesen Vorgang als Deserialisierung. Die Kombination der beiden Teile ergibt die Serialisierung von Anwendungsobjekten in Visual C++.
Die Serialisierung in Visual C++-Anwendungen läuft über die Klasse CArchive
. Diese
Klasse agiert als Eingabe-/Ausgabe- (E/A) Stream für ein CFile
-Objekt, wie es Abbildung
13.1 zeigt. Die C++-Streams gewährleisten einen effizienten Datenfluß in und
aus einer Datei, die als Speicher der Anwendungsdaten dient. Die Klasse CArchive
kann nicht ohne ein Objekt der Klasse CFile
existieren, an die sie gebunden ist.
Abbildung 13.1:
Die Klasse CArchive speichert Anwendungsdaten in einem CFile-Objekt.
Die Klasse CArchive
kann Daten in einer Vielzahl verschiedener Dateitypen speichern,
die alle von der Klasse CFile
abgeleitet sind. In der Standardeinstellung bindet
der Anwendungs-Assistent die gesamte Funktionalität ein, um normale CFile
-Objekte
für den Einsatz mit CArchive
zu erzeugen und zu öffnen. Wenn Sie mit einem der anderen
Dateitypen arbeiten wollen, müssen Sie zusätzlichen Code in Ihre Anwendung
einbinden, um die Verwendung abweichender Dateitypen zu ermöglichen.
Die Klasse CArchive
kommt in der Funktion Serialize
auf den Dokument- und Datenobjekten
in Visual C++-Anwendungen zum Einsatz. Wenn eine Anwendung eine
Datei liest oder schreibt, ruft sie die Funktion Serialize
des Dokumentobjekts auf und
übergibt das CArchive
-Objekt, das das Schreiben in oder Lesen aus der Datei abwikkelt.
In der Funktion Serialize
ist zunächst zu bestimmen, ob das Archiv zu schreiben
oder zu lesen ist. Dazu ruft man die Funktionen IsStoring
oder IsLoading
der Klasse
CArchive
auf. Aus dem Rückgabewert dieser Funktionen läßt sich ablesen, ob die Anwendung
in/aus E/A-Streams der Klasse CArchive
schreiben bzw. lesen muß. Eine typische
Serialize
-Funktion in der Ansichtsklasse ist in Listing 13.1 wiedergegeben.
Listing 13.1: Eine typische Serialize-Funktion
1: void CAppDoc::Serialize(CArchive& ar)
2: {
3: // Ist in das Archiv zu schreiben?
4: if (ar.IsStoring())
5: {
6: // Ja, Variable schreiben
7: ar << m_MyVar;
8: }
9: else
10: {
11: // Nein, Variable lesen
12: ar >> m_MyVar;
13: }
14: }
Eine Serialize
-Funktion können Sie in alle von Ihnen erstellten Klassen aufnehmen
und damit deren Serialize
-Funktion von der Funktion Serialize
des Dokuments
aufrufen. Wenn Sie Ihre benutzerdefinierten Objekte in ein Objektarray stellen,
wie zum Beispiel das in der Zeichenanwendung der letzten drei Tage verwendete
CObArray
, läßt sich die Serialize
-Funktion des Arrays von der Serialize
-Funktion
des Dokuments aufrufen. Das Objektarray ruft seinerseits die Serialize
-Funktion des
jeweiligen Objekts auf, das im Array gespeichert ist.
Als Sie am Tag 10 die Klasse CLine
erstellt haben, mußten Sie zwei Makros einbinden,
bevor Sie die Zeichnungen speichern und wiederherstellen konnten. Die beiden
Makros DECLARE_SERIAL
und IMPLEMENT_SERIAL
binden in Ihre Klassen die erforderliche
Funktionalität ein, damit die Funktion Serialize
ordnungsgemäß arbeiten kann.
In die Klassendeklaration ist das Makro DECLARE_SERIAL
gemäß Listing 13.2 einzubinden.
Das Makro DECLARE_SERIAL
übernimmt als einziges Argument den Klassennamen
und fügt automatisch in Ihre Klasse bestimmte Standardfunktionen und Operatordeklarationen
ein, die für den korrekten Ablauf der Serialisierung notwendig sind.
Listing 13.2: Das Makro DECLARE_SERIAL in die Klassendeklaration einbinden
1: class CMyClass : public CObject
2: {
3: DECLARE_SERIAL (CMyClass)
4: public:
5: virtual void Serialize(CArchive &ar);
6: CMyClass();
7: virtual ~CMyClass();
8: };
Das Makro IMPLEMENT_SERIAL
ist in die Implementierung Ihrer Klasse einzubinden. Es
muß außerhalb aller anderer Klassenfunktionen erscheinen, da es den Code für diejenigen
Klassenfunktionen hinzufügt, die mit dem Makro DECLARE_SERIAL
deklariert
werden.
Das Makro IMPLEMENT_SERIAL
übernimmt drei Argumente. Das erste gibt wie beim
Makro DECLARE_SERIAL
den Klassennamen an. Das zweite Argument übergibt den
Namen der Basisklasse, von der Ihre Klasse abgeleitet ist. Das dritte Argument bezeichnet
eine Versionsnummer, aus der man bestimmen kann, ob eine in die Anwendung
einzulesende Datei in der richtigen Version vorliegt. Die Versionsnummer muß
positiv sein und sollte jedesmal inkrementiert werden, wenn Sie Änderungen an der
Serialisierungsmethode der Klasse vornehmen und sich dabei die mit einer Datei auszutauschenden
Datenformate ändern. Listing 13.3 zeigt einen typischen Einsatz des
Makros IMPLEMENT_SERIAL
.
Listing 13.3: Das Makro IMPLEMENT_SERIAL in die Implementierung der Klasse einbinden
1: // MyClass.cpp: Implementierung der Klasse CMyClass.
2: //
3: //////////////////////////////////////////////////////////////////////
4:
5: #include "stdafx.h"
6: #include "MyClass.h"
7:
8: #ifdef _DEBUG
9: #undef THIS_FILE
10: static char THIS_FILE[]=__FILE__;
11: #define new DEBUG_NEW
12: #endif
13:
14: IMPLEMENT_SERIAL (CMyClass, CObject, 1)
15: //////////////////////////////////////////////////////////////////////
16: // Konstruktion/Destruktion
17: //////////////////////////////////////////////////////////////////////
18:
19: CMyClass::CMyClass()
20: {
21: }
22:
23: CMyClass::~CMyClass()
24: {
25: }
Neben den beiden Makros müssen Sie eine Serialize
-Funktion in Ihre Klasse einbinden.
Diese Funktion sollte vom Typ void
mit einem einzelnen Argument (CArchive
&ar
), dem Zugriffsstatus Public und eingeschaltetem Kontrollkästchen Virtual deklariert
sein, woraus die Funktionsdeklaration in Listing 13.2 hervorgeht. Wenn Sie die
Funktion Serialize
für Ihre Klasse implementieren, gehen Sie normalerweise genauso
vor, wie bei der in Listing 13.1 gezeigten Dokumentklasse. Auch hier ist zu prüfen,
ob die Datei zu lesen oder zu schreiben ist.
Wenn Sie eine neue Anwendung entwerfen, müssen Sie als eine der ersten Aufgaben entscheiden, wie die Daten in der Dokumentklasse, die Ihre Anwendung erzeugt und darauf arbeitet, zu speichern sind. Erstellen Sie zum Beispiel eine datenbezogene Anwendung, die in der Art einer Kontakt-Datenbank Datenmengen vom Benutzer entgegennimmt, wie wollen Sie diese Daten im Arbeitsspeicher der Anwendung halten? Oder wenn Sie eine Textverarbeitung erstellen - wie wollen Sie das geschriebene Dokument im Speicher unterbringen? Oder ein Tabellenblatt? Oder ein Zeichenprogramm? Oder - na ja, Sie ahnen schon, worum es geht.
Nachdem Sie die Datenstrukturen entworfen haben, mit denen Ihre Anwendung arbeitet,
können Sie bestimmen, wie sich Ihre Anwendung und die Klassen am besten
serialisieren lassen. Wenn Sie alle Daten direkt in der Dokumentklasse aufbewahren,
brauchen Sie sich nur darum zu kümmern, wie die Daten über das CArchive
-Objekt in
der Funktion Serialize
des Dokuments geschrieben und gelesen werden. Wenn Sie
Ihre eigene Klasse erstellen, um die Anwendungsdaten darin zu halten, müssen Sie die
Funktionalität der Serialisierung in Ihre Datenklassen einbinden, damit sich die Objekte
selbst speichern und wiederherstellen können.
In der heutigen Beispielanwendung erstellen Sie eine einfache, lineare Datenbankanwendung,
die verdeutlicht, wie man eine Mischung von Datentypen in einem einzigen
Datenstrom bei der Serialisierung der Anwendung kombinieren kann. Die Anwendung
zeigt ein paar Datenfelder an, von denen einige als Strings mit variabler Länge
und andere als Integer oder Boolesch festgelegt sind. Diese Variablen werden über einen
einzigen Datenstrom in das CArchive
-Objekt geschrieben bzw. daraus wiederhergestellt.
Für eine SDI- oder MDI-Anwendung kann man eigene Klassen erzeugen, die sich ebenfalls serialisieren lassen. Kurz gesagt kann jede Anwendung, die in irgendeiner Form mit Daten arbeitet, ob es sich nun um eine Datenbank oder ein Dokument handelt, serialisiert werden. Sie erstellen nun eine einfache, lineare Datenbankanwendung, die Sie dann serialisieren.
Zunächst erstellen Sie mit dem Anwendungs-Assistenten eine neue Anwendung. Geben
Sie der Anwendung einen treffenden Namen - der Kürze wegen etwa Serial
-
und klicken Sie auf OK, um den Anwendungs-Assistenten zu starten.
Im Anwendungs-Assistenten wählen Sie die Option Einzelnes Dokument (SDI) und lassen das Kontrollkästchen Unterstützung der Dokument-/Ansicht-Architektur eingeschaltet. Im dritten Dialogfeld des Assistenten können Sie die Unterstützung für ActiveX-Steuerelemente beibehalten, obwohl es für das heutige Beispiel eigentlich nicht erforderlich ist.
Im vierten Dialogfeld des Assistenten geben Sie die Dateierweiterung für die Dateien
an, die Ihre Anwendung erzeugt und liest. Ein Beispiel einer Dateierweiterung, die Sie
verwenden können, ist ser
für serialisieren oder fdb
für flat-file database (lineare Datenbank).
Im sechsten Schritt des Anwendungs-Assistenten müssen Sie die Basisklasse für die
Ansichtsklasse Ihrer Anwendung festlegen. Eine Erläuterung der verschiedenen Basisklassen,
von der Sie die Ansichtsklasse ableiten können, finden Sie in der Lektion 10.
Da es sich beim heutigen Beispiel um eine Datenbankanwendung handelt, ist es am
einfachsten, CFormView
als Basisklasse zu verwenden, von der Sie Ihre Ansichtsklasse
ableiten. Das bietet nebenbei die Möglichkeit, die Ansicht Ihrer Anwendung mit dem
Dialog-Editor zu gestalten.
Nachdem Sie mit dem Anwendungs-Assistenten das Gerüst Ihrer Anwendung erstellt haben, bietet Ihnen der Dialog-Editor eine große Fensterfläche wie bei einer dialogfeldbasierenden Anwendung, nur daß die Schaltflächen OK und Abbrechen nicht vorhanden sind (siehe Abbildung 13.2).
Abbildung 13.2:
Der Dialog-Editor für eine SDI-Anwendung
Nachdem Sie eine SDI- oder MDI-Anwendung erstellt haben, in der die Ansichtsklasse
auf der Klasse CFormView
basiert, müssen Sie nun die Ansicht der Anwendung entwerfen.
Das geht fast genauso wie beim Entwurf eines Dialogfelds, allerdings brauchen
Sie sich nicht um irgendwelche Schaltflächen zum Schließen des Fensters zu kümmern,
wobei der Benutzer die Arbeit entweder speichert oder abbricht. Bei einer SDI-
oder MDI-Anwendung ist die gesamte Funktionalität zum Speichern und Verlassen
des Fensters traditionsgemäß in den Anwendungsmenüs oder der Symbolleiste untergebracht.
Daher brauchen Sie nur Steuerelemente für die vom Anwendungsfenster zu
realisierenden Funktionen vorzusehen.
Für die heute zu erstellende Beispielanwendung plazieren Sie die Steuerelemente in der Fensterfläche gemäß Abbildung 13.3. Die Eigenschaften der jeweiligen Steuerelemente sind in Tabelle 13.1 aufgeführt.
Abbildung 13.3:
Das Layout der Beispielanwendung
Wenn Sie dialogfeldbasierende Anwendungen oder Fenster entwickeln, weisen Sie
den Steuerelementen im Fenster Variablen in der Dialogfeldklasse zu. Wie sieht es
nun bei einer SDI- oder MDI-Anwendung aus? In welcher Klasse erzeugen Sie die Variablen?
Da die Funktion UpdateData
ein Element der Klasse CWnd
ist, und die Ansichtsklasse
von der Klasse CWnd
abgeleitet ist, auch wenn das nicht für das Dokument
zutrifft, dann bietet sich die Ansichtsklasse als logischer Platz an, um die Variablen aufzunehmen,
die Sie den Steuerelementen im Fenster zuweisen.
Um Variablen mit den Steuerelementen in Ihrer Beispielanwendung zu verbinden,
öffnen Sie den Klassen-Assistenten und fügen der Ansichtsklasse (in diesem Fall
CSerialView
) Variablen für die Steuerelemente hinzu. Nehmen Sie für die Beispielanwendung
die in Tabelle 13.2 aufgeführten Variablen für die angegebenen Steuerelemente
auf.
Wenn Sie sich den Quellcode für die Ansichtsklasse ansehen, stellen Sie fest, daß es
keine Funktion OnDraw
gibt. Wenn Sie für Ihre SDI- oder MDI-Anwendung die Basisklasse
CFormView
verwenden, brauchen Sie sich um die Funktion OnDraw
nicht zu kümmern.
Statt dessen behandeln Sie die Ansichtsklasse genauso wie die Dialogfeldklasse
in einem Dialogfenster oder einer dialogfeldbasierenden Anwendung. Der Hauptunterschied
besteht darin, daß die Daten, mit denen Sie die Steuerelemente im Fenster
füllen, nicht in der Ansichtsklasse, sondern in der Dokumentklasse gespeichert sind.
Im Ergebnis müssen Sie die Interaktion zwischen diesen beiden Klassen realisieren,
um die Daten für die Steuerelemente in beiden Richtungen zu übertragen.
Wenn Sie eine formularbasierte Anwendung erstellen, kann man davon ausgehen, daß Ihre Anwendung mehrere Datensätze im Formular aufnimmt und der Benutzer durch die Datensätze navigieren kann, um Änderungen vorzunehmen. Der Benutzer kann zusätzliche Datensätze einfügen oder sogar Datensätze aus dem Recordset entfernen. Das eigentlich Interessante in dieser Phase der Anwendungsentwicklung ist es, wie man diese Menge von Datensätzen darstellt und dabei die gesamte erforderliche Funktionalität unterstützt.
Eine Lösung besteht darin, eine Klasse zu erzeugen, die jeden Datensatz verkapselt,
und dann diese Datensätze in ein Array aufzunehmen. Dieses Vorgehen entspricht der
Zeichenanwendung, die Sie in den vergangenen Tagen erstellt und erweitert haben.
Diese Klasse ist von der Klasse CObject
abzuleiten und muß Variablen für alle Steuerelementvariablen
enthalten, die Sie der Ansichtsklasse hinzugefügt haben. Außerdem
gehören noch die Methoden dazu, um diese Variablen lesen und schreiben zu können.
Sie müssen aber nicht nur die Methoden hinzufügen, um die Variablen zu setzen und
zu lesen, Sie müssen auch die Klasse serialisierbar machen, indem Sie sowohl die
Funktion Serialize
als auch die beiden Makros, die die Serialisierung der Klasse realisieren,
in die Klasse aufnehmen.
Wie Sie in Lektion 10 gesehen haben, können Sie das Projekt auf der Registerkarte Klassen des Arbeitsbereichs markieren, mit der rechten Maustaste klicken und Neue Klasse aus dem Kontextmenü wählen, wenn Sie eine neue Klasse erstellen möchten. Daraufhin öffnet sich das Dialogfeld Neue Klasse.
Im Dialogfeld Neue Klasse legen Sie den Typ der Klasse fest, d.h., ob es sich um eine MFC-Klasse, eine allgemeine Klasse oder eine Formularklasse handelt. Um eine Klasse zu erzeugen, die die Daten eines Datensatzes enthalten kann, müssen Sie höchstwahrscheinlich eine allgemeine Klasse erstellen. Mehr dazu, wie man den passenden Klassentyp bestimmt, finden Sie am Tag 16. Weiterhin müssen Sie Ihrer Klasse einen Namen verleihen und die Basisklasse angeben, von der die neue Klasse abzuleiten ist.
Da das erstellte Formular der Beispielanwendung Informationen zu einer Person enthält,
bietet es sich an, die neue Klasse CPerson
zu nennen. Damit Sie die Klasse im
Objektarray speichern können, müssen Sie die Klasse von CObject
als Basisklasse ableiten.
Wie schon am Tag 10 wird das Dialogfeld Neue Klasse feststellen, daß sich die
Header-Datei mit der Basisklasse nicht finden läßt und Sie diese manuell hinzufügen
müssen. Nun, die Header-Datei ist bereits eingebunden, so daß Sie diese Meldung
ignorieren können. (Tag 16 geht näher darauf ein, wann Sie dieser Meldung Beachtung
schenken müssen.)
Nachdem Sie die neue Klasse erstellt haben, sind noch die Variablen hinzuzufügen, um die auf dem Bildschirm für den Benutzer anzuzeigenden Datenelemente aufzunehmen. Gemäß einem guten objektorientierten Entwurf deklariert man diesen Variablen als privat, damit sie sich nicht durch andere Klassen manipulieren lassen. Die Variablentypen sollten den Typen der Variablen der Ansichtsklasse, die mit dem Steuerelementen des Fensters verbunden sind, entsprechen.
Für die Beispielanwendung fügen Sie die Variablen gemäß Tabelle 13.3 hinzu.
Nachdem die Klasse erstellt ist, müssen Sie Mittel bereitstellen, um die Variablen in der Klasse zu lesen und zu schreiben. Am einfachsten läßt sich das mit Inline-Funktionen in der Klassendefinition realisieren. Mit der einen Gruppe von Inline-Funktionen setzen Sie die Variablen, während Sie mit einer anderen Gruppe von Inline-Funktionen die aktuellen Werte der Variablen abrufen.
Wenn Sie die Variablenfunktionen Get
und Set
für Ihre Klasse CPerson
der Beispielanwendung
implementieren wollen, bearbeiten Sie die Header-Datei Person.h
und
fügen hier die Zeilen aus Listing 13.4 ein.
Listing 13.4: Die Deklarationen der Inline-Funktionen Get und Set
1: class CPerson : public CObject
2: {
3: public:
4: // Funktionen zum Setzen der Variablen
5: void SetEmployed(BOOL bEmployed) { m_bEmployed = bEmployed;}
6: void SetMaritalStat(int iStat) { m_iMaritalStatus = iStat;}
7: void SetAge(int iAge) { m_iAge = iAge;}
8: void SetName(CString sName) { m_sName = sName;}
9: // Funktionen zum Holen der aktuellen Variableneinstellungen
10: BOOL GetEmployed() { return m_bEmployed;}
11: int GetMaritalStatus() { return m_iMaritalStatus;}
12: int GetAge() {return m_iAge;}
13: CString GetName() {return m_sName;}
14: CPerson();
15: virtual ~CPerson();
16:
17: private:
18: BOOL m_bEmployed;
19: int m_iMaritalStatus;
20: int m_iAge;
21: CString m_sName;
22: };
Mit diesen Methoden können Sie nun die Werte von Variablen in Ihrer benutzerdefinierten
Klasse setzen und abrufen. Normalerweise initialisiert man die Variablen auch,
wenn die Klasse erstmalig erzeugt wird. Das läßt sich im Klassenkonstruktor erreichen,
indem man allen Variablen einen Standardwert zuweist. Für die heutige Beispielanwendung
zeigt Listing 13.5 den Konstruktor der Klasse CPerson
.
Listing 13.5: Der Konstruktor der Klasse CPerson
1: CPerson::CPerson()
2: {
3: // Klassenvariablen initialisieren
4: m_iMaritalStatus = 0;
5: m_iAge = 0;
6: m_bEmployed = FALSE;
7: m_sName = "";
8: }
Nachdem Sie Ihre benutzerdefinierte Klasse mit allen Variablen definiert und initialisiert
haben, müssen Sie die Klasse serialisierbar machen. Das läuft in drei Schritten
ab. Im ersten Schritt nehmen Sie die Funktion Serialize
in die Klasse auf. Diese
Funktion schreibt die Variablenwerte mit Hilfe von C++-Streams in das CArchive
-Objekt
und liest sie auf die gleiche Weise von dort wieder aus. In den beiden anderen
Schritten fügen Sie die Makros DECLARE_SERIAL
und IMPLEMENT_SERIAL
hinzu. Mit
diesen Elementen ist Ihre benutzerdefinierte Klasse serialisierbar und bereit zum Einsatz.
Um die Funktion Serialize
in die benutzerdefinierte Klasse aufzunehmen, fügen Sie
über die Registerkarte Klassen des Arbeitsbereichs der Klasse CPerson
eine Member-
Funktion hinzu. Legen Sie den Funktionstyp als void
, die Funktionsdeklaration mit
Serialize(CArchive &ar)
und den Zugriffsstatus als Public fest. Schalten Sie außerdem
das Kontrollkästchen Virtual ein. Die Funktion Serialize
wird nun hinzugefügt
und im Editor zur Bearbeitung angezeigt.
Als erstes ist in der Funktion Serialize
die gleichnamige Funktion der Basisklasse
aufzurufen. Wenn Sie die Funktion der Basisklasse zuerst aufrufen, werden alle gespeicherten
Basisinformationen zuerst wiederhergestellt. Damit steht die erforderliche Unterstützung
für Ihre Klasse bereit, bevor die Variablen in Ihrer Klasse wiederhergestellt
werden. Nach dem Aufruf der Funktion der Basisklasse müssen Sie ermitteln, ob die
Klassenvariablen zu lesen oder zu schreiben sind. Dazu können Sie die Methode IsStoring
der Klasse CArchive
aufrufen. Diese Funktion liefert TRUE
zurück, wenn das
Archiv zu schreiben ist, und FALSE
, wenn es zu lesen ist. Wenn die Funktion IsStoring
den Wert TRUE
zurückgibt, können Sie C++-E/A-Streams einsetzen, um alle
Klassenvariablen in das Archiv zu schreiben. Liefert die Funktion FALSE
, lesen Sie mit
C++-Streams aus dem Archiv. Insbesondere ist darauf zu achten, daß die Variablen
sowohl beim Lesen als auch beim Schreiben in derselben Reihenfolge erscheinen.
Mehr über C++-Streams finden Sie in Anhang A.
Listing 13.6 zeigt eine typische Serialize
-Funktion für Ihre benutzerdefinierte Klasse.
Beachten Sie, daß die Variablen von CPerson
sowohl beim Schreiben in als auch beim
Lesen aus dem Archiv in der gleichen Reihenfolge vorliegen.
Listing 13.6: Die Funktion Serialize der Klasse CPerson
1: void CPerson::Serialize(CArchive &ar)
2: {
3: // Funktion der Basisklasse aufrufen
4: CObject::Serialize(ar);
5:
6: // Wird geschrieben?
7: if (ar.IsStoring())
8: // Alle Variablen in der richtigen Reihenfolge schreiben
9: ar << m_sName << m_iAge << m_iMaritalStatus << m_bEmployed;
10: else
11: // Alle Variablen in der richtigen Reihenfolge lesen
12: ar >> m_sName >> m_iAge >> m_iMaritalStatus >> m_bEmployed;
13: }
Nachdem die Funktion Serialize
an Ort und Stelle ist, müssen Sie noch die Makros
in die benutzerdefinierte Klasse aufnehmen. Das erste Makro - DECLARE_SERIAL
- gehört
in die Header-Datei der Klasse und übernimmt den Klassennamen als einziges
Argument.
Um zum Beispiel das Makro DECLARE_SERIAL
in die benutzerdefinierte Klasse CPerson
der Beispielanwendung hinzuzufügen, schreiben Sie das Makro direkt nach dem Anfang
der Klassendeklaration, wo es den Standardzugriff für die Klasse erhält. Legen
Sie den Klassennamen CPerson
als einziges Argument an das Makro fest, wie es Listing
13.7 zeigt.
Listing 13.7: Die serialisierte Klassendeklaration von CPerson
1: class CPerson : public CObject
2: {
3: DECLARE_SERIAL (CPerson)
4: public:
5: // Funktionen zum Setzen der Variablen
6: void SetEmployed(BOOL bEmployed) { m_bEmployed = bEmployed;}
7: void SetMaritalStat(int iStat) { m_iMaritalStatus = iStat;}
8: void SetAge(int iAge) { m_iAge = iAge;}
9: void SetName(CString sName) { m_sName = sName;}
10: // Funktionen zum Holen der aktuellen Variableneinstellungen
11: BOOL GetEmployed() { return m_bEmployed;}
12: int GetMaritalStatus() { return m_iMaritalStatus;}
13: int GetAge() {return m_iAge;}
14: CString GetName() {return m_sName;}
15: CPerson();
16: virtual ~CPerson();
17:
18: private:
19: BOOL m_bEmployed;
20: int m_iMaritalStatus;
21: int m_iAge;
22: CString m_sName;
23: };
Um die Serialisierung der benutzerdefinierten Klasse fertigzustellen, müssen Sie noch
das Makro IMPLEMENT_SERIAL
in die Klassendefinition aufnehmen. Der beste Platz für
dieses Makro ist vor der Konstruktordefinition in der .cpp
-Datei, die den Quellcode
der Klasse enthält. Das Makro übernimmt drei Argumente: den Namen der benutzerdefinierten
Klasse, den Namen der Basisklasse und die Versionsnummer. Wenn Sie irgendwelche
Änderungen an der Funktion Serialize
vornehmen, sollten Sie die an
das Makro IMPLEMENT_SERIAL
übergebene Versionsnummer inkrementieren. Die Versionsnummer
gibt an, wenn eine Datei mit einer vorherigen Version der Funktion Serialize
geschrieben wurde und sich daher unter Umständen nicht durch die aktuelle
Version der Anwendung lesen läßt.
Um das Makro IMPLEMENT_SERIAL
in die Beispielanwendung aufzunehmen, fügen Sie
es in die Datei Person.cpp
unmittelbar vor dem Klassenkonstruktor von CPerson
ein.
Übergeben Sie CPerson
(den Klassennamen) als erstes Argument, CObject
(die Basisklasse)
als zweites Argument und 1
als Versionsnummer, wie es Listing 13.8 zeigt.
Listing 13.8: Das Makro IMPLEMENT_SERIAL im Code von CPerson
1: // Person.cpp: Implementierung der Klasse CPerson.
2: //
3: //////////////////////////////////////////////////////////////////////
4:
5: #include "stdafx.h"
6: #include "Serial.h"
7: #include "Person.h"
8:
9: #ifdef _DEBUG
10: #undef THIS_FILE
11: static char THIS_FILE[]=__FILE__;
12: #define new DEBUG_NEW
13: #endif
14:
15: IMPLEMENT_SERIAL (CPerson, CObject, 1)
16: //////////////////////////////////////////////////////////////////////
17: // Konstruktion/Destruktion
18: //////////////////////////////////////////////////////////////////////
19:
20: CPerson::CPerson()
21: {
22: // Klassenvariablen initialisieren
23: m_iMaritalStatus = 0;
24: m_iAge = 0;
25: m_bEmployed = FALSE;
26: m_sName = "";
27: }
Wenn Sie eine formularbasierte Anwendung erstellen, in der das Formular im Fenster der vorrangige Platz für die Benutzerinteraktion mit der Anwendung ist, gibt es eine stillschweigende Übereinkunft, daß Ihre Anwendung dem Benutzer die Arbeit mit einer Anzahl von Datensätzen gestattet. Sie müssen also eine Unterstützung vorsehen, um die Datensätze zwischenzuspeichern und die Navigation durch den Recordset zu ermöglichen. Am einfachsten läßt sich das realisieren, indem man ein Objektarray als Variable in die Dokumentklasse aufnimmt, wie Sie es am Tag 10 gesehen haben. Damit kann man bei Bedarf zusätzliche Datensatzobjekte hinzufügen. Die Navigation implementiert man als Funktionen, die das erste, letzte, nächste oder vorherige Datensatzobjekt abrufen. Schließlich müssen Sie noch die Funktionalität realisieren, um bestimmen zu können, welchen Datensatz im Recordset der Benutzer gerade bearbeitet.
Um diese Funktionalität zu unterstützen, braucht die Dokumentklasse zunächst einmal zwei Variablen für das Objektarray und die aktuelle Datensatznummer im Array. Diese beiden Variablen bieten die erforderliche Unterstützung, um den Recordset zu speichern und die Navigation zu ermöglichen.
In die Beispielanwendung fügen Sie die beiden Variablen für die Unterstützung des
Recordsets von CPerson
-Objekten gemäß Tabelle 13.4 hinzu. Legen Sie für beide
Variablen den Zugriff als Privat fest.
Weiterhin ist in der Dokumentklasse noch zu gewährleisten, daß das Dokument über das zu speichernde Datensatzobjekt informiert ist. Dazu binden Sie in die Quelldatei der Dokumentklasse die Header-Datei der benutzerdefinierten Klasse ein, und zwar vor der Header-Datei für die Dokumentklasse. Da die Dokumentklasse Aktionen in der Ansichtsklasse auslösen muß, empfiehlt es sich, auch die Header-Datei für die Ansichtsklasse in die Dokumentklasse aufzunehmen.
Um diese Header-Dateien in die Beispielanwendung einzubinden, öffnen Sie die
Quellcodedatei für die Dokumentklasse und fügen die beiden #include
-Anweisungen
gemäß Listing 13.9 ein.
Listing 13.9: Die benutzerdefinierten und Ansichtsklassen in die Implementierung der Dokumentklasse einbinden
1: // SerialDoc.cpp : Implementierung der Klasse CSerialDoc
2: //
3:
4: #include "stdafx.h"
5: #include "Serial.h"
6:
7: #include "Person.h"
8: #include "SerialDoc.h"
9: #include "SerialView.h"
10:
11: #ifdef _DEBUG
12: #define new DEBUG_NEW
13: #undef THIS_FILE
14: static char THIS_FILE[] = __FILE__;
15: #endif
16:
17: //////////////////////////////////////////////////////////////////////////
18: // CSerialDoc
Bevor Sie durch den Recordset navigieren können, müssen Sie neue Datensätze in das Objektarray aufnehmen können. Wenn Sie eine private Funktion für das Hinzufügen neuer Datensätze vorsehen, können Sie neue Datensätze dynamisch in den Recordset je nach Bedarf einfügen. Da neue Datensätze dem Benutzer leere Datenfelder präsentieren sollten, brauchen Sie keine Datensatzvariablen zu setzen, wenn Sie einen neuen Datensatz in das Objektarray aufnehmen. Damit können Sie auf den Standardkonstruktor zurückgreifen.
Nach der gleichen Logik, mit der Sie neue Liniendatensätze am Tag 10 hinzugefügt haben, nehmen Sie in der heutigen Beispielanwendung einen neuen Personendatensatz in das Objektarray der Dokumentklasse auf. Nachdem Sie einen neuen Datensatz hinzugefügt haben, können Sie einen Zeiger darauf zurückgeben, so daß die Ansichtsklasse direkt die Variablen im Datensatzobjekt aktualisieren kann.
Sobald der neue Datensatz hinzugefügt ist, setzen Sie den aktuellen Datensatzzeiger auf den neuen Datensatz im Array. Auf diese Weise läßt sich die aktuelle Datensatznummer leicht anhand des Positionszählers bestimmen.
Falls irgendwelche Probleme beim Erstellen des neuen Persondatensatzobjekts auftreten, weisen Sie den Benutzer darauf hin, daß der Anwendung nicht mehr ausreichend Speicher zur Verfügung steht, und löschen das reservierte Objekte genau wie am Tag 10.
Um diese Funktionalität in der Beispielanwendung zu realisieren, fügen Sie der Dokumentklasse
eine neue Member-Funktion hinzu. Legen Sie den Typ als Zeiger auf Ihre
benutzerdefinierte Klasse fest. Wenn die benutzerdefinierte Klasse CPerson
heißt, lautet
der Funktionstyp CPerson*
. Die Funktion benötigt keine Argumente. Geben Sie
der Funktion einen Namen, der ihren Zweck widerspiegelt, beispielsweise AddNewRecord
. Legen Sie den Zugriff auf die Funktion mit Privat fest, da sie nur von anderen
Funktionen innerhalb der Dokumentklasse aufgerufen wird. In die Funktion übernehmen
Sie den Code aus Listing 13.10.
Listing 13.10: Die Funktion AddNewRecord der Klasse CSerialDoc
1: CPerson * CSerialDoc::AddNewRecord()
2: {
3: // Ein neues CPerson-Objekt erzeugen
4: CPerson *pPerson = new CPerson();
5: try
6: {
7: // Neue Person in Objektarray hinzufügen
8: m_oaPeople.Add(pPerson);
9: // Dokument als bearbeitet markieren
10: SetModifiedFlag();
11: // Neue Position festhalten
12: m_iCurPosition = (m_oaPeople.GetSize() - 1);
13: }
14: // Speicherausnahme aufgetreten?
15: catch (CMemoryException* perr)
16: {
17: // Benutzer über schlechte Neuigkeiten
18: // informieren
19: AfxMessageBox("Speichermangel", MB_ICONSTOP | MB_OK);
20: // Wurde Person-Objekt erzeugt?
21: if (pPerson)
22: {
23: // Objekt löschen
24: delete pPerson;
25: pPerson = NULL;
26: }
27: // Ausnahmeobjekt löschen
28: perr->Delete();
29: }
30: return pPerson;
31: }
Um dem Benutzer die Navigation durch den Recordset zu erleichtern, ist es immer hilfreich, wenn man einen Anhaltspunkt liefert, wo sich der Benutzer im Recordset befindet. Um diese Informationen bereitzustellen, müssen Sie die aktuelle Datensatznummer und die Gesamtzahl der Datensätze aus dem Dokument ermitteln und sie dem Benutzer anzeigen.
Die betreffenden Funktionen sind recht einfach. Für die Gesamtzahl der Datensätze im Objektarray brauchen Sie lediglich die Größe des Arrays zu ermitteln und diesen Wert an den Aufrufer zurückgeben.
Fügen Sie für die Beispielanwendung eine neue Member-Funktion in die Dokumentklasse
ein. Legen Sie den Funktionstyp als int
, den Funktionsnamen mit GetTotalRecords
und den Zugriff als Public fest. In die Funktion übernehmen Sie den Code aus
Listing 13.11.
Listing 13.11: Die Funktion GetTotalRecords der Klasse CSerialDoc
1: int CSerialDoc::GetTotalRecords()
2: {
3: // Anzahl der Datensätze im Array zurückgeben
4: return m_oaPeople.GetSize();
5: }
Die aktuelle Datensatznummer läßt sich genauso einfach ermitteln. Wenn Sie einen Positionszähler in der Dokumentklasse verwalten, enthält diese Variable die Nummer des Datensatzes, den der Benutzer gerade bearbeitet. Letztendlich brauchen Sie nur den Wert dieser Variablen an den Aufrufer zurückgeben. Da das Objektarray mit Position 0 beginnt, addieren Sie zunächst eine 1 zur aktuellen Position, bevor Sie den Wert zurückgeben und dem Benutzer anzeigen.
Fügen Sie eine weitere Member-Funktion in die Dokumentklasse der Beispielanwendung
ein. Legen Sie den Typ mit int
, den Funktionsnamen mit GetCurRecordNbr
und
den Zugriff als Public fest. Übernehmen Sie den Code aus Listing 13.12 in die Funktion.
Listing 13.12: Die Funktion GetCurRecordNbr der Klasse CSerialDoc
1: int CSerialDoc::GetCurRecordNbr()
2: {
3: // Aktuelle Position zurückgeben
4: return (m_iCurPosition + 1);
5: }
Damit die Anwendung auch einen echten Nutzen bringt, müssen Sie dem Benutzer eine Möglichkeit bieten, durch den Recordset zu navigieren. Für die Minimalausstattung erstellen Sie einen Satz von Funktionen in der Dokumentklasse, um die Zeiger auf bestimmte Datensätze im Recordset zu ermitteln. Die erste Funktion holt einen Zeiger auf den aktuellen Datensatz. Zwei weitere Funktionen liefern die Zeiger auf den ersten bzw. letzten Datensatz im Recordset. Schließlich sind noch Funktionen erforderlich, um den vorherigen und den nächsten Datensatz im Recordset anzusprechen. Wenn der Benutzer bereits den letzten Datensatz im Recordset bearbeitet und versucht, zum nächsten Datensatz weiterzuschalten, können Sie automatisch einen neuen Datensatz in den Recordset aufnehmen und dem Benutzer als neuen, leeren Datensatz anzeigen.
Als erstes erstellen wir die Funktion, die den aktuellen Datensatz zurückgibt. Diese Funktion muß den Wert im Datensatzzeiger untersuchen, um sicherzustellen, daß der aktuelle Datensatz eine gültige Arrayposition ist. Nachdem Sie geprüft haben, daß die aktuelle Position gültig ist, kann die Funktion einen Zeiger auf den aktuellen Datensatz im Array zurückgeben.
Fügen Sie eine neue Member-Funktion in die Dokumentklasse der Beispielanwendung
ein. Legen Sie den Funktionstyp mit CPerson*
(ein Zeiger auf die benutzerdefinierte
Klasse), die Funktionsdeklaration als GetCurRecord
und den Zugriff als Public fest.
Übernehmen Sie den Code aus Listing 13.13 in die Funktion.
Listing 13.13: Die Funktion GetCurRecord der Klasse CSerialDoc.
1: CPerson* CSerialDoc::GetCurRecord()
2: {
3: // Ist Datensatznummer gültig?
4: if (m_iCurPosition >= 0)
5: // Ja, aktuellen Datensatz zurückgeben
6: return (CPerson*)m_oaPeople[m_iCurPosition];
7: else
8: // Nein, NULL zurückgeben
9: return NULL;
10: }
Als nächste Aufgabe realisieren Sie die Funktion, die den ersten Datensatz im Array zurückgibt. In dieser Funktion müssen Sie prüfen, ob das Array überhaupt Datensätze enthält. Sind Datensätze im Array vorhanden, setzen Sie den aktuellen Datensatzzeiger auf 0 und liefern einen Zeiger auf den ersten Datensatz im Array zurück.
Fügen Sie wiederum eine neue Member-Funktion in die Dokumentklasse der Beispielanwendung
ein. Legen Sie den Funktionstyp als CPerson*
(ein Zeiger auf die benutzerdefinierte
Klasse), die Funktionsdeklaration als GetFirstRecord
und den Zugriff als
Public fest. In die Funktion übernehmen Sie den Code aus Listing 13.14.
Listing 13.14: Die Funktion GetFirstRecord der Klasse CSerialDoc
1: CPerson* CSerialDoc::GetFirstRecord()
2: {
3: // Enthält das Array Datensätze?
4: if (m_oaPeople.GetSize() > 0)
5: {
6: // Ja, zur Position 0 gehen
7: m_iCurPosition = 0;
8: // Datensatz bei Position 0 zurückgeben
9: return (CPerson*)m_oaPeople[0];
10: }
11: else
12: // Keine Datensätze, NULL zurückgeben
13: return NULL;
14: }
Um zum nächsten Datensatz im Recordset zu navigieren, müssen Sie den aktuellen Datensatzzeiger inkrementieren und dann prüfen, ob das Ende des Arrays noch nicht überschritten ist. Wenn der Zeiger innerhalb der Arraygrenzen liegt, geben Sie einen Zeiger auf den aktuellen Datensatz im Array zurück. Ist das Ende des Arrays überschritten, fügen Sie einen neuen Datensatz an das Array an.
Zu diesem Zweck fügen Sie eine neue Member-Funktion in die Dokumentklasse der
Beispielanwendung hinzu. Legen Sie den Funktionstyp als CPerson*
(ein Zeiger auf
die benutzerdefinierte Klasse), die Funktionsdeklaration als GetNextRecord
, und den
Zugriff als Public fest. In die Funktion übernehmen Sie den Code aus Listing 13.15.
Listing 13.15: Die Funktion GetNextRecord der Klasse CSerialDoc
1: CPerson * CSerialDoc::GetNextRecord()
2: {
3: // Arraygrenze überschritten nach Inkrementieren
4: // des Positionszählers?
5: if (++m_iCurPosition < m_oaPeople.GetSize())
6: // Nein, Datensatz an der neuen aktuellen Position zurückgeben
7: return (CPerson*)m_oaPeople[m_iCurPosition];
8: else
9: // Ja, neuen Datensatz hinzufügen
10: return AddNewRecord();
11: }
In der Funktion, mit der Sie zum vorherigen Datensatz im Array navigieren, sind verschiedene Prüfungen auszuführen. Erstens ist zu testen, ob das Array über Datensätze verfügt. Ist das der Fall, dekrementieren Sie den aktuellen Datensatzzeiger. Wird dieser Zeiger kleiner als null, setzen Sie den aktuellen Datensatzzeiger gleich 0 und zeigen damit auf den ersten Datensatz im Array. Nach diesen Schritten können Sie einen Zeiger auf den aktuellen Datensatz im Array zurückgeben.
Fügen Sie auch hier wieder eine neue Member-Funktion in die Dokumentklasse der
Beispielanwendung ein. Legen Sie den Funktionstyp als CPerson*
(ein Zeiger auf die
benutzerdefinierte Klasse), die Funktionsdeklaration mit GetPrevRecord
und den Zugriff
als Public fest. In die Funktion übernehmen Sie den Code aus Listing 13.16.
Listing 13.16: Die Funktion GetPrevRecord der Klasse CSerialDoc
1: CPerson * CSerialDoc::GetPrevRecord()
2: {
3: // Enthält das Array Datensätze?
4: if (m_oaPeople.GetSize() > 0)
5: {
6: // Position nach Dekrementieren der aktuellen
7: // Position kleiner als 0?
8: if (--m_iCurPosition < 0)
9: // Ja, Datensatzzeiger auf 0 setzen
10: m_iCurPosition = 0;
11: // Datensatz an der neuen aktuellen Position zurückgeben
12: return (CPerson*)m_oaPeople[m_iCurPosition];
13: }
14: else
15: // Keine Datensätze, NULL zurückgeben
16: return NULL;
17: }
Für die Funktion, die zum letzten Datensatz im Array navigiert, müssen Sie ebenfalls prüfen, ob Datensätze im Array vorhanden sind. Ist das der Fall, können Sie die aktuelle Größe des Arrays ermitteln und den aktuellen Datensatzzeiger auf die um 1 verminderte Zahl der Datensätze im Array setzen. Dabei handelt es sich tatsächlich um den letzten Datensatz im Array, da die Zählung der Datensätze im Array bei 0 beginnt. Nachdem Sie den aktuellen Datensatzzeiger gesetzt haben, können Sie einen Zeiger auf den letzten Datensatz im Array zurückgeben.
Nehmen Sie eine neue Member-Funktion in die Dokumentklasse der Beispielanwendung
auf. Legen Sie den Funktionstyp als CPerson*
(ein Zeiger auf die benutzerdefinierte
Klasse), die Funktionsdeklaration mit GetLastRecord
und den Zugriff als Public
fest. In die Funktion schreiben Sie den Code aus Listing 13.17.
Listing 13.17: Die Funktion GetLastRecord der Klasse CSerialDoc
1: CPerson * CSerialDoc::GetLastRecord()
2: {
3: // Enthält das Array Datensätze?
4: if (m_oaPeople.GetSize() > 0)
5: {
6: // Zur letzten Position im Array gehen
7: m_iCurPosition = (m_oaPeople.GetSize() - 1);
8: // Datensatz auf dieser Position zurückgeben
9: return (CPerson*)m_oaPeople[m_iCurPosition];
10: }
11: else
12: // Keine Datensätze, NULL zurückgeben
13: return NULL;
14: }
Wenn Sie die Funktion Serialize
in die Dokumentklasse aufnehmen, brauchen Sie
nichts weiter zu tun, als das CArchive
-Objekt an die Funktion Serialize
des Objektarrays
zu übergeben, genauso wie Sie es von Tag 10 her kennen.
Beim Lesen der Daten aus dem Archiv fragt das Objektarray das CArchive
-Objekt ab,
um zu bestimmen, welcher Objekttyp und wie viele Objekte zu erzeugen sind. Das Objektarray
erzeugt dann jedes Objekt im Array, ruft dessen Serialize
-Funktion auf und
übergibt dabei das CArchive
-Objekt. Damit können die Objekte im Objektarray ihre eigenen
Variablenwerte aus dem CArchive
-Objekt in der gleichen Reihenfolge lesen,
wie sie in die Datei geschrieben wurden.
Beim Schreiben von Daten in das Dateiarchiv ruft das Objektarray alle Serialize
-
Funktionen der Reihe nach auf und übergibt das CArchive
-Objekt (genau wie beim Lesen
aus dem Archiv). Damit kann das Objekt im Array die eigenen Variablen in der erforderlichen
Form und Reihenfolge in das Archiv schreiben.
Für die Beispielanwendung bearbeiten Sie die Funktion Serialize
der Dokumentklasse,
um das Objekt CArchive
an die Funktion Serialize
des Objektarrays zu übergeben,
wie es Listing 13.18 zeigt.
Listing 13.18: Die Funktion Serialize der Klasse CSerialDoc
1: void CSerialDoc::Serialize(CArchive& ar)
2: {
3: // Serialisierung an Objektarray übergeben
4: m_oaPeople.Serialize(ar);
5: }
Es ist noch der Code zu realisieren, um das Dokument »aufzuräumen«, wenn es geschlossen
oder ein neues Dokument geöffnet wird. Dazu sind alle Objekte im Objektarray
zu durchlaufen und zu löschen. Anschließend kann man das Objektarray
über die Funktion RemoveAll
zurücksetzen.
Um diese Funktionalität in der Beispielanwendung zu realisieren, fügen Sie mit dem
Klassen-Assistenten eine Behandlungsroutine in die Dokumentklasse für die Nachricht
DeleteContents
hinzu. In diese Funktion übernehmen Sie den Code aus Listing
13.19.
Listing 13.19: Die Funktion DeleteContents der Klasse CSerialDoc
1: void CSerialDoc::DeleteContents()
2: {
3: // TODO: Speziellen Code hier einfügen und/oder Basisklasse aufrufen
4:
5: ///////////////////////
6: // EIGENER CODE, ANFANG
7: ///////////////////////
8:
9: // Anzahl der Datensätze im Objektarray ermitteln
10: int liCount = m_oaPeople.GetSize();
11: int liPos;
12:
13: // Enthält das Array Objekte?
14: if (liCount)
15: {
16: // Schleife durch das Array, dabei jedes Objekt löschen
17: for (liPos = 0; liPos < liCount; liPos++)
18: delete m_oaPeople[liPos];
19: // Array zurücksetzen
20: m_oaPeople.RemoveAll();
21: }
22:
23: ///////////////////////
24: // EIGENER CODE, ENDE
25: ///////////////////////
26:
27: CDocument::DeleteContents();
28: }
Wenn der Benutzer ein neues Dokument anlegt, soll ein leeres Formular erscheinen, in das er neue Informationen eingeben kann. Um diesen leeren Datensatz für die Aufnahme neuer Informationen vorzubereiten, nimmt man einen Datensatz in das Objektarray auf, der im übrigen leer ist. Damit steht im Objektarray ein einziger Datensatz. Im Anschluß muß man die Ansicht modifizieren, um den neuen Datensatz auch tatsächlich anzuzeigen. Andernfalls zeigt die Ansicht den letzten bearbeiteten Datensatz aus dem vorherigen Recordset an (und der Benutzer wundert sich, warum die Anwendung keinen neuen Datensatz angelegt hat).
Um diese Funktionalität zu implementieren, ist die Funktion OnNewDocument
in der Dokumentklasse
zu bearbeiten. Diese Funktion ist bereits in der Dokumentklasse vorhanden,
so daß man sie nicht extra über den Klassen-Assistenten hinzufügen muß. In der
Funktion fügt man zunächst einen neuen Datensatz in das Objektarray ein. Anschließend
holt man einen Zeiger auf das Ansichtsobjekt. Die Position des Ansichtsobjekts
läßt sich mit der Funktion GetFirstViewPosition
ermitteln. Mit der für das Ansichtsobjekt
zurückgegebenen Position ruft man die Funktion GetNextView
auf, um einen
Zeiger auf das Ansichtsobjekt zu erhalten. Mit dem gültigen Zeiger kann man eine
Funktion - die Sie in der Ansichtsklasse erstellen - aufrufen, um die Ansicht anzuweisen,
die aktuellen Datensatzinformationen auf dem Formular anzuzeigen.
Suchen Sie die Funktion OnNewDocument
im Quellcode der Dokumentklasse auf, und
übernehmen Sie in diese Funktion den Code aus Listing 13.20. Bevor Sie Ihre Anwendung
kompilieren können, müssen Sie die Funktion NewDataSet
in die Ansichtsklasse
aufnehmen.
Listing 13.20: Die Funktion OnNewDocument der Klasse CSerialDoc
1: BOOL CSerialDoc::OnNewDocument()
2: {
3: if (!CDocument::OnNewDocument())
4: return FALSE;
5:
6: // ZU ERLEDIGEN: Hier Code zur Reinitialisierung einfügen
7: // (SDI-Dokumente verwenden dieses Dokument)
8:
9: ///////////////////////
10: // EIGENER CODE, ANFANG
11: ///////////////////////
12:
13: // Läßt sich kein neuer Datensatz hinzufügen, FALSE zurückgeben
14: if (!AddNewRecord())
15: return FALSE;
16:
17: // Einen Zeiger auf die Ansicht holen
18: POSITION pos = GetFirstViewPosition();
19: CSerialView* pView = (CSerialView*)GetNextView(pos);
20: // Der Ansicht mitteilen, daß eine neue Datenmenge eingetroffen ist
21: if (pView)
22: pView->NewDataSet();
23:
24: ///////////////////////
25: // EIGENER CODE, ENDE
26: ///////////////////////
27:
28: return TRUE;
29: }
Wenn Sie einen vorhandenen Recordset öffnen, brauchen Sie keinen neuen Datensatz
hinzuzufügen. Dennoch müssen Sie das Ansichtsobjekt anweisen, den anzuzeigenden
Datensatz zu aktualisieren. Praktisch können Sie den gleichen Code in die
Funktion OnOpenDocument
schreiben, wie in die Funktion OnNewDocument
, nur daß Sie
den ersten Teil weglassen, der einen neuen Datensatz in das Objektarray einfügt.
Nehmen Sie mit dem Klassen-Assistenten eine neue Behandlungsroutine für das Ereignis
OnOpenDocument
in die Dokumentklasse auf. Schreiben Sie in diese Funktion
den Code gemäß Listing 13.21.
Listing 13.21: Die Funktion OnOpenDocument der Klasse CSerialDoc
1: BOOL CSerialDoc::OnOpenDocument(LPCTSTR lpszPathName)
2: {
3: if (!CDocument::OnOpenDocument(lpszPathName))
4: return FALSE;
5:
6: // TODO: Speziellen Erstellungscode hier einfügen
7:
8: ///////////////////////
9: // EIGENER CODE, ANFANG
10: ///////////////////////
11:
12: // Einen Zeiger auf die Ansicht holen
13: POSITION pos = GetFirstViewPosition();
14: CSerialView* pView = (CSerialView*)GetNextView(pos);
15: // Der Ansicht mitteilen, daß sie eine neue Datenmenge erhalten hat
16: if (pView)
17: pView->NewDataSet();
18:
19: ///////////////////////
20: // EIGENER CODE, ENDE
21: ///////////////////////
22:
23: return TRUE;
24: }
In die Dokumentklasse haben Sie die Unterstützung für den Recordset aufgenommen. Diese Funktionalität ist nun noch in der Ansichtsklasse zu realisieren, um durch die Datensätze zu navigieren, diese anzuzeigen und zu aktualisieren. Beim ersten Entwurf Ihrer Ansichtsklasse haben Sie eine Reihe von Steuerelementen im Fenster untergebracht, die für die Anzeige und Bearbeitung der verschiedenen Datenelemente der Datensätze vorgesehen sind. Weiterhin haben Sie Steuerelemente für die Navigation durch den Recordset aufgenommen. Diesen Steuerelementen müssen Sie nun »Leben einhauchen«, damit sie die Navigation durch die Datensätze und die Aktualisierung eines Datensatzes mit den vom Benutzer vorgenommenen Änderungen durchführen.
Durch den Umfang der direkten Interaktion, die das Formular mit dem Datensatzobjekt
hat (Lesen der Variablenwerte aus dem Datensatz und Schreiben der neuen Werte
in den Datensatz), ist es sinnvoll, einen Datensatzzeiger auf die Ansichtsklasse als private
Variable hinzuzufügen. Nehmen Sie für die Beispielanwendung eine neue Member-Variable
in die Ansichtsklasse auf, legen Sie den Typ mit CPerson*
, den Namen
mit m_pCurPerson
und den Zugriff mit Privat fest. Als nächstes bearbeiten Sie den
Quellcode der Ansicht und binden die Header-Datei für die Personenklasse gemäß Listing
13.22 ein.
Listing 13.22: Die Header-Datei für das benutzerdefinierte Objekt in den Quellcode der Ansichtsklasse einbinden
1: // SerialView.cpp : Implementierung der Klasse CSerialView
2: //
3:
4: #include "stdafx.h"
5: #include "Serial.h"
6:
7: #include "Person.h"
8: #include "SerialDoc.h"
9: #include "SerialView.h"
10:
11: #ifdef _DEBUG
12: .
13: .
14: .
Fügen Sie der Ansichtsklasse zuerst die Funktionalität hinzu, um den aktuellen Datensatz anzuzeigen. Da verschiedene Stellen in der Ansichtsklasse auf diese Funktionalität zurückgreifen, erstellen Sie am besten eine eigene Funktion dafür. In dieser Funktion holen Sie die aktuellen Werte aller Variablen im Datensatzobjekt und schreiben sie in die Variablen der Ansichtsklasse, die mit den Steuerelementen im Fenster verbunden sind. Weiterhin ermitteln Sie die aktuelle Datensatznummer und die Gesamtzahl der Datensätze im Recordset und zeigen diese Werte an, damit der Benutzer die relative Position innerhalb des Recordsets kennt.
Fügen Sie der Beispielanwendung eine neue Member-Funktion hinzu. Legen Sie den
Funktionstyp mit void
, die Funktionsdeklaration mit PopulateView
und den Zugriff als
Privat fest. In der Funktion ermitteln Sie einen Zeiger auf das Dokumentobjekt. Nachdem
Sie über einen gültigen Zeiger für das Dokument verfügen, geben Sie die aktuelle
Datensatznummer und die Gesamtzahl der Datensätze im Recordset - die Sie mit den
bereits in die Dokumentklasse hinzugefügten Funktionen GetCurRecordNbr
und GetTotalRecords
ermitteln - formatiert aus. Mit einem gültigen Zeiger auf ein Datensatzobjekt
setzen Sie als nächstes alle Variablen auf die Werte ihrer korrespondierenden
Felder im Datensatzobjekt. Anschließend aktualisieren Sie das Fenster mit den Variablenwerten,
wie es Listing 13.23 zeigt.
Listing 13.23: Die Funktion PopulateView der Klasse CSerialView
1: void CSerialView::PopulateView()
2: {
3: // Einen Zeiger auf das aktuelle Dokument holen
4: CSerialDoc* pDoc = GetDocument();
5: if (pDoc)
6: {
7: // Aktuelle Datensatzposition in der Menge anzeigen
8: m_sPosition.Format("Datensatz %d von %d", pDoc->GetCurRecordNbr(),
9: pDoc->GetTotalRecords());
10: }
11: // Ist Datensatzobjekt gültig?
12: if (m_pCurPerson)
13: {
14: // Ja, alle Werte des Datensatzes holen
15: m_bEmployed = m_pCurPerson->GetEmployed();
16: m_iAge = m_pCurPerson->GetAge();
17: m_sName = m_pCurPerson->GetName();
18: m_iMaritalStatus = m_pCurPerson->GetMaritalStatus();
19: }
20: // Anzeige aktualisieren
21: UpdateData(FALSE);
22: }
Da Sie beim Entwurf des Formulars Schaltflächen zur Navigation hinzugefügt haben, läßt sich die Navigation in einfacher Weise realisieren, indem Sie Behandlungsroutinen für alle Navigationsschaltflächen hinzufügen und die korrespondierende Navigationsfunktion im Dokument aufrufen. Nachdem das Dokument zum entsprechenden Datensatz im Recordset gegangen ist, müssen Sie die Funktion aufrufen, die Sie für die Anzeige des aktuellen Datensatzes erstellt haben. Wenn die Navigationsfunktionen des Dokuments Zeiger auf das neue aktuelle Datensatzobjekt zurückgeben, sollten Sie diese Zeiger zwischenspeichern, bevor Sie die Funktion zur Anzeige des aktuellen Datensatzes aufrufen.
Fügen Sie in Ihre Beispielanwendung mit dem Klassen-Assistenten eine Behandlungsfunktion
für das Klickereignis der Schaltfläche Erster hinzu. In der Funktion holen Sie
einen Zeiger auf das Dokumentobjekt. Nachdem Sie über einen gültigen Zeiger auf
das Dokument verfügen, rufen Sie die Funktion GetFirstRecord
des Dokumentobjekts
auf und speichern den zurückgegebenen Objektzeiger in der Zeigervariablen
CPerson
der Ansicht ab. Wenn Sie einen gültigen Zeiger erhalten, rufen Sie die Funktion
PopulateView
auf, um die Daten des Datensatzes anzuzeigen. Der zugehörige
Code ist in Listing 13.24 wiedergegeben.
Listing 13.24: Die Funktion OnBfirst der Klasse CSerialView
1: void CSerialView::OnBfirst()
2: {
3: // TODO: Code für die Behandlungsroutine der Steuerelement-
ÂBenachrichtigung hier einfügen
4:
5: // Zeiger auf das aktuelle Dokument holen
6: CSerialDoc * pDoc = GetDocument();
7: if (pDoc)
8: {
9: // Den ersten Datensatz aus dem Dokument holen
10: m_pCurPerson = pDoc->GetFirstRecord();
11: if (m_pCurPerson)
12: {
13: // Aktuellen Datensatz anzeigen
14: PopulateView();
15: }
16: }
17: }
Für die Schaltfläche Letzter führen Sie die gleichen Schritte aus wie für die Schaltfläche
Erster, rufen aber die Funktion GetLastRecord
des Dokumentobjekts auf, wie es
Listing 13.25 zeigt.
Listing 13.25: Die Funktion OnBlast der Klasse CSerialView
1: void CSerialView::OnBlast()
2: {
3: // TODO: Code für die Behandlungsroutine der Steuerelement-
ÂBenachrichtigung hier einfügen
4:
5: // Zeiger auf das aktuelle Dokument holen
6: CSerialDoc * pDoc = GetDocument();
7: if (pDoc)
8: {
9: // Letzten Datensatz vom Dokument holen
10: m_pCurPerson = pDoc->GetLastRecord();
11: if (m_pCurPerson)
12: {
13: // Aktuellen Datensatz anzeigen
14: PopulateView();
15: }
16: }
17: }
Die gleichen Schritte wiederholen Sie für die Schaltflächen Vorheriger und Nächster,
wobei Sie die Funktionen GetPrevRecord
bzw. GetNextRecord
des Dokumentobjekts
aufrufen. Diese abschließenden Schritte realisieren in Ihrer Anwendung die gesamte
Funktionalität, die man für die Navigation durch den Recordset benötigt. Da außerdem
der Aufruf der Funktion GetNextRecord
des Dokuments für den letzten Datensatz
im Recordset automatisch einen neuen Datensatz in den Recordset hinzufügt, haben
Sie auch die Möglichkeit, bei Bedarf neue Datensätze in den Recordset aufzunehmen.
Wenn der Benutzer Änderungen an den Daten in den Steuerelementen auf dem Bildschirm vornimmt, müssen diese Änderungen irgendwie in den aktuellen Datensatz im Dokument gelangen. Wenn Sie im Ansichtsobjekt einen Zeiger auf das aktuelle Datensatzobjekt verwalten, können Sie die verschiedenen Funktionen zum Setzen von Werten des Datensatzobjekts aufrufen und dabei den neuen Wert übergeben, um den Wert im Datensatzobjekt zu setzen.
Um dies in der Beispielanwendung zu implementieren, fügen Sie mit dem Klassen-Assistenten
eine Behandlungsroutine für das Ereignis BN_CLICKED
für das Kontrollkästchen
erwerbstätig hinzu. In der erzeugten Funktion rufen Sie zuerst UpdateData
auf,
um die Werte aus dem Formular in die Variablen der Ansicht zu kopieren. Prüfen Sie,
ob Sie einen gültigen Zeiger auf das aktuelle Datensatzobjekt erhalten, und rufen Sie
dann die entsprechende Set
-Funktion auf dem Datensatzobjekt auf (in diesem Fall die
Funktion SetEmployed
wie in Listing 13.26).
Listing 13.26: Die Funktion OnCbemployed der Klasse CSerialView
1: void CSerialView::OnCbemployed()
2: {
3: // TODO: Code für die Behandlungsroutine der Steuerelement-
ÂBenachrichtigung hier einfügen
4:
5: // Daten im Formular mit den Variablen synchronisieren
6: UpdateData(TRUE);
7: // Wenn gültiges Person-Objekt vorhanden, Daten an das Objekt übergeben
8: if (m_pCurPerson)
9: m_pCurPerson->SetEmployed(m_bEmployed);
10: }
Wiederholen Sie die gleichen Schritte für die anderen Steuerelemente, wobei Sie die
jeweiligen Funktionen des Datensatzobjekts aufrufen. Fügen Sie eine Behandlungsroutine
für das Ereignis EN_CHANGE
der Eingabefelder Name und Alter hinzu, und rufen
Sie die Funktionen SetName
und SetAge
auf. Die Optionsfelder für den Familienstand
statten Sie ebenfalls mit einer Behandlungsroutine aus, und zwar für das BN_CLICKED
-
Ereignis. Rufen Sie für alle vier Optionsfelder dieselbe Behandlungsfunktion auf. In
dieser Funktion rufen Sie die Funktion SetMaritalStat
im Datensatzobjekt auf.
Als letzte Funktionalität fügen Sie eine Funktion hinzu, um die Ansicht zurückzusetzen, wenn der Benutzer einen neuen Recordset anlegt oder öffnet, damit der alte Recordset nicht mehr angezeigt wird. Dazu rufen Sie die Behandlungsroutine für die Schaltfläche Erster auf und erzwingen, daß die Ansicht den ersten Datensatz im neuen Recordset anzeigt.
Um diese Funktionalität in der Beispielanwendung umzusetzen, fügen Sie eine neue
Member-Funktion in die Ansichtsklasse ein und legen deren Typ mit void
fest. Als
Funktionsnamen geben Sie den Namen an, den Sie vom Dokumentobjekt aus aufrufen
(NewDataSet
), und für den Zugriff wählen Sie die Option Public (damit sich die
Funktion aus der Dokumentklasse aufrufen läßt). In der Funktion rufen Sie die Behandlungsroutine
für die Schaltfläche Erster auf, wie es Listing 13.27 zeigt.
Listing 13.27: Die Funktion NewDataSet der Klasse CSerialView
1: void CSerialView::NewDataSet()
2: {
3: // Ersten Datensatz in der Menge anzeigen
4: OnBfirst();
5: }
Bevor Sie Ihre Anwendung kompilieren und ausführen können, müssen Sie die Header-Datei
für Ihre benutzerdefinierte Klasse in den Quellcode der Hauptanwendung
einbinden. Diese Datei trägt den gleichen Namen wie Ihr Projekt, hat aber die Erweiterung
.cpp
. Ihre benutzerdefinierte Header-Datei sollte vor den Header-Dateien sowohl
der Dokument- als auch der Ansichtsklasse eingebunden werden. Für die Beispielanwendung
fügen Sie in die Datei Serialize.cpp
die Zeile 8 aus Listing 13.28
hinzu.
Listing 13.28: Die Header-Datei der Datensatzklasse in die Quelldatei der Hauptanwendung einbinden
1: // Serial.cpp : Legt das Klassenverhalten für die Anwendung fest.
2: //
3:
4: #include "stdafx.h"
5: #include "Serial.h"
6:
7: #include "MainFrm.h"
8: #include "Person.h"
9: #include "SerialDoc.h"
10: #include "SerialView.h"
11:
12: #ifdef _DEBUG
13: .
14: .
15: .
Jetzt können Sie mit Ihrer Anwendung Gruppen von Datensätzen hinzufügen, bearbeiten, speichern und wiederherstellen. Wenn Sie die Anwendung kompilieren und ausführen, können Sie Datensätze für sich selbst, für alle Ihre Familienangehörigen, Ihre Freunde oder jeden, den Sie in diese Anwendung aufnehmen möchten, anlegen. Wenn Sie den erstellten Recordset speichern und erneut öffnen, sobald Sie das nächste Mal mit der Anwendung arbeiten, sollten die Datensätze in genau dem gleichen Zustand wiederhergestellt sein, wie Sie sie eingegeben haben (siehe Abbildung 13.4).
Abbildung 13.4:
Die laufende Serialisierungsanwendung
Die heutige Lektion hat im Detail gezeigt, was Serialisierung ist und wie sie funktioniert. Sie haben gelernt, wie man eine Klasse serialisierbar macht, und warum und wie man die beiden Makros einsetzt, die zur Serialisierung einer Klasse erforderlich sind. Weiterhin wurde dargestellt, wie man eine formularbasierte SDI-Anwendung entwirft und erstellt, wobei eine Gruppe von Datensätzen in einer linearen Datenbank für den Einsatz in der Anwendung verwaltet wurde. Sie haben gelernt, wie man die Serialisierung einsetzt, um die lineare Datenbank zu erstellen und zu verwalten, und wie man die Funktionalität in der Dokument- und der Ansichtsklasse realisiert, um die Navigation und Bearbeitung für diese Recordsets bereitzustellen.
Frage:
Wenn ich irgendwelche Änderungen an einem der Datensätze in meinem Recordset
vornehme, nachdem ich den Recordset gespeichert und dann die Anwendung
geschlossen habe oder einen anderen Recordset öffne, fordert die Anwendung
keine Bestätigung, ob ich die Änderungen speichern möchte. Wie kann ich erreichen,
daß die Anwendung eine entsprechende Bestätigung abruft, wenn Daten
geändert wurden?
Antwort:
Den Schlüssel zu diesem Problem liefert ein Funktionsaufruf in der Funktion
AddNewRecord im Dokumentobjekt. Nachdem Sie einen neuen Datensatz in
das Objektarray aufgenommen haben, rufen Sie die Funktion SetModifiedFlag
auf. Diese Funktion markiert das Dokument als geändert. Wenn Sie den
Recordset speichern, wird die Markierung automatisch wieder zurückgesetzt
(außer wenn die Anwendung den Recordset aus irgendwelchen Gründen nicht
speichern kann). Beim Übernehmen der Änderungen müssen Sie das Dokument
in den geänderten Zustand setzen, damit die Anwendung weiß, daß das
Dokument ungesicherte Änderungen enthält.
Schreiben Sie zu diesem Zweck den entsprechenden Code in die Behandlungsroutinen
für Ihre Datensteuerelemente. Nachdem Sie den neuen Wert im aktuellen
Datensatz gespeichert haben, holen Sie einen Zeiger auf das Dokumentobjekt und
rufen die Funktion SetModifiedFlag
des Dokuments auf, wie es Listing 13.29
zeigt. Wenn Sie die gleichen Änderungen an allen Behandlungsroutinen für Datensteuerelemente
vornehmen, fragt Ihre Anwendung nach, ob Sie die Änderungen,
die Sie seit der letzten Sicherung des Recordsets vorgenommen haben, speichern
möchten.
Listing 13.29: Die modifizierte Funktion OnCbemployed der Klasse CSerialView
1: void CSerialView::OnCbemployed()
2: {
3: // TODO: Code für die Behandlungsroutine der Steuerelement-
ÂBenachrichtigung hier einfügen
4:
5: // Daten im Formular mit den Variablen synchronisieren
6: UpdateData(TRUE);
7: // Wenn gültiges Person-Objekt vorhanden, Daten an das Objekt übergeben
8: if (m_pCurPerson)
9: m_pCurPerson->SetEmployed(m_bEmployed);
10: // Zeiger auf das Dokument holen
11: CSerialDoc * pDoc = GetDocument();
12: if (pDoc)
13: // Veränderungsflag im Dokument setzen
14: pDoc->SetModifiedFlag();
15: }
Frage:
Warum muß ich die Versionsnummer im Makro IMPLEMENT_SERIAL
ändern, wenn
ich die Funktion Serialize
in der benutzerdefinierten Datensatzklasse überarbeite?
Antwort:
Ob Sie die Versionsnummer inkrementieren müssen, hängt von der Art der
vorgenommenen Änderungen ab. Wenn Sie beispielsweise ein berechnetes
Feld in die Datensatzklasse aufnehmen und den Code hinzufügen, um diese
neue Variable aus den Werten zu berechnen, die Sie in die Variablen aus dem
CArchive-Objekt eingelesen haben, brauchen Sie die Versionsnummer eigentlich
nicht inkrementieren, weil sich die Variablen und die Reihenfolge der Variablen,
die Sie in das Archiv schreiben oder daraus lesen, nicht geändert haben.
Wenn Sie allerdings ein neues Feld in die Datensatzklasse aufnehmen
und das neue Feld in den E/A-Stream übernehmen, der in das CArchive-Objekt
geschrieben oder daraus gelesen wird, dann hat sich das Format der mit
dem Archiv ausgetauschten Daten geändert, und Sie müssen die Versionsnummer
inkrementieren. Wenn Sie die Versionsnummer unverändert lassen
und Dateien lesen, die mit vorherigen Versionen Ihrer Anwendung erzeugt
wurden, dann erhalten Sie eine Meldung wie »Ungültiges Dateiformat«, und
die Datei wird nicht gelesen. Nachdem Sie die Versionsnummer erhöht haben
und eine Datei lesen, die mit einer älteren Versionsnummer geschrieben wurde,
erhalten Sie zwar die gleiche Meldung, haben aber die Möglichkeit, die
Ausnahme mit eigenem Code zu behandeln und das Archiv in eine Umwandlungsroutine
umzuleiten, die die Datei in das neue Dateiformat konvertiert.
1. Welche zwei Makros müssen Sie in eine Klasse einfügen, um sie serialisierbar zu machen?
2. Wie kann man bestimmen, ob das CArchive
-Objekt aus der Archivdatei zu lesen
oder in die Archivdatei zu schreiben ist?
3. Welche Argumente sind an das Makro IMPLEMENT_SERIAL
zu übergeben?
4. Von welcher Klasse müssen Sie die Ansichtsklasse ableiten, damit Sie ein Formular für das Hauptfenster einer SDI- oder MDI-Anwendung mit dem Dialog-Editor erstellen können?
5. In welchen Dateityp schreibt CArchive
per Vorgabe?
Nehmen Sie Optionsfelder in das Formular auf, um das Geschlecht der Person festzulegen,
wie es Abbildung 13.5 zeigt. Binden Sie diese Änderungen in die Klasse CPerson
ein, um das Feld dauerhaft zu speichern.
Abbildung 13.5:
Die laufende Serialisierungsanwendung mit dem Geschlecht einer Person