Reputation: 84657
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
Upvotes: 25
Views: 14284
Reputation: 19937
The accepted answer is indeed the right solution. Now, if you want to check if a given TextBlock
has overflowed, here's an extension method that does exactly that.
public static class TextBlockExtensions
{
public static bool HasOverflowed(this TextBlock textBlock) =>
GetGlyphRuns(textBlock).Any(IsEllipsisGlyphRun);
private static bool IsEllipsisGlyphRun(GlyphRun run) =>
run.Characters.Count == 1 && run.Characters[0] == '\u2026';
private static IEnumerable<GlyphRun> GetGlyphRuns(Visual visual) =>
GetGlyphRuns(VisualTreeHelper.GetDrawing(visual));
private static IEnumerable<GlyphRun> GetGlyphRuns(Drawing drawing)
{
if (drawing is GlyphRunDrawing glyphRunDrawing)
{
yield return glyphRunDrawing.GlyphRun;
}
else if (drawing is DrawingGroup drawingGroup)
{
foreach (Drawing child in drawingGroup.Children)
{
foreach (var glyphRun in GetGlyphRuns(child))
{
yield return glyphRun;
}
}
}
}
}
Upvotes: 1
Reputation: 9
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
Upvotes: 0
Reputation: 14547
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);
}
}
}
}
Upvotes: 19
Reputation: 1705
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
Upvotes: 1
Reputation: 16899
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.
Upvotes: 10
Reputation: 935
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.
Upvotes: 1