Gesammelter Kleinkram zur MFC


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) 
{
	//Die ersten x Zeichen der Meldung kopieren:
	strncpy (lpszError, this->m_sError, nMaxError);
	//"strncpy" fügt kein "\0" in den String ein, wenn Ziel kürzer als
	//Quelle ist. Deshalb in jedem Fall in "nMaxError-1" ein "\0" einfüge.
	lpszError[nMaxError-1] = '0';
	return TRUE;
}
Zu beachten ist dass die Methode "GetErrorMessage" überladen ist. Dadurch können wir später die Exception-Message ohne Aufwand anzeigen.
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 () throws()

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

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

Hier erhält man die Compilerwarnung "warning C4290: C++ exception specification ignored except to indicate a function is not __declspec(nothrow)". 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-String ins Archive gespeichert und beim Laden aus dem XML-String im Archive geholt. Der von der MFC gebotene Serialize-Mechanismus wird teilweise verwendet (Dateiauswahl-Dialog, MRU-Liste), das Verarbeiten der Datei selbst erfolgt aber manuell.
Die so erzeugte XML-Datei sieht so aus:
<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>

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 in Static-Feld anzeigen

In diesem Beispiel soll demonstriert werden, wie ein ein Bitmap aus einer Resource der Anwendung in ein statisches Feld auf einer Formview geladen wird.
Und hier gibts das Beispiel: PictureControl.zip

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

Hinfügen der Bitmap:
In der Resource View den Punkt "Icon" wählen und Rechtsklick -> "Add Resource..." wählen.
Add Resource
Es erscheint folgender Dialog:
Bitmap erzeugen
Links wird als Resourcentyp "Bitmap" gewählt. Mittels "New" 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 Resourcenansicht
Per Doppelklick auf das Bild öffnet sich ein Resourceneditor, in dem man das Bild wie in Paint bearbeiten kann.

Hinzufügen des Controls für Anzeige der Bitmap:
Im Resourceneditor der FormView wird ein Control "Picture" gewählt (siehe Screenshot) und auf der Formview platziert.
Picture-Control
In die Eigenschaften des Controls wechseln.
Eigenschaften des Picture-Controls (1)
Den Control-Typ auf "Bitmap" umstellen, unter "Image" die ID des eben erstellten Bitmaps wählen.
Eigenschaften des Picture-Controls (2)

Nachteil dieses PictureControls: es kann keine Events auslösen (z.B. Mausklick). Diese müssen auf der FormView abgefangen werden und die Position muss mit den Bildkoordinaten abgeglichen werden.

Kommunikation per Messages / Contextmenüs

Dieses Beispiel löst eine konkrete Situation aus dem Pachisi: User hat eine Figur gewählt. An der möglichen Zielposition liegt ebenfalls eine seiner eigenen Figuren. Wenn er jetzt auf diese klickt weiß das Programm nicht ob er die Figur versetzen will oder die andere Figur auswählen will. Lösung: Dialog oder Contextmenü mit Auswahl der beiden Optionen.
Problem 1: Wie mache ich User-Interaktion aus der Zustandsklasse (darf per Definition keine MessageBoxen etc. anzeigen ?
Problem 2: Contextmenü anzeigen.
Hier gibt es das Beispiel: MessageTest.zip

Lösung für 1:
Bitte beachten: Die Ergebnisse des Contextmenüs werden NUR mittels TRACE(...) auf der Ausgabe-Console im VS.NET ausgegeben !


Stand 22.02.2009
Historie:
21.04.2005: Erstellt
24.04.2005: Exception-Abschnitt zugefügt
05.05.2005: Im SaveModified-Kapitel wurde der Stern bei "File->Save As" nicht zurückgesetzt.
30.05.2005: "Zeichnen ohne Flackern" und XML-Beispiel zugefügt
31.05.2005: XML-Beispiel: Link zu Knowledge-Base-Artikel
06.06.2005: "CTestException::GetErrorMessage" hatte ein herrenloses "throws" in Deklaration stehen. XML-Beispiel erweitert: Fehlerbehandlung, laden aus Archive statt separatem Dateizugriff.
14.06.2005: PictureControl-Beispiel
27.06.2005: Message/Contextmenü-Beispiel
12.07.2005: Doku im Exceptions-Beispiel erweitert
22.09.2009: "Zeichnen ohne Flackern": Rückgabe der DefWndProc von 0 auf 1 korrigiert.