Registry Tools


The names and behavior of the registry functions have little relation to my mental picture of the registry. One glance at the Registry Editor tells you that it contains a tree data structure, but unfortunately, the Windows designers chose to call the registry elements keys and values instead of branches and leaves, nodes and items, or some other evocative names. I’m sure this terminology is a metaphor for some real life object that has keys of keys or keys of values, but so far I haven’t run into one of those objects.


The way I imagine it, the registry is a tree of node objects. Each node can contain more nested node objects, and it can contain one or more item objects. Every node is named, but often you can access a node more efficiently through its handle.


The first item of any node (the default) is unnamed, and it might or might not have a value. In 16-bit Windows, that was the end of the story. In 32-bit Windows, a node can contain multiple named items in addition to the first unnamed one. Generally, you’ll only find multiple items on the final nodes (those without nested nodes), but there’s no rule to that effect, and if you choose to put lists of items on your middle level nodes, you won’t be alone.


Items are where the registry data is actually stored. The most common formats are strings, integers, and binary data—String, Long, and Byte array in Visual Basic terms. The 16-bit registry supported only string items, and you’ll see compa­ti-bility remnants such as integers stored as strings rather than as integers.


This structure might seem inconsistent, but that’s the idea. You’re supposed to be able to put anything you want anywhere in the registry. And you can. But the price is that when inspecting the registry, you have to be ready to extract anything from anywhere.


The functions required to do anything from any language are necessarily complex, but I’ve simplified them at two levels. First, the MRegTool module (REG­TOOL.BAS) makes some of the messier API functions more consistent and less Visual Basic–hostile, but still leaves the look and feel of a functional API.
The CRegNodes and CRegItems collections provide an easier object-oriented
interface.


Some of the API functions are inherited from 16-bit Windows
but have enhanced 32-bit versions with Ex on the end (Reg­Create­KeyEx rather than RegCreateKey). Don’t even think about using the old versions. Assuming any relationship between the 16-bit and 32-bit registries leads to madness. If you think you see functions called RegCreateKey, RegOpenKey, RegEnumKey, RegQueryValue, and RegSetValue in the Win32 documentation, it’s an illusion. Only the Ex versions really exist. I made this myth a reality in the Windows API type library. If you really want to use the old compatibility versions, you’ll have to write the Declare statements yourself.

Opening and reading keys


There’s a standard pattern to reading registry values, but it’s not as obvious as reading from a more typical tree structure such as the file directory tree. You can’t simply pass a string indicating the point at which you want to enter the tree. You have to worry about handles to root keys, handles to subkeys, and value types. Let’s look at the simplest useful example.


GetRegStr gets the most common type of registry data—a string. Usually, if you want a registry string, you know exactly what you’re looking for and where to find it. You don’t want to mess with key handles or data types. So if you want some Control Panel data, just ask for it:

Debug.Print GetRegStr(“Control Panel\Desktop”, “IconTitleFaceName”)

Here’s the function that makes this possible:

Function GetRegStr(sKey As String, sItem As String, _
Optional ByVal hRoot As EROOTKEY _
= HKEY_CURRENT_USER) As String
Dim e As Long, hKey As Long, s As String
‘ Open a subkey
e = RegOpenKeyEx(hRoot, sKey, 0, KEY_QUERY_VALUE, hKey)
ApiRaiseIf e
Dim ert As EREGTYPE, c As Long
‘ Get the length and make sure it’s a string
e = RegQueryValueEx(hKey, sItem, 0&, ert, 0&, c)
ApiRaiseIf e
BugAssert ert = REG_SZ
If c <> 0 Then
s = String$(c - 1, 0)
‘ Read the string
e = RegQueryValueExStr(hKey, sItem, 0&, ert, s, c)
ApiRaiseIf e
End If
RegCloseKey hKey
GetRegStr = s
End Function

First you call RegOpenKeyEx to open a subkey of one of the root keys. There are six root keys, of which you’ll generally use HKEY_CLASSES_ROOT for COM data and HKEY_CURRENT_USER or HKEY_LOCAL_MACHINE for general data. Most of the juiciest string data is in HKEY_CURRENT_USER, so GetRegStr makes it a default argument.


One unusual thing about registry functions is that they always return a Win32 error code rather than returning useful data and letting you check errors with GetLastError (Err.LastDllError in Visual Basic). My registry wrappers use the ApiRaiseIf procedure (see “Turning API Errors into Basic Errors” on page 251) to turn any non-zero error codes into raised Visual Basic errors.


Next you call RegQueryValueEx to get the size and type of the item. This is where the messy part usually begins because you have to take different actions depending on what type you come up with. But this function assumes the caller wants a string and asserts for anyone foolish enough to pass an invalid request. A second call to a string-specific RegQueryValueEx alias gets the data. Having got what we came for, we close the subkey handle and leave.

Reading values


If the registry portion of the Windows API had been written in Visual Basic, everyone (including C++ programmers) would have an easier time. The problem is that a registry value can be any of several types, but C++ doesn’t have a type that can contain any of several types. Visual Basic has Variant. We can make the registry API functions much simpler by mapping the messy parts of getting typeless types into Variant wrappers.


When using raw API functions, you can get values by name with RegQuery­ValueEx or by index number with RegEnumValue. Normally, you use RegEnum­Value to enumerate through all the items in a node. I wrap these functions in the more Visual Basic–friendly GetRegValue and GetRegValueNext functions. These functions look pretty much the same, so I’ll show only GetRegValueNext:

Function GetRegValueNext(ByVal hKey As Long, _
i As Long, _
sName As String, _
vValue As Variant) As Long
Dim cName As Long, cData As Long, sData As String
Dim ordType As Long, cJunk As Long, ft As FILETIME
Static hKeyPrev As Long, cNameMax As Long
‘ When enumerating, cache required data the first time
If hKeyPrev <> hKey Or cNameMax = 0 Then
hKeyPrev = hKey
GetRegValueNext = _
RegQueryInfoKey(hKey, sNullStr, cJunk, pNull, _
cJunk, cJunk, cJunk, cJunk, _
cNameMax, cJunk, cJunk, ft)
If GetRegValueNext Then Exit Function
End If

‘ Get the value name and type in the first call
vValue = Empty
cName = cNameMax + 1
sName = String$(cName, 0)
GetRegValueNext = _
RegEnumValue(hKey, i, sName, cName, _
pNull, ordType, pNull, cData)
If GetRegValueNext Then
If GetRegValueNext <> ERROR_MORE_DATA Then
Exit Function
End If
End If
sName = Left$(sName, cName)

‘ Handle each type separately
Select Case ordType
Case REG_DWORD, REG_DWORD_LITTLE_ENDIAN
Dim iData As Long
GetRegValueNext = _
RegEnumValueInt(hKey, i, sName, cName + 1, _
pNull, ordType, iData, cData)
vValue = iData

Case REG_DWORD_BIG_ENDIAN ‘ Unlikely, but you never know
Dim dwData As Long
GetRegValueNext = _
RegEnumValueInt(hKey, i, sName, cName + 1, _
pNull, ordType, dwData, cData)
vValue = MBytes.SwapEndian(dwData)

Case REG_SZ, REG_MULTI_SZ ‘ Same thing to Visual Basic
sData = String$(cData - 1, 0)
GetRegValueNext = _
RegEnumValueStr(hKey, i, sName, cName + 1, _
pNull, ordType, sData, cData)
vValue = sData

Case REG_EXPAND_SZ ‘ Expand environment variables
sData = String$(cData - 1, 0)
GetRegValueNext = _
RegEnumValueStr(hKey, i, sName, cName + 1, _
pNull, ordType, sData, cData)
vValue = MUtility.ExpandEnvStr(sData)

Case Else ‘ Catch REG_BINARY and anything else
Dim abData() As Byte
ReDim abData(cData)
GetRegValueNext = _
RegEnumValueByte(hKey, i, sName, cName + 1, _
pNull, ordType, _
abData(0), cData)
vValue = abData

End Select

End Function

The function starts by calling RegQueryInfoKey to get all possible information about the current key. Most of it is junk in this context, but the length of the longest value will be needed later and is therefore cached for subsequent calls. Afterward, we pretend to get the data but provide a too small buffer. All we want here is the name and the type. Notice that the first call uses a generic Reg­­EnumValue. After we have acquired the type, we use API aliases such as Reg­-Enum­ValueInt, RegEnumValueStr, and RegEnumValueByte to get the appropriate type-specific data.