I'm trying to create a pretty simple graphing component that consists of a series of Polylines within the same grid cell that represent graph lines. My strategy is to look at all the points in my set, determine the min and max, and then calculate a number between 0 to 1 accordingly and use Stretch="Fill" to stretch each Polyline to fill the grid cell. My desired effect would be that a point at 0,.5 would be vertically in the center of the cell, but in reality the Polyline gets stretched vertically to fill the entire cell depending on what the min and max Y value is. E.g. if .5 is my max and .7 is my min in the Polyline, then .5 will be clear at the top of the cell and .7 clear at the bottom, rather than .5 in the center and .7 7/10 to the bottom.
我正在尝试创建一个非常简单的图形组件,它由同一网格单元格中的一系列折线组成,代表图线。我的策略是查看我的集合中的所有点,确定最小值和最大值,然后相应地计算0到1之间的数字,并使用Stretch =“Fill”拉伸每条折线以填充网格单元格。我期望的效果是0,.5处的点将垂直地位于单元的中心,但实际上,折线被垂直拉伸以填充整个单元,这取决于最小和最大Y值。例如。如果.5是我的最大值,而.7是我在Polyline中的最小值,那么.5将在单元格顶部清晰,.7清除在底部,而不是.5在中心和.7 7/10到底部。
Here's a simple example with two Polylines and calculated points between 0 and 1. You'll notice the red Polyline is directly on top of the blue one, even though the red Y values are greater. The red Polyline should look the same as the blue, but be oriented slightly lower in the cell. However it's being stretched to fill the entire cell so it sits directly on top of the blue.
这是一个简单的例子,其中包含两条折线,计算出的点在0和1之间。您会注意到红色折线直接位于蓝色折线之上,即使红色Y值更大。红色折线应看起来与蓝色相同,但在细胞中的位置略低。然而,它被拉伸以填充整个单元格,因此它直接位于蓝色的顶部。
<Window x:Class="Test.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="100" Width="300">
<Grid>
<Polyline
Stretch="Fill"
Stroke="Blue"
Points="0,0 0.2,0 0.2,0.363636363636364 0.4,0.363636363636364 0.4,0.636363636363636 0.6,0.636363636363636 0.6,0.0909090909090909 0.8,0.0909090909090909 0.8,0 1,0" />
<Polyline
Stretch="Fill"
Stroke="Red"
Points="0,0.363636363636364 0.2,0.363636363636364 0.2,0.727272727272727 0.4,0.727272727272727 0.4,1 0.6,1 0.6,0.454545454545455 0.8,0.454545454545455 0.8,0.363636363636364 1,0.363636363636364" />
</Grid>
The reason I'm using 0 to 1 values is because I want the width and height of the grid cell to be easily changeable, e.g. via a slider or something to adjust the height of the graph, or dragging the window wider to adjust the width. So I tried to use this stretch strategy to achieve that instead of calculating pixel values w/out stretching.
我使用0到1值的原因是因为我希望网格单元的宽度和高度可以很容易地改变,例如通过滑块或其他东西来调整图形的高度,或者将窗口拖得更宽以调整宽度。所以我尝试使用这种拉伸策略来实现这一目标,而不是计算出拉伸的像素值。
Any advice on how to achieve this?
有关如何实现这一目标的任何建议?
Thanks.
2 个解决方案
#1
3
I had similar problem because I couldn't find an easy way to scale multiple shapes. Ended up using DrawingGroup
with multiple GeometryDrawing
inside. So they scale together. Here are your graphs with this approach. Looks bulky but should work fast. Plus you'll most likely populate line segments from code:
我有类似的问题,因为我找不到一种简单的方法来缩放多个形状。使用带有多个GeometryDrawing的DrawingGroup结束。所以他们一起扩展。以下是使用此方法的图表。看起来很笨,但应该快速工作。另外,您最有可能从代码中填充线段:
<Window x:Class="Polyline.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="100" Width="300">
<Grid>
<Image>
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="Transparent">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,1,1">
<RectangleGeometry.Transform>
<ScaleTransform ScaleX="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=Grid}}"
ScaleY="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType=Grid}}"/>
</RectangleGeometry.Transform>
</RectangleGeometry>
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing>
<GeometryDrawing.Pen>
<Pen Brush="Blue" Thickness="1"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Geometry>
<PathGeometry>
<PathGeometry.Transform>
<ScaleTransform ScaleX="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=Grid}}"
ScaleY="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType=Grid}}"/>
</PathGeometry.Transform>
<PathGeometry.Figures>
<PathFigure StartPoint="0,0">
<PathFigure.Segments>
<LineSegment Point="0.2,0"/>
<LineSegment Point="0.2,0.363636363636364"/>
<LineSegment Point="0.4,0.363636363636364"/>
<LineSegment Point="0.4,0.636363636363636"/>
<LineSegment Point="0.6,0.636363636363636"/>
<LineSegment Point="0.6,0.0909090909090909"/>
<LineSegment Point="0.8,0.0909090909090909"/>
<LineSegment Point="0.8,0"/>
<LineSegment Point="1,0"/>
</PathFigure.Segments>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing>
<GeometryDrawing.Pen>
<Pen Brush="Red" Thickness="1"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Geometry>
<PathGeometry>
<PathGeometry.Transform>
<ScaleTransform ScaleX="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=Grid}}"
ScaleY="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType=Grid}}"/>
</PathGeometry.Transform>
<PathGeometry.Figures>
<PathFigure StartPoint="0,0.363636363636364">
<PathFigure.Segments>
<LineSegment Point="0.2,0.363636363636364"/>
<LineSegment Point="0.2,0.727272727272727"/>
<LineSegment Point="0.4,0.727272727272727"/>
<LineSegment Point="0.4,1"/>
<LineSegment Point="0.6,1"/>
<LineSegment Point="0.6,0.454545454545455"/>
<LineSegment Point="0.8,0.454545454545455"/>
<LineSegment Point="0.8,0.363636363636364"/>
<LineSegment Point="1,0.363636363636364"/>
</PathFigure.Segments>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</Grid>
</Window>
You can remove first RectangleGeometry
if you don't need graphs always scale between 0 and 1.
如果不需要图形始终在0和1之间缩放,则可以删除第一个RectangleGeometry。
#2
3
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.
我不久前遇到了这个问题。当时我发现解决方案repka提出但不满意,因为它相对复杂,并没有我想要的那么高效。
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.
我通过编写一组简单的Viewbox形状类来解决这个问题,这些类的工作方式与内置的Path,Line,Polyline和Polygon类完全相同,只不过它们可以让您轻松地按照您想要的方式工作。
My classes are ViewboxPath
, ViewboxLine
, ViewboxPolyline
and ViewboxPolygon
, and they are used like this:
我的类是ViewboxPath,ViewboxLine,ViewboxPolyline和ViewboxPolygon,它们的用法如下:
<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
.
如您所见,我的Viewbox形状类与普通形状(折线,多边形,路径和线)一样使用,除了额外的Viewbox参数,以及它们默认为Stretch =“Fill”的事实。 Viewbox参数在用于指定形状的坐标系中指定应使用Fill,Uniform或UniformToFill设置拉伸的几何体区域,而不是使用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:
以下是我的Viewbox形状类的实际代码,包括包含常用功能的抽象基类ViewboxShape:
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!
#1
3
I had similar problem because I couldn't find an easy way to scale multiple shapes. Ended up using DrawingGroup
with multiple GeometryDrawing
inside. So they scale together. Here are your graphs with this approach. Looks bulky but should work fast. Plus you'll most likely populate line segments from code:
我有类似的问题,因为我找不到一种简单的方法来缩放多个形状。使用带有多个GeometryDrawing的DrawingGroup结束。所以他们一起扩展。以下是使用此方法的图表。看起来很笨,但应该快速工作。另外,您最有可能从代码中填充线段:
<Window x:Class="Polyline.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="100" Width="300">
<Grid>
<Image>
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="Transparent">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,1,1">
<RectangleGeometry.Transform>
<ScaleTransform ScaleX="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=Grid}}"
ScaleY="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType=Grid}}"/>
</RectangleGeometry.Transform>
</RectangleGeometry>
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing>
<GeometryDrawing.Pen>
<Pen Brush="Blue" Thickness="1"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Geometry>
<PathGeometry>
<PathGeometry.Transform>
<ScaleTransform ScaleX="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=Grid}}"
ScaleY="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType=Grid}}"/>
</PathGeometry.Transform>
<PathGeometry.Figures>
<PathFigure StartPoint="0,0">
<PathFigure.Segments>
<LineSegment Point="0.2,0"/>
<LineSegment Point="0.2,0.363636363636364"/>
<LineSegment Point="0.4,0.363636363636364"/>
<LineSegment Point="0.4,0.636363636363636"/>
<LineSegment Point="0.6,0.636363636363636"/>
<LineSegment Point="0.6,0.0909090909090909"/>
<LineSegment Point="0.8,0.0909090909090909"/>
<LineSegment Point="0.8,0"/>
<LineSegment Point="1,0"/>
</PathFigure.Segments>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing>
<GeometryDrawing.Pen>
<Pen Brush="Red" Thickness="1"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Geometry>
<PathGeometry>
<PathGeometry.Transform>
<ScaleTransform ScaleX="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=Grid}}"
ScaleY="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType=Grid}}"/>
</PathGeometry.Transform>
<PathGeometry.Figures>
<PathFigure StartPoint="0,0.363636363636364">
<PathFigure.Segments>
<LineSegment Point="0.2,0.363636363636364"/>
<LineSegment Point="0.2,0.727272727272727"/>
<LineSegment Point="0.4,0.727272727272727"/>
<LineSegment Point="0.4,1"/>
<LineSegment Point="0.6,1"/>
<LineSegment Point="0.6,0.454545454545455"/>
<LineSegment Point="0.8,0.454545454545455"/>
<LineSegment Point="0.8,0.363636363636364"/>
<LineSegment Point="1,0.363636363636364"/>
</PathFigure.Segments>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</Grid>
</Window>
You can remove first RectangleGeometry
if you don't need graphs always scale between 0 and 1.
如果不需要图形始终在0和1之间缩放,则可以删除第一个RectangleGeometry。
#2
3
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.
我不久前遇到了这个问题。当时我发现解决方案repka提出但不满意,因为它相对复杂,并没有我想要的那么高效。
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.
我通过编写一组简单的Viewbox形状类来解决这个问题,这些类的工作方式与内置的Path,Line,Polyline和Polygon类完全相同,只不过它们可以让您轻松地按照您想要的方式工作。
My classes are ViewboxPath
, ViewboxLine
, ViewboxPolyline
and ViewboxPolygon
, and they are used like this:
我的类是ViewboxPath,ViewboxLine,ViewboxPolyline和ViewboxPolygon,它们的用法如下:
<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
.
如您所见,我的Viewbox形状类与普通形状(折线,多边形,路径和线)一样使用,除了额外的Viewbox参数,以及它们默认为Stretch =“Fill”的事实。 Viewbox参数在用于指定形状的坐标系中指定应使用Fill,Uniform或UniformToFill设置拉伸的几何体区域,而不是使用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:
以下是我的Viewbox形状类的实际代码,包括包含常用功能的抽象基类ViewboxShape:
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!