Serialisierung
Haben wir bisher die Bearbeitung von Daten mehr oder weniger außen vorgelassen,
so werden wir uns in dieser Lektion ansehen, wie innerhalb einer Doc/View Anwendung
Dokumentdaten verarbeitet werden. Dazu werden wir zwei Fälle betrachten:
- Die Daten des Dokuments sind direkt in der Dokumentenklasse abgelegt.
- Die Daten des Dokuments sind in einer serialisierten Klasse abgelegt.
Fangen wir mit dem ersten Fall an: die Daten sind direkt in der Dokumentenklasse
abgelegt.
 |
Erstellen Sie zunächst, wie inzwischen hoffentlich bekannt,
eine SDI MFC-Anwendung mittels des Anwendungs-Assistenten. Beachten Sie bei
der Erstellung bitte, dass im Schritt 3 die ActiveX-Steuerelemente und im Schritt
4 alle Merkmale außer den 3D-Steuerelemente deaktiviert werden. Geben Sie dem
Projekt den Namen Serialize. |
Fügen wir zuerst zu unserem Dokument die Daten hinzu. Damit das Ansichtsobjekt
nachher die Daten einfacher auslesen kann, deklarieren wir eine Struktur, die die
Daten zusammenfasst.
 |
Da der Klassen-Assistent keine Strukturen erzeugen kann müssen
wir diese von Hand einbringen. Öffnen Sie dazu die Header-Datei SerializeDoc.h
indem Sie in der Klassenansicht einen Doppelklick auf den Klassennamen des Dokuments
durchführen. Fügen Sie dann oberhalb der Klasse CSerializeDoc folgende
Deklaration ein:
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
// Die Daten des Dokuments
struct strDATA
{
short nNoOfValue;
short *pnData;
CString CText;
};
class CSerializeDoc : public CDocument
{
....
} |
Anschließend fügen Sie der Klasse CSerializeDoc noch über den Klassen-Assistenten
ein Variable m_Data vom Typ der neuen Struktur hinzu.

|
Damit enthält unser Dokument nun drei Daten, die innerhalb der Struktur abgelegt
sind. pnData ist ein Zeiger auf ein nachher noch dynamisch zu erstellendes
Feld und nNoOfValue enthält die aktuelle Feldgröße. Die Variable CText
dient zur Aufnahme eines beliebigen Textes.
Gehen wir jetzt ans Initialisieren der Daten. Wir könnten die Daten im Prinzip
im Konstruktor des Dokuments initialisieren. Da aber die Daten auch dann wieder
neu initialisiert werden sollen, wenn ein neues Dokument über den Menüpunkt
Datei-Neu erstellt wird, initialisieren wir die Daten erst in der Methode
OnNewDocument(...).
 |
Erweitern Sie dazu die Methode OnNewDocument(...)
wie folgt:
BOOL CSerializeDoc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;
// ZU ERLEDIGEN: Hier Code zur Reinitialisierung einfügen
// (SDI-Dokumente verwenden dieses Dokument)
// Dok-Daten initialisieren
m_Data.CText = "Demo Programm";
m_Data.nNoOfValue = 10;
m_Data.pnData = new short[m_Data.nNoOfValue];
for (int iIndex=0; iIndex<m_Data.nNoOfValue; iIndex++)
m_Data.pnData[iIndex] = iIndex;
return TRUE;
} |
|
Wenn Sie sich den obigen Code mal genau ansehen, so sollten bei Ihnen sofort
die Alarmglocken läuten. In der Methode wird der Speicher für das short-Feld dynamisch
allokiert und muss demnach auch irgendwo wieder freigegeben werden. Was hier also
noch fehlt ist eine Methode die immer dann aufgerufen wird, wenn die aktuellen Dokumentdaten
gelöscht werden. Und genau diese Funktion hat die bereits vorhin behandelte Methode
DeleteContents(...). Bauen wir diese Methode in das Beispiel ein.
 |
Fügen Sie über den Klassen-Assistenten zunächst die Methode
DeleteContents(...) zur Klasse CSerializeDoc hinzu. Sie finden
diese Methode im Popup-Menü des Klassen-Assistenten unter dem Menüpunkt
Virtuelle Methoden hinzufügen..... Geben Sie innerhalb dieser Methode den
reservierten Speicher frei und setzen Sie den char-Zeiger anschließend auf den
Wert NULL.
void CSerializeDoc::DeleteContents()
{
// TODO: Speziellen Code hier einfügen und/oder Basisklasse aufrufen
// Belegten Speicher
freigeben
delete [] m_Data.pnData;
m_Data.nNoOfValue = 0;
m_Data.pnData = NULL;
CDocument::DeleteContents();
} |
Übersetzen und starten Sie das Beispiel jetzt. Was sehen Sie?
|
Vermutlich werden Sie ein Meldung in folgender Form erhalten:

Was ist hier passiert? Klicken Sie den Button Wiederholen
im Dialog an um zum Debugger zu gelangen. Laut Dialog hat die Anwendung in einer
Funktion in der Datei dbgheap.c einen Fehler ausgelöst. Sehen wir uns die
Aufrufliste (Stack-Trace) hierzu an. Sie erreichen die Aufrufliste übrigens über
folgendes Symbol:


Sehen Sie sich die Zeile 3 an, den Aufruf des delete-Operators.
Der delete-Operator erhält als Parameter einen Zeiger auf den freizugebenden
Bereich. Dieser Zeiger enthält aber eine unzulässige Adresse! Ferner können Sie
der Aufrufliste entnehmen, dass der delete-Operator aus der CSerializeDoc
Methode DeleteContents(...) heraus aufgerufen wurde. In der DeleteContents(...)
Methode wiederum wird in der Zeile 110 der Speicher für das short-Feld des Dokuments
freigegeben. Daraus lässt sich schließen, dass der Zeiger auf den freizugebenden
Speicherbereich zum Zeitpunkt des Aufrufs der DeleteContents(...) Methode
noch nicht initialisiert war.
 |
Aus der Anzeige des Aufrufliste kann man auch sehr gut ersehen, welche Methoden
in welcher Reihenfolge aufgerufen werden. |
Erinnern Sie sich noch an die Lektion über die Dokumentenklasse?
Dort haben wir unter anderem die Reihenfolge der Aufrufe der einzelnen Methoden
untersucht und dabei herausgefunden, dass die DeleteContents(...) Methode
auch unmittelbar nach der Erstellung des ersten Dokuments und vor der Methode
OnNewDocument(...) aufgerufen wird. Korrigieren wir jetzt noch diesen 'kleinen'
Fehler.
 |
Öffnen Sie den Konstruktor der Dokumentenklasse CSerializeDoc
und initialisieren Sie den Zeiger m_pszText dort mit NULL.
CSerializeDoc::CSerializeDoc()
{
// ZU ERLEDIGEN: Hier Code für One-Time-Konstruktion einfügen
m_Data.pnData = NULL;
m_Data.nNoOfValue = 0;
} |
|
Damit haben wir die Behandlung der Daten abgeschlossen. Sie könnten das Programm
jetzt übersetzen und starten ohne eine Fehlermeldung zu erhalten.
Wenden wir uns jetzt der Darstellung der Daten im Ansichtsobjekt zu. Da wir,
streng nach C++ Vorschrift, die Daten gekapselt haben, benötigen wir noch eine Methode
die es dem Ansichtsobjekt später erlaubt, auf die Daten zuzugreifen.
 |
Fügen Sie über den Klassen-Assistent die nachfolgende Methode
zur Dokumentenklasse hinzu. Beachten Sie dabei bitte, dass die Methode public
sein muss da sie vom Ansichtsobjekt aus aufgerufen wird.

Erweitern Sie jetzt den Code der Methode wie folgt:
strDATA* CSerializeDoc::GetData()
{
return &m_Data;
} |
Der Rest der notwendigen Arbeiten für die Darstellung der Daten ist einfach.
Versuchen Sie jetzt einmal selbst, die im Dokument enthaltenen Daten mit Hilfe
der obigen Methode im Debuggerfenster auszugeben. Sie wissen doch hoffentlich
noch das Makro, das für die Ausgabe der Daten zuständig ist?
 |
Bevor Sie an dieser Übung verzweifeln kann ich Ihnen auch meine Lösung
anzeigen. |
Lösung zur Ausgabe der Dok-Daten
void CSerializeView::OnDraw(CDC*
pDC)
{
CSerializeDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// ZU ERLEDIGEN: Hier Code zum Zeichnen der ursprünglichen Daten
hinzufügen
strDATA* pData
= pDoc->GetData();
TRACE("%s\n",pData->CText);
for (int iIndex=0; iIndex<pData->nNoOfValue; iIndex++)
TRACE("%d,",pData->pnData[iIndex]);
TRACE("\n");
} |
Zuerst holt sich das Ansichtsobjekt mit Hilfe der Dokument-Methode
GetData(...) den Zeiger auf die Dokumentdaten. Über diesen Zeiger
werden dann die einzelnen Daten ausgegeben. Ist doch gar nicht so schwer,
oder?
Ende der Lösung
|
|
Lassen Sie uns noch ein klein wenig mit den Dokumentdaten spielen. Bisher haben
sich die Dokumentdaten nicht verändert. Damit wir nachher bei der Serialisierung
auch sehen, dass tatsächlich die aktuellen Daten abgespeichert werden, fügen wir
zunächst zur Dokumentenklasse eine Methode hinzu, die alle Daten des short-Feldes
inkrementiert.
 |
Fügen Sie über den Klassen-Assistenten die public Methode
IncData(...) wie nachfolgend angegeben hinzu. Beachten Sie bitte, dass
die Daten explizit durch den Aufruf von SetModifiedFlag(...) als geändert
gekennzeichnet werden müssen, sonst funktioniert nachher die Serialisierung
nicht richtig! Zusätzlich müssen Sie die Ansichtsobjekte noch von dieser Änderung
benachrichtigen, damit diese die neuen Daten darstellen. Dies erfolgt durch
den Aufruf der Methode UpdateAllViews(...).
void CSerializeDoc::IncData()
{
// Daten um eins erhoehen
for (int iIndex=0; iIndex<m_Data.nNoOfValue;
iIndex++)
m_Data.pnData[iIndex]++;
// Und Dokument als veraendert
kennzeichen!
SetModifiedFlag();
// Und Ansichtsobjekte von
der Aenderung benachrichtigen
UpdateAllViews(NULL);
} |
|
Bleibt noch der Aufruf der Methode IncData(...) übrig. In unserem Beispiel
sollen die Daten immer dann verändert werden, wenn das Fenster in seiner Größe verändert
wurde.
 |
Fügen Sie über den Klassen-Assistenten den Nachrichtenbearbeiter
für die WM_SIZE hinzu und erweitern die Methode wie folgt:
void CSerializeView::OnSize(UINT
nType, int cx, int cy)
{
CView::OnSize(nType, cx, cy);
// TODO: Code für die Behandlungsroutine für Nachrichten hier
einfügen
CSerializeDoc* pDoc
= GetDocument();
pDoc->IncData();
} |
Übersetzen und starten Sie das Programm jetzt. Sie sollten bei jeder Größenänderung
des Fensters geänderte Daten im Ausgabefenster des Debuggers erhalten.
|
Nach dieser ausführlichen Vorarbeit, die Ihnen den grundsätzlichen Ablauf der
Behandlung der Dokumentdaten aufzeigen sollte, gehen wir jetzt an die Serialisierung.
Wie Sie bereits in einer der vorherigen Lektionen erfahren haben, erfolgt die Serialisierung
über die Methode Serialize(...) des Dokuments.
 |
Sehen Sie sich zuerst noch einmal die Methode Serialize(...)
an, indem Sie einen Doppelklick in der Klassenansicht auf die Methode ausführen.
void CSerializeDoc::Serialize(CArchive&
ar)
{
if (ar.IsStoring())
{
// ZU ERLEDIGEN: Hier Code zum Speichern
einfügen
}
else
{
// ZU ERLEDIGEN: Hier Code zum Laden
einfügen
}
} |
|
Die Methode Serialize(...) erhält als Parameter ar eine Referenz
auf ein Objekt vom Typ CArchive. Die Klasse CArchive ist sozusagen
eine fast allmächtige Dateiklasse. Sie erlaubt einer Anwendung nicht nur einfache
Daten abzuspeichern und wieder einzulesen, sondern auch komplexe Objektbeziehungen.
Wenn z.B. ein Objekt einen Zeiger auf ein weiteres Objekt enthält, so würde es nicht
viel Sinn machen diesen Objektzeiger abzuspeichern. Beim späteren Einlesen der Daten
werden die über diesen Zeiger adressierten Objektdaten mit großer Wahrscheinlichkeit
nicht mehr an der gleichen Stelle im Speicher zu liegen kommen. Vielmehr muss hier
anstelle des Zeigers das Objekt selbst abgespeichert werden. Beim Einlesen der Daten
ist dann aber der umgekehrte Vorgang durchzuführen, d.h. es muss zuerst das entsprechende
Objekt erstellt werden, dann können die Objektdaten eingelesen werden und zum Schluss
muss auch noch der Zeiger richtig initialisiert werden. All dies besorgt die Klasse
CArchive ohne Ihr zutun, wenn Sie sie richtig anwenden.
Sehen wir uns einige der wichtigsten Methode der Klasse CArchive an.
Ein der Methoden ist oben im Listing bereits aufgeführt, die Methode IsStoring(...).
Mit Hilfe dieser Methode kann festgestellt werden, ob das Archiv zum Schreiben geöffnet
ist. Zu IsStoring(...) gibt es auch noch ein Gegenstück IsLoading(...).
Zum Schreiben und Lesen von Daten mit Standard-Datentypen wir z.B. short
oder int, enthält CArchive die überladenen Operatoren << bzw. >>.
Auch einige MFC-Klassen, wie z.B. die Klasse CString, besitzen diese überladenen
Operatoren. Sollen beliebige Daten geschrieben bzw. gelesen werden, so sind hierfür
die Methoden Write(...) bzw. Read(...) einzusetzen. Aber wie gesagt,
dies sind nur die wichtigsten CArchive Methoden, nämlich die, die für uns
im Augenblick von Interesse sind.
 |
So, versuchen Sie nun wieder einmal selbst, die Dokumentdaten
abzuspeichern und wieder einzulesen. Versuchen Sie herauszubekommen, wann die
Operatoren >> bzw. << eingesetzt werden können und wann nicht.
 |
Bevor Sie an dieser Übung verzweifeln kann ich Ihnen auch wieder die
Lösung anzeigen. |
Lösung zur Serialisierung der Dok-Daten
void CSerializeDoc::Serialize(CArchive&
ar)
{
if (ar.IsStoring())
{
// ZU ERLEDIGEN: Hier Code zum Speichern
einfügen
ar << m_Data.CText;
ar << m_Data.nNoOfValue;
ar.Write(m_Data.pnData,sizeof(short)*m_Data.nNoOfValue);
}
else
{
// ZU ERLEDIGEN: Hier Code zum Laden
einfügen
ar >> m_Data.CText;
ar >> m_Data.nNoOfValue;
m_Data.pnData = new short[m_Data.nNoOfValue];
ar.Read(m_Data.pnData,sizeof(short)*m_Data.nNoOfValue);
}
} |
Das Schreiben der Daten dürfte eigentlich keine Schwierigkeiten bereitet
haben. Das CString-Objekt und den short-Wert können Sie
direkt mittels des Operators << in die Datei schreiben. Das Datenfeld hingegen
müssen Sie über die Write(...) abspeichern da CArchive
hierfür keinen überladenen Operator zur Verfügung stellt. Beim Aufruf der
Write(...) Methode müssen Sie aber beachten, dass Sie hier die
Anzahl der Bytes angegeben müssen die geschrieben werden sollen.
Etwas schwieriger gestaltet sich das Einlesen. Das CString-Objekt
und der short-Wert werden durch den Operator >> eingelesen. Anschließend
muss das Datenfeld für die Aufnahme der short-Daten erstellt werden.
Vor dem Aufruf von Serialize(...) zum Einlesen der Daten wurden
die alten Daten bereits durch die Methode DeleteContents(...) entfernt.
Und DeleteContents(...) löscht das short-Feld! Nachdem das
short-Feld angelegt wurde, können die Daten dann mittels Read(...)
eingelesen werden.
Ende der Lösung
|
|
Fertig ist unser erstes Beispiel zur Serialisierung. Das fertige Beispiel finden
Sie im Programmverzeichnis unter 04DocView\Serialize1.
Kommen wir nun zum nächsten Fall der Serialisierung: die Daten sind in einer
serialisierten Klasse abgelegt. Doch was unterscheidet eine serialisierte Klasse
von einer 'normalen' Klasse? Nun, eine serialisierte Klasse muss folgende vier Eigenschaften
besitzen:
- Sie muss von der MFC Klasse CObject abgeleitet sein.
- Sie muss die Makros DECLARE_SERIAL(...) und IMPLEMENT_SERIAL(...)
besitzen.
- Sie muss einen Standard-Konstruktor besitzen.
- Und Sie muss die virtuelle Methode Serialize(...) überschreiben.
Sehen wir uns an Hand eines weiteren Beispiels eine serialisierte Klasse einmal
an.
 |
Kopieren Sie zunächst aus dem Programmverzeichnis zum Kurs
das Projekt 99Templates\Serialize2 in Ihr Arbeitsverzeichnis. Öffnen
Sie anschließend die Klasse CData durch einen Doppelklick auf den Klassennamen
in der Klassenansicht.
class CData : public CObject
{
DECLARE_SERIAL(CData);
public:
CData (int nNumber);
void Serialize (CArchive& ar);
void IncData (void);
void GetData (CString& CText, short& nEntries, short** pnData);
CData();
virtual ~CData();
private:
short m_nNoOfValues;
short *m_pnData;
CString m_CText;
} |
|
Wenn Sie sich die Klasse ansehen, werden Sie alle vier vorhin genannten Eigenschaften
darin finden. Die Klasse besitzt die gleichen Daten, die im ersten Beispiel direkt
in der Dokumentenklasse abgelegt waren. Ferner werden Sie außer dem Standard-Konstruktor
einen zweiten Konstruktor entdecken. Wofür dieser benötigt wird erfahren Sie gleich
noch. Die Methode IncData(...) dient wieder zur Manipulation der in der
Klasse abgelegten Daten und die Methode GetData(...) zum Auslesen der Daten,
damit diese dann später in der OnDraw(...) Methode des Ansichtsobjekts
ausgegeben werden können.
 |
Damit Sie auch gleich etwas zu tun bekommen, versuchen Sie
einmal die Methode Serialize(...) der Datenklasse zu vervollständigen.
Wie Sie in der Zwischenzeit wissen, dient diese Methode zum Schreiben und Lesen
der Daten.
 |
Bevor Sie an dieser Übung verzweifeln kann ich Ihnen auch wieder die
Lösung anzeigen. |
Lösung zur Serialisierung der Datenklasse
void CData::Serialize(CArchive&
ar)
{
if (ar.IsStoring())
{
// Daten in Datei ablegen
ar << m_CText;
ar << m_nNoOfValues;
ar.Write(m_pnData,sizeof(short)*m_nNoOfValues);
}
else
{
// Daten aus Datei lesen
ar >> m_CText;
ar >> m_nNoOfValues;
// Zuerst Feld fuer die Daten anlegen
m_pnData = new short[m_nNoOfValues];
// und dann Daten einlesen!
ar.Read(m_pnData,sizeof(short)*m_nNoOfValues);
}
} |
Diese Übung dürfte eigentlich nicht sonderlich schwierig gewesen sein.
Das einzige worauf Sie hier achten mussten ist, dass Sie auch das Datenfeld
beim Einlesen der Daten anlegen müssen.
Ende der Lösung
|
|
Sehen wir uns nun die Dokumentenklasse an, so wie Sie vorgegeben ist. Wenn Sie
sich die Klasse in der Klassenansicht ansehen werden Sie feststellen, dass sie jetzt
nur noch ein einziges Datum m_pData enthält. Über diesen Zeiger ist die
Datenklasse mit dem Dokument verknüpft.
 |
Öffnen Sie jetzt einmal die Dokumentenklasse und sehen sich
dann die Methode OnNewDocument(...) an.
BOOL CSerializeDoc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;
// ZU ERLEDIGEN: Hier Code zur Reinitialisierung einfügen
// (SDI-Dokumente verwenden dieses Dokument)
int nNumber = rand() % 10 + 3;
m_pData = new CData(nNumber);
return TRUE;
} |
|
OnNewDocument(...) wird, wie der Name schon sagt, immer dann aufgerufen,
wenn ein neues Dokument erstellt wird. Bei SDI-Anwendung ist dies beim Starten der
Anwendung und bei der Auswahl des Menüs Datei-Neu der Fall. Beim Erstellen
eines neuen Dokuments wird nun ein neues Datenobjekt mit einer zufälligen Anzahl
(im Bereich von 3 bis 12) von short-Daten erstellt. Dies ist auch der Grund,
warum unsere Datenklasse zusätzlich einen Konstruktor mit einem Parameter enthält.
Beachten Sie im Zusammenhang mit OnNewDocument(...) auch die Methode
DeleteContents(...). Sie wissen doch noch: DeleteContents(...)
wird immer dann aufgerufen, wenn ein Dokument seine Daten löscht. In dieser Methode
wird folglich das Datenobjekt CData des Dokuments gelöscht.
Außerdem enthält die Dokumentenklasse noch die Methode GetData(...),
die es später dem Ansichtsobjekt erlaubt auf die Dokumentdaten überhaupt zuzugreifen.
Beachten Sie hierbei bitte, dass diese Methode public deklariert sein muss.
Kommen wir nun zur Ansichtsklasse. Diese Klasse enthält im Prinzip nichts Neues.
Auch hier werden innerhalb der Methode OnSize(...) die Dokumentdaten verändert.
Der einzigen Unterschied zur OnSize(...) Methode im vorherigen Beispiel
besteht darin, dass die Methode IncData(...) zum Verändern der Daten nun
nicht mehr zur Klasse des Dokuments gehört sondern zur Datenklasse CData.
Aus diesem Grund erfolgt der Aufruf dieser Methode nun zweifach indirekt.
 |
Und wieder kommt etwas Arbeit auf Sie zu. Sie sollen jetzt
die Methode OnDraw(...) des Ansichtobjekts vervollständigen. In dieser
Methode soll zum einen der im Datenobjekt abgelegte String ausgegeben werden
und zum anderen alle im short-Feld abgelegten Daten.
 |
Obwohl diese Übung wirklich nicht besonders schwer ist zeige ich Ihnen
auch hier wieder gerne meine Lösung. Aber erst, nach dem Sie es selber versucht
haben! Sehen Sie sich auch das Prototyping der entsprechenden Methode der
Datenklasse CData an um die Daten auszulesen. |
Lösung zur Ausgabe der Daten
void CSerializeView::OnDraw(CDC*
pDC)
{
CSerializeDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// ZU ERLEDIGEN: Hier Code zum Zeichnen der ursprünglichen Daten
hinzufügen
// Variablen fuer
die Dok-Daten definieren
CString
CText;
short nEntries;
short *pnData;
// Dok-Daten auslesen
pDoc->GetData()->GetData(CText,nEntries,&pnData);
// und dann ausgeben
TRACE("%s\n",CText);
for (int iIndex=0; iIndex<nEntries; iIndex++)
TRACE("%d,",pnData[iIndex]);
TRACE("\n");
} |
Die einzige 'Schwierigkeit' bei dieser Übung bestand darin, dass der
Zugriff auf die Daten hier ebenfalls zweifach indirekt erfolgt.
Ende der Lösung
|
Sie können das Beispiel nun übersetzen und starten. Im Debuggerfenster werden
dann nach dem Starten des Programms einige Daten ausgegeben. Und jedes Mal wenn
Sie das Fenster in seiner Größe verändern, werden die Daten im OnSize(...)
Handler um eins erhöht und anschließend erneut ausgegeben. Wählen Sie auch einmal
den Menüpunkt Datei-Neu aus. Sie werden eine Abfrage erhalten, ob die
geänderten Daten abgespeichert werden sollen. Diese Abfrage wird durch den Aufruf
der Methode SetModifiedFlag(...) in der OnSize(...)
Methode ausgelöst. Da wir aber zum jetzigen Zeitpunkt die Serialisierung noch
nicht implementiert haben, ist diese Abfrage jedoch noch wirkungslos. Außer
dass eventuell bestehenden Dateien nachher die Länge 0 haben!
|
So, kommen wir nun endlich wieder zur Serialisierung zurück. Das im Beispiel
innerhalb des Dokuments nur ein Zeiger auf die Datenklasse enthalten ist und nicht
etwa die Klasse selbst (als eingeschlossenes Objekt) hat seinen Grund. Ist eine
Klasse serialisiert so reicht es in der Methode Serialize(...) des Dokuments
aus, den Zeiger auf die Dokumentdaten zu schreiben bzw. einzulesen. Aber gehen wir
der Reihe nach vor. Anfangen werden wir mit dem Schreiben der Daten.
 |
Erweitern Sie nun die Serialize(...) Methode des Dokuments
um die Anweisung zum Schreiben des Datenzeigers.
void CSerializeDoc::Serialize(CArchive&
ar)
{
if (ar.IsStoring())
{
// ZU ERLEDIGEN: Hier Code zum Speichern
einfügen
ar << m_pData;
}
else
{
// ZU ERLEDIGEN: Hier Code zum Laden
einfügen
}
} |
Und das war es auch schon. Übersetzen und starten Sie das Programm nun und
speichern danach die Daten einmal gleich ab.
|
Beim Schreiben des Datenzeigers wird durch das MFC Rahmenprogramm irgend wann
die Serialize(...) Methode der Datenklasse aufgerufen, die dann letztendlich
die Daten ausgibt. Sie können sich den Inhalt der Datei auch einmal in binärer Form
ansehen.

Sie werden dann feststellen, dass das Rahmenprogramm außer den eigentlichen Daten
auch den Namen der Datenklasse mit in die Datei geschrieben hat. Beachten Sie bei
den nummerischen Daten, dass diese im INTEL-Format abgelegt sind, d.h. das Low-Byte
kommt zuerst und dann das High-Byte.
Sehen wir uns jetzt das Einlesen der Daten an.
 |
Wie bereits oben erwähnt, erfolgt das Lesen der Daten aus der
Datei fast genauso wie das Schreiben der Daten. Lediglich der Ausgabeoperator
<< wird durch den Eingabeoperator >> ersetzt.
void CSerializeDoc::Serialize(CArchive&
ar)
{
if (ar.IsStoring())
{
// ZU ERLEDIGEN: Hier Code zum Speichern
einfügen
ar << m_pData;
}
else
{
// ZU ERLEDIGEN: Hier Code zum Laden
einfügen
ar >> m_pData;
}
} |
|
Doch wie funktioniert das Ganze nun beim Einlesen eigentlich? Wie Sie sich vielleicht
noch erinnern, wird vor dem Aufruf der Methode Serialize(...) durch den
MFC-Rahmen die Methode DeleteContents(...) aufgerufen um die aktuellen
Daten zu löschen. Anschließend wird Serialize(...) aufgerufen. Der Datenzeiger
zeigt zu diesem Zeitpunkt aber auf kein gültiges Datenobjekt, denn dieses wurde
ja vorher in DeleteContents(...) gelöscht. Durch die Basisklasse CObject
der Datenklasse wird nun beim Einlesen zuerst dynamisch ein neues Datenobjekt erstellt
und dann dessen Standard-Konstruktor (der ohne Parameter) aufgerufen. Diese Erstellung
des neuen Datenobjektes ist übrigens auch Sinn und Zweck der beiden XXX_SERIAL(...)
Makros in der Datenklasse. Ist das Datenobjekt erst einmal erstellt, so wird dessen
Methode Serialize(...) aufgerufen um die eigentlichen Daten dann einzulesen.
Nach dem Einlesen wird dann noch das Ansichtsobjekt davon benachrichtigt damit die
Daten neu dargestellt werden.
Dieses Einlesen von Daten über Datenzeiger auf serialisierte Klassen kann in
der Praxis beliebig geschachtelt sein. D.h. es würde auch genauso funktionieren,
wenn unsere Datenklasse einen Zeiger auf weitere Datenklassen besäße.
Damit könnten wir eigentlich die Serialisierung beenden. Sie haben erfahren,
wie Daten direkt abgespeichert und gelesen werden und wie das Ganze mit Hilfe der
Serialisierung über die Klasse CObject funktioniert. Wir wollen uns zum
Schluss aber noch ein kleines Bonbon gönnen. In manchen Fällen kann es durch aus
sinnvoll sein, die Ansicht der Daten beim Laden genauso wieder herzustellen, wie
sie beim Abspeichern der Daten vorlag, d.h. die Fenstergröße und -position sollte
z.B. auch mit abgespeichert werden.
 |
Fügen Sie zur Klasse des Rahmenfensters zunächst mit
Hilfe des Klassen-Assistenten die virtuelle Methode Serialize(...)
hinzu und erweitern diese wie folgt:
void CMainFrame::Serialize(CArchive&
ar)
{
WINDOWPLACEMENT
WndPlace;
if (ar.IsStoring())
{ // Code wird gespeichert
// Fensterdatan abspeichern
GetWindowPlacement(&WndPlace);
ar.Write(&WndPlace,sizeof(WndPlace));
}
else
{ // Code wird geladen
// Fensterdaten einlesen
ar.Read(&WndPlace,sizeof(WndPlace));
SetWindowPlacement(&WndPlace);
}
} |
Anschließend muss aus der Serialize(...) Methode des Dokuments nur
noch diese Methode aufrufen.
void CSerializeDoc::Serialize(CArchive&
ar)
{
if (ar.IsStoring())
{
....
}
else
{
....
}
AfxGetApp()->m_pMainWnd->Serialize(ar);
} |
Übersetzen und starten Sie das Beispiel nun. Das fertig Beispiel finden Sie
auch unter 04DocView\Serialize2.
|
Beachten Sie bitte, dass Sie hier nicht einfach den Zeiger auf das Rahmenfenster
schreiben bzw. lesen können. Die Klasse CFrameWnd besitzt keine überladenen
Ein- bzw. Ausgabeoperatoren. Ferner müssen Sie die Daten des Rahmenfensters in die
Datei schreiben, da das Ansichtsobjekt durch die MFC immer an das Rahmenfenster
anpasst wird.
Damit ist dieses Kapitel im Prinzip beendet. Zum Schluss können Sie sich nun
noch einige Tipps&Tricks zum Thema Serialisierung ansehen.
|