24

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

Fredrik Hedblad
  • 83,499
  • 23
  • 264
  • 266

5 Answers5

19

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);
            }
        }
    }
}
Ian Griffiths
  • 14,302
  • 2
  • 64
  • 88
  • You're welcome! I just added an illustration of how you could go about the more complex approach that takes the position of each GlyphRunDrawing into account, rather than simply hoping that they come out in the right order. – Ian Griffiths Dec 06 '10 at 10:08
  • Nice. Might want to add `sb.AppendLine()` to get newlines instead of one long text. – stijn Mar 31 '23 at 11:54
10

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.

Stewbob
  • 16,759
  • 9
  • 63
  • 107
  • 1
    I really tried to get the value but you weren't kidding..:) TextBlock has a field TextBlockCache called _textBlockCache, from this a Line is created. This Line is formatted with loads of internal fields and properties from the TextBlock. The Line then has a TextLine that's created from both the TextBlock and the Line and finally Collapse is called on the TextLine and then we get the TextCollapsedRange. Tried simulating all this with reflection but I get an exception in Format.. – Fredrik Hedblad Dec 05 '10 at 19:02
1

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.

Moonshield
  • 925
  • 9
  • 16
  • Thanks for your answer. Yes, that's one way to do it. I was hoping more of something like a Reflection solution – Fredrik Hedblad Dec 01 '10 at 07:54
  • As far as I can tell, the content of a textbox is rendered to a TextBoxView (which is as wide as it needs to be) and placed inside a ScrollViewer that then provides the clipping. Not sure what reflection has to offer, but good luck. – Moonshield Dec 01 '10 at 09:23
  • Yes, for a TextBox that's true. I'm asking about a TextBlock with TextTrimming set to CharacterEllipsis. As for as I can tell, the only thing in the VisualTree is the TextBlock itself. I'm not sure what reflection has to offer either but it was just one thing that came to mind :) – Fredrik Hedblad Dec 01 '10 at 09:36
  • Oops, sorry, keep getting those two muddled up. I'll be interested to see what you come up with. – Moonshield Dec 01 '10 at 09:53
1

If you need the text for an effect - might it then be enough with the image of the rendered text? If so you could use a VisualBrush or System.Windows.Media.Imaging.RenderTargetBitmap

Rune Andersen
  • 1,650
  • 12
  • 15
  • Thanks for your answer! Yes maybe, in worst case. I'm also looking into saving the TextBlock to an Image and then translate it back to Text – Fredrik Hedblad Dec 06 '10 at 08:52
0

Also I reproduced on .Net framework with the following xaml:

<Window x:Class="TestC1Grid.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"   
        TextOptions.TextFormattingMode="Display"    
        TextOptions.TextRenderingMode="Auto"                                
        ResizeMode="CanResizeWithGrip"               
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"></ColumnDefinition>                
                <ColumnDefinition Width="Auto"></ColumnDefinition>
                <ColumnDefinition Width="*"></ColumnDefinition>
            </Grid.ColumnDefinitions>
            <TextBlock TextTrimming="CharacterEllipsis"                       
                       FontFamily="Tahoma"
                       FontSize="12"
                       HorizontalAlignment="Stretch"
                       TextAlignment="Left" xml:lang="nl-nl">My-Text</TextBlock>
            <TextBlock Grid.Column="1" TextTrimming="CharacterEllipsis"                       
                       FontFamily="Tahoma"
                       FontSize="12"
                       IsHyphenationEnabled="True">My-Text</TextBlock>
            <TextBlock Grid.Column="2" TextTrimming="CharacterEllipsis"                       
                       FontFamily="Tahoma"
                       FontSize="12"
                       IsHyphenationEnabled="True">My-Text</TextBlock>
        </Grid>
    </Grid>
</Window>

if you remove TextOptions.TextFormattingMode="Display"
TextOptions.TextRenderingMode="Auto"

or remove xml:lang="nl-nl" is working ok