Rare is the application that doesn't need to iterate through some collection of system data. Common needs include cruising system lists of installed fonts, current clipboard contents, printers, processes, or windows. More esoteric enumerations might be determining what domain groups the current user belongs to, or rifling through the records within a metafile.
Also works with VB5
Of the dozens of enumerations offered by the system, only a handful of basic enumeration strategies provide results. By far, the easiest and most common strategy employs a callback function, which the system calls once for each data instance. This technique was off-limits to VB developers until VB5 introduced the AddressOf operator. Another enumeration strategy is to fill a buffer with multiple data instances on a single call. This technique, used principally by NT-only DLLs, is especially suitable to class-based wrappers, because no callbacks (which must be made to BAS modules) are required. An increasingly archaic strategy requires the user to make repeated calls for the next data instance until no more are returned. In this column, I'll look at using each of these enumeration styles.
Some tasks offer a choice of enumeration methods. For example, you can look for a specific window with either a GetWindow loop or a series of EnumWindows callbacks. Windows offers the GetWindow, and the related GetTopWindow and GetNextWindow, APIs to navigate the system windows list. Prior to VB5, this was the only way to do so. At its most basic, a GetWindow loop looks like this:
' Loop through window list and add
' any/all new tasks.
hWnd = GetTopWindow(0)
Do While hWnd
' Determine if this meets task
If IsTask(hWnd) Then Call _
' Get next window and move on.
hWnd = GetNextWindow(hWnd, _
You can even descend window hierarchies by nesting loops and drilling down successively into lists of the children of each returned higher-level window. Unfortunately, this technique gets messy fast. There's also no guarantee you won't devolve into infinite loops or process windows that no longer exist. These sorts of limitations are common in enumeration schemes that rely on the user requesting each next piece of data.
Request a Callback
The best alternative to the potentially troublesome GetWindow loop is to request and accept callbacks from the system by calling EnumWindows. There's no way to fall into an infinite loop, and Windows assures you it won't pass handles to invalid windows. You typically request a callback by pointing to a routine you want the system to call once for each data instance. Often, you're allowed to pass a single piece of data yourself, which the callback procedure might use to determine what processing is required of it.
One limitation with system callbacks is that they must occur within a standard BAS module. If you're collecting cumulative data during the callbacks, it's often either necessary or most efficient to use module-level variables to store the data before return.
Frequently, passing a single Long value to each callback is all that's required, as is the case when you want to fire an enumeration of each top-level window's children (see Listing 1). But perhaps you want to support several types of enumerations within a single module, and each requires a different type of user-supplied data. In cases like this, you can modify the function declaration so that the lParam value is As Any rather than As Long. Doing this allows you to pass just about anything to the designated callback procedure (see Listing 2).
Callbacks can be the easiest, but at the same time most restricting, type of enumeration. A method remains that falls between callbacks and request loops on the complexity/flexibility scale. Several enumeration functions provide a snapshot of the desired data at the time you make the call. You're assured of the returned data's validity at call time, but might be less certain by the time it's used.
Diverging a bit from the windows theme, you can grab a snapshot of all running processes within the system with a single call to EnumProcesses. But rather than requesting the address of where to call back with each data instance, EnumProcesses wants the address of a buffer in memory into which the system will place an array of Longs, each representing a process handle (see Listing 3). EnumProcesses is an NT-only call; the Win9x equivalent is CreateToolhelp32Snapshot, followed by calls to Process32First and Process32Next.
Coming around full circle, some enumerations place large quantities of data within the buffer you supply, but must be called once for each new data instance, such as FindFirstUrlCacheEntry and FindNextUrlCacheEntry (see Resources,
Ask the VB Pro, September 1999). Other enumerations fill a buffer with as many data instances that fit, then tell you how many they've given you, such as NetShareEnum (see Resources,
Ask the VB Pro, January 2000). Picking apart the buffers can be anywhere from trivial, as is the case with an array of Longs returning from EnumProcesses, to nightmarish, as with the complicated structures returned from the Internet cache APIs (see Listing 4).
In cases like this, nothing helps like a little understanding of what pointers are and how you can best use them to your advantage in VB (see Resources, "Pick Pointers Apart" in Ask the VB Pro, February 2001). You might hear people scoff at the notion of counting bytes, but lemme tell yaif you don't count them in situations like this, you'll be the one bitten.
Karl E. Peterson is a GIS analyst with a regional transportation planning agency and serves as a member of the Visual Studio Magazine Technical Review and Editorial Advisory boards. Online, he's a Microsoft MVP and a section leader on several VSM forums. Find more of Karl's VB samples at