Wie berechnet man die WPF TextBlock-Breite für die bekannte Schriftgröße und die bekannten Zeichen?

79

Angenommen, ich habe TextBlockmit Text "Some Text" und Schriftgröße 10.0 .

Wie kann ich die entsprechende TextBlock Breite berechnen ?

DmitryBoyko
quelle
Dies wurde schon früher gefragt, auch das hängt auch von der Schriftart ab.
HB
Sie können auch nur die tatsächliche Breite von erhalten ActualWidth.
HB
2
Die Verwendung von TextRenderer sollte auch für WPF funktionieren
HB

Antworten:

152

Verwenden Sie die FormattedTextKlasse.

Ich habe in meinem Code eine Hilfsfunktion erstellt:

private Size MeasureString(string candidate)
{
    var formattedText = new FormattedText(
        candidate,
        CultureInfo.CurrentCulture,
        FlowDirection.LeftToRight,
        new Typeface(this.textBlock.FontFamily, this.textBlock.FontStyle, this.textBlock.FontWeight, this.textBlock.FontStretch),
        this.textBlock.FontSize,
        Brushes.Black,
        new NumberSubstitution(),
        1);

    return new Size(formattedText.Width, formattedText.Height);
}

Es gibt geräteunabhängige Pixel zurück, die im WPF-Layout verwendet werden können.

RandomEngy
quelle
Das war wirklich hilfreich
Rakesh
1
Wie man das gleiche in UWP macht
Arun Prasad
6
@ArunPrasad Das wäre eine hervorragende Sache, um eine separate Frage zu stellen. Diese Frage ist eindeutig auf WPF beschränkt.
RandomEngy
Dies gibt eine Breite von Null zurück, wenn der übergebene Kandidat ein Leerzeichen ist. Ich denke, das hat vielleicht mit Zeilenumbruch zu tun?
Mark Miller
42

Für die Aufzeichnung ... Ich gehe davon aus, dass der Benutzer versucht, die Breite, die der textBlock nach dem Hinzufügen zum visuellen Baum einnimmt, programmgesteuert zu bestimmen. IMO wäre eine bessere Lösung als formatierter Text (wie geht man mit so etwas wie textWrapping um?) Die Verwendung von Measure and Arrange für einen Beispiel-TextBlock. z.B

var textBlock = new TextBlock { Text = "abc abd adfdfd", TextWrapping = TextWrapping.Wrap };
// auto sized
textBlock.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
textBlock.Arrange(new Rect(textBlock.DesiredSize));

Debug.WriteLine(textBlock.ActualWidth); // prints 80.323333333333
Debug.WriteLine(textBlock.ActualHeight);// prints 15.96

// constrain the width to 16
textBlock.Measure(new Size(16, Double.PositiveInfinity));
textBlock.Arrange(new Rect(textBlock.DesiredSize));

Debug.WriteLine(textBlock.ActualWidth); // prints 14.58
Debug.WriteLine(textBlock.ActualHeight);// prints 111.72
user1604008
quelle
1
Mit ein paar kleinen Änderungen funktionierte dies für mich beim Schreiben einer Windows 8.1 Store-App (Windows Runtime). Vergessen Sie nicht, die Schriftartinformationen für diesen TextBlock so einzustellen, dass die Breite genau berechnet wird. Ordentliche Lösung und eine Gegenstimme wert.
David Rector
1
Der entscheidende Punkt hierbei ist das Erstellen dieses temporären Textblocks. Alle diese Manipulationen mit vorhandenem Textblock auf einer Seite funktionieren nicht: Die ActualWidth wird erst nach dem Neuzeichnen aktualisiert.
Tertium
12

Die bereitgestellte Lösung war für .NET Framework 4.5 geeignet. Da Windows 10 DPI-Skalierung und Framework 4.6.x jedoch unterschiedliche Unterstützungsgrade hinzufügen, wird der zum Messen von Text verwendete Konstruktor jetzt [Obsolete]zusammen mit allen Konstruktoren dieser Methode markiert pixelsPerDipParameter nicht einschließen .

Leider ist es etwas komplizierter, aber mit den neuen Skalierungsfunktionen wird es zu einer höheren Genauigkeit führen.

### PixelsPerDip

Laut MSDN bedeutet dies:

Der unabhängige Pixelwert für Pixel pro Dichte, der dem Skalierungsfaktor entspricht. Wenn beispielsweise die DPI eines Bildschirms 120 beträgt (oder 1,25, weil 120/96 = 1,25), werden 1,25 Pixel pro dichteunabhängiges Pixel gezeichnet. DIP ist die Maßeinheit, die von WPF verwendet wird, um unabhängig von Geräteauflösung und DPIs zu sein.

Hier ist meine Implementierung der ausgewählten Antwort basierend auf den Anweisungen des GitHub-Repositorys von Microsoft / WPF-Samples mit DPI-Skalierungsbewusstsein.

Es ist eine zusätzliche Konfiguration erforderlich, um die DPI-Skalierung ab Windows 10 Anniversary (unter dem Code) vollständig zu unterstützen. Ich konnte nicht arbeiten. Ohne diese Konfiguration funktioniert dies jedoch auf einem einzelnen Monitor mit konfigurierter Skalierung (und berücksichtigt Änderungen bei der Skalierung). Das Word-Dokument im obigen Repo ist die Quelle dieser Informationen, da meine Anwendung nicht gestartet werden würde, wenn ich diese Werte hinzugefügt hätte. Dieser Beispielcode aus demselben Repo diente auch als guter Bezugspunkt.

public partial class MainWindow : Window
{
    private DpiScale m_dpiInfo;
    private readonly object m_sync = new object();

    public MainWindow()
    {
        InitializeComponent();
        Loaded += OnLoaded;
    }
    
    private Size MeasureString(string candidate)
    {
        DpiScale dpiInfo;
        lock (m_dpiInfo)
            dpiInfo = m_dpiInfo;

        if (dpiInfo == null)
            throw new InvalidOperationException("Window must be loaded before calling MeasureString");

        var formattedText = new FormattedText(candidate, CultureInfo.CurrentUICulture,
                                              FlowDirection.LeftToRight,
                                              new Typeface(this.textBlock.FontFamily, 
                                                           this.textBlock.FontStyle, 
                                                           this.textBlock.FontWeight, 
                                                           this.textBlock.FontStretch),
                                              this.textBlock.FontSize,
                                              Brushes.Black, 
                                              dpiInfo.PixelsPerDip);
        
        return new Size(formattedText.Width, formattedText.Height);
    }

// ... The Rest of Your Class ...

    /*
     * Event Handlers to get initial DPI information and to set new DPI information
     * when the window moves to a new display or DPI settings get changed
     */
    private void OnLoaded(object sender, RoutedEventArgs e)
    {            
        lock (m_sync)
            m_dpiInfo = VisualTreeHelper.GetDpi(this);
    }

    protected override void OnDpiChanged(DpiScale oldDpiScaleInfo, DpiScale newDpiScaleInfo)
    {
        lock (m_sync)
            m_dpiInfo = newDpiScaleInfo;

        // Probably also a good place to re-draw things that need to scale
    }
}

Andere Vorraussetzungen

Gemäß der Dokumentation unter Microsoft / WPF-Samples müssen Sie dem Anwendungsmanifest einige Einstellungen hinzufügen, um die Fähigkeit von Windows 10 Anniversary abzudecken, in Konfigurationen mit mehreren Monitoren unterschiedliche DPI-Einstellungen pro Anzeige vorzunehmen. Es ist eine gute Vermutung, dass das OnDpiChanged-Ereignis ohne diese Einstellungen möglicherweise nicht ausgelöst wird, wenn ein Fenster mit unterschiedlichen Einstellungen von einer Anzeige zur anderen verschoben wird, sodass Ihre Messungen weiterhin auf den vorherigen basieren DpiScale. Die Anwendung, die ich schrieb, war für mich allein, und ich habe kein solches Setup, sodass ich nichts zu testen hatte. Als ich den Anweisungen folgte, erhielt ich eine App, die aufgrund eines Manifests nicht gestartet werden konnte Fehler, also habe ich aufgegeben, aber es wäre eine gute Idee, das zu überprüfen und Ihr App-Manifest so anzupassen, dass es Folgendes enthält:

<application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
        <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
        <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitor</dpiAwareness>
    </windowsSettings>
</application>

Laut Dokumentation:

Die Kombination dieser beiden Tags hat den folgenden Effekt: 1) Pro Monitor für> = Windows 10 Anniversary Update 2) System <Windows 10 Anniversary Update

mdip
quelle
5

Ich habe einige Methoden gefunden, die gut funktionieren ...

/// <summary>
/// Get the required height and width of the specified text. Uses Glyph's
/// </summary>
public static Size MeasureText(string text, FontFamily fontFamily, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, double fontSize)
{
    Typeface typeface = new Typeface(fontFamily, fontStyle, fontWeight, fontStretch);
    GlyphTypeface glyphTypeface;

    if (!typeface.TryGetGlyphTypeface(out glyphTypeface))
    {
        return MeasureTextSize(text, fontFamily, fontStyle, fontWeight, fontStretch, fontSize);
    }

    double totalWidth = 0;
    double height = 0;

    for (int n = 0; n < text.Length; n++)
    {
        ushort glyphIndex = glyphTypeface.CharacterToGlyphMap[text[n]];

        double width = glyphTypeface.AdvanceWidths[glyphIndex] * fontSize;

        double glyphHeight = glyphTypeface.AdvanceHeights[glyphIndex] * fontSize;

        if (glyphHeight > height)
        {
            height = glyphHeight;
        }

        totalWidth += width;
    }

    return new Size(totalWidth, height);
}

/// <summary>
/// Get the required height and width of the specified text. Uses FortammedText
/// </summary>
public static Size MeasureTextSize(string text, FontFamily fontFamily, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, double fontSize)
{
    FormattedText ft = new FormattedText(text,
                                            CultureInfo.CurrentCulture,
                                            FlowDirection.LeftToRight,
                                            new Typeface(fontFamily, fontStyle, fontWeight, fontStretch),
                                            fontSize,
                                            Brushes.Black);
    return new Size(ft.Width, ft.Height);
}
DmitryBoyko
quelle
2
Das Summieren von Glyphenbreiten funktioniert bei den meisten Schriftarten nicht, da das Kerning nicht berücksichtigt wird .
Nicolas Repiquet
4

Ich habe dieses Problem behoben, indem ich dem Element im Backend-Code einen Bindungspfad hinzugefügt habe:

<TextBlock x:Name="MyText" Width="{Binding Path=ActualWidth, ElementName=MyText}" />

Ich fand, dass dies eine viel sauberere Lösung ist, als den gesamten Aufwand der oben genannten Referenzen wie FormattedText zu meinem Code hinzuzufügen.

Danach konnte ich Folgendes tun:

double d_width = MyText.Width;
Chef Pharao
quelle
2
Sie können einfach d_width = MyText.ActualWidth;ohne Bindung tun . Das Problem ist, wenn das TextBlocknoch nicht im visuellen Baum ist.
Xmedeko
0

Ich benutze dieses:

var typeface = new Typeface(textBlock.FontFamily, textBlock.FontStyle, textBlock.FontWeight, textBlock.FontStretch);
var formattedText = new FormattedText(textBlock.Text, Thread.CurrentThread.CurrentCulture, textBlock.FlowDirection, typeface, textBlock.FontSize, textBlock.Foreground);

var size = new Size(formattedText.Width, formattedText.Height)
isxaker
quelle
-3

Fand dies für Sie:

Graphics g = control.CreateGraphics();
int width =(int)g.MeasureString(aString, control.Font).Width; 
g.dispose();
Setheron
quelle
24
Dieser spezielle Ansatz gilt nur für WinForms.
HB