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.
Es erscheint folgender Dialog:
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:
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.
In die Eigenschaften des Controls wechseln.
Den Control-Typ auf "Bitmap" umstellen, unter "Image" die ID des eben erstellten Bitmaps wählen.
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:
- Wir definieren uns in "stdafx.h" eine eigene Message:
#define WM_TESTMESSAGE WM_USER + 0x100
- Von unserer Zustandsklasse aus senden wir die Message an das aktuelle Fenster (der Weg
dahin ist zwar ebenfalls unsauber aber besser als die MessageBox oder das Contextmenü in der
Zustandsklasse zu fabrizieren).
LRESULT lResult = ::SendMessage (AfxGetApp()->m_pMainWnd->m_hWnd, WM_TESTMESSAGE, 0, 0);
Im Beispiel hat die Document-Klasse die Methode "RaiseTestMessage" zum Senden der Nachricht, sie
wird von der View aus im Mausklick aufgerufen.
- Die Nachricht geht auf diese Weise NUR an das Hauptfenster "CMainFrame". Deshalb
dort ein der MessageMap einen Eintrag einfügen, der auf die Funktion "OnTestMessage" verweist:
ON_MESSAGE (WM_TESTMESSAGE, OnTestMessage)
Die Implementation gibt die Nachricht nur an die aktuell aktive View weiter:
LRESULT CMainFrame::OnTestMessage (WPARAM wParam, LPARAM lParam)
{
TRACE ("TESTMESSAGE in MainFrame \n");
return this->m_pViewActive->SendMessage (WM_TESTMESSAGE, wParam, lParam);
}
- Die View zeigt ein (dynamisch erzeugtes) Contextmenü an und gibt die ID des gewählten
Items als Message-Rückgabe zurück zum Aufrufer.
In "stdafx.h" werden die beiden Contextmenü-Optionen und damit Menu-IDs "Option 1" und "Option 2"
deklariert:
#define TESTMESSAGE_OPTION1 0x101
#define TESTMESSAGE_OPTION2 0x102
Der Code von OnTestMessage der View sieht so aus:
LRESULT CMessageTestView::OnTestMessage (WPARAM wParam, LPARAM lParam)
{
TRACE ("TESTMESSAGE\n");
//Aus der Mausklick-Position absolute Koordinaten bauen.
//Zielfenster "NULL" = Bildschirm
//this->MapWindowPoints (NULL, &this->pointClicked, 1);
CPoint pointTemp = this->pointClicked;
this->ClientToScreen (&pointTemp);
//Contextmenü dynamisch bauen und anzeigen:
CMenu menu;
//Contextmenü bauen:
menu.CreatePopupMenu ();
//Zwei MenuItems einfügen:
menu.AppendMenu (MF_STRING, TESTMESSAGE_OPTION1, "Option 1");
menu.AppendMenu (MF_STRING, TESTMESSAGE_OPTION2, "Option 2");
//Rückgabe von "TrackPopupMenuEx" ist die ID des gewählten MenuItems (kein BOOL sondern Integer !)
BOOL bMenu = menu.TrackPopupMenuEx (TPM_LEFTALIGN | TPM_TOPALIGN | TPM_LEFTBUTTON | TPM_RETURNCMD,
pointTemp.x, pointTemp.y, this, NULL);
return bMenu;
}
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.