¿Cómo calcular el ancho de WPF TextBlock para su tamaño de fuente y caracteres conocidos?

81

Digamos que tengo TextBlockcon el texto "Some Text" y el tamaño de fuente 10.0 .

¿Cómo puedo calcular el TextBlock ancho apropiado ?

DmitryBoyko
fuente
Esto se ha preguntado antes, también depende de la fuente.
HB
También puede obtener el ancho real de ActualWidth.
HB
2
El uso de TextRenderer también debería funcionar para WPF: stackoverflow.com/questions/721168/…
HB

Respuestas:

156

Usa la FormattedTextclase.

Hice una función auxiliar en mi código:

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

Devuelve píxeles independientes del dispositivo que se pueden usar en el diseño de WPF.

RandomEngy
fuente
Esto fue realmente útil
Rakesh
1
Cómo hacer lo mismo en UWP
Arun Prasad
6
@ArunPrasad Eso sería excelente para hacer una pregunta separada. Esta pregunta está claramente dirigida a WPF.
RandomEngy
Esto devuelve un ancho de cero si el candidato pasado es un espacio. Creo que esto tiene que ver con el ajuste de palabras, ¿quizás?
Mark Miller
43

Para el registro ... supongo que el operador está tratando de determinar programáticamente el ancho que tomará el bloque de texto después de ser agregado al árbol visual. En mi opinión, una solución mejor que formattedText (¿cómo se maneja algo como textWrapping?) Sería usar Medir y Organizar en un TextBlock de muestra. p.ej

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
usuario1604008
fuente
1
Con algunas modificaciones menores, esto funcionó para mí al escribir una aplicación de la Tienda Windows 8.1 (Windows Runtime). No olvide configurar la información de fuente para este TextBlock para que calcule el ancho con precisión. Buena solución y vale la pena un voto a favor.
David Rector
1
El punto clave aquí es crear este bloque de texto temporal en su lugar. Todas estas manipulaciones con el bloque de texto existente en una página no funcionan: su ActualWidth se actualiza solo después de volver a dibujar.
Tercio
13

La solución proporcionada era apropiada para .Net Framework 4.5, sin embargo, con el escalado de DPI de Windows 10 y Framework 4.6.x agregando diversos grados de compatibilidad, el constructor utilizado para medir el texto ahora está marcado [Obsolete], junto con cualquier constructor de ese método que lo haga. no incluye el pixelsPerDipparámetro.

Desafortunadamente, es un poco más complicado, pero dará como resultado una mayor precisión con las nuevas capacidades de escalado.

### PixelsPerDip

Según MSDN, esto representa:

El valor de píxeles independientes por densidad de píxeles, que es el equivalente al factor de escala. Por ejemplo, si el DPI de una pantalla es 120 (o 1,25 porque 120/96 = 1,25), se dibuja 1,25 píxeles por píxel independiente de la densidad. DIP es la unidad de medida utilizada por WPF para ser independiente de la resolución del dispositivo y de los DPI.

Aquí está mi implementación de la respuesta seleccionada basada en la guía de Microsoft / WPF-Samples repositorio de GitHub de con reconocimiento de escala de DPI.

Se requiere una configuración adicional para admitir completamente el escalado de DPI a partir de Windows 10 Anniversary (debajo del código), que no pude hacer funcionar, pero sin él, esto funciona en un solo monitor con el escalado configurado (y respeta los cambios de escala). El documento de Word en el repositorio anterior es la fuente de esa información ya que mi aplicación no se iniciaría una vez que agregué esos valores. Este código de muestra del mismo repositorio también sirvió como un buen punto de referencia.

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

Otros requerimientos

De acuerdo con la documentación en Microsoft / WPF-Samples, debe agregar algunas configuraciones al manifiesto de la aplicación para cubrir la capacidad de Windows 10 Anniversary de tener diferentes configuraciones de DPI por pantalla en configuraciones de múltiples monitores. Es una suposición justa que sin estas configuraciones, el evento OnDpiChanged podría no aparecer cuando una ventana se mueve de una pantalla a otra con configuraciones diferentes, lo que haría que sus medidas siguieran dependiendo de las anteriores DpiScale. La aplicación que estaba escribiendo era para mí, solo, y no tengo ese tipo de configuración, así que no tenía nada con qué probar y cuando seguí las instrucciones, terminé con una aplicación que no se iniciaba debido al manifiesto. errores, así que me di por vencido, pero sería una buena idea revisarlo y ajustar el manifiesto de la aplicación para que contenga:

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

Según la documentación:

La combinación de [estas] dos etiquetas tiene el siguiente efecto: 1) Por monitor para> = Actualización de aniversario de Windows 10 2) Sistema <Actualización de aniversario de Windows 10

mdip
fuente
5

Encontré algunos métodos que funcionan bien ...

/// <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
fuente
3
Sumar anchos de glifos no funcionará para la mayoría de las fuentes, ya que no tiene en cuenta el interletraje .
Nicolas Repiquet
4

Resolví esto agregando una ruta de enlace al elemento en el código de backend:

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

Encontré que esta es una solución mucho más limpia que agregar toda la sobrecarga de las referencias anteriores como FormattedText a mi código.

Después, pude hacer esto:

double d_width = MyText.Width;
Chef Faraón
fuente
2
Puede hacerlo simplemente d_width = MyText.ActualWidth;sin atar. El problema es cuando aún TextBlockno está en el árbol visual.
xmedeko
0

Yo uso este:

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

Encontré esto para ti:

Graphics g = control.CreateGraphics();
int width =(int)g.MeasureString(aString, control.Font).Width; 
g.dispose();
Setheron
fuente
24
Este enfoque específico solo se aplica a WinForms.
HB