I ran into this problem a while back. At the time I found the solution repka proposed but was dissatisfied with it because it was relatively complex and not as efficient as I would have liked.
I solved the problem by coding a set of simple Viewbox shape classes that work exactly like the built in Path
, Line
, Polyline
, and Polygon
classes except they make it easy to get stretching to work the way you want it.
My classes are ViewboxPath
, ViewboxLine
, ViewboxPolyline
and ViewboxPolygon
, and they are used like this:
<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 Viewbox shape classes are used just like normal shapes (Polyline
, Polygon
, Path
and Line
) except for the extra Viewbox
parameter, and the fact that they default to Stretch="Fill"
. The Viewbox parameter specifies, in the coordinate system used to specify the shape, the area of the geometry that should be stretched using Fill
, Uniform
or UniformToFill
settings, instead of using Geometry.GetBounds
.
This gives very precise control over the stretching and makes it easy to have separate shapes align accurately with one another.
Here is the actual code for my Viewbox shape classes, including the abstract base class ViewboxShape
that 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!