Graphics Programming in Visual Basic - Part 3

"Advanced API Pixel Routines"

By: Tanner "DemonSpectre" Helland

Despite what many programmers will tell you, Visual Basic is an excellent programming language for high-end graphic applications - with or without DirectX and OpenGL. Many VB programmers seem to believe that the only way to get fast 2D graphics is with DirectDraw…and they are completely wrong! Using some creativity and efficient coding, you can get the API to do anything that DirectDraw does - and nearly as fast (and in some cases faster!). But this is not to say that only the DirectX-paranoid can benefit from this tutorial, because even the best DX experts could use a good explanation of how Windows handles graphics routines. This series of articles will explain not only how to do fast graphics but how fast graphics work. You have already learned about pure VB routines for graphics processing; that was followed by the basic API routines of GetPixel and SetPixel/V; now comes the more advanced GetBitmapBits and SetBitmapBits and their twice-removed cousins called DIB sections; the last tutorial will cover optimizing your graphics functions for additional speed, shall you need it. By the end of this series of tutorials you will be able to effectively write any graphics application you can dream up using nothing but Visual Basic and the Windows API.

So if you're ready, here's part three of how to become a professional graphics programmer using good ol' VB.

-THE PURPOSE OF THIS TUTORIAL-

This tutorial will go through two advanced ways of getting and setting pixels in Visual Basic: the API routines of GetBitmapBits/SetBitmapBits and GetDIBits/StretchDIBits. I recommend reading the previous two tutorials, "Visual Basic Pixel Routines" and "Basic API Pixel Routines," as they provide the foundation for the advanced graphics principles discussed in this tutorial.

-ADVANCED API GRAPHICS ROUTINES-

Now that you understand how to use both Visual Basic and the API to get per-pixel data, it is time to move to the next level: BitmapBits and DIB sections. This tutorial will go through the API routines of GetBitmapBits/SetBitmapBits and GetDIBits/StretchDIBits. Both sets of routines are very fast and very powerful, but I had better throw in a disclaimer here - pay close attention to any red warnings in this document. Because all of these API routines directly interface the heap (dynamically allocated memory), you can easily crash both the VBIDE and Windows with a page fault or worse. Believe me - it's not a pretty sight to watch your entire machine freeze because you accidentally allocated your array to the wrong size. (But it gives you a good taste of C/C++ programming, heh heh :).

But on the happy side of things, these are about as fast as graphics get in VB. There are ways to use CopyMemory, assembly language, and other freakish routines to get slightly faster effects, but they are even more perilous than these methods so avoid them if you can.

Onto the graphics!

PART I - GETTING AND SETTING PIXEL DATA USING GETBITMAPBITS AND SETBITMAPBITS

At this point in your VB career you are probably used to interacting with images one pixel at a time using something like PSet/Point or GetPixel/SetPixel. This makes for some pretty easy programming, but it's very, very slow. The main reason for this is that every time VB calls an API routine the code from that API routine is dynamically stripped out of the DLL file and placed streamline into the in-memory lines of your programming (hence the name "dynamically linked library"). Considering all the work involved in doing this VB actually moves pretty quickly, but there are better ways of doing things.

Enter GetBitmapBits and SetBitmapBits. Instead of getting each pixel individually, we pass each function an array and let it fill the whole thing at once with the picture's pixel data or set the picture's pixel data all at once with the information in the array. Much more efficient, eh? The trade-off, of course, is that this involves a little more programming and significantly more risk. If the array dimensions are off by a mere 1 byte all kinds of things can happen - the picture won't appear at all, your program will shut itself down, or your entire machine could freeze up. But these only happen if you're careless and don't heed my warnings, however, so don't get too scared. :)

The first step is to declare a whole bunch of things:

________________________________________________________

Private Type Bitmap

bmType As Long

bmWidth As Long

bmHeight As Long

bmWidthBytes As Long

bmPlanes As Integer

bmBitsPixel As Integer

bmBits As Long

End Type

Private Declare Function GetObject Lib "gdi32" Alias "GetObjectA" (ByVal hObject As Long, ByVal nCount As Long, ByRef lpObject As Any) As Long

Private Declare Function GetBitmapBits Lib "gdi32" (ByVal hBitmap As Long, ByVal dwCount As Long, ByRef lpBits As Any) As Long

Private Declare Function SetBitmapBits Lib "gdi32" (ByVal hBitmap As Long, ByVal dwCount As Long, ByRef lpBits As Any) As Long

________________________________________________________

This might seem a little extreme, so let me go through each of these one at a time.

The 'Bitmap' type is required for the 'GetObject' call - if you'll look at GetObject's declaration, you'll notice that the last parameter is of type 'Any.' This is where we pass in our 'Bitmap' object. As for the individual elements of the 'Bitmap' type, we only care about four out of the seven variables. They are:

The other three variables aren't needed for getting and setting pixels; we simply include them to ensure that our 'Bitmap' type matches the Windows 'Bitmap' type.

Next we have the GetObject call. The purpose of this API call is to, well, get an object. You'll see how this works in a moment.

GetBitmapBits and SetBitmapBits have identical parameters, which in turn are almost identical to the GetObject parameters.

Okay - that's a whole lot of information in a small space, so take a quick break to make sure you understand at least a part of each of those declarations. If some parts are a little hazy, that's okay because we're going to see how they work in just a second.

Ready? If so, here is how you use the calls.

First, GetBitmapBits:

________________________________________________________

Dim bm As Bitmap

GetObject PictureBox.Image, Len(bm), bm

Dim ImageData() as Byte

ReDim ImageData(0 To (bm.bmBitsPixel \ 8) - 1, 0 To bm.bmWidth - 1, 0 To bm.bmHeight)

GetBitmapBits PictureBox.Image, bm.bmWidthBytes * bm.bmHeight, ImageData(0, 0, 0)

________________________________________________________

The procedure is actually very straightforward. First, we declare a 'Bitmap' object and call the GetObject routine. When called, GetObject will analyze the Picture Box and assign the appropriate values to our Bitmap object, which we can then use to know how to prepare our array to receive the data.

Once we have all of the picture box's information available to us in the form of a 'Bitmap' object, we declare ourselves an array. This array is very special because we're going to use it to hold all of the picture's pixel information. To make sure that it is the right size for doing this, we use 'ReDim' to make its dimensions just the right size:

SIDE NOTE ABOUT 'GETOBJECT': You may be wondering why we use the GetObject call at all - couldn't we just resize the ImageData array using the picture box's ScaleWidth and ScaleHeight properties? In theory, you could. However, VB does strange things to the ScaleWidth and ScaleHeight properties depending on what is stored there. For example, the same image might report different ScaleWidth and ScaleHeight properties at different execution times. JPEGs are notoriously bad at this - when you load one, VB sometimes thinks that the picture's width is one pixel less than it is in memory. I honestly have no idea as to why VB has this problem. And the strangest thing is that it pops out of nowhere - just one of those strange Microsoft anomalies, I guess. GetObject never suffers from such a problem so you should always use it. The speed difference is trivial in any case.

SIDE NOTE ABOUT DECLARING YOUR ARRAY: You can use any number of dimensions in your array, so long as its total size is accurate. For example, you could also do something like ReDim ImageData(0 to bm.bmWidth * bm.bmHeight * 3) and the function would still work fine. The API call could care less about how the array is declared - all it gets is the address of the first element in the array and the number of bytes that it's allowed to work with. The way the array is dimensioned is only for your convenience. I like the above way because it makes editing the image very easy. This issue will be discussed further in tutorial 4.

WARNING!! This ReDim statement is where you can really screw your computer. If ImageData is too small, GetBitmapBits will attempt to put the picture data in unallocated memory, causing a general protection fault, a page fault, or some other nasty illegal operation. Make sure that ImageData is the right size!! If you use the above method you should never have any problems…but if you do, it's in no way my fault.

The GetBitmapBits call itself is very straightforward: it takes the address of ImageData() and fills it up with the pixel data located in PictureBox. Pretty easy, isn't it? Now you can edit the values any way that you want to. For example, the following loop would invert all of the pixels in the image:

________________________________________________________

'First, get the image data using the above code section

Dim X as long, Y as long

For X = 0 to PictureBox.ScaleWidth-1

For Y = 0 to PictureBox.ScaleHeight

'Change the R value

ImageData(2, X, Y) = 255 - ImageData(2, X, Y)

'Change the G value

ImageData(1, X, Y) = 255 - ImageData(1, X, Y)

'Change the B value

ImageData(0, X, Y) = 255 - ImageData(0 X, Y)

Next Y

Next X

________________________________________________________

(Really, this method is almost as easy as GetPixel if you can get used to the API structure…) SetBitmapBits is almost identical:

________________________________________________________

Dim bm As Bitmap

GetObject PictureBox.Image, Len(bm), bm

SetBitmapBits PictureBox.Image, bm.bmWidthBytes * bm.bmHeight, ImageData(0, 0, 0)

If PictureBox.AutoRedraw = True Then

PictureBox.Picture = PictureBox.Image

PictureBox.Refresh

End If

________________________________________________________

Everything is the same as GetBitmapBits, except that we aren't resizing the array (because it's already the right size and resizing it would erase all of its information). The last if/then statement is included because SetBitmapBits won't automatically initialize the AutoRedraw event, so we have to tell it to replace the 'Picture' property (what is shown on the screen) with the 'Image' property (what is stored in memory).

Pretty easy stuff, eh? In fact, it's almost too easy…so of course, there is a major problem with this method: both GetBitmapBits and SetBitmapBits only work in 24-bit color mode (16.7 million colors)!! Actually, they work in 16 and 8 bit color modes too, but the image data no longer occupies 3bpp so editing the image data is nearly impossible. It can be done, but you have to write a function to translate 2 bits into 3 as well as transferring the data into a separate array while you edit it. Then, to draw it, you have to translate the 3 bits into 2 bits and then transfer your array back into the original one. It's messy and slow, so I wouldn't recommend trying it.

So of course, someone is going to ask "but how can I do fast graphics in 16 or 8 bit color mode?" That is what DIB sections are for, so if you want to know about them then keep reading.

(Personally, I would recommend using DIB sections for all of your graphics programs because you'll never get unexpected errors with them and it’s a great way to add additional functionality to your graphics program. I have only discussed BitmapBits because they make for an excellent introduction to DIB sections.)

PART 2 - GETTING AND SETTING BITMAP BITS USING DIB SECTIONS

DIB section stands for 'Device Independent Bitmap.' The name is pretty self-explanatory: DIBs are simply a way of interacting with bitmaps in any color mode or on any computer and getting consistent results. There are actually two varieties of DIBs - OS/2 encoded and Windows encoded, so I'm not entirely sure of why they're called "device independent'... but that's okay. :)

DIBs share many characteristics with BitmapBits. The calls share certain parameters and the underlying logic is very much the same. However, DIB sections suffer from three unfortunate problems: they're slightly more confusing to use, they require more code, and they return the image data rotated 180 degrees (of course, they also rotate the data 180 degrees when you set it so this usually isn't a problem). The trade-off, of course, is that DIB sections work in any color mode and the StretchDIBits call is much more powerful than SetBitmapBits. Below are the required DIB section declarations:

Private Type BITMAP

bmType As Long

bmWidth As Long

bmHeight As Long

bmWidthBytes As Long

bmPlanes As Integer

bmBitsPixel As Integer

bmBits As Long

End Type

Private Declare Function GetObject Lib "gdi32" Alias "GetObjectA" (ByVal hObject As Long, ByVal nCount As Long, ByRef lpObject As Any) As Long

 

Private Type RGBQUAD

rgbBlue As Byte

rgbGreen As Byte

rgbRed As Byte

rgbAlpha As Byte

End Type

 

Private Type BITMAPINFOHEADER

bmSize As Long

bmWidth As Long

bmHeight As Long

bmPlanes As Integer

bmBitCount As Integer

bmCompression As Long

bmSizeImage As Long

bmXPelsPerMeter As Long

bmYPelsPerMeter As Long

bmClrUsed As Long

bmClrImportant As Long

End Type

 

Private Type BITMAPINFO

bmHeader As BITMAPINFOHEADER

bmColors(0 To 255) As RGBQUAD

End Type

 

Private Declare Function GetDIBits Lib "gdi32" (ByVal hDC As Long, ByVal hBitmap As Long, ByVal nStartScan As Long, ByVal nNumScans As Long, lpBits As Any, lpBI As BITMAPINFO, ByVal wUsage As Long) As Long

Private Declare Function StretchDIBits Lib "gdi32" (ByVal hDC As Long, ByVal x As Long, ByVal y As Long, ByVal dWidth As Long, ByVal dHeight As Long, ByVal SrcX As Long, ByVal SrcY As Long, ByVal SrcWidth As Long, ByVal SrcHeight As Long, lpBits As Any, lpBI As BITMAPINFO, ByVal wUsage As Long, ByVal RasterOp As Long) As Long

 

Quite the mess of declarations, isn't it? You should notice some similarities between these declarations and the BitmapBits ones - that makes our job somewhat easier. Here's the quick explanation of all that stuff:

TOTALLY USELESS SIDE NOTE ON ALPHA CHANNELS: This is just a thought to add to the "useless but cool" programming section of your brain: both DIBs and regular bitmaps can contain transparency information. If you have an expensive monitor and video card and run them at 32-bit color mode, that extra byte contains (mostly useless) transparency data. So technically, 32-bit bitmaps and DIBs could be used like GIFs - displayed transparently and all. There's actually an API call with Win2K/ME/XP called "AlphaBlend" that's similar to BitBlt/StretchBlt except that it utilizes an alpha channel (this method is very similar to DirectX and a transparent key color); you can read all about the "AlphaBlend" call at the MSDN site.

The GetDIBits call is somewhat more complicated than the GetBitmapBits one, so let's go through it one part at a time.

If you thought GetDIBits was long-winded, you're not going to like StretchDIBits very much :). StretchDIBits is a very powerful call, but there's also a lot of parameters we must provide. If you are familiar with BitBlt and/or StretchBlt, this part will probably make a lot of sense to you. The StretchDIBits parameters are:

That's a freaking lot of explanation. I need a break, and you should take one too! [Tanner goes outside and plays football for several hours]

USING THE DIB SECTION CALLS

 Okay - now that your brain has had some time to digest all of that, let's move onto using the GetDIBits call. Here's a full-blown example of how to get an image's data using the GetDIBits call, minus the declarations above:

________________________________________________________

'Routine to get an image's pixel information into an array dimensioned (rgb, x, y)

Public Sub GetImageData(ByRef SrcPictureBox As PictureBox, ByRef ImageData() As Byte)

'Declare us some variables of the necessary bitmap types

Dim bm As Bitmap

Dim bmi As BITMAPINFO

'Now we fill up the bmi (Bitmap information variable) with all of the appropriate data

bmi.bmHeader.bmSize = 40 'Size, in bytes, of the header (always 40)

bmi.bmHeader.bmPlanes = 1 'Number of planes (always one for this instance)

bmi.bmHeader.bmBitCount = 24 'Bits per pixel (always 24 for this instance)

bmi.bmHeader.bmCompression = 0 'Compression: standard/none or RLE

'Calculate the size of the bitmap type (in bytes)

Dim bmLen As Long

bmLen = Len(bm)

'Get the picture box information from SrcPictureBox and put it into our 'bm' variable

GetObject SrcPictureBox.Image, bmLen, bm

'Build a correctly sized array

ReDim ImageData(0 To 2, 0 To bm.bmWidth - 1, 0 To bm.bmHeight)

'Finish building the 'bmi' variable we want to pass to the GetDIBits call (the same one we used above)

bmi.bmHeader.bmWidth = bm.bmWidth

bmi.bmHeader.bmHeight = bm.bmHeight

'Now that we've completely filled up the 'bmi' variable, we use GetDIBits to take the data from

'SrcPictureBox and put it into the ImageData() array using the settings we specified in 'bmi'

GetDIBits SrcPictureBox.hDC, SrcPictureBox.Image, 0, bm.bmHeight, ImageData(0, 0, 0), bmi, 0

End Sub

________________________________________________________

Okay, kids, we're almost done with DIB sections - all that's left is StretchDIBits.

The procedure required to set up our variables for StretchDIBits is almost identical to the procedure we used for GetDIBits. In fact, everything up to the actual GetDIBits call is the same - everything except the ReDim ImageData() line, of course (if we ReDimmed the array before setting the data we would erase all of the pixel data!). Here's a full example:

________________________________________________________

'Routine to set an image's pixel information from an array dimensioned (rgb, x, y)

Public Sub SetImageData(ByRef DstPictureBox As PictureBox, ByRef ImageData() As Byte)

'Declare us some variables of the necessary bitmap types

Dim bm As Bitmap

Dim bmi As BITMAPINFO

'Now we fill up the bmi (Bitmap information variable) with all of the appropriate data

bmi.bmHeader.bmSize = 40 'Size, in bytes, of the header (always 40)

bmi.bmHeader.bmPlanes = 1 'Number of planes (always one for this instance)

bmi.bmHeader.bmBitCount = 24 'Bits per pixel (always 24 for this instance)

bmi.bmHeader.bmCompression = 0 'Compression: standard/none or RLE

'Calculate the size of the bitmap type (in bytes)

Dim bmLen As Long

bmLen = Len(bm)

'Get the picture box information from DstPictureBox and put it into our 'bm' variable

GetObject DstPictureBox.Image, bmLen, bm

'Now that we know the object's size, finish building the temporary header to pass to the StretchDIBits call

'(continuing to use the 'bmi' we used above)

bmi.bmHeader.bmWidth = bm.bmWidth

bmi.bmHeader.bmHeight = bm.bmHeight

'Now that we've built the temporary header, we use StretchDIBits to take the data from the

'ImageData() array and put it into SrcPictureBox using the settings specified in 'bmi' (the

'StretchDIBits call should be on one continuous line)

StretchDIBits DstPictureBox.hDC, 0, 0, bm.bmWidth, bm.bmHeight, 0, 0, bm.bmWidth, bm.bmHeight, ImageData(0, 0, 0), bmi, 0, vbSrcCopy

'Since this doesn't automatically initialize AutoRedraw, we have to do it manually

'Note: always keep AutoRedraw as 'True' when using DIB sections. Otherwise you will

'get unpredictable results.

If DstPictureBox.AutoRedraw = True Then

DstPictureBox.Picture = DstPictureBox.Image

DstPictureBox.Refresh

End If

End Sub

________________________________________________________

 

And there you have it - a complete explanation of how to get image data from any picture in any color mode and how to set that same data back into a picture once you're done editing it.

PART 3 - SEE DIB SECTIONS IN ACTION

DOWNLOAD THE DIB SECTION EXAMPLE PROGRAM

The included .zip file shows these exact routines - cut and pasted out of this tutorial into a form - being used to adjust the brightness of an image. While this is a pretty fast example, it's not quite as fast as is actually possible. To get it that fast, you'll have to read about additional optimizations in tutorial 4! (Or, you can download my Advanced Brightness Example to see instantaneous brightness adjustment - now that's some fast graphics!)

PART 4 - SOME FINAL THOUGHTS ON DIB SECTIONS

Well students, we've taken quite a journey through ways of getting and setting pixels in Visual Basic. We began with the appalling PSet and Point - the slowest possible way to do graphics. From there we were introduced to the wonders of the Windows API and we used GetPixel and SetPixel/V to speed up our graphics routines a great deal. In this last tutorial we delved into the magic of BitmapBits and DIB sections, and now your graphics programs should be faster than you ever imagined possible in VB! While this tutorial provides a lot of information, to really become knowledgeable about graphics programming you're going to have to do some work on your own.

One of the best ways to learn about graphics programming is just to download other people's programs off of the net and figure them out one line at a time. That's how I learned to program, and it's taught me a great deal about the many different ways that there are to do things within Visual Basic. All of my knowledge about DIB sections comes from reading articles on the internet and experimenting with the API - and I know there are people out there a lot smarter than me! If you're willing to do a little bit of studying, the sky's the limit!


Well, you brand-new DIB expert: to complete this series, I've included one last tutorial on graphics optimizations in general. Be sure to check it out next!

CONTINUE TO TUTORIAL 4

 

Copyright 2002 by Tanner "DemonSpectre" Helland. This article may not be reproduced in any form (printed or electronic) without prior written consent from the author. This site may, however, be hyperlinked on the world wide web without permission from the author.

This programming source code is provided "as is". In no event shall the author or any of his affiliates be liable for any consequential, special, incidental or indirect damages of any kind arising out of the delivery, performance or use of this source code, to the maximum extent permitted by applicable law. While the source code has been developed with great care, it is not possible to warrant that it is error free. This source code is not designed or intended to be used in any activity that may cause personal injury, death or any other severe damage or loss.

Please contact tannerhelland@hotmail.com with feedback and questions regarding this tutorial.