Get Displayed Text from TextBlock - wpf

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 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

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

Related

Dynamically modifying a Path, Geometry, Shape in XAML

I am stuck. I want to do some sophisticated animations in XAML, in which the geometry of the image gets modified at runtime. I want to start out simple, and then make something far more interesting, but nothing seems to work.
For now, all I want to do is draw an arc on the screen using a for-loop, and this arc will be modified by a slider. The slider will control the starting point of the arc. So, if I have the slider at Pi/4, it will draw an arc from Pi/4 to 2Pi.
I've tried so many different ways. Right now I've created a class of the type Shape, and tried to modify the DefiningGeometry which is a property of the Shape class. The startRadians gets modified by the slider, so that part works OK, I got the binding to work. But, after startRadians gets changed (it is a DependencyProperty, btw) I want the class to re-calculate the geometry of the circle. (Like a cherry pie that is missing a bigger piece as startRadians gets changed.) The real problem is that DefiningGeometry is a read-only property, so I can't change it on the fly. (Am I right about this?) Even if I could, I don't know the way to write the line of code so that DrawMyArc fires again, and the results get reloaded into DefiningGeometry.
OK, so I need some guidance. Should I change the parent class, so that I have an easily modifiable Path/geometry? I am at a loss here.
Should I use an entirely different approach, like where you dynamically make/delete the geometry using StreamGeometry?
Here's the relevant code:
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;
namespace January14noon
{
public class myCircle : Shape
{
public double startRadians
{
get { return (double)GetValue(startRadiansProperty); }
set { SetValue(startRadiansProperty, value); }
}
protected override Geometry DefiningGeometry
{
get
{
return DrawMyArc(100, 200, true, 40, 40, 360, startRadians, 2 * Math.PI);
}
}
// Using a DependencyProperty as the backing store for startRadians. This enables animation, styling, binding, etc...
public static readonly DependencyProperty startRadiansProperty =
DependencyProperty.Register("startRadians", typeof(double), typeof(myCircle), new PropertyMetadata(Math.PI / 4, new PropertyChangedCallback(startRadians_PropertyChanged)));
private static void startRadians_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//
//
}
public PathGeometry DrawMyArc(double centX, double centY, bool CW, double radiusX, double radiusY, int numberOfSegs, double startRad, double endRad)
{
double[,] rawPoints = new double[numberOfSegs, 2];
List<LineSegment> segments = new List<LineSegment>();
double arcLength;
arcLength = endRad - startRad;
for (int i = 0; i < numberOfSegs; i++)
{
rawPoints[i, 0] = radiusX * Math.Sin(i * (arcLength / numberOfSegs) + startRad) + centX;
rawPoints[i, 1] = radiusY * -Math.Cos(i * (arcLength / numberOfSegs) + startRad) + centY;
segments.Add(new LineSegment(new Point(rawPoints[i, 0], rawPoints[i, 1]), true));
}
LineSegment[] segArray = segments.ToArray();
PathFigure figure = new PathFigure(new Point(centX, centY), segments, false);
PathGeometry myGeometry = new PathGeometry();
myGeometry.Figures.Add(figure);
return myGeometry;
}
}
}
And XAML:
<Window x:Class="January14noon.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:January14noon"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:myCircle x:Key="myCircleDataSource" d:IsDataSource="True"/>
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource myCircleDataSource}}">
<Slider x:Name="slider" VerticalAlignment="Top" Margin="0,0,163.898,0" Value="{Binding startRadians, Mode=OneWayToSource}"/>
<TextBox x:Name="myTextBox" HorizontalAlignment="Right" Height="23" TextWrapping="Wrap" Text="{Binding startRadians}" VerticalAlignment="Top" Width="147.797"/>
<!--<local:myCircle x:Name="instanceOfCircle" />-->
<local:myCircle Stroke="Black" StrokeThickness="2"/>
</Grid>
</Window>
Any help would be appreciated. Just general approach, something specific even words of encouragement.
TYIA
Any dependency property that affects visual appearance of a control might be registered with appropriate FrameworkPropertyMetadataOptions, e.g. AffectsMeasure, AffectsArrange or AffectsRender.
Note also that class, property and method names in C# are supposed to use Pascal Casing, i.e. start with an uppercase letter
public static readonly DependencyProperty StartRadiansProperty =
DependencyProperty.Register(nameof(StartRadians), typeof(double), typeof(MyCircle),
new FrameworkPropertyMetadata(Math.PI / 4,
FrameworkPropertyMetadataOptions.AffectsMeasure, StartRadiansPropertyChanged));
public double StartRadians
{
get { return (double)GetValue(StartRadiansProperty); }
set { SetValue(StartRadiansProperty, value); }
}
private static void StartRadiansPropertyChanged(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
...
}
Make sure you bind the visible circle to the data source:
<Window.Resources>
<local:myCircle x:Key="myCircleDataSource" d:IsDataSource="True"/>
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource myCircleDataSource}}">
<Slider x:Name="slider" VerticalAlignment="Top" Margin="0,0,163.898,0" Value="{Binding startRadians, Mode=OneWayToSource}"/>
<TextBox x:Name="myTextBox" HorizontalAlignment="Right" Height="23" TextWrapping="Wrap" Text="{Binding startRadians}" VerticalAlignment="Top" Width="147.797"/>
<!--<local:myCircle x:Name="instanceOfCircle" />-->
<local:myCircle Stroke="Black" StrokeThickness="2" startRadians="{Binding startRadians}"/>
</Grid>
and invalidate the visual when the property changes:
private static void startRadians_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var circle = (myCircle)d;
circle.InvalidateVisual(); // <-- Invalidate!
}
Invalidating will tell engine to re-render this visual, which will call your DrawMyArc() method

How to exactly center rendered text inside a Border

I have a Border with a Content of TextBlock that I want to be perfectly centered both horizontally and vertically. No matter what I try it never looks centered. What am I missing?
Using the code below the top of the text is 19px below the border, the bottom of the text is 5px above the border. It's also off center left or right depending on the Text value which I assume is related to the font.
The solution should work for varying text (1-31) with any font.
Code
<Grid Width="50" Height="50">
<Border BorderThickness="1" BorderBrush="Black">
<TextBlock Text="13" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="50"/>
</Border>
</Grid>
Result
Well then, challenge accepted ;-) This solution is based on the following idea:
Fit the TextBlock inside the border and make sure the entire text is rendered, even if not visible.
Render the text into a bitmap.
Detect the glyphs (i.e. characters) inside the bitmap to get the pixel-exact position.
Update the UI layout so the text is centered inside the border.
If possible, allow simple, generic usage.
1. TextBlock inside border / fully rendered
This is simple once you realize that the entire content of a ScrollViewer is rendered, so here is my UserControl XAML:
<UserControl x:Class="WpfApplication4.CenteredText"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<ScrollViewer x:Name="scroll"
IsHitTestVisible="False"
VerticalScrollBarVisibility="Hidden"
HorizontalScrollBarVisibility="Hidden" />
</Grid>
</UserControl>
With the code behind as:
public partial class CenteredText : UserControl
{
public CenteredText()
{
InitializeComponent();
}
public static readonly DependencyProperty ElementProperty = DependencyProperty
.Register("Element", typeof(FrameworkElement), typeof(CenteredText),
new PropertyMetadata(OnElementChanged));
private static void OnElementChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var elem = e.NewValue as FrameworkElement;
var ct = d as CenteredText;
if(elem != null)
{
elem.Loaded += ct.Content_Loaded;
ct.scroll.Content = elem;
}
}
public FrameworkElement Element
{
get { return (FrameworkElement)GetValue(ElementProperty); }
set { SetValue(ElementProperty, value); }
}
void Content_Loaded(object sender, RoutedEventArgs e) /*...*/
}
This control is basically a ContentControlwhich allows to handle the Loaded event of the content generically. There may be a simpler way to do this, I'm not sure.
2. Render to Bitmap
This one is simple. In the Content_Loaded() method:
void Content_Loaded(object sender, RoutedEventArgs e)
{
FrameworkElement elem = sender as FrameworkElement;
int w = (int)elem.ActualWidth;
int h = (int)elem.ActualHeight;
var rtb = new RenderTargetBitmap(w, h, 96, 96, PixelFormats.Pbgra32);
rtb.Render(elem);
/* glyph detection ... */
}
3. Detect the glyphs
This is surprisingly easy since a TextBlock is rendered with fully transparent background by default and we are only interested in bounding rectangle. This is done in a separate method:
bool TryFindGlyphs(BitmapSource src, out Rect rc)
{
int left = int.MaxValue;
int toRight = -1;
int top = int.MaxValue;
int toBottom = -1;
int w = src.PixelWidth;
int h = src.PixelHeight;
uint[] buf = new uint[w * h];
src.CopyPixels(buf, w * sizeof(uint), 0);
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
// background is assumed to be fully transparent, i.e. 0x00000000 in Pbgra
if (buf[x + y * w] != 0)
{
if (x < left) left = x;
if (x > toRight) toRight = x;
if (y < top) top = y;
if (y > toBottom) toBottom = y;
}
}
}
rc = new Rect(left, top, toRight - left, toBottom - top);
return (toRight > left) && (toBottom > top);
}
The above method tries to find the leftmost, rightmost, topmost and bottommost pixel which is not transparent and returns the results as a Rect in the output parameter.
4. Update Layout
This is done later in the Content_Loaded method:
void Content_Loaded(object sender, RoutedEventArgs e)
{
/* render to bitmap ... */
Rect rc;
if (TryFindGlyphs(rtb, out rc))
{
if (rc.Height > this.scroll.ActualHeight || rc.Width > this.scroll.ActualWidth)
{
return; // todo: error handling
}
double desiredV = rc.Top - 0.5 * (this.scroll.ActualHeight - rc.Height);
double desiredH = rc.Left - 0.5 * (this.scroll.ActualWidth - rc.Width);
if (desiredV > 0)
{
this.scroll.ScrollToVerticalOffset(desiredV);
}
else
{
elem.Margin = new Thickness(elem.Margin.Left, elem.Margin.Top - desiredV,
elem.Margin.Right, elem.Margin.Bottom);
}
if (desiredH > 0)
{
this.scroll.ScrollToHorizontalOffset(desiredH);
}
else
{
elem.Margin = new Thickness(elem.Margin.Left - desiredH, elem.Margin.Top,
elem.Margin.Right, elem.Margin.Bottom);
}
}
}
This UI is updated using the following strategy:
Compute the desired offset between the border and the glyph rectangle in both directions
If the desired offset is positive, it means that the text needs to move up (or left in the horizontal case) so we can scroll down (right) by the desired offset.
If the desired offset is negative, it means that the text needs to move down (or right in the horizontal case). This cannot be done by scrolling since the TextBlock is top-left-aligned (by default) and the ScrollViewer is still at the initial (top/left) position. There is a simple solution though: Add the desired offset to the Margin of the TextBlock.
5. Simple Usage
The CenteredText control is used as follows:
<Border BorderBrush="Black" BorderThickness="1" Width="150" Height="150">
<local:CenteredText>
<local:CenteredText.Element>
<TextBlock Text="31" FontSize="150" />
</local:CenteredText.Element>
</local:CenteredText>
</Border>
Results
For border size 150x150 and FontSize 150:
For border size 150x150 and FontSize 50:
For border size 50x50 and FontSize 50:
Note: There is a 1-pixel error where the space to the left of the text is 1 pixel thicker or thinner than the space to the right. Same with the top / bottom spacing. This happens if the border has an even width and the rendered text an odd width (no sub-pixel perfectness is provided, sorry)
Conclusion
The presented solution should work up to a 1-pixel error with any Font, FontSize and Text and is simple to use.
And if you haven't noticed yet, very limited assumptions were made about the FrameworkElement which is used with the Elem property of the CenteredText control. So this should also work with any element which has transparent background and needs (near-)perfect centering.
What you are talking about is related to the specific font (and characters within that font) that you are using. Different fonts will have different baselines, heights and other attributes. In order to combat that, just use Padding on the Border or Margin on the TextBlock to make it fit where you want it:
<Grid Width="50" Height="50">
<Border BorderThickness="1" BorderBrush="Black">
<TextBlock Text="13" VerticalAlignment="Center" HorizontalAlignment="Center"
FontSize="50" Margin="0,0,3,14" />
</Border>
</Grid>
Note: You can also use the TextBlock.TextAlignment Property to make adjustments to the horizontal alignment of text content.
I'd add this as a comment but I haven't got enough reputation :P
It looks off center because the height and width you have specified for the grid (50x50) is too small to house a font size of 50. Either increase the size to 100x100 or lower the font size to something smaller.
To demonstrate that they will be perfectly aligned in the center by doing this - view this code in visual studio somewhere. You will see the numbers of these textblocks overlap perfectly.
<Grid Height="100" Width="100">
<Border BorderThickness="1" BorderBrush="Black" >
<TextBlock Text="13" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="50"/>
</Border>
<Border BorderThickness="1" BorderBrush="Black" >
<TextBlock Text="31" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="50"/>
</Border>
</Grid>
I hope this helps you out :)

scrollable wrap with overflow

I need to display a block of text in a resizable column. The text should wrap with overflow but, for a given column size, the user should be able to scroll horizontally to view the overflown text.
I do not believe this can be achieved w/ out of the box controls; if I'm wrong about that please tell me. I have attempted to achieve this with a custom control:
public class Sizer : ContentPresenter
{
static Sizer()
{
ContentPresenter.ContentProperty.OverrideMetadata(typeof(Sizer), new FrameworkPropertyMetadata(ContentChanged)); ;
}
public Sizer() : base() {}
protected override Size MeasureOverride(Size constraint)
{
var childWidth = Content==null ? 0.0 : ((FrameworkElement)Content).RenderSize.Width;
var newWidth = Math.Max(RenderSize.Width, childWidth);
return base.MeasureOverride(new Size(newWidth, constraint.Height));
}
private static void ContentChanged(DependencyObject dep, DependencyPropertyChangedEventArgs args)
{
var #this = dep as Sizer;
var newV = args.NewValue as FrameworkElement;
var oldV = args.OldValue as FrameworkElement;
if (oldV != null)
oldV.SizeChanged -= #this.childSizeChanged;
if(newV!=null)
newV.SizeChanged += #this.childSizeChanged;
}
private void childSizeChanged(object sender, SizeChangedEventArgs e)
{
this.InvalidateMeasure();
}
}
...and I can test it in a simple WPF application like so:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Auto" VerticalAlignment="Top">
<local:Sizer>
<local:Sizer.Content>
<TextBlock Background="Coral" Text="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaa"
TextWrapping="WrapWithOverflow" />
</local:Sizer.Content>
</local:Sizer>
</ScrollViewer>
<GridSplitter Grid.Column="1" Background="Black" VerticalAlignment="Stretch" HorizontalAlignment="Left" Width="5" />
</Grid>
This works, after a fashion. It wraps and displays the text correctly, and the horizontal scrollbar lets me view the overflown text. If I resize the column to the right (larger) the text re-wraps correctly. However, if I resize the column to the left (smaller) the text will not re-wrap. So, for instance, if I resize the column to the right so that all the text is on one line it will remain all on one line regardless of any subsequent re-sizing. This is an unacceptable bug.
I have tinkered w/ this code a great deal although I haven't had what you'd a call a good strategy for finding a solution. I do not know and have not been able to discover any mechanism for forcing a textblock to re-wrap its contents. Any advice?
I was able to get this working (w/ some limitations) by adding the following to the custom control:
//stores first ScrollViewer ancestor
private ScrollViewer parentSV = null;
//stores collection of observed valid widths
private SortedSet<double> wrapPoints = new SortedSet<double>();
protected override void OnVisualParentChanged(DependencyObject oldParent)
{
parentSV = this.FindParent<ScrollViewer>();
base.OnVisualParentChanged(oldParent);
}
... and editing MeasureOverride list this:
protected override Size MeasureOverride(Size constraint)
{
if (parentSV != null)
{
var childWidth = Content == null ? 0.0 : ((FrameworkElement)Content).RenderSize.Width;
var viewportWidth = parentSV.ViewportWidth;
if (childWidth > viewportWidth + 5)
wrapPoints.Add(childWidth);
var pt = wrapPoints.FirstOrDefault(d => d > viewportWidth);
if (pt < childWidth)
childWidth = pt;
return base.MeasureOverride(new Size(childWidth, constraint.Height));
}
else
return base.MeasureOverride(constraint);
}
I do not like this at all. It doesn't work for WrapPanels (although that isn't a required use case for me), it flickers occasionally and I'm concerned that the wrapPoints collection may grow very large. But it does what I need it to do.

How can I prevent scrollbars from losing their state when the datacontext changes?

If I have a datatemplate that will end up using a scrollbar in one form or another, how can I retain the scrollbars position when the datacontext changes back and forth?
I've read that you generally store the state of the UI in the ViewModel. This makes sense for things like selected item, or is expanded type states, but Im not sure how to apply it to the scrollbar state.
Ive posted an example of what I mean below. When you select a tab and move the scrollbar, then select another tab and move its scrollbar. Upon returning to the first tab, the scrollbar has moved.
I believe this is due to the View being shared, and so it retains the percentage scrolled from the previous view?
<Window x:Class="WpfApplication13.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<TabControl ItemsSource="{Binding Data}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Count}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ListBox ItemsSource="{Binding}"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Window>
namespace WpfApplication13
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Data = new List<string[]>();
for (int i = 1; i < 10; ++i)
{
string[] strings = new string[i * 10];
for (int j = 0; j < i * 10; ++j)
{
strings[j] = new string('z', 100);
}
Data.Add(strings);
}
DataContext = this;
}
public List<string[]> Data
{
get;
set;
}
}
}
Edit : I've noticed some controls have a "Bring into view" option, which I guess could be used to scroll back, but then that would require something to actually be selected, which isn't always going to be the case

ViewBox makes RichTextBox lose its caret

RichTextBox is placed inside a ViewBox and zoomed to various levels 10 - 1000%. At percentages less than 100%, caret disappears at random cursor locations.
I understand that when a visual is zoomed out (compressed), it will loose pixels. Is there any way that I can stop loosing my cursor?
<Viewbox>
<RichTextBox Name="richTextBox1" Width="400" Height="400" />
</Viewbox>
FINAL EDIT:
hey there, just wanted to say, you can even get this working without reflection at all!! This is not optimized code, I'll leave that for yourself. Also this is still relying on internal stuff. So here it comes:
Codebehind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
rtb.LayoutUpdated += (sender, args) =>
{
var child = VisualTreeHelper.GetChild(vb, 0) as ContainerVisual;
var scale = child.Transform as ScaleTransform;
rtb.ScaleX = scale.ScaleX;
};
}
}
public class RTBwithVisibleCaret:RichTextBox
{
private UIElement _flowDocumentView;
private AdornerLayer _adornerLayer;
private UIElement _caretSubElement;
private ScaleTransform _scaleTransform;
public RTBwithVisibleCaret()
{
LayoutUpdated += (sender, args) =>
{
if (!IsKeyboardFocused) return;
if(_adornerLayer == null)
_adornerLayer = AdornerLayer.GetAdornerLayer(_flowDocumentView);
if (_adornerLayer == null || _flowDocumentView == null) return;
if(_scaleTransform != null && _caretSubElement!= null)
{
_scaleTransform.ScaleX = 1/ScaleX;
_adornerLayer.Update(_flowDocumentView);
}
else
{
var adorners = _adornerLayer.GetAdorners(_flowDocumentView);
if(adorners == null || adorners.Length<1) return;
var caret = adorners[0];
_caretSubElement = (UIElement) VisualTreeHelper.GetChild(caret, 0);
if(!(_caretSubElement.RenderTransform is ScaleTransform))
{
_scaleTransform = new ScaleTransform(1 / ScaleX, 1);
_caretSubElement.RenderTransform = _scaleTransform;
}
}
};
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var cthost = GetTemplateChild("PART_ContentHost") as FrameworkElement;
_flowDocumentView = cthost is ScrollViewer ? (UIElement)((ScrollViewer)cthost).Content : ((Decorator)cthost).Child;
}
public double ScaleX
{
get { return (double)GetValue(ScaleXProperty); }
set { SetValue(ScaleXProperty, value); }
}
public static readonly DependencyProperty ScaleXProperty =
DependencyProperty.Register("ScaleX", typeof(double), typeof(RTBwithVisibleCaret), new UIPropertyMetadata(1.0));
}
working with this XAML:
<Window x:Class="RTBinViewBoxTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:RTBinViewBoxTest="clr-namespace:RTBinViewBoxTest" Title="MainWindow" Height="350" Width="525">
<Viewbox Height="100" x:Name="vb">
<RTBinViewBoxTest:RTBwithVisibleCaret Width="70" x:Name="rtb">
<FlowDocument>
<Paragraph>
<Run>long long long long long long long long long long long long long long long long long long long long long long long long text</Run>
</Paragraph>
</FlowDocument>
</RTBinViewBoxTest:RTBwithVisibleCaret>
</Viewbox>
</Window>
yeah, it got me thinking when I saw, that all these are accessible through the visual tree! Instead of inheriting from RichTextBox (which was needed to get the TemplateChild) you can also traverse the VisualTree to get to that FlowDocumentView!
original post:
ok, let's look at what your options are:
as stated in my comment above: the easiest way to accomplish this whould be to have RichTextBox's content zoom instead of the RichTextBox being inside a ViewBox. You haven't answered (yet) if this would be an option.
now everything else will get complex and is more or less problematic:
you can use Moq or something similar (think Moles or so...) to replace the getter of SystemParameters.CaretWidth to accommodate for the ScaleTransform the ViewBox exerts. This has several problems! First: these Libraries are designed for use in testing scenarios and not recommended for production use. Second: you would have to set the value before the RichTextBox instantiates the Caret. That'd be tough though, as you don't know beforehand how the ViewBox scales the RichTextBox. So, this is not a good option!
the second (bad) option would be to use Reflection to get to this nice little Class System.Windows.Documents.CaretElement. You can get there through RichTextBox.TextEditor.Selection.CaretElement (you have to use Reflection as these Properties and Classes are for the most part internal sealed). As this is an Adorner you might be able to attach a ScaleTransform there that reverses the Scaling. I have to say though: this is neither tested nor recommended!
Your options are limited here and if I were you I'd go for my first guess!
EDIT:
If you really want to get down that second (bad) route you might have more luck if you apply that ScaleTransform to that adorners single child that you can get through the private field _caretElement of type CaretSubElement. If I read that code right, then that subelement is your actual Caret Visual. The main element seems to be used for drawing selection geometry. If you really want to do this, then apply that ScaleTransform there.
EDIT:
complete example to follow:
XAML:
<Window x:Class="RTBinViewBoxTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Viewbox Height="100" x:Name="vb">
<RichTextBox Width="70" Name="rtb">
<FlowDocument>
<Paragraph>
<Run>long long long long long long long long long long long long long long long long long long long long long long long long text</Run>
</Paragraph>
</FlowDocument>
</RichTextBox>
</Viewbox>
</Window>
Codebehind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
rtb.GotFocus +=RtbOnGotFocus;
}
private void RtbOnGotFocus(object s, RoutedEventArgs routedEventArgs)
{
rtb.LayoutUpdated += (sender, args) =>
{
var child = VisualTreeHelper.GetChild(vb, 0) as ContainerVisual;
var scale = child.Transform as ScaleTransform;
rtb.Selection.GetType().GetMethod("System.Windows.Documents.ITextSelection.UpdateCaretAndHighlight", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(
rtb.Selection, null);
var caretElement=rtb.Selection.GetType().GetProperty("CaretElement", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(rtb.Selection, null);
if (caretElement == null)
return;
var caretSubElement = caretElement.GetType().GetField("_caretElement", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(caretElement) as UIElement;
if (caretSubElement == null) return;
var scaleTransform = new ScaleTransform(1/scale.ScaleX, 1);
caretSubElement.RenderTransform = scaleTransform;
};
}
}
this works for me. everything said.
AFAIK you can't really solve this. ViewBox is using ScaleTransform under the covers, and ScaleTransform, when scaling down, will hide certain lines, since it cannot display everything. the ScaleTransform is not using a very advanced algorithm to do the scale,(it just does it in the fastest way possible) and I don't think you can change that..

Resources