vitkuz573
vitkuz573

Reputation: 113

How to draw an animated rectangle?

How to draw a rectangle that could move to the edge of the PictureBox and when it reached the edge, it would unfold. And there could be several such rectangles

Here is the code of the class with the Draw function (Pike.cs):

using System.Drawing;

namespace course_project
{
    internal class Pike : Fish
    {
        private Point coordinates;
        private Size size = new Size(40, 40);
        private Size speed;

        public Pike(Point data, AquariumForm aquariumForm) : base(Color.Green, aquariumForm)
        {
            Data = data;

            speed = new Size();
        }

        public Pike Next { get; set; }

        public override void Draw(Graphics graphics)
        {
            graphics.FillEllipse(brush, coordinates.X, coordinates.Y, size.Width, size.Height);
        }

        public void UpdateLocation(Rectangle bounds)
        {
            if (!bounds.Contains(coordinates + speed))
            {
                if (coordinates.X + speed.Width < bounds.Left || coordinates.X + speed.Width > bounds.Right - size.Width)
                {
                    speed.Width *= -1;
                }

                if (coordinates.Y + speed.Height < bounds.Top || coordinates.Y + speed.Height > bounds.Bottom - size.Height)
                {
                    speed.Height *= -1;
                }
            }

            coordinates += speed;
        }
    }
}

Linked List (PikeFlock.cs):

using System.Collections;
using System.Collections.Generic;
using System.Drawing;

namespace course_project
{
    internal class PikeFlock : IEnumerable<Pike>
    {
        Pike head;
        Pike tail;
        int count;

        public void Add(Point data, AquariumForm aquariumForm)
        {
            Pike pike = new Pike(data, aquariumForm);

            if (head == null)
                head = pike;
            else
                tail.Next = pike;

            tail = pike;

            count++;
        }

        public bool Remove(Point data)
        {
            Pike current = head;
            Pike previous = null;

            while (current != null)
            {
                if (current.Data.Equals(data))
                {
                    if (previous != null)
                    {
                        previous.Next = current.Next;

                        if (current.Next == null)
                        {
                            tail = previous;
                        }
                    }
                    else
                    {
                        head = head.Next;

                        if (head == null)
                            tail = null;
                    }

                    count--;
                    return true;
                }

                previous = current;
                current = current.Next;
            }

            return false;
        }

        public int Count { get { return count; } }

        public bool IsEmpty { get { return count == 0; } }

        public void Clear()
        {
            head = null;
            tail = null;
            count = 0;
        }

        public bool Contains(Point data)
        {
            Pike current = head;

            while (current != null)
            {
                if (current.Data.Equals(data))
                    return true;

                current = current.Next;
            }

            return false;
        }

        public void AppendFirst(Point data, AquariumForm aquariumForm)
        {
            Pike pike = new Pike(data, aquariumForm)
            {
                Next = head
            };

            head = pike;

            if (count == 0)
            {
                tail = head;
            }

            count++;
        }

        public IEnumerator<Pike> GetEnumerator()
        {
            Pike current = head;

            while (current != null)
            {
                yield return current;
                current = current.Next;
            }
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IEnumerable)this).GetEnumerator();
        }
    }
}

AquariumForm.cs (The form itself):

using System;
using System.Drawing;
using System.Windows.Forms;

namespace course_project
{
    public partial class AquariumForm : Form
    {
        readonly Aquarium aquarium;

        public AquariumForm()
        {
            InitializeComponent();

            DoubleBuffered = true;

            aquarium = new Aquarium(ClientRectangle);
            aquarium_timer.Interval = 100;
            aquarium_timer.Tick += Timer_Tick;
            aquarium_timer.Enabled = true;
        }

        private void Timer_Tick(object sender, EventArgs e)
        {
            foreach (Pike pike in aquarium.pikeFlock)
            {
                pike.UpdateLocation(ClientRectangle);
            }

            foreach (Carp carp in aquarium.carpFlock)
            {
                carp.UpdateLocation(ClientRectangle);
            }

            Invalidate();
        }

        private void Add_carp_button_Click(object sender, EventArgs e)
        {
            aquarium.carpFlock.Add(new Point(), this);
        }

        private void Add_pike_button_Click(object sender, EventArgs e)
        {
            aquarium.pikeFlock.Add(new Point(), this);
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            aquarium.Paint(e.Graphics);
        }
    }
}

Aquarium.cs:

using System.Drawing;

namespace course_project
{
    internal class Aquarium
    {
        readonly public PikeFlock pikeFlock;
        readonly public CarpFlock carpFlock;
        readonly Color waterColor;

        public Aquarium(Rectangle clientRectangle)
        {
            waterColor = Color.LightSkyBlue;
            pikeFlock = new PikeFlock();
            carpFlock = new CarpFlock();
        }

        public void Paint(Graphics graphics)
        {
            graphics.Clear(waterColor);

            foreach (Pike pike in pikeFlock)
                pike.Draw(graphics);

            foreach (Carp carp in carpFlock)
                carp.Draw(graphics);
        }
    }
}

Fish.cs

using System.Drawing;

namespace course_project
{
    internal class Fish
    {
        private protected Brush brush;
        private protected AquariumForm aquarium_form;

        public Fish(Color color, AquariumForm aquariumForm)
        {
            aquarium_form = aquariumForm;

            brush = new SolidBrush(color);
        }

        public virtual void Draw(Graphics graphics) { }

        public Point Data { get; set; }
    }
}

Confused just at the point where you need to animate it all. By clicking the button everything is created as it should be, but animation does not work, even I do not know how :(

Upvotes: 1

Views: 208

Answers (1)

Gy&#246;rgy Kőszeg
Gy&#246;rgy Kőszeg

Reputation: 18013

You didn't post fish.cs so without testing I would suggest the following changes:

  • Do not use PictureBox, it just complicates things if you perform custom animations this way. So instead of extracting the Graphics from its assigned Image property, which is backed by a Bitmap that has the same size as your form (btw. what happens if you resize your form?) simply just use the Graphics that is already there when your form is redrawn.
  • Unlike PictureBox, a Form does not use double buffering by default so you might want to set DoubleBuffered = true in the form constructor to avoid flickering.
  • Override OnPaint in the form and initiate the whole repaint session from there:
protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);
    aquarium.Paint(e.Graphics); // instead of your Init() method
}
  • It means you don't need bitmap, graphics and aquarium_form fields in your Aquarium class. Instead, you can pass the form's Graphics to a Paint method:
// in Aquarium.cs:
public void Paint(Graphics graphics)
{
    // Please note that graphics can have any size now so it works even if you
    // resize the form. Please also note that I treat pikeFlock as a Pike
    // enumeration instead of just coordinates
    g.Clear(waterColor);
    foreach (Pike pike in pikeFlock) // instead of int[] items
        pike.Draw(graphics); // instead of Draw(int[])
}
  • Your PikeFlock is just IEnumerable, I would change it to IEnumerable<Pike> to make it strongly typed.
  • It also means that PikeFlock.GetEnumerator should yield Pike instances instead of int arrays: yield return current instead of current.Data
  • Pike has now int[] as coordinates. I would change it to Point. And do not pass new random coordinates in every drawing iteration because every fish will just randomly 'teleport' here and there. Instead, maintain the current horizontal and vertical speed.
// in Pike.cs:
private Point coordinates;
private Size size = new Size(40, 40);

// speed is declared as Size because there is an operator overload for Point + Size
// Do not forget to initialize speed in constructor
private Size speed; 

public override void Draw(Graphics graphics)
{
    // no Refresh is needed because graphics comes from the form's OnPaint now
    graphics.FillEllipse(brush, coordinates.X, coordinates.Y, size.Width, size.Height);
}

public void UpdateLocation(Rectangle bounds)
{
    // if a fish would go out of bounds invert its vertical or horizontal speed
    // TODO: if you shrink the form an excluded fish will never come back
    // because its direction will oscillate until you enlarge the form again.
    if (!bounds.Contains(coordinates + speed))
    {
        if (coordinates.X + speed.Width < bounds.Left
            || coordinates.X + speed.Width > bounds.Right - size.Width)
        {
            speed.Width *= -1;
        }
        if (coordinates.Y + speed.Height < bounds.Top
            || coordinates.Y + speed.Height > bounds.Bottom - size.Height)
        {
            speed.Height *= -1;
        }
    }

    coordinates += speed;
}


  • Now the only thing is missing is the animation itself. You can add a Timer component to the form from the Toolbox.
// form:
public AquariumForm()
{
    InitializeComponent();
    DoubleBuffered = true;

    // No need to pass the Form anymore. Pass the bounds instead to
    // generate the initial coordinates within the correct range.
    // No Random field is needed here, use one in Aquarium constructor only.
    aquarium = new Aquarium(ClientRectangle);
    timer.Interval = 100;
    timer.Tick += Timer_Tick;
    timer.Enabled = true;
}

private void Timer_Tick(object sender, EventArgs e)
{
    // Updating coordinates of all fish. We just pass the current bounds.
    foreach (Pike pike in aquarium.pikeFlock)
        pike.UpdateLocation(ClientRectangle);

    // Invalidating the form's graphics so a repaint will be automatically
    // called after processing the pending events.
    Invalidate();
}

Upvotes: 2

Related Questions