I ran into this problem a while ago. At that time, I found the repka solution proposed, but I was not happy with it because it was relatively complex and not as efficient as I would like.
I solved the problem by encoding a set of simple form window form classes that work just like the built-in Path , Line , Polyline and Polygon classes, except that they make it easy to get stretching to work the way you want it to.
My classes are ViewboxPath , ViewboxLine , ViewboxPolyline and ViewboxPolygon , and they are used as follows:
<edf:ViewboxPolyline Viewbox="0 0 1 1" <!-- Actually the default, can be omitted --> Stretch="Fill" <!-- Also default, can be omitted --> Stroke="Blue" Points="0,0 0.2,0 0.2,0.3 0.4,0.3" /> <edf:ViewboxPolygon Viewbox="0 0 10 10" Stroke="Blue" Points="5,0 10,5 5,10 0,5" /> <edf:ViewboxPath Viewbox="0 0 10 10" Stroke="Blue" Data="M10,5 L4,4 L5,10" />
As you can see, my form window form classes are used in the same way as regular shapes ( Polyline , Polygon , Path and Line ), with the exception of the optional Viewbox parameter, and the fact that they default to Stretch="Fill" . The Viewbox parameter specifies in the coordinate system used to indicate the shape, the area of ββthe geometry to be stretched using the Fill , Uniform or UniformToFill , instead of using Geometry.GetBounds .
This gives very precise control over stretching and makes it easy to accurately separate individual shapes with each other.
Here is the actual code for my form window form classes , including the abstract base class ViewboxShape , which contains common functionality:
public abstract class ViewboxShape : Shape { Matrix _transform; Pen _strokePen; Geometry _definingGeometry; Geometry _renderGeometry; static ViewboxShape() { StretchProperty.OverrideMetadata(typeof(ViewboxShape), new FrameworkPropertyMetadata { AffectsRender = true, DefaultValue = Stretch.Fill, }); } // The built-in shapes compute stretching using the actual bounds of the geometry. // ViewBoxShape and its subclasses use this Viewbox instead and ignore the actual bounds of the geometry. public Rect Viewbox { get { return (Rect)GetValue(ViewboxProperty); } set { SetValue(ViewboxProperty, value); } } public static readonly DependencyProperty ViewboxProperty = DependencyProperty.Register("Viewbox", typeof(Rect), typeof(ViewboxShape), new UIPropertyMetadata { DefaultValue = new Rect(0,0,1,1), }); // If defined, replaces all the Stroke* properties with a single Pen public Pen Pen { get { return (Pen)GetValue(PenProperty); } set { SetValue(PenProperty, value); } } public static readonly DependencyProperty PenProperty = DependencyProperty.Register("Pen", typeof(Pen), typeof(ViewboxShape)); // Subclasses override this to define geometry if caching is desired, or just override DefiningGeometry protected virtual Geometry ComputeDefiningGeometry() { return null; } // Subclasses can use this PropertyChangedCallback for properties that affect the defining geometry protected static void OnGeometryChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { var shape = sender as ViewboxShape; if(shape!=null) { shape._definingGeometry = null; shape._renderGeometry = null; } } // Compute viewport from box & constraint private Size ApplyStretch(Stretch stretch, Rect box, Size constraint) { double uniformScale; switch(stretch) { default: return new Size(box.Width, box.Height); case Stretch.Fill: return constraint; case Stretch.Uniform: uniformScale = Math.Min(constraint.Width / box.Width, constraint.Height / box.Height); break; case Stretch.UniformToFill: uniformScale = Math.Max(constraint.Width / box.Width, constraint.Height / box.Height); break; } return new Size(uniformScale * box.Width, uniformScale * box.Height); } protected override Size MeasureOverride(Size constraint) { // Clear pen cache if settings have changed if(_strokePen!=null) if(Pen!=null) _strokePen = null; else if(_strokePen.Thickness != StrokeThickness || _strokePen.Brush != Stroke || _strokePen.StartLineCap != StrokeStartLineCap || _strokePen.EndLineCap != StrokeEndLineCap || _strokePen.DashCap != StrokeDashCap || _strokePen.LineJoin != StrokeLineJoin || _strokePen.MiterLimit != StrokeMiterLimit || _strokePen.DashStyle.Dashes != StrokeDashArray || _strokePen.DashStyle.Offset != StrokeDashOffset) _strokePen = null; _definingGeometry = null; _renderGeometry = null; return ApplyStretch(Stretch, Viewbox, constraint); } protected override Size ArrangeOverride(Size availableSize) { Stretch stretch = Stretch; Size viewport; Matrix transform; // Compute new viewport and transform if(stretch==Stretch.None) { viewport = availableSize; transform = Matrix.Identity; } else { Rect box = Viewbox; viewport = ApplyStretch(stretch, box, availableSize); double scaleX = viewport.Width / box.Width; double scaleY = viewport.Height / box.Height; transform = new Matrix(scaleX, 0, 0, scaleY, -box.Left * scaleX, -box.Top * scaleY); } if(_transform!=transform) { _transform = transform; _renderGeometry = null; InvalidateArrange(); } return viewport; } protected Pen PenOrStroke { get { if(Pen!=null) return Pen; if(_strokePen==null) _strokePen = new Pen { Thickness = StrokeThickness, Brush = Stroke, StartLineCap = StrokeStartLineCap, EndLineCap = StrokeEndLineCap, DashCap = StrokeDashCap, LineJoin = StrokeLineJoin, MiterLimit = StrokeMiterLimit, DashStyle = StrokeDashArray.Count==0 && StrokeDashOffset==0 ? DashStyles.Solid : new DashStyle(StrokeDashArray, StrokeDashOffset), }; return _strokePen; } } protected Matrix Transform { get { return _transform; } } protected override Geometry DefiningGeometry { get { if(_definingGeometry==null) _definingGeometry = ComputeDefiningGeometry(); return _definingGeometry; } } protected Geometry RenderGeometry { get { if(_renderGeometry==null) { Geometry defining = DefiningGeometry; if(_transform==Matrix.Identity || defining==Geometry.Empty) _renderGeometry = defining; else { Geometry geo = defining.CloneCurrentValue(); if(object.ReferenceEquals(geo, defining)) geo = defining.Clone(); geo.Transform = new MatrixTransform( geo.Transform==null ? _transform : geo.Transform.Value * _transform); _renderGeometry = geo; } } return _renderGeometry; } } protected override void OnRender(DrawingContext drawingContext) { drawingContext.DrawGeometry(Fill, PenOrStroke, RenderGeometry); } } [ContentProperty("Data")] public class ViewboxPath : ViewboxShape { public Geometry Data { get { return (Geometry)GetValue(DataProperty); } set { SetValue(DataProperty, value); } } public static readonly DependencyProperty DataProperty = DependencyProperty.Register("Data", typeof(Geometry), typeof(ViewboxPath), new UIPropertyMetadata { DefaultValue = Geometry.Empty, PropertyChangedCallback = OnGeometryChanged, }); protected override Geometry DefiningGeometry { get { return Data ?? Geometry.Empty; } } } public class ViewboxLine : ViewboxShape { public double X1 { get { return (double)GetValue(X1Property); } set { SetValue(X1Property, value); } } public double X2 { get { return (double)GetValue(X2Property); } set { SetValue(X2Property, value); } } public double Y1 { get { return (double)GetValue(Y1Property); } set { SetValue(Y1Property, value); } } public double Y2 { get { return (double)GetValue(Y2Property); } set { SetValue(Y2Property, value); } } public static readonly DependencyProperty X1Property = DependencyProperty.Register("X1", typeof(double), typeof(ViewboxLine), new FrameworkPropertyMetadata { PropertyChangedCallback = OnGeometryChanged, AffectsRender = true }); public static readonly DependencyProperty X2Property = DependencyProperty.Register("X2", typeof(double), typeof(ViewboxLine), new FrameworkPropertyMetadata { PropertyChangedCallback = OnGeometryChanged, AffectsRender = true }); public static readonly DependencyProperty Y1Property = DependencyProperty.Register("Y1", typeof(double), typeof(ViewboxLine), new FrameworkPropertyMetadata { PropertyChangedCallback = OnGeometryChanged, AffectsRender = true }); public static readonly DependencyProperty Y2Property = DependencyProperty.Register("Y2", typeof(double), typeof(ViewboxLine), new FrameworkPropertyMetadata { PropertyChangedCallback = OnGeometryChanged, AffectsRender = true }); protected override Geometry ComputeDefiningGeometry() { return new LineGeometry(new Point(X1, Y1), new Point(X2, Y2)); } } [ContentProperty("Points")] public class ViewboxPolyline : ViewboxShape { public ViewboxPolyline() { Points = new PointCollection(); } public PointCollection Points { get { return (PointCollection)GetValue(PointsProperty); } set { SetValue(PointsProperty, value); } } public static readonly DependencyProperty PointsProperty = DependencyProperty.Register("Points", typeof(PointCollection), typeof(ViewboxPolyline), new FrameworkPropertyMetadata { PropertyChangedCallback = OnGeometryChanged, AffectsRender = true, }); public FillRule FillRule { get { return (FillRule)GetValue(FillRuleProperty); } set { SetValue(FillRuleProperty, value); } } public static readonly DependencyProperty FillRuleProperty = DependencyProperty.Register("FillRule", typeof(FillRule), typeof(ViewboxPolyline), new FrameworkPropertyMetadata { DefaultValue = FillRule.EvenOdd, PropertyChangedCallback = OnGeometryChanged, AffectsRender = true, }); public bool CloseFigure { get { return (bool)GetValue(CloseFigureProperty); } set { SetValue(CloseFigureProperty, value); } } public static readonly DependencyProperty CloseFigureProperty = DependencyProperty.Register("CloseFigure", typeof(bool), typeof(ViewboxPolyline), new FrameworkPropertyMetadata { DefaultValue = false, PropertyChangedCallback = OnGeometryChanged, AffectsRender = true, }); protected override Geometry ComputeDefiningGeometry() { PointCollection points = Points; if(points.Count<2) return Geometry.Empty; var geometry = new StreamGeometry { FillRule = FillRule }; using(var context = geometry.Open()) { context.BeginFigure(Points[0], true, CloseFigure); context.PolyLineTo(Points.Skip(1).ToList(), true, true); } return geometry; } } public class ViewboxPolygon : ViewboxPolyline { static ViewboxPolygon() { CloseFigureProperty.OverrideMetadata(typeof(ViewboxPolygon), new FrameworkPropertyMetadata { DefaultValue = true, }); } }
Enjoy it!