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
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
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);
}
}
}
}
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With