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:

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:

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