|
VB 与Windows API 讲座(五) Windows 的登录资料库
王国荣 病从口入, 这句话大致上也适用於电脑, 如果我们不??给电脑不明的软体或档案, 就不用担心电脑病毒的肆虐, 不过电脑之所以出状况, 电脑病毒只是其中一种原因, 虽然它是最容易联想到的原因, 但实际上, 就像人一样, 除了部分的病痛来自病毒的感染之外, 仍有不少的病痛来自於身体组织的毛病。 对 Windows 来说, 哪些是最重要的器官组织呢?除了驱动程式与应用程式之外, 当首推登录资料库(Registry Database), 举例来说, 开机时, Windows 会从登录资料库中读取硬体的相关设定, 如果登录资料库的设定与硬体不符合, 就可能无法开机或者 Windows 必须重新进行硬体的侦测, 除了硬体的设定之外, 软体的运作也与登录资料库有极大的关系, 举例来说, 当我们安装 Windows 之後, .bmp 档案预设的开启程式是「小画家」, 但是当我们又安装了其他绘图软体, .bmp 的开启程式却可能变成新安装的绘图软体, 这是因为新安装的绘图软体改变了登录资料库所致。 显然地, 能够了解 Windows 的登录资料库, 在 Windows 出状况时, 将更容易解决问题, 而对程式设计者而言, 甚至可以藉着写入资料到登录资料库而达到改变 Windows 或应用程式行为的目的。
|
Key | Key Handle |
HKEY_CLASSES_ROOT | &H80000000 |
HKEY_CURRENT_CONFIG | &H80000005 |
HKEY_CURRENT_USER | &H80000001 |
HKEY_DYN_DATA | &H80000006 |
HKEY_LOCAL_MACHINE | &H80000002 |
HKEY_USERS | &H80000003 |
但如果要取得这些 Key 的 Subkey Handle, 则必须呼叫 RegOpenKey API 函数, RegOpenKey 含有叁个参数, 意义如下:
(1) ByVal hKey As Long:Key Handle。
(2) ByVal lpSubkey As String:Subkey 的字串。
(3) phkResult As Long:若 RegOpenKey 成功, 则此一参数将传回 Subkey 的 hKey。
举例来说, 我们想取得 HKEY_LOCAL_MACHINE 之下的 "SOFTWARE\Microsoft" Subkey, 则使用的叙述是:
Dim ret As Long, hKey As Long
ret = RegOpenKey(HKEY_LOCAL_MACHINE, "SOFTWARE\Microsoft", hKey)
If ret = 0 Then ' 表示成功,
' hKey 的值即等於 "SOFTWARE\Microsoft" Subkey 的 Key Handle
End If
请注意呼叫登录资料库 API 函数(例如以上的 RegOpenKey)之後, 若成功, 将传回 0, 否则传回非 0 的值, 这一点与 VB 函数的惯例并不相同, 请注意。
RegOpenKey 的第一个参数 hKey 除了可以指定最上层的 Key Handle 值(例如 HKEY_CLASSES_ROOT、HKEY_LOCAL_MACHINE…等)之外, 也可以是一个 Subkey Handle, 以上一段程式为例, hKey 等於 "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft" 的 Subkey Handle, 接着如果我们要取得 "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion" 的 Subkey Handle, 则程式如下:
Dim ret As Long, hKey As Long, hKey2 As Long
ret = RegOpenKey(hKey, "Windows\CurrentVersion", hKey2)
' 则 hKey2 将等於 "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft" 的
' "Windows\CurrentVersion" Subkey Handle
在以上程式中, 请注意不要在 "Windows\CurrentVersion" 之前加上 "\" 使成为 "\Windows\CurrentVersion", 这是错误的表示法。
在登录资料库的存取中, 主要分成 Key 及 Value 两大部分, 首先让我们来了解 Value 的存取。在 Value 的存取方面, Windows API 提供的函数有 RegQueryValue、RegQueryValueEx、RegEnumValue、RegEnumValueEx …等, 这几个函数的宣告式请参本文附录, 从 VB 的习惯来看, 这几个函数并不好用, 主要是因为它们使用了 As Any 的宣告方式, 使得一般使用者很容易传错参数, 而无法得到正确的结果, 为了简化 Value 的存取, 笔者特别撰写一套符合 VB 习惯的 Value 存取函数, 这套函数已经将 API 函数封装起来, 也就是说, 您只要使用笔者所提供的 Value 存取函数, 可以暂时不必了解比较艰涩的 API 函数, 当然, 如果您对 Value 存取的 API 函数有兴趣, 可先参阅附录的原始程式码, 至於详细的介绍, 由於篇幅有限, 笔者将来会以专书说明, 本文则暂且略过。
首先参阅本文附录或者请进入笔者的网站下载相关的原始程式码, 笔者所提供的 Value 存取函数有:GetDefaultValue(读取 Default Value)、GetValue(读取特定 Value)、GetValueByIndex(读取任意 Value)、SetDefaultValue(写入 Default Value)、SetValue(写入特定 Value) 等 5 个, 兹说明如下:
◆ GetDefaultValue 函数:读取Default Value
此一函数的定义是:
Function GetDefaultValue(ByVal hKey As Long, ByVal Subkey As String, Value As String) As Boolean
其中 hKey 及 Subkey 参数用来传入欲读取的 Key 或 Subkey, 而 Value 则用来传回读取之资料, 若呼叫成功, 此一函数将传回 True。假设我们想读取 "HKEY_CLASSES_ROOT\.txt" 的 Default Value, 则呼叫的范例如下:
Dim S As String, ret As Boolean
ret = GetDefaultValue(HKEY_CLASSES_ROOT, ".txt", S)
' 如果 ret 为 True, 则 S 等於读取之资料
' 如果 "HKEY_CLASSES_ROOT\.txt" 没有预设值, 则 S = ""
若已事先求得 "HKEY_CLASSES_ROOT\.txt" 的 Subkey Handle 值, 则以下的呼叫也是正确的:
Dim S As String, ret As Boolean, hKey As Long
' 先呼叫 RegOpenKey 求取 "HKEY_CLASSES_ROOT\.txt" 的 Subkey Handle
ret = RegOpenKey(HKEY_CLASSES_ROOT, ".txt", hKey)
' hKey 已是正确的 Subkey Handle, 所以参数二 Subkey 只要传入空字串即可
ret = GetDefaultValue(hKey, "", S)
◆ GetValue 函数:读取特定Value
GetDefaultValue 只能读取某一个 Key(或 Subkey) 的 Default Value, 而 GetValue 则可以读取特定名称的 Value, 举例来说, 欲读取 "HKEY_CLASSES_ROOT\.txt" Subkey 之下名称为 "Content Type" 的 Value, 则必须使用 GetValue 函数。
GetValue 的函数定义是:
Function GetValue(ByVal hKey As Long, ByVal ValueName As String, Value() As Byte, vType As ValueType) As Boolean
请注意此一函数没有 Subkey 参数, 所以呼叫之前必须先取得 Subkey Handle, 举例来说, 欲读取 "HKEY_CLASSES_ROOT\.txt" Subkey 之下名称为 "Content Type" 的 Value, 呼叫的过程如下:
Dim bArr() As Byte, vType As Long, hKey As Long
' 先呼叫 RegOpenKey 取得 "HKEY_CLASSES_ROOT\.txt" 的 Subkey Handle
ret = RegOpenKey(HKEY_CLASSES_ROOT, ".txt", hKey)
ret = GetValue(hKey, "Content Type", bArr, vType)
除了先取得 Subkey Handle 之外, GetValue 与 GetDefaultValue 另一个最大的不同在於:GetDefaultValue 所读取的资料一定是字串型别, 而 GetValue 所读取的资料除了字串之外, 还可能是其他类型的资料, 在实务上, 笔者利用 bArr 及 vType 两种参数来接受 GetValue 所读回来的不同类型资料, 呼叫之前, bArr 为一个空的 Byte 阵列(利用 Dim bArr() As Byte 产生), 若呼叫成功, bArr 将成为一个含有读取资料的 Byte 阵列(不再是空的 Byte 阵列), 而 vType 则等於所读取资料的类型, 它可能有以下几种类型:
资料类型 |
设定值 |
意义 |
REG_SZ |
1 |
字串 |
REG_EXPAND_SZ |
2 |
可展开式字串 |
REG_MULTI_SZ |
7 |
多重字串 |
REG_BINARY |
3 |
Binary 资料 |
REG_DWORD |
4 |
长整数 |
REG_DWORD_BIG_ENDIAN |
5 |
Big Endian 长整数 |
您可能觉得奇怪, vType 含有以上几种类型, 为什麽读取的资料一律放在 Byte 阵列中(bArr 参数), 这是因为 Windows API 的设计使然, 为了将 Byte 阵列转换成不同型别的资料, 笔者又撰写了以下的几个函数:
资料类型 | 意义 |
ByteArrayToString | 将 Byte 阵列转换成字串 |
ByteArrayToMultiString | 将 Byte 阵列转换成多重字串 |
ByteArrayToLong | 将 Byte 阵列转换成长整数 |
使用以上的转换函数以前, 让笔者先说明 vType 几种资料类型的意义:
(1) REG_SZ:一般的字串, 若 vType 等於此一类型, 则呼叫 ByteArrayToString 进行转换。
(2) REG_EXPAND_SZ:也是呼叫 ByteArrayToString 进行转换, 但此类字串中含有 %WinDir% 之类的字串(注:%WinDir% 表示 Windows 的所在目录), 遇到此类字串, 应该再呼叫 ExpandEnvironmentStrings API 函数将字串展开, 举例来说, 假设 Windows 的所在目录是 "C:\Windows", 则 "LoadHigh %WINDIR%\Command\DOSKey" 经过 ExpandEnvironmentStrings API 的转换之後, 将成为 "LoadHigh C:Windows\Command\DOSKey"。(特别说明:有些程式虽然写入字串资料到登录资料库时, 所设定的资料类型为 REG_SZ, 但字串中仍然含有 %WinDir% 之类的字串, 因此若不能确定读取的字串中是否含有 %WinDir% 之类的字串, 则一律呼叫 ExpandEnvironmentStrings 是比较保险的)
(3) REG_MULTI_SZ:多重字串, 其结构如图-4, 此类资料, 若呼叫 ByteArrayToString 进行转换, 则得到如图-4 的字串, 为了转换此类字串, 笔者提供的副程式是 ByteArrayToMultiString, 呼叫前只要宣告一个空的字串阵列, 例如 Dim S() As String, 则转换之後, S 就等於含有多个字串的阵列了。
(4) REG_DWORD:表示含有 4 个字元的数值, 可呼叫 ByteArrayToLong 进行转换。
(5) REG_DWORD_BIG_ENDIAN:不同 CPU 对於数值的安排方式并不相同, 所谓 Big Endian 指的是高位元组放在前面的安排方式(注:Intel 的 CPU 是低位元组放在前面, 另外请注意 Windows NT 可能在非 Intel 的 CPU 底下执行), 虽然此一类型的位元组安排顺序有点不同, 但是对程式而言, 欲取得正确的长整数, 也是呼叫 ByteArrayToLong 进行转换。
(6) REG_BINARY:Binary 资料, 使用 Byte Array 表示最为恰当, 所以不必转换。
假设我们利用 GetValue 读取 Value 之後, 想将它显示出来, 则以下是参考范例:
ret = GetValue(hKey, Name, bArr, vType)
Debug.Print ValueOutput( bArr, vType ) ' 将读取的资料显示出来
Function ValueOutput(bArr() As Byte, ByVal vType As Long) As String
Dim S As String, S2 As String, length As Integer, L As Long
Dim i As Integer, sArr() As String
Select Case vType
Case REG_SZ, REG_EXPAND_SZ
ByteArrayToString bArr, S
' 呼叫 ExpandEnvironmentStrings
S2 = String(Len(S) + 256, Chr(0))
length = ExpandEnvironmentStrings(S, S2, Len(S2))
S = Left(S2, length - 1)
ValueOutput = "Type=String, Data=" & S
Case REG_MULTI_SZ
ByteArrayToMultiString bArr, sArr
ValueOutput = "Type=MultiString, Data="
For i = LBound(sArr) To UBound(sArr)
ValueOutput = ValueOutput & sArr(i) & ", "
Next i
Case REG_DWORD, REG_DWORD_BIG_ENDIAN
ByteArrayToLong bArr, L
ValueOutput = "Type=Long, Data=" & L
Case REG_BINARY
ValueOutput = "Type=Byte Array, Data="
For i = LBound(bArr) To UBound(bArr)
ValueOutput = ValueOutput + Format(Hex(bArr(i)), "00")
Next i
End Select
End Function
◆ GetValueByIndex 函数:读取任意Value
GetDefaultValue 可读取 Default Value, GetValue 可读取特定名称的 Value, 但如果我们不知道 Value 的名称而想读取某一 Key(或 Subkey) 的所有 Value(包含 Default Value), 则必须使用 GetValueByIndex。
GetValueByIndex 的函数定义是:
Function GetValueByIndex(ByVal hKey As Long, ByVal Index As Long, Name As String, Value() As Byte, vType As Long) As Boolean
与 GetValue 不同的是:GetValue 需传入 ValueName 参数, 而 GetValueByIndex 则是传入 Index, Index 的值是 0~N, 若要读取所有的 Value, 则程式大致如下:
Index = 0
Do
ret = GetValueByIndex( hKey, Index, Name, bArr, vType )
Index = Index + 1
Loop Until Not ret
也就是从 0、1、2…开始读取 Value 的内容, 直到 GetValueByIndex 传回 False 时, 即表示读取了所有的 Value。
就像 GetValue 会传回 Byte 阵列(Value() As Byte 参数) 及资料类型(vType As Long 参数)一样, GetValueByIndex 也会传回 Byte 阵列及资料类型, 但重要的是 GetValueByIndex 还会传回 Value 的名称(Name 参数), 而为了区分 GetValueByIndex 所读取的是 Default Value 还是特定名称的 Value(注:Default Value 的 Index 不一定等於 0), 程式必须判断读回的 Name 参数, 若 Name 参数等於 "", 则表示此一 Index 为 Default Value。以下是利用 GetValueByIndex 读取某一 Key(或 Subkey) 所有 Value 并且加以显示的范例程式:
Dim hKey As Long, Index As Long, Name As String
Dim bArr() As Byte, vType As Long, ret As Boolean
Index = 0
Do
ret = GetValueByIndex( hKey, Index, Name, bArr, vType )
If ret Then
If Name = "" Then Name = "(预设值)"
Debug.Print "Name=" & Name & ", " & ValueOutput( bArr, vType )
Index = Index + 1
End If
Loop Until Not ret
◆ SetDefaultValue 函数:写入Default Value
SetDefaultValue 是与 GetDefaultValue 相对应的函数, 前者用来写入资料, 後者则是读取资料, 两者除了读取与写入的差别之外, 使用方法与参数定义则完全相同。
比较值得注意的事情是, 当我们想写入某一个 Subkey 的 Default Value 时, 若此一 Subkey 不存在, 则 Windows 会自动建立此一 Subkey, 然後才写入 Default Value, 假设 "HKEY_LOCAL_MACHINE\SOFTWARE\kj\Registry" Subkey 并不存在, 则以下叙述:
ret = SetDefaultValue(HKEY_LOCAL_MACHINE, "SOFTWARE\kj\Registry", "kj Registry Master")
会先建立以下两个 Subkey:(HKEY_LOCAL_MACHINE\SOFTWARE 为已存在的 Subkey)
HKEY_LOCAL_MACHINE\SOFTWARE\kj
HKEY_LOCAL_MACHINE\SOFTWARE\kj\Registry
然後才写入 "kj Registry Master" 到 "HKEY_LOCAL_MACHINE\SOFTWARE\kj\Registry" Subkey 的 Default Value。
◆ SetValue 函数:写入特定Value
SetDefaultValue 用来写入 Default Value, 而 SetValue 则用来写入特定名称的 Value, 它的函数定义如下:
Function SetValue(ByVal hKey As Long, ByVal ValueName As String, ByVal vType As Long, Value As Variant, Optional ByVal lenValue As Integer) As Boolean
比较特别的参数是 Value 及 lenValue, 其中 Value 参数所定义的型别是 Variant(不定型), 表示可以接受任何型别的资料, 而 lenValue 参数则以 Optional 的方式来宣告, 表示可以省略。在使用上, 如果呼叫 SetValue 时所设定的是长整数或字串资料, 则 lenValue 参数可以省略, 因为长整数与字串都可以由程式自己取得长度(长整数的长度是 4, 字串的长度则是利用 Len(字串) 来取得), 因此只有设定 REG_BINARY 类型的资料时, 才需要设定 lenValue 参数, 以下是几种典型的呼叫方式:
' REG_SZ 类型资料, Value 参数传入字串
ret = SetValue(hKey, "String", REG_SZ, "String Data")
' REG_DWORD 类型资料, Value 参数传入长整数
ret = SetValue(hKey, "Dword", REG_DWORD, 99999)
' 若是 REG_MULTI_SZ 类型资料, 需使用 Chr(0) 将多个字串串起来
ret = SetValue(hKey, "MultiString", REG_MULTI_SZ, "Str1"+Chr(0)+"Str2"+Chr(0) )
' 以上叁种呼叫方式均省略了 lenValue 参数
' REG_BINARY 类型资料, Value 参数应传入 Byte 阵列, lenValue 参数则传入资料长度
Dim bArr(0 To 20) As Byte
ret = SetValue(hKey, "Binary", REG_BINARY, bArr, 21)
◆ RegDeleteValue API:删除特定 Value
除了笔者所提供的 Value 存取函数之外, 您可能还需要使用 API 函数中的 RegDeleteValue 来删除 Value, 此一函数很容易使用, 所以笔者不再提供封装版的 VB 函数, 它含有两个参数, 意义如下:
(1) ByVal hKey As Long:Key Handle。
(2) ByVal lpValueName As String:Value 的名称, 若传入 "", 则表示删除 Default Value 的内容。
若呼叫成功, 则函数传回 0, 与其他登录资料库 API 的惯例相同。
本段落最後让我们来参考一个比较完整的范例程式, 请开启下载档案中的 value.vbp 专案, 此一程式启动时, 会在 "HKEY_LOCAL_MACHINE\SOFTWARE\kj\Registry" Subkey 底下写入以下 Value:
资料类型 | 名称 | 资料 |
(预设值) | kj Registry Master | |
REG_SZ | StringData | 这是字串 |
REG_MULTI_SZ | MultiString | 字串一(0) +字串二+Chr(0) +Chr(0) |
REG_DWORD | LongData | 99999 |
REG_BINARY | BinaryData | 11 22 33 44 AA BB CC DD |
此时会使用到 SetDefaultValue 及 SetValue 函数, 接着当您按下「显示所有 Value」命令钮时, 如图-5, 程式会读出来所有 Value 并且显示在 ListBox 之中, 此时会呼叫 GetValueByIndex 函数。
最後当程式结束时, 则会删除以上所有的 Value, 此时会呼叫 GetValueByIndex 函数及 RegDeleteValue API 函数, 完整的程式如下:
Private Sub Form_Load()
Dim hKey As Long, ret As Long
ret = SetDefaultValue(HKEY_LOCAL_MACHINE, "SOFTWARE\kj\Registry", "kj Registry Master")
ret = RegOpenKey(HKEY_LOCAL_MACHINE, "SOFTWARE\kj\Registry", hKey)
ret = SetValue(hKey, "StringData", REG_SZ, "这是字串")
ret = SetValue(hKey, "MultiString", REG_MULTI_SZ, "字串一" + Chr(0) + "字串二" + Chr(0))
ret = SetValue(hKey, "LongData", REG_DWORD, 99999)
ret = SetValue(hKey, "BinaryData", REG_BINARY, Array(&H11, &H22, &H33, &H44, &HAA, &HBB, &HCC, &HDD), 8)
Call RegCloseKey(hKey)
MsgBox "已写入资料到登录资料库中, 您可以开启 RegEdit 加以检查!"
End Sub
Private Sub Command1_Click() ' 显示所有 Value
Dim Index As Long, ret As Long, hKey As Long
Dim bArr() As Byte, Name As String, vType As Long
ret = RegOpenKey(HKEY_LOCAL_MACHINE, "SOFTWARE\kj\Registry", hKey)
ret = GetValueByIndex(hKey, Index, Name, bArr, vType)
While ret
If Len(Name) = 0 Then Name = "(预 设 值)"
List1.AddItem Name & vbTab & ValueOutput(bArr, vType)
Index = Index + 1
ret = GetValueByIndex(hKey, Index, Name, bArr, vType)
Wend
Call RegCloseKey(hKey)
End Sub
Private Sub Form_Unload(Cancel As Integer)
Dim Index As Long, ret As Long, hKey As Long
Dim bArr() As Byte, Name As String, vType As Long
ret = RegOpenKey(HKEY_LOCAL_MACHINE, "SOFTWARE\kj\Registry", hKey)
ret = GetValueByIndex(hKey, Index, Name, bArr, vType)
While ret
Call RegDeleteValue(hKey, Name)
' 不可以执行 Index = Index + 1, 因为 Index = 0 的 Value 已删除,
' 後面的 Index 向前递减, 所以 Index = 0 又可以读到 Value,
' 其实在这一个 While 回圈中, 您可以将 Index 变数改成 0
ret = GetValueByIndex(hKey, Index, Name, bArr, vType)
Wend
Call RegCloseKey(hKey)
MsgBox "kj\Registry 的 Value 已删除, 利用 RegEdit 检查时, 记得要先执行功能的「检视/重新整理」!"
End Sub
相对於 Value 的存取, Key 的存取要简单得多, Windows 所提供的 API 函数有 RegCreateKey(建立 Subkey)、RegEnumKey(逐一列举 Subkey)、RegDeleteKey(删除 Key 或 Subkey), 而笔者也仿效 Value 的存取函数, 提供了 GetSubkeyByIndex 替代比较不容易使用的 RegEnumKey, 至於 RegCreateKey 及 RegDeleteKey, 由於比较简单, 所以直接呼叫即可, 以下就让笔者来说明这几个函数的用法。
RegCreateKey 的用法与 RegOpenKey 完全相同, 所不同的是 RegOpenKey 只能开启既有的 Subkey, 而 RegCreateKey 则可以建立 Subkey, 比较特别的是, 如果呼叫 RegCreateKey 所建立的 Subkey 是一个已存在的 Subkey, 则 RegCreateKey 的作用与 RegOpenKey 相同, 由於 RegCreateKey 具有以上的特性, 很多人乾脆就不用 RegOpenKey 了, 而不管开启或建立 Subkey, 都一概使用 RegCreateKey。
此一函数的定义如下:
Function GetSubkeyByIndex(ByVal hKey As Long, ByVal Index As Long, KeyName As String) As Boolean
如果已经了解之前存取 Value 的 GetValueByIndex 函数, 应该不难了解此一函数的用法, 假设我们想列举 "HKEY_LOCAL_MACHINESOFTWARE\Microsoft" 的所有 Subkey, 则程式大致如下:
Dim ret As Long, hKey As Long, Index As Integer, Name As String
ret = RegOpenKey(HKEY_LOCAL_MACHINE, "SOFTWARE\Microsoft", hKey)
While GetSubkeyByIndex(hKey, Index, Name)
Debug.Print Name ' 印出 Subkey Name
Index = Index + 1
Wend
RegDeleteKey 函数含有两个参数:
(1) ByVal hKey As Long:Key Handle。
(2) ByVal lpSubkey As String:Subkey 的字串, 若传入 "", 表示删除 Key。
请注意当我们利用 RegDeleteKey 删除某一个含有 Subkey 的 Key 或 Subkey 时(假设 "HKEY_LOCAL_MACHINE\kj" 含有 "Registry" Subkey, 则删除 "HKEY_LOCAL_MACHINE\kj" 即属於此一情况), 对於 Windows 95 或 NT 来说, 结果是不相同的, Windows 95 会将此一 Key(或 Subkey) 及其所有 Subkey 全数删除, 而 Windows NT 则传回失败值。
为了能够让 Windows NT 也具备 Windows 95 删除所有 Subkey 的功能, 笔者提供了另一个 VB 函数:
Function DeleteSubkeyTree(ByVal hKey As Long, ByVal Subkey As String) As Boolean
此一函数会呼叫 GetSubkeyByIndex 再向下读取所有的 Subkey, 然後一一将它们删除, 程式的内容如下:
Function DeleteSubkeyTree(ByVal hKey As Long, ByVal Subkey As String) As Boolean
Dim ret As Long, Index As Long, Name As String
Dim hSubKey As Long
ret = RegOpenKey(hKey, Subkey, hSubKey) ' 取得 Subkey 的 Handle
If ret <> 0 Then
DeleteSubkeyTree = False
Exit Function
End If
ret = RegDeleteKey(hSubKey, "")
If ret <> 0 Then ' 失败, 表示此一 Subkey 的下一层还有 Subkey
While GetSubkeyByIndex(hSubKey, 0, Name) And _
DeleteSubkeyTree(hSubKey, Name) ' 递回删除 Subkey 的 Subkey
Wend
ret = RegDeleteKey(hSubKey, "")
End If
DeleteSubkeyTree = (ret = 0)
End Function
以上程式在 While 回圈中利用了递回呼叫(在 DeleteSubkeyTree 函数中又呼叫 DeleteSubkeyTree)的特性, 很轻易地就可以向下删除所有层次的 Subkey。
由於登录资料库是 Windows 与应用程式存放常用资料的大本营, 同时也会影响 Windows 及应用程式的运作模式, 因此直接存取登录资料库, 犹如通往 Windows 及应用程式核心的直达车, 确实是一件很过瘾的事情, 但如果胡乱写入资料或更改资料, 却可能造成 Windows 及应用程式执行的错误。
在 DOS 底下, 我们会利用 PATH 环境变数来设定程式的路径, 而当我们输入某一执行档名时, DOS 便会从目前工作目录及设定於 PATH 环境变数中的路径来寻找执行档。
对 Windows 而言, 搜寻执行档的顺序是:(1) 目前工作目录 (2) Windows 的所在目录及 System 目录 (3) 设定於 PATH 环境变数中的路径。如果我们想让使用者直接在「执行」交谈窗输入档名(不必输入完整路径)就可以启动程式, 比较普通的方法是将程式的执行档复制到 Windows 的所在目录(或 System 目录), 或者设定好 PATH 环境变数, 不过这两种方法都有缺点, 将执行档复制到 Windows 的所在目录, 会造成 Windows 所在目录的档案越来越多, 而不易於管理, 设定 PATH 环境变数则必须重新启动电脑才能生效, 且环境变数的空间是有限的。
其实 Windows 搜寻执行档的方法除了以上的 (1)(2)(3) 之外, 还会搜寻登录资料库 "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths" 之下的 Subkey, 假设我们在此一 Subkey 之下, 建立了名称为 "Project1.exe" 而 Default Value 等於 "c:\vb\myapp\Project1.exe" 的 Subkey, 则将来我们在「执行」交谈窗中输入 "Project1" 之後, Windows 就会搜寻到 "c:\vb\myapp\Project1.exe" 的执行档完整路径而启动此一程式, 您可以利用 RegEdit 观察 ""HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths" 的内容, 即可了解其中的端倪。
使用 Windows 时, 我们可能会使用「控制台」来改变系统的设定值, 此时, 系统的设定值是存放在 "HKEY_CURRENT_USER\Control Panel" 之下, 举例来说, 利用「控制台」改变桌面的底图, 将会改变 "Desktop" Subkey 的 Wallpaper 设定值, 反过来说, 如果改变 "HKEY_CURRENT_USER\Control Panel\Desktop" 的 Wallpaper 设定值, 也应该可以改变桌面的底图, 基本上, 这句话是正确的, 但笔者要特别说明的是, 当我们改变登录资料库的内容时, Windows 或应用程式并不知道登录资料库的内容已经改变了, 因此不一定会立刻反映出来, 而通常要等到下次开机, Windows 或应用程式重新读取登录资料库时才有作用。(注:有关「控制台」相关的设定, Windows 提供了另一个 API 函数—SystemParametersInfo, 呼叫此一函数除了改变登录资料库之外, 也会通知 Windows 立刻反应结果)。
在档案总管里面, 如果我们双按 .txt 的档案, 档案总管会启动「记事本」开启此一档案, 如果双按 .bmp 的档案, 则会启动「小画家」开启此一档案…, 此一工作模式, 也与登录资料库有关, 以下就让笔者以 ".txt" 为例, 来说明登录资料库中的相关设定。
首先请检视 HKEY_CLASSES_ROOT 之下的 ".txt" Subkey, 它的 Default Value 等於 "txtfile", 接着继续在 HKEY_CLASSES_ROOT 之下寻找此一内容("txtfile")的 Subkey, 果然可以找到, 而它的 Default Value 等於 "纯文字文件", 接着再检视 "txtfile" 之下的内容, 以笔者机器为例, 结构如下:
txtfile: (预设值)="纯文字文件"
shell: (预设值)=""
open: (预设值)=""
command: (预设值)="C:\\WINDOWS\\NOTEPAD.EXE %1"
print: (预设值)=""
command: (预设值)="C:\\WINDOWS\\NOTEPAD.EXE /p %1"
由於以上的结构, 使得我们在 ".txt" 的档案上面按下滑鼠右钮时, 所有出现的快显功能表含有「开启旧档」及「列印」两种命令, 如图-6:
图-6 因为 txtfile\shell 之下含有 open 及 print, 所以档案总管的快显功能表会出现「开启旧档」与「列印」两种命令
接着请看 "open\command" 的 Default Value(预设值), 它等於 "C:\WINDOWS\NOTEPAD.EXE %1", 这表示当我们选取「开启旧档」命令时, 档案总管会将其中的 %1 替换成 .txt 档案名(假设是 abc.txt), 然後执行 "C:\WINDOWS\NOTEPAD.EXE abc.txt", 同样的, 由於 "print\command" 的 Default Value(预设值) 等於 "C:\WINDOWS\NOTEPAD.EXE /p %1", 当我们选取「列印」命令时, 档案总管的所执行的命令是 "C:\WINDOWS\NOTEPAD.EXE /p abc.txt"。
在图-6 的快显功能表中, 「开启旧档」是以粗体显示, 这表示当我们双按 ".txt" 档案时, 所启动的命令是「开启旧档」, 在此笔者想问的是, 为什麽双按启动的是「开启旧档」, 而不是「列印」, 其实这跟 "txtfile\shell" 的 Default Value 有关, 由於 "txtfile\shell" 的没有 Default Value, 所以档案总管便以第一个 Subkey(=open)为双按时的命令, 如果我们将 "txtfile\shell" 的 Default Value 改成 "print", 则双按所执行的命令将变成「列印」。
笔者以上所举的只是少数的几个例子, 了解越多 Key 及 Value 的意义, 将越懂得藉助登录资料库的设定来控制 Windows 及应用程式的行为, 本文受限於篇幅, 暂且介绍至此, 如果您有兴趣进一步了解登录资料库内各种 Key 及 Value 的意义, 坊间可以找到不少中英文的参考书。
有不少读者问:「不同程式之间, 如何共用资料?」, 这个问题真的很困难, 对 Windows 3.1 而言, 记忆体的定址空间对所有程式而言是共用的, 为了达到资料的共用, 可以由程式 A 配置资料, 然後传递资料的位址给程式 B, 程式 B 便可以透过此一位址使用此一资料, 而达到共用资料的目的, 这一种共用资料的特色是每一程式都可以存取记忆体中任何位址的资料, 不管这份资料是否属於自己所有, 乍看之下, 此一存取资料的模式十分方便, 但也正因为如此, 很容易破坏他人的程式码造成当机, 而这正是 Windows 3.1 最为人诟病的一件事情。
对 32-bits 的 Windows(Windows 95 或 NT)而言, 每个程式都拥有独立的 2GB 定址空间, 首先让笔者来解释何谓独立的定址空间, 假设程式 A 某一资料的位址是 &H12340, 而资料的内容是 "AAAA", 但是对程式 B 而言, 同样的 &H12340 位址却可能存放着 "BBBB" 的资料, 虽然位址相同, 但实际上, 在 Windows 的巧妙安排下, 使用的却是不同的记忆体位置, 为什麽要这麽做呢?道理很简单, 也许程式 A 有 bug 把资料写错了位置, 如果它的定址空间不是独立的, 便可能破坏其他程式的程式码或资料, 而造成其他程式的错误, 而相反的, 若每一个程式的定址空间都是独立的, 写得再烂的程式也不会毒到别人的程式, 如此一来, 便可以让 Windows 更趋於稳定。
但有一得, 却也有一失, 由於每个程式的定址空间是独立的, 便造成了程式之间共用资料的困难, 为了共用资料, 最普遍的解决方式是利用「记忆体对映档案」(Memory Mapped File)或是 ActiveX EXE 来达到资料共用的目的, 但这两种解决方案都不算简单, 而本期我们介绍的登录资料库正是一个简单的资料共用方法, 只要程式之间讲好资料存放在哪一个 Key 及 Value 之下, 就可以利用本文所介绍的函数存取资料而达到共用的目的。