8.5 Programming with Visual BasicVisual 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.
8.5.1 Talking with the Outside World in VBVB 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.
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 interfacesBesides 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:
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 subtletiesThe 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 VBNow 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 functionsVB includes a set of functions for accessing the Registry. Unfortunately, they are so limited as to be practically worthless:
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.BASEven 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.
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 CloneYou 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 clone8.5.3.1 Creating the initial treeThe 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 treeThe 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:
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 valuesSo 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) |