Der etwas bessere CMemDC

CMemDC aus dem Artikel Flicker free drawing with MFC ist wohl der Klassiker für Doublebuffering und ist wohl jedem MFC Entwickler bekannt.

Er macht was er soll und ich habe ihn selbst über Jahre hinweg unverändert verwendet. Solange man einfache Sachen macht und ausschließlich den MM_TEXT Mappingmode verwendet ist alles OK.
Hat man sich aber was spezielles – zoomfähiges – zusammengebaut mit MM_ANISOTROPIC dann funktioniert das Ganze nicht mehr, weil Keith an den Window Origin Koordinaten dreht und nicht am Viewport.

Meine abgewandelte Variante trägt einer entsprechende Verwendung auch für andere Mappingmodes Rechnung. Es müsste IMHO für jeden funktionieren. Intensiv getestet habe ich es mit MM_ANISOTROPIC.

Will/muss man auch an den Viewport Koordinaten drehen, dann sollten die alten Viewport Koordinaten gelesen werden und dann entsprechend durch ein Offset geändert werden.
Alles anderen Werte wie die Extents können beliebig verändert werden.
Bei der Verwendung sollte MM_TEXT eingeschaltet sein (entsprechende ASSERTs habe ich ergänzt). MM_TEXT ist normalerweise gesetzt, wenn CMemDC direkt nach Instanzierung des CPaintDC zum Einsatz kommt. Der Destruktor setzt dann den Mappingmode auf MM_TEXT zurück.

Dieser neue, modifizierte CMemDC kann einfach die bisherige Implementierung ersetzen.

Have fun & Happy coding 🙂

class CMemDC : public CDC
{
public:
  // constructor sets up the memory DC
  CMemDC(CDC* pDC)
    : CDC()
    , m_pDC(pDC)
    , m_pOldBitmap(NULL)
    , m_bMemDC(!m_pDC->IsPrinting())
  {
    ASSERT(pDC != NULL);

    if (m_bMemDC)
    {
      // Create a Memory DC.
      // At this moment we should have mapping mode text
      ASSERT(m_pDC->GetMapMode()==MM_TEXT);
      ASSERT(m_pDC->GetWindowOrg()==CPoint(0,0));
      ASSERT(m_pDC->GetViewportOrg()==CPoint(0,0));

      // Get the clip box and create bitmap for this size
      m_pDC->GetClipBox(&m_rect);
      CreateCompatibleDC(m_pDC);
      m_bitmap.CreateCompatibleBitmap(m_pDC, 
              m_rect.Width(), m_rect.Height());
      m_pOldBitmap = SelectObject(&m_bitmap);

      // Adjust the view port so that we hit the clip rect.
      SetViewportOrg(-m_rect.left, -m_rect.top);
      IntersectClipRect(m_rect);

      // Fill background in case the user has overridden
      // WM_ERASEBKGND.  We end up with garbage otherwise.
      FillSolidRect(m_rect, m_pDC->GetBkColor());
    }
    else
    {
      // Make a copy of the relevant parts of the current DC for printing
      m_bPrinting = m_pDC->m_bPrinting;
      m_hDC     = m_pDC->m_hDC;
      m_hAttribDC = m_pDC->m_hAttribDC;
    }
  }

  // Destructor copies the contents of the mem DC to the original DC
  ~CMemDC()
  {
    if (m_bMemDC)
    {
      // The mapping mode might effect the BitBlt. So we need to return to
      // MM_TEXT. This makes m_rect.left and m_rect.top again the
      // coordinates of our clipped rectangle. And to make sure that the
      // coordinates are really used we just clear the ViewportOrg and the
      // WindowOrg
      SetMapMode(MM_TEXT);
      SetViewportOrg(0,0);
      SetWindowOrg(0,0);

      // Copy the off screen bitmap onto the screen.
      // For this we just make sure that we really target the rectangle of
      // our temporal bitmap.
      m_pDC->BitBlt(m_rect.left, m_rect.top, 
              m_rect.Width(), m_rect.Height(),
              this, 0, 0, SRCCOPY);

      //Swap back the original bitmap.
      SelectObject(m_pOldBitmap);
    }
    else
    {
      // All we need to do is replace the DC with an illegal value,
      // this keeps us from accidentally deleting the handles associated
      // with the CDC that was passed to the constructor.
      m_hDC = m_hAttribDC = NULL;
    }
  }

  // Allow usage as a pointer
  CMemDC* operator->()  {   return this;  }

  // Allow usage as a pointer
  operator CMemDC*()  {   return this;  }

private:
  CBitmap  m_bitmap;    // Offscreen bitmap
  CBitmap* m_pOldBitmap;  // bitmap originally found in CMemDC
  CDC*   m_pDC;     // Saves CDC passed in constructor
  CRect  m_rect;    // Rectangle of drawing area.
  bool   m_bMemDC;    // true if CDC really is a Memory DC.
};

Aus zwei mach eins: Wie man zwei Cursor kombinieren kann

Wer komplexere UIs baut, der kommt um Drag&Drop oder andere Extrafunktionen mit der Maus nicht herum. Um den Benutzer gut zu unterstützen verwendet man natürlich auch spezielle Cursor.
Auffällig ist aber oft genug der Unterschied zwischen den schönen Standardzeigern im 3D-Stil und den oft flachen Zeigern, die gerade mal zusätzlich ein Plus oder ein Stoppzeichen tragen. Oder ganz und gar der Unterschied , wenn man eigene Cursorstile verwendet, wie z.B. Dirigent oder Hände.

Ich habe mich bei meinem aktuellen Projekt gefragt, warum man eigentlich nicht den aktuellen Standard-Cursor verwendet und diesen zusätzlich mit dem entsprechenden Symbol versieht (Pluszeichen, Stopp, Dragframe etc). Das würde besser aussehen und Arbeit sparen, weil man sich nur noch Gedanken um die Extrasymbolik machen muss und der Benutzer behält seinen eingestellten normalen Cursor.

Schaut man sich den Explorer unter XP+Vista an, dann macht man die Entdeckung, dass der es genauso macht. Der aktuelle Cursor wird beim Drag&Drop Vorgang um ein Stopp- oder Pluszeichen ergänzt.

Warum es also nicht selber auch so machen ❓

Meine Suche im Internet zu diesem Thema: „Wie kombiniert man zwei Cursor zu einem?“, war nicht von Erfolg gekrönt. Beispielcode dazu habe ich nicht gefunden. Aber auch wirklich gar nichts.
Also selbst Hand anlegen.

Bei meinem ersten Versuch wollte ich die Cursor-Bitmaps direkt manipulieren. Bei meiner Untersuchung und dem ersten Democode wurde klar, dass Windows gar keinen Unterschied mehr zwischen Icons und Cursor macht. Erstaunlich ❗ Ob Icon oder Cursor spielt eigentlich keine Rolle. HCURSOR und HICON sind identische Typedefs. Also egal ob toller Farbcursor oder monochromer Cursor, alles gleich.
Mit dieser Entdeckung nahm ich schnell Abstand von BitBlt und Konsorten und verwendete eine Imagelist, die genau diese Funktion des Overlays von Symbolen bereits unterstützt und zudem auch noch perfekt mit Icons umgehen kann.

Herausgekommen ist der folgende Code, der es erlaubt zwei Icons, oder Cursor zu überlagern und ein neues Icon oder einen neuen Cursor zu erzeugen.

Man muss sich aso nur mit LoadCursor, den aktuellen Cursor laden und kann diesen damit ganz einfach modifzieren. Nicht vergessen: Das erzeugte Icon/Cursor muss allerdings mit DestroyIcon auch wieder entsorgt werden. Das Ganze habe ich der Einfachheit halber mit MFC Funktionen geschrieben, aber das Ganze in pure WinAPI zu transformieren dürfte nicht schwer sein. 😉

 Hier nun der Code. Have fun!

HICON CombineIcons(HICON hIcon1, HICON hIcon2)
{
 // Remember that HCURSOR and HICON are identical!
 // hIcon1 is overlayed by hIcon2.
 // hIcon2 isn't adjusted in size or position.
 // It just overlays hIcon1
 // Get bitmaps of icon 1
 ICONINFO iconInfo;
 ::ZeroMemory(&iconInfo,sizeof(iconInfo));
 if (!GetIconInfo(hIcon1,&iconInfo))
  return NULL;

 // Attach the bitmaps to get them automatically freed
 // upon error.
 CBitmap bitmap, mask;
 bitmap.Attach(iconInfo.hbmColor);
 mask.Attach(iconInfo.hbmMask);

 // Get size and width
 BITMAP bm;
 if (bitmap.m_hObject)
  bitmap.GetObject(sizeof(bm),&bm);
 else
  mask.GetObject(sizeof(bm),&bm);

 // Get the color depth from the icon and create an image list
 // Remember we need a
 UINT flags = 0;
 switch (bm.bmBitsPixel)
 {
 case 4:  flags = ILC_COLOR4;  break;
 case 8:  flags = ILC_COLOR8;  break;
 case 16: flags = ILC_COLOR16; break;
 case 24: flags = ILC_COLOR24; break;
 case 32: flags = ILC_COLOR32; break;
 default: flags = ILC_COLOR4;  break;  
 }
 CImageList il;
 // be ware that the monochrom cursor bitmap is twice the height
 if (!il.Create(bm.bmWidth,
      bm.bmHeight/(iconInfo.hbmColor!=NULL ? 1 : 2),
      ILC_MASK|flags,2,2))
  return NULL;

 // Load the both icons into the image list
 il.Add(hIcon1);
 il.Add(hIcon2);

 // Define the second icon as an overlay image
 il.SetOverlayImage(1,1);

 // Get a new icon with icon 2 overlayed
 HICON hCombined = ImageList_GetIcon(il.m_hImageList,0,
              ILD_NORMAL|INDEXTOOVERLAYMASK(1));
 if (!hCombined)
  return NULL;

 // Need the icon infos for this new icon
 ICONINFO iconInfoCombined;
 ::ZeroMemory(&iconInfoCombined,sizeof(iconInfo));
 if (!GetIconInfo(hCombined,&iconInfoCombined))
  return NULL;

 // Destroy the combined icon, we just have the bitmap and the mask
 ::DestroyIcon(hCombined);

 // Get the bitmaps into objects to get them automatically freed
 CBitmap bitmapCombined, maskCombined;
 bitmapCombined.Attach(iconInfo.hbmColor);
 maskCombined.Attach(iconInfo.hbmMask);

 // Get the hotspotinto and cursor data from
 // the ICONINFO of hCursor1
 iconInfoCombined.fIcon = iconInfo.fIcon;
 iconInfoCombined.xHotspot = iconInfo.xHotspot;
 iconInfoCombined.yHotspot = iconInfo.yHotspot;

 // OK we have can create a new Cursor out of the target
 // Don't forget to use DestroyIcon for the new Cursor/Icon
 return ::CreateIconIndirect(&iconInfoCombined);
}

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.

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

Probleme finden, die mit dem Heap zusammenhängen ist oft genug eine Sache für sich und für Anfänger nicht selten ein Buch mit sieben Siegeln. Die CRT und der Debugger stellen aber einige Werkzeuge zur Verfügung, die es einem doch mit etwas Geschick und Wissen erlauben auch komplexere versteckte Fehler zu finden, die Heapfehler auslösen.

Jeder C/C++ Entwickler hat schon Meldungen dieser Art beim Testen seiner Programme gesehen:

HEAP[CrashTest.exe]: Heap block at 006D7920 modified at 006D79B0 past requested size of 88
Windows has triggered a breakpoint in CrashTest.exe.
This may be due to a corruption of the heap, which indicates a bug in CrashTest.exe or any of the DLLs it has loaded. 

oder etwas in dieser Art:

Debug Error!
Program: …nts\Visual Studio 2008\Projects\CrashTest\Debug\CrashTest.exe
HEAP CORRUPTION DETECTED: after Normal block (#110) at 0x000D7948.
CRT detected that the application wrote to memory after end of heap buffer.

Ursache ist ein Fehler wie in diesem kleinen Beispielprogramm:

#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; 
}

Wenn die Debug-CRT benutzt wird erhält man automatisch einen Break im Debugger wenn der Speicherblock pCorrupt freigeben wird (Zeile 11). Man braucht also nur den Call-Stack oder Stacktrace anzusehen und kann zumindest feststellen, welcher Block defekt ist.

Schwieriger wird es dann schon die Stelle zu finden, an der der Block überschrieben wird. In meinem Beispiel also die Zeile 7. Besonders dann wird es komplex, wenn das Programm größer ist, und der Speicherblock an evtl. sehr vielen Stellen genutzt wird.

Um die Position einzugrenzen und nicht evtl. bis zum Programmende warten zu müssen, wenn man (hoffentlich) alle Objekte freigibt kann man die CRT veranlassen den Heap zu prüfen. Dies geschieht mit _CrtCheckMemory oder AfxCheckMemory.
Streut man also in seinem Code an strategisch guten Stellen das folgende Statement in seinen Code

ASSERT(AfxCheckMemory()); // oder _CrtCheckMemory

kann man relativ gut die Stelle einkreisen die den Fehler verursacht, und das ohne große Performanceverluste. Man erhält sofort einen ASSERT, ab dem Moment ab dem die Integrität des Heaps zerstört wurde und der Check durchgeführt wird.

Noch etwas einfacher ist es, die CRT dazu zu bringen sich sofort zu melden wenn der Heap zerstört wird. Dies kann man erreichen indem man das Debug-Flag _CRTDBG_CHECK_ALWAYS_DF setzt.

Platziert man bei Programmstart die folgende Codezeile in seinem Programm

_CrtSetDbgFlag(_CrtSetDbgFlag(0)|_CRTDBG_CHECK_ALWAYS_DF);

dann unterbricht die CRT das Programm sofort bei der nächsten Allokation eines Speicherbocks, nachdem der Heap zerstört wurde. In meinem Beispiel also direkt bei der nächsten Allokation in Zeile 8!
Nachteil ist, dass bei jeder Allokation der Heap geprüft wird und damit die Performance schon in den Keller gehen kann, wenn das Programm groß ist und der Fehler evtl. selten auftritt.

Man kann also mit den einfachen Bordmitteln der CRT einen Fehler schon relativ leicht eingrenzen.

Soweit für heute. Was man noch alles machen kann um effektiv Heap-Fehler zu finden werde ich demnächst noch in weiteren Artikeln zu diesem Thema beschreiben.

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;
}

Jubiläum: Mein 10. Award als deutscher MVP für Visual C++

Am 1. Oktober habe ich nun zum zehnten Mal den MVP-Award für Visual C++ erhalten.

Nach dem nun zehnten Award erlaube ich mir mal einen kleinen Rückblick, denn in all den Jahren hat sich doch einiges geändert.

  • In den ersten Jahren war ich hauptsächlich in den stark genutzten NNTP Foren tätig. Leider sind die im Allgemeinen auf den absteigenden Ast. Von weit über 2000 Postings im Monat in 2000 sind wir auf um die 500 abgesunken. Früher langte es auch als MVP einfach ein Teil der Gruppe von Postern zu sein, die qualitativ und auch quantitativ Fragen beantworten. Die meisten MVPs damals waren mehr oder weniger fast alles Regulars. Ich vermute jedoch, dass dies heute nicht mehr langt um MVP zu werden.
  • Vor ein paar Jahren hat mich die deutsche Szene dann doch ziemlich gelangweilt und ich war dann ziemlich aktiv in den neuen MSDN HTTP Foren, die allerdings nur in englisch wirklich Sinn machen. Die deutschen Foren benutzt kein Mensch.
    In den englischen Foren war es in den Anfangszeiten spannend und interessant. Ich habe es da sogar bis zum Moderator gebracht.
    Allerdings tummeln sich da nun auch so viele US-MVPs, dass ich mehr und mehr wieder den Spaß an diesen Foren verloren habe.
  • Und seit April 2006 bin ich dann im Deutschen C++ Forum eingestiegen.
    Seit Oktober diesen Jahres bin ich nun auch dort Moderator für das MFC und WinAPI Forum geworden.
  • Um 2006 fing ich auch mit meinem Blog an und war doch sehr erstaunt wie viele Leser ich erreiche.
    Und ich merke, dass auch das von den heutigen MVPs erwartet wird: Öffentlichkeit zu erreichen!
    Nicht wenige der aktuellen MVPs zeichnen sich mehr durch WEB-Seiten und Veröffentlichungen (Bücher) aus, als durch große Präsenz in Foren.   

Wenn ich auf all die Jahre so zurückblicke, dann sehe ich doch etwas wehleidig auf die Hochzeiten von nntp://microsoft.public.de.vc zurück, wo zeitweilig bis zu 5 VC++ MVPs aktiv waren und vermisse einige von ihnen.
Meines Wissens gibt es nur noch 2 „echte“ VC++ MVPs: Jochen Kalmbach und mich.
Hierzu jedoch noch zwei Anmerkungen:

  1. Muss ich hinzufügen: Soweit ich es weiß! Der Titel wird zwar verliehen, aber es steht einem ernannten MVP frei den Titel anzunehmen aber nicht öffentlich zu machen. So ist es möglich, dass es weitere MVPs gibt, die wir anderen MVPs gar nicht kennen. Oft kommen diese MVPs auch nicht zu treffen oder Konferenzen und somit bleibt die echte Anzahl von VC++ MVPs in Deutschland ein „kleines“ Geheimnis.
  2. Den Zusatz „Echte“ MVPs habe ich gewählt, weil die Windows SDK und TAPI MVPs mit uns VC++ MVPs zusammengelegt worden sind. Die Produktgruppe Windows SDK hat nun keine eigenen „speziellen“ MVPs mehr. Auch ein komisches und für mich nicht nachvollziehbares Vorgehen von Microsoft.
  3. Ich selbst kann nur Vermutungen über den Nominierungsprozess anstellen und so sind auch meine Äußerungen hier bitte zu werten. Es gibt sicherlich Empfehlungen für neue MVPs. Aber was genau notwendig ist um MVP zu werden weiß kein MVP (zumindest nicht ich). Genaues wissen da nur unsere MVP-Leads.

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!