MeasureString und DrawString - GDI auf die harte Tour



  • Hallo miteinander,
    jetzt arbeite ich schon eine Weile mit GDI+ und war auch schon erfolgreich genug, mich an komplexere Themen zu wagen. An diesem Problem beiße ich mit (im Moment noch) die Zähne aus:

    Ich möchte erst einmal verschiedene Textelemente mit demselben Font in einer Zeile darstellen, dabei aber die Textfarbe für einzelne Zeichen oder Zeichengruppen ändern (z.B. erstes Zeichen in schwarz, folgender Text in grau, .....). Unter Umständen möchte später ich sogar denFont und/oder die Schriftgröße innerhalb der Zeile ändern.

    Die GDI+-Methode MeasureString schien mir eine gute Lösung zu sein; so konnte ich den Text in Elemente mit gleicher Schrift+Farbe aufteilen, die Elemente ausmessen und anschließend mit (GDI+)DrawString hintereinander ausgeben. Das Resultat war aber nicht zufriedenstellend: Es waren unerwünschte Lücken im Text zu sehen, die nicht zu sehen waren, wenn man den Text als einen zusammenhängenden String mit einer einzigen Farbe und Font ausgibt.

    Das Problem scheint in (meiner?) Anwendung der Measure-String-Methode zu liegen. Meine Versuche mit MeasureString zeigten, dass z.B. die doppelte Breite des String "M" nicht der Breite des String "MM" entspricht - da gibt es erhebliche Differenzen, die nicht mit Rundungsfehlern zu erklären sind.

    Dieses Problem ist sicher nicht neu oder einzigartig, aber ich konnte für diesen Grad der Komplexität bisher nirgends weiter führende Informationen finden.

    Edit: Nachträglich füge ich noch meinen Testcode hinzu:

    void ApplWindow_TextDrawTest(Gdiplus::Graphics *GraphicsContext)
    {
    	Gdiplus::Font TextFont(L"Calibri", 12, Gdiplus::FontStyle::FontStyleBold, Gdiplus::Unit::UnitPoint);
    
    	GraphicsContext->SetTextRenderingHint(TextRenderingHint::TextRenderingHintClearTypeGridFit);
    
    	Gdiplus::StringFormat MyFormat;
    	MyFormat.SetAlignment(Gdiplus::StringAlignment::StringAlignmentNear);
    	MyFormat.SetFormatFlags(Gdiplus::StringFormatFlags::StringFormatFlagsNoWrap);
    
    	const wchar_t *Text1M = L"M";
    	Gdiplus::PointF TextOrigin1M(0, 0);
    	Gdiplus::RectF TextBounds1M;
    
    	const wchar_t *Text2M = L"MM";
    	Gdiplus::PointF TextOrigin2M(0, 50);
    	Gdiplus::RectF TextBounds2M;
    
    	GraphicsContext->MeasureString(Text1M, (INT)wcslen(Text1M), &TextFont, TextOrigin1M, &MyFormat, &TextBounds1M);
    	GraphicsContext->MeasureString(Text2M, (INT)wcslen(Text2M), &TextFont, TextOrigin2M, &MyFormat, &TextBounds2M);
    }
    

    Vor dem Rücksprung finde ich nun die folgenden Werte vor:
    TextBounds1M.Width = 19.7395802
    TextBounds3M.Width = 34.1458282 ≠ TextBounds1M.Width * 2 !!!

    Wie muss ich mir dieses Verhalten erklären?
    Hat jemand einen Tipp (Link) für mich?
    Vielen Dank vorab.



  • Schau dir MeasureCharacterRanges an. Ist schon lang her, dass ichs benutzt habe, deswegen kann ich dir jetzt nicht mehr als den Stichpunkt geben. Damit müsstest du das aber schaffen können, was du brauchst.



  • Danke für den Tipp, er würde wahrscheinlich bei dem oben gezeigten Code helfen; ich werde es aber konkret ausprobieren und das Ergebnis zurückmelden.

    MeasureCharacterRanges hat allerdings den Nachteil, dass ich für die Kalkulation nur einen einzigen Font verwenden kann. Damit könnte ich Fontwechsel innerhalb der Zeile nicht bedienen.



  • Ja, ist wohl nicht so einfach... Der Abstand zwischen zwei Zeichen heißt Kerning. Einige Fonts haben Kerning Tabellen, wie groß der Abstand zwischen zwei bestimmten Zeichen sein soll. Wenn ich mich recht erinnere, kommst du mit GDI+ nicht an die Infos ran, nur mit GDI.
    Kann auch sein, dass MeasureString und MeasureCharacterRanges dir die Größe mit einem gewissen Rand rechts zurückgeben. Ist jetzt eh die Frage, wie man das "richtig" macht... Du könntest sicher eine Möglichkeit finden, die Textbreite ohne diesen Rand zu bekommen, notfalls mit GDI. Wenn du dann noch die Breite von dem Text in der anderen Schriftart bekommst, könntest du die aneinanderreihen. Aber mit welchem Abstand? Und wenn du den Abstand rechts dranlässt, und einfach den nächsten String dranhängst, muss das auch nicht gut unbedingt ausschauen. Ich glaub, das ist tatsächlich auch gar nicht so einfach.



  • Tja ... jedes gute Textedit-Programm kann das alles, aber das Wissen um diese Zusammenhänge wird doch ziemlich verschleiert.
    Aber egal -- die gesamte benötigte Information befindet sich in den Daten der Schrift, und ich werde jetzt einige Zeit damit verbringen, den Weg zu diesen Daten zu finden. Dabei bestehe ich nicht auf der Anwendung von GDI+; ich war nur davon ausgegangen, dass GDI+ einen Fortschritt bedeutet ( 😃 ).



  • Sehe ich nicht so... Wie gesagt, EIN Font hat Tabellen, wie groß der Abstand zwischen zwei bestimmten Zeichen sein soll. Aber was machst du, wenn du zwei verschiedene Fonts in einer Zeile hast, auch noch in unterschiedlichen Größen? Da gibt es meiner Meinung nach keine eindeutige Antwort, da muss man einfach rumprobieren.
    Von Verschleiern würd ich nicht unbedingt reden. Es gibt genug OpenSource Editoren, die sowas können. Du kannst ja mal versuchen dir anzuschauen, wie die sowas machen, nur bin ich skeptisch, ob du das so schnell rausfindest. Jedenfalls hab ich schon paar mal probiert, irgendwelche interessanten Erkenntnisse aus dem Quellcode von OpenOffice, AbiWord usw. zu gewinnen, habs aber nie geschafft.
    Du kannst vielleicht mal in Qt reinschauen, wie die das machen. Ist aber auch nicht so einfach, die ganzen Zusammenhänge zu verstehen.



  • Es ist klar, dass z.B. Kerning bei einem Fontwechsel versagen muss. Aber wenn ich z.B. Textelemente desselben Font (mit derselben Schriftausprägung) mit verschiedenen Farben ausgeben möchte, sollte der Text schon umbrochen werden können, als ob er mit einem einzigen DrawString-Aufruf gezeichnet worden wäre. Bei einem Schriftgrößen-Wechsel kann ich tatsächlich nur den neuen Text an das horizontale Geviert-Ende des vorangegangenen Zeichens setzen.
    Aber dazu muss ich eben eindeutigen Resultate für die Textbreiten aus MeasureString oder ähnlichen Funktionen bekommen.

    Inzwischen habe ich meine Tests auch fortgesetzt und mich an der GDI-Funktion GetCharABCWidthsFloat versucht. Das Ergebnis dieses Versuches sieht - im Vergleich zu den beiden vorangegangenen Tests - noch abwegiger aus. Ich sehe erst einmal nach, ob ich die Bedingungen für die beiden GDI+- und den neuen GDI-Test überhaupt hinreichend vergleichbar gemacht habe, dann werde ich mich wieder melden.

    (Bei Standard-GDI gibt es übrigens auch Funktionen, die mir die Kerning-Werte für bestimmte Zeichenpaare zurückgeben, da werde ich auch noch genauer nachsehen. Denn GDI+ kann auch nicht mehr Informationen über eine Schrift haben als die "alten" Grafik-Funktionen)



  • WishfulThinking schrieb:

    Aber wenn ich z.B. Textelemente desselben Font (mit derselben Schriftausprägung) mit verschiedenen Farben ausgeben möchte, sollte der Text schon umbrochen werden können, als ob er mit einem einzigen DrawString-Aufruf gezeichnet worden wäre.

    Das müsste relativ einfach gehen. Entweder über GDI die Kerning Werte für die zwei Zeichen holen und dann entsprechend selber ausrichten, oder den Abstand mit GDI+ berechnen (Breite von beiden Zeichen zusammen minus die Breite der einzelnen Zeichen sollte doch eigentlich funktionieren...).



  • Klingt gut.

    Aber damit der Text wirklich gut aussieht, muss ich ihn horizontal auch mit Bruchteilen von Pixeln platzieren können -- das geht nur mit Gdi+.

    Die weiter führenden Fontinformationen erhalte ich aber nur aus GDI, und dafür muss ich sogar einen Gdi-kompatiblen Font erstellen.

    Inzwischen habe ich meinen Versuchsaufbau erweitert:
    Test #1 und #2 wie gehabt in Gdi+ aber jetzt mit der Unit Pixel, damit ich gleiche Bedingungen für die Anwendung von Gdi schaffen kann. Test #3 soll dasselbe einfach nur in Gdi nachvollziehen, aber die Resultate sind doch zu verschieden von den beiden ersten Tests. Das kann ich im Moment noch nicht erklären.

    Hier ist der gesamte geänderte und erweiterte Code meines Versuchsaufbaus; die Debug-Resultate für die String-Weiten habe ich überall als Kommentare hinzugefügt:

    void ApplWindow_TextDrawTest(HDC hDC)
    {
    	Gdiplus::Graphics *G = new Gdiplus::Graphics(hDC);
    	G->SetTextRenderingHint(TextRenderingHint::TextRenderingHintClearTypeGridFit);
    
    	Gdiplus::StringFormat MyFormat;
    	MyFormat.SetAlignment(Gdiplus::StringAlignment::StringAlignmentNear);
    	MyFormat.SetFormatFlags(Gdiplus::StringFormatFlags::StringFormatFlagsNoWrap);
    
    	Gdiplus::Font TextFont(L"Calibri", 36, Gdiplus::FontStyle::FontStyleBold, Gdiplus::Unit::UnitPixel);
    
    	const wchar_t *Text1M = L"M";
    	Gdiplus::PointF TextOrigin1M(0, 0);
    	Gdiplus::RectF TextBounds1M;
    
    	const wchar_t *Text2M = L"MM";
    	Gdiplus::PointF TextOrigin2M(0, 50);
    	Gdiplus::RectF TextBounds2M;
    
    	//--- Test #1:  using MeasureString ----------
    	G->MeasureString(Text1M, (INT)wcslen(Text1M), &TextFont, TextOrigin1M, &MyFormat, &TextBounds1M);
    	G->MeasureString(Text2M, (INT)wcslen(Text2M), &TextFont, TextOrigin2M, &MyFormat, &TextBounds2M);
    	//--- Results:  Text 1 Width= 44.414  ("M")
    	//---           Text 2 Width= 76.828  ("MM")
    
    	//--- Test #2:  using MeasureCharacterRanges ----------
    	Gdiplus::Status RCode;
    	Gdiplus::RectF LayoutRect(0, 0, 1000, 100);
    	Gdiplus::Region RegionsList[3];
    	Gdiplus::CharacterRange CRanges[3];
    	CRanges[0].First = 0; CRanges[0].Length = 1;
    	CRanges[1].First = 1; CRanges[1].Length = 1;
    	CRanges[2].First = 0; CRanges[2].Length = 2;
    	MyFormat.SetMeasurableCharacterRanges(3, CRanges);
    	G->MeasureCharacterRanges(Text2M, (INT)wcslen(Text2M), &TextFont, LayoutRect, &MyFormat, 3, RegionsList);
    	RCode = RegionsList[0].GetBounds(&TextBounds1M, G);		// Result: Text 1 Width = 32.000  ("M")
    	RCode = RegionsList[1].GetBounds(&TextBounds1M, G);		// Result: Text 1 Width = 32.000  ("M"; the second char)
    	RCode = RegionsList[2].GetBounds(&TextBounds2M, G);		// Result: Text 2 Width = 64.000  ("MM")
    
    	//--- Test #3:  using the good old GDI ----------
    	int MapModeResult = SetMapMode(hDC, MM_TEXT);			// MM_TEXT is equivalent to Unit::UnitPixel?
    	HFONT TextFont3 = CreateFont(36, 0, 0, 0, FW_BOLD, false, false, false, ANSI_CHARSET, OUT_TT_ONLY_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, VARIABLE_PITCH | FF_DONTCARE, L"Calibri");
    	HGDIOBJ PrevFont = SelectObject(hDC, TextFont3);
    	ABCFLOAT ABCCharData;
    	BOOL RFlg = GetCharABCWidthsFloat(hDC, (UINT)'M', (UINT)'M', &ABCCharData); // Results: abcfA = 2.000; abcfB = 22.000; abcfC = 2.000
    
    	//--- End of Test ---
    	delete G;
    }
    

    Bei einem einzigen Zeichen wird per Definition kein Kerning aktiv, aber trotzdem habe ich bei jedem der drei Tests vollkommen verschiedene Stringbreiten für das Einzelzeichen "M" erhalten.

    Ich habe wohl noch was übersehen, aber ich weiß nicht was ... 😃



  • WishfulThinking schrieb:

    Die weiter führenden Fontinformationen erhalte ich aber nur aus GDI, und dafür muss ich sogar einen Gdi-kompatiblen Font erstellen.

    Du kannst Gdiplus::Font in einen HFONT konvertieren. Gdiplus::Font::GetLogFontW und dann CreateFontIndirect oder so. Vielleicht sind die Fonts ja nicht gleich.

    Ansonsten musst mal mit den Optionen spielen. Auf den ersten Blick seh ich keine Fehler, aber das sagt noch nichts 😉



  • Danke, das war ein klasse Hinweis. Dabei habe ich in den LOGFONT-Daten auch gesehen, dass ich den GDI-Font mit einer falschen Schriftgröße erstellt hatte: Der Parameter hätte auf -36 (statt +36) gesetzt werden müssen.

    Mein dritter Testabschnitt sieht nun so aus:

    //--- Test #3:  using the good old GDI ----------
    	int MapModeResult = SetMapMode(hDC, MM_TEXT);			// MM_TEXT is equivalent to Unit::UnitPixel?
    	LOGFONTW TextFontLogDataW3;
    	RCode = TextFont.GetLogFontW(G, &TextFontLogDataW3);
    	HFONT TextFont3 = CreateFontIndirectW(&TextFontLogDataW3);
    	HGDIOBJ PrevFont = SelectObject(hDC, TextFont3);
    	ABCFLOAT ABCCharData;
    	BOOL RFlg = GetCharABCWidthsFloat(hDC, (UINT)'M', (UINT)'M', &ABCCharData); // Results: abcfA = 2.000; abcfB = 27.000; abcfC = 2.000
    

    Damit kommt das "M" nun auf eine Zeichenbreite von 31 Pixel, das ist aber noch immer nicht nahe genug am Resultat der Messung mit MeasureCharacterRanges.

    Bei den Resultaten aus dem zweiten und dritten Test bin ich aber auch noch generell skeptisch, denn ich kann mir nicht vorstellen, dass hier nur ganzzahlige Werte vorkommen...



  • WishfulThinking schrieb:

    Damit kommt das "M" nun auf eine Zeichenbreite von 31 Pixel, das ist aber noch immer nicht nahe genug am Resultat der Messung mit MeasureCharacterRanges.

    Du hast doch einmal 31 und ein mal 32 Pixel, sind doch schon mal ziemlich ähnliche Werte?
    GetCharABCWidthsFloat gibt dir ja den Abstand vor dem Zeichen, nach dem Zeichen, und die Breite vom Zeichen zurück. Du hast jeweils zwei Pixel vor und hinter dem "M". Aber wenn die gezeichnet werden, sind zwischen "MM" doch keine vier Pixel?
    D.h., das eigentlich interessante für dich ist wahrscheinlich die B Breite von dem GetCharABCWidthsFloat.

    Kannst du die Zeichen stark vergrößert in ein Messraster zeichnen? Also, keinen größeren Font verwenden, sondern einfach hochskalieren, das Raster aber nicht mitskalieren, damit man die Pixel von Hand abzählen kann.



  • Nein, leider kann ich diese Differenz bei weitem nicht akzeptieren. Im Beispiel handelt es sich ja nur um einen einzelnen Buchstaben, und solch eine Differenz würde sich über eine Zeile auch noch vergrößern.

    Was mich auch noch stört, ist, dass die Funktion GetCharABCWidthsFloat immer nur ganzzahlige Werte zurückliefert, egal, welche Schriftgröße ich einsetze.

    Mir ist bekannt, dass Fonts in einem ganzzahligen Raster (i.d.R. 1024x1024 oder 20148x2048) designed werden. daraus errechnen sich dann in Abhängigkeit von der tatsächlich gewählten Schriftgröße die aktuellen Werte der Punkte, u.a. auch für die Breite des Glyph, und dann natürlich immer in float-Werten.

    Wenn ich an die Werte für die Design-Schriftgröße und an die Weiten-Werte dieser Design-Schriftgröße herankäme, wäre die Umrechnung wohl deutlich einfacher. Aber diese Funktionen habe ich noch nicht gefunden. Irgendwie sind diese Werte in den Font-Daten enthalten, und ich mag einfach nicht glauben, dass ich dafür die Daten aus der Font-Datei selbst noch einmal interpretieren muss.



  • Noch schnell ein Nachtrag:

    Messraster usw sind sicher Lösungen für einzelne Zeichen, aber ich möchte schon eine mehr generelle Messmethode finden. (Jeder Editor kann das, und ganz bestimmt auch ohne sehr aufwändige Umwege - sonst würde er sich totrechnen).



  • WishfulThinking schrieb:

    Noch schnell ein Nachtrag:

    Du kannst deinen Beitrag auch bearbeiten.



  • Ist schon klar, aber irgendwie sieht das dann nicht authentisch aus.



  • Das Thema interessiert mich auch. Gibt es eigentlich auch eine Möglichkeit, einen GDI-Font (HFONT) direkt aus dem GDI+-Font abzuleiten und nicht, wie im Beispiel geschehen, den Font in GDI neu zu erstellen?



  • WishfulThinking schrieb:

    Nein, leider kann ich diese Differenz bei weitem nicht akzeptieren. Im Beispiel handelt es sich ja nur um einen einzelnen Buchstaben, und solch eine Differenz würde sich über eine Zeile auch noch vergrößern.

    Das ist schon klar, ich wollte nur sichergehen, dass wir von denselben Werten reden.
    Das Messraster wäre eine Hilfsmethode um festzustellen, wie die Funktionen genau ticken. Das muss man erstmal genau verstanden haben, und die verschiedenen Werte könnte man am Messraster dann evtl. besser nachvollziehen.

    GetCharABCWidthsFloat gibt im MM_TEXT Modus tatsächlich nur Integer zurück. Der Sinn von den Floats hier ist nur, dass du bei anderen Koordinatensystemen genauere Umrechnungen hast.

    PaulB48a schrieb:

    Gibt es eigentlich auch eine Möglichkeit, einen GDI-Font (HFONT) direkt aus dem GDI+-Font abzuleiten und nicht, wie im Beispiel geschehen, den Font in GDI neu zu erstellen?

    Hab ich doch schon geschrieben.



  • Du könntest vielleicht mal versuchen, dir das Buch "Windows Graphics Programming" zu besorgen. Da steht auch einiges zu dem Thema drin. Hab jetzt z.B. noch die Funktion GetCharacterPlacement gefunden, die hatte ich gar nicht mehr in Erinnerung. Bin mir nicht sicher, ob sie dir jezt weiterhilft, könnte aber sein.



  • Mit dem modifizierten Test #3 hatte ich tatsächlich nur die LOGFONT-Daten aus dem Gdi+-Font gewonnen (Funktion GetLogFontW ) und mit diesen Daten dann per CreateFontIndirectW einen neuen Gdi-Font erzeugt, in der Hoffnung, dass dieser wirklich dem Gdi+-Font ähnlich genug ist. Ich habe keinen Zweifel daran, dass beide Fonts ihre Daten aus derselben Quelle bezogen haben, aber ob die beiden Fonts auch in allen Parametern gleich sind, weiß ich leider nicht.

    Immerhin gibt es bei der Test-Messung der Breite von "M" ja Unterschiede.....

    MM_TEXT hatte ich gewählt, weil dieses Maßsystem in GDI und GDI+ gleich zu sein scheint; für die anderen Koordinatensysteme sah ich zwischen GDI und GDI+ eben keine Übereinstimmungen. Welches alternative Koordinatensystem würdest du vorschlagen?

    Dass GetABCCharWidthsFloat im Maßsystem MM_TEXT nur ganzzahlige Werte zurückgibt, habe ich in meiner Quelle (http://msdn.microsoft.com/en-us/library/dd144858%28v=vs.85%29.aspx) nicht nachvollziehen können. Hast du dazu eine andere Quelle? Die würde mich sehr interessieren. Bei der Beschreibung von GetABCCharWidths habe ich übrigens noch einen Hinweis gefunden, wie ich an Messwerte in Font Design Units kommen kann. Die sind, soweit ich weiß, tatsächlich nur ganzzahlig. Der Sache werde ich in dieser Woche mal nachgehen.


Anmelden zum Antworten