Gesammelter Kleinkram zur MFC

Inhalt:

CDocument: Modified-Flag
Exceptions und MFC
Zeichnen ohne Flackern
XML-Speicherung
Bitmap als Resource


CDocument: Modified-Flag

Die MFC bringt Bordmittel mit, mittels derer wir ein Dokument als geändert markieren können und beim Anwendungsschließen eine automatische Sicherheitsabfrage ("Änderungen in DocumentName speichern") verwenden können. Hierzu muss nur bei jeder Änderung im Document (z.B. Zufügen einer Adresse) folgendes aufgerufen werden:
	this->SetModifiedFlag ();
Ein weiteres erstrebenswertes Ziel wäre es, ähnlich wie in den Visual-Studio-Fenstern, nach einer Änderung einen Stern ("*") im Fenstertitel zu haben. Hierzu muss man manuell den Document-Title anpassen. Dies geschieht durch folgenden Aufruf (an allen Stellen, wo auch "SetModifiedFlag" aufgerufen wird):
	this->SetModifiedFlag ();
	if (this->GetTitle().Right(1) != "*")
		this->SetTitle ( this->GetTitle() + "*");
Zu beachten: Der Stern wird nur angehängt wenn nicht bereits geschehen.
Die negative Auswirkung dieses Sterns: er taucht jetzt leider auch beim Schließen eines geänderten Dokuments in der Abfrage auf, dito beim Speichern eines vorher noch nicht gespeicherten (also neuen) Dokuments. Deshalb folgende drei Methoden überladen:

	BOOL CAdressenDoc::DoFileSave()
	{
		CString sTitleOld = this->GetTitle();
		if (sTitleOld.Right(1) == "*")
			this->SetTitle( sTitleOld.Left (sTitleOld.GetLength() - 1) );

		BOOL bReturn = CDocument::DoFileSave();

		//Wenn Speichern nicht durchgeführt wurde, dann wiederum den Title zurücksetzen:
		if (bReturn == FALSE)
			this->SetTitle (sTitleOld);

		return bReturn;
	}

	BOOL CAdressenDoc::SaveModified()
	{
		CString sTitleOld = this->GetTitle();
		if (sTitleOld.Right(1) == "*")
			this->SetTitle( sTitleOld.Left (sTitleOld.GetLength() - 1) );

		BOOL bReturn = CDocument::SaveModified();

		//Wenn Speichern nicht zugelassen wurde, dann wiederum den Title zurücksetzen:
		if (bReturn == FALSE)
			this->SetTitle (sTitleOld);

		return bReturn;
	}

	BOOL CAdressenDoc::DoSave(LPCTSTR lpszPathName, BOOL bReplace)
	{
		CString sTitleOld = this->GetTitle();
		if (sTitleOld.Right(1) == "*")
			this->SetTitle( sTitleOld.Left (sTitleOld.GetLength() - 1) );

		BOOL bReturn =  CDocument::DoSave(lpszPathName, bReplace);

		//Wenn Speichern nicht durchgeführt wurde, dann wiederum den Title zurücksetzen:
		if (bReturn == FALSE)
			this->SetTitle (sTitleOld);

		return bReturn;
	}
In den drei Methoden geschieht das gleiche: Ein eventuell vorhandener Stern wird aus dem Dateinamen entfernt. Dann wird die Methode der Basisklasse aufgerufen. Falls die Operation vom User abgebrochen wurde wird der Stern wieder gesetzt (z.B. weil Speichern abgebrochen wurde).
"SaveModified" wird immer dann aufgerufen, wenn das Dokument geschlossen werden soll (z.B. beim Beenden der Anwendung und beim Öffnen eines anderen Dokuments).
"DoFileSave" müssen wir überladen, damit bei Speichern eines neuen Dokuments der Stern entfernt wird (taucht sonst im FileSave-Dialog auf). Bei bereits vorhandenen Dokumenten wird dieser Dialog mit dem Originalnamen des Dokuments initialisiert, d.h. der Stern stört nicht.
"DoSave" muss überladen werden, da es bei neuem Document und Auswahl von "File -> Save As..." aufgerufen wird und dann keine der anderen Methoden greift.

Exceptions und MFC

Erster Schritt ist, eine eigene Exception-Klasse, die von CException abgeleitet ist, anzulegen. Empfehlenswert ist, CException um die Möglichkeit eines beliebigen Meldungstexts zu erweitern (siehe Java-Exceptions). Die Header-Datei sollte so aussehen:
class CTestException :
	public CException
{
private:
	CString m_sError;
public:
	CTestException(void);
	CTestException(CString sError);
	~CTestException(void);
	virtual BOOL GetErrorMessage(LPTSTR lpszError, UINT nMaxError, PUINT pnHelpContext = NULL);
};
Die Implementierung sieht so aus:

CTestException::CTestException(void)
{
	this->m_sError = "No message";
}

CTestException::CTestException(CString sError)
{
	this->m_sError = sError;
}

CTestException::~CTestException(void)
{
}


BOOL CTestException::GetErrorMessage(LPTSTR lpszError, UINT nMaxError, PUINT pnHelpContext) 
{	
	//Entweder die gesamte Meldung kopieren (wenn sie ganz in den übergebenen Buffer paßt)
	//oder die ersten x Zeichen der Meldung:
	UINT intStringLength = min( (UINT) this->m_sError.GetLength(), nMaxError);

	//Ohne aktivierten Unicode würde der Aufruf so aussehen:
	//strncpy_s (lpszError, nMaxError, this->m_sError, intStringLength);
	//Mit aktiviertem Unicode so:
	wcsncpy_s (lpszError, nMaxError, this->m_sError, intStringLength );

	return TRUE;
}
Zu beachten ist dass die Methode "GetErrorMessage" überladen ist. Dadurch können wir später die Exception-Message ohne Aufwand anzeigen. Man beachte den Unterschied, je nachdem ob Unicode im Projekt aktiviert ist oder nicht. Die oben verwendeten Methoden mit dem Suffix "_s" sind die Puffer-Überlauf-geschützten Varianten der String-Copy-Methoden !

Auslösen der Exception:
	throw new CTextException ("Ein Fehler !");
Achtung: Im Gegensatz zu Java (und der in Programmieren2 gelehrten Syntax) ist es nicht möglich, bei einer Funktion eine "throws MyException1, MyException"-Anweisung anzugeben. Hierzu gibt es eigentlich nur zwei praktikable Varianten:
Festlegen dass eine Methode keine Exceptions wirft:
	void MyMethod () throw()

Festlegen dass eine Methode Exceptions werfen kann:
	void MyMethod () throw(...)

Eine Deklaration wie die folgende funktioniert nicht:
	void MyMethod () throw(CTestException, CAndereException)

Hier erhält man die Compilerwarnung "warning C4290: C++-Ausnahmespezifikation ignoriert, es sei denn, es wird angezeigt, dass eine Funktion nicht __declspec(nothrow) ist". Scheinbar ist es also nur möglich, anzugeben dass eine Methode Exceptions werfen kann (Default) oder dass sie definitiv keine werfen kann (Compileroptimierung).


Fangen der Exception:
	try
	{
		//Greife in Steckdose.
	}
	catch (CTestException *ex)
	{
		//Exception anzeigen (ruft intern "GetErrorMessage" auf):
		ex->ReportError();

		//Wichtig: weg damit !
		ex->Delete();

	}
	catch (CException *ex)
	{
		//Exception anzeigen (ruft intern "GetErrorMessage" auf):
		ex->ReportError();

		//Wichtig: weg damit !
		ex->Delete();

	}


Zeichnen ohne Flackern

Wenn in eine CView gezeichnet wird, dann kommt es auch auf reichlich schnellen Rechnern zu bösem Flackern. Hierfür gibt es zwei Schuldige:
-Zeichnen auf dem Bildschirm ist generell ziemlich langsam und
-CView zeichnet bei jedem Refresh den Hintergrund komplett neu, dadurch hat man kurzzeitig ein weißes Rechteck vor Augen bevor es im OnDraw übermalt wird.
Die Lösung des ganzen wird im beiliegenden Beispiel gezeigt. Im Menü "Ansicht" hat man die Wahl zwischen zwei Zeichenmodi. Im einen Fall wird wie gehabt gezeichnet, der zweite Modus zeichnet zuerst in den Memory Device Context einer Bitmap und kopiert die ins eigentliche Bild.
Beim Mausklick wird ein Refresh der Anzeige durchgeführt, durch den wie eine Checkbox arbeitenden Menüpunkt "Bei Klick Hintergrundfarbe löschen" kann man zwischen einem Refresh mit Löschen des Hintergrunds und einem Refresh ohne Hintergrundlöschen wechseln.
Hier gibt es das Beispiel zum Runterladen:
MemoryDC.zip
Achtung:
Der Beispielcode enthält noch eine veraltete Version von CMemoryDCView::DefWindowProc, die 0 statt 1 zurückgibt! Das führt wohl zu vermehrtem Flackern (22.02.2009).

Das Zeichnen im Memory Device Context geht mit diesem Code (im OnDraw):
		CDC dcMem;
		dcMem.CreateCompatibleDC(pDC);

		CBitmap *oldBitmap;
		CBitmap *bitmap = new CBitmap();
		
		//In Memory-DC zeichnen statt in "echtem" DeviceContext:
		//Bitmap von width*height Pixeln mit 32 Bit Farbtiefe erzeugen:
		bitmap->CreateBitmap ( intWidth, intHeight, 1, 32, NULL);

		//Kopieren der Bitmap in Temporären Device-Context:
		oldBitmap = dcMem.SelectObject (bitmap);

		//Hintergrund der Bitmap initialisieren.
		//Dazu ein weißes Hintergrund-Rechteck zeichnen:
		dcMem.FillRect ( CRect (0, 0, intWidth, intHeight), &brushWhite);

		//In der leeren Bitmap einen Haufen Zeichenoperationen machen...

		//Bitmap in Device-Context kopieren:
		BOOL bolResult = pDC->BitBlt(0,0, intWidth, intHeight, &dcMem, 0, 0, SRCCOPY);

		dcMem.SelectObject (oldBitmap);

		dcMem.DeleteDC();

		delete bitmap; 

Das Verhindern des automatischen Hintergrund-Löschens (automatisch ausgelöst bei Größenänderungen des Fenster oder bei unserem Beispiel auch beim Mausklick) geschieht durch Überladen der Methode "DefWindowProc" und ignorieren der "Erase Background"-Nachricht (Rückgabe von "1"):
	LRESULT CMemoryDCView::DefWindowProc(UINT message, WPARAM wParam, LPARAM lParam)
	{
		if (message == WM_ERASEBKGND)
			return 1;
		else
			return CView::DefWindowProc(message, wParam, lParam);
	}  


XML-Speicherung

Unter folgender Adresse findet man einen Schnelleinstieg in die MSXML-Library: www.devhood.com.

Hier nochmal als Klau, falls diese Seite irgendwann verdunstet: tutorial_XML.html
Eine Anpassung dieser Doku an Visual Studio .NET ist zu beachten: Gemäß der oben stehenden Anleitung erhält man einen Compilefehler. Hierzu die MDSN-Doku nach dem Knowledge-Base-Artikel KB316317 durchsuchen. Man gelangt zu dem Artikel "Compiler Errors When You Use #import with XML in Visual C++ .NET". (MS-Knowledge-Base: KB316317).
Lösung des Problems: alle XML-Klassen müssen mit dem Namespace "MSXML2" angegeben werden. Außerdem muss beim Import statt "msxml.dll" eine neuere Version (bei WinXP: "msxml3.dll") angegeben werden. Diese DLL liegt im System32-Verzeichnis.

Diese Beispielanwendung zeigt an einem einfachen Beispiel, wie man in XML speichert und lädt:
Jeder Klick auf den Bilschirm wird in einer Liste von CPoints im Document gesichert.
Beim Speichern (document->Serialize() ) wird diese Punkte-Liste als XML-Datei in der Datei gespeichert die im Dateiauswahl-Dialog gewählt wurde. Beim Laden wird das XML-Document wieder eingelesen. Der von der MFC gebotene Serialize-Mechanismus wird teilweise verwendet (Dateiauswahl-Dialog, MRU-Liste), aber ansonsten gewaltig ausgetrickst: das Schreiben/Lesen der XML- Datei erfolgt komplett über die Methoden der MSXML-Library, deshalb wird die Datei im CArchive vor dem Speichern/Laden geschlossen und erst danach wieder geöffnet ;-).

Die so erzeugte XML-Datei sieht so aus:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE points PUBLIC "XML-Beispiel Softwaretechnik 2006 FH Wiesbaden" "http://www.informatik.fh-wiesbaden.de/~knauf/SWT2006/MFCSchnipsel/points.dtd">
<points>
	<point x="35" y="30"/>
	<point x="62" y="61"/>
	<point x="93" y="88"/>
	<point x="122" y="106"/>
	<point x="173" y="132"/>
	<point x="205" y="155"/>
	<point x="253" y="190"/>
	<point x="305" y="216"/>
	<point x="380" y="253"/>
</points>
Damit sichergestellt werden kann dass die XML-Datei gültig ist habe ich eine DTD angeben, die sich auch hier befindet: points.dtd. Sie sieht so aus:
	<!ELEMENT points (point)*>
	<!ELEMENT point EMPTY>
	<!ATTLIST point
		x CDATA #REQUIRED
		y CDATA #REQUIRED> 
Falls ihr keinen Zugriff auf meinen FH-Webserver habt dann könnt ihr die DTD auch durch eine lokale Deklaration ersetzt. Angenommen "points.dtd" wäre in c:\temp gespeichert, dann würde die DOCTYPE-Angabe so aussehen:
<!DOCTYPE points PUBLIC "XML-Beispiel Softwaretechnik 2006 FH Wiesbaden" "file://c:/temp/points.dtd">
Die dritte Variante wäre es die DTD auf einem lokalen Webserver abzulegen.

Eine seeehr wichtige Info: Beim Laden der XML-Datei wird bei einer vorhandenen DTD nur der Header der Datei geladen, das weitere Aufbauen des DOM-Baums erfolgt asynchron. Wenn man (wie ich) den Fehler macht direkt auf domDocument->documentElement zuzugreifen, dann erhält man NULL. Hier hätte es geholfen über domDocument->childNodes zu gehen.
Oder aber man schaltet das asynchrone Nachladen ab, so wie im Codebeispiel:

domDocument->async = _variant_t (false);


Bitte beachten dass in der Exception-Klasse (siehe Beispiel "Exceptions und MFC") und im CXMLDoc::Serialize ("SysAllocString") Codefragmente stecken die spezifisch für Unicode-Projekte sind ! Falls ihr keinen Unicode verwendet gibt das Compilefehler !

Das Beispiel enthält eine vollständige Fehlerbehandlung beim Laden: XML-Syntax-Fehler (aus dem MSXML-Parser) sowie falsche XML-Struktur lösen Exceptions aus. Für die Exceptions wird die eigene Klasse CXMLException verwendet. Diese ist von CFileException abgeleitet und entspricht der Klasse aus dem obigen Exception-Beispiel. CDocument fängt Laden eines Documents auftretende Exceptions, und bei CFileException werden die in der Exception steckenden Fehlermeldungen dem User angezeigt (bei anderen Exception-Typen wird nicht "GetErrorMessage" der Exceptionklasse aufgerufen !).

Und hier gibts das Beispiel: XML.zip


Bitmap als Resource

In diesem Beispiel soll demonstriert werden, wie ein ein Bitmap aus einer Resource der Anwendung geladen und in einer CScrollView angezeigt wird.
Und hier gibts das Beispiel:
BitmapResource.zip

Es wird ein Standard-MFC-Projekt erstellt, als Hauptfenster wird eine CScrollview gewählt.

Hinfügen der Bitmap:
In der Ressourcenansicht per Rechtsklick -> "Ressource hinzufügen..." wählen.
Ressource hinzufügen
Es erscheint folgender Dialog:
Bitmap hinzufügen
Links wird als Resourcentyp "Bitmap" gewählt. Mittels "Neu" kann man eine Bitmap zufügen, die mit dem VisualStudio-internen Malprogramm bearbeitet werden kann. Mittels "Import" kann man ein Bild importieren. Das Beispielbild wurde als neues Bitmap erstellt.
In der Resourcenansicht erscheint jetzt die Bitmap:
Bitmap in Ressourcenansicht
Per Doppelklick auf das Bild öffnet sich ein Resourceneditor, in dem man das Bild wie in Paint bearbeiten kann.
Wichtig ist hier dass man dem Bild eine eindeutige ID gibt und außerdem die Farbtiefe auf einen besseren Wert als den Default "16 Farben" setzt. In den Eigenschaften erkennt man auch unter welchem Namen die Bitmap im Anwendungsverzeichnis (bzw. im Unterverzeichnis "res") gespeichert wird.
Bitmap-Properties
Jetzt können wir frei zeichnen. Ich habe im Beispiel einen Screenshot meiner FH-Seite im IE verwendet.

Das Verwenden der Bitmap ist ein Vierzeiler im "OnDraw" der View:
	CImage *pBitmap = new CImage();
	pBitmap->LoadFromResource (AfxGetApp()->m_hInstance, IDB_BITMAP_VIEW);
	pBitmap->Draw (*pDC, 0, 0);
	delete pBitmap;  
Wichtig: wir müssen in "stdafx.h" wieder die Header-Datei für "CImage" einbinden (#include <atlimage.h>).
Ein alternatives Vorgehen geht über die Klasse "CBitmap" und einen Memory-Device-Context (siehe Beispiel "Zeichnen ohne Flackern" weiter oben, oder auch die Doku zu "CDC::CreateCompatibleDC()"):
	//Wir erzeugen eine CBitmap und laden die Resource:
	CBitmap* pBitmap = new CBitmap ();
	pBitmap->LoadBitmapW (IDB_BITMAP_VIEW);
	 
	//Jetzt brauchen wir die Größe.
	//Dies geht nur indem wir eine Struktur "BITMAP" auslesen.
	BITMAP bitmapInfo;
	pBitmap->GetBitmap(&bitmapInfo);
	
	//Wir müssen mal wieder über einen Memory Device Context gehen,
	//in den wir die Bitmap laden:
	CDC dcMem;
	dcMem.CreateCompatibleDC (pDC);
	CBitmap* pOldBitmap = dcMem.SelectObject (pBitmap);
	
	//Umkopieren des Memory-Device-Contexts in den "pDC":
	BOOL bolResult = pDC->BitBlt(0,0, bitmapInfo.bmWidth, bitmapInfo.bmHeight,
			&dcMem, 0, 0, SRCCOPY);

	dcMem.SelectObject(pOldBitmap);

	delete pBitmap;  
Anmerkung zu diesem Beispiel: in Visual Studio 2003 und früher konnten Ressourcen maximal mit einer Farbtiefe von 256 Farben verarbeitet werden, erst in Visual Studio 2005 werden True-Color-Ressourcen unterstützt.

Stand 22.02.2009
Historie:
12.06.2006: Übernommen aus letztem Semester und angepaßt an Visual Studio 2005 (und Unicode)
15.06.2006: Beispiel "Bitmap aus Resource"
18.06.2006: XML-Beispiel korrigiert (schreibt jetzt eine saubere Datei)
19.06.2006: Problem der DTD im XML-Beispiel gelöst
21.06.2006: XML-Beispiel: DTD im HTML verwies auf localhost.
22.09.2009: "Zeichnen ohne Flackern": Rückgabe der DefWndProc von 0 auf 1 korrigiert.