如何根据已知字体大小和字符计算WPF TextBlock的宽度?

89
假设我有一个文本块(TextBlock),其中包含文本"Some Text"和字体大小为10.0。
我该如何计算合适的TextBlock宽度?

这个问题以前已经被问过了,而且这也取决于字体。 - H.B.
你也可以直接从 ActualWidth 获取实际宽度。 - H.B.
2
使用TextRenderer也适用于WPF:https://dev59.com/questions/KHRB5IYBdhLWcg3wF0HH - H.B.
7个回答

178

使用FormattedText类。

我在我的代码中创建了一个辅助函数:

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

它返回可用于 WPF 布局的设备无关像素。


2
这真的很有帮助。 - Rakesh
1
如何在UWP中执行相同操作 - Arun Prasad
9
这是一个需要分开提问的好问题,请在另一个问题中询问。这个问题明确限定了范围,是关于WPF的。 - RandomEngy
如果传入的候选项是空格,则返回零宽度。我认为这可能与自动换行有关? - Mark Miller
在我的情况下,您还需要配置以下内容: formattedText.MaxTextHeight = textBlock.MaxHeight; formattedText.MaxTextWidth = textBlock.MaxWidth; formattedText.Trimming = TextTrimming.None; // 如果有修剪,请更改此值 - gil123

51

就记录而言,我假设操作者正在尝试通过编程确定将文本块添加到可视树后占用的宽度。在我看来,比使用格式化文本(如何处理文本换行?)更好的解决方案是在示例TextBlock上使用Measure和Arrange。

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

1
通过一些小的修改,这个方法对我编写Windows 8.1商店应用(Windows Runtime)很有效。不要忘记为这个TextBlock设置字体信息,以便它准确计算宽度。这是一个简洁的解决方案,值得点赞。 - David Rector
2
这里的关键点是在原地创建这个临时文本块。对页面上现有文本块的所有操作都无效:它的ActualWidth只有在重绘后才会更新。 - Tertium

16
提供的解决方案适用于.Net Framework 4.5,然而随着Windows 10 DPI缩放和Framework 4.6.x对其添加不同程度的支持,用于测量文本的构造函数现在被标记为[Obsolete],以及该方法上未包括pixelsPerDip参数的任何构造函数。
不幸的是,这需要更多的工作,但它将在新的缩放能力方面具有更高的准确性。
### PixelsPerDip 根据MSDN,这代表:
像素每个独立像素密度值,相当于比例因子。例如,如果屏幕的DPI为120(或1.25,因为120/96 = 1.25),则绘制1.25像素每个独立像素密度。DIP是WPF使用的测量单位,以独立于设备分辨率和DPI。
以下是基于Microsoft/WPF-Samples GitHub存储库的指导和DPI缩放意识的选定答案的实现。

在Windows 10周年更新中,为了完全支持DPI缩放,还需要进行一些额外的配置(以下是代码),但我无法使其正常工作。不过,即使没有这些配置,它也可以在单个显示器上配置缩放并且能够遵循缩放更改。以上存储库中的Word文档是该信息的来源,因为一旦添加了那些值,我的应用程序将无法启动。同一存储库中的此示例代码也是一个很好的参考点。

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

其他要求

根据Microsoft/WPF-Samples的文档,您需要向应用程序清单中添加一些设置,以覆盖Windows 10周年纪念版在多显示器配置中具有不同DPI设置的能力。 没有这些设置,当窗口从一个具有不同设置的显示器移动到另一个显示器时,可能不会引发OnDpiChanged事件,这将使您的测量继续依赖于先前的DpiScale。 我编写的应用程序只是为我自己而写的,我没有那种设置可以进行测试,当我遵循指南时,我最终得到了一个由于清单错误而无法启动的应用程序,所以我放弃了,但检查并调整应用程序清单以包含以下内容是个好主意:

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

根据文档:

这两个标签的组合具有以下效果: 1)对于 Windows 10 周年更新及以上版本,为 Per-Monitor 2)对于低于 Windows 10 周年更新版本,为 System


1
除非我在这里漏掉了什么,否则这些“锁”是完全不必要的 - 不仅分配“DpiScale”已经是原子操作,因此无需锁定它,而且只有一件事在“m_dpiInfo”上锁定,而其余的都是“m_sync”,但是“OnDpiChanged”难道不像其他所有东西一样在UI线程上执行吗?这里甚至有多个线程吗? - ABPerson
1
@ABPerson - 你说得对,两点都没错(而且发现了笔误...我已经编辑过来了,至少做了不必要的事情是正确的)。这段代码是从其他地方提取出来的,它的工作方式与原来的不同,而且已经过去很长时间了,我已经记不起来为什么我觉得需要锁定了(考虑到我怀疑我在使用它时可能是无锁的)。或者这可能是我的愚蠢之举 ;)。 - mdip
1
@ABPerson - 你两点都说对了(而且发现了错误...我没看到那个拼写错误;我已经编辑过了,至少把不必要的事情做正确了)。这段代码是从另一个工作方式不同的地方提取出来的,而且已经过去很久了,我已经记不起当初为什么觉得需要加锁了(根据我怀疑我使用它的情况,最初可能是无锁的)。或者这可能只是我的愚蠢之举 ;)。 - mdip
哈哈,好的,是的,我只是想提一下,因为我碰巧路过看到了!一切都好。 - ABPerson
1
为什么必须将DpiScale信息存储在字段中?为什么不使用var dpiInfo = VisualTreeHelper.GetDpi(this.textBlock)?性能?线程?有人可以帮我理解吗? - mike
1
@mike - 这个应用程序最初是为了将自己停靠到另一个程序中,以使其看起来像一个“插件”。该应用程序必须最小化“随着其他窗口移动”的影响,同时永远不会“漂移”,因此我怀疑这样做是因为其他性能考虑非常高,但这可能代表了一种过早的优化。 - mdip

5

我发现一些方法可以很好地运作...

/// <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);
}

5
对大多数字体来说,简单地将字形宽度相加是行不通的,因为它没有考虑到字距调整(kerning)。 - Nicolas Repiquet

5
我在后端代码中添加了一个绑定路径,以解决此问题:

在后端代码中为元素添加一个绑定路径即可解决此问题:

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

我发现这是一种比将诸如FormattedText之类的参考内容添加到我的代码中更为简洁的解决方案。

然后,我能够做到这一点:

double d_width = MyText.Width;

4
无需绑定,您可以简单地使用 d_width = MyText.ActualWidth;。问题出现在当 TextBlock 尚未出现在可视树中时。 - xmedeko

0
我使用这个:
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)

-2

为您找到了这个:

Graphics g = control.CreateGraphics();
int width =(int)g.MeasureString(aString, control.Font).Width; 
g.dispose();

24
此特定方法仅适用于WinForms。 - H.B.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接