WPF Polyline Relative Point Values ​​and Graphic Stretch

I am trying to create a fairly simple graphic component consisting of a series of polylines in the same grid cell that represent the lines of the graph. My strategy is to look at all the points in my set, determine the minutes and max, and then calculate a number from 0 to 1, respectively, 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 fact the polyline is stretched vertically to fill the entire cell depending on what is the value of min and max Y. For example. 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 from the bottom to the bottom, not 0.5 in the center and .7 7/10 to the bottom.

Here is a simple example with two polylines and computed points between 0 and 1. You will notice that the red polyline is directly on top of the blue, even if the red Y is greater. The red polyline should look the same as the blue one, but orient itself a little lower in the cell. However, it stretches to fill the entire cell so that it sits right on a blue background.

<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 use values ​​from 0 to 1 is because I want the mesh width and height to be easily changed, for example. through a slider or something to adjust the height of the chart, or drag the window wide to adjust the width. So I tried to use this stretch strategy to achieve this instead of calculating pixel values ​​without stretching.

Any tips on how to achieve this?

Thanks.

+4
source share
2 answers

I had a similar problem because I could not find an easy way to scale multiple shapes. Finished with a DrawingGroup with several GeometryDrawing inside. Therefore, they scale together. Here are your graphs with this approach. It looks bulky, but should work quickly. In addition, you will most likely fill out the line segments from the code:

 <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 the first RectangleGeometry if you do not need graphs that always scale from 0 to 1.

+3
source

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!

+3
source

All Articles