Creating a Shape Editor in .NET

Introduction

Drawing applications have always had a soft place in my heart. In the past, I have written a lot about drawing and creating funky stuff with shapes and the .NET Framework’s System.Drawing namespace. Today is not much different. Today, you will learn how to create an editable shape. This shape will be able to be moved around, resized, have some Properties set, as well as have a snap line between shapes.

There is a lot of work, so let’s get started.

Start Visual Studio and create a new C# or VB.NET Windows Forms application. You do not need to design anything because the design will take place mostly through code. Add a Class Library project to your Windows Forms project by selecting File, Add, New Project.

You may name your projects and classes anything you like, but keep in mind that I may have named mine differently.

Add a new Class to your Class Library project. This class will be used to handle the Shape’s events and settable properties. These properties will be set through a Properties Window built into the control. The Properties include:

  • BackColor
  • Name
  • Size

The events include Resizing events as well as moving events. Add the complete Class now:

C#

   public abstract class shpShape
   {
      protected shpShape() : this(Point.Empty)
      {

      }

      protected shpShape(Point loc)
      {

         this.MinimumSize = new Size(50, 50);
         this.Bounds = new Rectangle(loc, this.DefaultSize);
         this.BackColor = Color.White;
         this.Locked = false;

      }

      public enum HitStatus
      {

         None,
         Drag,
         ResizeTopLeft,
         ResizeTopRight,
         ResizeBottomLeft,
         ResizeBottomRight,
         ResizeLeft,
         ResizeTop,
         ResizeRight,
         ResizeBottom

      }

      public event EventHandler LocationChanged;
      public event EventHandler SizeChanged;
      public event EventHandler AppearanceChanged;

      protected virtual void OnLocationChanged(EventArgs e)
      {

         if (LocationChanged != null)
            this.LocationChanged(this, e);

      }

      protected virtual void OnSizeChanged(EventArgs e)
      {

         if (this.SizeChanged != null) this.SizeChanged(this, e);

      }

      protected virtual void OnAppearanceChanged(EventArgs e)
      {

         if (this.AppearanceChanged != null)
            this.AppearanceChanged(this, e);

      }
      private string name = String.Empty;
      public string Name
      {

         get { return name; }

         set
         {

            name = value;

         }

      }

      private Rectangle bounds;
      [Browsable(false)]
      public Rectangle Bounds
      {

         get { return bounds; }

         set
         {

            bounds = value;
            this.GrabHandles.SetBounds(value);

         }

      }

      [XmlIgnore]
      public Point Location
      {

         get { return this.Bounds.Location; }

         set
         {

            if (this.Bounds.Location == value) return;
            Rectangle rect = this.Bounds;
            rect.Location = value;
            this.Bounds = rect;
            this.OnLocationChanged(EventArgs.Empty);

         }
      }

      [XmlIgnore]
      public Size Size
      {

         get { return this.Bounds.Size; }

         set
         {

            if (this.Bounds.Size == value) return;
            Rectangle rect = this.Bounds;

            rect.Size = value;
            this.Bounds = rect;
            this.OnSizeChanged(EventArgs.Empty);

         }
      }

      private bool locked;

      public virtual bool Locked
      {

         get { return locked; }

         set
         {

            locked = value;
            this.GrabHandles.Locked = value;

         }
      }

      private shpHandles grabhandles;
      [XmlIgnore]
      internal shpHandles GrabHandles
      {

         get
         {

            if (grabhandles == null) grabhandles = new
                  shpHandles(this);
               return grabhandles;

         }

      }

      private Size minsize;
      [Browsable(false)]
      public Size MinimumSize
      {

         get { return minsize; }

         set
         {

            if (value.Width < 0 || value.Height < 0)

               minsize = value;
         }

      }

      internal Point MoveStart { get; set; }

      [XmlIgnore]
      protected virtual Size DefaultSize
      {

         get { return new Size(150, 150); }

      }

      [XmlIgnore]
      public Color BackColor { get; set; }

      [Browsable(false)]
      public int XmlBackColor
      {

         get { return this.BackColor.ToArgb(); }
         set { this.BackColor = Color.FromArgb(value); }

      }

      [Browsable(false)]
      [XmlIgnore]
      public ContextMenuStrip ContextMenuStrip { get; set; }

      public abstract void Draw(Graphics g);

      internal virtual void DrawGrabHandles(Graphics g, bool first)
      {

         this.GrabHandles.Draw(g, first);

      }

      public void Move(Point newLocation)
      {

         if (this.Locked) return;
         this.Location = newLocation;

      }

      public void Resize(HitStatus hitStatus, int x, int y)
      {

         if (this.Locked) return;

         switch (hitStatus)
         {

            case HitStatus.ResizeBottomLeft:
               this.ResizeBottomLeft(x, y);
               break;

            case HitStatus.ResizeBottomRight:
               this.ResizeBottomRight(x, y);
               break;

            case HitStatus.ResizeTopLeft:
               this.ResizeTopLeft(x, y);
               break;

            case HitStatus.ResizeTopRight:
               this.ResizeTopRight(x, y);
               break;

            case HitStatus.ResizeLeft:
               this.ResizeLeft(x, y);
               break;

            case HitStatus.ResizeRight:
               this.ResizeRight(x, y);
               break;

            case HitStatus.ResizeTop:
               this.ResizeTop(x, y);
               break;

            case HitStatus.ResizeBottom:
               this.ResizeBottom(x, y);
               break;

         }

      }

      private void ResizeBottomLeft(int x, int y)
      {

         Rectangle oldBounds = this.Bounds;
         int newTop = oldBounds.Top;

         int newLeft = x;
         int newWidth = oldBounds.Right - x;
         int newHeight = y - oldBounds.Top;

         if (newWidth < this.MinimumSize.Width)
         {

            newWidth = this.MinimumSize.Width;
            newLeft = oldBounds.Right - newWidth;

         }

         if (newHeight < this.MinimumSize.Height)
         {

            newHeight = this.MinimumSize.Height;

         }

         this.Bounds = new Rectangle(newLeft, newTop, newWidth,
            newHeight);
      }

      private void ResizeBottomRight(int x, int y)
      {

         Rectangle oldBounds = this.Bounds;

         int newTop = oldBounds.Top;
         int newLeft = oldBounds.Left;
         int newWidth = x - newLeft;
         int newHeight = y - oldBounds.Top;


         if (newWidth < this.MinimumSize.Width)
         {

            newWidth = this.MinimumSize.Width;

         }

         if (newHeight < this.MinimumSize.Height)
         {

            newHeight = this.MinimumSize.Height;

         }

         this.Bounds = new Rectangle(newLeft, newTop, newWidth,
            newHeight);

      }

      private void ResizeTopLeft(int x, int y)
      {

         Rectangle oldBounds = this.Bounds;

         int newTop = y;
         int newLeft = x;
         int newWidth = oldBounds.Right - x;
         int newHeight = oldBounds.Bottom - y;

         if (newWidth < this.MinimumSize.Width)
         {

            newWidth = this.MinimumSize.Width;
            newLeft = oldBounds.Right - newWidth;

         }

         if (newHeight < this.MinimumSize.Height)
         {

            newHeight = this.MinimumSize.Height;
            newTop = oldBounds.Bottom - newHeight;

         }

         this.Bounds = new Rectangle(newLeft, newTop, newWidth,
            newHeight);

      }

      private void ResizeTopRight(int x, int y)
      {

         Rectangle oldBounds = this.Bounds;

         int newTop = y;
         int newLeft = oldBounds.Left;
         int newWidth = x - newLeft;
         int newHeight = oldBounds.Bottom - y;

         if (newWidth < this.MinimumSize.Width)
         {

            newWidth = this.MinimumSize.Width;

         }
         if (newHeight < this.MinimumSize.Height)
         {

            newHeight = this.MinimumSize.Height;
            newTop = oldBounds.Bottom - newHeight;

         }

         this.Bounds = new Rectangle(newLeft, newTop, newWidth,
            newHeight);
      }

      private void ResizeTop(int x, int y)
      {

         Rectangle oldBounds = this.Bounds;

         int newTop = y;
         int newLeft = oldBounds.Left;
         int newWidth = oldBounds.Width;
         int newHeight = oldBounds.Bottom - y;

         if (newHeight < this.MinimumSize.Height)
         {

            newHeight = this.MinimumSize.Height;
            newTop = oldBounds.Bottom - newHeight;

         }

         this.Bounds = new Rectangle(newLeft, newTop, newWidth,
            newHeight);
      }

      private void ResizeLeft(int x, int y)
      {

         Rectangle oldBounds = this.Bounds;

         int newTop = oldBounds.Top;
         int newLeft = x;
         int newWidth = oldBounds.Right - x;
         int newHeight = oldBounds.Height;

         if (newWidth < this.MinimumSize.Width)
         {

            newWidth = this.MinimumSize.Width;
            newLeft = oldBounds.Right - newWidth;

         }

         this.Bounds = new Rectangle(newLeft, newTop, newWidth,
            newHeight);

      }

      private void ResizeRight(int x, int y)
      {

         Rectangle oldBounds = this.Bounds;

         int newTop = oldBounds.Top;
         int newLeft = oldBounds.Left;
         int newWidth = x - newLeft;
         int newHeight = oldBounds.Height;

         if (newWidth < this.MinimumSize.Width)
         {

            newWidth = this.MinimumSize.Width;

         }

         this.Bounds = new Rectangle(newLeft, newTop, newWidth,
            newHeight);

      }

      private void ResizeBottom(int x, int y)
      {

         Rectangle oldBounds = this.Bounds;

         int newTop = oldBounds.Top;
         int newLeft = oldBounds.Left;
         int newWidth = oldBounds.Width;
         int newHeight = y - oldBounds.Top;

         if (newHeight < this.MinimumSize.Height)
         {

            newHeight = this.MinimumSize.Height;

         }

         this.Bounds = new Rectangle(newLeft, newTop, newWidth,
            newHeight);

      }

      public HitStatus HitTest(Point loc)
      {

         if (this.GrabHandles.TotalBounds.Contains(loc))
         {

            if (this.GrabHandles.TopLeft.Contains(loc))
               return HitStatus.ResizeTopLeft;

            else if (this.GrabHandles.TopRight.Contains(loc))
               return HitStatus.ResizeTopRight;

            else if (this.GrabHandles.BottomLeft.Contains(loc))
               return HitStatus.ResizeBottomLeft;

            else if (this.GrabHandles.BottomRight.Contains(loc))
               return HitStatus.ResizeBottomRight;

            if (Rectangle.Union(this.GrabHandles.TopLeft,
                  this.GrabHandles.TopRight).Contains(loc))
               return HitStatus.ResizeTop;

            else if (Rectangle.Union(this.GrabHandles.TopRight,
                  this.GrabHandles.BottomRight).Contains(loc))
               return HitStatus.ResizeRight;

            else if (Rectangle.Union(this.GrabHandles.BottomRight,
                  this.GrabHandles.BottomLeft).Contains(loc))
               return HitStatus.ResizeBottom;

            else if (Rectangle.Union(this.GrabHandles.BottomLeft,
                  this.GrabHandles.TopLeft).Contains(loc))
               return HitStatus.ResizeLeft;

            return HitStatus.Drag;

         }

         else
         {

            return HitStatus.None;

         }
      }

      public virtual string ShapeName()
      {

         return this.GetType().Name;

      }

   }

VB.NET

Imports System
Imports System.ComponentModel
Imports System.Drawing
Imports System.Windows.Forms
Imports System.Xml.Serialization
Public MustInherit Class shpShape
   Protected Sub New()
      Me.New(Point.Empty)
   End Sub

   Protected Sub New(ByVal loc As Point)
      Me.MinimumSize = New Size(50, 50)
      Me.bounds = New Rectangle(loc, Me.DefaultSize)
      Me.BackColor = Color.White
      Me.locked = False
   End Sub

   Public Enum HitStatus
      None
      Drag
      ResizeTopLeft
      ResizeTopRight
      ResizeBottomLeft
      ResizeBottomRight
      ResizeLeft
      ResizeTop
      ResizeRight
      ResizeBottom
   End Enum

   Public Event LocationChanged As EventHandler
   Public Event SizeChanged As EventHandler
   Public Event AppearanceChanged As EventHandler

   Protected Overridable Sub OnLocationChanged(ByVal e As _
         EventArgs)
      RaiseEvent LocationChanged(Me, e)
   End Sub

   Protected Overridable Sub OnSizeChanged(ByVal e As EventArgs)
      RaiseEvent SizeChanged(Me, e)
   End Sub

   Protected Overridable Sub OnAppearanceChanged(ByVal e As _
         EventArgs)
      RaiseEvent AppearanceChanged(Me, e)
   End Sub

   Private nme As String = String.Empty

   Public Property Name As String
      Get
         Return nme
      End Get
      Set(ByVal value As String)
         nme = value
      End Set
   End Property

   Private bonds As Rectangle

   <Browsable(False)>
   Public Property Bounds As Rectangle
      Get
         Return bonds
      End Get
      Set(ByVal value As Rectangle)
         bonds = value
         Me.grabhandles.SetBounds(value)
      End Set
   End Property

   <XmlIgnore>
   Public Property Location As Point
      Get
         Return Me.bounds.Location
      End Get
      Set(ByVal value As Point)
         If Me.bounds.Location = value Then Return
         Dim rect As Rectangle = Me.bounds
         rect.Location = value
         Me.bounds = rect
         Me.OnLocationChanged(EventArgs.Empty)
      End Set
   End Property

   <XmlIgnore>
   Public Property Size As Size
      Get
         Return Me.bounds.Size
      End Get
      Set(ByVal value As Size)
         If Me.bounds.Size = value Then Return
         Dim rect As Rectangle = Me.bounds
         rect.Size = value
         Me.bounds = rect
         Me.OnSizeChanged(EventArgs.Empty)
      End Set
   End Property

   Private lcked As Boolean

   Public Overridable Property Locked As Boolean
      Get
         Return lcked
      End Get
      Set(ByVal value As Boolean)
         lcked = value
         Me.grabhandles.Locked = value
      End Set
   End Property

   Private grhandles As shpHandles

   <XmlIgnore>
   Friend ReadOnly Property GrabHandles As shpHandles
      Get
         If grhandles Is Nothing Then grhandles = New shpHandles(Me)
         Return grhandles
      End Get
   End Property
   Private minsize As Size

   <Browsable(False)>
   Public Property MinimumSize As Size
      Get
         Return minsize
      End Get
      Set(ByVal value As Size)
         If value.Width < 0 OrElse value.Height < 0 Then _
            minsize = value
      End Set
   End Property

   Friend Property MoveStart As Point

   <XmlIgnore>
   Protected Overridable ReadOnly Property DefaultSize As Size
      Get
         Return New Size(150, 150)
      End Get
   End Property

   <XmlIgnore>
   Public Property BackColor As Color

   <Browsable(False)>
   Public Property XmlBackColor As Integer
      Get
         Return Me.BackColor.ToArgb()
      End Get
      Set(ByVal value As Integer)
         Me.BackColor = Color.FromArgb(value)
      End Set
   End Property

   <Browsable(False)>
   <XmlIgnore>
   Public Property ContextMenuStrip As ContextMenuStrip
   Public MustOverride Sub Draw(ByVal g As Graphics)

   Friend Overridable Sub DrawGrabHandles(ByVal g As Graphics, _
         ByVal first As Boolean)
      Me.grabhandles.Draw(g, first)
   End Sub

   Public Sub Move(ByVal newLocation As Point)
      If Me.locked Then Return
      Me.Location = newLocation
   End Sub

   Public Sub Resize(ByVal hitStatus As HitStatus, ByVal x _
         As Integer, ByVal y As Integer)
      If Me.locked Then Return

      Select Case hitStatus
         Case HitStatus.ResizeBottomLeft
            Me.ResizeBottomLeft(x, y)
         Case HitStatus.ResizeBottomRight
            Me.ResizeBottomRight(x, y)
         Case HitStatus.ResizeTopLeft
            Me.ResizeTopLeft(x, y)
         Case HitStatus.ResizeTopRight
            Me.ResizeTopRight(x, y)
         Case HitStatus.ResizeLeft
            Me.ResizeLeft(x, y)
         Case HitStatus.ResizeRight
            Me.ResizeRight(x, y)
         Case HitStatus.ResizeTop
            Me.ResizeTop(x, y)
         Case HitStatus.ResizeBottom
            Me.ResizeBottom(x, y)
      End Select
   End Sub

   Private Sub ResizeBottomLeft(ByVal x As Integer, _
         ByVal y As Integer)
      Dim oldBounds As Rectangle = Me.bounds
      Dim newTop As Integer = oldBounds.Top
      Dim newLeft As Integer = x
      Dim newWidth As Integer = oldBounds.Right - x
      Dim newHeight As Integer = y - oldBounds.Top

      If newWidth < Me.MinimumSize.Width Then
         newWidth = Me.MinimumSize.Width
         newLeft = oldBounds.Right - newWidth
      End If

      If newHeight < Me.MinimumSize.Height Then
         newHeight = Me.MinimumSize.Height
      End If

      Me.bounds = New Rectangle(newLeft, newTop, newWidth, _
         newHeight)
   End Sub

   Private Sub ResizeBottomRight(ByVal x As Integer, _
         ByVal y As Integer)
      Dim oldBounds As Rectangle = Me.bounds
      Dim newTop As Integer = oldBounds.Top
      Dim newLeft As Integer = oldBounds.Left
      Dim newWidth As Integer = x - newLeft
      Dim newHeight As Integer = y - oldBounds.Top

      If newWidth < Me.MinimumSize.Width Then
         newWidth = Me.MinimumSize.Width
      End If

      If newHeight < Me.MinimumSize.Height Then
         newHeight = Me.MinimumSize.Height
      End If

      Me.bounds = New Rectangle(newLeft, newTop, newWidth, _
         newHeight)
   End Sub

   Private Sub ResizeTopLeft(ByVal x As Integer, _
         ByVal y As Integer)
      Dim oldBounds As Rectangle = Me.bounds
      Dim newTop As Integer = y
      Dim newLeft As Integer = x
      Dim newWidth As Integer = oldBounds.Right - x
      Dim newHeight As Integer = oldBounds.Bottom - y

      If newWidth < Me.MinimumSize.Width Then
         newWidth = Me.MinimumSize.Width
         newLeft = oldBounds.Right -
      End If

      If newHeight < Me.MinimumSize.Height Then
         newHeight = Me.MinimumSize.Height
         newTop = oldBounds.Bottom - newHeight
      End If

      Me.bounds = New Rectangle(newLeft, newTop, newWidth, _
         newHeight)
   End Sub

   Private Sub ResizeTopRight(ByVal x As Integer, _
         ByVal y As Integer)
      Dim oldBounds As Rectangle = Me.bounds
      Dim newTop As Integer = y
      Dim newLeft As Integer = oldBounds.Left
      Dim newWidth As Integer = x - newLeft
      Dim newHeight As Integer = oldBounds.Bottom - y

      If newWidth < Me.MinimumSize.Width Then
         newWidth = Me.MinimumSize.Width
      End If

      If newHeight < Me.MinimumSize.Height Then
         newHeight = Me.MinimumSize.Height
         newTop = oldBounds.Bottom - newHeight
      End If

      Me.bounds = New Rectangle(newLeft, newTop, newWidth, _
         newHeight)
   End Sub

   Private Sub ResizeTop(ByVal x As Integer, ByVal y As Integer)
      Dim oldBounds As Rectangle = Me.bounds
      Dim newTop As Integer = y
      Dim newLeft As Integer = oldBounds.Left
      Dim newWidth As Integer = oldBounds.Width
      Dim newHeight As Integer = oldBounds.Bottom - y

      If newHeight < Me.MinimumSize.Height Then
         newHeight = Me.MinimumSize.Height
         newTop = oldBounds.Bottom - newHeight
      End If

      Me.bounds = New Rectangle(newLeft, newTop, newWidth, _
         newHeight)
   End Sub    

   Private Sub ResizeLeft(ByVal x As Integer, ByVal y As Integer)
      Dim oldBounds As Rectangle = Me.bounds
      Dim newTop As Integer = oldBounds.Top
      Dim newLeft As Integer = x
      Dim newWidth As Integer = oldBounds.Right - x
      Dim newHeight As Integer = oldBounds.Height

      If newWidth < Me.MinimumSize.Width Then
         newWidth = Me.MinimumSize.Width
         newLeft = oldBounds.Right - newWidth
      End If

      Me.bounds = New Rectangle(newLeft, newTop, newWidth, _
         newHeight)
      End Sub

   Private Sub ResizeRight(ByVal x As Integer, ByVal y As Integer)
      Dim oldBounds As Rectangle = Me.bounds
      Dim newTop As Integer = oldBounds.Top
      Dim newLeft As Integer = oldBounds.Left
      Dim newWidth As Integer = x - newLeft
      Dim newHeight As Integer = oldBounds.Height

      If newWidth < Me.MinimumSize.Width Then
         newWidth = Me.MinimumSize.Width
      End If

      Me.bounds = New Rectangle(newLeft, newTop, newWidth, _
         newHeight)
   End Sub

   Private Sub ResizeBottom(ByVal x As Integer, ByVal y As Integer)
      Dim oldBounds As Rectangle = Me.bounds
      Dim newTop As Integer = oldBounds.Top
      Dim newLeft As Integer = oldBounds.Left
      Dim newWidth As Integer = oldBounds.Width
      Dim newHeight As Integer = y - oldBounds.Top

      If newHeight < Me.MinimumSize.Height Then
         newHeight = Me.MinimumSize.Height
      End If

      Me.bounds = New Rectangle(newLeft, newTop, newWidth,
         newHeight)
   End Sub

   Public Function HitTest(ByVal loc As Point) As HitStatus
      If Me.grabhandles.TotalBounds.Contains(loc) Then

         If Me.grabhandles.TopLeft.Contains(loc) Then
            Return HitStatus.ResizeTopLeft
         ElseIf Me.grabhandles.TopRight.Contains(loc) Then
            Return HitStatus.ResizeTopRight
         ElseIf Me.grabhandles.BottomLeft.Contains(loc) Then
            Return HitStatus.ResizeBottomLeft
         ElseIf Me.grabhandles.BottomRight.Contains(loc) Then
            Return HitStatus.ResizeBottomRight
         End If

         If Rectangle.Union(Me.grabhandles.TopLeft, _
               Me.grabhandles.TopRight).Contains(loc) Then
            Return HitStatus.ResizeTop
         ElseIf Rectangle.Union(Me.grabhandles.TopRight, _
               Me.grabhandles.BottomRight).Contains(loc) Then
            Return HitStatus.ResizeRight
         ElseIf Rectangle.Union(Me.grabhandles.BottomRight, _
               Me.grabhandles.BottomLeft).Contains(loc) Then
            Return HitStatus.ResizeBottom
         ElseIf Rectangle.Union(Me.grabhandles.BottomLeft, _
               Me.grabhandles.TopLeft).Contains(loc) Then
            Return HitStatus.ResizeLeft
         End If

         Return HitStatus.Drag
      Else
         Return HitStatus.None
      End If
   End Function

   Public Overridable Function ShapeName() As String
      Return Me.[GetType]().Name
   End Function
End Class

The above class looks scary because of the size, but it is actually pretty straightforward, as was explained earlier. Luckily, the rest of our work today is a bit less than what we have already done.

Add another class. I have named mine shpHandles. This class will be used to draw grab handles on the shapes. These grab handles then can be clicked and used in the Shape resizing or moving process. Add the code to it:

C#

   public class shpHandles
   {
      public const int Size = 3;

      public shpHandles(shpShape shape)
      {

         this.BorderWidth = 4;
         this.SetBounds(shape.Bounds);

      }

      public Rectangle BorderBounds { get; private set; }
      public int BorderWidth { get; set; }
      public bool Locked { get; set; }

      public Rectangle TotalBounds
      {

         get { return Rectangle.Union(this.TopLeft,
            this.BottomRight); }

      }

      internal Rectangle TopLeft
      {

         get
         {
            return new Rectangle(this.BorderBounds.X - Size,
               this.BorderBounds.Y - Size, 2 * Size + 1,
               2 * Size + 1);

         }

      }

      internal Rectangle TopRight
      {

         get
         {

            return new Rectangle(this.BorderBounds.Right - Size,
               this.BorderBounds.Y - Size,  2 * Size + 1,
               2 * Size + 1);

         }

      }

      internal Rectangle TopMiddle
      {

         get
         {

            return new Rectangle(this.BorderBounds.X +
               this.BorderBounds.Width / 2 - Size,
               this.BorderBounds.Y - Size, 2 * Size + 1,
               2 * Size + 1);

         }

      }

      internal Rectangle MiddleLeft
      {

         get
         {

            return new Rectangle(this.BorderBounds.X - Size,
               this.BorderBounds.Y + this.BorderBounds.Height / 2 -
               Size, 2 * Size + 1, 2 * Size + 1);

         }

      }

      internal Rectangle MiddleRight
      {

         get
         {

            return new Rectangle(this.BorderBounds.Right - Size,
               this.BorderBounds.Y + this.BorderBounds.Height / 2 -
               Size, 2 * Size + 1, 2 * Size + 1);

         }

      }

      internal Rectangle MiddleMiddle
      {

         get
         {

            return new Rectangle(this.BorderBounds.X +
               this.BorderBounds.Width / 2 - Size,
               this.BorderBounds.Y + this.BorderBounds.Height / 2 -
               Size, 2 * Size + 1, 2 * Size + 1);

         }

      }

      internal Rectangle BottomLeft
      {

         get
         {

            return new Rectangle(this.BorderBounds.X - Size,
               this.BorderBounds.Bottom - Size, 2 * Size + 1,
               2 * Size + 1);

         }

      }

      internal Rectangle BottomRight
      {

         get
         {

            return new Rectangle(this.BorderBounds.Right - Size,
               this.BorderBounds.Bottom - Size, 2 * Size + 1,
               2 * Size + 1);

         }

      }

      internal Rectangle BottomMiddle
      {

         get
         {

            return new Rectangle(this.BorderBounds.X +
               this.BorderBounds.Width / 2 - Size,
               this.BorderBounds.Bottom - Size, 2 * Size + 1,
               2 * Size + 1);

         }

      }

      internal void SetBounds(Rectangle shape)
      {

         this.BorderBounds = new Rectangle(shape.X -
            this.BorderWidth, shape.Y - this.BorderWidth,
            shape.Width + 2 * this.BorderWidth, shape.Height + 2 *
            this.BorderWidth);

      }

      internal void Draw(Graphics g, bool firstSelection)
      {

         ControlPaint.DrawBorder(g, this.BorderBounds,
            ControlPaint.ContrastControlDark,
            ButtonBorderStyle.Dotted);

         if (this.Locked)
         {

            this.DrawLock(g);

         }

         else
         {
            this.DrawGrabHandle(g, this.TopLeft, firstSelection);
            this.DrawGrabHandle(g, this.TopMiddle,
               firstSelection);
            this.DrawGrabHandle(g, this.TopRight, firstSelection);
            this.DrawGrabHandle(g, this.MiddleLeft,
               firstSelection);
            this.DrawGrabHandle(g, this.MiddleRight,
               firstSelection);
            this.DrawGrabHandle(g, this.BottomLeft,
               firstSelection);
            this.DrawGrabHandle(g, this.BottomMiddle,
               firstSelection);
            this.DrawGrabHandle(g, this.BottomRight,
               firstSelection);

         }
      }

   private void DrawGrabHandle(Graphics g, Rectangle rect,
         bool firstSel)
      {

         if (firstSel)
         {
            var rect1 = rect;
            var rect2 = rect;

            var innerRect = rect;

            innerRect.Inflate(-1, -1);

            rect1.X += 1;
            rect1.Width -= 2;
            rect2.Y += 1;
            rect2.Height -= 2;

            g.FillRectangle(Brushes.Black, rect1);
            g.FillRectangle(Brushes.Black, rect2);
            g.FillRectangle(Brushes.White, innerRect);

         }

         else
         {

            g.FillRectangle(Brushes.Black, rect);

         }

      }

      private void DrawLock(Graphics g)
      {

         var rect = this.TopLeft;

         rect.X -= 1;
         rect.Width -= 1;
         rect.Height -= 2;

         var innerRect = rect;
         innerRect.Inflate(-1, -1);

         g.FillRectangle(Brushes.White, innerRect);
         g.DrawRectangle(Pens.Black, rect);

         var outerHandleRect1 = rect;

         outerHandleRect1.Y -= 2;
         outerHandleRect1.Height = 2;
         outerHandleRect1.Width = 5;
         outerHandleRect1.X += 1;

         var outerHandleRect2 = outerHandleRect1;

         outerHandleRect2.Y -= 1;
         outerHandleRect2.X += 1;
         outerHandleRect2.Width = 3;
         outerHandleRect2.Height = 1;

         var innerHandleRect = outerHandleRect1;

         innerHandleRect.X += 1;
         innerHandleRect.Width = 3;

         g.FillRectangle(Brushes.Black, outerHandleRect1);
         g.FillRectangle(Brushes.Black, outerHandleRect2);
         g.FillRectangle(Brushes.White, innerHandleRect);

      }

   }

VB.NET

Imports System.Drawing
Imports System.Windows.Forms
Public Class shpHandles
   Public Const Size As Integer = 3

   Public Sub New(ByVal shape As shpShape)
      Me.BorderWidth = 4
      Me.SetBounds(shape.Bounds)
   End Sub

   Public Property BorderBounds As Rectangle
   Public Property BorderWidth As Integer
   Public Property Locked As Boolean

   Public ReadOnly Property TotalBounds As Rectangle
      Get
         Return Rectangle.Union(Me.TopLeft, Me.BottomRight)
      End Get
   End Property

   Friend ReadOnly Property TopLeft As Rectangle
      Get
         Return New Rectangle(Me.BorderBounds.X - Size, _
            Me.BorderBounds.Y - Size, 2 * Size + 1, 2 * Size + 1)
      End Get
   End Property

   Friend ReadOnly Property TopRight As Rectangle
      Get
         Return New Rectangle(Me.BorderBounds.Right - Size, _
            Me.BorderBounds.Y - Size, 2 * Size + 1, 2 * Size + 1)
      End Get
   End Property

   Friend ReadOnly Property TopMiddle As Rectangle
      Get
         Return New Rectangle(Me.BorderBounds.X + _
            Me.BorderBounds.Width / 2 - Size, Me.BorderBounds.Y - _
            Size, 2 * Size + 1, 2 * Size + 1)
      End Get
   End Property

   Friend ReadOnly Property MiddleLeft As Rectangle
      Get
         Return New Rectangle(Me.BorderBounds.X - Size, _
            Me.BorderBounds.Y + Me.BorderBounds.Height / 2 - Size,_
            2 * Size + 1, 2 * Size + 1)
      End Get
   End Property

   Friend ReadOnly Property MiddleRight As Rectangle
      Get
         Return New Rectangle(Me.BorderBounds.Right - Size, _
            Me.BorderBounds.Y + Me.BorderBounds.Height / 2 - Size,_
            2 * Size + 1, 2 * Size + 1)
      End Get
   End Property

   Friend ReadOnly Property MiddleMiddle As Rectangle
      Get
         Return New Rectangle(Me.BorderBounds.X + _
            Me.BorderBounds.Width / 2 - Size, Me.BorderBounds.Y + _
            Me.BorderBounds.Height / 2 - Size, 2 * Size + 1, _
            2 * Size + 1)
      End Get
   End Property

   Friend ReadOnly Property BottomLeft As Rectangle
      Get
         Return New Rectangle(Me.BorderBounds.X - Size, _
            Me.BorderBounds.Bottom - Size, 2 * Size + 1, _
            2 * Size + 1)
      End Get
   End Property

   Friend ReadOnly Property BottomRight As Rectangle
      Get
         Return New Rectangle(Me.BorderBounds.Right - Size, _
            Me.BorderBounds.Bottom - Size, 2 * Size + 1, _
            2 * Size + 1)
      End Get
   End Property

   Friend ReadOnly Property BottomMiddle As Rectangle
      Get
         Return New Rectangle(Me.BorderBounds.X + _
            Me.BorderBounds.Width / 2 - Size, _
            Me.BorderBounds.Bottom - Size, 2 * Size + 1, _
            2 * Size + 1)
      End Get
   End Propert

   Friend Sub SetBounds(ByVal shape As Rectangle)
      Me.BorderBounds = New Rectangle(shape.X - Me.BorderWidth, _
         shape.Y - Me.BorderWidth, shape.Width + 2 *_
         Me.BorderWidth, shape.Height + 2 * Me.BorderWidth)
   End Sub

   Friend Sub Draw(ByVal g As Graphics, _
         ByVal firstSelection As Boolean)
      ControlPaint.DrawBorder(g, Me.BorderBounds, _
         ControlPaint.ContrastControlDark, _
         ButtonBorderStyle.Dotted)

      If Me.Locked Then
         Me.DrawLock(g)
      Else
         Me.DrawGrabHandle(g, Me.TopLeft, firstSelection)
         Me.DrawGrabHandle(g, Me.TopMiddle, firstSelection)
         Me.DrawGrabHandle(g, Me.TopRight, firstSelection)
         Me.DrawGrabHandle(g, Me.MiddleLeft, firstSelection)
         Me.DrawGrabHandle(g, Me.MiddleRight, firstSelection)
         Me.DrawGrabHandle(g, Me.BottomLeft, firstSelection)
         Me.DrawGrabHandle(g, Me.BottomMiddle, firstSelection)
         Me.DrawGrabHandle(g, Me.BottomRight, firstSelection)
      End If
   End Sub

   Private Sub DrawGrabHandle(ByVal g As Graphics, ByVal rect _
         As Rectangle, ByVal firstSel As Boolean)
      If firstSel Then
         Dim rect1 = rect
         Dim rect2 = rect
         Dim innerRect = rect
         innerRect.Inflate(-1, -1)
         rect1.X += 1
         rect1.Width -= 2
         rect2.Y += 1
         rect2.Height -= 2
         g.FillRectangle(Brushes.Black, rect1)
         g.FillRectangle(Brushes.Black, rect2)
         g.FillRectangle(Brushes.White, innerRect)
      Else
         g.FillRectangle(Brushes.Black, rect)
      End If
   End Sub

   Private Sub DrawLock(ByVal g As Graphics)
      Dim rect = Me.TopLeft
      rect.X -= 1
      rect.Width -= 1
      rect.Height -= 2
      Dim innerRect = rect
      innerRect.Inflate(-1, -1)
      g.FillRectangle(Brushes.White, innerRect)
      g.DrawRectangle(Pens.Black, rect)
      Dim outerHandleRect1 = rect
      outerHandleRect1.Y -= 2
      outerHandleRect1.Height = 2
      outerHandleRect1.Width = 5
      outerHandleRect1.X += 1
      Dim outerHandleRect2 = outerHandleRect1
      outerHandleRect2.Y -= 1
      outerHandleRect2.X += 1
      outerHandleRect2.Width = 3
      outerHandleRect2.Height = 1
      Dim innerHandleRect = outerHandleRect1
      innerHandleRect.X += 1
      innerHandleRect.Width = 3
      g.FillRectangle(Brushes.Black, outerHandleRect1)
      g.FillRectangle(Brushes.Black, outerHandleRect2)
      g.FillRectangle(Brushes.White, innerHandleRect)
   End Sub
End Class

Add the final two classes. One class is for the Snap Lines, and the other is for our Extension methods.

C#

   internal static class Ext
   {
      public static Point Add(this Point Point1, Point Point2)
      {

         return new Point(Point1.X + Point2.X, Point1.Y +
            Point2.Y);

      }

      public static Point Subtract(this Point Point1, Point Point2)
      {

         return Point1.Add(Point2.Negate());

      }

      public static Point Negate(this Point Point1)
      {

         return new Point(-Point1.X, -Point1.Y);

      }

      public static Point Floor(this Point Point1, int Grid)
      {

         var X = Point1.X / Grid;
         var Y = Point1.Y / Grid;

         return new Point(X * Grid, Y * Grid);

      }

      public static bool Between(this int Location1, int Location2,
         int Location3)
      {
         if (Location2 > Location3) return (Location3
            <= Location1 && Location2 > Location1);
         if (Location2 < Location3) return (Location2
            <= Location1 && Location3 > Location1);

         return false;
      }

   }
   public class lnSnap
   {
      public lnSnap(int X1, int Y1, int X2, int Y2,
         Color LineColor)
      {
         this.X1 = X1;
         this.Y1 = Y1;
         this.X2 = X2;
         this.Y2 = Y2;

         this.Color = LineColor;
      }

      public int X1 { get; set; }
      public int Y1 { get; set; }
      public int X2 { get; set; }
      public int Y2 { get; set; }
      public Color Color { get; set; }

      public void Draw(Graphics g)
      {

         using (var p = new Pen(this.Color))
         {

            g.DrawLine(p, this.X1, this.Y1, this.X2, this.Y2);

         }

      }
}

VB.NET

Imports System.Drawing
Imports System.Runtime.CompilerServices

Friend Module Ext
   <Extension()>
   Function Add(ByVal Point1 As Point, ByVal Point2 As Point) _
         As Point
      Return New Point(Point1.X + Point2.X, Point1.Y + Point2.Y)
   End Function

   <Extension()>
   Function Subtract(ByVal Point1 As Point, ByVal Point2 As Point) _
         As Point
      Return Point1.Add(Point2.Negate())
   End Function

   <Extension()>
   Function Negate(ByVal Point1 As Point) As Point
      Return New Point(-Point1.X, -Point1.Y)
   End Function

   <Extension()>
   Function Floor(ByVal Point1 As Point, ByVal Grid As Integer) _
         As Point
      Dim X = Point1.X / Grid
      Dim Y = Point1.Y / Grid
      Return New Point(X * Grid, Y * Grid)
   End Function

   <Extension()>
   Function Between(ByVal Location1 As Integer, ByVal Location2 _
         As Integer, ByVal Location3 As Integer) As Boolean
      If Location2 > Location3 Then Return (Location3 <= _
         Location1 AndAlso Location2 > Location1)
      If Location2 < Location3 Then Return (Location2 <= _
         Location1 AndAlso Location3 > Location1)
      Return False
   End Function
End Module
Imports System.Drawing
Public Class lnSnap
   Public Sub New(ByVal X1 As Integer, ByVal Y1 As Integer, _
         ByVal X2 As Integer, ByVal Y2 As Integer, _
         ByVal LineColor As Color)
      Me.X1 = X1
      Me.Y1 = Y1
      Me.X2 = X2
      Me.Y2 = Y2
      Me.Color = LineColor
   End Sub

   Public Property X1 As Integer
   Public Property Y1 As Integer
   Public Property X2 As Integer
   Public Property Y2 As Integer
   Public Property Color As Color

   Public Sub Draw(ByVal g As Graphics)
      Using p = New Pen(Me.Color)
         g.DrawLine(p, Me.X1, Me.Y1, Me.X2, Me.Y2)
      End Using
   End Sub
End Class

All that is needed now is to create the various shapes; then, everything can gel together. I am stopping here today because the creation of the shapes and the canvas is a topic on its own.

Conclusion

For now, the stage is basically set. All we need is a platform to draw on, and the various shapes. This is what we will do next time. Until then, happy coding!

Hannes DuPreez
Hannes DuPreez
Ockert J. du Preez is a passionate coder and always willing to learn. He has written hundreds of developer articles over the years detailing his programming quests and adventures. He has written the following books: Visual Studio 2019 In-Depth (BpB Publications) JavaScript for Gurus (BpB Publications) He was the Technical Editor for Professional C++, 5th Edition (Wiley) He was a Microsoft Most Valuable Professional for .NET (2008–2017).

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read