Team LiB   Previous Section   Next Section

8.5 Programming with Visual Basic

Visual Basic used to be regarded as a toy language, in large part because that's what it was. In true Microsoft tradition, though, it has been continually enhanced, revised, tweaked, and improved to the point where it's a real honest-to-goodness programming tool. While hard-core programmers may look down their noses at any language with "Basic" in its name, many administrators have come to know and love VB because it makes it extremely easy to construct robust applications with the full Windows look and feel.

Besides that, VB includes a wide range of components that allow it to easily connect to large databases, generate custom reports, and do a number of other things that are much more difficult to do in C++ (or even Perl, unless you're already fluent). A good friend of mine described VB by saying that its learning curve didn't reach as high as Visual C++, but it was a lot flatter at the bottom.

While you could use VB to write a tool whose purpose was to manipulate the Registry, it's more likely that you'll need to add Registry access to a VB program you already have (or are writing). Accordingly, in this section I focus on how to get data into and out of the Registry; that means opening and closing keys, enumerating keys and values, querying and setting values, and deleting keys and values. If you want to do anything else, you can do so using the API definitions discussed next.

As in the sections on C++ and Perl above, I'm going to assume that you're already familiar with VB and how to use it. If you're really interested in learning lots more about Registry programming in any language--but particularly VB--I recommend Inside the Windows 95 Registry (O'Reilly & Associates).

8.5.1 Talking with the Outside World in VB

VB is arguably the most successful programming tool ever developed.[10] Its success is primarily because it's easy to write programs that actually accomplish something. This ease of use in turn comes from the ways that VB lets you extend its base functionality to add new features. First of all, you can write new procedures and functions in VB itself. This allows you to build your own library of reusable pieces you can apply to new programs as you write them.

[10] In terms of sales, anyway; let's not start any religious wars about what the One True Language is or should be.

That's nothing very new, though; almost all other languages offer some support for recycling code so it can be reused. VB also offers a sophisticated component model based on ActiveX controls; almost any functionality can be wrapped up into an ActiveX control so that other VB programmers can just drag and drop it into their own programs. This is part of the reason why so many VB programs sport sophisticated interfaces, with things such as calendars, spreadsheet-style grids, and other frills. Since those elements can be packaged and reused, many programmers do just that. As a side effect of this componentization, there's a healthy market for selling VB components, and this acts as a further spur to component development.

8.5.1.1 DLL interfaces

Besides its component support, though, VB allows you to load any Windows DLL and call the routines it exports. Most of its support for Win32 API routines is actually implemented this way; there are function declarations that map a VB routine or symbol name to an exported symbol in a DLL somewhere. Here's an example:

Declare Function RegOpenKey Lib "advapi32.dll" _
      Alias "RegOpenKeyA" _
      (ByVal hKey As Long, ByVal lpctstr As String, _
      phkey As Long) As Long

This tells VB that you're declaring a function named RegOpenKey. The actual implementation of the function lives in the advapi32.dll library, and the function in that DLL is actually named RegOpenKeyA. (Remember, all the Registry routines have both ANSI and Unicode variants, but VB usually uses the ANSI versions.) The rest of the function declaration contains the argument list. This particular function takes three parameters:

  • hKey is declared as a Long, and the ByVal keyword tells VB to pass the value of the parameter, not its location in memory. This distinction is critical, since the DLL being called expects data to arrive in a particular format.

  • lpctstr is declared as a String; because it's also declared with ByVal, its contents are passed instead of its address. There's another reason why the string is declared with ByVal: VB uses its own string format, which the standard Win32 DLLs can't decipher. In this case, the ByVal keyword tells VB to convert the string into a standard ANSI string, with a NULL terminator, before passing it to the DLL.

  • phkey is not passed by value; instead, its address is passed in to the DLL so that the DLL can return a handle to the newly opened key. When you don't use ByVal, VB assumes you're passing parameters by reference. You can also use the ByRef keyword to explicitly declare that you want something passed by reference.

The last element in the declaration is the return type of the function. The Win32 API standard is that all functions return a long integer, which corresponds to VB's Long type, so that's what RegOpenKey returns.

8.5.1.2 A few more subtleties

The VB documentation includes an entire chapter on how to construct the correct VB function declaration for any C or C++ DLL routine. Even after reading this chapter several times, you may find the details confusing. Rather than send you back to read it again, let's see if I can boil the rules down to their essentials.

First of all, you've already seen the basic rule above: if you're passing in a variable the API routine fills in and returns, you need to pass it by reference, not with ByVal. Strings are the exceptions to this: you always need to include ByVal so that VB knows it should convert strings to and from its own funny format instead of passing them on to the unsuspecting Win32 DLLs.

The corollary to this rule is that you use ByVal when you're passing in a non-string parameter the API routine can't modify. Examples include the HKEY you must pass in for all the Registry routines or the REGSAM and DWORD values you use with RegCreateKeyEx.

In C, C++, and Perl, you probably use NULL sometimes to indicate that you don't want to supply a value for a parameter. The Registry API routines allow this for most parameters, but you'll quickly run into trouble if you do something like the following:

Call RegCreateKeyEx(HKEY_CURRENT_USER, "SOFTWARE\LJL\ArmorMail\PFXLocation", 
                    0,                   'reserved
                    0,                   'class -- but it's WRONG!
                    0,                   'dwOptions
                    KEY_READ, 
                    0,                   'security attributes -- WRONG AGAIN
                    resultKey,
                    disposition);

This seems perfectly legal; after all, it's a well-known fact that NULL is just a textual representation of 0. Unfortunately, this code isn't passing a pointer whose value is NULL. Instead, you're passing a pointer that points to a NULL. This is like the difference between sending a letter to your spouse and sending a letter about your spouse--the consequences can be unintended and possibly severe.

Here's the correct way to call RegCreateKeyEx. The fix is to add ByVal to the two pointer parameters (lpSecurityAttributes and lpClass). This tells VB that you really want to pass NULL pointers instead of pointers to NULL:

Call RegCreateKeyEx(HKEY_CURRENT_USER, "SOFTWARE\LJL\ArmorMail\PFXLocation", 
                    0,                   'reserved
                    ByVal 0,             'class
                    0,                   'dwOptions
                    KEY_READ, 
                    ByVal 0,             'security attributes
                    resultKey,
                    disposition);

Here's another bear trap that's waiting to snap shut on your ankle. Some of the Registry APIs accept raw data. For example, RegSetValueEx accepts the value contents you want to store as a block of type BYTE. Since VB doesn't know whether you're storing a DWORD, a string, or something else, the function prototype doesn't specify a definite type. VB includes a rough equivalent, the As Any keyword. When you use it, you're telling VB not to check the datatype of that parameter, which is tantamount to begging for trouble.

The solution recommended by Ron Petrusha in Inside the Windows 95 Registry is to declare aliases of functions that normally might use As Any; e.g., this declaration adds an alias for RegSetValueEx that "knows" it's storing a string value:

Declare Function RegSetStringValue Lib "advapi32.dll" _
      Alias "RegSetValueExA" _
      (ByVal hKey As Long, ByVal lpValueName As String, _
      ByVal Reserved As Long, ByVal dwType As Long, _
      ByVal lpData As String, ByVal cbData As Long) As Long

This makes it possible to declare the lpData parameter as a string so the VB compiler can check it for correctness. You can also define similar aliases for setting DWORD, REG_MULTI_SZ, or REG_BINARY data.

8.5.2 Using the Registry with VB

Now that you know what to watch out for when calling the Registry API routines, it's time to move on to actually writing some Registry code in VB.

8.5.2.1 The VBA functions

VB includes a set of functions for accessing the Registry. Unfortunately, they are so limited as to be practically worthless:

  • You may only access keys under HKCU\Software\VB and VBA Program Settings. Period. This is a severe limitation if you're writing programs that need to access keys that aren't under HKCU.

  • The provided routines can work only with one level of keys, so if you open a key named HKCU\Software\VB and VBA Program Settings\MyStuff, you can't access values under HKCU\Software\VB and VBA Program Settings\MyStuff\CurrentVersion.

  • You can store and retrieve only string values: no binary or DWORD data allowed.

These limitations came about because the built-in Registry routines were designed to be seamlessly compatible when the compiled VB applications were run on Win3.x, Win9x, Windows NT, and 2000. This means that the functionality is restricted to the lowest common denominator. There's no reason to use them unless you want your programs to run under Win3.x; not likely if you're reading this book. I won't talk about them any further.

8.5.2.2 Using WINREG.BAS

Even though the built-in VB functions are unsuitable for most uses, you still have another alternative: you can use the original Win32 API routines with suitable VB function declarations. If you follow the rules in the previous section, you could easily write your own set of VB function declarations for the Registry API, but doing so would be wasteful, because Andrew Schulman has already done so. WINREG.BAS (available from the O'Reilly web site at http://www.oreilly.com) contains declarations for all the Registry API routines discussed in this chapter (except for RegQueryMultipleValues), plus some additional routines you may find useful. You need this file to use the examples later in the chapter, and you'll probably want to use it in your own projects.

You can also use a third-party VB control that encapsulates Registry functions into a higher-level set of routines. The Desaware Registry Control (http://www.desaware.com) is one example, but there are others. The chief drawback to these controls is that they cost money, but they can save you time if you're not entirely comfortable with using the raw Registry routines.

Since all WINREG.BAS does is put a VB-compatible face on the API routines described earlier in the chapter, I'm not going to reiterate how those routines work or what their parameters are. Instead, let's see them in action.

8.5.3 Example: A RegEdit Clone

You may have noticed that RegEdit looks like a pretty simple program. For the most part, it is; it has to gather small amounts of data from the user, then pass that data to one or another of the Registry API routines. In Inside the Windows 95 Registry, Ron Petrusha provides a RegEdit clone written in Visual Basic! It's not truly a clone; in fact, it doesn't do anything except display keys and values, but it does so with the familiar tree control, just like RegEdit. However, if you look at the clone in operation (see Figure 8.1), you'll be hard-pressed to tell the difference between the two.

Figure 8.1. The RegEdit clone
figs/mwr2_0801.gif
8.5.3.1 Creating the initial tree

The first step in creating a RegEdit clone is to build the VB form definitions. Since that has nothing to do with the Registry, I won't talk about it here. (If you want to see the code, check out http://windows.oreilly.com/registry.) Instead, let's focus on the interesting stuff. The first block of interest is in the main form's Load method. It creates a new root key named "My Computer" in the tree list, then adds nodes for each root key:

Set nodRegTree = TreeView1.Nodes.Add("home", tvwChild, "HKCR", _
                                    "HKEY_CLASSES_ROOT", "closed", "open")
Set nodRegTree = TreeView1.Nodes.Add("home", tvwChild, "HKCU", _
                                    "HKEY_CURRENT_USER", "closed", "open")
Set nodRegTree = TreeView1.Nodes.Add("home", tvwChild, "HKLM", _
                                    "HKEY_LOCAL_MACHINE", "closed", "open")
Set nodRegTree = TreeView1.Nodes.Add("home", tvwChild, "HKU", "HKEY_USERS", _ 
                                    "closed", "open")
If blnWinNT4 Then
   Set nodRegTree = TreeView1.Nodes.Add("home", tvwChild, "HKCC", _
                                       "HKEY_CURRENT_CONFIG", "closed", "open")
   Set nodRegTree = TreeView1.Nodes.Add("home", tvwChild, "HKDD", _
                                       "HKEY_DYN_DATA", "closed", "open")
End If

Once the tree view is set up, the next step is to make any root key that has subkeys expandable. Part of this is making sure there's enough space to store the name of the longest subkey name that might ever appear:

For intctr = 1 To TreeView1.Nodes.Count
   strNode = TreeView1.Nodes.Item(intctr).Key
   If strNode <> "home" Then
      ' Convert node abbreviation to handle
      Select Case strNode
         Case "HKCR"
            hKey = HKEY_CLASSES_ROOT
         Case "HKCU"
            hKey = HKEY_CURRENT_USER
         Case "HKLM"
            hKey = HKEY_LOCAL_MACHINE
         Case "HKU"
            hKey = HKEY_USERS
         Case "HKCC"
            hKey = HKEY_CURRENT_CONFIG
         Case "HKDD"
            hKey = HKEY_DYN_DATA
      End Select
      
      ' Get size of longest subkey for each key and use that to size the
      ' retrieval buffer
      Call RegQueryInfoKey(hKey, 0, 0, 0, 0, lngLenSubkeyName, 0, 0, 0, 0, 0, 0)
      lngLenSubkeyName = lngLenSubkeyName + 1
      strSubkeyName = String(lngLenSubkeyName + 1, 0)

      ' Retrieve one subkey; if that succeeds, get one subkey to find out if 
      ' there are any subkeys for this node.
      If RegEnumKeyEx(hKey, 0, strSubkeyName, lngLenSubkeyName, 0&, _
                      strClass, ByVal 0, ByVal 0&) = ERROR_SUCCESS Then
         strSubkeyName = Left(strSubkeyName, lngLenSubkeyName)
         'Add node to top-level key so icon appears with a "+"
         Set nodRegTree = TreeView1.Nodes.Add(strNode, tvwChild, _
                          , strSubkeyName, "closed", "open")
      End If
   End If
Next

Notice the use of ByVal 0 to pass NULL pointers in the call toRegEnumKeyEx. There's another trick here, too: the call to RegQueryInfoKey gets the length of the longest subkey name, and that length allocates the buffer that holds the subkey name returned by RegEnumKeyEx. This ensures that the buffer is always long enough to hold the name, even if the longest name comes up first.

This code alone displays the initial tree, but it's dead; users won't be able to expand or contract nodes in the tree as they can with RegEdit. Time for some additional code.

8.5.3.2 Expanding the tree

The next step is to allow users to expand tree nodes that have subkeys. The snippet in Example 8.14, taken from the tree view's Expand method, does just this. The code performs five basic operations:

  1. If the user's trying to expand a subkey, the code opens it. This means no subkeys are opened until the user explicitly asks for them, which is a big performance win.

  2. It calls RegQueryInfoKey to find out how many subkeys there are. In addition, it gets the length of the longest subkey name.

  3. If the number of elements in this node doesn't match the number of subkeys, or if the node's Tag field indicates that it was collapsed, it's time to expand the tree by enumerating each subkey of the target and adding it as a node. In this step, the subkey count obtained in Step 2 is invaluable.

  4. While traversing subkeys of the expanded key, any subkey that has at least one subkey itself is marked as having children. This forces the tree view control to mark them with the "+" icon so users know that node can be expanded.

  5. If any keys were opened in Step 1, they're closed again.

Example 8.14. Expanding a Node in the Registry Tree
' If we're expanding a subkey, open it. This allows us to only open a key
' when the user clicks on it.
If Len(Trim(strSubkey)) > 0 Then
   Call RegOpenKey(hRootKey, strSubkey, hKey)
   blnKeyOpen = True
Else
   hKey = hRootKey
End If

' Find out how many subkeys and values there are and their maximum name lengths
Call RegQueryInfoKey(hKey, 0, 0, 0, lngSubkeys, lngLenSubkeyName, _
                     0, lngValues, lngLenValueName, lngLenValueData, 0, 0)

' If the node isn't fully expanded, go ahead and expand it.
If (Val(Node.Tag)) <> 1 Or (Node.Children <> lngSubkeys) Then

   ' First, delete existing nodes
   lngChildren = Node.Children
   For intctr = 1 To lngChildren
      TreeView1.Nodes.Remove Node.Child.Index
   Next
   
   ' Enumerate all this key's subkeys, adding each one as a node.
   For lngIndex = 0 To lngSubkeys - 1
      lngLenSubkey = lngLenSubkeyName + 1
      strSubkey = String(lngLenSubkey, 0)
      
      ' get the IngIndex'th key
      Call RegEnumKeyEx(hKey, lngIndex, strSubkey, lngLenSubkey, 0&, 0, 0, 0)
      strSubkey = Left(strSubkey, lngLenSubkey)

      ' Add it as a tree node
      Set nodRegTree = TreeView1.Nodes.Add(Node.Index, tvwChild, , strSubkey, _
                                           "closed", "open")

      ' If this node has at least one subkey, add a child to it to enable it 
      ' to be expanded too
      Call RegOpenKey(hKey, strSubkey, hChildKey)
      Call RegQueryInfoKey(hChildKey, 0, 0, 0, lngSubSubkeys, lngLenSubkey, _
                     0, 0, 0, 0, 0, 0)
      If lngSubSubkeys > 0 Then
         lngLenSubkey = lngLenSubkey + 1
         strSubkey = String(lngLenSubkey, 0)
         Call RegEnumKeyEx(hChildKey, 0, strSubkey, lngLenSubkey, 0&, 0, 0, 0)
         Call RegCloseKey(hChildKey)
         ' Add to most recent key
         lngNodeIndex = nodRegTree.Index
         Set nodRegTree = TreeView1.Nodes.Add(lngNodeIndex, tvwChild, , _
                                              strSubkey, "closed", "open")
      End If
   Next
   Node.Tag = 1
End If

' If we opened a key earlier, close it
If blnKeyOpen Then Call RegCloseKey(hKey)
8.5.3.3 Displaying values

So now our clone can display the root keys and expand them when users request it, but what about displaying the values? To get value-display capability, the code needs to do something when the user clicks on a tree node. That means adding a NodeClick event handler. After some setup and variable declarations (which I'm not showing here), our NodeClick routine starts by preflighting the value list and opening the requested key:

' Clear current contents of ListView control
ListView1.ListItems.Clear

' Open the registry key attached to this node
If Len(Trim(strPath)) > 0 Then
   Call RegOpenKey(hRootKey, strPath, hKey)
   blnOpenKey = True
Else
   hKey = hRootKey
End If

The next step is to find out how many values there are and how big the largest value name and contents are. Armed with that data, it's possible to display a default value and stop if there aren't actually any values attached to this key:

' Get value count, max value name length, and max value contents length
Call RegQueryInfoKey(hKey, 0, 0, 0, 0, 0, 0, lngValues, _
                     lngMaxNameLen, lngMaxValueLen, 0, 0)

' Add default value entry if there aren't any real values present
If lngValues = 0 Then
   Set objLItem = ListView1.ListItems.Add(, , "<Default>", "string", "string")
   Exit Sub
End If

If there are real values, the earlier call to RegQueryInfoKey indicates what the biggest value data block is, so the data buffer can be sized accordingly:

' Redimension the byte array for value data using the max value contents length
ReDim bytValue(lngMaxValueLen)

Now the fun begins. The code must enumerate over every value of this subkey, getting both its name and its contents. Each value has a type, and it would be nice to display an appropriate icon in the left-most column of the value list, just like RegEdit :

' Enumerate all value entries of this subkey
For lngIndex = 0 To lngValues - 1
   lngNameLen = lngMaxNameLen + 1
   ' make sure our buffer's big enough
   strValueName = String(lngMaxNameLen, 0)
   lngValueLen = lngMaxValueLen
   Call RegEnumValue(hKey, lngIndex, strValueName, lngNameLen, 0, _
                     lngDataType, bytValue(0), lngValueLen)

   ' Determine icon type
   Select Case lngDataType
      Case REG_SZ, REG_MULTI_SZ, REG_EXPAND_SZ
         strIcon = "string"
      Case Else
         strIcon = "bin"
   End Select
   
   ' if it's empty, substitute "<Default>"; otherwise, use the name
   If lngNameLen = 0 Then
      strValueName = ""
      Set objLItem = ListView1.ListItems.Add(, , "< Default >", strIcon, strIcon)
   Else
      strValueName = Left(strValueName, lngNameLen)
      Set objLItem = ListView1.ListItems.Add(, , strValueName, strIcon, strIcon)
   End If

Users would hate our clone if it displayed everything in binary or hex, so it should neatly format and display the value data, no matter its type. In all cases, a call to RegQueryValueEx, combined with some formatting tweaking depending on the datatype, achieves this happy result. Notice that for binary data there's no call to RegQueryValueEx ; that's because the earlier call to RegEnumValue loaded the data directly into the bytValue array:

' Format and display data
   Select Case lngDataType
   
   	  ' for a string, get the value and display it directly
      Case REG_SZ, REG_EXPAND_SZ
         strTemp = String(lngValueLen, 0)
         Call RegQueryValueEx(hKey, strValueName, 0, 0, ByVal strTemp, _
                              lngValueLen)
         objLItem.SubItems(1) = Left(strTemp, lngValueLen)
         
      ' for a multistring, get the value and pick it apart
      Case REG_MULTI_SZ
         strTemp = String(lngValueLen, 0)
         Call RegQueryValueEx(hKey, strValueName, 0, 0, ByVal strTemp, _
                              lngValueLen)
         strTemp = Left(strTemp, lngValueLen - 2)
         intPos = 1
         While intPos > 0
            intPos = InStr(1, strTemp, Chr(0))
            If intPos > 1 Then
               strTemp = Left(strTemp, intPos - 1) & "|" & _
                              Mid(strTemp, intPos + 1)
            End If
         Wend
         objLItem.SubItems(1) = strTemp

      ' for binary or BIG_ENDIAN values, display  in hex
      Case REG_BINARY, REG_DWORD_BIG_ENDIAN
         strTemp = ""
         For intctr = 0 To lngValueLen - 1
            strHex = Hex(bytValue(intctr))
            If Len(strHex) = 1 Then strHex = "0" & strHex
            strTemp = strTemp & strHex & " "
         Next
         objLItem.SubItems(1) = strTemp

	  ' for a DWORD, display as a DWORD
      Case REG_DWORD
         lngLenDW = Len(lngTemp)
         Call RegQueryValueEx(hKey, strValueName, 0, 0, lngTemp, lngLenDW)
         objLItem.SubItems(1) = lngTemp
      End Select
Next

The last--but not least--step is to clean up any messes made before this point:

' Close the key if we opened it
If blnOpenKey Then Call RegCloseKey(hKey)
    Team LiB   Previous Section   Next Section