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.

Aa140088.ima-logo(en-us,office.10).gif

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:

  [Public | Private] Declare Sub name Lib "libname" _[Alias "aliasname"] [([arglist])]

[Public | Private] Declare Function name Lib "libname" _[Alias "aliasname"] [([arglist])] [As type]

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.

Listing A: API function and constant declarations

  Public Declare Function SHGetSpecialFolderLocation _
    Lib "shell32" (ByVal hWnd As Long, _
    ByVal nFolder As Long, ppidl As Long) As Long

Public Declare Function SHGetPathFromIDList _
    Lib "shell32" Alias "SHGetPathFromIDListA" _
    (ByVal Pidl As Long, ByVal pszPath As String) As Long

Public Declare Sub CoTaskMemFree Lib "ole32" (ByVal pvoid As Long)
    
Public Const CSIDL_PERSONAL = &H5
Public Const CSIDL_DESKTOPDIRECTORY = &H10
Public Const MAX_PATH = 260
Public Const NOERROR = 0

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.
[ Figure A ]

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.

Copyright © 2001 Element K Content LLC. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of Element K Content LLC is prohibited. Element K is a service mark of Element K LLC.