views:

1805

answers:

3

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.

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.

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

Any advice on how to achieve this?

Thanks.

+2  A: 

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:

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

repka
Works perfectly, thanks! I'm actually leaning towards using the WPF Toolkit charting features now, but this is good to know with the GeometryDrawing for the future. Thanks.
Derek
Or you could check out my Viewbox shape classes (see the code below)
Ray Burns
+2  A: 

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!

Ray Burns
Cheers, bice solution! One question, though: A large part of the ViewboxShape code concerns the Pen property. Why did you choose to overwrite that much of the Pen handling? Expression Blend does not seem to like that, complaining "'Pen' property was already registred by Pen".
Jens
You found a bug in my property registration code: I should have registered the Pen property to the ViewboxShape class. Fixed. As for the amount of code: The Pen property itself only adds 6 lines of code. The rest of the pen-related code is to support properties such as "Stroke", "StrokeThickness", etc that are defined on the base Shape class. Supporting these properties correctly and efficiently requires code to construct and cache a pen and code to detect changes to these properties and update the cached pen.
Ray Burns
A: 

hi I like your solution, Ray Burns, but I can't get it to work :-( I copied the code and added your examples in my UserControl (inside a canvas). The code itself compiles without errors. But when the designer loads, there is an exception saying that "DesiredSize" should not be "PositiveInfinity". I'm rather new to WPF so I'm a bit helpless here. Do you have any idea where this error might come from, or what I have to add to my code to make your example work? thx

sth_Weird
The Viewbox Shape Classes have no "inherent" size and will expand to fill the space you give them. A canvas gives each child infinite space. Since no control is allowed to be infinitely large, to use my Viewbox Shape Classes with a canvas you must explicitly specify their size. Side note: Canvas is usually the worst choice of layout in WPF.
Ray Burns