Flood Filling Objects with VB

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

Introduction

I have always loved GDI+. It has given me much joy experimenting and playing with all things graphics related. I guess it is because I come from a VB 6 background where drawing mundane things was always a great chore. Now, I try to make use of the full power of GDI+ whenever I can. Today, I will show you how to flood fill drawn objects.

What Is Flood Filling?

It is the complicated term for coloring in drawn shapes. If you are accustomed to working with CorelDRAW, Photoshop, or even Paint, you will have noticed that you can click a colour and fill the empty spaces with it. This is flood filling. Let me start.

Design

Create a new VB Windows Forms project. There is no design as such because you will create the drawings dynamically. Make sure the form is big enough so that it gives you ample space to see the drawn objects.

Code

Add a class named ARGB32 to your project and add the following code into the newly created class:

Imports System.Drawing.Imaging
Imports System.Runtime.InteropServices

Public Class ARGB32

   Public bytImage() As Byte
   Public intSize As Integer
   Public Const bytPixel As Integer = 4
   Public Const intPixel As Integer = bytPixel * 8

   ' Reference to Bitmap.
   Private bmpBitmap As Bitmap

   Public Sub New(ByVal bm As Bitmap)
      bmpBitmap = bm
   End Sub

   ' Bitmap data.
   Private bmddBitmapData As BitmapData

   Public Sub LockBitmap()
      ' Lock the bitmap data.
      Dim rectBounds As Rectangle = New Rectangle( _
         0, 0, bmpBitmap.Width, bmpBitmap.Height)
      bmddBitmapData = bmpBitmap.LockBits(rectBounds, _
         Imaging.ImageLockMode.ReadWrite, _
         Imaging.PixelFormat.Format32bppArgb)
      intSize = bmddBitmapData.Stride

      ' Allocate room
      Dim intTotalSize As Integer = bmddBitmapData.Stride * _
         bmddBitmapData.Height
      ReDim bytImage(intTotalSize)

      ' Copy the data into the ImageBytes array.
      Marshal.Copy(bmddBitmapData.Scan0, bytImage, _
         0, intTotalSize)
   End Sub

   ' Copy the data back into the Bitmap
   ' and release resources.
   Public Sub UnlockBitmap(Optional ByVal blnSave _
      As Boolean = True)
      ' Copy the data back into the bitmap.
      If blnSave Then
         Dim total_size As Integer = bmddBitmapData.Stride * _
            bmddBitmapData.Height
         Marshal.Copy(bytImage, 0, _
            bmddBitmapData.Scan0, total_size)
      End If

      ' Unlock the bitmap.
      bmpBitmap.UnlockBits(bmddBitmapData)

      ' Release resources.
      bytImage = Nothing
      bmddBitmapData = Nothing
   End Sub
End Class

The purpose of this class is to create and save a reference to a bitmap. This bitmap object will be the one that will ultimately hold the in-memory image of the colored-in images(s).

Add the next class (Fill):

Public Class Fill
   Public Xmin As Integer
   Public Xmax As Integer
   Public Y As Integer
   Public Sub New(ByVal intXMin As Integer, _
         ByVal intXMax As Integer, _
         ByVal intY As Integer)
      Xmin = intXMin
      Xmax = intXMax
      Y = intY
   End Sub

   ' Color the pixels in the run.
   Public Sub SetColor(ByVal rgbBytes As ARGB32, _
         ByVal bytR As Byte, ByVal bytG As Byte, _
         ByVal bytB As Byte)
      Dim pix As Integer = _
         Y * rgbBytes.intSize + _
         Xmin * rgbBytes.bytPixel
      For x As Integer = Xmin To Xmax
         rgbBytes.bytImage(pix) = bytB
         rgbBytes.bytImage(pix + 1) = bytG
         rgbBytes.bytImage(pix + 2) = bytR
         pix += rgbBytes.bytPixel
         Next x
   End Sub
End Class

This class handles the physical filling of the drawn objects.

Add a Module to your project and add the following code:

Module Flood

   ' See if this point has the target color.
   Private Function PointHasColor(ByVal bm_bytes As ARGB32, _
         ByVal x As Integer, ByVal y As Integer, _
         ByVal target_r As Byte, ByVal target_g As Byte, _
         ByVal target_b As Byte) As Boolean
      Dim pix As Integer = y * bm_bytes.intSize + x * _
         bm_bytes.bytPixel
      Dim b As Byte = bm_bytes.bytImage(pix)
      Dim g As Byte = bm_bytes.bytImage(pix + 1)
      Dim r As Byte = bm_bytes.bytImage(pix + 2)

      Return _
         (r = target_r) AndAlso _
         (g = target_g) AndAlso _
         (b = target_b)
   End Function


   ' Flood the area at this point.
   Public Sub UnsafeFloodFillRuns(ByVal bm As Bitmap, _
         ByVal x As Integer, ByVal y As Integer, _
         ByVal new_color As Color)
      ' Get the old and new colors' components.
      Dim old_r As Byte = bm.GetPixel(x, y).R
      Dim old_g As Byte = bm.GetPixel(x, y).G
      Dim old_b As Byte = bm.GetPixel(x, y).B

      Dim new_r As Byte = new_color.R
      Dim new_g As Byte = new_color.G
      Dim new_b As Byte = new_color.B

      ' Make a BitmapBytesARGB32 object.
      Dim bm_bytes As New ARGB32(bm)

      ' Lock the bitmap.
      bm_bytes.LockBitmap()

      ' Find and color the initial run.
      Dim initial_run As Fill = MakeRun(bm_bytes, bm.Width, _
         bm.Height, x, y, old_r, old_g, old_b
      initial_run.SetColor(bm_bytes, new_r, new_g, new_b)

       ' Start with the initial run in the stack.
      Dim runs As New Stack(1000)
      runs.Push(initial_run)

      ' While the stack is not empty, process a run.
      Dim pix As Integer
      Do While runs.Count > 0
         ' Get the next run.
         Dim the_run As Fill = DirectCast(runs.Pop(), Fill)
         y = the_run.Y

         ' Look for runs above.
         If y > 0 Then
            Dim x0 As Integer = the_run.Xmi
            Do
               If x0 > the_run.Xmax Then Exit Do
               ' See if this point has the old color.
               If PointHasColor(bm_bytes, x0, y - 1, old_r, _
                     old_g, old_b) Then
                  ' Make and color a run here.
                  Dim new_run As Fill = MakeRun(bm_bytes, _
                     bm.Width, bm.Height, x0, y - 1, old_r, _
                     old_g, old_b)
                  new_run.SetColor(bm_bytes, new_r, new_g, new_b)
                  runs.Push(new_run)

                  ' Skip one pixel beyond the end of this run.
                  x0 = new_run.Xmax + 2
               Else
                  x0 += 1
               End If
            Loop
         End If

         ' Look for runs below.
         If y < bm.Height - 1 Then
            Dim x0 As Integer = the_run.Xmin
            Do
               If x0 > the_run.Xmax Then Exit Do
               ' See if this point has the old color.
               If PointHasColor(bm_bytes, x0, y + 1, old_r, _
                     old_g, old_b) Then
                  ' Make and color a run here.
                  Dim new_run As Fill = MakeRun(bm_bytes, _
                     bm.Width, bm.Height, x0, y + 1, _
                     old_r, old_g, old_b)
                  new_run.SetColor(bm_bytes, new_r, new_g, new_b)
                  runs.Push(new_run)

                  ' Skip one pixel beyond the end of this run.
                  x0 = new_run.Xmax + 2
               Else
                  x0 += 1
               End If
            Loop
         End If
      Loop

      ' Unlock the bitmap.
      bm_bytes.UnlockBitmap()
   End Sub

   ' Find the run at this point.
   Private Function MakeRun(ByVal bm_bytes As ARGB32, _
         ByVal wid As Integer, ByVal hgt As Integer, _
         ByVal x As Integer, ByVal y As Integer, _
         ByVal old_r As Byte, ByVal old_g As Byte, _
         ByVal old_b As Byte) As Fill
      Dim x0 As Integer = x
      Do
         If x0 < 0 Then Exit Do
         If Not PointHasColor(bm_bytes, x0, y, old_r, _
               old_g, old_b) Then Exit Do
            x0 -= 1
      Loop
      Dim x1 As Integer = x
      Do
         If x1 >= wid Then Exit Do
         If Not PointHasColor(bm_bytes, x1, y, old_r, _
            old_g, old_b) Then Exit Do
         x1 += 1
      Loop
      Return New Fill(x0 + 1, x1 - 1, y)
   End Function

End Module

Now that everything is set up, we obviously need some objects to fill in. Add the necessary Namespaces:

Imports System.Math
Imports System.Drawing.Drawing2D

Add the rest of the code:

   Private bmpBitmap As Bitmap
      ' Draw random circles.
      Private Sub Form1_Load(ByVal sender As Object, _
           ByVal e As System.EventArgs) Handles MyBase.Load
         DrawBitmap()
      End Sub
      Private Sub Form1_Resize(ByVal sender As Object, _
            ByVal e As System.EventArgs) Handles MyBase.Resize
         DrawBitmap()
         Me.Invalidate()
      End Sub
      Private Sub DrawBitmap()
         If Me.ClientRectangle.Width < 10 OrElse _
            Me.ClientRectangle.Height < 10 Then Exit Sub

         bmpBitmap = New Bitmap(Me.ClientRectangle.Width, _
            Me.ClientRectangle.Height)
         Dim gr As Graphics = Graphics.FromImage(bmpBitmap)
         gr.Clear(Color.White)

         Dim rnd As New Random
         Dim max_r As Integer = Min(Me.ClientRectangle.Width, _
            Me.ClientRectangle.Height)  3
         Dim min_r As Integer = max_r  4
         For i As Integer = 1 To 20
            Dim r As Integer = rnd.Next(min_r, max_r)
            Dim x As Integer = rnd.Next(min_r, _
               Me.ClientRectangle.Width - min_r)
            Dim y As Integer = rnd.Next(min_r, _
            Me.ClientRectangle.Height - min_r)
            gr.DrawEllipse(Pens.Black, x - r, y - r, _
               2 * r, 2 * r)
         Next i
         gr.Dispose()
      End Sub

      ' Display the picture.
      Private Sub Form1_Paint(ByVal sender As Object, _
            ByVal e As System.Windows.Forms.PaintEventArgs) _
            Handles MyBase.Paint
         e.Graphics.DrawImage(bmpBitmap, 0, 0)
      End Sub

      ' Flood fill the clicked area.
      Private Sub Form1_MouseDown(ByVal sender As Object, _
            ByVal e As System.Windows.Forms.MouseEventArgs) _
            Handles MyBase.MouseDown
         ' Pick a random new color.
         Dim rnd As New Random
         Dim old_color As Color = bmpBitmap.GetPixel(e.X, e.Y)
         Dim new_color As Color
         Do
            Dim qb_clr As Integer = QBColor(rnd.Next(1, 16))
            new_color = Color.FromArgb(qb_clr Or &HFF000000)
         Loop Until Not (new_color.Equals(old_color))

         ' Flood.
         Dim start_time As Date = Now
         If e.Button = MouseButtons.Left Then
            UnsafeFloodFillRuns(bmpBitmap, e.X, e.Y, new_color)

         End If
         Dim elapsed_time As TimeSpan = Now.Subtract(start_time)
         Debug.WriteLine(elapsed_time.TotalSeconds.ToString("0.00") _
            & " seconds")

         ' Redraw.
         Me.Invalidate()
      End Sub

In Form_Load, you create circle shapes randomly. Inside The MouseDown event, you randomly choose a colour and fill the area that was clicked upon.

Conclusion

Thank you for reading today’s article. I have attached a working sample with this article. Until we meet again, cheers!

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