vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 2

Tag 13

Dateizugriff

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 ...

Serialisierung

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 Klassen CArchive und CFile

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 Funktion Serialize

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.

Objekte serialisierbar machen

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.

Das Makro DECLARE_SERIAL einbinden

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 einbinden

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: }

Die Funktion Serialize definieren

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.

Eine serialisierbare Klasse implementieren

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.

Eine serialisierte Anwendung erstellen

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.

Eine lineare - oder »flache« - Datenbank ist der Urahn aller Datenbanken. Es handelt sich dabei um eine einfache, dateibasierte Datenbank, bei der die Datensätze sequentiell gespeichert sind und jeweils an den vorherigen Datensatz anschließen. Man vermißt hier die märchenhafte Funktionalität von relationalen Datenbanken, die mittlerweile zum Standard in der Datenbanktechnologie avanciert sind. Die Datenbank, die Sie in der heutigen Lektion erstellen, kommt einer alten - ohne Indexe arbeitenden - dBASE- oder Paradox-Datenbank näher als einer Datenbank wie Access oder SQL Server.

Das Anwendungsgerüst erstellen

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

Das Anwendungsfenster entwerfen

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.

Wenn Sie dialogfeldbasierende Anwendungen erstellen, nimmt der Anwendungs-Assistent keinerlei Code für die Serialisierung in Ihr Anwendungsgerüst auf. Wenn Sie eine derartige Anwendung serialisieren müssen, sind Sie für den entsprechenden Code selbst verantwortlich.

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.

Tabelle 13.1: Einstellungen der Steuerelementeigenschaften

Objekt

Eigenschaft

Einstellung

Text

ID

Titel

IDC_STATIC

&Name:

Eingabefeld

ID

IDC_ENAME

Text

ID

Titel

IDC_STATIC

&Alter:

Eingabefeld

ID

IDC_EAGE

Text

ID

Titel

IDC_STATIC

Familienstand:

Optionsfeld

ID

Titel

Gruppe

IDC_RSINGLE

&ledig

eingeschaltet

Optionsfeld

ID

Titel

IDC_RMARRIED

&verheiratet

Optionsfeld

ID

Titel

IDC_RDIVORCED

&geschieden

Optionsfeld

ID

Titel

IDC_RWIDOW

ver&witwet

Kontrollkästchen

ID

Titel

IDC_CBEMPLOYED

erwerbs&tätig

Schaltfläche

ID

Titel

IDC_BFIRST

&Erster

Schaltfläche

ID

Titel

IDC_BPREV

Vor&heriger

Schaltfläche

ID

Titel

IDC_BNEXT

N&ächster

Schaltfläche

ID

Titel

IDC_BLAST

Let&zter

Text

ID

Titel

IDC_SPOSITION

Datensatz 0 von 0

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.

Tabelle 13.2: Variablen für die Steuerelemente

Objekt

Name

Kategorie

Typ

IDC_CBEMPLOYED

m_bEmployed

Wert

BOOL

IDC_EAGE

m_iAge

Wert

int

IDC_ENAME

m_sName

Wert

CString

IDC_RSINGLE

m_iMaritalStatus

Wert

int

IDC_SPOSITION

m_sPosition

Wert

CString

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.

Eine serialisierbare Klasse erstellen

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.

Die Basisklasse erstellen

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.

Tabelle 13.3: Klassenvariablen für die Klasse CPerson

Name

Typ

m_bEmployed

BOOL

m_iAge

int

m_sName

CString

m_iMaritalStatus

int

Methoden zum Lesen und Schreiben von Variablen hinzufügen

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.

Eine Inline-Funktion ist eine kurze C++-Funktion, bei der der Funktionskörper beim Kompilieren der Anwendung anstelle des Funktionsaufrufs kopiert wird. Wenn die kompilierte Anwendung dann läuft, wird der Funktionscode ausgeführt, ohne daß ein Kontextsprung zur Funktion und nach Beendigung der Funktion wieder zurück erfolgen muß. Damit verringert sich der Overhead in der laufenden Anwendung, die Ausführungsgeschwindigkeit ist etwas besser, aber die ausführbare Anwendungsdatei nimmt etwas an Umfang zu. Je mehr Aufrufe der Inline-Funktion stattfinden, desto größer ist schließlich die Anwendung. Weitere Informationen zu Inline-Funktionen finden Sie im Anhang.

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: }

Die Klasse serialisieren

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: };

Als Zugriffsstatus für Funktionen und Variablen in C++-Klassen ist Public (öffentlich) voreingestellt. Alle Funktionen und Variablen, die vor der ersten Zugriffsdeklaration deklariert werden, sind per Vorgabe öffentlich. Es ist natürlich am einfachsten, alle öffentlichen Funktionen und Variablen von Klassen in diesem Bereich der Klassendeklaration unterzubringen, aber die explizite Deklaration der Zugriffsrechte für alle Funktionen und Variablen gehört zum besseren Programmierstil, weil damit eindeutig geklärt ist, wie die Funktionen oder Variablen der Klasse zugänglich (sichtbar) sind.

Die meisten C++-Funktionen erfordern ein Semikolon am Ende der Codezeile. Bei den beiden Makros zur Serialisierung ist das nicht notwendig, weil der C-Präprozessor die Makros insgesamt durch den Code ersetzt, bevor die Anwendung an den Compiler übergeben wird. Es schadet allerdings nicht, die Semikolons zu schreiben. Der Präprozessor ignoriert sie einfach.

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.

Wenn Sie eine Datei mit einer älteren Version der Funktion Serialize in Ihrer Klasse geschrieben haben, löst die Anwendung in der Praxis eine Exception aus, die man mit den üblichen Verfahren der Ausnahmebehandlung in C++ abfangen kann. Damit können Sie in Ihrer Anwendung Code vorsehen, um ältere Dateiversionen zu erkennen und geeignet zu konvertieren. Weitere Informationen zur Ausnahmebehandlung in C++ finden Sie im Anhang A.

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: }

Unterstützung in der Dokumentklasse

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.

Tabelle 13.4: Variablen der Dokumentklasse

Name

Typ

m_iCurPosition

int

m_oaPeople

CObArray

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

Neue Datensätze hinzufügen

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: }

Die aktuelle Position ermitteln

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: }

Durch den Recordset navigieren

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: }

Den Recordset serialisieren

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: }

Aufräumarbeiten

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: }

Ein neues Dokument öffnen

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.

Wenn Sie diesen Code schreiben, sollten Sie beachten, daß der Typ des Zeigers auf die Ansicht in einen Zeiger der Klasse des Ansichtsobjekts umzuwandeln ist. Die Funktion GetNextView liefert einen Zeiger vom Typ CView, so daß keine Aufrufe der zusätzlichen Funktionen in der Ansichtsklasse möglich sind, bis Sie den Zeiger in Ihre Ansichtsklasse umwandeln. Die Typumwandlung des Zeigers teilt dem Compiler mit, daß der Zeiger eigentlich ein Zeiger auf die Objekte Ihrer Ansichtsklasse ist und demzufolge alle Funktionen enthält, die Sie hinzugefügt haben. Wenn Sie den Zeiger nicht umwandeln, nimmt der Compiler an, daß das Ansichtsobjekt keine der von Ihnen hinzugefügten Funktionen enthält, und läßt das Kompilieren der Anwendung nicht zu.

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: }

Navigation und Bearbeitung in der Ansichtsklasse unterstützen

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: .

Den aktuellen Datensatz anzeigen

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: }

Durch den Recordset navigieren

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.

Änderungen speichern

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.

Einen neuen Recordset anzeigen

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: }

Das Projekt umhüllen

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

Zusammenfassung

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.

Fragen und Antworten

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.

Workshop

Kontrollfragen

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?

Übung

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



vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbackKapitelanfangnächstes Kapitel


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