by Dan Fergus
Reprinted with permission from Visual Basic Programmer's Journal, 4/98, Volume 8, Issue 4, Copyright 1998, Fawcette Technical Publications, Palo Alto, CA, USA. To subscribe, call 1-800-848-5523, 650-833-7100, visit www.vbpj.com, or visit The Development Exchange
Using an INI file to store your settings is for amateurs. Work around the limitations of SaveSetting and GetSetting and really access the registry.
Odds are you still use INI files for storing application-specific information. But to really make your apps a part of Windows 95, Windows NT or whatever comes next, you need to leave the INI file behind and start using the System Registry. I'd like to show you why the INI file has outlived its usefulness, the advantages of using the registry, and how to use and modify a class I built to do the dirty work of registry access.
Admittedly, Win.ini had its place. In the early days of Windows, all programs that needed to store custom information used the Win.ini file, and it worked fine. But Windows grew. Moreand more apps began to write to the Win.ini file. It became big, slow, and fragile. If anything corrupted your Win.ini, countless apps were affected.
Then Windows 3.0 appeared, with support for using API calls to write and read from private INI files. GetPrivateProfileString and WritePrivateProfileString soon became many a programmer's favorite API calls. This method beat sharing a single file, but it too began to have problems. Hundreds—sometimes thousands—of INI files started popping up all over folks' hard drives. These files had no security, and were restricted to a maximum size of 64K.
The basic INI file provided no mechanism for easily storing user-specific information. Worse yet, those who started using private INI files discovered that different apps could no longer share information.
A potential solution appeared with Windows 3.1: its Registration Database, implemented in a tree hierarchy and as a binary file. But the registration database didn't expose an API, because it was intended only for the use of the system to store OLE-specific information. That changed with Windows 95, whose System Registry can be accessed. In fact you get two sets of APIs to use for accessing registry values. The system registry also eliminates the 64K limit that existed in both the INI file and the Win 3.1 registry. It is still based on the tree hierarchy device of the Win 3.1 Registration Database, but it has been divided across multiple files.
The 16-bit versions of the API calls don't let you assign more than one value to a key, nor do they let you assign a name to the value you put in the registry. I mention the older versions in case you come across them in the documentation. You can spot them by the absence of the Ex on the end of the API call for the 32-bit version. You use the API calls RegSaveKey and RegSaveKeyEx to save a Name\Value pair to the registry. Don't use the older versions unless you need to read an unnamed value that had been written to by the older version.
map your appAs the registry has grown, so has the amount and type of data being stored. Make no mistake about it, the registry contains a lot of information. For example, you'll find all the system Shell information stored in registry keys. And every creatable OLE class has an entry in the registry. Here I'll focus on how to store application-specific information though direct manipulation of the registry. If you want to delve into the registry's system and shell data, see Keith Pleas's article, "Hacking the Windows Registry" [VBPJ March 1996].
The complexity of the API calls used to access the registry led me to create my CRegistry class (see Table 1). Being an old C/C++ coder, I can handle most API calls; but the registry calls are some of the hardest to use and understand.
Table 1: PropertyDescription
RootSets the active Root Key
KeySets the active SubKey
AutoCreateWhen True, creates active key if nonexistent
ValueCountThe count of values found in a key
SubKeyCountThe count of subkeys in the active key
MethodDescription
CreateCreates a new key and makes it active
DeleteKeyDeletes the key and any subkeys
DeleteValueDeletes the Name\Value pair from a key
GetNameReturns the name of a Name\Value pair
GetValueReturns the value of a Name\Value pair
SetValueSets the value of a Name\Value pair
GetSubKeyReturns the name of a subkey in the active key
RefreshReloads the cached data from the active key
Table 1: The Hardest-Working Class in Registry Business. The CRegistry class was designed to have an "active" key. This active key is defined by the values in the Root and Key properties. The active key's values are held in the class and can be accessed using class methods. The default for AutoCreate is True and the default for Root is HKEY_LOCAL_MACHINE.
To view the registry, use RegEdit.exe or Regedt32.exe. Regedt32, the NT version, separates each root key into its own window. Both editors provide the same basic operations, so feel free to choose either.
As you begin to look into the registry you'll notice the similarity to the directory/folder structure of Windows (see Figure 1). The registry has top-level keys, often called root keys. Windows NT and Windows 95 have six root keys (see Table 2).
Each root can hold any number of values or keys. A key is a node of the registry hierarchy. Each key can have can have one or more named values and any number of subkeys.
Each value is associated with a name (unless you're still using the 16-bit API calls). These name/value pairs provide the values you set using the API. You can also create a new key under any Root Key or any other key, in the same way that you create folders in the Windows file structure.
When you create a name for the Name\Value pair, you can use spaces or backslashes but not wildcard characters. The value portion of the Name\Value pair has to be one of the 10 data types you can store in the registry (see Table 3). Only three are used with any regularity. In my sample code I only implement the REG_SZ type, a null-terminated string. REG_BINARY is restricted to 1 MB.
A key doesn't have to have any named values associated with it. However, every key does have a default value—always of the REG_SZ data type—that may or may not have a value assigned to it.
As I've said, VB does give you access to the registry through four calls: SaveSetting, GetSetting, GetAllSettings and DeleteSetting. All your entries are placed in the registry's 'HKEY_CURRENT_USER\Software\VBA and Visual Basic Project Information' key. The calls simplify both your access and your options, however.
For example, just try to delete a key when it has subkeys. Using DeleteSetting generates a runtime error, Error 5 - Invalid Procedure Call. The workaround requires you to traverse the sub-key structure yourself and delete each bottom key, working your way up the to the parent that you wanted deleted in the first place. And you can store only string data using SaveSetting, so you lose the flexibility of data stores that the registry provides.
Access to other parts of the registry is what makes using the registry so useful. In particular the SOFTWARE subkey from HKEY_LOCAL_MACHINE is used by just about every application on the system. If your program is well behaved, it will create a company key under SOFTWARE and store your application-specific information there. If your group produces more than one app, each should have its own subkey under your group key.
Table 2: HKEY_CLASSES_ROOTUsed by Windows for filename-extension associations and OLE application information
HKEY_CURRENT_USERA subkey of HKEY_USERS that defines the profile for the user who is currently logged on
HKEY_LOCAL_MACHINEConfiguration information for a particular computer, including hardware and OS data and software
HKEY_USERSStores user profiles on the local machine
HKEY_CURRENT_CONFIGContains the hardware profile for the system startup; these values are non–user-specific
HKEY_DYN_DATAContains performance data
Table 2: Get to the Root of the Matter. There are six root keys in your operating system, each of which has its own purpose. Knowing what that purpose is will help you use the system registry.
Table 3
REG_SZA null-terminated string
REG_BINARYData stored in a binary format of any form
REG_DWORDA long value
REG_DWORD_BIG_ENDIANA long (32-bit) number with the low-order byte being the most significant
REG_EXPAND_SZA string containing unexpanded refernces to environment variables
REG_MULTI_SZA string containing several strings, separated by single null characters and terminated by two null characters
REG_NONENo data type defined
REG_RESOURCE_LISTA device-driver resource list
Table 3: Data for the Asking. Several types of data can be stored in the registry. The first three types are used most often. Only REG_SZ data types are used in the article code.
Use the registry API set with careAs a developer, you use the registry to store application- and user-specific information. Where were the windows when the application was closed? Do users want tooltips? Do they want large toolbar buttons or small? Any kind of information that should carry over from previous executions can be put into the registry.
It was tough to maintain information in an INI file that related to different versions. Yet in the hierarchical structure of the registry you can just add a new key for a new version. Then you can easily support multiple versions of your app.
Windows stores software and hardware information in the registry. You may not want to write into these keys—but you may want to know what sort of hardware is available, and you can get much of that information from the registry.
Figure 1: The View from RegEdit. To view the registry, use RegEdit.exe or Regedt32.exe (the NT version). Both editors provide the same basic operations. Note the similarity to the directory/folder structure of Windows. The registry has top-level keys, often called root keys. Windows NT and Windows 95 have six root keys. Each root can hold any number of values or keys. A key is a node of the registry hierarchy. Each key can have one or more named values and any number of subkeys.
To get data from the registry, first open a key. Once it's open, you can get data from any value in that key. However, when you open a registry key you consume system resources. Be sure to close every key you open. Pseudocode for this simple operation looks appropriately simple:
OpenKey( key name )
GetValue
CloseKey
You should be so lucky. The real thing is complex and loaded with declare statements (see Listing 1). Use the RegOpenKeyEx API call to open a key, RegQueryValueEx to get a value, and RegCloseKey to close the key.
Now I'm going to show you how to dive deeply into the registry. I'm also going to warn you that GPFs in abundance await the unwary. Writing the right thing to the wrong place (or the wrong thing to the right place) can not only lock up your system, it may even kill the OS. Be careful what you do, and back up your registry before you start poking around.
Still there? Good. But don't say I didn't warn you.
That said, I will provide something of a net for your high wire act: all my samples use a new key in HKEY_LOCAL_MACHINE: SOFTWARE\DFergus. It should be a pretty safe key for you—one where you can freely delete and add values without affecting other programs on your system.
Listing 1 (VB5)
Declare Function RegOpenKeyEx Lib "advapi32" Alias _
"RegOpenKeyExA" (ByVal hKey As Long, _
ByVal lpSubKey As String, _
ByVal ulOptions As Long, ByVal samDesired As Long, _
phkResult As Long) As Long
Private Declare Function RegQueryValueEx Lib "advapi32" _
Alias "RegQueryValueExA" (ByVal hKey As Long, _
ByVal lpValueName As String, _
ByVal lpReserved As Long, ByRef lpType As Long, _
ByVal szData As String, _
ByRef lpcbData As Long) As Long
Private Declare Function RegCloseKey Lib "advapi32" _
(ByVal hKey As Long) As Long
Listing 1: Opening Up the Register. To get data from the registry, first open a key. Once it's open you can get data from any value in that key. However, when you open a registry key, you consume system resources. Be sure to close any and every key you open. As you can see, going beyond VB's easy but limited calls takes some doing. But in my experience it's worth it for the added flexibility. Use the RegOpenKeyEx API call to open a key, RegQueryValueEx to get a value, and RegCloseKey to close the key.
I designed the class to hide the 'open, read, close' architecture of the registry and expose only methods resembling the SaveSetting and GetSetting statements. You define an "active" key by setting the Key property. This causes the class to open, read, and close the key. You then get values from the cached data. Originally I designed the class to open the key and keep it open until the class was destroyed or the user closed it through a Close method. But after much angst (and discussion with colleagues), I thought better of keeping a system resource open for an extended period. Internally the Name\Value pairs are kept in a multiple-dimension array. My class only reads and writes strings, ignoring the other data types that can be stored in the registry.
Windows NT has a lot to do with security. Windows 95 does not. The CRegistry class uses system default security privileges. You'll see several API calls that use security attributes type definitions. I didn't design an advanced implementation of this class that would handle the NT security concerns, but you're welcome to do so.
turn key, unlock classAs I mentioned earlier, you'll write all your sample Name\Value pairs into the SOFTWARE\DFergus subkey of HKEY_LOCAL_MACHINE. Create this new key using the Create method:
Dim cReg As New CRegistry
cReg.Root = RootValues.HkeyLocalMachine
cReg.Key = "SOFTWARE"
cReg.Create("DFergus")
bRet = cReg.SetValue("Version", "5.00.11")
Set the desired root key and subkey using the properties Root and Key. The default for Root is HKEY_LOCAL_MACHINE, so you could skip the first line of my sample. Key is the subkey of the root key. No "\\" precedes the subkey name. Often programmers try to use the UNC naming style in their registry work, but trust me—you won't get far. Because you can specify multiple subkeys in the value of this property, you might just as well have entered "SOFTWARE\Microsoft\Cryptography\Defaults."
Once you've created the desired key, you'll put in some values. The class's SetValue method takes a name and value, then writes the information into the active key. Or you can use the optional parameters of the Create method to perform both actions in one call:
bRet = cReg.Create("DFergus", _
"Version", "5.00.11")
If you try to create a subkey that already exists, the class opens that subkey and reads the values. But if you try to open a nonexistent one, you might run into trouble. The underlying registry APIs provide two methods of opening a key, RegOpenKeyEx and RegCreateKeyEx:
RegOpenKeyEx(m_varRoot, m_varKey, _
0, lAccess, m_lOpenKey)
RegCreateKeyEx(m_varRoot, m_varKey, _
0, vbNull, REG_OPTION_NON_VOLATILE, _
KEY_ALL_ACCESS, sa, m_lOpenKey, _
lpdwDisposition)
The CRegistry class has a property called AutoCreate, set to True by default. Whenever a key is opened, AutoCreate tells the class to use RegCreateKeyEx instead of RegOpenKeyEx. Consequently any subkey you specify gets created if it doesn't exist. Using the class with this property set to False raises an error when you try to set Key to a nonexistent key.
Some differences between WinNT and Win95 affect what you do next. If you try to open the same key multiple times under Win95 without closing the previous handle(s), the same handle returns each time. Win95 keeps a reference count of how many times the key was opened. If you close the handle only once, you leave the key open and tie up system resources. On the other hand, WinNT returns a different handle each time you open a key. If you then close each handle, the key gets closed and all is well.
Also, Win95 doesn't care what access mask is passed to RegOpenKeyEx. Passing KEY_READ still lets you write to the opened key. But WinNT forces you to pass the correct access or it won't let you perform the operation. I learned this when I did my initial development on the road with Win95, then came back to the office and my WinNT machine. Suddenly the class I'd been using for weeks stopped working. Needless to say, be sure to test your code on both platforms. And to manage these operating system differences, the private function OpenKey method of CRegistry performs a Boolean OR operation on the KEY_READ and KEY_WRITE:
Private Function OpenKey( _
Optional lAccess As Long = KEY_READ _
Or KEY_WRITE) As Boolean
Fortunately, using the CRegistry class hides this type of detail and keeps you from tripping over Win95/WinNT incongruities, but it's still interesting to note the things you run into as you go about your daily chores.
Get data backNow that you can create a key and place values in it, you'll have no trouble getting data out of the registry. You had added the values to the key, so you know what you're looking for. Just use the GetValue method directly:
Dim cReg As New CRegistry
Dim sValue as String
cReg.Root = RootValues.RegLocalMachine
cReg.Key = "SOFTWARE\DFergus"
sValue = cReg.GetValue("Version")
If you were just browsing through the registry, you wouldn't know what values were in a key. In this case you'd use the ValueCount property to iterate through the entries of the key and get both the name and value of each pair:
Dim cReg As New CRegistry
Dim sValue as String
Dim sName as String
cReg.Root = RootValues.RegLocalMachine
cReg.Key = "HARDWARE\Description\System"
For i = 0 to cReg.ValueCount - 1
sName = cReg.GetName(i)
sValue = cReg.GetValue(i)
Next I
Passing the parameter as a Variant allows the class to effectively overload the method, allowing you to access the data either by index or name (for the code for GetValue and GetName, see Listing 2).
As I've said, each key has a default value. To access the default value using the CRegistry class, you use the same GetValue and SetValue pair you use when you access a named value. However, you leave the vRequest field blank:
cReg.key = _
"SOFTWARE\DFergus\Application\_
CurrentVersion"
tmp = cReg.GetValue("")
cReg.key = "SOFTWARE\ _
DFergus\Application\Version\" + tmp
tmp = cReg.GetValue("ReportErrors")
Last stop on this excursion through the class methods is DeleteKey. This method takes care of another difference between Windows 95 and Windows NT, along with the DeleteSetting runtime error problem I mentioned earlier. CRegistry uses the API RegDeleteKey. Under Win95 this key deletes not only the key specified but any associated subkeys as well. In contrast, the WinNT version of RegDeleteKey deletes only the particular key specified and leaves subkeys alone. So DeleteKey uses a recursive call to make the same method work for either operating system (see Listing 3).
You won't have trouble with the public members of the CRegistry class. To get a feel for what's happening, look into the private method that drives the class, GetKeyInfo. This method performs all direct access to registry data. The first part of the function checks the AutoCreate flag and opens the key (see Listing 4). The call to RegQueryInfoKey gets information about the requested key. This is done to get the count of keys and values, along with the maximum size of the buffers you'll need as you iterate through the key getting data. It would have been just as easy—but not as foolproof—to define maximum sizes for the buffers.
here we go loop do loopOnce these values are obtained, the function goes into a Do Loop until it runs out of values to query. The first action inside the loop determines the value's data type. A call to RegEnumValue is made, with all nonessential values set to a null value. The open key is specified, along with the desired index. You're looking for the IType value that reveals what type of data is stored in the value. Possible values for the data types are defined in the Enum RegTypes. Once you ascertain that you have a string data value, read the data and store it into your array. Reset the strings and length variables before each call to RegEnumValueStr. Otherwise you may get an error back from the API call, stating that more data is available. This error results from passing a string that lacks the space needed to hold the data passing back.
Listing 2 (VB5)
Public Function GetName(ByVal iRequest As Integer) _
As String
If iRequest >= LBound(m_aStringData, 2) _
And iRequest <= UBound(m_aStringData, 2) Then
GetName = m_aStringData(0, CInt(iRequest))
Else
GetName = ""
End If
End Function
Public Function GetValue(ByVal vRequest As Variant) _
As String
On Error GoTo GetValue_Error
If VarType(vRequest) = vbInteger Then
GetValue = m_aStringData(1, CInt(vRequest))
Else
Dim i As Integer
i = LBound(m_aStringData, 2)
Do
If StrComp(m_aStringData(0, i), vRequest) = 0 Then
GetValue = m_aStringData(1, i)
Exit Do
End If
i = i + 1
Loop While i <= UBound(m_aStringData, 2)
End If
GetValue_Exit:
Exit Function
GetValue_Error:
MsgBox Err.Description, , Err.Source
GoTo GetValue_Exit
End Function
Listing 2: A Variant Paramter. Using a Variant parameter lets the class use both indexes or names when retrieving values from a key. GetName is more straightforward in that you always get the name by an index.
If you've browsed the API calls, you may wonder where I got RegEnumValueStr. Actually it's an aliased call that causes the data to be passed back into a string variable. Another quirk of VB arose while I was testing the code. I'd defined one of the parameters incorrectly. The test app ran fine inside the IDE, but when I compiled the app and ran it I got an access error and the program terminated. Now I've often seen VB totally disappear when I've improperly used an API call from within the IDE. When I'm using APIs I always save my work before I run the app. But this is the first time I've found VB running the app normally as p-code, then crashing after compiling.
I use the last loop in GetKeyInfo to gather the subkey names within the active key. Using the RegEnumKeyEx call, each name is retrieved and added to the member variable array m_aSubKeyData. Once all the data is stored in internal, private string arrays, the public members of the class can access it.
Listing 3 (VB5)
Private Function DeleteKeyAdv(sKey As String) As Long
Dim lIndex As Long
Dim lpftLastWriteTime As FILETIME
Dim lRet As Long
Dim lRet1 As Long
Dim lValuename As Long
Dim strValueName As String
Dim lOpenKey As Long
Dim sPrevKey As String
lRet1 = -1
sPrevKey = m_varKey
If ERROR_SUCCESS = RegOpenKeyEx(m_varRoot, _
m_varKey & "\" & sKey, 0, KEY_ALL_ACCESS, _
lOpenKey) Then
lIndex = 0
Do
lValuename = 255
strValueName = String$(lValuename, 0)
lRet = RegEnumKeyEx(lOpenKey, lIndex, _
strValueName, lValuename, 0&, 0&, 0&, _
lpftLastWriteTime)
If lRet = ERROR_NO_MORE_ITEMS Then
RegCloseKey lOpenKey
If ERROR_SUCCESS = RegOpenKeyEx(m_varRoot, _
m_varKey, 0, KEY_ALL_ACCESS, lOpenKey) Then
lRet1 = RegDeleteKey(lOpenKey, sKey)
End If
Else
Dim s As String
m_varKey = m_varKey & "\" & sKey
s = Left$(strValueName, lValuename)
lRet1 = DeleteKeyAdv(s)
m_varKey = sPrevKey
End If
Loop While lRet <> ERROR_NO_MORE_ITEMS
RegCloseKey lOpenKey
End If
DeleteKeyAdv = lRet1
End Function
Listing 3: A Recursive Call. Using a recursive call into DeleteKeyAdv, we can make the DeleteKey method work on Windows 95 or Windows NT.
You may feel some concern over the caching of the data. Well, I've provided a method to update the cached data. Calling the public member Refresh causes the class to go through the work detailed above, thereby getting new data from the registry. The simplicity of the public class members can make your transition from INI files to open use of the registry a snap.
Download the code for this article here