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!