Erstaunen: CMemDC ist Bestandteil der MFC!

Wer Double Buffering benötigt und die MFC nutzt, der kennt auch CMemDC. Vermutlich eine der meist genutzten und kopierten Klassen, die auf CodeProject und CodeGuru vorgestellt wurden.
http://www.codeproject.com/KB/GDI/flickerfree.aspx
http://www.codeguru.com/cpp/misc/misc/flickerfreedrawing/article.php/c389/Flicker-free-drawing-using-memory-DC.htm

Ich habe meine Erweiterung hier im Blog vorgestellt und die liegt normalerweise in einem separaten Namespace, wie alle meine Tool-Klassen.

Nicht schlecht staunte ich, als ich keinen Compilerfehler bekam obwohl ich CMemDC nutzte aber keinen Namespace angab. Siehe da: CMemDC hat in einer eigenen Implementierung den Weg in die MFC gefunden. Man findet sie in:
C:\Program Files\Microsoft Visual Studio 10.0\VC\atlmfc\include\afxcontrolbarutil.h

Im Großen und Ganzen ist es die bekannte Standard-Implementierung, allerdings verfügt diese CMemDC Version auch Code, der auf Windows Vista und Windows 7, die fest im Betriebssystem verankerten Funktionen nutzt: BeginBufferedPaint, EndBufferedPaint
Siehe http://msdn.microsoft.com/en-us/library/bb773178(VS.85).aspx
Diese Funktionen werden innerhalb des Themeings von Windows Vista und Windows 7 verwendet und in dieser Funktionsgruppe ist auch Alphablending direkt verankert. (BufferedPaintSetAlpha). Ich vermute sogar, dass diese integrierten Klassen effektiver arbeiten, als die eigenen Klassen (ein Test steht noch aus), denn Windows weiß intern natürlich viel besser was wie zu puffern und zu zeichnen ist, als wir, wenn WM_PAINT aufgerufen wird.

Vielleicht ein guter Grund, die eigene CMemDC Klasse auch auf Vista/Windows 7 Funktionen zu erweitern oder die integrierte Klasse in der MFC zu verwenden.

Tipp: Übrigens hat die MFC CMemDC Klasse einen statischen Member, der es auf einfache Weise erlaubt das Double-Buffering abzuschalten (CMemDC:: m_bUseMemoryDC), dass ist besonders interessant beim Debuggen von grafischen Operationen, deren Ergebnisse man auch gleich sehen will, allerdings wird dieser Member nicht benutzt wenn das interne Windows Double-Buffering genutzt werden kann, schade eigentlich.

PS: Aber eigentlich muss man sich auch die Frage stellen, warum die Entwickler genau diesen Klassennamen verwendet haben, denn er provoziert ja auch direkt den Konflikt mit existierendem Code.

Achtung: Die festen Mapping Modes des GDI basieren nicht auf LOGPIXELSX und LOGPIXELSY!

Jeder der mit Fontgrößen und Darstellungsgrößen herumspielt, oder wer selber in Fenstern zeichnet kennt LOGPIXELSX und LOGPIXELSY, die durch GetDevCaps geliefert werden. Diese Werte dienen auch CFont::CreatePointFont und anderen Funktionen bei der Umrechnung von „realen“ Maßen auf die Devicepoints, die man dann benötigt. Alles kein Hexenwerk und überall im Netz beschrieben.
Auf diesem Weg kann man mit etwas MulDiv Arithmetik schnell umrechnen wie viele Punkte man benötigt um etwas von 10mm Größe auf einem Device darzustellen.

Der nachfolgende Code wandelt Einheiten von 1mm entsprechend der Auflösung eines Devices in Pixel um.

pDC->SetMapMode(MM_TEXT);
// Convert mm to with LOGPIXELSX
CSize sizeLogPixel(pDC->GetDeviceCaps(LOGPIXELSX),
            pDC->GetDeviceCaps(LOGPIXELSY));
rect.top = ::MulDiv(rect.top,sizeLogPixel.cy*10,254);
rect.bottom = ::MulDiv(rect.bottom,sizeLogPixel.cy*10,254);
rect.left = ::MulDiv(rect.left,sizeLogPixel.cx*10,254);
rect.right = ::MulDiv(rect.right,sizeLogPixel.cx*10,254);

Die Auflösung von 0,1mm je Einheit ist die Metrik des Mappingmodes MM_LOMETRIC. Man sollte also meinen, dass die Verwendung von MM_LOMETRIC mit einem Faktor 10, der obigen Umrechnung gleich kommt.

pDC->SetMapMode(MM_LOMETRIC);
// Convert mm to 0.1mm
rect.top *= -10;
rect.bottom *= -10;
rect.left *= 10;
rect.right *= 10;

Probiert man dies aus, so stellt man überrascht fest, dass die Größen nicht übereinstimmen.
Der Dokumentation nach müsste man es aber denken.

Mit einem bisschen experimentieren bin ich letztlich auf den Grund gekommen.
Keiner der Mappingmodes MM_LOENGLISH, MM_HIENGLICH, MM_LOMETRIC oder MM_HIMETRIC verwendet LOGPIXELSX oder LOGPIXELSY. Diese Mappingmodes verwenden die Werte HORZRES und VERTRES in dem Viewport-Extent und die Werte HORZSIZE und VERTSIZE im Window-Extent.

D.h. der Viewport-Extent bekommt die Größe des Bildschirmes in Pixeln zugewiesen und das Window-Extent bekommt die Größe des Devices in mm zugewiesen. Nun ist diese Größe (HORZSIZE/VERTSIZE) bei Bildschirmen nicht die reale Größe sondern eine Größe, die der Hersteller festlegt. (Anmerkung: Bei Druckern stimmt dieser Wert)

Nun wäre noch alles OK, wenn sich aus den Werten von VERT/HORZSIZE und VERT/HORZRES nun der Quotient LOGPIXELSX/SY ermitteln ließe. Das ist aber nicht der Fall! LOGPIXELSX/SY sind Skalierungswerte die bei Bildschirmen unabhängig von der realen Auflösung angegeben werden und die z.B. dazu Dienen Schriftgrößen grundsätzlich größer oder kleiner anzeigen zu lassen (siehe auch High DPI Mode).

Die Konsequenz daraus ist, dass die Mappingmodes ein relativ exotisches Einzelleben führen, weil die meisten Entwickler eben korrekterweise auf LOGPIXELSX/SY zurückgreifen. Noch mal sei hier bemerkt, dass für Drucker DCs hier in mir bekannten Fällen kein Unterschied existiert und auch das ist gut so.

Die Lösung die sich anbietet, ist nicht weiter schwierig und sie auch der Grund warum ich erst jetzt auf diesen gesamten Umstand gestoßen bin. Ich habe niemals die MM_LO…/MM_HI… Mappingmodes verwendet. Entweder pur MM_TEXT und wenn ich was anderes benötigt habe einfach MM_ANISOTROPIC, und in der entsprechenden Skalierung habe ich dann meistens die Werte aus LOGPIXELSX/SY verwendet. Also musste es passen.

MM_ANISOTROPIC ist sowieso der Mappingmode der Wahl, wenn es um skalierbare Darstellungen und Zoomfaktoren geht, aber dazu vielleicht mehr in einem Artikel demnächst.

Ich habe ein kleines MFC-Programm gebaut (MappingModeTest), dass diese Konflikte aufzeigt. Ich zeichne dort ein Rechteck auf den Koordinaten 20mm,10mm mit der Größe 60mm,40mm. Damit die verschiedenen Rechtecke alle sichtbar werden verwende ich immer einen Versatz von 1mm und zeichne die Rechtecke in unterschiedlichen Farben.
In der Debugausgabe kann man wunderschön sehen wie Extents mit den Werten aus GetDeviceCaps zusammenhängen.

Dieser Artikel basiert auf zwei Anfragen in microsoft.public.de.vc die dieses unterschiedliche Verhalten aufzeigten und diskutieren:
http://groups.google.de/group/microsoft.public.de.vc/browse_thread/thread/23467a5e95051291/7c66c01b8295eaab
http://groups.google.de/group/microsoft.public.de.vc/browse_thread/thread/201719d23a411256/17b41c09844bf105

Kann man GDI Objekte aus anderen Prozessen verwenden und auslesen?

Frage eines Regulars neulich in microsoft.public.de.vc

Habe einen HRGN Handle von einem anderen Process.
Deshalb liefert GetRegionData wahrscheinlich Size 0 zurück.
int iSize = ::GetRegionData((HRGN)hHandle, 0, NULL); 
Wie kann ich GetRegionData eines anderen Processes auslesen ?

Die Antwort ist klar und eindeutig ein Nein!

Zitat Feng Yuan in einem fast 7 Jahre alten Thread, den ich mir damals hinter die Löffel geschrieben habe:

On Windows NT/2000/XP, GDI does have a system-wide GDI object table. But certain data is stored in per-process user mode area, and the process ID of the creating process is stored in GDI object table.
For every operation with a GDI object, GDI32.DLL always checks for matching  process ID. So cross-process usage of GDI object is not allowed by design, at least for NT/2000/XP.

Wem das aber nicht langt, der kann diese Info auch in der MSDN nachlesen unter GDI Objects (gleich der erste Satz):

GDI objects support only one handle per object. Handles to GDI objects are private to a process. That is, only the process that created the GDI object can use the object handle.

Wer also Informationen zu GDI Objekten aus anderen Prozessen benötigt, der muss wohl oder übel zu DLL-Injection greifen.