Laden...

MeasureString und Alternativen [Minimales einschließendes Rechteck der Schrift ermitteln]

Erstellt von cellardoor vor 15 Jahren Letzter Beitrag vor 15 Jahren 10.954 Views
C
cellardoor Themenstarter:in
40 Beiträge seit 2007
vor 15 Jahren
MeasureString und Alternativen [Minimales einschließendes Rechteck der Schrift ermitteln]

Hallo Community,

es geht, wie schon in einigen der Beiträge hier des öfteren behandelt, um die Ermittlung der Zeichenbreite einer Zeichenkette.

Wenn ich für eine Zeichenkette die Methode MeasureString anwende, wird mir eine Breite von 848 Pixeln zurückgegeben. Diese ist zu weit.

Wenn ich, wie hier in einigen Beiträgen gelesen, ein StringFormat anpasse und übergebe, erhalte ich eine Breite von 815 Pixeln. Dies ist aber wieder zu kurz.

Der verwendete Code hierfür ist:


   StringFormat sf = new StringFormat(StringFormat.GenericTypographic);
   sf.FormatFlags = StringFormatFlags.MeasureTrailingSpaces;		
   SizeF sz = gCheck.MeasureString(text, font, -1, sf);

Bei Codeproject werden zwei Alternativen für die Methode MeasureString genannt.

Der Artikel ist unter Bypass Graphics.MeasureString limitations verfügbar.

Hier der Excerpt der beiden besprochenen Funktionen:


static public int MeasureDisplayStringWidth(Graphics graphics, string text, Font font)
{
    const int width = 32;
    System.Drawing.Bitmap   bitmap = new System.Drawing.Bitmap (width, 1, graphics);
    System.Drawing.SizeF    size   = graphics.MeasureString (text, font);
    System.Drawing.Graphics anagra = System.Drawing.Graphics.FromImage(bitmap);

    int measured_width = (int) size.Width;

    if (anagra != null)
    {
        anagra.Clear (Color.White);
        anagra.DrawString (text+"|", font, Brushes.Black, width - measured_width, -font.Height / 2);

        for (int i = width-1; i >= 0; i--)
        {
            measured_width--;
            if (bitmap.GetPixel (i, 0).R != 255)    // found a non-white pixel ?
                break;
        }
    }

    return measured_width;
}

und


static public int MeasureDisplayStringWidth(Graphics graphics, string text, Font font)
{
    System.Drawing.StringFormat format  = new System.Drawing.StringFormat ();
    System.Drawing.RectangleF   rect    = new System.Drawing.RectangleF(0, 0, 1000, 1000);
    System.Drawing.CharacterRange[] ranges  =  { new System.Drawing.CharacterRange(0, text.Length) };
    System.Drawing.Region[] regions = new System.Drawing.Region[1];

    format.SetMeasurableCharacterRanges (ranges);

    regions = graphics.MeasureCharacterRanges (text, font, rect, format);
    rect    = regions[0].GetBounds (graphics);

    return (int)(rect.Right + 1.0f);
}

Diese beiden Methoden geben mir eine Weite von 821 Pixeln zurück.

Ich bekomme, also 3 verschiedene Werte, die alle falsch sind:

  • Methode 1: 848px -> zu gross
  • Methode 2: 815px -> zu klein
  • Methoden 3 und 4: 821px -> zu klein

Noch ein Hinweis: Der verwendete Font ist eine externe TTF-Datei. Könnte dies die Ursache dafür sein, dass auch die Methoden 2,3 und 4, die in Foren als genau genannt werden, nicht funktionieren?

Any help would be fine.

André

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo cellardoor,

Ich bekomme, also 3 verschiedene Werte, die alle falsch sind:

  • Methode 1: 848px -> zu gross
  • Methode 2: 815px -> zu klein
  • Methoden 3 und 4: 821px -> zu klein

was wäre denn richtig und wonach definierst du richtig (und falsch)?

herbivore

C
cellardoor Themenstarter:in
40 Beiträge seit 2007
vor 15 Jahren

Hallo herbivore,

wenn ich die Weite des ausgegebenen Bildes manuell auf 840 setze, schliesst das Bild rechts genau mit dem Ende des letzten Buchstabens der Zeichenkette ab.

Und in meinem Verständnis würde ich diesen Rückgabewert auch von der Funktion MesureString erwarten.

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo cellardoor,

ich habe die bisherige Diskussion um MeasureString immer nur am Rande verfolgt, aber es scheint ja wirklich schwierig zu sein, bzw. keine echte Lösung zu geben, zumindest für den Fall, dass man wie du genau das minimal einschließende Rechteck der Schrift auf dem Bildschirm haben will.

Deshalb schlage ich mal folgende, sicher nicht besonders performante, aber dafür vermutlich 100%ig akkurate Lösung vor:

Man holt sich mit MeasureString ganz normal ein umschließendes Rechteck und erzeugt eine leere weiße Bitmap in dieser Größe (float-Werte dazu immer aufrunden). Dann zeichnet man mit Graphics.FromImage ().DrawString den Text schwarz in die Bitmap an Position (0, 0). Anschließend durchläuft man immer eine ganze Pixel-Spalte senkrecht nach und nach von links nach rechts, bis man den am weitesten Links liegenden nicht weißen Pixel gefunden hat. Das gleiche von rechts nach links, bis man den am weitesten rechts liegenden Pixel gefunden hat. Analog kann man das für oben und unten machen. Damit hat man das das minimale Rechteck gefunden.

herbivore

C
cellardoor Themenstarter:in
40 Beiträge seit 2007
vor 15 Jahren

Hi,

der von Dir gemachte Vorschlag sollte eigentlich mit Methode 2 umgesetzt sein.

Hier nochmal der Code:


static public int MeasureDisplayStringWidth(Graphics graphics, string text, Font font)
{
    const int width = 32;
    System.Drawing.Bitmap   bitmap = new System.Drawing.Bitmap (width, 1, graphics);
    System.Drawing.SizeF    size   = graphics.MeasureString (text, font);
    System.Drawing.Graphics anagra = System.Drawing.Graphics.FromImage(bitmap);

    int measured_width = (int) size.Width;

    if (anagra != null)
    {
        anagra.Clear (Color.White);
        anagra.DrawString (text+"|", font, Brushes.Black, width - measured_width, -font.Height / 2);

        for (int i = width-1; i >= 0; i--)
        {
            measured_width--;
            if (bitmap.GetPixel (i, 0).R != 255)    // found a non-white pixel ?
                break;
        }
    }

    return measured_width;
} 

Es wird die mit MeasureString ermittelte Größe verwendet, dann wird ans Ende der Zeichenkette ein "|" gehangen und der String so auf ein neues Bild gezeichnet, dass nur die letzten 32Pixel untersucht werden müssen.

Trotzdem erhalte ich bei dieser Methode einen zu kleinen Wert, obwohl es richtig und logisch aussieht.

Ich bin den Code schon gefühlte 1000mal durchgegangen, und kann weder einen Fehler, noch Verbesserungsmöglichkeiten finden.

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo cellardoor,

der von Dir gemachte Vorschlag sollte eigentlich mit Methode 2 umgesetzt sein.

kannst du mal sehen, dann ist ja meine Idee doch nicht so abwegig. 🙂

Allerdings war mein Vorschlag, nicht nur auf das Ende des Textes zu gucken, sondern auf den gesamten Text.

Ich habe das mal umgesetzt und siehe da, es funktioniert.

Dabei habe ich es mir ganz einfach gemacht. Ich suche nicht wie von mir vorgeschlagen, von außen, nach innen, sondern einfach alle Pixel durch und ermittle das horizontale und vertikale Minimum und Maximum der Pixelpositionen der von der Hintergrundfarbe abweichenden Pixel. Dabei verwende ich das grottenlangsame GetPixel. Hier besteht also in doppelter Hinsicht Optimierungspotential. Ich verstehe meinen Code nur als Proof-of-Concept.

Im OnPaint zeichne ich ein Rechteck in der ermittelten Größe und darüber/davor den Text. Das Ergebnis ist im Anhang zu sehen.


using System;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Imaging;

//*****************************************************************************
public class MyWindow : Form
{
   //--------------------------------------------------------------------------
   Font   _font    = new Font ("Arial", 20);
   String _strText = "Guten Tag";

   //==========================================================================
   protected override void OnPaint (PaintEventArgs e)
   {
      //-----------------------------------------------------------------------
      // Erstmal lassen wir die Oberklasse machen, ...
      //-----------------------------------------------------------------------
      base.OnPaint (e);

      //-----------------------------------------------------------------------
      // ...  dann ermitteln wir das den Text umschließende Rechteck und ...
      //-----------------------------------------------------------------------
      Rectangle rect = MeasureString (_strText, _font, e.Graphics);

      //-----------------------------------------------------------------------
      // ... zeichnen Rechteck und Text.
      //-----------------------------------------------------------------------
      e.Graphics.DrawRectangle (Pens.Red, rect);
      e.Graphics.DrawString (_strText, _font, Brushes.Black, 0, 0);
   }

   //==========================================================================
   public Rectangle MeasureString (String strText, Font font, Graphics gDevice)
   {
      //-----------------------------------------------------------------------
      // Bitmap in der maximalen Größe des Texts erstellen, deren
      // Hintergrundfarbe ermitteln und schließlich den Text hineinzeichnen.
      //-----------------------------------------------------------------------
      SizeF  sizef = gDevice.MeasureString (strText, font);
      Bitmap bmp   = new Bitmap (((int)sizef.Width)  + 1,
                                 ((int)sizef.Height) + 1,
                                 gDevice);
      Color clrBackground = bmp.GetPixel (0,0);
      Graphics gImage = Graphics.FromImage (bmp);
      gImage.DrawString (strText, font, Brushes.Red, 0, 0);

      //-----------------------------------------------------------------------
      // Wir ermitteln die Position des jeweils am weitesten links, rechts,
      // oben und unten liegenden Pixels, das von der Hintergrundfarbe
      // abweicht.
      //-----------------------------------------------------------------------
      int iXMin = bmp.Width;
      int iXMax = 0;
      int iYMin = bmp.Height;
      int iYMax = 0;

      for (int iY = 0; iY < bmp.Height; ++iY) {
         for (int iX = 0; iX < bmp.Width; ++iX) {
            if (bmp.GetPixel (iX, iY) != clrBackground) {
               if (iX < iXMin) { iXMin = iX; }
               if (iX > iXMax) { iXMax = iX; }
               if (iY < iYMin) { iYMin = iY; }
               if (iY > iYMax) { iYMax = iY; }
            }
         }
      }

      //-----------------------------------------------------------------------
      // Wenn wir kein solches Pixel gefunden haben, geben wir ein Rechteck
      // der Größe 0 zurück.
      //-----------------------------------------------------------------------
      if (iXMin > iXMax || iYMin > iYMax) {
         return default (Rectangle);
      }

      //-----------------------------------------------------------------------
      // Ansonsten geben wir das Rechteck zurück, das den Text genau
      // umschließt. Mit Umschließen ist gemeint, dass ein Text, der über
      // (also vor) das Rechteck gezeichnet wird, alle vier Kanten des
      // Rechtecks teilweise verdeckt, aber an keiner Stelle über die
      // Kanten nach außen herausragt.
      //-----------------------------------------------------------------------
      return new Rectangle (iXMin, iYMin, iXMax - iXMin, iYMax - iYMin);
   }
}

//*****************************************************************************
public static class App
{
   //==========================================================================
   public static void Main (string [] astrArg)
   {
      Application.Run (new MyWindow ());
   }
}

herbivore

Suchhilfe: 1000 Worte, MeasureText, MeasureString

C
cellardoor Themenstarter:in
40 Beiträge seit 2007
vor 15 Jahren

Hallo Herbivore,

erstmal vielen Dank für Deine Hilfe. Das sieht schon ganz gut aus.

Ich werde mir dies in den nächsten Tagen mal anschauen.

Was ich mich aber frage, ist, wieso die anderen Methoden, die als funktionierende Alternativen zu der Funktion MeasureString aufgeführt werden, nicht funktionieren.

In den Foren bzw. bei Codeproject werden diese Methoden als funktionierend aufgeführt. Und dies tun sie zumindest bei mir nicht.

Daher nochmal die Frage, ob es daran liegen könnte, dass die verwendete TTF-Datei keine Systemdatei ist, sondern eine Fremddatei.

Grüße Andre.

4.931 Beiträge seit 2008
vor 15 Jahren

Hallo cellardoor,

kennst du dich denn mit Fonts (und deren Interna) aus?

Die Schwierigkeit besteht darin, den überlappenden Bereich zweier oder mehrerer Glyphen (Zeichen) richtig zu berechnen.
Zum einen ist die MeasureString-Methode auf Performance hin programmiert (denn sonst müßte sie wirklich erst den Text auf eine Bitmap zeichnen und dann deren Größe bestimmen) und zum anderen ist die Größe nicht eindeutig festgelegt (da je nach Anwendungsfall die Ränder hinzu oder abgerechnet werden sollen).

In deiner Methode 2 ist zum Beispiel 'MeasureTrailingSpaces' angegeben, d.h. die nachfolgenden Leerzeichen werden miteingerechnet (um z.B. daran einen weiteren Text zu hängen).

Hast du denn deinen Text auch mit demselben Format-Parameter ausgegeben, als du ihn danach von Hand ausgemessen hast.
Denn was mich sehr wundert, ist, daß du mit 'MeasureTrailingSpaces' einen kleineren Wert erhälst als ohne!!!

Und wegen deiner Frage zu der TTF-Datei.
Es kann je nach Text (und demnach je nach Zeichen) zu einigen Ungenauigkeiten bei der Berechnung kommen (diese sollten aber eigentlich nur ein paar Pixel sein - abgesehen jetzt mal von MeasureTrailingSpaces: ich hatte bei mir dort einen Unterschied von 57,060 zu 51,841).
Die Nachkommastellen kommen wegen der Skalierung auf die darzustellende Fontgröße zustande.

C
401 Beiträge seit 2007
vor 15 Jahren

Hi,

ich weiß nicht ob es alle Controls haben, aber zumindest die TextBox hat ein Property namens PreferredSize. Vielleicht kannst du damit ja was anfangen. Ich habe damit z.B. GridViewColumns in der Länge angepasst.

Gruß

Dario

5.299 Beiträge seit 2008
vor 15 Jahren

Hi celladore!

Du kannst dir mal die Methode Graphics.MeasureCharacterRanges() angucken. Die mißt exakt, wie im Bild von Herbivore gezeigt.
Problem mit diesen exakt gemessenen Rechtecken: für ein LayoutRectangle in einem DrawString-Aufruf ist das zu klein. Aber willst du ja eh nicht.

Der frühe Apfel fängt den Wurm.