How to calculate WPF TextBlock width for its known font size and characters?
Asked Answered
C

7

92

Let's say I have TextBlock with text "Some Text" and font size 10.0.

How I can calculate appropriate TextBlock width?

Counterstamp answered 13/2, 2012 at 16:47 Comment(3)
This has been asked before, also that is dependent on the font as well.Hadhramaut
Also you can just get the actual width from ActualWidth.Hadhramaut
Using TextRenderer should work for WPF as well: #721668Hadhramaut
W
179

Use the FormattedText class.

I made a helper function in my code:

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(),
        VisualTreeHelper.GetDpi(this.textBlock).PixelsPerDip);

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

It returns device-independent pixels that can be used in WPF layout.

Waves answered 13/2, 2012 at 19:0 Comment(5)
This was really helpfulKimura
How to do the same in UWPBowne
@ArunPrasad That would be an excellent thing to ask on a separate question. This question is clearly scoped to WPF.Waves
This returns a width of zero if the candidate passed in is a space. I think this has to do with word wrapping perhaps?Derrick
In my case you need also to configure these: c# formattedText.MaxTextHeight = textBlock.MaxHeight; formattedText.MaxTextWidth = textBlock.MaxWidth; formattedText.Trimming = TextTrimming.None; // Change this value in case you do have trimming Proprietress
F
52

For the record... I'm assuming the op'er is trying to programically determine the width that the textBlock will take up after being added to the visual tree. IMO a better solution then formattedText (how do you handle something like textWrapping?) would be to use Measure and Arrange on a sample TextBlock. e.g.

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
Fleury answered 31/10, 2014 at 17:53 Comment(2)
With a few minor modifications, this worked for me writing a Windows 8.1 Store app (Windows Runtime). Don't forget to set the font information for this TextBlock so that it calculates the width accurately. Neat solution and worth an upvote.Clydesdale
The key point here is creating this temporary textblock in place. All these manipulations with existing textblock on a page don't work: its ActualWidth is updated only after redraw.Adamantine
T
18

The provided solution was appropriate for .Net Framework 4.5, however, with Windows 10 DPI scaling and Framework 4.6.x adding varying degrees of support for it, the constructor used to measure text is now marked [Obsolete], along with any constructors on that method that do not include the pixelsPerDip parameter.

Unfortunately, it's a little more involved, but it will result in greater accuracy with the new scaling capabilities.

###PixelsPerDip

According to MSDN, this represents:

The Pixels Per Density Independent Pixel value, which is the equivalent of the scale factor. For example, if the DPI of a screen is 120 (or 1.25 because 120/96 = 1.25) , 1.25 pixel per density independent pixel is drawn. DIP is the unit of measurement used by WPF to be independent of device resolution and DPIs.

Here's my implementation of the selected answer based on guidance from the Microsoft/WPF-Samples GitHub repository with DPI scaling awareness.

There is some additional configuration required to completely support DPI scaling as of Windows 10 Anniversary (below the code), which I couldn't get to work, but without it this works on a single monitor with scaling configured (and respects scaling changes). The Word document in the above repo is the source of that information since my application wouldn't launch once I added those values. This sample code from the same repo also served as a good reference point.

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_sync)
            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
    }
}

Other Requirements

According to the documentation at Microsoft/WPF-Samples, you need to add some settings to the application's manifest to cover Windows 10 Anniversary's ability to have different DPI settings per display in multiple-monitor configurations. It's a fair guess that without these settings, the OnDpiChanged event might not be raised when a window is moved from one display to another with different settings, which would make your measurements continue to rely on the previous DpiScale. The application I was writing was for me, alone, and I don't have that kind of a setup so I had nothing to test with and when I followed the guidance, I ended up with an app that wouldn't start due to manifest errors, so I gave up, but it'd be a good idea to look that over and adjust your app manifest to contain:

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

According to the documentation:

The combination of [these] two tags have the following effect : 1) Per-Monitor for >= Windows 10 Anniversary Update 2) System < Windows 10 Anniversary Update

Transfigure answered 9/4, 2017 at 20:7 Comment(5)
Unless I'm missing something here, these locks are totally unnecessary - not only is assigning the DpiScale already an atomic operation so there's no need to lock it, and not only is only one thing locking on m_dpiInfo while the rest do m_sync, but isn't OnDpiChanged just executed on the UI thread like everything else anyway? Where even are the multiple threads here?Overstep
@Overstep - You're right on both points (and good catch ... I didn't see the typo; I edited it to at least do the needless thing correctly). The code was pulled from something else that worked differently and it's been so long that I can't remember why I felt the need to lock (it was probably lockless, originally, given what I suspect I used it in). Or it may have been stupidity on my part ;).Transfigure
Haha fair, yeah just wanted to bring it up since I happened to see it in passing! All goodOverstep
Why must the DpiScale information be stored in a field at all? Why not use var dpiInfo = VisualTreeHelper.GetDpi(this.textBlock)? Performance? Threading? Can anyone help me understand?Tildatilde
@Tildatilde - The original application this was written for docked itself to another program to make it appear as a "plug-in". The app had to minimize the impact of "moving the window along with the other one" while never "drifting" so I suspect that was done b/c other performance considerations were so high but it probably represents a premature optimization.Transfigure
C
5

I found some methods which work fine...

/// <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);
}
Counterstamp answered 13/2, 2012 at 19:6 Comment(1)
Summing glyph widths will not work for most fonts as it doesn't account for kerning.Serigraph
L
5

I resolved this by adding a binding path to the element in the backend code:

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

I found this to be a much cleaner solution than adding all the overhead of the above references like FormattedText to my code.

After, I was able to do this:

double d_width = MyText.Width;
Longeron answered 21/6, 2013 at 23:14 Comment(1)
You can do simply d_width = MyText.ActualWidth; without binding. The problem is when the TextBlock is not in the visual tree yet.Contradance
M
0

I use this one:

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)
Mincemeat answered 24/10, 2018 at 14:53 Comment(0)
K
-2

Found this for you:

Graphics g = control.CreateGraphics();
int width =(int)g.MeasureString(aString, control.Font).Width; 
g.dispose();
Killigrew answered 13/2, 2012 at 16:58 Comment(1)
This specific approach only applies to WinForms.Hadhramaut

© 2022 - 2024 — McMap. All rights reserved.