Identify the Location of Special Folders with API Calls

This content is no longer actively maintained. It is provided as is, for anyone who may still be using these technologies, with no warranties or claims of accuracy with regard to the most recent product version or service release.

Identify the Location of Special Folders with API Calls

by Sean Kavanagh

Application: Microsoft Access 97/2000

You'll often come across situations where you'd like to save or
retrieve data from a particular Windows folder. For example, if you have an
application that exports data to a text file, you may want to automatically
save the file to the Windows desktop or a user's My Documents folder. Although
this seems like it should be an easy task, you can run into problems if your
project uses hardcoded paths to these destinations, as the path won't
necessarily be the same on every PC that will run your application.
In this article, we'll show you how to build flexible solutions using the
Windows API. By taking advantage of functions provided through the API you can
identify the correct location of special system folders regardless of the
customizations or operating system a user has in place. In the process of
creating our solution, we'll examine how to declare API functions and get a
brief glimpse of how Windows manages files.

The problem with paths

Although it's likely many of the systems that will run your Access application
will have compatible folder settings, coding to work with particular folders
will inevitably lead to problems. For example, let's say that you want to
automatically save a file to the desktop. The default path on a typical Windows
9x PC is:

C:\Windows\Desktop

However, if Windows has been upgraded or reinstalled on a PC, the operating system may
be installed in a different directory like

C:\Win98\Desktop

If your application is later moved to a Windows NT box, the desktop path on the
local machine resembles

C:\WINNT\Profiles\username\Desktop

In addition to differences due to the operating system, users can customize key
system folders. For example, it's common to move the My Documents folder from
its default C:\My Documents location to another hard drive partition to make
performing backups easier.

The way to avoid problems with path inconsistencies is to use the Windows API.
To step back a bit, an API is an Application Programming Interface,
which is basically a way to interact with an application or operating system
from another program. In the case of the Windows API, or Win32 API, you're able
to work with the Windows operating system through a collection of Dynamic
Linking Libraries (DLLs). DLLs provide a way to package stored procedures
so that they can be shared among applications, eliminating redundant coding
efforts.

To get a special folder path, you'll need to use two functions:
SHGetSpecialFolderLocation() and SHGetPathFromIDList(). These two procedures
are provided through the Shell32.dll. To call the functions exposed by a DLL,
you need to first provide VBA with access to the DLL. In the past, you've
probably set up a reference to a DLL through the VBE's Tools | References menu
command to gain access to its procedures. Unfortunately, you can't simply set
references directly to the DLLs that comprise the Windows API. Instead, you'll
need to use a Declare statement in your VBA module that identifies where the
DLL is and what procedure within that DLL you want to work with.

Declaring API procedures in modules

As with native VBA modules, DLLs can contain both function and subroutine
procedures. The Declare statement uses one of the following syntaxes as
appropriate:

If the procedure you're working with doesn't return a value, use the first
declaration statement, otherwise use the second. Once a link has been set to
the DLL's procedure you can use it within your code just like any other
function or subroutine in your application. As you might expect, you enter your
API call Declare statements in the declarations section of a code module.

Before we start working with the Windows API, we'll take a closer look at the
parts of the Declare statements. Keep in mind that we can't cover all the
nuances of making API calls in this article; we're just going to examine some
of the fundamentals.

Specifying the scope

First, you should set the scope of the procedure as being Public or Private. As
with regular VBA procedures, a Public scope lets you use the procedure
throughout your project, and a Private scope only lets you call the procedure
within the given module. If you don't specify the scope, the default is Public;
however if your Declare statement is within a form or class module it
must be scoped as Private.

Identifying the procedure's location

The name argument is simply the name you'll use in your code to call the
procedure. The libname argument is the name of the DLL that contains the
procedure you're calling. When you refer to DLLs in the Windows API, you can
refer to them without an extension, such as "shell32". However, calling
procedures from other DLLs requires the full name and extension, and may
require you to enter the full pathname.

Using an alias in the declaration

The Alias part of the declaration statement allows you to specify a name
argument that's different from the procedure name used within the DLL. The
order of the arguments in the Declare statement can make Alias a bit confusing
at first, since it seems contrary to what we're used to--in a SQL statement,
you state the original name first, then the alias you want to use. However, in
a Declare statement, the aliasname specified after Alias is the real
name of the procedure as it appears in the DLL. If you omit Alias, your
specified name argument must match a procedure name in the DLL.

There are a few items to keep in mind when working with the name and aliasname
arguments. First, the procedures you call are case-sensitive. If you choose to
use an alias, aliasname must exactly match the case used in the DLL, but name
can use whatever case you wish. If you don't use an alias, name must match the
case used in the DLL.

The other major consideration regarding an alias is simply when to use one. You
can choose to use an alias anytime you have a preference for how the called
procedure should look within your code, but sometimes you'll be forced to use
an alias. For example, a function in a DLL might have the same name as another
procedure in your project or the function's name might begin with an underscore
character, which isn't allowed in VBA.

A common reason developers use an alias is to remove the character appended to
functions that indicates how the procedure receives String arguments. Windows
API functions handle strings two ways: as ANSI and Unicode. The Win32 DLLs
contain two versions of each function to accommodate both ways. Functions that
use Unicode strings end with a W character. When you're working in VBA, you'll
use the ANSI versions of the functions, which end in A.

Passing parameters and returning values

On a basic level, passing and returning values with a Win32 API procedure is
like working with a regular VBA procedure. There are a few things to watch for,
however. First, you need to be careful to specify the correct data types and
use ByVal and ByRef as dictated by the procedure you're working with. Otherwise
it's likely you'll generate an Invalid Page Fault or General Protection
Fault--and lose any unsaved work in the process.

We won't get into all of the differences between typical procedures and called
API procedures, but one to be aware of is how you need to handle String
parameters. DLL functions can't change the size of a string, so if a function
accepts String arguments you often must pass a string buffer variable that's
sized to hold the maximum number of characters applicable to the argument. In
some cases, you must also pass a separate parameter that indicates how long the
string is.

If you're looking for a String result, as we are in the example discussed in
the introduction of this article, you also need to work a bit differently than
usual. Unlike typical VBA functions, DLL functions won't directly return a
String result. Instead, the functions will typically return a number, which may
indicate the number of bytes used by the processed string or whether the
function was able to execute successfully. To get the actual string value, you
need to look at the string buffer variable specified in arglist.

Use the Win32 API to retrieve special folder paths

Now that we've examined how to declare Win32 API procedures, let's put some to
work. We'll simply display the path to some special folders in the
Immediate/Debug window, but you can easily incorporate the code we'll use into
other applications. To start, open any Access database and choose Insert |
Module from the menu bar. Then, enter the declarations shown in Listing
A at the insertion point.

In addition to the two API functions we previously discussed, we're going to call
the procedure CoTaskMemFree from the ole32.dll. We'll explain what these
procedures do in a moment, but for now, notice how the three Declare statements
illustrate the syntaxes discussed in the previous section. First, notice that
we've declared both functions and a subroutine. We've scoped all of the
procedures as Public. Since the procedures are from the Windows API, we
excluded the .dll extension in our Declare statements. In the case of the
SHGetPathFromIDList() function, we incorporated an alias. Remember the true
name of the function as it's stored in the shell32.dll is
SHGetPathFromIDListA(); however, our alias removes the trailing A that
indicates the function accepts ANSI strings. Finally, note that we took care to
specify the ByVal keyword to ensure that information is correctly passed to the
procedures. If you don't specify ByVal, VBA passes parameters by reference
(ByRef) by default.

We also declared a few constants that will make our code more readable. First
we declared CSIDL_PERSONAL and CSIDL_DESKTOPDIRECTORY, which will be used to
identify the system-independent locations for the My Documents and Desktop
folders.

The next constant, MAX_PATH, represents the maximum number of characters
Windows allows for a file path. As mentioned earlier, API functions can't
change the length of a string parameter, so we'll need to pass a string buffer
that can hold the maximum size we could expect to receive. The last constant,
NOERROR, is the value that SHGetSpecialFolderLocation() returns if it executes
successfully.

The API procedures

Before we create the function that returns a special path, it's worth taking a
closer look at the three procedures we're calling. The first,
SHGetSpecialFolderLocation(), accepts three parameters: hWnd, nFolder and
ppidl. The hWnd parameter is typically used to identify the handle of the
window that called the function. In our case, it doesn't really matter--we'll
just use a value of 0. The second parameter accepts a CSIDL constant to
identify the folder you're looking for. The final parameter, ppidl, is a Long
variable that stores a pointer to an item ID list, or PIDL.

This last term probably needs some clarification. In much the same way that we
mentally keep track of objects on a computer system in terms of files, Windows
keeps track of objects in terms of item identifiers, or item IDs.
An item ID list can be thought of as the full path to the object that
Windows uses. A PIDL is a memory pointer to an object that lets you
programmatically reference it in terms Windows understands. That's a hugely
simplified description of how file management in the Windows shell works, but
it's enough to understand what our function will do.

The second API function we're working with, SHGetPathFromIDList(), is the
function that converts the PIDL into something human beings can understand. The
first parameter is the PIDL and the second is a String variable that will
contain a pathname in the form we're used to seeing. When an API function
creates a PIDL, memory is automatically allocated to storing it. To free that
memory, we'll use the CoTaskMemFree procedure.

Create the SpecFolder() function

Now that we've looked at how the API procedures work, let's pull everything
together. Enter the SpecFolder() function procedure shown in Listing B.
As you can see, our function will return a String value, the path that will be
extracted with SHGetPathFromIDList(). After declaring a few variables, our code
sets strPath to the appropriate size needed to store a pathname using the
Space() function, creating our string buffer.

Our procedure then executes the SHGetSpecialFolderLocation() function using
whatever CSIDL was passed to SpecFolder through the lngFolder parameter.
Assuming lngPidlFound equals 0 (NOERROR), we calculate lngFolderFound using the
PIDL retrieved by the previous function, stored in the lngPidl variable.

In a somewhat confusing twist, the SHGetPathFromIDList() function returns 0 if
the folder path can't be retrieved; otherwise the value 1 is returned.
Once lngFolderFound has been calculated, and assuming no errors occurred, the
path for the special folder will now be stored in the strPath string buffer.
Here's the hitch--since the API function is unable to change the length of the
string, strPath is still 260 characters long. To remove any extra spaces in the
string, we use the Left$() and InStr() functions. Strings changed by an API
function end with a Null character, allowing us to easily identify and extract
the relevant characters from the string buffer.

Listing B: Function to return special path

Public Function SpecFolder(ByVal lngFolder As Long) As String
Dim lngPidlFound As Long
Dim lngFolderFound As Long
Dim lngPidl As Long
Dim strPath As String
strPath = Space(MAX_PATH)
lngPidlFound = SHGetSpecialFolderLocation(0, lngFolder, lngPidl)
If lngPidlFound = NOERROR Then
lngFolderFound = SHGetPathFromIDList(lngPidl, strPath)
If lngFolderFound Then
SpecFolder = Left$(strPath, _
InStr(1, strPath, vbNullChar) - 1)
End If
End If
CoTaskMemFree lngPidl
End Function

Test the function

To test the function, we'll simply send the return value to the Immediate/Debug
window, so press [Ctrl]G to open the window. Now, choose Debug | Compile
filename (in Access 97, Debug | Compile And Save All Modules) from the
menu bar to verify that there are no errors in your code. Then, save the module
as basWin32APICalls.

Alert: Always save your projects before running code that calls API
functions. If you incorrectly set up a Declare statement you're likely to cause
Access to crash, losing your work in the process.

At this point, simply type the following in the Immediate/Debug window:

?SpecFolder(CSIDL_PERSONAL)

Then, press [Enter], type the following:

?SpecFolder(CSIDL_DESKTOPDIRECTORY)

and press [Enter] again. In each instance, the function returns the proper path to
the appropriate folders for the system you're working on, as shown in Figure
A. As you can see in our example, the PC we're using has a customized path
for the My Documents folder--which would cause problems if we were running code
that depended on hardcoded path information.

Figure A: An application that used hardcoded paths would have problems on our system,
but the Win32 API provides the correct special folder locations.

Alternative approaches

At this point, you might be thinking that you have to go to an awful lot of
work to retrieve information that you'd assume should be easy to get. In a
sense, that's true. Although part of Windows' nature is to break tasks down in
to the smallest reusable chunks possible (resulting in the need to call three
procedures to get a special folder's path), there are other ways to retrieve
the path using less code. However, the technique we used in this article
provides a lowest common denominator approach.

Since we use some of the Windows API's most basic native functions, you can
create code that will run on any 32-bit version of Windows. The other, somewhat
simpler solutions, require additional DLLs or type library files, which places
the burden on you to ensure that the files are installed on your end users'
PCs. However, these alternate techniques are definitely worth looking at,
especially if you're developing for a consistent PC environment. We'll examine
other ways to retrieve special folder paths in a future issue.

Avoid system-dependent problems

Using the Windows API to get path information lets VBA work with the special
folders for the current system environment regardless of OS or user
customizations. In this article, we've reviewed the basics of calling Windows
API procedures and provided you with a function you can incorporate into your
own applications. Next month, we'll look at alternative techniques that can
reduce the amount of code you need and provide access to additional special
folders.