Windows API Tutorial - Part Two, Page 4
A good few years ago, I made one rather drastic mistake. I presumed.
Not just any old presumption, but a whopping great huge presumption that ended in unhappy users and unhappy bosses.
You see, I thought hey, my Windows directory is C:\Windows\ - and therefore, that must be the case for every other computer lover in the entire world.
You see, Windows NT users typically have Windows in a directory called C:\WinNT\ - as I learned. And some picky people opt to choose a different installation path such as C:\Windoze\ - as I learned.
And yet others would go ahead and choose completely different drives to install the operating system, such as F:\Windows\
Still others would do both, such as J:\WinDiddlyDonk\ - I hated those the most.
But hey, I was young and innocent as my users figured when the application crashed due to an incorrect path error. Oops. Still, it taught me never to presume in the wacky world of Windows. Instead, use the API.
<Ed groans: Why did I sense that was coming? >
The API provides a couple of groovy little functions that allow us to grab the main directories such as the Windows directory, System directory and Temporary directory. For the moment, we're just going to look at GetWindowsDirectory:
- Load up the API Viewer
- Find and copy the following declarations: GetWindowsDirectory
- Create a new project in Visual Basic and paste the declaration into a module
Now this declaration is a little different to the ones we have uncovered so far. Why? Well, take a look at it:
Public Declare Function GetWindowsDirectory _ Lib "kernel32" _ Alias "GetWindowsDirectoryA" _ (ByVal lpBuffer As String, ByVal nSize As Long) As Long
Notice that the first parameter requires us to pass a string. We've not done that before and unfortunately it ain't all that simple.
Why? Well, alas, most API DLLs were created in the C programming language. And that means peculiarities. For instance, when dealing with strings, the API adds an extra character to the end a null 'terminator' - so it knows where in memory the string stops.
But VB doesn't use those, so when you get strings back from the API, you have to knock off that end terminator. Also, when you're passing the actual string, you need to tell the API how big the string is so it doesn't put too much information in.
You see, the API doesn't really know what a string is. It talks in terms of memory. So if it tried to put more information in than it should, that extra info could run out of your string and into some other area of memory which could result in a system crash. So remember API calls can cause havoc - save your work regularly.
Anyway, we'll see all of this and more by the end of this lesson. Worried yet? Don't be. I'm here to help you. Promise. Let's go:
- Add a Command Button to your Form
- Place the following code behind the button:
Private Sub Command1_Click() Dim strWinDir As String Dim lngSize As Long strWinDir = Space(255) lngSize = 255 Call GetWindowsDirectory(strWinDir, lngSize - 1) strWinDir = TrimNull(strWinDir) MsgBox strWinDirEnd Sub
Don't run it straight away it won't work properly. First, allow me to explain the code so far:
Dim strWinDir As StringDim lngSize As Long
You know this bit. We're just declaring two variables, as required by the API call one a String, the other a Long.
strWinDir = Space(255)
Now this is an interesting piece of code. The Space function just returns a certain number of spaces, dependant on the number you pass to it. In this case, we sent 255 its way so the function returns 255 spaces.
Huh? So we're setting our string to equal 255 spaces? What's the bloomin' point in that? Well, when we're passing strings to the API, we need to 'pad them out' in other words, set aside 255 spaces in memory, reserved for our string.
Before we added these spaces, Visual Basic didn't know how many characters the string would hold. And that's fine, VB handles all that stuff dynamically. But C doesn't. Oh no. Before you can use a string, C needs to know how much memory it has to work with so we pad it out with duff information to create a memory 'buffer'.
lngSize = 255
This line records the size of that memory buffer. In this case, 255 characters worth. You see, when we call this API function, it asks for two things the string (strWinDir) and how much 'memory' in that string it can play with. We're going to use lngSize to hold that information.
Call GetWindowsDirectory(strWinDir, lngSize - 1)
This is the actual API call. We pass the Windows directory string and the Long. Notice that we actually pass in lngSize minus 1, aka 254. Why? Well, remember I said C adds a null terminator to the end of its strings so it knows where they finish in memory?
Well, this is how we handle it. We're saying "Hey API Guy, here's the strWinDir string. You can use 254 characters of it"
And the API thinks to itself, "Sure, you think I'm only going to use 254 characters but I'm actually going to add one extra character on the end, a null terminator, just so I know where to stop. Haha!"
And we think, "I know what that evil API Guy is like. That's why I actually passed it a string that has space to hold 255 characters in memory. But I only told the API to use a maximum of 254, to account for him naughtily adding the one-character terminator!"
Phew! And now the next line:
strWinDir = TrimNull(strWinDir)
This just calls a function I've written called 'TrimNull'. It basically chops that extra terminator character off the end. We'll add this function in a minute.
But hold on for two twitches of a ducks whisker! As the GetWindowsDirectory declaration shows, we're passing strWinDir to the API 'ByVal'. This means we're passing the variable by value (which simply passes the string data) rather than by reference (which passes a pointer to the string variable).
If you were to typically pass the variable by value the thing you're passing it to can't change the original strWinDir variable. But if you pass by reference, the thing you're passing it to can change the original.
Many of you will already be aware of this common Visual Basic concept. And so you're probably asking how can we, on the line after the ByVal API call, use strWinDir in code like that? You see, really, it should still hold that set of 255 spaces we initially set strWinDir to.
But when you pass strings to an API call 'ByVal' - behind the scenes, Visual Basic actually passes a pointer to the original string. In other words, it passes your string ByRef meaning the API code can change the value of the original string.
I know, I know - don't ask me why, it's just another peculiarity but that's how the API changes strWinDir and allows you to use it later in the code. Just thought I'd attempt an explanation, weird though it may be.
But if you're not familiar with ByVal and ByRef whatnots, you can ignore the last few paragraphs.
<Reader: NOW you tell me!>
And now for that finally line of code:
Here, we're just displaying the strWinDir variable after it's been passed in and out of the API call, then trimmed for nulls.
Let's now add that extra TrimNull function I was talking about:
- Add the following code to your module:
Public Function TrimNull(MyString As String) Dim intPosition As Integer intPosition = InStr(MyString, Chr$(0)) If intPosition Then TrimNull = Left(MyString, intPosition - 1) Else: TrimNull = MyString End If End Function
Once again, this simply checks the passed string for a terminator, Chr(0). If it finds one, it deletes it.
Please, take this function and treasure it. You'll be able to use it in many of the API calls involving strings, making it a jolly useful method to have around.
But now, let's get testing!!
- Run your application and hit the Command Button
What happens? Hopefully and I'm keeping my fingers crossed for you this end the message box should display your Windows path! Well done!
Now that may seem like a lot of work just to get a few characters of information, but you only have to write it once. You could, perhaps, throw the code behind a function, residing in a common module and call it from anywhere within your application.
And it's a lot better than... erm... presuming. D'OH!
Next up, I'd like you to go ahead and attempt a few of your own API calls. Try using the below pair to get both the System and Temporary directory:
Public Declare Function GetSystemDirectory Lib "kernel32" _Alias "GetSystemDirectoryA" _(ByVal lpBuffer As String, ByVal nSize As Long) As LongPublic Declare Function GetTempPath Lib "kernel32" _Alias "GetTempPathA" _(ByVal nSize As Long, ByVal lpBuffer As String) As Long
Go on, follow all the rules we've discussed so far and have a go! Make sure you note how the second one declares it's arguments back-to-front.
You might also want to try the GetComputerName call, also rather similar:
Declare Function GetComputerName Lib "kernel32" Alias "GetComputerNameA" _(ByVal lpBuffer As String, nSize As Long) As Long
Note: A download of the GetWindowsDirectory API call is available here.