89

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

How I can calculate appropriate TextBlock width?

user181813
  • 1,861
  • 6
  • 24
  • 42
NoWar
  • 36,338
  • 80
  • 323
  • 498
  • This has been asked before, also that is dependent on the font as well. – H.B. Feb 13 '12 at 16:54
  • Also you can just get the actual width from `ActualWidth`. – H.B. Feb 13 '12 at 16:57
  • 2
    Using TextRenderer should work for WPF as well: http://stackoverflow.com/questions/721168/how-to-determine-the-size-of-a-string-given-a-font – H.B. Feb 13 '12 at 17:03

7 Answers7

178

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.

RandomEngy
  • 14,931
  • 5
  • 70
  • 113
  • 2
    This was really helpful – Rakesh Dec 23 '15 at 22:30
  • 1
    How to do the same in UWP – Arun Prasad Feb 14 '17 at 15:43
  • 9
    @ArunPrasad That would be an excellent thing to ask on a separate question. This question is clearly scoped to WPF. – RandomEngy Feb 14 '17 at 16:53
  • This returns a width of zero if the candidate passed in is a space. I think this has to do with word wrapping perhaps? – Mark Miller Jun 16 '20 at 18:19
  • 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 ``` – gil123 Jan 03 '22 at 22:33
51

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
Hakan Fıstık
  • 16,800
  • 14
  • 110
  • 131
user1604008
  • 995
  • 9
  • 17
  • 1
    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. – David Rector Apr 30 '16 at 09:37
  • 2
    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. – Tertium Mar 03 '17 at 14:35
16

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

mdip
  • 600
  • 4
  • 10
  • 1
    Unless I'm missing something here, these `lock`s 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? – ABPerson Oct 19 '22 at 20:22
  • 1
    @ABPerson - 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 ;). – mdip Feb 25 '23 at 20:11
  • Haha fair, yeah just wanted to bring it up since I happened to see it in passing! All good – ABPerson Feb 26 '23 at 18:53
  • 1
    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? – mike Feb 27 '23 at 20:25
  • 1
    @mike - 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. – mdip Mar 03 '23 at 15:46
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;
Chef Pharaoh
  • 2,387
  • 3
  • 27
  • 38
  • 4
    You can do simply `d_width = MyText.ActualWidth;` without binding. The problem is when the `TextBlock` is not in the visual tree yet. – xmedeko Oct 15 '15 at 19:38
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);
}
Hakan Fıstık
  • 16,800
  • 14
  • 110
  • 131
NoWar
  • 36,338
  • 80
  • 323
  • 498
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)
isxaker
  • 8,446
  • 12
  • 60
  • 87
-2

Found this for you:

Graphics g = control.CreateGraphics();
int width =(int)g.MeasureString(aString, control.Font).Width; 
g.dispose();
Setheron
  • 3,520
  • 3
  • 34
  • 52