[转载]Visual Basic 6 Win32 API Tutorial
VB6是上个世纪末,本世纪初的技术,极大的方便了普通人在Windows下编写代码,实现功能,在这以前,小伙伴只能学习C/C++/Pascal,还必须学习Windows系统基本架构才能行。
突然想要学习提高一下,所以就以"vb6 win32api"搜索了一下,得到几篇不错的文章,转载一下。BTW,原作者的其它几个关于VB6的长篇也很不错。
原作者的其它文章:
https://www.developer.com/author/james-limm/
[转载]Visual Basic 6 Win32 API Tutorial
https://www.developer.com/microsoft/visual-basic/visual-basic-6-win32-api-tutorial/
看起来这是2002年的文章,很不错啊,该页面的相关链接可以找到有关VB6的其它文档。
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.";
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:
- Grab the value of LastDllError immediately and store it in a variable (call it lngErrVB).
- Grab the value of GetLastError and store it in a variable (call it lngErr32).
- If lngErrVB is nonzero, then use it to report the error condition.
- 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.