Monday, 10 May 2010

Drawing WPF Curves with Arrow heads

Having drawn the shapes, the curves and found their intersection point, I finally needed to draw an arrow head. In order to draw the arrow at the right angle we need to know the tangent of the Bezier curve at the intersection point. Since that is beyond my maths capability I chose to find the intersection point between the line and an imaginary, slightly larger, rectangle that surrounds the target shape. The difference between the two intersection points provides the required angle for the arrow head.
private static void DrawArrowHead(Canvas canvas, PathGeometry linePath, Rect shapeRect, Color color)
{
// Get the intersection point of the imaginary, slightly
// larger rectangle that surrounds the targer shape.
Rect outerRect = new Rect(shapeRect.Left - 10, shapeRect.Top - 10, shapeRect.Width + 20, shapeRect.Height + 20);

RectangleGeometry shapeGeometry = new RectangleGeometry(shapeRect);
Point[] intersectPoints = GetIntersectionPoints(linePath, shapeGeometry);


double innerLeft = intersectPoints[0].X;
double innerTop = intersectPoints[0].Y;


shapeGeometry = new RectangleGeometry(outerRect);
intersectPoints = GetIntersectionPoints(linePath, shapeGeometry);


double outerLeft = intersectPoints[0].X;
double outerTop = intersectPoints[0].Y;


Polygon arrowHead = new Polygon();
arrowHead.Points = new PointCollection();
arrowHead.Points.Add(new Point(innerLeft, innerTop));
arrowHead.Points.Add(new Point(innerLeft + 10, innerTop + 5));
arrowHead.Points.Add(new Point(innerLeft + 10, innerTop - 5));
arrowHead.Points.Add(new Point(innerLeft, innerTop));
arrowHead.Stroke = new SolidColorBrush(color);
arrowHead.Fill = new SolidColorBrush(color);


// The differences between the intersection points on
// the inner and outer shapes gives us the base and
// perpendicular of the right-angled triangle
double baseSize = innerLeft - outerLeft;
double perpSize = innerTop - outerTop;
// Calculate the angle in degrees using ATan
double angle = Math.Atan(perpSize / baseSize) * 180 / Math.PI;


// Rotate another 180 degrees for lines in the 3rd & 4th quadrants
if (baseSize >= 0) angle += 180;


// Apply the rotation to the arrow head
RotateTransform rt = new RotateTransform(angle, innerLeft, innerTop);
arrowHead.RenderTransform = rt;


// Arrow heads are drawn over the lines but
// under the shapes
Canvas.SetZIndex(arrowHead, (int)Layer.Arrow);


canvas.Children.Add(arrowHead);
}

Drawing WPF Bezier Curves programmatically

I needed to draw a curved line between the mid-points of two shapes, centered on X1,Y1 and X2, Y2.
This method also returns a PathGeometry which is then used to determine the Intersection between the curve and the shapes.
private static PathGeometry DrawLine(Canvas canvas, double X1, double Y1, double X2, double Y2, Color color)
{
QuadraticBezierSegment qbs = new QuadraticBezierSegment(new Point(X2, Y1), new Point(X2, Y2), true);

PathSegmentCollection pscollection = new PathSegmentCollection();
pscollection.Add(qbs);

PathFigure pf = new PathFigure();
pf.Segments = pscollection;
pf.StartPoint = new Point(X1, Y1);

PathFigureCollection pfcollection = new PathFigureCollection();
pfcollection.Add(pf);

PathGeometry pathGeometry = new PathGeometry();
pathGeometry.Figures = pfcollection;

Path path = new Path();
path.Data = pathGeometry;
path.Stroke = new SolidColorBrush(color);
path.StrokeThickness = 2;
Canvas.SetZIndex(path, (int)Layer.Line);
canvas.Children.Add(path);

return pathGeometry;
}

Intersection of two WPF geometries

I needed to determine the intersection point of a Rectangle and a Bezier curve. I found the following code here.

I've included the code below in case the link is lost in the future.
public static Point[] GetIntersectionPoints(Geometry g1, Geometry g2)
{
Geometry og1 = g1.GetWidenedPathGeometry(new Pen(Brushes.Black, 1.0));
Geometry og2 = g2.GetWidenedPathGeometry(new Pen(Brushes.Black, 1.0));

CombinedGeometry cg = new CombinedGeometry(GeometryCombineMode.Intersect, og1, og2);
PathGeometry pg = cg.GetFlattenedPathGeometry();
Point[] result = new Point[pg.Figures.Count];

for (int i = 0; i < pg.Figures.Count; i++)
{
Rect fig = new PathGeometry(new PathFigure[] { pg.Figures[i] }).Bounds;
result[i] = new Point(fig.Left + fig.Width / 2.0, fig.Top + fig.Height / 2.0);
}
return result;
}

Canvas

I think this is about the minimum you need to draw a shape on a Canvas programmatically.
<Window x:Class="DrawStateMachineMap.Window2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window2" Height="300" Width="300">
<Grid>
<Canvas Name="MainCanvas"></Canvas>
</Grid>
</Window>

      public Window2()
{
InitializeComponent();

Rectangle aRectangle = new Rectangle();
aRectangle.Width = 100;
aRectangle.Height = 50;
aRectangle.Stroke = Brushes.Red;

Canvas.SetLeft(aRectangle, 10);
Canvas.SetTop(aRectangle, 15);

MainCanvas.Children.Add(aRectangle);
}

Saturday, 8 May 2010

Loading XML documents stored in the Output directory

I wanted to load an XML document that is distributed with my project.

// Load the XML State Transition Map
XmlDocument xmlStateMap = new XmlDocument();
xmlStateMap.Load(stateMapName);

I set the Copy to Output Directory property of the XML file to "Copy always". However the file never appeared in bin\debug. I also had to change the Build Action property from the default value of "Resource" to "Content" .