http://www.developer.com/

Back to article

Visual Basic 6 Win32 API Tutorial


November 19, 2002

In the previous chapter, we looked at the basics of an API call and went over some examples of how to use them in VB. So you should now be ready to get down to some of the real nitty-gritty.

In this chapter we'll be investigating how API calls work at a much lower level. This will allow you to avoid some of the classic problems that have tripped up many other VB programmers in the past.

In this chapter we'll cover:

  • Data Types and API calls
  • Error handling with API calls
  • Other troublesome calling issues

Finally, we'll be introduced to the Encryption Program. We will use this application throughout the rest of this book as a test ground for our Win32 development. In this chapter we'll build the UI for this project as well as covering how the encryption algorithm will function although we'll leave the coding for the next chapter.

If you've ever programmed in languages other than Basic, you've probably noticed that most of the data types are very similar overall, but not exactly the same. For example, if you added the following variable declaration in Visual Basic:

Dim intCount as Integer

You would have a variable that could hold integer values from -32,768 to 32,767. However, if you make the following declaration in Visual C++:

int intCount;

intCount would have a range of -2,147,483,648 to 2,147,483,647.

I'm already envisioning a response like, "Well, what does this have to do with us? We're VB programmers, not Pascal or C programmers." True, you don't have to worry about other languages when you're in VB. But when you make an API call, you're starting to walk out of the world of Visual Basic and into foreign territory. There are a lot of little surprises in store for the VB developer who doesn't take the time to figure out the exact data type that should be passed into an API call. For example, say a C programmer made a DLL for you on April 1st named HaHa.DLL, and said that the Gotcha function takes one integer parameter called WatchOut. It also returns a string. Ignoring the subtle hints, you add the following declaration to your code:

Declare Function Gotcha Lib "HaHa" _
(WatchOut as Integer) as String

This looks like it should work OK, but if you try and use this function like a VB function, you will soon run into a few problems. After this chapter is done, you'll be able to see the warning signs immediately. For now, let's go over the data types that can do cause trouble if you're not careful (I'll leave the method of revenge that should be incurred upon that C developer as an exercise to the reader).

By the way, even though I'm going to show you code in C, don't think that you've got another language to learn. I'm only doing this to demonstrate the differences in data types between languages, especially with a language that is used to create DLLs. Appendix A lists the data types you should use in VB for the C data types found in most DLL calls.

Visual Basic Strings and API Calls

If you stay within the Visual Basic world, strings are pretty straightforward. Variable-length strings by definition grow and shrink with no special coding tricks. Here's a code snippet to demonstrate this:

Dim strTest as String

strTest = "First value."
strTest = ""
strTest = "Make it relatively bigger than before." 

There's no magic going on here. VB is handling whatever memory allocation is needed to add and remove space as needed. C programmers don't have that luxury. They have to declare their strings using character arrays, like the following C code line:

char strTest[13] = "First value.";

C also requires that you end, or terminate, any string with the null character ' '. Most VB programmers use the returned value from Chr$(0) to create this character. When you declare strTest in C this way, you don't have to explicitly add the null character to the end of the initialization value - it's done for you. Therefore, even though the string is only twelve characters long, we have to make sure there's room for that terminating character. You could allow the compiler to figure out how much space strTest needs by changing the code like this:

char strTest[] = "First value.";

but you could never have more than 13 characters in strTest after this declaration.

Of course, C has pointers and addresses that makes string manipulation much faster than VB, so if you have a lot of string processing going on in your application, you may want to have that friendly C programmer down the hall create a DLL for you to speed that up. (You may also want to tone down that revenge thing I mentioned before a little bit). But that's exactly where one of our problems lie. In C, strings are usually declared of the type LPSTR, or long pointer to a string. Here's what that looks like in memory:

Technically, the acronym LPSTR is a bit deceiving. In C, LPSTR is typed as:

typedef CHAR* LPSTR

This means that LPSTR is actually a pointer to the first character of a string.

As we have seen, this string is just an array of characters. In VB, however, strings are actually of a type called BSTR. Here's what this looks like:

VB used to use the HLSTR data type to define its strings. I won't go into the memory structure here, but if your fellow VB programmer who's stuck on maintaining a 16-bit application in version 3.0 is having problems with API calls and strings, you may want to start looking at this type. They used a pointer to a pointer to the first character in the string, which really made things confusing.

You may wonder why you don't see the termination character whenever you use strings in VB. It's one of those magical features of the tool - it simply hides it from you whenever you read from or write to strings. Technically, it is there and as we'll find out, it can be quite useful.

Differences Between Fixed and Variable Length Strings

Fixed-length strings are similar to variable-length strings in that they both hold character information. Also, you pass a fixed-length string ByVal just like you would for a variable-length string. The only way they differ is that with fixed-length strings…well, they're fixed in size. As we saw with variable-length strings, we can grow and shrink the string with ease. However, as this following line of code shows:

Dim strFixed as String * 5

we can only have 5 characters in the string at one time. Personally, I use variable-length strings when I make API calls that require strings as parameters, but there are always exceptions.

As you can see, there's one big difference between BSTRs and LPSTRs, and it's the length parameter that's tacked onto the beginning of the BSTR. There's a nice feature to this - VB doesn't have to calculate the length of a string every time the Len is called. It simply looks up the value stored within the string, and passes that back to the caller. (Of course, VB must change this value when the string changes length.) However, one major drawback of this is that a DLL function expects a different type of string. The DLL doesn't want a BSTR - it wants a LPSTR.

So how do you get around this? Is there some convoluted method to convert a BSTR into a LPSTR? The answer is no - once again, VB handles all of this for you. Let's look at a simple API call to illustrate this - it's called FindWindow:

Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, ByVal lpWindowName As String) _
As Long

This function will return a window handle for a window that matches the information given. What we're more concerned about here is how we're going to get that information to FindWindow correctly.

You may wonder why the function is really called FindWindowA in the DLL. This is the ANSI declaration; a Unicode version called FindWindowW exists as well. We'll cover Unicode later on in the chapter.

As it turns out, it's the ByVal keyword that saves us. When you pass a variable of a String type using this keyword, VB will actually pass a reference, or pointer to be exact, to your string. Furthermore, since your string is actually null-terminated internally, you don't have to append a termination character to the end of your string every time you call an API that requires a string.

There are a couple of catches here. The function will stop at the first null terminating character in the string. Remember that when VB passes the string to the API, it's actually passing the address of the first character to the call. Therefore, the call really has no idea how long the string is, or, more accurately, how much memory you've allocated to hold the string's contents. It's depending upon that end character to notify the DLL when the end of the string has been reached. So make sure that there are no null characters within the string when you pass it! Although you probably won't throw a memory exception if you do this, any of the data that exists past the first null character will not be read by the DLL.

Another catch is the keyword overloading. Although you have explicitly declared that the string should be passed ByVal, the string could get modified within the DLL. This is a head-twister when you first read it. You're passing it ByVal, but it's acting as though you passed it ByRef! Why is this the case? I really, really wish I knew! Personally, it would make sense to me that if the DLL function can change the contents of a variable, it should be passed ByRef. But that's not the case. This is one of those little quirks of the VB environment that, whether or not it makes logical sense, you must follow to ensure your strings are handled correctly by DLL functions.

Also, be sure that you read the documentation on the call you're making - since the DLL is actually referring to your string variable, they can alter the contents of the string itself. This is usually what you want to have happen in most calls, just don't be surprised if the data is different after an API call.

Usually when the DLL will change your string, it also will ask you to tell it how long the string is. For example, let's take a look at the following API call to illustrate this scenario:

Declare Function GetWindowsDirectory Lib "kernel32" _
Alias "GetWindowsDirectoryA" (ByVal lpBuffer As String, _
ByVal nSize As Long) As Long

This function returns the full path of the Windows directory. The lpBuffer is the variable that GetWindowsDirectory will fill with the path, and nSize is the length of the string. This function will return the length of the string copied into lpBuffer on success (minus the termination character), and zero on failure. Note that if the string you passed in wasn't big enough, this function will return the length of the buffer required. Here's how you'd get the path in VB:

Dim lngRet as Long
Dim lngSize as Long
Dim strWindowsPath as String
lngSize = 255
strWindowsPath = String$(lngSize, " ")
lngRet = GetWindowsDirectory(strWindowsPath, lngSize - 1)
If lngRet <> 0 then
If lngRet > lngSize Then
' We have to make the call again
' with a bigger string!
Else
strWindowsPath = Left$(strWindowsPath, lngRet)
End If
End If

What we're really doing here is creating a memory buffer. We use the String$ function to fill the string with 255 spaces. After calling GetWindowsDirectory, we check the return value (note that we didn't have to use the ByVal keyword for the two arguments, since the function was declared with the arguments ByVal). If everything's OK, we get rid of the spaces in strWindowsPath using the Left$ function. Note that we passed in the length of the string minus 1. This is kind of a "safety net". Since we know that we've allocated for 255 characters, and we also know that the string is null-terminated underneath the covers, we should expect that the DLL will only use those 255 characters. But to be safe, we'll tell the function that we've only allocated enough space for 254 characters.

The problem lies with initializing the string and passing in the right length value. If we're not careful in our code, we may pass in a value like 2555 for nSize. The DLL thinks that you've allocated 2555 bytes in your string, and it might try to use them all. If it actually tries to go past that 255th byte, you're in big trouble! Whatever memory exists past that 255th byte, we didn't allocate it. Therefore, you're almost guaranteed to cause a memory exception in this case. This is a classic problem that many VB programmers have run into, so make sure you pass in the size correctly!

Another problem with allocating the string is not doing it at all. In this case, the value of nSize always corresponds to the size of the string. However, some functions do not have arguments that allow you to tell the function how much space is available in a buffer, and require that you allocate enough space in a string. If you don't do this, it's possible that the DLL might try to write to a location in memory that your string hasn't allocated, and a memory exception will occur. Again, make sure the string is allocated correctly for the function used.

One nice trick that you can use to make sure you always pass in a nice, "safety net" value for string lengths is by using the Len function on the string, and then subtracting one from the value. This will ensure that the DLL function has enough valid space to write data to.

There's one more issue we need to address with strings. We now know how to pass them in, but what happens if the call returns a string? You may think that it's not that big of a deal, but if you're guessing that it gets really ugly, you're right. Fortunately, these calls are the exception and not the rule, but we still need to understand what we need to do to make the call correctly. Let's look at another API call:

Declare Function GetEnvironmentStrings Lib "kernel32" _
Alias "GetEnvironmentStringsA" () As String

All this API call does is return the current Windows environment settings. Note, though, that the Declaration Loader says the call returns a String data type. From what we just went through with string lengths, do you think this code will work?

Dim strEnv as String
strEnv = GetEnvironmentStrings

When I tried to do this, I got a bunch of garbage. DLL functions don't return a String data type the way you would in VB. They're actually returning a LPSTR - a pointer to the first character in a string. In fact, if you did some more research on this call, you'd see how the function was declared for a C point of view:

LPVOID GetEnvironmentStrings(VOID)

That LPVOID is telling us that we're getting a pointer back. As you know, VB doesn't have any kind of pointer operations to help us out, so it looks like we're stuck. Fortunately, there's an API call that can help us out here, and by using this call, we'll move into another area where memory exceptions can really nail you: typeless variable declaration.

To find out what the function declarations look like in C (like I did above for GetEnvironmentStrings), go to Microsoft's search site at http://search.microsoft.com/default.asp. All of the Win32 documentation is at their Premier level, but registering for this level of service is free

There's an API call that comes in really handy when you need to move blocks of memory around. It's called CopyMemory:

Declare Sub CopyMemory Lib "kernel32" _
Alias "RtlMoveMemory" (lpvDest As Any, _
lpvSource As Any, ByVal cbCopy As Long)

This procedure allows you to copy memory from one location to another. It does this by reading memory from a source (lpvSource) and copying it to another location in memory (lpvDest). The size of the copy is determined by cbCopy.

This procedure has been used by many VB programmers to solve a number of problems. I don't have the space to go over each application of the call, but if you use it, beware of the first two parameters. The first two characters of the argument names should point out (no pun intended) that these are pointers to blocks of memory. Furthermore, the declaration is made with the arguments As Any. The Any keyword is not a data type, like Variant; rather, it's used to let virtually any data type into the function call (it's analogous to the void keyword in C). This makes the call extremely flexible, since it will take virtually anything you pass into it. You can copy memory from longs to longs, from arrays to arrays, and so on. However, by doing this, you've left yourself wide open to a lot of errors. For example, what happens if you pass in a pointer to a string as the source, a pointer to a long as the destination, and set the length of the string as cbCopy? It may work, but I would never bet on it. You have to make sure what kind of information will go where, and if it will make any sense once it gets there.

It's even worse with strings. As we saw before, we have to pass in the string ByVal to force VB into passing a pointer to the first character in the string. The same holds true in this case. If we have a pointer to the string, it may seem like we could just pass in the value of this pointer ByRef. However, this would cause a memory exception, since we're not adhering to what VB is doing for us underneath the scenes.

Let's create an application called TestAPIStringReturn. This program simply makes a call to GetEnvironmentStrings and puts that information into a VB string. First off, before we jump into the code, we need to initialize the project. Add a module called StringLib, and add the CopyMemory declaration I gave before to this module. However, the GetEnvironmentSetting declaration, which should also be added to the StringLib module, has to change a bit:

Declare Function GetEnvironmentStrings Lib "kernel32" _
Alias "GetEnvironmentStringsA" () As Long

Also, add a form to the project with one command button. Change the property values in this project according to the following table:

Object Name Caption
Form frmStringReturn Test API String Return
Command Button cmdAPIGet Call API

It's not a very complex project, as the following screen shot shows:

Here's the call that uses the CopyMemory procedure to get the information returned to use via a pointer from GetEnvironmentStrings. The following code should be typed into the code window of your form:

Private Sub TestStringRet()
Dim lngRet As Long
Dim strDest As String
lngRet = GetEnvironmentStrings
strDest = String$(1000, " ")
CopyMemory ByVal strDest, ByVal lngRet, Len(strDest) - 1
Debug.Print strDest
End Sub

We call this function from the Click event of cmdAPIGet:

Private Sub cmdAPIGet_Click()
TestStringRet
End Sub

Let's take a look at what this method is doing in more detail. First, we get the pointer to the string via GetEnvironmentStrings:

lngRet = GetEnvironmentStrings

Then, we allocate space for our destination buffer strDest using the String$ function.

strDest = String$(1000, " ")

Next, we use the handy CopyMemory procedure to copy 1000 bytes from the string pointed at by lngRet to strDest:

CopyMemory ByVal strDest, ByVal lngRet, Len(strDest) - 1

Finally, we print out the contents of strDest to the Debug window:

Debug.Print strDest

Depending upon the length of the environment string, you may end up with a bunch of null characters in strDest. I won't demonstrate it here, but you could use string functions like Instr$, Left$, and Mid$ to find the first occurrence of a null and take off that character and any others that follow it.

You'll notice that both pointer parameters were passed in ByVal, even though the declaration says the parameters are going to be used by reference. This is good, since we're passing in pointers to strings as our values. If we made the call this way:

CopyMemory strDest, lngRet, Len(strDest)

we'd throw a memory exception (trust me, I've tried it - if you really must have a go yourself make sure that you save everything first). Also, you may have wondered why I arbitrarily buffered strDest with 1000 space characters. Since we don't know the length of the string that lngRet is pointing at, I just picked a number that I thought would be large enough to handle the result. Obviously, if you wanted to use this function in a production application, you'd have to be more creative to make sure you're getting all of the environment information into strDest.

The reason we could get this to work in the first place is because we handled the data types correctly on our end. By declaring the data type As Any in the API call, you're forced to make sure that the data you're copying from can be stored in the destination location you've given. If we take another look at the FindWindow procedure, you'll notice that VB wouldn't have allowed a Long to be passed into either of its arguments:

Declare Function FindWindow Lib "user32" _
Alias "FindWindowA" _
(ByVal lpClassName As String, _
ByVal lpWindowName As String) _
As Long

VB won't do a type check when you use As Any, you're on your own. However, by passing in two strings ByVal, the call is successful.

I'd suggest that you always type your arguments whenever possible. It makes your VB coding life a lot less painful. Sure, there are exceptions - like the one we just covered - but make sure you know what's going into your code, because VB is going to drop the ball on those data types.

Remember in Chapter 1 when I said that I'd get back to this function? Well, now is the time to reveal what the big stink about this function was. The function itself isn't the problem; it's the declaration from the API Viewer that causes it. If you pasted the declaration from the version supplied in VB 4.0 because you wanted to read INI files, you got something like this:

Declare Function GetPrivateProfileString Lib "kernel32" _
Alias "GetPrivateProfileStringA" (ByVal lpApplicationName _
As String, lpKeyName As Any, ByVal lpDefault As String, _
ByVal lpReturnedString As String, ByVal nSize As Long, _
ByVal lpFileName As String) As Long

Many VB programmers started to have a lot of problems in using this call, and if you look at the declaration hard enough, you'll see why. It's that second parameter that is causing all the problems. If you look at Microsoft's documentation for this parameter, you'll see this definition of lpKeyName:

Pointer to the null-terminated string containing the key name whose associated string is to be retrieved. If this parameter is NULL, all key names in the section specified by the lpAppName parameter are copied to the buffer specified by the lpReturnedString parameter.

If the parameter is a string value, why isn't it passed in ByVal like all the others? The hitch is when you have to pass in a null value. Declaring a string like this:

Dim strNull as String
strNull = ""

does not make the string null as far as a DLL function is concerned! VB has added a string constant called vbNullString, which you should use when you need to pass in a null value to a string parameter. Microsoft posted the fix on their web site, which said that the declaration should be (incidentally, this is the declaration you will get if you use the API Viewer with Visual Basic 6):

Declare Function GetPrivateProfileString _
Lib "kernel32" Alias "GetPrivateProfileStringA" _
(ByVal lpApplicationName As String, _
ByVal lpKeyName As Any, ByVal lpDefault _
As String, ByVal lpReturnedString As _
String, ByVal nSize As Long, _
ByVal lpFileName As String) As Long

However, I disagree with this declaration, since the data type declaration for the lpKeyName argument has been left as As Any. The function's declaration clearly states that the argument expects a pointer to a null-terminated string; so why would anyone want to pass in a UDT or Long, etc. to this function? I usually declare GetPrivateProfileString like this:

Declare Function GetPrivateProfileString _
Lib "kernel32" Alias _
"GetPrivateProfileStringA" (ByVal _
lpApplicationName As String, ByVal _
lpKeyName As String, ByVal lpDefault As String, _
ByVal lpReturnedString As String, _
ByVal nSize As Long, _
ByVal lpFileName As String) As Long

It makes my life a lot easier when I use it this way!

I think the reason why this mistake got more publicity than all of the other bugs that VB has had is that a lot of programmers needed to manipulate INI files. Therefore, this call probably got more use than, say, GetProcAddress. Whenever you make an API call, it's a good idea to find out exactly what the DLL is expecting. Check the SDK from Microsoft whenever possible - it might save you a lot of pain when you're trying to find a very subtle bug with parameter data types.

Arrays are pretty simple to use, but they tripped me up the first time I had to use them in a call. Granted, I didn't know what I was doing, so I'm trying to stop you from wasting time here.

Anyway, let's take a look at a call that we'll be using a lot throughout this book. It's called ReadFile:

Declare Function ReadFile Lib "kernel32" Alias "ReadFile" _
(ByVal hFile As Long, lpBuffer As Any, ByVal _
nNumberOfBytesToRead As Long, lpNumberOfBytesRead As _
Long, lpOverlapped As OVERLAPPED) As Long

This comes straight from the API Viewer. Remember what I said about not taking that tool at its word? We need to make some changes here:

Declare Function ReadFile Lib "kernel32" Alias "ReadFile" _
(ByVal hFile As Long, lpBuffer As Byte, ByVal _
nNumberOfBytesToRead As Long, lpNumberOfBytesRead As _
Long, ByVal lpOverlapped As Long) As Long

We'll deal with UDTs shortly. We've already dealt with the traps that can bite you by using the Any keyword for data types, which is why I've explicitly declared the type. I'm more concerned here with the lpBuffer variable. If you read the documentation on this call, it will tell you that this is a buffer that the DLL will put file information into. That's why we declare it as a Byte data type, since it seems very natural to handle file information in a Byte format. If we had decided to store the file information in a Long array that would be fine as well, but there are many data conversions that we would have to deal with. For example, say we needed to read in a file that was 16 bytes long. If we used a long array, we'd only have 4 elements, but we would have to do some bit manipulation to separate out each Byte from a Long array element. Since VB doesn't have a lot of intrinsic bit manipulation functions (especially the really cool ones like shift-left and shift-right), it's easier to use a type that maps to the file extremely well.

Now that we've got some of the data declaration issues out of the way, we can tackle the array issue. If an API is going to fill a buffer for us and we decide to use an array, we have to pass in the first element of the array to the call. Note that I didn't say "element 1". It doesn't matter what the first element is - in fact, we could pass in any element of our array into the call. For example, each of the ReadFile calls would work in this example:

Dim bytFile(1 to 10) as Byte
Dim bytFile2(1 to 10) as Byte
Dim lngHFile as Long
Dim lngRet as Long
Dim lngTotalBytes as Long

' Code to open the file would go here.
' lngHFile is the handle to the file.

lngRet = ReadFile(lngHFile, bytFile(1), 10&, lngTotalBytes, 0&)
lngRet = ReadFile(lngHFile, bytFile2(6), 5&, lngTotalBytes, 0&)

In the first call, we're trying to read 10 bytes from a file and passing that information into bytFile. In the second example, we're only reading 5 bytes and we're starting at element 6. The first five elements in bytFile2 won't be changed after the second ReadFile call.

When I tried to pass in an array to a DLL the first time, I wrote this:

lngRet = ReadFile(lngHFile, bytFile(), 10&, lngTotalBytes, 0&)

This didn't work. When you pass in an array, VB is actually passing in a pointer to the element of the array that you specify (which is why the parameter is passed in by reference). If I had read up on the documentation, I would have saved myself hours of frustration. I had assumed that the call would be able to "know" what it should do with my buffer.

The only place you should be concerned with arrays and API calls is telling the DLL how much space you've allocated. For example, let's rewrite the first call to ReadFile to virtually guarantee that we crash our application:

Dim bytFile(1 to 10) as Byte
Dim bytFile2(1 to 10) as Byte
Dim lngHFile as Long
Dim lngRet as Long
Dim lngTotalBytes as Long

' Code to open the file would go here.

lngRet = ReadFile(lngHFile, bytFile(1), _
15&, lngTotalBytes, 0&)

In this case, we've told the DLL that there are 5 more elements from the starting point than what we've allocated for in memory. As we saw with strings, there's no way that the DLL knows how long your array is - you have to specify that yourself. In this case, the DLL is going to try and write to elements 11, 12, 13, 14, and 15. However, the area of memory that exists after our array is off limits, so the chance of a memory exception occurring is quite high when the DLL goes beyond the array boundaries - if you want to experiment with this don't forget to save your work.

UDTs are usually not a problem when it comes to API calls. Whenever an API call requires a UDT to be passed in as a parameter, it needs to be passed in by reference. For example, the GlobalMemoryStatus allows you to find out the current status of some OS memory parameters. Here's what the call looks like:

Declare Sub GlobalMemoryStatus Lib "kernel32" _
(lpBuffer As MEMORYSTATUS)

That last parameter is a UDT - here's its definition:

Type MEMORYSTATUS
dwLength As Long
dwMemoryLoad As Long
dwTotalPhys As Long
dwAvailPhys As Long
dwTotalPageFile As Long
dwAvailPageFile As Long
dwTotalVirtual As Long
dwAvailVirtual As Long
End Type

Don't forget that you can get this information from the API Viewer as well, but you have to go to Types instead of Declares.

As we stated before, the MEMORYSTATUS UDT is passed in by reference. Therefore, the DLL may modify the contents of the variable, but in most cases this is exactly what we're looking for.

Note that we saw this same situation with the calls we made in Chapter 1 for the high-resolution timer calls that used the LARGE_INTEGER UDT.

The following code would work just fine in VB:

Private Sub GetMemoryStatus()

Dim udtMemory as MEMORYSTATUS

GlobalMemoryStatus udtMemory

' Code can be added here to use the memory information
' stored in udtMemory.

End Sub

Once the call is made, each value in udtMemory will be changed to reflect some aspect of the current Window memory allocation. If for some reason you won't pass in a UDT to the call, simply add another call (or change the one you already have) that will accept a Long data type ByVal. This situation arises when you need to make a call that needs a SECURITY_ATTRIBUTES UDT as an argument (we'll see this in the next chapter for the CreateFile call). If you're programming in NT, you can use some security features that define how processes can share system objects. However, this has no meaning in the Win9x world. By redefining the argument as a Long data type, you can pass in a null pointer value, or zero in the VB world. This passes in a null pointer to the function call, which will know that you didn't pass in the UDT. You'd have to redefine the argument's data type in these cases. In fact, take a look at our array discussion we just went through. We redefined ReadFile such that we could ignore the OVERLAPPED UDT.

The only issue that should really concern you when you need to use a UDT is memory alignment. The rules that govern UDT memory alignment are as follows:

  • A byte can exist anywhere within a structure
  • An integer must exist at an address location that is evenly divisible by two
  • A long must exist at an address location that is evenly divisible by four

Therefore, if your type has a Byte data type declared along with some other types, VB has to "pad" the structure with some extra memory so the UDT fits the rules. For example, let's take a look at this type:

Private Type WeirdType
ByteType As Byte
LongType As Long
End Type

If you declared udtWeird as a WeirdType UDT and called Len(udtWeird), you would get a 5. However, calling LenB(udtWeird) would return an 8. Since the first type is a Byte, VB has to add three extra bytes to make sure that LongType exists at a proper memory location. LenB returns the actual memory size, including any byte padding that the UDT needs to follow the rules stated above. Len simply returns the length of the UDT "as-is," without the memory padding added in. Here's a diagram to demonstrate what the UDT looks like in code, and how it is actually lined up in memory:

Most APIs are aware of the memory alignment issue and follow the requirements stated above. But what about strings? For example, say we changed WeirdType so that it looks like this:

Private Type WeirdType
ByteType As Byte
LongType As Long
StringType As String * 5
End Type

Now what happens if we call Len and LenB? We get 10 and 20, respectively. But why? We know that VB is adding offsetting memory to get LongType in the correct spot. But it looks like we're only adding 5 more bytes with StringType. Well, guess what - we've run into yet another topic: Unicode

Internally, VB handles its strings as Unicode strings. A Unicode string is just like any other string; the difference is that a character is defined as being 2 bytes long, rather than the standard 1 byte that we're used to. The reason for this is that many languages have more than 256 characters in their alphabets, so Unicode was created to support any language currently used by mankind (can you think of a language that has more than 65,535 characters?).

This usually doesn't impact any VB code, but as soon as you tackle the APIs, you may start to see where the Unicode standard effects you. For example, in our extended WeirdType UDT, we noticed that the byte count went up to 20 when we added the fixed string. If we look at the memory layout, we'll see why an extra 5 bytes have been added:

The string is actually taking up 10 bytes of memory. Although we usually don't see this, DLLs really care about memory allocation, especially when strings are involved. In fact, as we'll see in Chapter 3, a UDT that has a String data type has a size parameter as well. This is to inform the DLL how much memory the UDT is taking up.

Although VB handles strings internally as Unicode strings, it will automatically handle the ANSI/Unicode conversions for us. However, if an API call specifically needs a Unicode string, you have to run though some hoops to do this. For now, we can declare a Byte array to retrieve the information, and then use StrConv to convert the Byte array's information to a string that we're comfortable with.

An API call that ends with the letter W needs Unicode strings. In one of the previous sections, we looked at the FindWindow call. There's a counterpart to the declaration that we made, and it's called FindWindowW in the DLL. You'll notice that our FindWindow call is actually calling FindWindowA. Furthermore, if you ever use the OLE or COM APIs, they only take Unicode strings, so you'll have to use the proper calls to communicate with them. However, to keep things simple we'll stick with the ANSI calls whenever possible in this book.

By the way, don't try looking for Unicode calls in the API Viewer. All of the calls are aliased to use the ANSI version of the call. So if you try to search for FindWindowW, you won't find it. Nor, for that matter, would you find FindWindowA. The only call that will show up is FindWindow, which is actually calling FindWindowA. If you want to find out if a call like FindWindow has a Unicode version of the call, check the Microsoft SDK, or do a Quick View… on the DLL that contains the call in question.

So Why 20 Bytes?

Before we leave the issue of strings and Unicode, I wanted to address an issue that, as of the writing of this book, is still unresolved. Let's break down the WeirdType structure element by element. The first one is a byte. Since the second element is a long, we know that VB will add three bytes between the byte variable and the long variable. This gives us 8 bytes. Well, we know the string is 10 bytes in length, so shouldn't that lead to 18 bytes?

You might think that LenB is taking into consideration the null-termination character at the end of the string, or something like that. Two odd things counter this idea, though. If you increase the length of the string to 6, you still get 20 from LenB. Plus, if you define WeirdType in a VB 5.0 application, you'll get 18 from LenB!

I hate pleading ignorance on this subject, but unfortunately I don't have a choice. I wish that I could give you a clear-cut answer as to why this is so, but I can't. Plus, I bet somebody out there has a simple answer as to what's going on. But I've asked some real gurus of the language, and they can't answer this dilemma.

I'm not too thrilled that there is this discrepancy, though, between the two languages. Granted, I doubt anyone's figured out what the byte length of their UDTs are, and stores those values in constants. If somebody did that in 5.0, they have a potentially big surprise coming up when the application gets ported over to 6.0! This goes to show you that, from version to version, VB may change things beneath the scenes. It may be subtle or it may be dramatic, but it happens. Something changed with LenB, and I'd really like to know what that is.

I have a hunch that this might have something to do with the fact that you can now pass UDTs as typed arguments from class modules in 6.0. Maybe the VB designers had to do something below the VB level to pull this off. This gets into issues with COM and marshaling, something that's best left for other discussions, but I wouldn't be surprised if this new feature is a part of the problem

We've covered the majority of issues, problems, and errors that most VB programmers run into when using the API calls. The rest of them are pretty minor, so I've lumped them together into this "Other" category.

Currency Variables

The Currency data type is only valid inside of VB; no API calls that I'm aware of can handle it. If you need to pass in information from a Currency variable to a DLL, convert it to a Double or a Long. You may lose some precision, but that's the best you can do now.

Single and Double Types

The Single data type maps to the float type in C, and the Double type maps to the double type in C. However, none of the Win32 calls use floats or doubles, so unless you're using a custom DLL written in-house, you won't have to worry about it in this book.

Pointers to Functions

Some API calls need a function to call back to inform the calling application of some event. We'll address this (no pun intended) in chapters 7 and 8.

Window Handles

Most of the graphical API calls need a window handle to change something about a particular window. Thankfully, all of the forms and most of the controls (remember, most controls are windows) have a hWnd property, so you can just pass in that value straight into the API (by the way, window handles are declared using the Long data type).

Boolean Return Values

Watch out if a function says it returns zero on error, and nonzero on success. This doesn't translate to VB's True/False Boolean values, where False does equal 0, but True is equal to -1. Use the CBool function on these return values, since CBool will return True for any nonzero value.

Variants

Variants are used with any OLE or COM API calls, but none of the Win32 calls use this type. Therefore, I would strongly suggest avoiding using variants with Win32 calls.

Revisiting the Gotcha Function

Remember that evil C programmer that tried to fool us on April 1st? Well, let's take a look at that call again:

Declare Function Gotcha Lib _
"HaHa" (WatchOut as Integer) as String

Two big problems should immediately jump out at you:

  • We're passing in an Integer data type, but the call may try to put information into our variable that will make it overflow.
  • We can't return a String data type; we have to return a Long that points to a String data type, and use CopyMemory to get that information into a string variable that we can use.

Now that you know there are pitfalls waiting for you, change that employee's salary in the corporate database appropriately

Well, we've covered a lot of the major data type issues that we can run into when using the Win32 calls. But there's still one more that we should address, and that's error handling.

The majority of API calls will inform you in some way, shape, or form if an error occurred (note, however, that this doesn't include memory exceptions). Most of the time, it takes the form of a return value or some parameter that you pass into the procedure. But virtually all of the calls set some internal OS information that you can obtain using just two API calls. Let's review these calls, and then we'll create a VB function that we can use within our main development application.

Declare Function GetLastError Lib "kernel32" _
Alias "GetLastError" () As Long

Declare Function FormatMessage Lib "kernel32" _
Alias "FormatMessageA" (ByVal dwFlags As Long, _
lpSource As Any, ByVal dwMessageId As Long, _
ByVal dwLanguageId As Long, ByVal lpBuffer As String, _
ByVal nSize As Long, Arguments As Long) As Long

The GetLastError function simply returns a number that corresponds to the last error that occurred within a Win32 API call. We can then use this value and pass it into FormatMessage to obtain a message that may make more sense than error code 10594. As you can see, that second parameter is already causing me some concern. We'd better look into this further.

If you want, you can use the SetLastError API call to set the error code.I can't think of a reason why you'd need to do this in VB, but it is possible to change this value.

The first parameter, dwFlags, is used to tell the call how it should be used. It's cryptic, I know, but this function gets pretty flexible in a hurry when you start to read the documentation on the call. The only value of dwFlags we're concerned about is FORMAT_MESSAGE_FROM_SYSTEM (equal to 4096), which will tell the call to look up a description from an internal resource. We can ignore the second parameter for our purposes (thank goodness), since Microsoft's documentation tells us this argument is ignored when we set dwFlags equal to 4096. We can pass in the return value from GetLastError to dwMessageId. We don't care about language issues for now, so we'll set dwLanguageId equal to 0. The lpBuffer is another case where we need to pass in a pre-allocated string to the call - the length of the string is passed in through nSize. We can also ignore the Arguments argument as well.

There's a lot more that you can use FormatMessage for, but we just want to use it to obtain error information. If you're curious, hop onto Microsoft's web site and look up the documentation on the call. For now, let's just use what we know about the call to create a prototype API error call:

Function GetWin32ErrorDescription(ErrorCode _
as Long) as String

Dim lngRet as Long
Dim strAPIError as String

' Preallocate the buffer.
strAPIError = String$(2048, " ")

' Now get the formatted message.
lngRet = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, _
ByVal 0&, ErrorCode, 0, strAPIError, Len(strAPIError), 0)

' Reformat the error string.
strAPIError = Left$(strAPIError, lngRet)

' Return the error string.
GetWin32ErrorDescription = strAPIError

End Function

We'll improve upon the function in a moment, but I hope you see the point. Whenever you get an error from a Win32 call, run this function to get an error message. It may help you in debugging your applications.

Which DLL Error Code?

Notice that the GetWin32ErrorDescription uses an argument to get the error code. This was done for a lot of reasons; such as letting us enter in any value to see what the result would be (not the most exciting thing to do in the world, but it might be somewhat educational). However, it was also done to illustrate the behavior of capturing the error code from a DLL. Let's use a very simple but powerful example to illustrate this fact.

Create a new Standard EXE project in VB called MutexTest. Add one label to the form. Here's some more specific information about the project:

Object Name Caption
Form frmMutex Mutex Tester
Label lblMutex (leave blank)

Your main form should look something like this:

As with the TestAPIStringReturn project, simplicity is the key with this project. We want to focus in on the API calls and keep the UI to a minimum where possible.

Add the GetWin32ErrorDescription function that we just looked at to the code window for the form. Now we'll need four API declarations in this project along with one form-level variable and two constants. Add them to the form's Declarations section like this:

Private mlngHMutex As Long

Private Const ERROR_ALREADY_EXISTS = 183&
Private Const FORMAT_MESSAGE_FROM_SYSTEM = 4096

Private Declare Function GetLastError Lib "kernel32" () As Long

Private Declare Function CreateMutex Lib "kernel32" Alias _
"CreateMutexA" (ByVal lpMutexAttributes As Long, _
ByVal bInitialOwner As Long, ByVal lpName As String) As Long

Private Declare Function CloseHandle Lib "kernel32" _
(ByVal hObject As Long) As Long

Private Declare Function FormatMessage Lib "kernel32" _
Alias "FormatMessageA" (ByVal dwFlags As Long, _
lpSource As Any, ByVal dwMessageId As Long, _
ByVal dwLanguageId As Long, ByVal lpBuffer As String, _
ByVal nSize As Long, Arguments As Long) As Long

Note that we didn't use a module this time. For simple projects like this, we really don't need a module to house the API declaration.

Now that we've got the project set up, let's back up for a second so I can explain what this project will actually do!

You may run into a situation in your project development where you want to prevent the user from opening up more than one instance of your application. The global App object does have a property call PrevInstance, which you can use to determine if another instance is running. But I was curious one day, and I started looking into ways to do this myself. As it turns out, you can use a kernel object called a mutex (short for mutual exclusion) to make this a very easy task.

Before we get into the details of the code, I should note that we're using a mutex in a way that it probably wasn't intended (of course, that doesn't necessarily make it wrong, either!). Mutexes are commonly used in multithreaded applications, and that's beyond the scope of this book. However, we'll use one nice little feature of a mutex for our problem at hand: it can be accessed from any process in Windows.

So how does this work? Let's add this function to our form:

Private Function IsPrevAppRunning() As Boolean

On Error GoTo error_IsPrevAppRunning

Dim lngVBRet As Long

mlngHMutex = CreateMutex(0, 0, "MutexTest.frmMutex")

lngVBRet = Err.LastDllError

If lngVBRet = ERROR_ALREADY_EXISTS Then
' This app is already running.
IsPrevAppRunning = True
MsgBox GetWin32ErrorDescription(lngVBRet)
End If

Exit Function

error_IsPrevAppRunning:

IsPrevAppRunning = False

End Function

I'll come back to this function in a moment. For now, assume that this function will return a True if the application is already running and False if it isn't. In the Initialize event of the form, add this code:

Private Sub Form_Initialize()

If IsPrevAppRunning = True Then
MsgBox "This app is already running.", _
vbOKOnly + vbExclamation, "App Already In Use"
Unload Me
Set frmMutex = Nothing
Else
lblMutex.Caption = "Mutex Number = " & CStr(mlngHMutex)
End If

End Sub

And, in the Terminate event of the form, add this code:

Private Sub Form_Terminate()

If mlngHMutex <> 0 Then
' Close the mutex.
CloseHandle mlngHMutex
End If

End Sub

Now let's go through how this all works. The first thing we do is create a mutex using the CreateMutex API call. The first argument is set to 0 since we don't care about the security attributes of this mutex. The second argument is also set to 0, which means that we don’t "own" the mutex if we create it successfully. The last parameter is the one that we're concerned about. If we didn't name the mutex, we'd have to identify it by the return value, which is the handle to the mutex. However, a named mutex can be used by the name given. Furthermore, as I hinted at before, mutexes can be used across processes. Therefore, if another instance of our application has already created this mutex, CreateMutex will let us know about it.

The way CreateMutex lets us know of this situation is a bit weird, though. The return value is the handle to the mutex on success (i.e. a non-zero value), so we can't use the return value to let us know if the mutex already exists. However, if the mutex already exists, the GetLastError function would return a value equal to ERROR_ALREADY_EXISTS. In our case, we catch that situation and return a True. Note, though, that we don't use GetLastError - we use Err.LastDLLError. This will be important, as we'll see.

If we haven't created the mutex yet, the app will load up just fine. We should get a window that looks like this:

Of course, we should get rid of the mutex when the application is done. This is why we use CloseHandle on mlngHMutex in the Terminate event of the form.

So what does any of this have to do with GetLastError? Here's the problem. First, compile the project into an executable (make sure the code is native code, not p-code! To check go to Project | Properties and select the Compile tab.). Then run two instances of the program. The first one should load up fine, but the second one will show the following two message boxes:

That's the last you'll hear from the second instance.

Now go back into the code and replace this line from within IsPrevAppRunning:

lngVBRet = Err.LastDllError

to this:

lngVBRet = GetLastError

As before, compile the app and run it twice. Now the app shows up both times! What's the deal?

Internally, VB is making Win32 calls all the time in your application. Sometimes, when you make a Win32 call, this may trigger an event within your application that causes VB to make other Win32 calls (we'll address window messages later on in the book). Well, what happens if your initial call causes an error, but the internal Win32 call made by VB clears out the value of the error? You get the situation that we just saw. GetLastError returns a zero, but Err.LastDllError returns the correct error code.

In previous versions of VB, the LastDllError property didn't exist, and strange situations like the one we just saw popped up again and again. Therefore, the VB designers decided to add the LastDllError to the Err object. This property should be used when a Win32 call is made that causes an error, because VB will track your Win32 call and make sure that this property reflects any possible error conditions.

But you need to be quick. Grab the value of LastDllError after any Win32 calls in question, even before you look at the return value! This is the only way to guarantee that any error condition generated by the Win32 call you just made is in LastDllError. If you start changing the size of the form, or adding text to a text box, all bets are off, and you've lost the error code (unless you're really, really, REALLY lucky).

So what's the best situation to be in? Let's leave this discussion of capturing Win32 error codes in VB with this outline. It's no guarantee that we'll report the correct error code all of the time, but this is about as close as we're going to get:

  1. Grab the value of LastDllError immediately and store it in a variable (call it lngErrVB).
  2. Grab the value of GetLastError and store it in a variable (call it lngErr32).
  3. If lngErrVB is nonzero, then use it to report the error condition.
  4. If it's zero, check lngErr32. Chances are it's probably zero as well, but if it's nonzero, use it to report the error.

The moral of the story? You should always use the value from LastDllError whenever possible. VB is making a conscious effort to intervene on your behalf, so this is your best bet for Win32 error codes.

Let's try to sum up all of the issues we've addressed so far in this chapter concerning API calls:

  • Save your work often
  • Always pass strings ByVal
  • Be very careful when using the Any keyword
  • Pass in the first element when using arrays as buffers
  • Make sure your UDTs map to what the DLL is expecting
  • Never tell a DLL that you've allocated more space than you really have
  • Don't embed null characters in strings if an API call needs it
  • Save your work often
  • And…save your work often

You're bound to run into other subtle "features" when you use the Win32 API. In a way, the Win32 APIs remind me of the English language; it does have structure, but there are a lot of exceptions to the rules. But as long as you try to find out as much as you can about what's going on underneath the scenes, you should be able to solve any problem.

And yes, I did mention the save edict three times. NEVER run your code without saving it first, and I mean NEVER. You're throwing the safety net away when you tear into the Win32 APIs, so save your work before you try to run your code. A crash may shut down VB or even Windows itself, but if your code was saved, you can always go back and figure out what went wrong after the OS reboots

As we have seen, there's a lot of power in the Win32 APIs. As long as you steer clear of the potential traps, your development will be a smooth process. Throughout the rest of this book, we'll use an application as our main "playground" for API testing and debugging. This application is called The Encryption Program. I'll explain why I've decided to use this project in this book, and then we'll take a look at the program itself.

If you don't want to read about the political or social reasons that motivated me to write this program, please skip over the next section. But I would encourage you to do it anyway. Think of it this way: If I was in a business area at the company you worked at, and I came to you with this problem, how would you effectively solve it? You may or may not agree with my arguments or ideas, but I’m coming to you with a problem, I have the money in my budget, and I want you to solve it for me (yesterday, of course). One of the biggest criticisms I’ve heard about programmers is their inability to relate to business areas, so I’d ask you to set judgment aside and tackle the problem with a technical solution in the back of your mind.

Why This Program?

One issue that you tend to hear about every so often is privacy rights. For example, more and more cameras are showing up in our society every day. Some people claim that these cameras infringe upon our privacy (like when you try on clothes at a department store), while others state that the cameras help minimize criminal activity (like being able to spot a person stuffing a shirt into a bag when they're in the dressing room). This issue is rather complex, and this book is not the place to analyze it in detail, but there is one part of it that, admittedly, annoys me!

A thorny side issue has been raised with employees using e-mail at work. Some employees have used these services to send jokes or pictures that other may consider derogatory or inappropriate. In this day and age of lawsuits and allegations, the last thing a company would want is to be sued by an employee who feels that the content within a message is harmful. Therefore, some companies have set up policies and procedures that allow them to read anyone's e-mail at any time. Content that has been defined as inappropriate and is found within a message can lead to disciplinary action, or even termination.

Granted, this sounds nice, but the reason I want to encrypt a file is that I don't want anyone to see the cookie recipes I'm sending to my parents via e-mail other than my parents! This is where the issue gets personal, so I'll stop at this point. But it motivated me to start investigating cryptography a bit more. I don't claim to be an expert in the field, and the technique I came up with is fairly simplistic, but it required me to break open a lot of API calls to pull it off. So let's take a look at how the algorithm works before we start coding in VB.

If you're interested in learning more about encryption, check out the RSA Data Security, Inc. web page at http://www.rsa.com/. I make no claims that my code will secure the content of your files, since it’s a pretty simplistic process compared to RSA. But RSA is very secure, so check out this site if you need tight cryptography.

How the Algorithm Works

Say you have the following message that you'd like to encrypt:

As you can see, the message is broken up such that each character is in its own box. To encrypt this message, we're going to handle each character in the message one at a time. We also have to convert each character into its ASCII character value. Here's what the message looks like in ASCII values:

We also need two other parts: the seed value and the key. The seed value is to set the starting point in a random sequence, and the key is used as a "filter" on the message. For this example, let's say the key is "XKf7" and the seed value is 24.

Now for the fun stuff. We line up the message's first character against the key's first character value, the message's second character against the key's second character value, and so on. If the message is longer than the key, we just start over. Here's what that looks like:

The next step is to simply add the character values up between the two strings, and store the results into another array of characters. Here's what happens during this operation:

The last step is to introduce some randomness into the picture. If we didn't, our key would stick out like a sore thumb in the resultant array. For example, if we had a message like "ZZZZZZZZZZZZZZZZ", and our key was "XKf7", here's what would happen:

Granted, the exact key value isn't repeated, but that's a glaring hole for any hacker to try and decrypt the message. This gets worse if the file we encrypt has a bunch of null characters in a row. Remember that a null character is equal to 0 in ASCII. Add 0 to the key value, and you get the key value, thereby exposing the key itself! Therefore, we need to mess up the result a little bit. Here's another catch, though: we have to be able to "unmess" the result when we decrypt it. If this isn't possible, the message will be lost forever.

Thankfully, the random number generator in VB helps us out here. If you search for the word Rnd in VB's help file, you'll find this sidebar:

To repeat sequences of random numbers, call Rnd with a negative argument immediately before using Randomize with a numeric argument. Using Randomize with the same value for number does not repeat the previous sequence.

Rnd is a function that returns a random number. However, without getting into the statistical details, Rnd is cyclical. That is, eventually Rnd will start to repeat.

It actually takes 16,777,000 iterations, from what I can tell. I've created a project called RandomSequenceCheck that you'll get if you download the code. It has absolutely nothing to with API calls, which is why I don't cover it explicitly in the book; I just wanted to show this interesting little fact in a VB project for the curious reader.

If you want to start the sequence at a specific point, you use the following code:

Rnd (-1)
Randomize (SeedValue)

No matter when or where you run this code, your program will produce the same random sequence every single time as long as SeedValue is the same. I know that may sound weird - how can you call a sequence random if you know what's going to be generated? - but for our purposes, this is exactly what we need. Now, if we would run this code in VB and call Rnd as many times as we need for our message we get:

If the concept of generating a random number sequence that's predetermined and cyclical sounds like a paradox, you may have to break open a book on statistics and probability to get a thorough explanation on random number generators. One good source that I know of is written by Athanasios Papoulis, entitled "Probability, Random Variables, and Stochastic Processes," but it's definitely not easy on the eyes from a mathematical standpoint!

We still have one minor process to run. The ASCII character set only goes from 0 to 255, so if the resultant value exceeds 255, we simply subtract 256 from the value until it is less than 256. (For all you non-mathematicians out there, we'll skip the group theory analysis.) So here's the final encrypted message:

In ASCII, this is the message from start to finish: "__DISREY-kMC_m". Pretty messy, isn't it? You'd have to spend a little bit of time to crack this one. Now if we want to decrypt it, we just run the process in reverse. We take the message, subtract each character with the correct number in the random sequence along with the correct value in the key. This time, if the number is less than 0, we add 256 until the value is non-negative. Here's the process in reverse:

If you look back at the original message in ASCII, we got it back! Also, notice that the randomness is exactly the same sequence as before. If it wasn't, who knows what your friend may be reading on the other end.

It's a simple algorithm, once you get down to it. There are more complex mathematical issues that we didn't delve into, but it does its job. However, you still have the issue of the key and seed value. How do you transmit these components? Well, that's more of a social issue than anything. You may use the key "Stop6Watch" between one group of friends, and simply tell them the seed value in the e-mail along with the attached encrypted file. If anyone intercepted the message, they wouldn't have the key to decrypt the file. However, if someone reveals the key, then the system breaks down.

The Encryption.vbp file is where we'll start incorporating API calls into our code. Here's a screen shot of the main form:

Here's a screenshot of the About form:

Let's cover some of the real basic glue code that we'll need throughout the development of this project. First off, here are some of the basic project information you need to know. Note the Contained In entry in the table that designates where the control should go. Because the project has a lot of UI components, this is needed for clarification. Bear with me as we go through the list, but rest assured, this is by far the biggest list of project properties we'll see. Also note that I've left the labels out of the controls list that are used for descriptive purposes only. All the controls can be found in your default Toolbox except for the Progress Bar which is located in Microsoft Common Controls 6.0. By looking at the screen shot, you should be able to add them in if you think it's necessary:

Object Contained In Property Value
Form   Name frmMain
    Caption The Encryption Program
    Borderstyle 3 - Fixed Dialog
Frame frmMain Name fraMain
    Index 0
    Caption File:
Frame frmMain Name fraMain
    Index 1
    Caption Algorithm Parameters
Drive List Box fraMain(0) Name drvMain
Directory List Box fraMain(0) Name dirMain
File List Box fraMain(0) Name filMain
Text Box fraMain(0) Name txtFileFilter
    Text (leave blank)
Command Button fraMain(0) Name cmdSetFilter
    Caption Set
Option Button fraMain(1) Name optEncrypt
    Index 0
    Caption Encrypt Selected File
Option Button fraMain(1) Name optEncrypt
    Index 1
    Caption Decrypt Selected File
Text Box fraMain(1) Name txtKey
    Text (leave blank)
    PasswordChar *
Text Box fraMain(1) Name txtSeedValue
    Text (leave blank)
    PasswordChar *
Check Box fraMain(1) Name chkShowValues
    Caption Show Values?
Check Box frmMain Name chkSaveAs
    Caption Save Altered File As...
Command Button frmMain Name cmdRunEncryption
    Caption Start Encryption
Text Box frmMain Name txtStatus
    Text (leave blank)
    BackColor &H00C0C0C0&
Progress Bar frmMain Name ProgressBar
Command Button frmMain Name cmdAbout
    Caption About...
Command Button frmMain Name cmdExit
    Caption E&xit
Form   Name frmAbout
    Caption About the Encryption Program
Label frmAbout Name lblTitle
    Caption The Encryption Program
Label frmAbout Name lblCreatedBy
    Caption Created By Victor
Label frmAbout Name lblVersion
    Caption (leave blank)
Timer frmAbout Name tmrAbout
    Enabled True
    Interval 5000
Command Button frmAbout Name cmdClose
    Caption &Close

There's some code we need to add to this skeleton layout before we start adding API calls to the project. We won't spend too much time on them, but they give some basic functionality to the project. Let's start with frmMain. The first method is called SetFilePattern:

Private Sub SetFilePattern()

If Trim(txtFileFilter.Text) = "" Then
filMain.Pattern = "*.*"
Else
filMain.Pattern = Trim(txtFileFilter.Text)
End If

End Sub

This is called from the Click event of cmdSetPattern:

Private Sub cmdSetPattern_Click()

SetFilePattern

End Sub

SetFilePattern sets the Pattern property of the file list box to show either all the files or files with a specific pattern given by the user.

The next function we need is called ValidateInterface. This function checks entries made in the interface and returns "" on success and something on failure.

Private Function ValidateInterface() As String

On Error Resume Next

Dim lngCheck As Long

If Trim$(txtKey.Text) = "" Then
ValidateInterface = "Please enter in a key."
Exit Function
End If
If Trim$(txtSeedValue.Text) = "" Then
ValidateInterface = "Pleae enter in the seed value."
Exit Function
Else
lngCheck = CLng(txtSeedValue.Text)
If Err.Number <> 0 Then
ValidateInterface = "The seed value is too large."
Exit Function
ElseIf lngCheck < 1 Then
ValidateInterface = "The seed value must be greater than 0."
Exit Function
End If
End If

End Function

This isn't called yet by a control in the project, but we'll need it to verify what the data that the user entered.

The function begins by checking that a key was entered:

If Trim$(txtKey.Text) = "" Then
ValidateInterface = "Please enter in a key."
Exit Function
End If

Then we ensure a seed value was entered:

If Trim$(txtSeedValue.Text) = "" Then
ValidateInterface = "Pleae enter in the seed value."
Exit Function

If a seed value has been entered we check for an overflow:

Else
lngCheck = CLng(txtSeedValue.Text)
If Err.Number <> 0 Then
ValidateInterface = "The seed value is too large."
Exit Function
ElseIf lngCheck < 1 Then
ValidateInterface = "The seed value must be greater than 0."
Exit Function
End If
End If

The Click event of the cmdAbout command button brings up the frmAbout form:

Private Sub cmdAbout_Click()
Screen.MousePointer = vbHourglass
frmAbout.Show vbModal
End Sub

The Click event of the cmdExit command button shuts down the application:

Private Sub cmdExit_Click()
Unload Me
Set frmMain = Nothing
End Sub

The drive, directory, and file list boxes changes are all coordinated using the following lines of code:

Private Sub drvMain_Change()
dirMain.Path = drvMain.Drive
End Sub
Private Sub dirMain_Change()
On Error Resume Next
SetFilePattern
filMain.Path = dirMain.Path
End Sub

The chkShowValues check box is used to let the user see what is actually contained in the seed and key text boxes. It does this by calling the CheckKeyAndSeed method in its Click event:

Private Sub chkShowValues_Click()
CheckKeyAndSeed
End Sub

Here's what the CheckKeyAndSeed looks like:

Private Sub CheckKeyAndSeed()
On Error Resume Next

If chkShowValues.Value = vbUnchecked Then
txtKey.PasswordChar = "*"
txtSeedValue.PasswordChar = "*"
Else
txtKey.PasswordChar = ""
txtSeedValue.PasswordChar = ""
End If

End Sub

If the user want's to see what's in the boxes, we set PasswordChar to an empty string. Otherwise, the text is masked with the "*" character.

Now let's move on to frmAbout. The first method to look at is InitializeForm:

Private Sub InitializeForm()

Screen.MousePointer = vbHourglass
lblVersion.Caption = "Beta Version " & CStr(App.Major) & _
"." & CStr(App.Minor) & "." & CStr(App.Revision)
Randomize
Screen.MousePointer = vbDefault

End Sub

It's called from the Load event of the Form:

Private Sub Form_Load()
InitializeForm
End Sub

The other method is called AlterLabels:

Private Sub AlterLabels()
End Sub

It does nothing yet; trust me, it will! It's called from the Timer event of tmrTimer:

Private Sub tmrAbout_Timer()
AlterLabels
End Sub

Now that the skeleton code is done, let's go over what we really have to worry about:

  • Implement the encryption algorithm defined above and show the status of the algorithm to the screen
  • Add a Save As… process
  • Have some fun with the About screen
  • Change each form to look more appealing - gray is getting old

We'll tackle all of these issues along with some other neat tricks in the next three chapters. Whenever we add code that uses the API calls, we'll try to achieve the same functionality using VB code only. We'll compare them from a performance standpoint as well, and see which one "wins".

I hope you are beginning to see some of the complexity of using API calls and also how they have gained such a poor reputation. However, I would also hope that you can approach API calls with confidence now that you know how to avoid the pitfalls that lie in wait.

In this chapter we:

  • Saw how many of the data types are communicated between VB and the DLL
  • Used this knowledge to learn how to avoid other problems with calls we haven't seen yet
  • Covered how to handle errors when using API calls in VB
  • Quick overview of some other problems that might occur

We also built the UI for the Encryption application and defined what the encryption algorithm is in readiness for building the real functionality of the application in the next chapter.

In the next three chapters, we'll use calls from the three main Win32 DLLs: kernel32, user32, and gdi32 to begin to examine how we can access some of Windows key programming. We'll also be developing the Encryption program by incorporating calls to these three DLLs to get the program up and running and to spice up the interface.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date