Search code examples
wpftexttextblocktexttrimming

Get Displayed Text from TextBlock


I have a simple TextBlock defined like this

<StackPanel>
    <Border Width="106"
            Height="25"
            Margin="6"
            BorderBrush="Black"
            BorderThickness="1"
            HorizontalAlignment="Left">
        <TextBlock Name="myTextBlock"
                   TextTrimming="CharacterEllipsis"
                   Text="TextBlock: Displayed text"/>
    </Border>
</StackPanel>

Which outputs like this

alt text

This will get me "TextBlock: Displayed text"

string text = myTextBlock.Text;

But is there a way to get the text that's actually displayed on the screen?
Meaning "TextBlock: Display..."

Thanks


Solution

  • You can do this by first retrieving the Drawing object that represents the appearance of the TextBlock in the visual tree, and then walk that looking for GlyphRunDrawing items - those will contain the actual rendered text on the screen. Here's a very rough and ready implementation:

    private void button1_Click(object sender, RoutedEventArgs e)
    {
        Drawing textBlockDrawing = VisualTreeHelper.GetDrawing(myTextBlock);
        var sb = new StringBuilder();
        WalkDrawingForText(sb, textBlockDrawing);
    
        Debug.WriteLine(sb.ToString());
    }
    
    private static void WalkDrawingForText(StringBuilder sb, Drawing d)
    {
        var glyphs = d as GlyphRunDrawing;
        if (glyphs != null)
        {
            sb.Append(glyphs.GlyphRun.Characters.ToArray());
        }
        else
        {
            var g = d as DrawingGroup;
            if (g != null)
            {
                foreach (Drawing child in g.Children)
                {
                    WalkDrawingForText(sb, child);
                }
            }
        }
    }
    

    This is a direct excerpt from a little test harness I just wrote - the first method's a button click handler just for ease of experimentation.

    It uses the VisualTreeHelper to get the rendered Drawing for the TextBlock - that'll only work if the thing has already been rendered by the way. And then the WalkDrawingForText method does the actual work - it just traverses the Drawing tree looking for text.

    This isn't terribly smart - it assumes that the GlyphRunDrawing objects appear in the order you'll want them. For your particular example it does - we get one GlyphRunDrawing containing the truncated text, followed by a second one containing the ellipsis character. (And by the way, it's just one unicode character - codepoint 2026, and if this editor lets me paste in unicode characters, it's "…". It's not three separate periods.)

    If you wanted to make this more robust, you would need to work out the positions of all those GlyphRunDrawing objects, and sort them, in order to process them in the order in which they appear, rather than merely hoping that WPF happens to produce them in that order.

    Updated to add:

    Here's a sketch of how a position-aware example might look. Although this is somewhat parochial - it assumes left-to-right reading text. You'd need something more complex for an internationalized solution.

    private string GetTextFromVisual(Visual v)
    {
        Drawing textBlockDrawing = VisualTreeHelper.GetDrawing(v);
        var glyphs = new List<PositionedGlyphs>();
    
        WalkDrawingForGlyphRuns(glyphs, Transform.Identity, textBlockDrawing);
    
        // Round vertical position, to provide some tolerance for rounding errors
        // in position calculation. Not totally robust - would be better to
        // identify lines, but that would complicate the example...
        var glyphsOrderedByPosition = from glyph in glyphs
                                        let roundedBaselineY = Math.Round(glyph.Position.Y, 1)
                                        orderby roundedBaselineY ascending, glyph.Position.X ascending
                                        select new string(glyph.Glyphs.GlyphRun.Characters.ToArray());
    
        return string.Concat(glyphsOrderedByPosition);
    }
    
    [DebuggerDisplay("{Position}")]
    public struct PositionedGlyphs
    {
        public PositionedGlyphs(Point position, GlyphRunDrawing grd)
        {
            this.Position = position;
            this.Glyphs = grd;
        }
        public readonly Point Position;
        public readonly GlyphRunDrawing Glyphs;
    }
    
    private static void WalkDrawingForGlyphRuns(List<PositionedGlyphs> glyphList, Transform tx, Drawing d)
    {
        var glyphs = d as GlyphRunDrawing;
        if (glyphs != null)
        {
            var textOrigin = glyphs.GlyphRun.BaselineOrigin;
            Point glyphPosition = tx.Transform(textOrigin);
            glyphList.Add(new PositionedGlyphs(glyphPosition, glyphs));
        }
        else
        {
            var g = d as DrawingGroup;
            if (g != null)
            {
                // Drawing groups are allowed to transform their children, so we need to
                // keep a running accumulated transform for where we are in the tree.
                Matrix current = tx.Value;
                if (g.Transform != null)
                {
                    // Note, Matrix is a struct, so this modifies our local copy without
                    // affecting the one in the 'tx' Transforms.
                    current.Append(g.Transform.Value);
                }
                var accumulatedTransform = new MatrixTransform(current);
                foreach (Drawing child in g.Children)
                {
                    WalkDrawingForGlyphRuns(glyphList, accumulatedTransform, child);
                }
            }
        }
    }