VS Tipps & Tricks: Heap Bugs finden (Teil 4)

In meine ersten Artikeln über Heap-Bugs habe ich bereits erwähnt, dass die CRT aber auch Windows selbst Speicher unter bestimmten Umständen vorbelegt bzw. beim Freigeben des Speichers mit einem festen Wert löscht.

Für einen Entwickler ist es gut zu wissen welche Werte durch wen gesetzt werden. Zudem erleichtert einem dieses Wissen auch das Debuggen und die Identifikation von Problemen im Zusammenhang mit dem Heap, deshalb habe ich hier mal diese Magic-Bytes, die von Microsoft verwendet werden hier zusammengetragen.

Würde man im Debugger zum Beispiel eine Variable mit dem Wert 0xCCCCCCCC entdecken, dann ist davon auszugehen, dass man diese Variable auf dem Stack nicht initialisiert hat.

  • 0xABABABAB
    Wird von HeapAlloc als Wert für die Guard Bytes („no man’s land“) vor und hinter Speicherblöcken verwendet.
  • 0xBAADF00D
    Wird von LocalAlloc(LMEM_FIXED) verwendet um nicht nicht initialisierten Speicher im Heap zu kennzeichnen.
  • 0xCCCCCCCC
    Wird von der Debug-CRT verwendet um nicht initialisierten Stack zu kennzeichnen.
  • 0xCDCDCDCD
    Wird von der Debug-CRT verwendet um nicht initialisierten Speicher im Heap zu kennzeichnen.
  • 0xDDDDDDDD
    Wird von der Debug-CRT verwendet um freigegebenen Speicher im Heap zu kennzeichnen.
  • 0xFDFDFDFD
    Wird von vom Debug-Heap verwendet für Guard Bytes („no man’s land“), vor und hinter Speicherblöcken.
  • 0xFEEEFEEE
    Wird von HeapFree() verwendet um freigegebenen Speicher zu kennzeichnen.

Siehe auch: http://en.wikipedia.org/wiki/Magic_number_(programming)

CFileDialog öffnet unter Umständen nicht

Ein Problem bei einem Kunden hat mich ins ungläubige Staunen getrieben.
Wir haben in einem Einstellungsdialog ein Edit Control für einen Dateinamen. Dazu einen simplen Schalter mit dem man einen CFileDialog öffnet und die Datei so auswählen kann. Bei einem Kunden wollte aber der Dialog in einigen Fällen nicht öffnen, der Klick auf den Schalter blieb ohne Wirkung. Da der entsprechende Code so simpel war, kam ein Programmfehler kaum in Frage.

Nachdem ich mir einen Screenshot habe senden lassen, staunte ich nicht schlecht, denn ich konnte den Fehler mit gleichen Eingaben zuerst nicht nachvollziehen, denn ich nutzte Vista und der Kunde hatte XP SP2. Sobald ich aber unter XP die selben Vorgaben machte, trat der Fehler auch bei mir auf.

Es scheint ein Bug in den Common Dialogs zu sein. Ist der Dateiname, den man im zweiten Parameter von CFileDialog angibt (vorschlägt) in bestimmter Weise „ungültig“, dann öffnet der Dialog nicht. Der Rückgabewert ist IDCANCEL (2) und CommDlgExtendedError liefert FNERR_INVALIDFILENAME. Ungültig ist aber fast zu viel gesagt, denn der Pfad enthielt an einer Stelle nur einfach 2 Backslashes statt einen, wie üblich.

Einfach nachzuvollziehen mit dem folgenden Snippet:

// Preset filename with a double backslash in the path:
// C:\TEMP\\Schrott.txt
CFileDialog dlg(TRUE,_T("txt"),_T("C:\\TEMP\\\\Schrott.txt"),
            OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
            _T("Text files|*.txt|All files|*.*||"));
INT_PTR iResult = dlg.DoModal();
DWORD dwLastError = GetLastError();
DWORD dwCommLastError = CommDlgExtendedError();

CString strResult;
strResult.Format(_T("Returncode %d, Lasterror 0x%08X, CommLastError 0x%08X"),
            iResult,dwLastError,dwCommLastError);
AfxMessageBox(strResult);

Eigentlich nur verwunderlich, denn der Name selbst mit doppelten Backslashes wird vom Dateisystem sonst überall akzeptiert. Man kann eine Datei mit dem Namen öffnen, lesen und schreiben. Nur CFileDialog mag ihn nicht. Oder besser der Standard-Common-File-Dialog mag ihn nicht.

Bleibt die Frage warum ich unter Vista den Fehler nicht bekam?

Nutzt man VS-2008 und lässt das Programm unter Vista laufen, dann ist auch alles wieder gut. Denn unter Vista benutzt Der CFileDialog ein neues Interface (Common Item Dialog) und nicht mehr den normalen Common-File-Dialog. Die neuen in die MFC gewrappten Dateidialoge von Vista können mit dieser Vorgabe umgehen.
Lässt man das gleiche Programm wieder unter XP SP2 laufen, bekommt man den entsprechenden Fehler wieder, da in diesem Fall wieder die Standard-Common-Dialoge verwendet werden.

VS Tipps & Tricks: Heap Bugs finden (Teil 3)

Mancher Bug macht einem nicht den Gefallen und lässt sich in der Debug-Version finden. Ursache ist oft genug eine Variable, die in der Debug-Version initialisiert (0xCC) wird aber in der Release-Version zu einem Crash führt, wenn zufällige Daten auf dem Stack für undefiniertes Verhalten sorgen.

Also macht man sich an das debuggen der Release Version und kann keinen Fehler finden.
Kaum startet man das Programm ohne Debugger dann kracht es wieder. Warum?

Manch einer könnte jetzt denken: Der Debugger verändert das Memory Layout! Das tut er schon, aber entscheidend für ein anderes Verhalten ist der Debug Heap!
Die wenigsten Entwickler wissen überhaupt, dass es ihn gibt. Ich meine hier nicht die Debug Funktionen, die die CRT zur Verfügung stellt, denn mein Thema heute ist ja das Debuggen eines Release-Programms, und die Debug-CRT hat ja bekanntlich in einem Release Programm nichts zu suchen!

Machen wir es praktisch und nehmen wieder mein kleines Crashtest-Programm:

#include <windows.h>
#include <tchar.h>
#include <crtdbg.h>
int _tmain(int argc, _TCHAR* argv[])
{
  char *pCorrupt = new char[100];
  ZeroMemory(pCorrupt,104);
  char *pOther = new char[100];
  ZeroMemory(pOther,100);
  delete [] pOther;
  delete [] pCorrupt;
  return 0;
}

Wenn wir dieses Programm als Release Version kompilieren und ausführen, dann erhalten wir keine Fehlermeldung ❗ Interessant. Der Heap ist nicht soweit zerstört, dass es zu einer Zugriffsverletzung kommt. Starten wir aber unser Programm mit dem Debugger, dann wird der so genannte Debug Heap des Systems verwendet, der wie die Debug-CRT Guardbytes setzt und kontrolliert.

Ein weiteres Problem entsteht dadurch, dass der Debug Heap den allokierten Speicher auf feste Werte initialisiert genau wie die Debugversion der CRT. Wenn also nicht initialisierter Speicher genutzt wird, dann ist das Verhalten mit dem Debug-Heap deterministisch, ohne Debug Heap eher zufällig.

Das im Debugger alles etwas anders sein kann ist sogar dokumentiert 😉

Processes that the debugger creates (also known as spawned processes) behave slightly differently than processes that the debugger does not create.
Instead of using the standard heap API, processes that the debugger creates use a special debug heap. On Microsoft Windows XP and later versions of Windows, you can force a spawned process to use the standard heap instead of the debug heap by using the _NO_DEBUG_HEAP environment variable or the -hd command-line option.

In diesem Text steht auch, wie man den Debug-Heap ausschalten kann, mit:

SETX _NO_DEBUG_HEAP 1

Diese Environment-Variable sorgt dafür, dass sich auch bei geladenem Debugger, das Programm so verhält wie ohne Debugger (hoffentlich). Führt man mein Testprogramm nun im Debugger aus, wenn die Environment-Variable _NO_DEBUG_HEAP auf 1 gesetzt ist, erhält man keinen Debug-Break mehr. Denn in diesem Fall gibt es keine Guardbytes, die geprüft werden.
Löscht man den Eintrag _NO_DEBUG_HEAP wieder, dann erhält man im Debugger wieder wie erwartet einen Break.

Will man also wirklich realitätsnah eine Release-Version debuggen, dann kommt man um das Ausschalten des Debug-Heaps nicht herum.

PS: Man kann es auch etwas einfacher haben, wenn man sich nachträglich an den Prozess mit dem Debugger attached (wenn das geht). Ideal ist dieses Verfahren auch beim Remote-Debugging (dazu demnächst mehr).

VS Tipps & Tricks: Heap Bugs finden (Teil 2)

Einige Hilfsmittel um einen Heap-Fehler zu finden habe ich in meinem letzten Beitrag ja beschrieben.

Eigentlich wünscht sich der Entwickler nichts mehr, als dass ein falscher Zugriff auf den Heap, sofort einen Break im Debugger auslöst. Die Methoden, die ich bisher gezeigt habe (AfxCheckMemory, _CrtCheckMemory, _CrtSetDbgFlag) können das nicht direkt , aber zumindest helfen sie den Fehler einzukreisen.

Ein unverzichtbarer Helfer, der sofort solch einen Break auslösen kann, ist der Application Verifier, den ich bereits in einem älteren Artikel als Freund und Helfer vorgestellt habe.

Seit Visual Studio 2005 kann man direkt Parameter für den Application Verifier im Projekt einstellen und auch direkt den Debug-Prozess mit dem Application Verifier starten (Umschalt+Alt+F5).
An den Standardeinstellungen im Projekt braucht man hier gar nichts zu ändern:
Conserve Memory – No
Protection Location – Je nach Testfall (man sollte mit beiden Einstellungen mal debuggen)
Alle anderen Einstellungen Verification Layers Settings – auf Enable

Mit dem Application Verifier lässt sich der so genannte Paged Heap nutzen, der Guard Pages anlegt hinter oder vor den allokierten Speicherbereichen (siehe auch GFLAGS.EXE). Der Vorteil: Man erhält sofort eine Access Violation, wenn man den Speicherbereich überschreitet.

Mein kleines Demoproramm

#include <windows.h>
#include <tchar.h>
#include <crtdbg.h>
int _tmain(int argc, _TCHAR* argv[])
{
  char *pCorrupt = new char[100];
  ZeroMemory(pCorrupt,106); // -- This will corrupt the heap
  char *pOther = new char[100];
  ZeroMemory(pOther,100);
  delete [] pOther;
  delete [] pCorrupt;
  return 0;
}

crashed mit der Nutzung des Application Verifiers sofort und man kann im Call Stack die Zeile 7 ausmachen.
Genial ist besonders, dass der Application Verifier auch mit der Release Version sofort die Zeile 7 als Ursache identifiziert. Gerade wenn man also nicht auf die Debug-CRT zurückgreifen kann, ist der Application Verifier ein super Hilfsmittel.

Der Nachteil: Die Guard Pages liegen nicht exakt und direkt hinter dem allokierten Bereich, sondern auf der nächsten Page Boundary. Deshalb crashed mein Sample auch nicht wenn man den Speicher um nur 1 Byte überschreitet.

Aber der Application Verifier ist zum Testen ein absolutes Muss, weil auch falsche Handles erkannt werden und auch der Lock Verfification Layer für die Qualitätssicherung einfach nützlich zum entwanzen sind. (siehe auch Application Verifier Einstellungen in der MSDN).

Hinweis ❗

Auf Windows XP und Windows Server 2003 erhält man ohne administrative Rechte die folgende Fehlermeldung:

Access denied. You need administrative credentials to use Application Verifier on image <App_Name.exe> on machine <Machine_Name>. Contact your system administrator for assistance

Unter Windows Vista oder Windows Server 2008 erhält man die flogende Fehlermeldung wenn der Application Verifier nicht elevated gestartet wird:

Access denied. You need administrative credentials to use Application Verifier on image <App_Name.exe> on machine <Machine_Name> or per user verifier settings should be enabled by the administrator. Please refer to documentation for more information.

Durch einen simplen Eintrag in der Registry lässt sich aber auch als normaler Benutzer, ohne administrative Rechte, der Application Verifier nutzen, man erzeugt einen DWORD Eintrag in der Registry mit dem Wert 1
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manger\ImageExecutionOptions
Nach einem Reboot kann man nun einfach den Application Verifier auch non-elevated, als normaler Benutzer nutzen.

Kleiner Workarround für MFCNext in Verbindung mit CScrollView

Wenn man die BCG-Library oder MFCNext aus der VC++ 9.0 SP1 nutzt erhält man einen ASSERT wenn man ein CScrollView verwendet und wenn das Programm maximiert gestartet wird.

—————————
Microsoft Visual C++ Debug Library
—————————
Debug Assertion Failed!

Program: …\Debug\TestSDIScrollView.exe
File: f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\viewscrl.cpp
Line: 385

For information on how your program can cause an assertion
failure, see the Visual C++ documentation on asserts.

(Press Retry to debug the application)
—————————
Abbrechen   Wiederholen   Ignorieren  
—————————

Der Grund liegt darin, dass in der MFCNext Implementierung schon sehr früh ein RedrawWindow ausgeführt wird wenn das Main Window maximiert wird. In diesem Fall wird OnDraw/OnPaint bereits ausgeführt wenn SetScrollSizes noch nicht aufgerufen wurde. Das geschieht ja normalerweise meistens erst in OnInitialUpdate.
Dieser ASSERT soll dem Programmierer darauf hinweisen, dass SetScrollSizes unabdingbar für die korrekte Funktion des CScrollView notwendig ist.
Leider ist in diesem alten Code ein Seiteneffekt nicht berücksichtigt worden, der durch MFCNext in Spiel kam.

Das Ganze lässt sich jedoch einfach umschiffen indem man im Konstruktor seines Views vorab SetScrollSizes mit Dummywerten aufruft. Die eigentliche Initialisierung mag dann später wie gewohnt in OnInitialUpdate erfolgen.

CScriptEditorView::CScriptEditorView()
{
  // If the program is launched maximized, a RedrawWindow occurs in a very
  // early stage and OnDraw would be called without an initialized mapping mode
  // So we just do a dummy init here.
  SetScrollSizes(MM_TEXT,CSize(0,0));
}

Der Handler CWnd::OnGetDlgCode hat eine falsche Signatur

Ich habe mich in der letzten Zeit viel mit den Standard-Fensterklassen beschäftigen müssen. Insbesondere war hier auch das Zusammenspiel der Control in Dialogen in meinem Fokus.
In meinem Artikel Die Return-Taste in Dialogen, eine unendliche Geschichte habe ich ja bereits einiges dazu geschrieben.

Wer Aufmerksam nun die Windows API zu WM_GETDLGCODE liest und diese mit CWnd::OnGetDlgCode vergleicht, dem wird sofort auffallen, dass die Signatur nicht mit der Windows API Beschreibung übereinstimmt.

Ich habe aufgrund dieser Signatur in der MFC immer gedacht, dass alle Controls immer eine Konstante zurückgeben. Das die Controls individuell für jede Taste entscheiden können ob sie die haben wollen oder nicht ist mir dabei glatt entgangen. Will man es also richtig machen, dann muss man auf AfxGetCurrentMessage zurückgreifen um die entsprechenden wParam und lParam Werte zu erhalten.

UINT CMyWnd::OnGetDlgCode()
{
    MSG* pMsg = AfxGetCurrentMessage();
    // virtual keycode
    UINT uiVirtKey = pMsg->wParam;
    // get keyboard message causing WM_GETDLGCODE
    MSG *pKeyboardMsg = reinterpret_cast<MSG*>(pMsg->lParam);
...
    return uiNewDlgCode;
}

Die Return-Taste in Dialogen, eine unendliche Geschichte

Wer sich in Windows-Programmierer-Foren tummelt, dem wird die folgende Frage mindestens einmal in der Woche über den Weg laufen:

Ich möchte in einem Dialog die Eingabetaste abfangen, so dass sich der Dialog nicht schließt.
Wie geht das?

Vom Sinn und Unsinn dieses Wunsches wollen wir mal hier nicht reden. Also schauen wir mal auf dieses Problem etwas genauer.

Die Lösung WM_COMMAND mit IDOK abfangen, lasse ich nicht gelten , denn evtl. hat der Dialog ja einen OK Button. (In der MFC käme das mit dem Überschreiben von CDialog::OnOK gleich)

Die Standardantwort von MFC Entwicklern lautet immer:

Überschreibe PreTranslateMessage und fange WM_KEYDOWN mit VK_RETURN dort ab und behandle die Nachricht dort.

OK. Aber was macht ein Win32-API Entwickler. Der hat keinen Einfluss auf die Message-Loop. Muss der nun zu der Hook-Kanone greifen?

Wo liegt eigentlich das Problem?
Die Funktion, die VK_RETURN in ein WM_COMMAND mit IDOK umwandelt ist die API Funktion IsDialogMessage. Diese Funktion wird in der Messageloop von modalen Dialogen verwendet. In der MFC direkt in CDialog::PreTranslateMessage und in Win32 Dialogen wird sie in der Messageloop die DialogBox ausführt.

Diese Funktion geht aber mit solchen Tastatureingaben nicht einfach wahllos um. Sie macht das sehr intelligent. Das sieht man schon daran, dass man in einer aufgeklappten ComboBox die Eingabetaste drücken kann und es schließt die ComboBox und nicht der Dialog.
Ebenfalls kennt man den Effekt, dass ein mehrzeiliges Eingabefeld (Edit Control) sehr wohl die Eingabetaste als Zeilenschaltung nutzen kann und auch hier schließt der Dialog nicht.
Kennt also IsDialogMessage seine Pappenheimer von Controls?

Woher weiß IsDialogMessage also wann VK_RETURN mal so und mal so behandelt werden muss?
Die Antwort ist einfach: IsDialogMessage fragt seine Controls ab was sie gerne hätten!
Und das wird erreicht mit einer einfachen Windows-Nachricht, die in diesem Zusammenhang fast nie genannt wird: WM_GETDLGCODE.

Bevor IsDialogMessage eine Tastatureingabe selbst behandelt, fragt die Funktion das Zielfenster mit WM_GETDLGCODE ab, ob das Fenster evtl. selbst Verwendung für diese Tastatureingabe hat.
WM_GETDLGCODE kann hier sehr flexibel reagieren, denn in wParam erhält die Nachricht den virtuellen Tastencode und das Control weiß nun sehr genau um was es hier geht.

Kann man also in jedem Control (das natürlich den Fokus hat) die Eingabetaste selbst behandeln?
Antwort: Ja!

Das Control muss nur einfach WM_GETDLGCODE behandeln für die Taste VK_RETURN. In diesem Fall gibt es DLGC_WANTMESSAGE zurück (ist übrigends identisch zu DLGC_WANTALLKEYS). IsDIalogMessage wird in diesem Fall die Eingabetaste an das Control weitergeben und nicht selbst behandeln. So macht es übrigens auch eine ComboBox die aufgeklappt ist wenn die Eingabetaste gedrückt wird.

Ich habe einfach mal hier eine Win32 Lösung aufgebaut. Es wird einfach das entsprechende Control mit einer eigenen WndProc gesubclassed und das sieht etwa so aus:

LRESULT CALLBACK SpecialWndProc(HWND hWnd,
                        UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_GETDLGCODE:
        if (wParam==VK_RETURN)
            return DLGC_WANTMESSAGE;
        break;
    case WM_KEYDOWN:
        if (wParam==VK_RETURN)
        {
            MessageBox(hWnd,_T("VK_RETURN received!"),NULL,MB_OK);
            return 0;
        }
        break;
    }
    WNDPROC pWndProc = reinterpret_cast(GetWindowLongPtr(hWnd,GWL_USERDATA));
    return CallWindowProc(pWndProc,hWnd,message,wParam,lParam);
}

...
// Subclass the control
HWND hWndEdit = GetDlgItem(hDlg,IDC_EDIT1);
LONG_PTR pOldWndProc = SetWindowLongPtr(hWndEdit,GWL_WNDPROC,
                        reinterpret_cast(SpecialWndProc));
SetWindowLongPtr(hWndEdit,GWL_USERDATA,pOldWndProc);

Effekt: Jedes Control, dass man mit dieser WndProc subclassed wird die Messagebox anzeigen und der Dialog wird nicht geschlossen! Subclassed man also alle Controls eines Dialoges, die den Fokus bekommen können, dann kann man sofort ohne Hook, auch die Eingabetaste selbst behandeln.
Sinnvollerweise sendet man bei Erhalt von VK_RETURN dann einfach eine entsprechende Nachricht an das Parent, dass die gewünschte Aktion dann ausführt, sofern nicht das Control selbst die Eingabetaste direkt behandelt.

Man kommt also auch in reinen Win32 API Programmen ganz ohne Hooks aus, um VK_RETURN in Dialogen so zu behandeln wie man es selbst gerne möchte!

Dialog basierende MFC-Anwendungen einmal anders

Ich habe mich zwar gerade erst darüber ausgelassen, dass man dialog basierende Anwendungen gar nicht braucht (sieh hier), aber ganz und gar unsinnig sind sie ja nicht.

Bei mir ist der häufigste Anwendungsfall eine simpler Dialog, der nur zum Steuern eines Programmes dient, das ein Icon in der Taskleiste abablegt.

Ärgerlich ist dann nur, dass man nicht vermeiden kann, dass beim Start der Anwendung der Dialog kurz aufflackert, bevor man ihn mit einem Timer oder mit PostMessage und einer benutzerdefinierten Nachricht wieder versteckt.

Aber es geht viel einfacher und dazu noch in einer Form, die der MFC-Konstruktion besser entspricht, als das Gerüst, das der Applikations-Wizard erzeugt.
Ich möchte das hier einfach kurz erläutern:

1. Erzeuge ich eine normale dialog basierende Anwendung mit dem Wizard.

2. Dann lege ich ein Objekt der Dialog Klasse mit dem Namen m_dlg in der CWinApp Klasse an. Ich möchte damit erreichen, dass die Dialogklasse so lange existiert wie die Applikation und nicht mehr nur als lokale Variable in InitInstance auftaucht.

3. Der gesamte Code, den der Wizard erzeugt hat:

CTestDLGDlg dlg;
m_pMainWnd = &dlg;
INT_PTR nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
    // TODO: Place code here to handle when the dialog is
    // dismissed with OK
}
else if (nResponse == IDCANCEL)
{
    // TODO: Place code here to handle when the dialog is
    // dismissed with Cancel
}

wird ersetzt durch:

// Create the Dialog
if (m_dlg.Create(CMyDlg::IDD))
{
    m_pMainWnd = &m_dlg;
    return TRUE;
}
else
    return FALSE;

4. Kleine Schönheitskorrektur nun. Man verlagert das Laden des Applikations-Icons aus dem Konstruktor des Dialogs nach OnInitDialog.

m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);

Fertig ❗

Kurz erklärt: Aus dem modalen Dialog der durch DoModal gestartet wird wird nun ein modaler Dialog, der durch die Messageloop der CWinApp::Run gesteuert wird. Von der sonstigen Funktionsweise ändert sich nichts.
Dieser Konstrukt hält sich nach meiner Meinung weit mehr an das MFC Gerüst, als die Anwendung, die der Wizard erzeugt.
Die Vorteile sind schnell aufgezählt:

  1. InitInstance wird durchlaufen und CWinApp:Run wird verwendet.
    Dies hat z.B. zur Folge, dass auch CWinApp::OnIdle durchlaufen wird und temporäre Map Objekte entsorgt werden. (siehe Kommentar unten)
  2. Man kann die Applikation ohne Flackern zu dem Moment sichtbar machen an dem man es möchte. Dazu muss man nur das WS_VISIBLE Flag im Dialogtemplate entfernen und ShowWindow aufrufen, wenn es einem passt.

Vielleicht sehen ja andere Leser noch mehr Vorteile… die Diskussion ist eröffnet 😉

PS: Noch ein kleiner Nachtrag zu den oben erwähnten temporären Handle Maps in einer dialog basierenden Applikation. Diese temporären Handlemaps werden automatisch aufgeräumt und gelöscht wenn CWinApp::OnIdle ausgeührt wird. Das ist normalerweise der Fall wenn in CWinApp::Run keine Nachricht in der Messagequeue liegt, die abgearbeitet werden muss (PeekMessage gibt FALSE zurück). Im Gegensatz dazu werden Handlemaps nicht gelöscht wenn die Nachrichtenschleife mit CWnd::RunModalLoop für einen modalen Dialog ausgeführt wird. So also auch niemals in einer dialog basierenden Anwendung, oder wenn ein modaler Dialog in einer MFC-Applikation ausgeführt wird!
Das fast noch mal einen Artikel wert…

Manche ungenaue Dokumentation nervt einfach

Die MSDN ist eines der größten und besten Nachschlagewerke, die ich kenne, allerdings gehen mir auch seine Unzulänglichkeiten ab und zu auf die Nerven (und hier meine ich nicht die funktionellen Defizite, sondern die inhaltlichen)!

Das sind dann ganz besonders die kleinen Sachen, die einem das Leben leicht machen könnten, die dann nicht „vollständig“ dokumentiert sind.

Beispiel:
Die MFC List-Container (z.B. CObList). Wir lesen in der Doku zu InsertAfter:

position
A POSITION value returned by a previous GetNext, GetPrev, or Find member function call.

Gut! Aber da steht nichts zu NULL. 😕
Aber wenn ich genauer darüber nachdenken, kann GetNext und Find auch NULL zurückgeben…
Was passiert also wenn ich NULL als Argument für position?
Mein Verstand sagt mir: „Es wird ein Element am Ende eingeführt!“ und ein Blick in den Code bestätigt den Verdacht.

Warum steht in der Doku nicht gleich, dass InsertAfter mit NULL als position ein AddTail ausführt?
Wer weiß wieviele Entwicler solchen „unnützen“ Code aufgrund der mangelnden Doku geschrieben haben:

CStringList lst;
FillMyList(lst);
POSITION pos = lst.Find(_T("Anything"));
if (pos==NULL)
    lst.AddTail(_T("Something to insert"));
else
    lst.InsertAfter(pos,_T("Something to insert"));

Es würde ja genügen wie folgt zu schreiben:

CStringList lst;
FillMyList(lst);
POSITION pos = lst.Find(_T("Anything"));
lst.InsertAfter(pos,_T("Something to insert"));

Korrespondierend ist die Doku von CObjList::InsertBefore genauso unvollständig.
Wird NULL als Position verwendet wird hier ein AddHead ausgeführt.

PS: Mich ärgert auch jedesmal wenn einer meiner Kollegen solchen Code schreibt:

CSomeObject *p = NULL;
...
// Conditional create
if (SomeThing())
     p = new CSomeObject();
...
// Cleanup
if (p)		// Category meaningless
    delete p;
  1. Warum der Test auf !=NULL?
  2. Warum wird kein Autopointer verwendet, der das auch Exception-Save gemacht hätte?

Die Unsitte dialogbasierende Anwendungen zu bauen statt SDI mit CFormView zu verwenden

Es scheint mir meistens ein Anfängerfehler zu sein, dass viele MFC Entwickler (oder solche die es werden wollen) zuerst mal zu einer dialogbasierenden Anwendung greifen.
Ist ja auch nett. Man kümmert sich nur um die paar Dialogfelder und hat kein Doc/View zu verwalten.

Am Ende kommen aber dann noch weitere Wünsche:

  • Ich hätte gerne ein Menü
  • Ich hätte gernenoch einen Toolbar
  • Ich hätte gerne einen Status Bar
  • Schön wäre ein Accelerator

Triviales und nicht triviales schließt sich an:

  • Warum kann ich kein Command/Routing für meinen Toolbar und mein Menü verwenden?
  • Warum schließt mein Dialog bei Nutzung Eingabe-Taste?
  • Kann ich meinen Dialog auch resizen?
  • Die Daten sollen auch gespeichert werden, wie geht das?
  • Ich hätte gerne so einen schönen MFCNext Toolbar in meinem Dialog, oder ein Ribbon, geht das?
  • … (die Liste ist bestimmt nicht vollständig)

Gibt man dann die Antwort, dass man x Klimmzüge machen muss um so etwas in eine dialogbasierende Anwendung einzubauen (wenn es überhaupt geht), dann erntet man noch noch die stöhnende Klage: „Ohhh Mann! Ist das kompliziert!“

Und all das geht nur mit Mühe in einen CDialog einzubauen. Der Grund ist einfach: Das Commandrouting für all diese Elemente ist in CFrameWnd integriert. Aber ein CDialog leitet sich von CWnd ab und ist von Grunde auf für keine dieser Funktionen vorbereitet.

Dabei könnte alles so einfach sein!
Man muss nur einfach eine SDI Applikation mit einem CFormView erzeugen und alle die Wünsche die mancher später hat, kann man sofort erfüllen und wenn man es wirklich nicht will auch weglassen.

Die Frage stellt sich für mich also:
Warum nicht einfach immer gleich eine SDI/CFormView Anwendung bauen ❓
Das Potential dieser Anwendungsform ist einfach unerreichbar verglichen mit einer dialogbasierenden Anwendung.

Also sollte man sich mal die Liste der Wünsche, die ich hier aufgestellt habe ansehen und als Checkliste betrachten. Sollte einer dieser oben aufgeführten Punkte für die Applikation wichtig sein, würde ich dringend anraten zum SDI/CFormView zu greifen.
Das ist auch der Fall, wenn diese Anforderungen erst später integriert werden soll. Oft genug ist ja die dialogbasierende Anwendung schon fertig und es heißt dann: Ich möchte doch nur noch…

PS: Ich persönlich benutze nicht mal mehr zu Testzwecken dialogbasierende Anwendungen 😉