Search code examples
c#winformsgraphicsposition

Draw an arrow between 2 positions in an interval of 4 seconds in c#


I want to make an arrow in c#, which goes from A position to B position in 5 seconds for example. I want to put a map image in the form and when i click on a button i want to draw an arrow from A position to B position in an interval of seconds. i have made an arrow when it is in a horizontal position, but when i try to make it oblique it draws me a triangle instead of an arrow and i don't know how to fix it. here i made an arrow from a position 12 with a width of 300

and i try to make the same with an oblique arrow but when i put different positions it draws me a triangle not an arrow.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Drawing.Drawing2D;

namespace WindowsFormsApp4
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
            this.ClientSize = new System.Drawing.Size(400, 273);
            this.Text = "";
            this.Resize += new System.EventHandler(this.Form1_Resize);
            this.Paint += new System.Windows.Forms.PaintEventHandler(this.Form1_Paint);
        }

        private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
        {
            Graphics g = e.Graphics;
            g.SmoothingMode = SmoothingMode.AntiAlias;
            g.FillRectangle(Brushes.White, this.ClientRectangle);

            Pen p = new Pen(Color.Black, 5);
            p.StartCap = LineCap.Round;
            for(int i=1; i<=300;i++)
            {
                System.Threading.Thread.Sleep(2);
                g.DrawLine(p, 12, 30, i, 30);
                Cursor.Current = Cursors.Default;
            }
            p.EndCap = LineCap.ArrowAnchor;
            g.DrawLine(p, 12, 30, 310, 30);
            p.Dispose();
        }

        private void Form1_Resize(object sender, System.EventArgs e)
        {
            Invalidate();
        }
    }
}

Solution

  • The fundamental problem with your code is that you are doing the entire animation loop inside the Paint event handler. This means that the window is never clear out between each line you draw, so you get all of the copies of the line you're drawing, start to finish, laid on top of each other in the same view.

    It is not clear from your question exactly what you expect to see on the screen. However, another potential problem with your code is that the moving end point of the line does not start at the start point of the line, but rather at a point with the same Y coordinate where you want the line to end. This means that the arrow end of the line traverses a horizontal line leading to the final end point, rather than gradually extending from the start point of the line.

    There is also the minor point that you seem to be confused about what the DrawLine() method does. You state that the width of your line is 300, but in fact the second argument of the DrawLine() method is just another point. The width of the line is defined by the Pen you use to draw the line. The width of the box containing the line is defined by the start and end point, but in this case is not 300, but rather (at the final length of the line) the difference between your start X coordinate and end X coordinate (i.e. 288).

    The fundamental problem described above can be addressed by running a loop outside of the Paint event handler, which updates values that describe the line, and then call Invalidate() so that the Paint event handler can be called to draw just the current state of the animation.

    On the assumption that what you really wanted was for a line to extend out from the start point, rather than traverse a horizontal line, the example I show below implements the animation that way as well, in addition to fixing the fundamental issue. I did nothing to change the length or width of the line.

    public Form1()
    {
        InitializeComponent();
    
        this.DoubleBuffered = true;
        this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
        this.ClientSize = new System.Drawing.Size(400, 273);
        this.Resize += new System.EventHandler(this.Form1_Resize);
        this.Paint += new System.Windows.Forms.PaintEventHandler(this.Form1_Paint);
    
        var task = AnimateLine();
    }
    
    private readonly Point _lineStart = new Point(12, 30);
    private readonly Point _lineFinalEnd = new Point(300, 60);
    private const int _animateSteps = 300;
    
    private Point _lineCurrentEnd;
    private bool _drawArrow;
    
    private async Task AnimateLine()
    {
        Size size = new Size(_lineFinalEnd) - new Size(_lineStart);
    
        for (int i = 1; i <= _animateSteps; i++)
        {
            await Task.Delay(2);
            Size currentSize = new Size(
                size.Width * i / _animateSteps, size.Height * i / _animateSteps);
    
            _lineCurrentEnd = _lineStart + currentSize;
            Invalidate();
        }
        _drawArrow = true;
        Invalidate();
    }
    
    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
    {
        Graphics g = e.Graphics;
        g.SmoothingMode = SmoothingMode.AntiAlias;
    
        using (Pen p = new Pen(Color.Black, 5))
        {
            p.StartCap = LineCap.Round;
            p.EndCap = _drawArrow ? LineCap.ArrowAnchor : p.EndCap;
            g.DrawLine(p, _lineStart, _lineCurrentEnd);
        }
    }
    

    Note that the repeated erasing and redrawing of the window would make the window flicker. This is a basic issue with any sort of animation, and the fix is to enable double-buffering for the window, hence the this.DoubleBuffered = true; statement added to the constructor.

    Some other points worth mentioning:

    • The await Task.Delay() call is used so that the loop can yield the UI thread with each iteration of the loop, which allows the UI thread to raise the Paint event, as well as allows any other UI activity to still work during the animation. You can find lots more information about that C# feature in the How and When to use async and await article, and of course by reading the documentation.
    • Whether you use Thread.Sleep() or Task.Delay(), specifying a delay of 2 ms isn't very useful. The Windows thread scheduler does not schedule threads to that degree of precision. A thread that sleeps for 2 ms could be woken up as much as 50 ms later in the normal case, and even later if the CPU is under heavy load. Nor does a 2 ms delay provide a useful animation frame rate; that would be a 500 Hz refresh rate, which is easily 10x or more faster than the human brain needs in order to perceive a smooth animation.

      My example above does nothing to try to address this issue, but you should explore implementing the loop slightly differently, such that instead of the number of animation steps, you specify a reasonable animation interval (say, every 50 or 100 ms), make an attempt to delay that interval, but then use a Stopwatch to actually measure what the real delay was and compute the progress within the animation based on the actual time elapsed. This will allow you to have precise control over the total duration of the animation, as well as somewhat precise control over the refresh rate used for the animation.