Dynamically modifying a Path, Geometry, Shape in XAML - wpf

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

Related

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.

Silverlight 5: Terrible Performance Issues with DropShadowEffect

Environment Info:
Windows 7 64bit SP1
6GB Memory
Intel(R) Core(TM) i5-2400S CPU # 2.50GHz (4 cores)
Silverlight 5
I have very terrible performance issues with DropShadowEffect in Silerlight.
I created a minimized demo for it:
http://www.peterlee.com.cn/public/ShadowEffectTestDemo/ShadowEffectTestTestPage.html
Click the button Zoom to try to zoom the canvas (with a Star shape with DropShadowEffect). You will find that when the Zoom = 64, your CPU and memory are very crazy.
But, if you remove the DropShadowEffect by clicking the Remove Effect button, and then zoom it, everything is okay.
However, if we use a TextBlock with DropShadowEffect, everything is fine. You can try it by clicking the "Use TextBlock` button.
I don't know what Silverlight is dealing with the TextBlock and my customized Star shape for the DropShadowEffect.
Please help me out. Thanks.
UPDATE:
According to the replies:
I also tried it in Release mode (now the sample demo link was built in Release mode);
I also added GPA acceleration suggested by ChrisF:
<object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%">
<param name="EnableGPUAcceleration" value="true" />
<param name="source" value="ShadowEffectTest.xap"/>
<param name="onError" value="onSilverlightError" />
<param name="background" value="white" />
<param name="minRuntimeVersion" value="5.0.61118.0" />
<param name="autoUpgrade" value="true" />
<a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=5.0.61118.0" style="text-decoration:none">
<img src="http://go.microsoft.com/fwlink/?LinkId=161376" alt="Get Microsoft Silverlight" style="border-style:none"/>
</a>
</object>
Here is the UPDATED code
MainPage.xmal:
<UserControl x:Class="ShadowEffectTest.MainPage"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button x:Name="btnZoom" Width="75" Height="23" Content="Zoom"
Grid.Column="0" Grid.Row="0"
Click="btnZoom_Click"/>
<Button x:Name="btnRemoveEffect" Width="100" Height="23" Content="Remove Effect"
Grid.Row="0" Grid.Column="1"
Click="btnRemoveEffect_Click"/>
<Button x:Name="btnUseTextBlock" Width="120" Height="23" Content="Use TextBlock"
Grid.Row="1" Grid.Column="0"
Click="btnUseTextBlock_Click" />
<Canvas x:Name="mainCanvas" Width="200" Height="150" Background="LightBlue"
Grid.Column="1" Grid.Row="1" />
</Grid>
</UserControl>
MainPage.xaml.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Diagnostics;
using System.Windows.Media.Effects;
namespace ShadowEffectTest
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
// These are the points for a Star shape
List<Point> points = new List<Point>()
{
new Point(0, 36.3634469292486),
new Point(-12.3606797688474, 43.2188191057189),
new Point(-24.7213595376948, 50.0741912821893),
new Point(-37.0820393065422, 56.9295634586596),
new Point(-34.4313595448175, 42.9654855127096),
new Point(-31.7806797830927, 29.0014075667595),
new Point(-29.130000021368, 15.0373296208095),
new Point(-39.4200000080385, 5.31010468057062),
new Point(-49.709999994709, -4.41712025966827),
new Point(-59.9999999813794, -14.1443451999072),
new Point(-46.0011100186002, -15.919247828416),
new Point(-32.002220055821, -17.694150456925),
new Point(-18.0033300930418, -19.4690530854338),
new Point(-12.0022200620278, -32.3361808961241),
new Point(-6.00111003101392, -45.2033087068145),
new Point(0, -58.0704365175048),
new Point(6.00111003101392, -45.2033087068145),
new Point(12.0022200620278, -32.3361808961241),
new Point(18.0033300930418, -19.4690530854338),
new Point(32.002220055821, -17.694150456925),
new Point(46.0011100186002, -15.919247828416),
new Point(59.9999999813794, -14.1443451999072),
new Point(49.709999994709, -4.41712025966827),
new Point(39.4200000080385, 5.31010468057062),
new Point(29.130000021368, 15.0373296208095),
new Point(31.7806797830927, 29.0014075667595),
new Point(34.4313595448175, 42.9654855127096),
new Point(37.0820393065422, 56.9295634586596),
new Point(24.7213595376948, 50.0741912821893),
new Point(12.3606797688474, 43.2188191057189),
new Point(0, 36.3634469292486)
};
uie = RenderBezier(points);
// This the evil for the performance issues (crazy memory and CPU)
uie.Effect = ShadowEffect;
uie.CacheMode = new BitmapCache();
uie.SetValue(Canvas.LeftProperty, 25.0);
uie.SetValue(Canvas.TopProperty, 25.0);
mainCanvas.Children.Add(uie);
}
private UIElement uie = null;
public Path RenderBezier(List<Point> ctrlPoints, List<List<Point>> innersCtrlPoints = null)
{
// Step 0: Merge ctrlPoints lists
List<List<Point>> ctrlPointsLists;
if (innersCtrlPoints == null)
{
ctrlPointsLists = new List<List<Point>>(1);
ctrlPointsLists.Add(ctrlPoints);
}
else
{
ctrlPointsLists = new List<List<Point>>(1 + innersCtrlPoints.Count);
ctrlPointsLists.Add(ctrlPoints);
foreach (List<Point> list in innersCtrlPoints)
ctrlPointsLists.Add(list);
}
PathGeometry pg = new PathGeometry();
foreach (List<Point> list in ctrlPointsLists)
{
// Step 0: check (Debug.Assert)
Debug.Assert(list.Count % 3 == 1,
"public Path RenderBezier(IList<Point> ctrlPoints): number of control points is not 3n+1.");
int n = (list.Count - 1) / 3; // Number of BezierSegments
Debug.Assert(n > 0,
"public Path RenderBezier(IList<Point> ctrlPoints): at least one Bezier segment required.");
// Step 1: Add BezierSegments to PathFigure
PathFigure pf = new PathFigure();
pf.StartPoint = list[0];
for (int i = 0; i < n; ++i)
pf.Segments.Add(GetBezierSegment(
list[3 * i + 1],
list[3 * i + 2],
list[3 * i + 3]
));
// Step 2: Add PathFigures to PathGeometry
pg.Figures.Add(pf);
}
// Step 3: Add PathGemotry to GeometryGroup
GeometryGroup gg = new GeometryGroup();
gg.Children.Add(pg);
// Step 4: Set GeometryGroup as Path.Data
Path path = new Path();
path.Data = gg;
// Step 5: Set some Path properties
// if (ShowOutline)
{
path.Stroke = new SolidColorBrush(Colors.Black);
path.StrokeThickness = 1.0;
path.StrokeEndLineCap = PenLineCap.Round;
path.StrokeLineJoin = PenLineJoin.Round;
path.StrokeStartLineCap = PenLineCap.Round;
}
// Finally, return it
return path;
}
// This the evil for the performance issues (crazy memory and CPU)
private static DropShadowEffect ShadowEffect
{
get
{
return new DropShadowEffect()
{
Color = Colors.Blue,
BlurRadius = 5,
Direction = 0,
ShadowDepth = 0
};
}
}
private static BezierSegment GetBezierSegment(Point p1, Point p2, Point p3)
{
BezierSegment bs = new BezierSegment();
bs.Point1 = p1;
bs.Point2 = p2;
bs.Point3 = p3;
return bs;
}
public static readonly double[] ZoomingSteps = new double[]
{
1.0,
1.5,
2.0,
3.0,
4.0,
6.0,
8.0,
12.0,
16.0,
24.0,
32.0,
48.0,
64.0,
128.0
};
private int index = 0;
private void btnZoom_Click(object sender, RoutedEventArgs e)
{
if (index >= ZoomingSteps.Length - 1)
return;
ScaleTransform st = new ScaleTransform();
st.ScaleX = st.ScaleY = ZoomingSteps[index++];
btnZoom.Content = ZoomingSteps[index].ToString();
mainCanvas.RenderTransform = st;
}
private void btnRemoveEffect_Click(object sender, RoutedEventArgs e)
{
index = 0;
btnZoom.Content = "Zoom";
uie.Effect = null;
mainCanvas.RenderTransform = new ScaleTransform();
}
// If I use TextBlock as the child UIElement, then everything is okay
// path = new TextBlock() { Text = "Text Block" };
private void btnUseTextBlock_Click(object sender, RoutedEventArgs e)
{
mainCanvas.Children.Remove(uie);
index = 0;
btnZoom.Content = "Zoom";
uie = new TextBlock() { Text = "Text Block" };
mainCanvas.Children.Add(uie);
uie.Effect = ShadowEffect;
uie.CacheMode = new BitmapCache();
mainCanvas.RenderTransform = new ScaleTransform();
}
}
}
I've tried running WinDbg against your application and dumpheap the application. here is the Result. Now dont worry, I dont understand half of it either. But I went a little deeper and there seem to be large amounts of arrays/lists where atleast 80% are null.
You might want to look at WinDbg yourself. Here is a small Tutorial
I'll try to look at it in the morning if I can and I hope I've helped you atleast in the right direction
Here's my trick: never use DropShadowEffect. Create a clone of whatever you want to create a drop shadow for, put it behind the main content, and use a BlurEffect on it. Much better performance, much better quality. I have no idea why.
I think the problem is here:
uie.CacheMode = new BitmapCache();
Typically, this tells silverlight to try and cache the bitmap in memory so that the computer does not need to re-render the element in your application.
Look here for the suggested tips on silverlight performance:
http://msdn.microsoft.com/en-us/library/cc189071(v=vs.95).aspx
The problem has to do with how the rendering pipeline of Silverlight works. The DropShadowEffect is just a pixel shader, and pixel shaders work in Silverlight by creating a buffered copy of the image they will be applied to and then allow you to change pixel properties of the real image using the values of the buffered copy. This buffered copy is the entire image and is not affected by clipping. Because zooming creates a very large pre-clipped image, the pixel shader must make a physical buffered copy of a very large pre-clipped image...this is why the performance is so bad when you zoom.
The only solution I have found for this is to disable the pixel shader effect while you are zooming (or panning the zoomed image), and add it back when the image is zoomed in (or panning is complete). This way you don't incur the performance penalty of the pixel shader being applied for every frame of zooming or panning.

Databound Triangle Creation?

I've got a requirement to create several shapes based on a supplied size (all of them have the same height/width) and have their sizes be databound to that supplied property on the datacontext.
Most of the shapes are easy: Circle (ellipse with height/width bound), square (rectangle with height/width bound), diamond (same as square, then use a RotateTransform), + (two lines), X (two lines).
But I'm trying to figure out how to do it for a triangle and I can't figure it out. It needs to be a filled object, so I can't just do it with three lines.
But all of the ways i've seen to do it (w/ a Path or a Polygon) end up taking Point objects (StartPoint, EndPoint, etc). And you can't bind to the X or Y values of the Point object.
Am I missing something? Or do I need to write my own custom shape or something?
Edit: To add a little bit of clarity... the type of triangle I'm creating doesn't really matter. It can be equilateral or isosceles. I was targeting an isosceles, so that it would have a base with the databound width and the top "tip" of the triangle will be at the mid-point of the databound width and at Y=0. That was just an optimization for simplicity's sake
The behavior class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Shapes;
using System.Windows.Media;
namespace WpfApplication1
{
public enum ShapeType
{
Rectangle,
Isosceles,
Ellipse,
Dice,
Hexagon
}
public class PathControl
{
public static readonly DependencyProperty ShapeTypeProperty =
DependencyProperty.RegisterAttached("ShapeType",
typeof(ShapeType?),
typeof(DependencyObject),
new PropertyMetadata(null,
new PropertyChangedCallback((sender, args) =>
{
Path path = sender as Path;
ShapeType? shapeType = (ShapeType?)args.NewValue;
//todo: use a WeakEvent
path.SizeChanged +=
(pathSender, pathArgs) =>
{
PathControl.InvalidatePath((Path)sender, shapeType);
};
})));
private static void InvalidatePath(Path path, ShapeType? shapeType)
{
if (path != null
&& shapeType.HasValue)
{
string source = null;
double netWidth = path.Width - 2 * path.StrokeThickness,
netHeight = path.Height - 2 * path.StrokeThickness;
if (shapeType == ShapeType.Rectangle)
{
source = string.Format("M0,0 h{0} v{1} h-{0} z",
new object[2]
{
netWidth,
netHeight
});
}
else if (shapeType == ShapeType.Isosceles)
{
source = string.Format("M0,{1} l{0},-{1} {0},{1} z",
new object[2]
{
netWidth / 2,
netHeight
});
}
else
{
throw new NotImplementedException(shapeType.ToString());
}
path.Data = Geometry.Parse(source);
}
}
public static void SetShapeType(DependencyObject o, ShapeType e)
{
o.SetValue(PathControl.ShapeTypeProperty, e);
}
public static ShapeType? GetShapeType(DependencyObject o)
{
return (ShapeType)o.GetValue(PathControl.ShapeTypeProperty);
}
}
}
The XAML:
<Window x:Class="WpfApplication1.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"
xmlns:local="clr-namespace:WpfApplication1">
<Grid>
<Path Width="100" Height="100" Stroke="Green" StrokeThickness="2" Fill="Yellow"
local:PathControl.ShapeType="Isosceles">
<Path.RenderTransform>
<RotateTransform Angle="90"></RotateTransform>
</Path.RenderTransform>
</Path>
</Grid>
</Window>
Binding to the Points is the best/only way. The X and X properties of a Point cannot be bound to because they do not raise the PropertyChanged event. The Point is a structure and structures should be read-only.
The PointCollection class raises the correct events so you can bind to it. This allows you to manipulate the triangles by modifying the collection of point by replacing the points. Do not change the point but replace them so the proper events will be raised.

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

How to keep relative position of WPF elements on background image

I am new to WPF, so the answer to the following question might be obvious, however it isn't to me.
I need to display an image where users can set markers on (As an example: You might want to mark a person's face on a photograph with a rectangle), however the markers need to keep their relative position when scaling the image.
Currently I am doing this by using a Canvas and setting an ImageBrush as Background. This displays the image and I can add elements like a Label (as replacement for a rectangle) on top of the image. But when I set a label like this, it's position is absolute and so when the underlying picture is scaled (because the user drags the window larger) the Label stays at it's absolute position (say, 100,100) instead of moving to the new position that keeps it "in sync" with the underlying image.
To cut the matter short: When I set a marker on a person's eye, it shouldn't be on the person's ear after scaling the window.
Any suggestions on how to do that in WPF? Maybe Canvas is the wrong approach in the first place? I could keep a collection of markers in code and recalculate their position every time the window gets resized, but I hope there is a way to let WPF do that work for me :-)
I am interested in hearing your opinions on this.
Thanks
Okay that seems to work. Here's what I did:
Wrote a custom converter
Every time a user clicks on the canvas, I create a new Label (will exchange that with a UserComponent later), create bindings using my converter class and do the initial calculations to get the relative position to the canvas from the absolute position of the mouse pointer
Here's some sample code for the converter:
public class PercentageConverter : IValueConverter
{
/// <summary>
/// Calculates absolute position values of an element given the dimensions of the container and the relative
/// position of the element, expressed as percentage
/// </summary>
/// <param name="value">Dimension value of the container (width or height)</param>
/// <param name="parameter">The percentage used to calculate new absolute value</param>
/// <returns>parameter * value as Double</returns>
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
//input is percentage
//output is double
double containerValue = System.Convert.ToDouble(value, culture.NumberFormat);
double perc;
if (parameter is String)
{
perc = double.Parse(parameter as String, culture.NumberFormat);
}
else
{
perc = (double)parameter;
}
double coord = containerValue * perc;
return coord;
}
/// <summary>
/// Calculates relative position (expressed as percentage) of an element to its container given its current absolute position
/// as well as the dimensions of the container
/// </summary>
/// <param name="value">Absolute value of the container (width or height)</param>
/// <param name="parameter">X- or Y-position of the element</param>
/// <returns>parameter / value as double</returns>
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
//output is percentage
//input is double
double containerValue = System.Convert.ToDouble(value, culture.NumberFormat);
double coord = double.Parse(parameter as String, culture.NumberFormat);
double perc = coord / containerValue;
return perc;
}
}
And here's how you can create bindings in XAML (note that my canvas is declared as <Canvas x:Name="canvas" ... >):
<Label Background="Red" ClipToBounds="True" Height="22" Name="label1" Width="60"
Canvas.Left="{Binding Converter={StaticResource PercentageConverter}, ElementName=canvas, Path=ActualWidth, ConverterParameter=0.25}"
Canvas.Top="{Binding Converter={StaticResource PercentageConverter}, ElementName=canvas, Path=ActualHeight, ConverterParameter=0.65}">Marker 1</Label>
More useful, however, is to create Labels in code:
private void canvas_MouseDown(object sender, MouseButtonEventArgs e)
{
var mousePos = Mouse.GetPosition(canvas);
var converter = new PercentageConverter();
//Convert mouse position to relative position
double xPerc = (double)converter.ConvertBack(canvas.ActualWidth, typeof(Double), mousePos.X.ToString(), Thread.CurrentThread.CurrentCulture);
double yPerc = (double)converter.ConvertBack(canvas.ActualHeight, typeof(Double), mousePos.Y.ToString(), Thread.CurrentThread.CurrentCulture);
Label label = new Label { Content = "Label", Background = (Brush)new BrushConverter().ConvertFromString("Red")};
//Do binding for x-coordinates
Binding posBindX = new Binding();
posBindX.Converter = new PercentageConverter();
posBindX.ConverterParameter = xPerc;
posBindX.Source = canvas;
posBindX.Path = new PropertyPath("ActualWidth");
label.SetBinding(Canvas.LeftProperty, posBindX);
//Do binding for y-coordinates
Binding posBindY = new Binding();
posBindY.Converter = new PercentageConverter();
posBindY.ConverterParameter = yPerc;
posBindY.Source = canvas;
posBindY.Path = new PropertyPath("ActualHeight");
label.SetBinding(Canvas.TopProperty, posBindY);
canvas.Children.Add(label);
}
So basically, it's almost like my first idea: Use relative position instead of absolute and recalculate all positions on every resize, only this way it's being done by WPF. Just what I wanted, thanks Martin!
Note however, that these examples only work if the Image inside the ImageBrush has exactly the same dimensions as the surrounding Canvas, because this relative positioning does not take margins etc into account. I will have to tune that
Of the top of my head you could write a converter class that would take in a percentage and return an absolute position. As an example if your window was 200 X 200 and you placed the label at 100 X 100 when you scale the window to 400 X 400 the label would stay where it is (as per your original question). However if you used a converter so that instead you could set the labels position to 50% of its parent container's size then as the window scaled the label would move with it.
You may also need to use the same converter for width and height so that it increased in size to match as well.
Sorry for the lack of detail, if I get a chance I'll edit this with example code in a little while.
Edited to add
This question gives some code for a percentage converter.
Although this post is old and already answered, it can still be helpful to others so I will add my answer.
I came up with two ways for maintaining a relative position for elements in a Canvas
MultiValueConverter
Attached Properties
The idea is to provide two values (x,y) in range [0,1] that will define the relative position of the element with respect to the top-left corner of the Canvas. These (x,y) values will be used to calculate and set the correct Canvas.Left and Canvas.Top values.
In order to place the center of the element at a relative position, we will need the ActualWidth and ActualHeight of the Canvas and the element.
MultiValueConverter
The MultiValueConverter RelativePositionConverter:
This converter can be used to relatively position the X and/or Y position when binding with Canvas.Left and Canvas.Top.
public class RelativePositionConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values?.Length < 2
|| !(values[0] is double relativePosition)
|| !(values[1] is double size)
|| !(parameter is string)
|| !double.TryParse((string)parameter, out double relativeToValue))
{
return DependencyProperty.UnsetValue;
}
return relativePosition * relativeToValue - size / 2;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Example usage of RelativePositionConverter:
A Canvas width and height are binded to an Image. The Canvas has a child element - an Ellipse that maintains a relative position with the Canvas (and Image).
<Grid Margin="10">
<Image x:Name="image" Source="Images/example-graph.png" />
<Canvas Background="#337EEBE8" Width="{Binding ElementName=image, Path=ActualWidth}" Height="{Binding ElementName=image, Path=ActualHeight}">
<Ellipse Width="35" Height="35" StrokeThickness="5" Fill="#D8FFFFFF" Stroke="#FFFBF73C">
<Canvas.Left>
<MultiBinding Converter="{StaticResource RelativePositionConverter}" ConverterParameter="0.461">
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=Canvas}" Path="ActualWidth" />
<Binding RelativeSource="{RelativeSource Self}" Path="ActualWidth" />
</MultiBinding>
</Canvas.Left>
<Canvas.Top>
<MultiBinding Converter="{StaticResource RelativePositionConverter}" ConverterParameter="0.392">
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=Canvas}" Path="ActualHeight" />
<Binding RelativeSource="{RelativeSource Self}" Path="ActualHeight" />
</MultiBinding>
</Canvas.Top>
</Ellipse>
</Canvas>
</Grid>
Attached Properties
The Attached Properties RelativeXProperty, RelativeYProperty and RelativePositionProperty:
RelativeXProperty and RelativeYProperty can be used to control the X and/or Y relative positioning with two separate attached properties.
RelativePositionProperty can be used to control the X and Y relative positioning with a single attached property.
public static class CanvasExtensions
{
public static readonly DependencyProperty RelativeXProperty =
DependencyProperty.RegisterAttached("RelativeX", typeof(double), typeof(CanvasExtensions), new PropertyMetadata(0.0, new PropertyChangedCallback(OnRelativeXChanged)));
public static readonly DependencyProperty RelativeYProperty =
DependencyProperty.RegisterAttached("RelativeY", typeof(double), typeof(CanvasExtensions), new PropertyMetadata(0.0, new PropertyChangedCallback(OnRelativeYChanged)));
public static readonly DependencyProperty RelativePositionProperty =
DependencyProperty.RegisterAttached("RelativePosition", typeof(Point), typeof(CanvasExtensions), new PropertyMetadata(new Point(0, 0), new PropertyChangedCallback(OnRelativePositionChanged)));
public static double GetRelativeX(DependencyObject obj)
{
return (double)obj.GetValue(RelativeXProperty);
}
public static void SetRelativeX(DependencyObject obj, double value)
{
obj.SetValue(RelativeXProperty, value);
}
public static double GetRelativeY(DependencyObject obj)
{
return (double)obj.GetValue(RelativeYProperty);
}
public static void SetRelativeY(DependencyObject obj, double value)
{
obj.SetValue(RelativeYProperty, value);
}
public static Point GetRelativePosition(DependencyObject obj)
{
return (Point)obj.GetValue(RelativePositionProperty);
}
public static void SetRelativePosition(DependencyObject obj, Point value)
{
obj.SetValue(RelativePositionProperty, value);
}
private static void OnRelativeXChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is FrameworkElement element)) return;
if (!(VisualTreeHelper.GetParent(element) is Canvas canvas)) return;
canvas.SizeChanged += (s, arg) =>
{
double relativeXPosition = GetRelativeX(element);
double xPosition = relativeXPosition * canvas.ActualWidth - element.ActualWidth / 2;
Canvas.SetLeft(element, xPosition);
};
}
private static void OnRelativeYChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is FrameworkElement element)) return;
if (!(VisualTreeHelper.GetParent(element) is Canvas canvas)) return;
canvas.SizeChanged += (s, arg) =>
{
double relativeYPosition = GetRelativeY(element);
double yPosition = relativeYPosition * canvas.ActualHeight - element.ActualHeight / 2;
Canvas.SetTop(element, yPosition);
};
}
private static void OnRelativePositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is FrameworkElement element)) return;
if (!(VisualTreeHelper.GetParent(element) is Canvas canvas)) return;
canvas.SizeChanged += (s, arg) =>
{
Point relativePosition = GetRelativePosition(element);
double xPosition = relativePosition.X * canvas.ActualWidth - element.ActualWidth / 2;
double yPosition = relativePosition.Y * canvas.ActualHeight - element.ActualHeight / 2;
Canvas.SetLeft(element, xPosition);
Canvas.SetTop(element, yPosition);
};
}
}
Example usage of RelativeXProperty and RelativeYProperty:
<Grid Margin="10">
<Image x:Name="image" Source="Images/example-graph.png" />
<Canvas Background="#337EEBE8" Width="{Binding ElementName=image, Path=ActualWidth}" Height="{Binding ElementName=image, Path=ActualHeight}">
<Ellipse Width="35" Height="35" StrokeThickness="5" Fill="#D8FFFFFF" Stroke="#FFFBF73C"
local:CanvasExtensions.RelativeX="0.461"
local:CanvasExtensions.RelativeY="0.392">
</Ellipse>
</Canvas>
</Grid>
Example usage of RelativePositionProperty:
<Grid Margin="10">
<Image x:Name="image" Source="Images/example-graph.png" />
<Canvas Background="#337EEBE8" Width="{Binding ElementName=image, Path=ActualWidth}" Height="{Binding ElementName=image, Path=ActualHeight}">
<Ellipse Width="35" Height="35" StrokeThickness="5" Fill="#D8FFFFFF" Stroke="#FFFBF73C"
local:CanvasExtensions.RelativePosition="0.461,0.392">
</Ellipse>
</Canvas>
</Grid>
And hear is how it looks:
The Ellipse that is a child of a Canvas maintains a relative position with respect to the Canvas (and an Image).

Resources