Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 990
Fredrik Hedblad Avatar asked Nov 30 '10 21:11

Fredrik Hedblad


3 Answers

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);
            }
        }
    }
}
like image 153
Ian Griffiths Avatar answered Oct 16 '22 11:10

Ian Griffiths


After rooting around I Reflector for a while, I found the following:

System.Windows.Media.TextFormatting.TextCollapsedRange 

which has a Length property that contains the number of characters that are NOT displayed (are in the collapsed/hidden portion of the text line). Knowing that value, it's just a matter of subtraction to get the characters that ARE displayed.

This property is not directly accessible from the TextBlock object. It looks like it is part of the code that is used by WPF to actually paint the text on the screen.

It could end up being quite a lot of fooling around to actually get the value of this property for the text line in your TextBlock.

like image 36
Stewbob Avatar answered Oct 16 '22 10:10

Stewbob


Well, it's a bit of a specific request so I'm not sure there's a ready made function in the framework to do it. What I would do is to calculate the logical width of each character, divide the ActualWidth of the TextBlock by this value and there you have the number of characters from the start of the string that are visible. That is of course assuming that clipping will only occur from the right.

like image 28
Moonshield Avatar answered Oct 16 '22 12:10

Moonshield