Как рассчитать ширину текстового блока WPF для известного размера шрифта и символов?

81

Допустим, у меня есть TextBlockтекст "Some Text" и размер шрифта 10.0 .

Как я могу рассчитать подходящую TextBlock ширину ?

ДмитрийБойко
источник
Об этом уже спрашивали раньше, также это зависит от шрифта.
HB,
Также вы можете просто получить фактическую ширину из ActualWidth.
HB,
2
Использование TextRenderer также должно работать для WPF: stackoverflow.com/questions/721168/…
HB

Ответы:

156

Воспользуйтесь 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(),
        1);

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

Он возвращает независимые от устройства пиксели, которые можно использовать в макете WPF.

RandomEngy
источник
Это было действительно полезно
Ракеш
1
Как сделать то же самое в UWP
Арун Прасад
6
@ArunPrasad Было бы неплохо задать отдельный вопрос. Этот вопрос явно относится к WPF.
RandomEngy
Это возвращает нулевую ширину, если переданный кандидат является пробелом. Я думаю, это может быть связано с переносом слов?
Марк Миллер
43

Для записи ... Я предполагаю, что оператор пытается программно определить ширину, которую textBlock займет после добавления в визуальное дерево. ИМО лучшим решением, чем formattedText (как вы обрабатываете что-то вроде textWrapping?), Было бы использовать Measure and Arrange в образце TextBlock. например

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
источник
1
С небольшими изменениями у меня это сработало, когда я написал приложение Windows 8.1 Store (Windows Runtime). Не забудьте установить информацию о шрифте для этого TextBlock, чтобы он точно рассчитывал ширину. Отличное решение, за которое стоит проголосовать.
Дэвид Ректор
1
Ключевым моментом здесь является создание этого временного текстового блока на месте. Все эти манипуляции с существующим текстовым блоком на странице не работают: его ActualWidth обновляется только после перерисовки.
Tertium
13

Предоставленное решение было подходящим для .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.

Для полной поддержки масштабирования DPI начиная с Windows 10 Anniversary (под кодом) требуется некоторая дополнительная конфигурация , которую я не мог заставить работать, но без нее это работает на одном мониторе с настроенным масштабированием (и с учетом изменений масштабирования). Документ 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_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
    }
}

Другие требования

Согласно документации в Microsoft / WPF-Samples, вам необходимо добавить некоторые настройки в манифест приложения, чтобы охватить возможность Windows 10 Anniversary иметь разные настройки 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) Per-Monitor для> = Windows 10 Anniversary Update 2) System <Windows 10 Anniversary Update

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);
}
ДмитрийБойко
источник
3
Суммирование ширины глифов не работает для большинства шрифтов, так как не учитывает кернинг .
Николя Репике,
4

Я решил это, добавив путь привязки к элементу во внутреннем коде:

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

Я обнаружил, что это гораздо более чистое решение, чем добавление всех накладных расходов на приведенные выше ссылки, такие как FormattedText, в мой код.

После этого я смог сделать это:

double d_width = MyText.Width;
Повар Фараон
источник
2
Можно просто обойтись 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)
Isxaker
источник
-3

Нашел это для вас:

Graphics g = control.CreateGraphics();
int width =(int)g.MeasureString(aString, control.Font).Width; 
g.dispose();
Сетерон
источник
24
Этот конкретный подход применим только к WinForms.
HB,