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.

Die Daten des Dokuments

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:

Eine Assertion!

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:

Die Aufrufliste

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.

Zugriff auf die Dok-Daten

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?

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.

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.

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.

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.

Die geschriebene Datenklasse

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.



Copyright © 2004

Senden Sie Emails mit Fragen oder Kommentaren zu dieser Website an: mailto:info@cpp-tutor.de
 Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein. Tel: +49 7129 6470