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ść ?

DmitryBoyko
źródło
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.

RandomEngy
źródło
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
user1604008
źródło
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

mdip
źródło
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);
}
DmitryBoyko
źródło
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;
Szef kuchni Faraon
źródło
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)
isxaker
źródło
-3

Znalazłem to dla Ciebie:

Graphics g = control.CreateGraphics();
int width =(int)g.MeasureString(aString, control.Font).Width; 
g.dispose();
Setheron
źródło
24
To specyficzne podejście dotyczy tylko WinForms.
HB