Creating a Shape Editor in .NET, Part 3

CodeGuru content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

Introduction

Hello and welcome to the third and final installment of this article series. So far, we have done a lot of work by building the shapes, the logic to resize them, the properties of the various shapes, and the lines measuring the distance between the shapes whilst moving them around. This installment will bring everything together by actually (finally!) creating the canvas to draw upon.

So, let’s continue from where we last stopped.

Create a new class and name it shpCanvas.

Override the OnPaint event, and the following:

C#

   protected override void OnPaint(PaintEventArgs e)
   {

      base.OnPaint(e);

      if (this.Mode == SnapModes.SnapToGrid)
      {

         this.PaintGrid(e.Graphics);

      }
      else if (this.Mode == SnapModes.SnapLines)
      {

         if (snapLines != null && snapLines.Count > 0)
         {

            foreach (var snapLine in snapLines)

            snapLine.Draw(e.Graphics);

         }

      }

      foreach (var s in this.Shapes)
      {

         s.Draw(e.Graphics);

      }

      e.Graphics.SmoothingMode =
         System.Drawing.Drawing2D.SmoothingMode.Default;

      foreach (var s in this.SelectedShapes)
      {

         s.DrawGrabHandles(e.Graphics,
            this.SelectedShapes.IndexOf(s) == 0);

     }

   }

   private void PaintGrid(Graphics g)
   {
      const int GridSize = 10;

      for (var x = 0; x < this.Width; x += GridSize)
      {

         for (var y = 0; y < this.Height; y += GridSize)
         {

            g.DrawLine(Pens.LightGray, 0, y, this.Width, y);
            g.DrawLine(Pens.LightGray, x, 0, x, this.Height);

         }

      }

   }

VB.NET

   Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
      MyBase.OnPaint(e)

      If Me.Mode = SnapModes.SnapToGrid Then
         Me.PaintGrid(e.Graphics)
      ElseIf Me.Mode = SnapModes.SnapLines Then

         If snapLines IsNot Nothing AndAlso snapLines.Count _
               > 0 Then

            For Each snapLine In snapLines
               snapLine.Draw(e.Graphics)
            Next
         End If
      End If

      For Each s In Me.shapes
         s.Draw(e.Graphics)
      Next

      e.Graphics.SmoothingMode = System.Drawing.Drawing2D. _
         SmoothingMode.[Default]

      For Each s In Me.selectedshapes
         s.DrawGrabHandles(e.Graphics, _
            Me.selectedshapes.IndexOf(s) = 0)
      Next
   End Sub

   Private Sub PaintGrid(ByVal g As Graphics)
      Const GridSize As Integer = 10
      Dim x = 0

      While x < Me.Width
         Dim y = 0

         While y < Me.Height
            g.DrawLine(Pens.LightGray, 0, y, Me.Width, y)
            g.DrawLine(Pens.LightGray, x, 0, x, Me.Height)
            y += GridSize
         End While

         x += GridSize

      End While
   End Sub

For every change to the canvas, the program calculates its size of the grid and draws the necessary snaplines.

Add the shape selection logic to show the user that the particular shape has been selected.

C#

   private shpColl shapes;
   public shpColl Shapes
   {

      get
      {

         return shapes;

      }

   }
   [Browsable(false)]
   public shpShape SelectedShape
   {

      get
      {

         if (this.SelectedShapes != null &&
            this.SelectedShapes.Count > 0) return
            this.SelectedShapes[0];

         return null;

      }
   }

   private List<shpShape> selectedshapes;
   public List<shpShape> SelectedShapes
   {

      get
      {

         return selectedshapes;

      }

   }
   public event EventHandler SelectedShapeChanged;
   protected virtual void OnSelectedShapeChanged(EventArgs e)
   {

      this.SelectedShapeChanged?.Invoke(this, e);

   }
   public void Selection(shpShape shape)
   {

      if (shape == null)
      {

         selectedshapes = new List<shpShape>();

         this.OnSelectedShapeChanged(EventArgs.Empty);

         return;

      }

      if (this.SelectedShapes == null) selectedshapes = new
         List<shpShape>();

      if (this.SelectedShapes.Contains(shape))
      {

         this.SelectedShapes.Remove(shape);
         this.SelectedShapes.Insert(0, shape);

      }

      else
      {

         selectedshapes = new List<shpShape>();
         this.SelectedShapes.Add(shape);

      }

      this.OnSelectedShapeChanged(EventArgs.Empty);
      this.Invalidate();

   }

   public void InvalidateRect(Rectangle rect)
   {

      rect.Inflate(1, 1);
      this.Invalidate(rect);

   }

   public void InvalidateShape(shpShape shape)
   {

      this.InvalidateRect(shape.Bounds);

   }

VB.NET

   Private shpes As shpColl

   Public ReadOnly Property Shapes As shpColl
      Get
         Return shpes
      End Get
   End Property

   <Browsable(False)>
   Public ReadOnly Property SelectedShape As shpShape
      Get
         If Me.selectedshapes IsNot Nothing AndAlso _
            Me.selectedshapes.Count > 0 Then Return _
            Me.selectedshapes(0)
         Return Nothing
      End Get
   End Property

   Private selshapes As List(Of shpShape)

   Public ReadOnly Property SelectedShapes As List(Of shpShape)
      Get
         Return selshapes
      End Get
   End Property
   Public Event SelectedShapeChanged As EventHandler

   Protected Overridable Sub OnSelectedShapeChanged(ByVal e _
         As EventArgs)

      RaiseEvent SelectedShapeChanged(Me, e)
   End Sub
   Public Sub Selection(ByVal shape As shpShape)
      If shape Is Nothing Then
         selshapes = New List(Of shpShape)()
         Me.OnSelectedShapeChanged(EventArgs.Empty)
         Return
      End If

      If Me.SelectedShapes Is Nothing Then selshapes = New _
         List(Of shpShape)()

      If Me.selectedshapes.Contains(shape) Then
         Me.selectedshapes.Remove(shape)
         Me.selectedshapes.Insert(0, shape)
      Else
         selshapes = New List(Of shpShape)()
         Me.selectedshapes.Add(shape)
      End If

      Me.OnSelectedShapeChanged(EventArgs.Empty)
      Me.Invalidate()
   End Sub

   Public Sub InvalidateRect(ByVal rect As Rectangle)
      rect.Inflate(1, 1)
      Me.Invalidate(rect)
   End Sub

   Public Sub InvalidateShape(ByVal shape As shpShape)
      Me.InvalidateRect(shape.Bounds)
   End Sub

Add the code to align the shapes.

C#

   private void AlignShapes(shpShape moving, shpShape shpfixed)
   {
      Snapping = true;

      if (this.AlignShape(moving.Bounds.Top, shpfixed.Bounds.Top))
      {

         this.AlignTop(shpfixed, moving);

      }

      if (this.AlignShape(moving.Bounds.Bottom,
         shpfixed.Bounds.Bottom))
      {

         this.AlignBottom(shpfixed, moving);

      }

      if (this.AlignShape(moving.Bounds.Left,
         shpfixed.Bounds.Left))
      {

         this.AlignLeft(shpfixed, moving);

      }

      if (this.AlignShape(moving.Bounds.Right,
         shpfixed.Bounds.Right))
      {

         this.AlignRight(shpfixed, moving);

      }

      Snapping = false;

   }
   private bool AlignShape(int shapeLoc, int otherLoc)
   {

      return shapeLoc.Between(otherLoc + this.ShapeAlignDist,
         otherLoc - this.ShapeAlignDist);

   }
   private void AlignLeft(shpShape shpfixed, shpShape moving)
   {

      moving.Move(new Point(shpfixed.Bounds.Left,
         moving.Bounds.Y));

      lnSnap snap;

      if (moving.Bounds.Y > shpfixed.Bounds.Y)
      {

         snap = new lnSnap(moving.Bounds.Left, shpfixed.Bounds.Top,
            moving.Bounds.Left, moving.Bounds.Bottom, Color.Blue);

      }

      else
      {

         snap = new lnSnap(moving.Bounds.Left, moving.Bounds.Top,
            moving.Bounds.Left, shpfixed.Bounds.Bottom,
            Color.Blue);

      }

      snapLines.Add(snap);

   }

   private void AlignRight(shpShape shpfixed, shpShape moving)
   {

      moving.Move(new Point(shpfixed.Bounds.Right -
         moving.Bounds.Width, moving.Bounds.Y));

      lnSnap snap;

      if (moving.Bounds.Y > shpfixed.Bounds.Y)
      {

         snap = new lnSnap(moving.Bounds.Right,
         shpfixed.Bounds.Top, moving.Bounds.Right,
         moving.Bounds.Bottom, Color.Blue);

      }

      else
      {

         snap = new lnSnap(moving.Bounds.Right, moving.Bounds.Top,
            moving.Bounds.Right, shpfixed.Bounds.Bottom,
            Color.Blue);

      }

      snapLines.Add(snap);

   }

   private void AlignTop(shpShape shpfixed, shpShape moving)
   {

      moving.Move(new Point(moving.Bounds.X, shpfixed.Bounds.Top));

      lnSnap snap;

      if (moving.Bounds.X > shpfixed.Bounds.X)
      {

         snap = new lnSnap(shpfixed.Bounds.Left, moving.Bounds.Top,
            moving.Bounds.Right, moving.Bounds.Top, Color.Blue);

      }

      else
      {

         snap = new lnSnap(moving.Bounds.Left, moving.Bounds.Top,
         shpfixed.Bounds.Right, moving.Bounds.Top, Color.Blue);

      }

      snapLines.Add(snap);

   }

   private void AlignBottom(shpShape shpfixed, shpShape moving)
   {

      moving.Move(new Point(moving.Location.X,
         shpfixed.Bounds.Bottom - moving.Bounds.Height));

      lnSnap snap;

      if (moving.Bounds.X > shpfixed.Bounds.X)
      {

         snap = new lnSnap(shpfixed.Bounds.Left,
            moving.Bounds.Bottom, moving.Bounds.Right,
            moving.Bounds.Bottom, Color.Blue);

      }

      else
      {

         snap = new lnSnap(moving.Bounds.Left,
            moving.Bounds.Bottom, shpfixed.Bounds.Right,
            moving.Bounds.Bottom, Color.Blue);

      }

      snapLines.Add(snap);

   }

VB.NET

   Private Sub AlignShapes(ByVal moving As shpShape, _
         ByVal shpfixed As shpShape)
      Snapping = True

      If Me.AlignShape(moving.Bounds.Top, shpfixed.Bounds.Top) Then
         Me.AlignTop(shpfixed, moving)
      End If

      If Me.AlignShape(moving.Bounds.Bottom, _
            shpfixed.Bounds.Bottom) Then
         Me.AlignBottom(shpfixed, moving)
      End If

      If Me.AlignShape(moving.Bounds.Left, _
            shpfixed.Bounds.Left) Then
         Me.AlignLeft(shpfixed, moving)
      End If

      If Me.AlignShape(moving.Bounds.Right, _
            shpfixed.Bounds.Right) Then
         Me.AlignRight(shpfixed, moving)
      End If

      Snapping = False
   End Sub
   Private Function AlignShape(ByVal shapeLoc As Integer, _
         ByVal otherLoc As Integer) As Boolean

      Return shapeLoc.Between(otherLoc + Me.ShapeAlignDist, _
         otherLoc - Me.ShapeAlignDist)
   End Function

   Private Sub AlignLeft(ByVal shpfixed As shpShape, _
         ByVal moving As shpShape)
      moving.Move(New Point(shpfixed.Bounds.Left, moving.Bounds.Y))
      Dim snap As lnSnap

      If moving.Bounds.Y > shpfixed.Bounds.Y Then
         snap = New lnSnap(moving.Bounds.Left, _
            shpfixed.Bounds.Top, moving.Bounds.Left, _
            moving.Bounds.Bottom, Color.Blue)
      Else
         snap = New lnSnap(moving.Bounds.Left, moving.Bounds.Top, _
            moving.Bounds.Left, shpfixed.Bounds.Bottom, Color.Blue)
      End If

      snapLines.Add(snap)
   End Sub

   Private Sub AlignRight(ByVal shpfixed As shpShape, _
         ByVal moving As shpShape)
      moving.Move(New Point(shpfixed.Bounds.Right - _
         moving.Bounds.Width, moving.Bounds.Y))
      Dim snap As lnSnap

      If moving.Bounds.Y > shpfixed.Bounds.Y Then
         snap = New lnSnap(moving.Bounds.Right, _
            shpfixed.Bounds.Top, moving.Bounds.Right, _
            moving.Bounds.Bottom, Color.Blue)
      Else
         snap = New lnSnap(moving.Bounds.Right, _
            moving.Bounds.Top, moving.Bounds.Right, _
            shpfixed.Bounds.Bottom, Color.Blue)
      End If

      snapLines.Add(snap)
   End Sub

   Private Sub AlignTop(ByVal shpfixed As shpShape, ByVal moving _
         As shpShape)
      moving.Move(New Point(moving.Bounds.X, shpfixed.Bounds.Top))
      Dim snap As lnSnap

      If moving.Bounds.X > shpfixed.Bounds.X Then
         snap = New lnSnap(shpfixed.Bounds.Left, _
            moving.Bounds.Top, moving.Bounds.Right, _
            moving.Bounds.Top, Color.Blue)
      Else
         snap = New lnSnap(moving.Bounds.Left, moving.Bounds.Top, _
            shpfixed.Bounds.Right, moving.Bounds.Top, Color.Blue)

      End If

      snapLines.Add(snap)
   End Sub

   Private Sub AlignBottom(ByVal shpfixed As shpShape, _
         ByVal moving As shpShape)
      moving.Move(New Point(moving.Location.X, _
         shpfixed.Bounds.Bottom - moving.Bounds.Height))
      Dim snap As lnSnap

      If moving.Bounds.X > shpfixed.Bounds.X Then
         snap = New lnSnap(shpfixed.Bounds.Left, _
            moving.Bounds.Bottom, moving.Bounds.Right, _
            moving.Bounds.Bottom, Color.Blue)
      Else
         snap = New lnSnap(moving.Bounds.Left, _
            moving.Bounds.Bottom, shpfixed.Bounds.Right, _
            moving.Bounds.Bottom, Color.Blue)

      End If

      snapLines.Add(snap)
   End Sub

This aligns the various shapes according to the selection. The snapping logic works similar to the preceding alignment functions.

The code can be found on GitHub:

Conclusion

This concludes this article series. Remember, not all the code is listed here, but is indeed available on GitHub. I hope you have enjoyed this little project as much as I did.

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