Click here to Skip to main content
15,896,606 members
Articles / Programming Languages / C#

Draw Straight and Curved Arrows

Rate me:
Please Sign up or sign in to vote.
4.88/5 (30 votes)
30 Jan 2007CPOL2 min read 100.6K   7.1K   93  
Render custom arrows on any graphics
using System;
using System.Drawing;
using System.Xml.Serialization;

namespace Arrows
{
    [Serializable, XmlRoot("Arrow", Namespace = "", IsNullable = false)]
    public class ArrowRenderer
    {
        #region Properties

        private float width = 15;
        /// <summary>
        /// Gets or sets width (in pixels) of the full base of the arrowhead
        /// </summary>
        /// <value>The width.</value>
        public float Width
        {
            get { return width; }
            set { width = value; }
        }

        private float theta = 0.5f;
        /// <summary>
        /// Gets or sets angle (in radians) at the arrow tip between the two sides of the arrowhead
        /// </summary>
        /// <value>The theta.</value>
        public float Theta
        {
            get { return theta; }
            set { theta = value; }
        }

        public void SetThetaInDegrees(float degrees)
        {
            if (degrees <= 0) degrees = 1;
            if (degrees >= 180) degrees = 179;
            theta = (float)Math.PI / 180 * degrees;
        }

        private bool fillArrowHead = true;
        /// <summary>
        /// Gets or sets flag indicating whether or not the arrowhead should be filled
        /// </summary>
        /// <value><c>true</c> if arrow head should be filled; otherwise, <c>false</c>.</value>
        public bool FillArrowHead
        {
            get { return fillArrowHead; }
            set { fillArrowHead = value; }
        }

        private int numberOfBezierCurveNodes = 100;
        /// <summary>
        /// Gets or sets the number of nodes used to calculate bezier curve.
        /// </summary>
        /// <value>The number of bezier curve nodes.</value>
        public int NumberOfBezierCurveNodes
        {
            get { return numberOfBezierCurveNodes; }
            set
            {
                if (value > 4) numberOfBezierCurveNodes = value;
            }
        }

        #endregion

        #region Constructors

        public ArrowRenderer() { }

        public ArrowRenderer(float width)
        {
            this.width = width;
        }

        public ArrowRenderer(float width, float theta)
        {
            this.width = width;
            this.theta = theta;
        }

        public ArrowRenderer(float width, float theta, bool fillArrowHead)
        {
            this.width = width;
            this.theta = theta;
            this.fillArrowHead = fillArrowHead;
        }

        public ArrowRenderer(float width, float theta, bool fillArrowHead, int numberOfBezierCurveNodes)
        {
            this.width = width;
            this.theta = theta;
            this.fillArrowHead = fillArrowHead;
            this.numberOfBezierCurveNodes = numberOfBezierCurveNodes;
        }

        #endregion

        #region DrawArrow

        public void DrawArrow(Graphics g, Pen pen, Brush brush, float x1, float y1, float x2, float y2)
        {
            DrawArrow(g, pen, brush, new PointF(x1, y1), new PointF(x2, y2));
        }

        /// <summary>
        /// Renders the arrow on given graphics using desired paramethers.
        /// </summary>
        /// <param name="g">The grahpics object.</param>
        /// <param name="pen">The pen of the stroke.</param>
        /// <param name="brush">The brush of the fill.</param>
        /// <param name="p1">Initial point.</param>
        /// <param name="p2">Terminal point.</param>
        public void DrawArrow(Graphics g, Pen pen, Brush brush, PointF p1, PointF p2)
        {
            PointF[] aptArrowHead = new PointF[3];

            // set first node to terminal point            
            aptArrowHead[0] = p2;

            Vector vecLine = new Vector(p2.X - p1.X, p2.Y - p1.Y);// build the line vector
            Vector vecLeft = new Vector(-vecLine[1], vecLine[0]);// build the arrow base vector - normal to the line

            // setup remaining arrow head points
            float lineLength = vecLine.Length;
            float th = Width / (2.0f * lineLength);
            float ta = Width / (2.0f * ((float)Math.Tan(Theta / 2.0f)) * lineLength);

            // find the base of the arrow
            PointF pBase = new PointF(aptArrowHead[0].X + -ta * vecLine[0], aptArrowHead[0].Y + -ta * vecLine[1]); //base of the arrow

            // build the points on the sides of the arrow
            aptArrowHead[1] = new PointF(pBase.X + th * vecLeft[0], pBase.Y + th * vecLeft[1]);
            aptArrowHead[2] = new PointF(pBase.X + -th * vecLeft[0], pBase.Y + -th * vecLeft[1]);

            g.DrawLine(pen, p1, pBase); //master line

            if (FillArrowHead) g.FillPolygon(brush, aptArrowHead); //fill arrow head if desired

            g.DrawPolygon(pen, aptArrowHead); //draw outline
        }

        #endregion

        #region DrawArrowOnCurve

        public void DrawArrowOnCurve(Graphics g, Pen pen, Brush brush, float[] controlPoints)
        {
            if (controlPoints.Length != 8)
                throw new ArgumentException("controlPoints has to be valid float array 8 elements in length");
            DrawArrowOnCurve(g, pen, brush, controlPoints[0], controlPoints[1], controlPoints[2], controlPoints[3],
                                            controlPoints[4], controlPoints[5], controlPoints[6], controlPoints[7]);
        }

        /// <summary>
        /// Renders the arrow on given graphics using desired paramethers.
        /// </summary>
        /// <param name="g">The grahpics object.</param>
        /// <param name="pen">The pen of the stroke.</param>
        /// <param name="brush">The brush of the fill.</param>
        /// <param name="p1">Initial point.</param>
        /// <param name="p2">Terminal point.</param>
        /// <param name="p1c">First control point.</param>
        /// <param name="p2c">Second control point.</param>
        public void DrawArrowOnCurve(Graphics g, Pen pen, Brush brush, PointF p1, PointF p2, PointF p1c, PointF p2c)
        {
            DrawArrowOnCurve(g, pen, brush, p1.X, p1.Y, p2.X, p2.Y, p1c.X, p1c.Y, p2c.X, p2c.Y);
        }

        public void DrawArrowOnCurve(Graphics g, Pen pen, Brush brush, float p1X, float p1Y, float p2X, float p2Y, float p1cX, float p1cY, float p2cX, float p2cY)
        {
            if (numberOfBezierCurveNodes < 4) numberOfBezierCurveNodes = 4;
            PointF[] bezierLine = GetBezierCurveNodes(p1X, p1Y, p2X, p2Y, p1cX, p1cY, p2cX, p2cY, numberOfBezierCurveNodes);

            float arrowHeadHeight = width / (2.0f * ((float)Math.Tan(theta / 2.0f)));
            float distDelta = arrowHeadHeight;
            int lineTerminalNode = bezierLine.Length - 2;
            for (int i = bezierLine.Length - 2; i >= 1; i--)
            {
                float currDist = GetDistance(bezierLine[i].X, bezierLine[i].Y, p2X, p2Y);
                float tempDelta = Math.Abs(arrowHeadHeight - currDist);

                if (tempDelta > distDelta) break;
                distDelta = tempDelta;
                lineTerminalNode = i;
            }

            PointF pBase = bezierLine[lineTerminalNode]; //set arrow base node
            arrowHeadHeight = GetDistance(pBase.X, pBase.Y, p2X, p2Y);

            PointF[] aptArrowHead = new PointF[3];
            aptArrowHead[0] = new PointF(p2X, p2Y); // set first node to terminal point

            //Vector vecLine = new Vector(p2.X - pBase.X, p2.Y - pBase.Y); // build the arrow height vector
            //Vector vecLeft = new Vector(pBase.Y - p2.Y, p2.X - pBase.X); // build the arrow base vector - normal to the line

            float th = width / (2.0f * arrowHeadHeight);//coeficient used for remaining arrow head points setup
            // build the points on the remaining sides of the arrow
            aptArrowHead[1] = new PointF(pBase.X + th * (pBase.Y - p2Y), pBase.Y + th * (p2X - pBase.X));
            aptArrowHead[2] = new PointF(pBase.X + th * (p2Y - pBase.Y), pBase.Y + th * (pBase.X - p2X));

            g.DrawCurve(pen, bezierLine, 0, lineTerminalNode); //master line
            if (FillArrowHead) g.FillPolygon(brush, aptArrowHead); //fill arrow head if desired
            g.DrawPolygon(pen, aptArrowHead); //draw outline

            #region Debug - nodes
            /* //visually check position of all nodes
            using (Pen ppp = new Pen(Color.FromArgb(150, Color.Red)))
                for (int i = 0; i < bezierLine.Length; i++)
                {
                    g.DrawEllipse(ppp, bezierLine[i].X - 2, bezierLine[i].Y - 2, 4, 4);
                }*/
            #endregion

            #region Alternate - draw path

            /*using(GraphicsPath gp = new GraphicsPath())
            {
                //gp.FillMode = FillMode.Alternate;
                gp.AddCurve(bezierLine, 0, lineTerminalNode, 1); //master line
                gp.AddPolygon(aptArrowHead); //outline

                if (FillArrowHead) 
                    g.FillPath(brush, gp); //fill arrow head if desired
                g.DrawPath(pen, gp);
            }*/

            #endregion
        }

        private PointF[] GetBezierCurveNodes(PointF p1, PointF p2, PointF p1c, PointF p2c, int numberOfNodes)
        {
            return GetBezierCurveNodes(p1.X, p1.Y, p2.X, p2.Y, p1c.X, p1c.Y, p2c.X, p2c.Y, numberOfNodes);
        }

        protected PointF[] GetBezierCurveNodes(float p1X, float p1Y, float p2X, float p2Y, float p1cX, float p1cY, float p2cX, float p2cY, int numberOfNodes)
        {
            PointF[] apt = new PointF[numberOfNodes];

            float dt = 1f / (apt.Length - 1);
            float t = -dt;
            for (int i = 0; i < apt.Length; i++)
            {
                t += dt;
                float tt = t * t;
                float ttt = tt * t;
                float tt1 = (1 - t) * (1 - t);
                float ttt1 = tt1 * (1 - t);

                float x = ttt1 * p1X +
                          3 * t * tt1 * p1cX +
                          3 * tt * (1 - t) * p2cX +
                          ttt * p2X;

                float y = ttt1 * p1Y +
                          3 * t * tt1 * p1cY +
                          3 * tt * (1 - t) * p2cY +
                          ttt * p2Y;

                apt[i] = new PointF(x, y);
            }
            return apt;
        }

        /// <summary>
        /// Calculate Euclieden distance between two points.
        /// </summary>
        /// <param name="x1">The first point x coordinate.</param>
        /// <param name="y1">The first point y coordinate</param>
        /// <param name="x2">The second point x coordinate</param>
        /// <param name="y2">The second point y coordinate</param>
        /// <returns></returns>
        protected float GetDistance(float x1, float y1, float x2, float y2)
        {
            return (float)Math.Sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
        }

        #endregion

        #region DrawArrows

        public void DrawArrows(Graphics g, Pen pen, Brush brush, PointF[] apf)
        {
            DrawArrows(g, pen, brush, apf, 0, 0);
        }

        public void DrawArrows(Graphics g, Pen pen, Brush brush, PointF[] apf, float startOffset, float endOffset)
        {
            if (apf.Length < 2) throw new ArgumentException("PointF[] apf has to be at least 2 in length");
            PointF[] newApf = new PointF[apf.Length + 1];
            Array.Copy(apf, newApf, apf.Length);
            newApf[newApf.Length - 1] = newApf[0];

            PointF fromP = newApf[0], toP;
            for (int i = 0; i < newApf.Length - 1; i++)
            {
                toP = newApf[i + 1];
                Vector vecLine = new Vector(toP.X - fromP.X, toP.Y - fromP.Y);
                Vector vecStart = vecLine - (vecLine.Length - startOffset);
                PointF nFromP = new PointF(fromP.X + vecStart.X, fromP.Y + vecStart.Y);
                Vector vecEnd = vecLine - endOffset;
                PointF nToP = new PointF(fromP.X + vecEnd.X, fromP.Y + vecEnd.Y);
                DrawArrow(g, pen, brush, nFromP, nToP);
                fromP = toP;
            }
        }

        /// <summary>
        /// Draws arrows between consecutive points.
        /// </summary>
        /// <param name="g">The graphics object.</param>
        /// <param name="pen">The pen of the stroke.</param>
        /// <param name="brush">The brush of the fill.</param>
        /// <param name="apf">Array of points at which arrow polygon will be spanned.</param>
        /// <param name="startOffsets">The start offsets.</param>
        /// <param name="endOffsets">The end offsets.</param>
        public void DrawArrows(Graphics g, Pen pen, Brush brush, PointF[] apf, float[] startOffsets, float[] endOffsets)
        {
            if (apf.Length < 2) throw new ArgumentException("PointF[] apf has to be at least 2 in length");
            if (apf.Length != startOffsets.Length || apf.Length != endOffsets.Length ||
                endOffsets.Length != startOffsets.Length)
                throw new ArgumentException("apf, startOffsets and endOffsets have to be arrays of equal length");

            PointF[] newApf = new PointF[apf.Length + 1];
            Array.Copy(apf, newApf, apf.Length);
            newApf[newApf.Length - 1] = newApf[0];

            PointF fromP = newApf[0], toP;
            for (int i = 0; i < newApf.Length - 1; i++)
            {
                toP = newApf[i + 1];
                Vector vecLine = new Vector(toP.X - fromP.X, toP.Y - fromP.Y);
                Vector vecStart = vecLine - (vecLine.Length - startOffsets[i]);
                PointF nFromP = new PointF(fromP.X + vecStart.X, fromP.Y + vecStart.Y);
                Vector vecEnd = vecLine - endOffsets[i];
                PointF nToP = new PointF(fromP.X + vecEnd.X, fromP.Y + vecEnd.Y);
                DrawArrow(g, pen, brush, nFromP, nToP);
                fromP = toP;
            }
        }

        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Nokia Siemens Networks
Poland Poland
Michał is C# and whole .NET enthusiast. He graduated from computer science MSc studies at Wroclaw University of Technology, Poland.

He is interested in photography and diving. He is member of PADI, currently with divemaster certificate.

His favorite movies are Matrix, Amélie(Le Fabuleux Destin d'Amélie Poulain), Stargate SG-1 TV Serie and comedies of Mel Brooks.

Michał lives in Wroclaw, Poland. To contact Michał, email him at michal.brylka[mail-'"at'"-sign]op.pl.

Comments and Discussions