MFC


C++DebuggingMFCVista / Windows 7Windows APIMartin Richter - Sa 24 Aug 2013 12:08

Bei der Entwicklung von neuen Funktionen in einem Modul bekam ich irgendwann einen ASSERT. Diesen ASSERT hatte ich in einem Cache eingebaut. Der ASSERT sprang an, wenn bei Programmende der Cache nicht sauber aufgeräumt wurde. Also eigentlich in der Entwicklungsphase nichts ungewöhnliches. Irgendwo wurde also eine Funktion zum Aufräumen nicht aufgerufen.

Aber was mich in diesem Moment extrem stutzig machte, war, dass in der Debugausgabe meines VisualStudios keine Memory Leaks angezeigt wurden :!: Das machte irritierte mich nun schon sehr. Erster Verdacht. Evtl. hat ja jemand die Leakprüfung für den Cache ausgeschaltet (siehe AfxEnableMemoryTracking). Aber eine kurze Prüfung ergab, dass dem nicht so ist.

Also den Test noch mal ausgeführt. Diesmal wieder der ASSERT und zusätzlich der Dump. “Oha” dachte ich “Hier ist aber was richtig faul!”

Nachdem ich immer wieder andere Testszenarien verwendet habe, erschienen mal die Leaks und mal erschienen sie nicht. Und nach einiger Zeit kaum ich dahinter, dass immer wenn PlaySound in meiner Debug Version der Anwendung verwendet wurde, kein Leak-Check erfolgte. Wurde PlaySound nicht verwendet war alles gut und die Leaks wurden ausgegeben.

Jetzt ging es ans eingemachte. Schnell isolierte ich folgende DLLs die zusätzlich beim ersten PlaySound geladen wurden.

'xyz.exe': Loaded 'C:\Windows\SysWOW64\MMDevAPI.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\propsys.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\wdmaud.drv', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\ksuser.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\avrt.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\setupapi.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\cfgmgr32.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\devobj.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\AudioSes.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\msacm32.drv', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\msacm32.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\midimap.dll', Symbols loaded (source information stripped).

Und nach etwas weiter debuggen kam ich dahinter, dass der DllMain Code von setupapi.dll beim DETACH_PROCESS den Prozess sofort terminiert und nicht alle DLLs einen DETACH_PROCESS Aufruf erhalten. Aber der Code für die Leak-Detection der MFC liegt in der MFCx.DLL und wird durch eine statische Variable ausgelöst, die beim Entladen der MFCx.DLL dann zur Ausgabe der Speicherlecks führt. (Siehe PROCESS_LOCAL(_AFX_DEBUG_STATE, afxDebugState) und AfxDiagnosticInit).

Eine genauere Analyse des Stacktraces ergab folgendes Bild:

ntdll.dll!_ZwTerminateProcess@8()  + 0x5 bytes	
ntdll.dll!_RtlpWaitOnCriticalSection@8()  + 0x1d38f bytes	
ntdll.dll!_RtlEnterCriticalSection@4()  + 0x16a38 bytes	
setupapi.dll!_pSetupInitGlobalFlags@4()  + 0x1fc bytes	
setupapi.dll!_pSetupGetGlobalFlags@0()  + 0xe2 bytes	
setupapi.dll!_FlushWVTCache@0()  + 0x2f bytes	
setupapi.dll!_DestroyDeviceInfoSet@8()  + 0x3f bytes	
setupapi.dll!_SetupDiDestroyDeviceInfoList@4()  + 0x44 bytes	
MMDevAPI.dll!CDeviceEnumerator::FinalRelease()  + 0x78 bytes	
MMDevAPI.dll!ATL::CComObjectCached::~CComObjectCached()  + 0x3c bytes	
MMDevAPI.dll!ATL::CComObjectCached::`scalar deleting destructor'()  + 0xd bytes	
MMDevAPI.dll!ATL::CComObjectCached::Release()  + 0xf0c6 bytes	
MMDevAPI.dll!ATL::CComClassFactorySingleton::~CComClassFactorySingleton()  + 0x18 bytes	
MMDevAPI.dll!ATL::CComObjectNoLock<ATL::CComClassFactorySingleton >::`scalar deleting destructor'()  + 0x1a bytes	
MMDevAPI.dll!ATL::CComObjectNoLock<ATL::CComClassFactorySingleton >::Release()  + 0xe0e3 bytes	
MMDevAPI.dll!ATL::CAtlComModule::Term()  + 0x27 bytes	
MMDevAPI.dll!__CRT_INIT@12()  + 0x26e6 bytes	
MMDevAPI.dll!__CRT_INIT@12()  + 0x2588 bytes	
ntdll.dll!_LdrpCallInitRoutine@16()  + 0x14 bytes	
ntdll.dll!_LdrShutdownProcess@0()  + 0x141 bytes	
ntdll.dll!_RtlExitUserProcess@4()  + 0x74 bytes	
kernel32.dll!75eb7a0d() 	
msvcr100d.dll!__crtExitProcess(int status=0)  Line 709	C
msvcr100d.dll!doexit(int code=0, int quick=0, int retcaller=0)  Line 621 + 0x9 bytes	C
msvcr100d.dll!exit(int code=0)  Line 393 + 0xd bytes	C
xyz.exe!__tmainCRTStartup()  Line 568	C

Ursache war, dass MMDevAPI.DLL in seinem DllMain Code ausführt in der setupapi.dll, die aber bereits den DETACH_PROCESS abgearbeitet hat. Das grundsätzliche Problem wird hier in diesem Blog Artikel geschildert: http://blogs.msdn.com/b/oldnewthing/archive/2005/05/23/421024.aspx
Mit eigenen Worten: Die MMDevAPI.dll ruft Funktionen in einer DLL auf, die bereits alle Ihren Speicher und Ressourcen freigegeben hat. Und wie es hier im Code so aussieht, versucht die SetupAPI.DLL wieder Ressourcen zu akquirieren, die eben bereits schon freigegeben wurden, weil eine DLL sie erneut benutzt.

Die Folge ein “Crash” in DllMain, die der Windows-Lader aber abfängt und sofort mit einer Terminierung des Prozesses ahndet. D.h. nun aber, dass nicht alle DLLs, die noch einen DllMain Aufruf mit DETACH_PROCESS erhalten müssten,  auch an die Reihe kommen.

Etwas weitere Recherche ergab, dass dieses Problem auch in den MSDN Foren bereits diskutiert wurde inkl. einer möglichen Lösung.
http://social.msdn.microsoft.com/Forums/vstudio/en-US/8cb1847d-3218-4610-9cb8-6905bd255ff5/no-dllprocessdetach-after-calling-playsound-on-windows-7-64bit

Die Lösung ist erstaunlich einfach: Wenn vor der Benutzung von PlaySound explizit die SetupAPI.DLL geladen wird, dann verläuft der Rest der Deinitialisierung vollkommen normal. SetupAPI.DLL wird nicht entladen, weil die DLL durch den LoadLibrary Aufruf die DLL im Speicher sperrt. MMDevAPI.DLL kann erfolgreich selbst aufräumen und der Code läuft nicht mehr ins Nirvana.

Hier handelt sich offensichtlich um einen Bug in Windows 7 (Windows 8 konnte ich dies bzgl. nicht testen).
Dieser Bug kann natürlich auch empfindlichere Probleme mit sich bringen, wenn Ressourcen betroffen sind, die nicht automatisch mit Prozessende freigegeben werden und wenn diese Ressourcen ausschließlich in der DllMain bei einem DETACH_PROCESS behandelt werden.

ATLC++CRTMFCProgrammierenVS 2013Martin Richter - Mi 21 Aug 2013 17:32

Im Microsoft C++ Team Blog kann nachgelesen werden was sich in ALT+MFC in VS-2013 tun wird:
http://blogs.msdn.com/b/vcblog/archive/2013/08/20/atl-and-mfc-changes-and-fixes-in-visual-studio-2013.aspx

Wer hätte es gedacht…, ja es gibt wieder etwas “Neues”, auch wenn es mehr oder weniger nur Bugfixes sind.

Besonders erwähnenswert ist der Wegfall des MBCS Supports in der MFC, siehe auch hier.
Das die ATL jetzt nur noch als statische Bibliothek mit sehr kleinem Footprint existiert ist nett. Warum eigentlich nicht gleich so?

Nachtrag und Korrektur durch den aufmerksamen Blog-Leser Sasche:

Wie auch im verlinkten Artikel beschrieben, fällt der MBCS Support in VS-2013 noch nicht weg, sondern ist erst mal nur “deprecated” (und die entsprechenden Libs nur noch als separater Download verfügbar). Erst in einer Folgeversion soll der Support für MBCS ganz wegfallen.

 

ATLC++MFCProgrammierenVS 2012Windows APIMartin Richter - Fr 04 Mai 2012 09:15

Gestern Abend hatten wir hier auf der ADC 2012 für C++ in Ohlstadt bei einem schönen Abendevent eine Q&A. Letztes Jahr fand diese Q&A auf einer Schiffahrt auf dem Chiemsee statt. Dieses Jahr war es ein Fußweg von ca. 20 Minuten vom Konferenzhotel, zu einem großen “Grillplatz”, dort standen Zelte, Fackeln Lagerfeuer und es wurde gut gegessen und wie immer viel “Networking” betrieben.

Wie auch letztes Jahr sollten detailierte Fragen auf diese Q&A zu späterer Stunde vertagt werden.

DIE FRAGE die viele Entwicklern brennend interessierte war:
Kann man mit VS-11 Programme für Windows XP entwickeln oder nicht?

Rede und Antwort stand in diesem Fall Steve Teixeira, als Director of Program Management. Also in diesem Fall jemand, der wirklich etwas sagen und auch mit zu entscheiden hat.

Ich gebe seine Antwort von Steve, auf diese Frage zusammengefasst wie folgt wieder:

  1. Zu dem Zeitpunkt als die Entscheidung für das Fallenlassen vom XP-Support gefällt wurde erschien dies als richtig.
    Jetzt muss man allerdings eingestehen, dass diese Entscheidung ein Fehler von Microsoft war.
  2. Die Benutzerzahlen wurden weiter als sinkend berechnet. Man vermutete, dass zum Zeitpunkt der Veröffentlichung von VS-11 noch maximal 20% XP-Nutzer vorhanden wären. Neue Umfragen gehen aber von einer Verbreitung von mindestens noch 46% Windows XP Installationen aus.
  3. Bei Microsoft wurde auch vermutet, dass es genügt den Entwicklern die neue VS-11 Oberfläche anzubieten aber das für das Compilieren das Toolset von VS-2010 genügen würde. Es wurde unterschätzt wie groß das Interesse an den neuen Compiler Funktionen in VS-11 mit C++11  ist. Was eben auch AMP und neue STL Funktionalität einschließt.
  4. Die Folge ist nun, dass Microsoft die Entscheidung für das Fallenlassen des Windows-XP Support neu überdenkt.
    Allerdings kann dies nicht mehr bis zum RTM geschafft werden.
    (Anmerkung von mir: Aktuell in der Beta wurde der gesamte Code, der die Windows-Vista/7 Funktionen isoliert entfernt und alle DLLs werden implizit geladen).

Möglich ist also, dass es ein Featurepack geben wird, dass nach dem RTM ausgeliefert wird, und in dem es dann doch einen XP Support in allen Bibliotheken und im Compiler gibt.

Die Antwort erschien mir ehrlich und geradeaus und war gewisslich keine Vertröstung ohne echten Hintergrund.
Ich weiß nicht wie groß wirklich die Chancen sind, aber diese Aussage deckt sich auch mit den “Gerüchten”, die ich um 5 Ecken gehört habe, und deckt sich auch mit der internen Diskussion, die mit den MVPs geführt wird.

Meine persönliche Schätzung ist eine 25 prozentige Chance, dass wir doch noch einen Windows-XP Support im VS-11 erhalten werden.

Lassen wir und überraschen. Aber es ist eine gute Nachricht :!:

PS: Ich schreibe dies direkt von der ADC für C++ 2012 in Ohlstadt.

Nachträge und Kommentare habe ich direkt in den Blog-Post übernommen, da diese manchmal übersehen werden:

Kommentar 1 vom 04.05.2012 von Steive Teixeira zur Klarstellung:

Hi Martin,
It was great to see you at ADC C++ this week! Just so that there is no confusion for the readers of your blog, the issue of XP support for C++ in Dev11 is one we’re taking very seriously, and we’re continuing to take customer feedback on Dev11 beta. However, we are not yet prepared to make an announcement on platform support for the RTM version of Dev11. We will be making an announcement on this in the coming weeks.
Thanks!
Steve

Kommentar 2 vom 04.05.2012 von Michael Kühlshammer 

Steve Teixeira schickte mir heute einen Link für einen Workaround und empfahl mir, dass möglichst viele Leute auf dem unten angegebenen Link einen Beitrag dazu schreiben sollen (dafür voten sollen), dass der VC11-Compiler auch Code für Windows XP erzeugt. Hier die Email-Antwort von Steve Teixeira:

“Thanks for your email. We continue to devote resources to and support MFC in Dev11. The Windows XP issue is still unsettled, and I appreciate your feedback on this. You me have seen that I posted some of my thoughts on XP support in the comment thread of this blog entry on the VC++ team blog: http://blogs.msdn.com/b/vcblog/archive/2012/04/18/10295093.aspx.

Thanks again,
Steve

C++MFCProgrammierenWindows APIMartin Richter - Mo 02 Apr 2012 21:46

Wenn man ein COM Objekt erzeugt und dieses im System über die ROT (Running Object Table) sichtbar, dann sollte man normalerweise Weak-Locks benutzen. Das kann man auch in der Doku zu RegisterActiveObject  nachlesen. Ansonsten wird es schwierig zu entscheinden, wann man seine Objekte zerstören kann.

Wenn aber nun eine Anwendung sichtbar gemacht wird, also das Objekt vom Benutzer übernommen wird, dann darf es ja nicht beendet werden, wenn der externe Erzeugende Prozess beendet wird und die letzte Referenz zu dem Objekt beendet wird.

Wie verhindert man das?

Die Lösung ist relativ simpel. Solange die Anwendung sichtbar ist, oder besser, wenn sie sichtbar wird ruft man einmalig CoLockObjectExternal auf! Dadurch wird ein weitere Lock auf das Objekt ausgeführt.
Aber Achtung :!: Hier wird keine Referenzzählung verwendet. Egal wie oft man CoLockObjectExternal aufruft, der Referenzzähler wird nur einmal erhöht.

Beendet der User das Programm entsperrt man das Objekt wieder. Sollten keine weiteren Objekte in der Anwendung benutzt werden, dann terminiert die Anwendung wenn man alles richtig gemacht hat ;)
Man ruft CoLockObjectExternal am Besten entweder auf, wenn die Anwendung sichtbar wird (WM_SHOWWINDOW) und erneut wenn WM_CLOSE aufgerufen wird. Die MFC macht alles fast automatisch richtig, bis eben auf die Aufrufe von CoLockObjectExternal, die man selbst im Code unterbringen muss, wie auch die Registrierung der Objekte in der ROT.
Ist noch eine externe Referenz vorhanden wird die Anwendung nicht terminiert, weil der interne Objektzähler der MFC dies verhindert (Code in CFrameWnd::OnClose). Ist kein externer Lock mehr vorhanden sperrt CoLockObjectExternal die Anwendung vom terminieren weil damit exakt eine Referenz aufrecht erhalten wird. Wird durch den Benutzer das Schließen der Anwednung angefordert wird dann im WM_CLOSE diese letzte Refrenz aufgelöst und die Anwednung kann terminieren auch wnen noch ein Eintrag in der ROT vorhanden ist. Dieser wird dann beim Beenden der Applikation auch entfernt.

ATLCRTIDEMFCProgrammierenVS 2008VS 2010Martin Richter - So 16 Okt 2011 19:54

In den C++ Compilereinstellungen finden sich ein vorgegebener Makro %(PreprocessorDefinitions) in den C++ Präprozessor Definitionen. Die Verwendung dieses Makros ist nicht ganz offensichtlich.

Dieser Makro sollten in jedem Fall nicht entfernt werden, denn Sie dienen der Übernahme einiger Einstellungen aus der General-Seite für die C++ Projekte. Zum Beispiel werden die Einstellungen für Unicode und MBCS über den Makro %(PreprocessorDefinitions) in die allgemeinen Compiler-Einstellungen übernommen (die entsprechenden Defines sind _UNICODE; UNICODE; _MBCS ).
Erzeugt man eine DLL wird zusätzlich _WINDLL gesetzt.
Setzt man ATL Optionen in der General Seite wird auch über die %(PreprocessorDefinitions) _ATLDLL bzw. _ATL_STATIC_REGISTRY gesetzt oder zurückgesetzt.
Gleiches gilt, wenn die MFC als shared DLL verwendet wird. In diesem Fall wird der Define _AFXDLL zusätzlich gesetzt.

Löscht man also %(PreprocessorDefinitions) dann werden alle diese Einstellungen nicht mehr  korrekt übernommen.

Anmerkung:
Bei dem Linker Makro %(AdditionalDependencies) habe ich eine ähnliche Verwendung vermutet, konnte aber keine direkte Beziehung zur Seite General herstellen.

Obwohl es auch hier Einflüsse auf die Linkereinstellungen gibt bei Änderungen in den General-Einstellungen. Werden allerdings die MFC als zusätzliche Bibliothek ausgewählt werden die Standard-LIBs aus dem SDK komplett entfernt. Hier gibt die MFC Bibliothek selbst vor in welchen zusätzlichen Libs, des SDK gesucht werden soll über #pragma comment(lib,..).

C++DebuggingMFCProgrammierenMartin Richter - Sa 23 Jul 2011 18:16

Dann will ich mal das Problem lüften, dass sich mit diesem Code ergibt, dein ich meinem letzten Artikel vorgestellt habe:

template <class T>
void SecureClearString(T &strText)
{
  ::SecureZeroMemory(strText.GetBuffer(0),strText.GetAllocLength());
  strText.Empty();
}

Zuerst einmal liegt es nicht daran, dass es hier Template verwendet wurde.
Ein Template wurde verwendet, weil in dem Code nicht nur CString, sondern implizit CStringA und CStringW verwendet wurde. Der Code sollte also mit beiden Typen funktionieren.

Und damit sind wir bei Problem 1, das auch gelöst wurde:
Wenn ein CStringW verwendet wird, dann wird nur die Hälfte des Strings gelöscht, und nicht alles.

Das Szenario, dass zu einem miesen Crash führen kann, will ich nun in den einzelnen Schritten schildern (es wurde ja vermutet, dass es mit GetBuffer zusammenhängt und die Vermutung ist richtig):

  1. Der CString der mit diesem template behandelt wurde enthielt einen größeren CString und anschließend wurde ein kürzerer CString zugewiesen. Damit ist GetAllocLength>GetLength.
  2. Dieser CString wird nun an eine weitere Variable zugewiesen. Durch die Referenzzählung wird keine volle Kopie erzeugt.
  3. Nun kommt unsere schöne Funktion ins Spiel und einer der beiden Strings wird mit dieser Template Funktion behandelt.
  4. Die Funktion hat zwei Argumente, die von rechts nach links berechnet und auf den Stack geschoben werden.
  5. D.h. Zuerst wird GetAllocLength ausgeführt. Und dies ergibt einen Wert für die Länge, der ursprünglich einmal in diese Variable passte.
  6. Als zweites erfolgt nun der Aufruf von GetBuffer. Da wir aber einen CString haben, der mehrfach benutzt wird, muss nun ein Copy on Write erfolgen. D..h. der String wird kopiert und mit der jetzt benötigten Länge neu alloziert und der Zeiger auf diesen Speicher wird zurückgegeben, dieser ist aber eben kürzer als der ursprüngliche Puffer.
  7. Und nun erfolgt der memset, auf einen Speicher der nur noch so groß ist wie der kurze String. Folgerichtig wird der Heap zerstört, weil der Speicher hinter dem String überschrieben wird.
  8. Peng :!: Wir haben hier einen ganz miesen Seiteneffekt.

Hier der Code, mit dem man den Crash gezielt nachbauen kann:

void Crash()
{
  CString str1 = _T("12345678901234567890");
  str1 = _T("123");
  CString str2 = str1;
  SecureClearString(str1); // Crash
  SecureClearString(str2);
}

Der Vollständigkeit halber will ich aber auch noch ein Stück Code zegen, der es richtig macht:

template <class T>
void SecureClearString(T &strText)
{
  // We need this only if there is a private buffer
  if (strText.GetAllocLength()!=0)
  {
    // Execute GetBuffer first. This might cause a fork and may change
    // GetAllocLength.
    T::XCHAR *pBuffer = strText.GetBuffer(0);
    size_t iLen =strText.GetAllocLength();
    ::SecureZeroMemory(pBuffer,iLen*sizeof(T::XCHAR));
  }
  strText.Empty();
}

PS: Der Leser kann sich denken, dass mich dieser Bug und die entsprechende Reproduktion einige Nerven gekostet haben.  Denn es war nicht einfach die Vorbedingung (erst langer String, dann kurzer String, dann Zuweisung) zu ermitteln. Und wie es oft so ist führen Heap-Fehler erst sehr verzögert zu einem Problem.
Wen es genau interessiert: Ich habe ca 7 Stunden an dem Fall geknobelt und hatte 3 verschiedene Crashdumps zur Verfügung. Selbst konnte ich diesen Fehler in unserem Testfeld zuvor nicht erzeugen, weil eben nie alle Bedingungen erfüllt waren. Erst als mir klar war wo das Problem lag, gelang es mir natürlich auch sofort Eingaben zu erzeugen, die den Crash reproduzierten.

C++MFCProgrammierenVS-Tipps&TricksMartin Richter - So 10 Jul 2011 17:45

Für manche Standardklassen bzw. Header oder Libraries ist es manchmal schön zu wissen ob die ATL oder die MFC in einem Projekt verwendet werden.  In der Vergangenheit habe ich dies oft benutzt um bestimmte Member in Klassen einzubauen, die dann zum Beispiel Daten auch als CString aktzeptieren, oder diese Member dann eben nicht einzubauen um eine Nutzung in einem “puren” WinAPI Projekt zu ermöglichen.
Seit die CString Klassen allerdings eigenständige Templates wurden ist dieser Grund für mich eigentlich weggefallen.
Ich benutzte es heute nur noch um evtl. Memberfunktionen zu unterscheiden die evtl. CWnd* zusätzlich zu HWND Parametern akzeptieren.

Aber wer weiß, vielleicht hat der eine oder andere doch die Frage wie er erkennen kann ob die ATL oder die MFC in einem Projekt Verwendung finden.

Vordefinierte Preprozessor Variablen gibt es dafür nicht, allerdings kann man erkennen ob die Standard ATL/MFCHeader in einem Projekt bereits als Include eingefügt wurden, denn in diesem Fall kann man die Existenz der Include-Guards prüfen.

Die MFC benutzt __AFX_H__ als Guard für die afx.h.
Die Basisklassen der ATL befinden sich in der atlbase.hund entsprechend lautet der Guard: __ATLBASE_H__.

Sofern also diese Guards definiert sind wurden auch die entsprechenden Libraries in der stdafx.h oder anderen Headern zuvor included.

Nachtrag 12.07.2011:
Stefan
hat natürlich vollkommen recht mit seinem Kommentar, dass es die zwei Präprozessor-Variablen _MFC_VER und _ATL_VER gibt, die natürlich für den hier erwähnten Einsatz weitaus besser geeigent sind.
Siehe: http://msdn.microsoft.com/de-de/library/b0084kay.aspx
Ich habe hier den Wald vor lauter Bäumen nicht gesehen ;)
Herzlichen Dank für diese produktive Ergänzung.

C++MFCProgrammierenMartin Richter - Sa 28 Mai 2011 10:44

Ich bin Tastatur-Fan und ich achte in meinen Anwendungen immer darauf, dass ein Benutzer meine Anwendungen einfach mit der mit der Tastatur bedienen kann. Accelerator sind in Windows für den Entwickler hier ein einfaches Tool, Funktionen über die Tastatur einfach verfügbar zu machen.

Tastatur Acceleratoren werden in der MFC in CFrameWnd geladen und PreTranslateMessage angewendet. Üblicherweise passiert das Laden direkt wenn LoadFrame ausgeführt wird, oder wenn in einer MDI-Anwendung das Childframe mit dem entsprechenden Document-Template erzeugt wird.

Allerdings wird es spannend, wenn man den Accelerator wechseln will aufgrund verschiedener Ansichten oder Zustände in der Anwendung. Denn ein CMainFrame weiß ja eigentlich nichts von den Dokumententypen oder dessen Zustand. Und ich fände es eigentlich nicht schön entsprechenden Code imFrame zu verankern.

Zum Glück ist das auch nicht nötig, denn es gibt schon immer die zwei netten undokumentierten Funktionen CFrameWnd::GetDefaultAccelerator und CDocument::GetDefaultAccelerator.

Sobald eine Tastatureingabe in CFrameWnd::PreTranslateMessage ankommt wird CFrameWnd::GetDefaultAccelerator aufgerufen. Nur wenn diese Funktion NULL zurückgibt wird der im Frame gespeicherte m_hAccel verwendet. Nun und CFrameWnd::GetDefaultAccelerator ruft nun CDocument::GetDefaultAccelerator für ds aktuelle Dokument auf.
Damit kann nun das Dokument selbst über den Accelerator bestimmen, der verwendet werden soll. Wird NULL zurückgegeben wird der Accelerator des Frames verwendet.

In der MFC-Next ist das Verhalten etwas anders, weil dort der Keyboard Handler eingeschaltet ist, aber letzten Endes wird GetDefaultAccelerator auch von dort aufgerufen.

C++MFCProgrammierenMartin Richter - Mi 18 Mai 2011 21:21

Accelerator sind ein einfach gutes Hilfsmittel um Funktionen in einem Programm über die Tastatur verfügbar zu machen.
Behandelt werden Accelerator in CFrameWnd::PreTranslateMessage. D.h. bevor eine Eingabe-Nachricht aus der Message Queue an ein Fenster ausgeliefert wird, bekommt das Fenster selbst und jedes Parent des Fensters die Nachricht zur Behandlung in PreTranslateMessage angeboten.

Aber das kann auch zu einem kleinen Problem werden. Nehmen wir an, wir haben eine Datenbank Anwendung und Bild-Hoch/Runter werden über Accelerator für das Blättern in den Datensätzen definiert. Soweit OK.

Was aber wenn man nun ein Inplace Control hat, oder eine kleine dynamische Listbox, die man einblendet und in der man nun auch blättern will? Dann wird der Accelerator im CFrameWnd zum Tastenschlucker und in dem Moment führen Bild-Hoch/Runter nicht zu dem gewünschten Blättern im Popupfenster.

Wie geht man nun vor?
Wenn man im View nun in PreTranslateMessage FALSE zurückgibt frisst der Accelerator im CFrameWnd die Taste. Gibt man TRUE zurück, dann wird die Nachricht nicht ausgeliefert. Der Trick ist eigentlich ganz simpel. Wenn alle PreTranslateMessage Funktionen FALSE zurückgeben wird die Nachricht ausgeliefert indem die beiden Funktionen TranslateMessage und DispatchMessage aufgrufen werden.
Das Ganze kann man abkürzen. Im PreTranslateMessage Handler des Fensters selbst  kann man nun einfach TranslateMessage und DispatchMessage aufrufen und nun TRUE zurückgeben. Das Fenster selbst oder evtl. sein Parent sorgt dafür, dass es die gewünschte Nachricht bekommt wie man es erwartet und der Accelerator im CFrameWnd wird umgangen.

C++MFCProgrammierenVS-Tipps&TricksMartin Richter - Fr 13 Mai 2011 21:28

Die Microsoft C/C++ Compiler haben schon immer eine durchgängige Versionsnummer, die mit den Produkten in denen sie eingebunden sind (z.B. VS-2010) nicht zu tun hat.

Diese Produktversion spiegelt auch auch in der vordefinierten Compiler Präprozessor Variable _MSC_VER wieder. Über diese Variable ist es zum Beispiel möglich verschiedene CRT oder STL Library Eigenarten abzufragen und entsprechen den eigenen Code für mehrere Compiler lauffähig zu machen. Gleiches gilt natürlich auch für den Code der MFC (siehe Anmerkung am Fuß der Tabelle).

Hier eine kleine Tabelle der Werte, die _MSC_VER für die verschiedenen Compiler annimmt mit ein paar zusätzlichen Hinweisen.

_MSC_VER  = Compiler
510  = C Compiler 5.1 (DOS)     - 1988?
       Mein aller erster Kontakt mit dem MS-Compiler
600  = C Compiler 6.x (DOS)     - 1990?
700  = C/C++ 7.0                - 1992
       Die UI war damals die PWB. MFC 1.0 wurde veröffentlicht
800  = Visual C++ 1.0           - 1993
       Existierte IMHO als 16bit und 32bit Compiler
900  = Visual C++ 2.0
       Existierte IMHO als 16bit und 32bit Compiler. MFC 3.0
1000 = Visual C++ 4.0           - 1995-03 ?
       Ab dieser Verison nur noch 32bit Compiler. MFC 4.0
???? = Visual C++ 4.1
       War nur für MSDN Subscriber verfügbar. Kam mit erstem Game/DirectX SDK.
???? = Visual C++ 4.2 und 4.21
       Erste Cross-Platform Version für Mac und PC. MFC 4.2, 4.21
1100 = Visual Studio 97 (5.0)   - 1997
       Enthielt weiterhin MFC 4.21
1200 = Visual C++ 6.0           - 1998-06
       Populärtse VC Version. MFC 6.0
1300 = Visual Studio.NET (2002) - 2002-02-13
       .NET hält Einzug. MFC 7.0
1310 = Visual Studio.NET 2003   - 2003-04-24
       MFC 7.1
1400 = Visual Studio 2005       - 2005-11-07
       MFC 8.0
1500 = Visual Studio 2008       - 2007-11-19
       MFC 9.0
1600 = Visual Studio 2010       - 2010-04-10
       MFC 10.0
1700 = Visual C++ 2011          - 2011 ???
       Produktname noch offen evtl. WinC++

Noch ein Hinweis auf _MFC_VER:
In der Tabelle habe ich nur die Versionen der zum Compiler passenden MFC Versionen aufgeführt. Die MFC setzt einen Define mit dem Namen _MFC_VER entsprechend. Dieser definiert ein WORD im Format 0xVVRR. Wobei VV für die Version der MFC steht in hexadezimaler schreibweise und RR für die Revision allerdings in dezimaler Schreibweise. Die aktuelle _MFC_VER von VS-2010 hat also den Wert 0x0A00 und in der MFC 4.21 wurde _MFC_VER als 0×0421 definiert.

PS: Auf die Idee für diese Tabelle kam ich durch eine Diskussion mit einem Teilnehmer auf der ADC für C++ in Prien, der auch schon x-Jahre mit dem MS-Compiler verbracht hat wie ich, und wir über die Entwicklung der Sprache C/C++ nachgedacht haben und was wann als Innovation unsere Programme veränderte. Eben ein typisches Bits+Bytes Gespräch… ;)

Nächste Seite »