Creating and using your own VC++ dlls
Author: Pieter Philippaerts

3. Processor Intensive function

In part 3, we're going to write a more complex function than we did in Part 2. This is where you can see the real benifits of using C++ instead of Visual Basic.
We're going to write a function that converts a picture to grayscale.

Before I continue the DLL tutorial, I'm first going to tell you how pointers work in C/C++, because we'll definitely need them in our new grayscale function.
In C/C++, the star ('*') can be interpreted in several ways. As you probably know, it's the multiplication operator, but it's also used to define and use pointers. I'm going to be *very* short with this explanation, so if you have never used pointers, I suggest you take a look at an excellent tutorial about pointers and arrays in C.

int *p;

The above code creates a new variable where we can store a pointer to an integer. We can change the location where the pointer is pointing to by simply assigning the new location like this:

p = [NEW LOCATION];

Where [NEW LOCATION] is the memory location where you want it to point to. This is often a very large (or very small) number.
To access the value of the integer where our pointer points to, we can use the following code:

*p = 5;

Where *p means we want to access the integer where the pointer points to, not the pointer itself.

int val = 5;
int *p;
int q;
p = &value;
q = value;
*p = 7;
// value is now equal to 7
q = 10;
// value is still equal to 7

In the above code, we first define an integer and assign the value '5' to it, then a pointer p and then an integer q.
Then we assign the address of val to our pointer (the '&' means 'address of') and copy the value of the integer val to q.
If we modify *p we also modify val because the pointer p points to the address of val. If we modify q, val doesn't change because q is a copy of val, not a pointer to val.

 

We're now ready to start programming our grayscale function.
Our fucntions needs the following parameters: a pointer to the 24bit bitmap data, the width of the bitmap and the height of a bitmap. Hence, the function declaration looks like this:

int _stdcall ConvertToGray(BYTE *picData, int width, int height) {

In Visual Basic this translates to either

Private Declare Function ConvertToGray Lib "MyDll.dll" (ByRef picData As Byte, ByVal width As Long, ByVal height As Long) As Long

or

Private Declare Function ConvertToGray Lib "MyDll.dll" (ByVal picData As Long, ByVal width As Long, ByVal height As Long) As Long

The difference between the two is that the first declaration requires a byte array to be passed to the function and the second fucntion wants a pointer to a byte array.
In the end, it's both the same because the pointer is passed ByVal and the byte array is passed ByRef.
We're going to use the second version because the CreateDIBSection function returns a pointer to the byte array, instead of the byte array itself.

In a moment, we're now going to start writing our Visual Basic code, but I'll explain what it's going to do first.
We're going to write code that first creates a 200x200 pixels 24bit Device Independant Bitmap, then copies a part of the desktop to that memory bitmap, then converts it to grayscale (with our VC++ function) and finally displays the result.

Ok, here we go:

Private Sub Form_Load()
    Dim bi24BitInfo As BITMAPINFO, Cnt As Long
    Dim hPointer As Long, iBitmap As Long, iDC As Long
    Me.AutoRedraw = True

First we declare the variables we're going to use. Nothing special to see here, except perhaps the bi24BitInfo declaration. The BITMAPINFO is a structure that we're going to need to create our Device Independant Bitmap.
We set the AutoRedraw property of the form to True, to make sure our grayscale picture doesn't get erased when the form redraws.

    With bi24BitInfo.bmiHeader
        .biBitCount = 24
        .biCompression = BI_RGB
        .biPlanes = 1
        .biSize = Len(bi24BitInfo.bmiHeader)
        .biWidth = 200
        .biHeight = 200
    End With

Then, we initialize the BITMAPINFO structure. Note that we're going to create a 24bit bitmap with width and height set to 200 pixels. 

    iDC = CreateCompatibleDC(0)
    iBitmap = CreateDIBSection(iDC, bi24BitInfo, _
      DIB_RGB_COLORS, hPointer, ByVal 0&, ByVal 0&)
    SelectObject iDC, iBitmap

We create our Device Independant Bitmap and select it into a newly created device context. Also note that the variable hPointer now contains the value of a pointer to the bitmap bits.

    BitBlt iDC, 0, 0, bi24BitInfo.bmiHeader.biWidth, _
      bi24BitInfo.bmiHeader.biHeight, GetDC(0), 0, _
      0, vbSrcCopy
    ConvertToGray hPointer, _
      bi24BitInfo.bmiHeader.biWidth, _
      bi24BitInfo.bmiHeader.biHeight
    BitBlt Me.hdc, 0, 0, bi24BitInfo.bmiHeader.biWidth, _
      bi24BitInfo.bmiHeader.biHeight, iDC, 0, 0, vbSrcCopy

The first BitBlt copies the pixels from the screen into our memory bitmap.
Then we call our VC++ function 'ConvertToGray'.
The second BitBlt copies the result to the form.

    DeleteDC iDC
    DeleteObject iBitmap
End Sub

These final lines are cleanup code to delete our memory bitmap from the memory.

 

Now that we have our Visual Basic code, we can finally start writing our ConvertToGray function. What this function basically does is looping trough the image, calculating the grayscale values of each pixel.

int _stdcall ConvertToGray(BYTE *picData, int width, int height) {
    int BytesPerScanLine = ((width * 3 + 3) & 0xfffffffc) - 3;
    BYTE *r, *g, *b;
    int index;
    for (int cy=0; cy<height; cy++) {
        index = cy * BytesPerScanLine;
        for (int cx=0; cx < BytesPerScanLine; cx+=3) {
            b = &picData[index];
            g = &picData[index + 1];
            r = &picData[index + 2];
            *r = (BYTE)(0.299 * *r + 0.587* *g + 0.114* *b);
            *g = *r;
            *b = *r;
            index += 3;
        }
    }
    return 1;
}

A bitmap is divided in several horizontal lines, called Scan Lines. A bitmap with a height of y pixels has y Scan Lines. The number of bytes in a scan line must be divisible by 4. If the number of bytes required by a bitmap to store one horizontal line of pixels is not divisable by 4, the line is padded with empty bytes until it is divisable by 4.
For instance, lets say we have a 3x3 24bit bitmap.This bitmap has 3 scan lines (because its height is 3). The number of bytes required to store one horizontal line of pixels is 9 (3 pixels x 3 bytes/pixel). Nine is not divisable by 4, so each scan line is padded with 3 empty bytes, because 9+3 = 12 = divisable by 4.
On the first line of code in our function, we calculate the number of bytes per scan line, and we store it in the integer 'BytesPerScanLine'.

We define three pointers and an integer. In this integer, we're going to store the current offset in the byte array.

We enter our first loop that counts from 0 to 'the height of the bitmap-1'.

We adjust the index, to make sure we start from the right offset in the array.

We enter our second loop which moves trough all the bytes of a scan line.

We assign values to aour pointers r, g & b (note that the pixels are stored in BGR format in the bitmap data).

We calculate the corresponding gray value and we assign this gray value to our r, g and b components.

We move our offset variable.

 

This is a very processor intensive function, because the code loops trough the entire bitmap, pixel per pixel. The code is quite fast (it takes only 130ms on my machine to convert a 1600x1200 picture to grayscale). A similar function written in VB would be noticeable slower (also when it's compiled to Native code).