Jak obliczyć szerokość WPF TextBlock dla jego znanego rozmiaru czcionki i znaków?


81

Powiedzmy, że mam TextBlocktekst „Some Text” i rozmiar czcionki 10.0 .

Jak mogę obliczyć odpowiednią TextBlock szerokość ?


Pytano o to wcześniej, również to zależy od czcionki.
— HB

Możesz również pobrać rzeczywistą szerokość z ActualWidth.
— HB

2
Używanie TextRenderer powinno działać również dla WPF: stackoverflow.com/questions/721168/…
— HB

Odpowiedzi:


156

Skorzystaj z FormattedTextklasy.

Zrobiłem funkcję pomocniczą w moim kodzie:

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

Zwraca piksele niezależne od urządzenia, których można używać w układzie WPF.


To było naprawdę pomocne
— Rakesh

1
Jak zrobić to samo w UWP
— Arun Prasad

6
@ArunPrasad To byłaby doskonała rzecz do zadania osobnego pytania. To pytanie jest wyraźnie ograniczone do WPF.
— RandomEngy,

Zwraca to szerokość równą zero, jeśli przekazany kandydat jest spacją. Myślę, że może to być związane z zawijaniem słów?
— Mark Miller

43

Dla przypomnienia ... Zakładam, że operator próbuje programowo określić szerokość, jaką textBlock zajmie po dodaniu do drzewa wizualnego. IMO lepszym rozwiązaniem niż formattedText (jak sobie radzić z czymś takim jak textWrapping?) Byłoby użycie opcji Measure and Arrange na przykładowym TextBlock. na przykład

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
Po kilku drobnych modyfikacjach zadziałało to dla mnie przy pisaniu aplikacji Sklepu Windows 8.1 (Windows Runtime). Nie zapomnij ustawić informacji o czcionce dla tego TextBlock, aby dokładnie obliczał szerokość. Zgrabne rozwiązanie i warte uznania.
— David Rector

1
Kluczową kwestią jest tutaj utworzenie tymczasowego bloku tekstu w miejscu. Wszystkie te manipulacje z istniejącym blokiem tekstowym na stronie nie działają: jego ActualWidth jest aktualizowana dopiero po ponownym narysowaniu.
— Tertium

13

Dostarczone rozwiązanie było odpowiednie dla .Net Framework 4.5, jednak ze skalowaniem Windows 10 DPI i Framework 4.6.x dodającym różne stopnie obsługi, konstruktor używany do pomiaru tekstu jest teraz oznaczony [Obsolete]wraz z wszystkimi konstruktorami tej metody, które to robią. nie obejmująpixelsPerDip parametru.

Niestety jest to trochę bardziej skomplikowane, ale zaowocuje większą dokładnością dzięki nowym możliwościom skalowania.

### PixelsPerDip

Według MSDN oznacza to:

Wartość niezależnych pikseli pikseli na gęstość, która jest odpowiednikiem współczynnika skali. Na przykład, jeśli rozdzielczość ekranu wynosi 120 (lub 1,25, ponieważ 120/96 = 1,25), rysowany jest 1,25 piksela na piksel niezależny od gęstości. DIP to jednostka miary używana przez WPF, która ma być niezależna od rozdzielczości urządzenia i DPI.

Oto moja implementacja wybranej odpowiedzi na podstawie wskazówek z repozytorium Microsoft / WPF-Samples GitHub ze świadomością skalowania DPI.

Do pełnej obsługi skalowania DPI od rocznicy Windows 10 (poniżej kodu) wymagana jest dodatkowa konfiguracja , której nie mogłem uruchomić, ale bez niej działa to na jednym monitorze ze skonfigurowanym skalowaniem (i uwzględnia zmiany skalowania). Dokument Worda w powyższym repozytorium jest źródłem tych informacji, ponieważ moja aplikacja nie uruchomiła się po dodaniu tych wartości. Ten przykładowy kod z tego samego repozytorium służył również jako dobry punkt odniesienia.

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

Inne wymagania

Zgodnie z dokumentacją w Microsoft / WPF-Samples, musisz dodać pewne ustawienia do manifestu aplikacji, aby objąć zdolność Windows 10 Anniversary do posiadania różnych ustawień DPI na ekran w konfiguracjach z wieloma monitorami. Można się domyślać, że bez tych ustawień zdarzenie OnDpiChanged może nie zostać wywołane, gdy okno jest przenoszone z jednego ekranu na inny z innymi ustawieniami, co spowodowałoby, że pomiary nadal opierałyby się na poprzednich DpiScale. Aplikacja, którą pisałem, była dla mnie, sama i nie mam takiej konfiguracji, więc nie miałem z czym testować, a kiedy zastosowałem się do wskazówek, skończyło się na aplikacji, która nie mogła się uruchomić z powodu manifestu błędy, więc zrezygnowałem, ale warto przyjrzeć się temu i dostosować manifest aplikacji, aby zawierał:

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

Zgodnie z dokumentacją:

Połączenie [tych] dwóch tagów ma następujący efekt: 1) Per-Monitor dla> = Rocznicowa aktualizacja systemu Windows 10 2) System <Rocznicowa aktualizacja systemu Windows 10


5

Znalazłem metody, które działają dobrze ...

/// <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
Sumowanie szerokości glifów nie będzie działać dla większości czcionek, ponieważ nie uwzględnia kerningu .
— Nicolas Repiquet

4

Rozwiązałem to, dodając ścieżkę powiązania do elementu w kodzie zaplecza:

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

Okazało się, że jest to o wiele czystsze rozwiązanie niż dodanie całego narzutu powyższych odwołań, takich jak FormattedText, do mojego kodu.

Potem udało mi się to zrobić:

double d_width = MyText.Width;

2
Możesz to zrobić po prostu d_width = MyText.ActualWidth;bez wiązania. Problem polega na tym, że TextBlocknie ma jeszcze drzewa wizualnego.
— xmedeko

0

Używam tego:

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)

-3

Znalazłem to dla Ciebie:

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

24
To specyficzne podejście dotyczy tylko WinForms.
— HB
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.