Basically Visual

October/November 1997

by Peter G. Aitken (c)1997

Originally published in Visual Developer magazine .


Map Them Bits!

Graphics, graphics everywhere! It's a graphical world, my friend, and the better able you are to use graphics in your programs, the better you will be able to compete. We Visual Basic programmers are lucky in that our chosen development tool provides a rich assortment of tools able to handle many graphical tasks with the object/event/property/method approach that we have all come to love. The PictureBox, PictureClip, and Image controls, along with the Picture and Form objects, are usually all you need. But not always, as they do have some limitations. Perhaps the most serious limitation is the limited number of graphical file formats that are supported: bitmap, metafile, icon, JPEG, and GIF - that's all! Unless the image you are working with already exists in one of these formats, you are out of luck.

To work with other standard graphics file formats, such as Tagged Image File (TIF), Paintbrush (PCX), or encapsulated Postscript (EPS) you should look for a third party tool that does what you need. A number of vendors sell components that you can plug into your Visual Basic program to deal with different graphics file formats.

What if your graphics data is not in a standard file format? I faced this problem recently when developing a Visual Basic program to display and manipulate images acquired with a charge-coupled device (CCD) camera that was used to take images of experimental samples in a laboratory. The data from the camera arrived at the computer as a pure, unadulterated stream of integers, with each number representing the light level at an individual pixel. This is basically what any bitmap is – a series of numbers representing the brightness or color of pixels – but Visual Basic can make no sense out of raw data like this. Fortunately the Windows API provides the tools we need to convert raw pixel data into a Windows bitmap. Once you've got the picture in bitmap format, you are home free. It's a useful technique.

Windows Bitmaps

Windows supports two types of bitmaps. The older type is called a device dependent bitmap, or DDB for short. As the name implies, this type of bitmap is dependent on the specific display device for which it was originally created, and cannot be moved to a different device without potential problems. DDB's were introduced with versions of Windows prior to 3.0, and Windows 95/NT supports this type of bitmap only for purposes of backward compatibility.

The device independent bitmap, or DIB, was Microsoft's answer to these problems. A DIB contains, in addition to the raw pixel data, information about the color format, resolution, and palette of the device on which the bitmap was created (there's additional information in a DIB, as you'll see soon). The DIB contains within itself all of the information that a program needs to display the image properly on any device. This information is stored in a BITMAPINFO structure, which is defined as follows:

Type BITMAPINFO

bmiHeader As BITMAPINFOHEADER

bmiColors As RGBQUAD

End Type

Well, that's not too helpful! Two more structures to deal with. The second one, RGBQUAD, is concerned with the bitmap's color table and we'll deal with it soon. The BITMAPINFOHEADER structure contains the device information that I have been talking about:

Type BITMAPINFOHEADER

biSize As Long

biWidth As Long

biHeight As Long

biPlanes As Integer

biBitCount As Integer

biCompression As Long

biSizeImage As Long

biXPelsPerMeter As Long

biYPelsPerMeter As Long

biClrUsed As Long

biClrImportant As Long

End Type

Let's look at the various members of this data structure. I have simplified this explanation to cover the types of bitmaps you'll be working with most often. There are more details that you can investigate if you need to go beyond the basics.

For most applications, you will need to be concerned only with three members of this structure: biWidth, biHeight, and biBitCount. The others are all set to their default values or to 0.

Colors in Bitmaps

Any color on a computer is represented by an RGB value that gives the relative intensities of the three primary colors red, green, and blue. If all three primaries are at their minimums – an RGB value of 0,0,0 – the result is black. If all three are at their maximums, the result is white. Intermediate RGB values represent the entire spectrum of visible colors. In most RGB schemes, each primary is represented by one byte and can therefore take values in the range 0-255. This gives a total of 256 * 256 * 256 or over 16 million different colors.

Bitmaps store RGB values in two ways. One type is indexed color bitmaps, used when the bits per pixel values (biBitCount) is 1, 4, or 8. These bitmaps contain a color table, or palette, that holds the actual RGB values used by the bitmap. Each individual pixel's data specifies which entry in the color table to use for that pixel. The color table contains two or more elements; each element is an RGBQUAD structure, defined as follows:

Type RGBQUAD

rgbBlue As Byte

rgbGreen As Byte

rgbRed As Byte

rgbReserved As Byte

End Type

You can see that three bytes of an RGBQUAD structure are used to hold the red, green, and blue values; the fourth byte in this structure is unused. The color table consists of an array of type RGBQUAD; the number of elements in the array depends on the number of colors the bitmap can display, which in turn depends on the number of bits used for each pixel - in other words, the biBitCount member. If each pixel is represented by n bits, the maximum number of colors in the bitmap is therefore 2n. Thus, a 1 bit per pixel bitmap can represent 2 colors and the color table will contain 2 RGBQUAD elements. For 4 and 8 bit per pixel bitmaps the values are 16 and 256 respectively.

The second type of bitmap applies when the bits per pixel value is 16, 24, or 32. In these bitmaps there is no color table. Rather each pixel's data represents the color's RGB value directly. In a 24 bit per pixel bitmap the representation is straightforward - one byte per color. In a 32 bit per pixel bitmap there is also one byte per color, with the fourth byte going unused. Things get a bit more complicated with a 16 bit per pixel bitmap. In the most common arrangement, 15 of the 16 bits are used with the least significant 5 bits representing blue, followed by 5 bits for green and 5 for red. This mode permits 215 or approximately 32,000 different colors, and is sometimes referred to as high color mode (as opposed to the 24 and 32 bit per pixel modes which are called true color).

Pixel Data

The pixel data from which the bitmap will be constructed must be in an array. It does not matter whether this is a 1 or 2 dimensional array as long as it is the correct size and the data is arranged properly. In this case "properly" means that the pixels start at the lower left corner of the image and then move across and up. For an 8 bit per pixel bitmap that will be 100 pixels wide and 50 pixels high, for example, the first byte in the array corresponds to the pixel in the lower left corner of the bitmap, the 100th byte corresponds to the pixel in the lower right corner, the 101st byte corresponds to the first pixel in the second row from the bottom, and so on. This is counter to the normal Windows way of treating the top left corner of an mage as its origin.

There's one catch: each row in the data array must end on a word boundary. In other words, each row must contain a number of bytes that is evenly divisible by 4. If the dimensions of the bitmap do not fall on this boundary, the data array must be padded with 0's to meet this requirement. Let's look at an example. Suppose you are creating an 8 bit per pixel bitmap that is 50 pixels wide and 100 pixels high. Each row is 50 bytes, which does not divide evenly by 4. We must pad each row to 52 bytes to meet the Word alignment requirement. Rather than dimensioning an array like this:

Dim PixelData(4999) as Byte

we must do this:

Dim PixelData(5199) as Byte

Then, PixelData(0) through PixelData(49) will contain the data for the bottom row of the bitmap, and PixelData(50) and PixelData(51) are not used. Continuing on, PixelData(52) through PixelData(101) will contain the data for the next to bottom row of the bitmap, and PixelData(102) and PixelData(103) are not used.

Constructing a Palette

If you are creating a bitmap with a color table, then you need to create a color table or palette for the bitmap. The size of the color table is determined by the bits per pixel parameter, as described above. Then simply declare your type BITMAPINFO containing an array of type RGBQUAD of the appropriate size. Here's the BITMAPINFO declaration for an 8 bits per pixel bitmap (256 colors):

Type BITMAPINFO256

bmiHeader As BITMAPINFOHEADER

bmiColors(255) As RGBQUAD

End Type

For a 1 bit per pixel bitmap (2 colors) you would use this:

Type BITMAPINFO2

bmiHeader As BITMAPINFOHEADER

bmiColors(1) As RGBQUAD

End Type

Note that I have assigned different Type names to these two structures, permitting you to use both in the same program. To initialize the color table, you need to know what colors your bitmap will require. For the simplest case, a 1 bit per pixel bitmap that displays black and white, you would initialize the color table like this:

Dim biDib As BITMAPINFO2

biDib.bmiColors(0).rgbRed = 0

biDib.bmiColors(0).rgbGreen = 0

biDib.bmiColors(0).rgbBlue = 0

biDib.bmiColors(0).rgbReserved = 0

biDib.bmiColors(1).rgbRed = 255

biDib.bmiColors(1).rgbGreen = 255

biDib.bmiColors(1).rgbBlue = 255

biDib.bmiColors(1).rgbReserved = 0

For larger color tables, you may be able to automate the initialization process with a loop. The following code creates a 256 color table in shades of gray, with the first table entry black and the last one white:

Dim biDib as BITMAPINFO256

For i = 0 To 255

biDib.bmiColors(i).rgbRed = i

biDib.bmiColors(i).rgbGreen = i

biDib.bmiColors(i).rgbBlue = i

biDib.bmiColors(i).rgbReserved = 0

Next i

Initializing the BITMAPINFOHEADER Structure

The final step that is required before creating the bitmap is to initialize the BITMAPINFOHEADER structure. As I explained earlier, there are only three members of this structure that are important in most bitmaps; biWidth, biHeight, and biBitCount. The other members have to be initialized all the same, setting most to 0. Assume that the following code has already been executed:

Type BITMAPINFO256

bmiHeader As BITMAPINFOHEADER

bmiColors(255) As RGBQUAD

End Type

Dim biDib As BITMAPINFO256

Then we can initialize the header for an 8 bits per pixel, 100 x 200 pixel bitmap as follows:

With biDib.bmiHeader

.biSize = Len(biDib.bmiHeader)

.biWidth = 100

.biHeight = 200

.biPlanes = 1

.biBitCount = 8

.biCompression = BI_RGB

.biSizeImage = 0

.biXPelsPerMeter = 0

.biYPelsPerMeter = 0

.biClrUsed = 0

.biClrImportant = 0

End With

We have declared the necessary header structures, initialized the relevant data members, and constructed the color table. All of the preliminaries are complete.

Creating the Bitmap (finally!)

When you create a bitmap in Windows, it cannot be created in isolation - it must be created in relation to a display device. This does not mean that the bitmap is necessarily going to be displayed on that device, but only that details of the device will be used in creating the bitmap. To be honest with you, I have never understood why a device independent bitmap requires a specific device in order to be created, but that's the way things work. Fortunately, it presents no problem. Windows represents specific devices as device contexts, and you can obtain a device context with the API function GetDC(). The function declaration is shown here:

Declare Function GetDC Lib "user32" (ByVal hwnd As Long) As Long

If you pass the hWnd property of a Visual Basic form, the function return's that form's device context. If you pass 0, which is what we will do here, it returns the device context of the entire screen. Why turn to the API when we could use a form's device context? We do not know where the bitmap creation code will be placed. It may end up in a class module or an ActiveX control, and there is no assurance that the code will be associated with a form. We ensure the availability of a usable device context by using GetDC(0).

The actual bitmap creation is performed by the API function CreateDIBitmap(). Its declaration is as follows:

Declare Function CreateDIBitmap Lib "gdi32" _

(ByVal hdc As Long, lpInfoHeader As _

BITMAPINFOHEADER, ByVal dwUsage As Long, _

lpInitBits As Any, lpInitInfo As BITMAPINFO, _

ByVal wUsage As Long) As Long

The function returns a handle to the new bitmap, or Null on failure. The arguments are described here:

hdc The hDC you obtained from GetDC().

lpInfoHeader The type BITMAPINFOHEADER structure containing bitmap parameters

dwUsage Specifies whether the bitmap will be initialized or not. If this argument is CBM_INIT (value = &H4) then the bitmap will be initialized with the data in the array indicated by the lpInitBits argument. If set to 0, the bitmap is not initialized (you get a bitmap with no picture) and the lpInitBits argument can be 0.

lpInitBits The array containing the pixel data.

lpInitInfo The type BITMAPINFO structure.

wUsage Set to DIB_RGB_COLORS (value = 0) if the color table contains RGB values. Set to DIB_PAL_COLORS (value = 1) if the color table contains palette entries (not covered here).

Using Your New Bitmap

Once you have successfully created a bitmap, then what? While Visual Basic includes a variety of tools of working with bitmaps, they are unfortunately all designed to use bitmaps that are on disk. If you create a bitmap in memory, there is nothing you can do with it using Visual Basic on its own. You cannot, for example, directly display a memory bitmap in a Picture Box or a Form. What to do?

Again the API comes to the rescue. We will use the important Windows concept of a memory device context, which can be thought of as a virtual display device - it exists in memory but is not associated with any physical device. A memory device context will, however, have the logical characteristics - will be compatible with - a specific physical display device. Once you have created a memory device context, you can draw and otherwise manipulate the "image" that is located there. Among the possible manipulations is to select a bitmap into the device context. Then, once the bitmap is "displayed" on the virtual device you can use one of the API's so-called blit functions to transfer it to a real device (a compatible device, of course), resulting in the bitmap being displayed on-screen or printed.

To create a memory device context you use the function CreateCompatibleDC(). The declaration is:

Declare Function CreateCompatibleDC Lib "gdi32" _

(ByVal hdc As Long) As Long

The function's single argument is the device context of the device that you want the memory device context to be compatible with. Pass a value of 0 to get a device context compatible with the application's current screen. The function returns the device context, or 0 on failure.

Once you have created the compatible device context, the next step is to select the bitmap into it. This is accomplished with the SelectObject() API function:

Declare Function SelectObject Lib "gdi32" _

(ByVal hDC As Long, ByVal hObject As Long) As Long

The argument hDC is the compatible device context that was returned by the CreateCompatibleDC() function, and hObject is the handle of the object - in our case the bitmap - that you are selecting. The function's return value is usually ignored.

Why not just select the bitmap directly into the device context of a real screen object - a Form, for example? You can - but it won't do what you want. The bitmap will not be displayed. You must use the seemingly roundabout method described here.

To display the bitmap, you must transfer it from the memory device context to a "real" device context, one associated with, for example, a Visual Basic form. This kind of graphical transfer is done with the "blitting" functions. I cannot go into full details on this very flexible and powerful family of API functions, and will limit myself to explaining the basics of the most useful one, StretchBlt(). In a nutshell, StretchBlt() takes a rectangular image from a source device context and copies it to a rectangle in a destination device context, stretching or shrinking the image as needed. The declaration is shown here:

Declare Function StretchBlt Lib "gdi32" (ByVal DestDC As Long, _

ByVal DestX As Long, ByVal DestY As Long, _

ByVal DestWidth As Long, ByVal DestHeight As Long, _

ByVal SrcDC As Long, ByVal SrcX As Long, _

ByVal SrcY As Long, ByVal SrcWidth As Long, _

ByVal SrcHeight As Long, ByVal dwRop As Long) As Long

DestDC The destination device context.

DestX, DestY The destination X and Y coordinates where the top left of the rectangle is to be placed.

DestWidth, DestHeight The destination Width and Height of the rectangle.

SrcDC The source device context.

SrcX, SrcY The source X and Y coordinates of the top left of the rectangle to be copied.

SrcWidth, SrcHeight The source dimensions of the rectangle to be copied.

dwRop The type of copy operation to be performed. Use SRCCOPY (value &HCC0020) to perform a basic copy operation.

Because API graphics functions use pixels as their units of measurement, you should set the ScaleMode property to Pixels before blitting to a Visual Basic form or Picture Box. After the image has been displayed, you should release the memory device context as you no longer need it. For this you use the DeleteDC() function:

Declare Function DeleteDC Lib "gdi32" (ByVal hdc As Long) As Long

This function's one argument is the device context to be deleted. Now let's look at some code. Assume that hBMP is a bitmap handle that you obtained when you created a bitmap as described earlier in this chapter, and that this bitmap's dimensions are in the variables xSize and ySize. The following code can be placed in a Visual Basic form's Paint() event procedure. Remember to set the form's ScaleMode property to Pixels, and AutoRedraw to False.

Dim cDC As Long

If hBMP Then

cDC = CreateCompatibleDC(0)

SelectObject cDC, hBMP

Call StretchBlt(hdc, 0, 0, ScaleWidth, ScaleHeight, _

cDC, 0, 0, xSize, ySize, SRCCOPY)

DeleteDC cDC

End If

In the argument list to StretchBlt(), hdc, ScaleWidth, and ScaleHeight refer to properties of the form. By specifying ScaleWidth and ScaleHeight as the size of the destination rectangle, we cause the source bitmap to be stretched as needed to fill the entire form, whatever its size. By specifying 0,0 as the top left coordinates for both source and destination we cause the top left corner of the bitmap to be placed in the top left corner of the form.

Why place this code in the form's Paint() event procedure? The Paint event is automatically triggered every time the form needs to be redrawn, such as when another window that was covering it is moved away, or when the form is restored from the minimized state. By placing your drawing code in this event procedure you ensure that the contents of the form are redrawn when needed. Of course you can also place the drawing code in a separate procedure, as long as it is called from Paint(). If the AutoRedraw property of the object containing the bitmap is set to True, you do not need to repaint the image in this manner because Windows keeps a copy in memory and does the redrawing automatically. It is necessary only to draw the bitmap once. As we'll see below, setting AutoRedraw to True is necessary if you want to be able to save the bitmap to disk. However AutoRedraw introduces some additional overhead that you may wish to avoid in a graphics intensive application.

What happens if you draw a bitmap on a form that has controls on it? No problem - the bitmap goes behind the controls. This can be a nice method for creating visually attractive forms that go beyond what is possible with the BackColor property. Of course, if the bitmap exists in a disk file you do not need to go to all this trouble. You can simply use the LoadPicture() function to load the bitmap into a Picture object, and then use the Render method to copy the bitmap to a form or Picture Box. You can find the details in the Visual Basic on-line help. I'm sure you will note that Render is a thinly disguised version of StretchBlt()!

Saving a Bitmap

Once you have created and displayed a bitmap, can you save it to disk? Yep - and this is an easy one. The SavePicture statement is all you need:

SavePicture Image, FileName

Image is the Image property of the Form or Picture Box where the bitmap has been displayed, and Filename is the name of the file. Images are stored as bitmap files with the .BMP extension. The only limitation is that to use SavePicture, the AutoRedraw property of the Form or Picture Box containing the image must be True. With AutoRedraw set to True, you must also execute the Refresh method immediately after blitting the bitmap in order to initially display it.

A Demonstration

The program presented in Listings 1 and 2 shows how to create a bitmap, display it in a Picture Box control, and save it to disk. The bitmap was generated by a mathematical algorithm to display an even graduation from dark to light blue - something that you may find useful for form backgrounds in your projects. To create the project, place a PictureBox on a Form and set the Picture Box's AutoRedraw property to True and its ScaleMode property to Pixel. Use the menu editor to create a single menu with the Caption Bitmap and the Name mnuBitmap. Add the following three menu commands to the menu:

Caption Name

&Create mnuBitmapCreate

&Save mnuBitmapSave

E&xit mnuBitmapExit

Use the Project, Add Module command to add a Basic module to the project. The code in Listing 1 goes in the Basic module, and the code in Listing 2 in the form module. When you run the project, select Create from the menu to generate the bitmap and display it in the Picture Box. You'll see how the bitmap display is retained even when you resize the form, or temporarily hide it. Try setting the Picture Box's AutoRedraw property to False and commenting out the call to DisplayBitmap in the Paint event procedure to see how things change. You'll find that the bitmap is not redrawn after being temporarily hidden.

There's more to bitmaps, but we have covered the most important areas. Being able to create Windows bitmaps, either from your own generated data or from pixel data obtained from external devices, is a valuable programming tool to add to your arsenal.

Listing 1. Code in the BITMAP.BAS module.

' Constants

Public Const SRCCOPY = &HCC0020

Public Const BI_RGB = 0&

Public Const CBM_INIT = &H4

Public Const DIB_RGB_COLORS = 0

' Types

Public Type BITMAPINFOHEADER

biSize As Long

biWidth As Long

biHeight As Long

biPlanes As Integer

biBitCount As Integer

biCompression As Long

biSizeImage As Long

biXPelsPerMeter As Long

biYPelsPerMeter As Long

biClrUsed As Long

biClrImportant As Long

End Type

Type RGBQUAD

rgbBlue As Byte

rgbGreen As Byte

rgbRed As Byte

rgbReserved As Byte

End Type

Type BITMAPINFO

bmiHeader As BITMAPINFOHEADER

bmiColors(255) As RGBQUAD

End Type

' Function declarations

Declare Function StretchBlt Lib "gdi32" (ByVal hdc As Long, _

ByVal x As Long, ByVal y As Long, ByVal nWidth As Long, _

ByVal nHeight As Long, ByVal hSrcDC As Long, _

ByVal xSrc As Long, ByVal ySrc As Long, _

ByVal nSrcWidth As Long, ByVal nSrcHeight As Long, _

ByVal dwRop As Long) As Long

Declare Function GetDC Lib "user32" (ByVal hwnd As Long) As Long

Declare Function CreateCompatibleDC Lib "gdi32" (ByVal hdc As Long) _

As Long

Declare Function DeleteDC Lib "gdi32" (ByVal hdc As Long) As Long

Declare Function SelectObject Lib "gdi32" (ByVal hdc As Long, _

ByVal hObject As Long) As Long

Declare Function CreateDIBitmap Lib "gdi32" (ByVal hdc As Long, _

lpInfoHeader As BITMAPINFOHEADER, ByVal dwUsage As Long, _

lpInitBits As Any, lpInitInfo As BITMAPINFO, _

ByVal wUsage As Long) As Long

 

Listing 2. Code in the BITMAP.FRM form.

Option Explicit

' Bitmap dimensions

Const XSIZE = 200

Const YSIZE = 200

' Starting value for bottom row.

Const STARTVAL = 55

' Maximum value, 255 or less.

Const LASTVAL = 255

' For the bitmap pixel data.

Dim Data() As Byte

' The bitmap header.

Dim bm As BITMAPINFO

' For the bitmap handle.

Dim hBMP As Long

Private Sub InitColorTable()

Dim i As Integer

' Create a color table that ranges

' from light to dark blue.

For i = 0 To 255

bm.bmiColors(i).rgbBlue = i

bm.bmiColors(i).rgbRed = 0

bm.bmiColors(i).rgbGreen = 0

bm.bmiColors(i).rgbReserved = 0

Next i

End Sub

Private Sub InitPixelData()

' Fills the Data() array with rows of

' gradually increasing values.

Dim i As Long, j As Long

Dim v As Byte, z As Long

z = XSIZE

z = z * YSIZE

ReDim Data(z - 1)

v = STARTVAL

For i = 0 To YSIZE - 1

For j = 0 To XSIZE - 1

Data(i * XSIZE + j) = v

Next j

If v < LASTVAL Then

v = v + 1

Else

v = STARTVAL

End If

Next i

End Sub

Private Sub CreateBitmap()

' Creates a device independent bitmap

' from the pixel data in Data().

Dim hdc As Long

With bm.bmiHeader

.biSize = Len(bm.bmiHeader)

.biWidth = XSIZE

.biHeight = YSIZE

.biPlanes = 1

.biBitCount = 8

.biCompression = BI_RGB

.biSizeImage = 0

.biXPelsPerMeter = 0

.biYPelsPerMeter = 0

.biClrUsed = 0

.biClrImportant = 0

End With

' Get the DC.

hdc = GetDC(0)

hBMP = CreateDIBitmap(hdc, bm.bmiHeader, _

CBM_INIT, Data(0), _

bm, DIB_RGB_COLORS)

End Sub

Private Sub Form_Resize()

' Make the Picture Box fill the form.

Pic1.Move 0, 0, ScaleWidth, ScaleHeight

End Sub

Private Sub mnuBitmapCreate_Click()

If hBMP = 0 Then

InitColorTable

InitPixelData

CreateBitmap

DrawBitmap

' Required because AutoRedraw is True.

Pic1.Refresh

End If

End Sub

Private Sub mnuBitmapExit_Click()

End

End Sub

Private Sub mnuBitmapSave_Click()

If hBMP Then

SavePicture Pic1.Image, App.Path & "\blue.bmp"

End If

End Sub

Private Sub Pic1_Paint()

' Not necessary if Redraw is True.

DrawBitmap

End Sub

Private Sub DrawBitmap()

Dim cDC As Long

If hBMP Then

cDC = CreateCompatibleDC(Pic1.hdc)

SelectObject cDC, hBMP

Call StretchBlt(Pic1.hdc, 0, 0, _

Pic1.ScaleWidth, Pic1.ScaleHeight, _

cDC, 0, 0, XSIZE, YSIZE, SRCCOPY)

DeleteDC cDC

End If

End Sub