Dokumentenklasse

Wie schon mehrfach erwähnt, werden die Daten einer Anwendung beim Doc/View Modell innerhalb eines Objektes einer von CDocument abgeleiteten Klasse abgespeichert. Da der Anwendungs-Assistent aber logischerweise nichts über die abzuspeichernden Daten wissen kann, erzeugt er nur einen Klassenrahmen. Sehen wir uns in der Klassenansicht diesen Rahmen zunächst an:

Die Dokumentenklasse

Zwei der erzeugten Methoden sollten Ihnen vom Namen her bekannt vorkommen: die Methoden AssertValid(...) und Dump(...). Der Zwecke dieser Methoden wird später behandelt. Weiterhin besitzt die Klasse noch einen Konstruktor und einen Destruktor. Wenn Sie sich den Klassenbaum oben genau ansehen werden Sie vor dem Konstruktor ein Schlüsselsymbol erkennen. Methoden die mit einem Schlüsselsymbol gekennzeichnet sind, sind protected-Elemente der Klasse, d.h. Sie können kein CDVBasisDoc-Objekt direkt erstellen. Das CDBasisDoc-Objekt wird indirekt über Dokumentenverwaltung erstellt die in der übernächsten Lektion behandelt wird. Die beiden letzten Methoden OnNewDocument(...) und Serialize(...) dienen zur Verarbeitung der Dokumentdaten und werden gleich noch ausführlich behandelt.

Sehen wir uns nun einen Auszug aus der Klassendeklaration der Dokumentenklasse CDVBasisDoc an. Falls Sie das Projekt DVBasis noch nicht geöffnet haben, öffnen Sie es jetzt. Führen dann in der Klassenansicht einen Doppelklick auf die Klasse CDVBasisDoc aus:
class CDVBasisDoc : public CDocument
{
protected: // Nur aus Serialisierung erzeugen
    CDVBasisDoc();
    DECLARE_DYNCREATE(CDVBasisDoc)

    ....
protected:

// Generierte Message-Map-Funktionen
protected:
    //{{AFX_MSG(CDVBasisDoc)
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};

Die Dokumentenklasse ist zunächst public von der MFC Klasse CDocument abgeleitet und enthält unter anderem einen protected-Konstruktor.

Nach der Deklaration des Konstruktor folgt wieder eines der vielen MFC-Makros, das Makro DECLARE_DYNCREATE(...), die einem den Einstieg in die MFC-Programmierung nicht gerade erleichtern, bei der täglichen Arbeit später aber sehr hilfreich sind. Außer DECLARE_DYNCREATE(...) gibt es noch zwei weitere ähnliche Makros die wie uns auch gleich in ansehen:

Makro

Bedeutung

DECLARE_DYNAMIC Fügt zur Klasse Laufzeit-Informationen mittels der Klasse CRuntimeClass hinzu.  Intern werden der Klasse u.a. der Klassenname so wie die Objektgröße hinzugefügt.
DECLARE_DYNCREATE Wie DECLARE_DYNAMIC, erlaubt jedoch zusätzlich die dynamische Erstellung eines Objektes der Klasse.
DECLARE_SERIAL Wie DECLARE_DYNCREATE, zusätzlich werden die Operatoren '>>' bzw. '<<' für die Ein- und Ausgabe eines Objektes überladen.

Sehen wir uns einen Auszug aus der Implementierung der Dokumentenklasse an.

Führen Sie einen Doppelklick auf den Konstruktor der Klasse aus um den Code der Klasse im Editorfenster angezeigt zu bekommen.
IMPLEMENT_DYNCREATE(CDVBasisDoc, CDocument)

BOOL CDVBasisDoc::OnNewDocument()
{
    if (!CDocument::OnNewDocument())
        return FALSE;

    // ZU ERLEDIGEN: Hier Code zur Reinitialisierung einfügen
    // (SDI-Dokumente verwenden dieses Dokument)

    return TRUE;
}

void CDVBasisDoc::Serialize(CArchive& ar)
{
    if (ar.IsStoring())
    {
        // ZU ERLEDIGEN: Hier Code zum Speichern einfügen
    }
    else
    {
        // ZU ERLEDIGEN: Hier Code zum Laden einfügen
    }
}

Als erstes folgt das Makro IMPLEMENT_DYNCREATE(...). Es ist das Gegenstück zum Makro DECLARE_DYNCREATE(...) das Sie schon bei der Klassendeklaration kennen gelernt haben. IMPLEMENT_DYNCREATE(...) erzeugt eine Struktur die u.a. den Namen der Klasse sowie deren Größe (in Bytes) enthält. Außerdem wird in der Struktur vermerkt, ob und von welcher weiteren Klasse die Klasse abgeleitet ist. Durch dieses 'Wissen' kann ein Objekt der Klasse dynamisch, d.h. zur Laufzeit des Programms, erstellt werden. Dies ist eine der Grundvoraussetzungen für die Serialisierung (Abspeichern und Laden) von Objekten.

Die nächste interessante Methode in obigen Klassenbaum ist die Methode OnNewDocument(...). OnNewDocument(...) wird immer dann aufgerufen wenn ein neues Dokument erstellt wird. Bei SDI-Anwendungen, und eine solche haben wir hier erstellt, wird die Methode nach dem Starten des Programms einmal aufgerufen und dann nur noch wenn im Datei-Menü den Menüpunkt Neu ausgewählt wird. Diese Methode wird nicht aufgerufen wenn ein Dokument über den Menüpunkt Datei-Öffnen... geladen wird! Merke:

SDI-Anwendungen verwenden in der Regel immer das gleiche Dokument, auch wenn eine andere Datei geöffnet wird.

OnNewDocument(...) muss als Returnwert einen Wert ungleich 0 zurückliefern wenn das Dokument erfolgreich initialisiert wurde. Wenn Sie eigenen Code in die Methode einfügen wollen, fügen Sie diesen, wie im Kommentar angegeben, immer nach dem Aufruf der Basismethode ein. Welche Funktionalität die Basismethode besitzt schauen wir uns zum Schluss dieser Lektion noch kurz an.

Die nächste interessante Methode der Dokumentenklasse ist die Methode Serialize(...). Sie dient zum Laden und Abspeichern der Daten des Dokuments. Als Parameter erhält die Methode eine Referenz auf ein CArchive Objekt übergeben. Mit der Klasse CArchive und der damit zusammenhängenden Klasse CFile beschäftigen wir uns später in einer gesonderten Lektion am Ende dieses Kapitels noch.

Damit wir den Ablauf beim Starten der Anwendung und den daraus resultierenden Aufrufen der verschiedenen Methoden nachvollziehen können bauen wir in das Rahmenprogramm einige TRACE(...) Anweisungen ein. Als erstes lassen wir uns den Beginn und das Ende der InitInstance(...) Methode der Applikationsklasse ausgeben.
BOOL CDVBasisApp::InitInstance()
{
    TRACE("Beginn InitInstance()\n");
    ....
    TRACE("Ende InitInstance()\n");
    return TRUE;
}

Anschließend zeichnen wir den Aufruf des Konstruktor und des Destruktor des Dokuments auf.

CDVBasisDoc::CDVBasisDoc()
{
    // ZU ERLEDIGEN: Hier Code für One-Time-Konstruktion einfügen
    TRACE("ctor DVBasisDoc\n");
}

CDVBasisDoc::~CDVBasisDoc()
{
    TRACE("dtor DVBasisDoc\n");
}

Zum Schluss fügen wir den beiden Methode OnNewDocument(...) und Serialize(...) ebenfalls noch TRACE(...) Anweisungen hinzu.

BOOL CDVBasisDoc::OnNewDocument()
{
    ....
    TRACE("OnNewDocument()\n");
    return TRUE;
}
....
void CDVBasisDoc::Serialize(CArchive& ar)
{
    if (ar.IsStoring())
    {
        // ZU ERLEDIGEN: Hier Code zum Speichern einfügen
        TRACE("Daten speichern\n");
    }
    else
    {
        // ZU ERLEDIGEN: Hier Code zum Laden einfügen
        TRACE("Daten einlesen\n");
    }
}

Übersetzen und starten Sie das Programm.

Im Ausgabefensters des Debuggers sollten Sie nach dem Starten der Anwendung folgende Ausgabe erhalten:

Beginn InitInstance()
ctor DVBasisDoc
OnNewDocument()
Ende InitInstance()

Wie Sie der Ausgabe entnehmen können, wird das Dokument in der InitInstance(...) Methode erzeugt und initialisiert.

Bei den nachfolgenden Versuchen arbeiten Sie niemals mit Originaldateien sondern nur mit Kopien. Es kann Ihnen sonst unter Umständen passieren, dass die Originaldaten gelöscht werden!

Spielen wir nun ein klein wenig mit dem Beispiel.

Wählen Sie nun einmal aus dem Datei-Menü den Menüpunkt Öffnen... aus. Im Ausgabefensters des Debuggers erhalten Sie die Meldung der Serialize(...) Methode Daten einlesen. Öffnen Sie eine weitere Datei und es wird erneut die gleiche Meldung ausgegeben. Wenn Sie die Datei abspeichern, erhalten Sie wie erwartet die Meldung Daten speichern.
Wenn Sie, wie in der Übung angegeben, die Datei abgespeichert haben, so hat diese Datei danach die Länge 0 Bytes! Ein weiteres Einlesen dieser Datei dann nicht mehr möglich. Der Grund dafür, dass die Datei nach dem Abspeichern die Länge 0 aufweist, ist, dass das Rahmenprogramm der MFC die Datei zum Schreiben geöffnet hat, Sie aber noch nichts in die Datei geschrieben haben.

Doch ein 'kleines' Problem bleibt bei unserem Beispiel. Nehmen wir einmal an, Sie haben die Dokumentdaten in der Anwendung verändert. Wenn Sie nun eine neue Datei öffnen so wird als Reaktion darauf bisher nur die Methode Serialize(...) aufgerufen um die neuen Daten einzulesen. Aber wie können die eventuell vorher veränderten Daten abgespeichert werden? Nun, wenn Sie immer vor dem Öffnen einer Datei die geänderten Dokumentdaten explizit über den Menüpunkt Speichern im Datei-Menü zurückschreiben dann funktioniert das Daten-Handling. Besser wäre es aber eine Methode zu finden die automatisch vor dem Öffnen einer neuen Datei aufgerufen wird um die geänderten Daten abzuspeichern. Und selbstverständlich gibt es eine solche Methode, die CDocument-Methode DeleteContents(...). DeleteContents(...) wird immer dann aufgerufen wenn die Daten eines Dokuments gelöscht werden sollen ohne das Dokument-Objekt selbst zu zerstören. Bauen wir diese Methode in unser Beispiel ein:

Öffnen Sie zunächst wieder die Klassenansicht und klicken dann mit der rechten Maustaste die Dokumentenklasse CDVBasisDoc an. Daraufhin wird ein Kontextmenü eingeblendet in dem Sie den Menüpunkt Virtuelle Funktion hinzufügen... auswählen. Als Reaktion darauf wird folgender Dialog angezeigt:

Virtuelle Methoden hinzufügen

Im linken Teil Neue virtuelle Funktionen werden die Methoden aufgelistet, die überladbar sind aber noch nicht überladen wurden. Wenn Sie hier eine der Methoden anklicken, dann erscheint im unteren Teil des Dialogs eine kurze Beschreibung, für welche Aktion die entsprechende Methode vorgesehen ist. Im rechten Teil Vorhand. Funktionsüberschreibungen werden alle Methoden angezeigt die bereits überschrieben sind; im Beispiel sind dies wie erwartet die Methoden OnNewDocument(...) und Serialize(...).

Klicken Sie jetzt im linken Teil des Dialogs die Methode DeleteContents(...) an und anschließend den Button Hinzufügen und Bearbeiten. Der Klassen-Assistent fügt jetzt die ausgewählte Methode zur Klasse hinzu und im Editorfenster sehen Sie das Grundgerüst der Methode. Fügen Sie der Methode eine TRACE-Meldung wie unten angegeben hinzu.
void CDVBasisDoc::DeleteContents()
{
    // TODO: Speziellen Code hier einfügen und/oder Basisklasse aufrufen
    TRACE("DeleteContents()\n");
    CDocument::DeleteContents();
}

Übersetzen und starten Sie das Programm jetzt wieder.

Sie werden dann folgende Ausgabe im Debugfenster erhalten:

Beginn InitInstance()
ctor DVBasisDoc
DeleteContents()
OnNewDocument()
Ende InitInstance()

Sie sehen, die neue Methode DeleteContents(...) wird noch vor OnNewDocument(...) aufgerufen und das obwohl zu diesem Zeitpunkt noch gar keine Dokumentdaten vorhanden sein können. Sie müssen in dieser Methode also immer abprüfen ob auch tatsächlich etwas zu tun ist.

Und noch mal, weil's so wichtig ist! Bei den nachfolgenden Versuchen arbeiten Sie niemals mit Originaldateien sondern nur mit Kopien. Es kann Ihnen sonst unter Umständen passieren, dass die Originaldaten gelöscht werden!
Nach dem Sie das Programm gestartet haben, öffnen Sie einmal eine Datei.

Im Debugger-Fenster erscheint nun wieder die TRACE-Ausgabe der DeleteContents(...) Methode. Da in der Zwischenzeit noch keine Dokumentdaten vorhanden sind, bräuchten wir hier auch noch nichts durchführen. Öffnen Sie eine weitere Datei, aber bitte keine Originaldatei! Und wieder wird die TRACE-Ausgabe erscheinen. Wenn Sie nacheinander verschiedene Dateien öffnen erhalten Sie folgende TRACE-Ausgaben:

DeleteContents()
Daten einlesen
....
DeleteContents()
Daten einlesen
....
DeleteContents()
Daten einlesen
....

Damit haben wir also eine Methode gefunden die immer vor dem Einlesen von neuen Daten aufgerufen wird. Doch was immer noch fehlt ist der Aufruf der Serialize(...) Methode zum Abspeichern Daten bevor die neuen Daten eingelesen werden. Wenn Sie sich in der Online-Hilfe einmal die Member der CDocument-Klasse ansehen, werden Sie auf die Methode SetModifiedFlag(...) stoßen. Die MFC speichert nämlich nur dann die Daten eines Dokuments wenn diese als geändert gekennzeichnet wurden. Und genau dies kann durch den Aufruf von SetModifiedFlag(...) erreicht werden.

Und noch ein Achtung! Wenn Sie also in einer realen Anwendung die Dokumentdaten verändern, rufen Sie anschließend immer die Methode SetModifiedFlag(...) auf. Ansonsten werden die geänderten Daten nicht so ohne weiteres gesichert!
Nun zur Abwechslung einmal 'nur' ein Hinweis: Es gibt ebenfalls eine Methode IsModified(...) die Ihnen als BOOL Wert zurückliefert ob die aktuellen Daten als geändert gekennzeichnet wurden oder nicht.

Bauen wir den Aufruf von SetModifiedFlag(...) jetzt in das Beispiel ein:

Da wir im Augenblick noch keine Daten besitzen und damit verändern können, markieren wir unsere virtuellen Daten unmittelbar nach dem Einlesen in der Methode Serialize(...) als geändert.
void CDVBasisDoc::Serialize(CArchive& ar)
{
    if (ar.IsStoring())
    {
        ....
    }
    else
    {
        // ZU ERLEDIGEN: Hier Code zum Laden einfügen
        TRACE("Daten einlesen\n");
        SetModifiedFlag();
    }
}

Übersetzen und starten Sie das Programm. Öffnen Sie danach zwei unterschiedliche Dateien hintereinander. Welche Ausgabe erhalten Sie?

Nun, vermutlich die gleiche wie vorhin. Die Ursache dafür liegt in der Methode OnOpenDocument(...) der Dokumentenklasse, die die Serialize(...) Methode aufruft. OnOpenDocument(...) setzt nach dem Aufruf von Serialize(...) das Modified-Flag automatisch wieder zurück.

Sehen wir uns die CDocument-Klasse nochmals in der Online-Hilfe an. Sie werden dort eine weitere Methode finden die mit dem Abspeichern von Daten zu tun hat, die Methode SaveModified(...). Laut Beschreibung in der Online-Dokumentation wird diese Methode aufgerufen bevor ein verändertes Dokument geschlossen wird. Dies ist aber nicht ganz richtig. SaveModified(...) wird immer aufgerufen bevor die Daten eines Dokuments überschrieben (bei SDI-Anwendungen, die ja immer das gleiche Dokument verwenden) bzw. gelöscht (bei MDI-Anwendungen) werden. Als Returnwert muss die Methode einen Wert ungleich 0 liefern wenn die alten Dokument-Daten überschrieben werden können. Bei einem Returnwert von 0 bleiben die alten Daten erhalten. Sie können diese Methode nun auch dazu verwenden um beim erneuten Laden von Daten zunächst festzustellen, ob die alten Daten verändert wurden. Wurden die Daten verändert, so markieren Sie zunächst die alten Daten durch den Aufruf von SetModifiedFlag(...) als verändert und rufen dann die Basismethode auf.

Fügen Sie jetzt zuerst zu unserer Dokumentenklasse eine neue private Membervariable namens m_bLoaded vom Typ bool hinzu. Sie soll verhindern dass beim Öffnen der ersten Datei, wenn also noch gar keine Daten geladen sind, auch die Basismethode SaveModified(...) aufgerufen wird. Diese Variable muss nun noch initialisiert werden. Dies geschieht sinnvollerweise im Konstruktor der Klasse. Erweitern Sie also den Konstruktor wie folgt:
CDVBasisDoc::CDVBasisDoc()
{
    // ZU ERLEDIGEN: Hier Code für One-Time-Konstruktion einfügen
    TRACE("ctor DVBasisDoc\n");
    m_bLoaded = false;
}

Danach muss die Membervariable in der Serialize(...) Methode entsprechend der durchgeführten Aktion gesetzt werden. Entfernen Sie in diesem Zug auch den vorhin eingefügten Aufruf von SetModifiedFlag(...) wieder aus der Methode da er dort wirkungslos war.

void CDVBasisDoc::Serialize(CArchive& ar)
{
    if (ar.IsStoring())
    {
        // ZU ERLEDIGEN: Hier Code zum Speichern einfügen
        TRACE("Daten speichern\n");
        m_bLoaded = false;
    }
    else
    {
        // ZU ERLEDIGEN: Hier Code zum Laden einfügen
        TRACE("Daten einlesen\n");
        m_bLoaded = true;
    }
}

Beim Starten der Anwendung wird durch den Konstruktor von CDVBasisDoc die Member-Variable m_bLoaded zunächst auf  false gesetzt. In der Methode Serialize(...) wird diese Variable dann entsprechend umgesetzt, je nach dem, ob die eingelesenen Daten bereits abgespeichert wurden oder nicht.

Was zum Schluss nur noch fehlt ist die Auswertung der Variablen.

Fügen Sie zur Klasse CDVBasisDoc mit Hilfe des Klassen-Assistenten die virtuelle Methode SaveModified(...) hinzu. Erweitern Sie die Methode anschließend wie folgt:
BOOL CDVBasisDoc::SaveModified()
{
    // TODO: Speziellen Code hier einfügen und/oder Basisklasse aufrufen
    TRACE("SaveModified()\n");
    if (m_bLoaded)
        SetModifiedFlag();
    return CDocument::SaveModified();
}

Wenn Sie nun zwei verschiedene Dateien laden, wird beim Laden der zweiten Datei durch die Basismethode SaveModified(...) über einen in der MFC implementierten Dialog abgefragt ob die Daten gesichert werden sollen. Wird die Frage bejaht, so wird Serialize(...) aufgerufen um die Daten zu sichern.

Und noch ein letztes Mal dieser wichtige Hinweis: Da die Methode Serialize(...) zum jetzigen Zeitpunkt noch keine Daten schreibt, ist die aktuell geladene Datei nachher auch leer, d.h. sie hat die Dateilänge '0!. Führen Sie diese Übung daher unbedingt nur mit Dateikopien durch!

Nun noch, wie weiter vorne versprochen, kurz die Funktionalität der Basismethode OnNewDocument(...). Sie finden den Quellcode der Methode übrigens in der Datei doccore.cpp. OnNewDocument(...) ruft zuerst die Methode IsModified(...) auf um im Falle von veränderten, noch nicht abgespeicherten Daten eine TRACE-Warnung auszugeben. Anschließend wird die Methode DeleteContents(...) aufgerufen um die 'alten' Dokumentdaten zu löschen. Im Anschluss daran wird der Pfadname des aktuellen Dokuments und das Modified-Flag gelöscht.

So, damit beenden wird die Übersicht über die Dokumentenklasse und wenden uns in der nächsten Lektion der Ansichtsklasse zu.



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