15.1 注册表和INI文件简介
在一个操作系统中,无论是操作系统本身还是运行于其中的大部分应用程序,都需要使用某种方式保存配置信息。在DOS系统中,配置信息往往是软件的开发者根据自己的喜好用各种途径加以保存的,比如在磁盘上面写一个二进制的 .dat文件,或者写一个文本文件等,这些配置文件中数据的格式也是各不相同的。
到了Windows 3.x系统中,这种情况发生了变化,虽然软件的开发者还可以沿用这种自成体系的方法,但是系统也提供了一种标准的初始化文件格式(Initialization File)来保存配置信息。初始化文件是一种以INI为扩展名的文本文件,操作系统提供了一套专用的函数对INI文件进行操作。Windows 3.x不仅鼓励程序员使用INI文件,操作系统本身也使用INI文件来保存配置信息,比如,Windows安装目录下的Win.ini文件中保存了桌面设置和与应用程序运行有关的信息;System.ini文件中保存了与硬件配置有关的信息,另外,Control.ini与Program.ini等文件也是很重要的配置文件。
INI文件在使用中存在诸多缺陷。由于INI文件是文本文件,使用任何文本编辑器都可以随意对它进行修改,所以安全性不是很好。另外,INI文件的结构比较简单,无法保存格式复杂的数据,如很长的二进制数据或带回车的字符串等。最主要的缺点还是单个INI文件的大小不能超过64 KB。如果不同的应用程序都将自己的配置信息保存于Win.ini或者System.ini中,那么这些文件的规模很快就会超过限制的长度。如果不同应用程序都使用自己的INI的话,那么集中管理又成了一个问题。
在Windows 9x和Windows NT系列操作系统中,改用了一种全新的方法来管理配置信息,那就是使用集中管理的注册表(Registry)。注册表并不像它的中文名称所称的那样是“表”,而是一种格式由系统定义的数据库,它存放于某些二进制文件中。不同操作系统中对应的文件名可能有所不同,比如,Windows 9x系统中的注册表文件由位于Windows安装目录中的System.dat和User.dat两个文件组成,而NT系统的注册表往往由位于Windows安装目录下的System32\Config目录中的多个文件构成。操作系统将这些不同的文件集中“虚拟”成整个注册表供系统自身及应用程序使用。
与INI文件对于Windows 3.x系统的重要性相比,注册表对于Windows 9x和NT系统来说显得更加重要。因为绝大多数系统使用的配置信息都存放于此,如系统的硬件配置、安装的驱动程序列表、文件的关联信息、系统的网络配置和权限配置等,这些配置信息直接关系到Windows系统的启动和初始化过程。如果注册表受到了破坏,那么轻者Windows的启动过程出现异常,重者可能会使整个系统无法启动。另外,大部分应用程序的配置信息也存放于此,如果注册表受到了破坏,即使操作系统能正常启动,应用程序的运行也可能会受影响。
正是因为注册表结构的封闭性和重要性,我们无法再使用像编辑INI文件那样的简单方法来编辑注册表文件。实际上,Windows系统对注册表文件的保护很严格,当系统在运行的时候,注册表文件是被操作系统以独占方式打开的,其他应用程序即使是用最基本的读权限也无法打开它们,更不用说对它们进行写操作了。
要对注册表进行操作,必须使用系统提供的接口。Windows为此提供了一系列的注册表操作函数,应用程序可以通过它们来完成注册表编辑器(Regedit程序)能完成的全部功能,甚至包括远程操作注册表以及对.reg文件进行导入和导出等操作。
为了提供向下兼容性,Windows 9x和NT系统在支持注册表操作的同时也支持INI文件的操作。实际上对于某些“Copy and play”的小程序来说,需要保存的配置信息并不复杂,使用INI文件可能更加简单实用,而且保存于注册表中的配置信息是无法随文件拷贝到其他计算机中的。如果某些应用程序希望拷贝程序的同时可以拷贝配置信息,那么最好还是使用INI文件,所以在Windows 9x和NT系统中,INI文件的使用还是相当广泛的。
本章用两个单独的例子,详细介绍INI文件和注册表的使用方法。
15.2 INI文件的操作
15.2.1 INI文件的结构
INI文件是一种文本格式的文件,其中的数据组织格式为:
;注释
[Section1 Name]
KeyName1=value1
;注释
KeyName2=value2
...
[Section2 Name]
KeyName1=value1
KeyName2=value2
...
INI文件中可以存在多个小节(Section),每个小节的开始用包括在一对方括号中的小节名称指定,不同的小节不能重名,一个小节的内容从小节名称的下一行开始,直到下一个小节开始为止。用户程序可以按照自己的需求建立多个小节。
在每个小节中可以定义多个键(Key),每一个键由一个“键名=键值”格式的字符串组成,并独自占用一行。在同一个小节中不能存在同名的键,但是在不同的小节中可以存在同名的键。
如果需要在INI文件的某些地方加注释,可以将注释放在单独的一行中,行首以分号开始,注释行出现的地方并没有什么限制,既可以出现在文件的最前面,也可以出现在文件的任何一行中,如上面的例子中注释出现在小节名称的前面以及两个键的中间。
一般来说,如果在自己开发的应用程序中使用系统定义的INI文件,如Win.ini等,由于文件中已经存在多个小节,那么自己建立一个独立的小节比较合适,然后在这个小节中定义不同的键值,比如,下面是笔者的计算机上Win.ini文件的片断:
...
[MCI Extensions]
asf=MPEGVideo
asx=MPEGVideo
m3u=MPEGVideo
mp2v=MPEGVideo
mp3=MPEGVideo
mpv2=MPEGVideo
wma=MPEGVideo
wmv=MPEGVideo
[Hex Workshop]
Path=C:\PROGRA~1\BREAKP~1\HEXWOR~1.1\hworks32.exe
CurrentVersion=3.11
...
其中的“MCI Extensions”小节是Windows系统自身使用的小节,Windows在这里定义了一些媒体文件的关联方式,而“Hex Workshop”小节是安装了十六进制编辑器HexWorkshop后由这个软件创建并使用的,该软件在小节中用“Path”键定义了软件的安装目录、在“CurrentVersion”键中定义了软件的版本号。
如果觉得往系统INI文件中写数据显得不是那么“绿色环保”,那么应用程序可以建立一个独立的INI文件。如本节的例子文件就在自己运行的目录中建立了一个Option.ini文件,并在“Windows Position”小节的“X”,“Y”键中保存窗口的位置,以便在下一次运行的时候将窗口移动到上一次退出时所处的位置,内容如下:
[Windows Position]
X=194
Y=162
...
Windows系统提供了一系列函数对INI文件进行操作,其中包括读取和设置键值,获取小节名称列表及获取和删除整个小节内容等函数。下面的例子演示了这些功能的使用方法。
15.2.2 管理键值
本节的例子程序存放在所附光盘的Chapter15\ini目录中,运行后的界面如图15.1所示。例子程序在运行目录下创建了一个Option.ini文件,程序中的所有操作都是针对这个文件进行的。当用户在“Section”一栏中输入小节名称、在“Key”一栏中输入键名后,如果INI文件中对应的小节和键定义是已经存在的,那么按下“读取Key”按钮后就会将键值读取到“Value”一栏中;而按下“删除Key”按钮的时候,可以将这个键删除。
图15.1 INI文件操作例子的运行界面
在输入小节和键名后继续在“Value”一栏中输入一个字符串,并按下“保存Key”按钮,如果指定键已经存在,那么程序用新的键值替换原来的键值;如果键名不存在,则程序创建这个键;如果创建键的时候小节名是不存在的,那么程序在创建键值之前会自动创建小节;在最极端的情况下,当INI文件也不存在的时候,那么程序也会创建INI文件。
当用户按下“删除Key”按钮将一个小节中的键逐一删除直到全部键都被删除的时候,小节名称并不会被删除,INI文件中还会留有一个空的小节名称,按下“删除Section”按钮后可以将小节名称删除。如果在小节中尚包含有键的时候按下“删除Section”按钮,那么小节名称包括小节中的全部键都会被删除。
每次进行操作后,程序自动将INI文件中的所有小节和键值枚举一遍并将内容显示在图15.1下面的编辑框中,以便观察操作的结果。下面通过分析这个程序来了解这些功能的实现方法。
源文件目录中的Ini.rc文件定义了如图15.1所示的对话框,代码如下:
#include <resource.h>
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define ICO_MAIN 1000
#define DLG_MAIN 1000
#define IDC_SEC 1001
#define IDC_KEY 1002
#define IDC_VALUE 1003
#define IDC_INI 1004
#define IDC_DEL_SEC 1005
#define IDC_DEL_KEY 1006
#define IDC_GET_KEY 1007
#define IDC_SET_KEY 1008
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN ICON "Main.ico"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_MAIN DIALOG 205, 128, 245, 168
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "INI文件操作"
FONT 9, "宋体"
{
RTEXT "Section", -1, 4, 7, 30, 8
EDITTEXT IDC_SEC, 39, 5, 78, 12, ES_AUTOHSCROLL | WS_BORDER | WS_TABSTOP
RTEXT "Key", -1, 4, 23, 30, 8
EDITTEXT IDC_KEY, 39, 21, 78, 12, ES_AUTOHSCROLL | WS_BORDER | WS_TABSTOP
RTEXT "Value", -1, 4, 39, 30, 8
EDITTEXT IDC_VALUE, 39, 37, 78, 12, ES_AUTOHSCROLL | WS_BORDER | WS_TABSTOP
LTEXT "当前INI文件内容:", -1, 8, 57, 141, 8
EDITTEXT IDC_INI, 6, 71, 232, 91, ES_MULTILINE | ES_AUTOVSCROLL |
ES_AUTOHSCROLL | ES_READONLY | WS_BORDER | WS_VSCROLL | WS_TABSTOP
PUSHBUTTON "删除 &Section", IDC_DEL_SEC, 122, 4, 57, 14
PUSHBUTTON "删除K&ey", IDC_DEL_KEY, 183, 4, 57, 14
PUSHBUTTON "读取 &Key", IDC_GET_KEY, 122, 20, 57, 14
PUSHBUTTON "保存Ke&y", IDC_SET_KEY, 183, 20, 57, 14
}
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
汇编源文件Ini.asm的内容如下:
; Ini.asm ---------- Ini 文件操作的例子
; ------------------------------------------------------------------
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff Ini.asm
; rc Ini.rc
; Link /subsystem:windows Ini.obj Ini.res
.386
.model flat,stdcall
option casemap:none ; include 文件定义
include c:/masm32/include/windows.inc
include c:/masm32/include/user32.inc
include c:/masm32/include/kernel32.inc
includelib c:/masm32/lib/user32.lib
includelib c:/masm32/lib/kernel32.lib ; equ 等值定义
ICO_MAIN equ 1000
DLG_MAIN equ 1000
IDC_SEC equ 1001
IDC_KEY equ 1002
IDC_VALUE equ 1003
IDC_INI equ 1004
IDC_DEL_SEC equ 1005
IDC_DEL_KEY equ 1006
IDC_GET_KEY equ 1007
IDC_SET_KEY equ 1008; 数据段
.data?
hInstance dword ?
hWinMain dword ?
szProfileName dword MAX_PATH dup(?)
szBuffer1 byte 32760 dup(?)
szBuffer2 byte 32760 dup(?)
.const
szFileName byte '\Option.ini',0
szSecPos byte 'Windows Position',0
szKeyX byte 'X',0
szKeyY byte 'Y',0
szFmt1 byte '%d',0
szFmtSection byte '[%s]'
szCrLf byte 0dh,0ah,0; 代码段
.code
; 枚举全部 Section 和全部 Key
_EnumINI proc local @szBuffer[256]:byte invoke SetDlgItemText, hWinMain, IDC_INI, NULL ;读取 Section 列表并循环处理invoke GetPrivateProfileSectionNames, addr szBuffer1, \sizeof szBuffer1, addr szProfileName mov esi, offset szBuffer1 .while byte ptr[esi]invoke wsprintf, addr @szBuffer, addr szFmtSection, esi invoke SendDlgItemMessage, hWinMain, IDC_INI, EM_REPLACESEL, FALSE, addr @szBuffer ;读取 Key 列表并循环显示invoke GetPrivateProfileSection, esi, addr szBuffer2, \sizeof szBuffer2,addr szProfileNamemov edi, offset szBuffer2 .while byte ptr [edi]invoke SendDlgItemMessage, hWinMain, IDC_INI, EM_REPLACESEL, FALSE, edi invoke SendDlgItemMessage, hWinMain, IDC_INI, EM_REPLACESEL, FALSE, addr szCrLf invoke lstrlen, edi add edi, eax inc edi .endw invoke lstrlen, esi add esi, eax inc esi .endw ret
_EnumINI endp _GetPosition proc local @szBuffer[512]:byte ;将当前路径和 ini 文件名组合起来invoke GetCurrentDirectory, MAX_PATH, addr szProfileName mov esi, offset szProfileName invoke lstrlen, esi mov ecx, offset szFileName .if byte ptr [esi+eax-1] == '\'inc ecx .endif invoke lstrcat, esi, ecx ;读存放在 ini 文件中的数据invoke GetPrivateProfileInt, addr szSecPos, \addr szKeyX, 50, addr szProfileName push eax invoke GetPrivateProfileInt, addr szSecPos, \addr szKeyY, 50, addr szProfileName pop ecx invoke SetWindowPos, hWinMain, HWND_TOP, ecx, eax, 0, 0, SWP_NOSIZE ret
_GetPosition endp _SavePosition proc local @szBuffer[512]:byte, @szRect:RECT invoke GetWindowRect, hWinMain, addr @szRect invoke wsprintf, addr @szBuffer, addr szFmt1, @szRect.left invoke WritePrivateProfileString, addr szSecPos, addr szKeyX, \addr @szBuffer, addr szProfileName invoke wsprintf, addr @szBuffer, addr szFmt1, @szRect.top invoke WritePrivateProfileString, addr szSecPos, addr szKeyY, \addr @szBuffer, addr szProfileName ret
_SavePosition endp _ProcDlgMain proc uses ebx edi esi hWnd, wMsg, wParam, lParam local @szSection[256]:byte local @szKey[256]:byte local @szValue[256]:byte local @szBuffer[256]:byte mov eax, wMsg .if eax == WM_CLOSE invoke _SavePosition invoke EndDialog, hWnd, NULL .elseif eax == WM_INITDIALOG push hWnd pop hWinMain invoke LoadIcon, hInstance, ICO_MAIN invoke SendMessage, hWnd, WM_SETICON, ICON_BIG, eax invoke _GetPosition invoke _EnumINI .elseif eax == WM_COMMAND invoke GetDlgItemText, hWnd, IDC_SEC, addr @szSection, sizeof @szSection invoke GetDlgItemText, hWnd, IDC_KEY, addr @szKey, sizeof @szKey invoke GetDlgItemText, hWnd, IDC_VALUE, addr @szValue, sizeof @szValue mov eax, wParam .if ax >= IDC_SEC && ax <= IDC_INI mov eax, TRUE ret .elseif ax == IDC_DEL_SEC invoke WritePrivateProfileString, addr @szSection, \NULL, NULL, addr szProfileName .elseif ax == IDC_DEL_KEY invoke WritePrivateProfileString, addr @szSection, \addr @szKey, NULL, addr szProfileName .elseif ax == IDC_SET_KEY invoke WritePrivateProfileString, addr @szSection, \addr @szKey, addr @szValue, addr szProfileName .elseif ax == IDC_GET_KEY invoke GetPrivateProfileString, addr @szSection, \addr @szKey, NULL, addr @szBuffer, \sizeof @szBuffer, addr szProfileName invoke SetDlgItemText, hWnd, IDC_VALUE, addr @szBuffer .endif invoke _EnumINI .else mov eax, FALSE ret .endif mov eax, TRUE ret
_ProcDlgMain endp main proc invoke GetModuleHandle, NULL mov hInstance, eax invoke DialogBoxParam, eax, DLG_MAIN, NULL, offset _ProcDlgMain, NULL invoke ExitProcess, 0
main endp
end main
在对话框关闭的时候,程序在WM_CLOSE函数中调用_SavePosition子程序将对话框窗口当前的位置保存到INI文件中,以便在下一次运行的时候恢复原来的窗口位置。程序在WM_INITDIALOG中调用_GetPosition子程序以恢复上一次保存的位置,这样,程序就可以“记住”窗口的摆放位置。
枚举小节和键值的功能是在_EnumINI子程序中完成的。程序在一开始执行的时候或者在每次收到WM_COMMAND消息的时候都调用这个函数,所以每次用户有所操作,INI文件的变化就会马上在窗口中的编辑框中反映出来。
1.键值的创建和删除
当按下“保存Key”按钮时,例子程序使用WritePrivateProfileString函数保存键值,这个函数可以往指定的INI文件中写入键值,函数的用法是:
invoke WritePrivateProfileString,lpAppName,lpKeyName,lpString,lpFileName
在函数的参数中,lpAppName参数指向包含Section名称的字符串,lpKeyName参数指向包含键名称的字符串,lpString参数指向键值字符串,最后一个参数指向INI文件名字符串。这些字符串都是以0字符结束的。
当这些参数全部指定为字符串的时候,函数将在指定INI文件的指定小节中写入“键名=键值”格式的行;当指定的INI文件、文件中的小节和小节中的键名都已经存在的时候,函数用新键值替换原来的键值;当指定的INI文件存在而小节不存在的时候,函数自动创建小节并将键写入;如果连指定的INI文件也不存在的话,函数会自动创建文件。总之,程序不必考虑INI文件是否存在,小节是否存在或键值定义是否存在等情况,只要调用WritePrivateProfileString函数就可以保证配置信息被正确保存。
WritePrivateProfileString函数也可以用来删除键或者小节,当lpAppName和lpKeyName参数指定了小节名称和键名,而lpString参数指定为NULL的时候,函数将指定的键删除,如例子文件中对“删除Key”按钮的操作就是这样的:
.elseif ax == IDC_DEL_KEYinvoke WritePrivateProfileString,addr @szSection,\addr @szKey,NULL,addr szProfileName
但是使用这种方法逐一将某个小节中的键全部删除时,空白小节的定义字符串“[SectionName]”还保留在INI文件中。如果想要将小节的定义字符串连同小节的全部键定义全部删除的话,可以将lpKeyName和lpString参数全部指定为NULL,而lpAppName参数指定要删除的小节,如例子文件中对“删除Section”按钮的处理代码:
.elseif ax == IDC_DEL_SECinvoke WritePrivateProfileString,addr @szSection,\NULL,NULL,addr szProfileName
如果函数执行成功,将返回一个非0的值,如果执行失败将返回0。在定义键名的时候,注意不用在名称字符串中包括“=”号,因为等号被用来分隔键名和键值,键名也不能以注释字符“;”开始。在定义键值的时候可以使用等号和分号,但注意不要将键值定义为多行的文本。如果在字符串中包含换行和回车,比如将键值字符串指向下列所示的一个字符串:
"hello,world!",0dh,0ah,"this is the second line",0
那么函数会成功地被调用,但是最后的INI文件中会出现这样的内容:
[Section]
Key=hello,world!
this is the second line
显然,函数不加判断地将换行和回车也写到了INI文件中,但是当取回键值的时候,只有第一行能被正确取回,而底下的行将当做格式错误的“垃圾”留在INI文件中。
由于INI文件是以文本方式保存的,所以实际上键值也只能用字符串方式表示,如果需要保存一个数值类型的值,那么程序需要自己使用wsprintf函数将数值转换成字符串后再保存。比如例子程序在退出时为保存窗口位置,就是在_SavePosition子程序中首先用GetWindowRect函数获取窗口位置,然后使用wsprintf函数转换后再保存的。
2.获取键值
获取键值的操作比较方便,因为这时既可以用GetPrivateProfileString函数获取键值字符串,也可以使用GetPrivateProfileInt函数让Windows将键值字符串转换成数值后再返回,就像使用GetDlgItemInt函数转换对话框子窗口中的字符串一样。(比较奇怪的是保存键值的时候并没有一个WritePrivateProfileInt函数,结果每次还要首先使用wsprintf函数!)
GetPrivateProfileString函数的用法是:
invoke GetPrivateProfileString,lpAppName,lpKeyName,lpDefault, \lpReturnedString,nSize,lpFileName
该函数的几个参数与WritePrivateProfileString的参数类似,也是使用lpAppName,lpKeyName和lpFileName参数分别指定小节名称、键名和INI文件名,但是其余几个参数则有所不同:
lpReturnedString参数指向一个缓冲区,函数在这里返回获取的键值字符串,缓冲区的长度用nSize参数指定,当缓冲区的长度太小以至于无法容纳返回的字符串时,字符串会被截止到nSize−1的长度后返回,余下的一个字节用来存放一个0字符用做结尾。
lpDefault参数指向一个默认字符串,当指定的键无法找到的时候,函数将这个字符串拷贝到返回缓冲区中。
GetPrivateProfileString还有两种特殊用法:首先,当lpAppName参数指定为NULL的时候,函数在缓冲区中返回的是全部小节名称的列表,每个小节名以0结尾,全部的名称列表再以一个附加的0结束,返回到缓冲区中的数据格式如下所示:
小节名称1,0,小节名称2,0,…,小节名称n,0,0
另外,当lpAppName参数指定了小节名称,而lpKeyName参数指定为NULL的时候,函数在缓冲区中返回该小节的全部键名列表,每个键名以0结尾,全部列表后面再以一个附加的0结束,如下所示:
键名1,0,键名2,0,…,键名n,0,0
所以用这两种方法调用GetPrivateProfileString函数可以实现枚举小节名称和枚举键名的功能。
不管用何种方式使用GetPrivateProfileString函数,函数的返回值是返回到缓冲区中的字符串长度(长度中并不包括结尾的0字符)。
如果保存的键值是全部由数字字符组成的话(比如例子程序的_SavePosition子程序中用wsprintf函数从窗口位置的坐标值转换过来的字符串),那么可以使用GetPrivateProfileInt函数直接将字符串转换成数值后再返回:
invoke GetPrivateProfileInt,lpAppName,lpKeyName,nDefault,lpFileName
其中lpAppName,lpKeyName和lpFileName参数的使用方法同上,函数将指定的键值字符串转换成数值类型以后返回,但是函数不支持负数,如果键值字符串是“−1234”格式的负数,那么函数的返回值是0。nDefault指定一个默认数值,如果指定的键名不存在的话,函数返回nDefault指定的数值。
例子程序在初始化的时候,在_GetPosition子程序中使用GetPrivateProfileInt函数读出上次退出时保存的窗口位置,并使用SetWindowPos函数将窗口移动到这个位置上。
15.2.3 管理小节
在键名已知的情况下固然可以使用GetPrivateProfileString等函数获取键值,但在某些情况下并不是所有的键名都是已知的,比如一个编辑文件需要保存近来编辑过的文件名列表,它可以建立一个小节如下:
[History]
file1=C:\My documents\Readme.txt
file2=D:\MyApp\Help.txt
file3=C:\download\win32asm.txt
...
这时小节中键的数量和名称就是不定的。另外,也有小节的数量和名称不定的情况,这时就需要对小节或键进行枚举。上一节中已经介绍了在GetPrivateProfileString函数中通过将lpAppName或lpKeyName参数设置为NULL来获取小节名称列表和键名列表的方法。实际上,Windows中还有专门用来实现此功能的函数,这些函数不仅可用来枚举小节和键,也可以用来一次性修改整个小节的内容。
GetPrivateProfileSectionNames函数可以用来返回全部小节名称的列表:
invoke GetPrivateProfileSectionNames,lpBuffer,nSize,lpFileName
lpFileName参数指向INI文件名,lpBuffer参数是一个指针,指向用来返回小节名称列表的缓冲区,nSize参数指定缓冲区的长度。返回到缓冲区中的数据格式也是:“小节名称1,0,小节名称2,0,…,小节名称n,0,0”的格式。当缓冲区太小以至于不能容纳全部数据的时候,后面的数据被丢弃,全部数据被截尾到nSize−2的长度,剩下的两个字节用来保存两个表示结束的0字符。函数的返回值是返回到缓冲区中的数据长度(不包括结尾的0字符)。
GetPrivateProfileSection函数则可以用来返回整个小节的键定义,与上一节介绍的调用GetPrivateProfileString函数时将lpKeyName参数设置为NULL以获取键名列表不同,GetPrivateProfileSection函数返回的是键定义列表。函数的用法是:
invoke GetPrivateProfileSection,lpAppName,lpBuffer,nSize,lpFileName
函数的lpAppName指向一个包含小节名称的字符串,返回到缓冲区中的数据格式为:
键名1=键值1,0,键名2=键值2,0,…,键名n=键值n,0,0
所以,使用这个函数可以同时完成枚举键名和键值的功能。例子程序中就是使用它来枚举键值的,但使用中如果觉得自己处理“键名=键值”字符串来分解键名和键值比较麻烦的话,可以用GetPrivateProfileString函数枚举键名并再次调用它获取指定键的键值。
在Windows 9x中,缓冲区最大不能超过32767 B,而在Windows NT中则没有限制,函数的返回值是返回到缓存区中的数据长度(不包括结尾的0字符)。
WritePrivateProfileSection函数则将“键名1=键值1,0,键名2=键值2,0,…,键名n=键值n,0,0”格式的小节数据一次性全部写入。函数的用法是:
invoke WritePrivateProfileSection,lpAppName,lpString,lpFileName
lpString参数指向包含键值定义列表的缓冲区,函数执行后,指定小节原来的键定义被全部删除,然后加入新的键定义。如果执行成功,函数返回非0值,否则返回0。
15.2.4 使用不同的INI文件
在前面介绍的这些INI文件函数中,当lpFileName参数指定的文件名字符串中不包括路径时,系统将认为文件位于Windows安装目录下,这样当函数创建INI文件的时候,就会把文件创建于Windows安装目录下。但是大部分情况下,希望INI文件位于程序的运行目录下,这样拷贝文件的时候可以连同INI文件一起拷贝,另外,在卸载或删除程序的时候可以避免在Windows目录中留下一个“垃圾”INI文件。
如果希望在程序的运行目录而非Windows目录中建立INI文件,那么最简单的方法就是在INI文件名前面加上“.\”路径,也就是说把文件名写成“.\MyIniFile.ini”的格式,这样系统会在当前目录下建立INI文件。这种方法的缺陷是如果程序运行中需要不断切换当前目录的话,系统就会在不同的地方乱建INI文件。一个常见的情况就是使用“打开文件”通用对话框的时候,系统会将当前目录切换到对话框中浏览的那个目录,这样写INI文件时就会造成INI文件的位置根本无法确定。一个相对保险的办法就是像例子程序中所示的那样,在程序一开始运行的时候就使用GetCurrentDirectory函数获取当前目录,然后将INI文件名添加到目录后组成一个全路径的文件名,以后在所有的操作中都使用这个文件名。而最保险的办法就是首先用GetModuleFileName获取包含全路径的当前执行文件名,再从这个文件名中分离出路径,最后添加上INI文件名。
如果要操作的是Windows安装目录下的Win.ini文件而非其他INI文件时,那么既可以使用上面这些函数,也可以使用另一组专门用于操作Win.ini文件的函数,这组函数是:
invoke GetProfileString,lpAppName,lpKeyName,lpDefault,lpBuffer,nSize
invoke GetProfileInt,lpAppName,lpKeyName,nDefault
invoke WriteProfileString,lpAppName,lpKeyName,lpString
invoke GetProfileSection,lpAppName,lpBuffer,nSize
invoke WriteProfileSection,lpAppName,lpString
可以看出,与操作通用INI文件的函数相比,这组函数的函数名中少了“Private”单词,参数中少了lpFileName参数(因为操作的INI文件名就是Win.ini,并不需要单独指定),所有其他参数的用法都是相同的。
读者可以注意到这些函数中似乎少了一个GetProfileSectionNames函数,实际上并没有这个函数,如果要枚举Win.ini文件中的小节列表的话,只需把GetPrivateProfileSectionNames函数的lpFileName参数指定为NULL即可,并不需要单独设置一个类似于GetProfileSectionNames的函数。
15.3 对注册表的操作
注册表在Windows 9x及NT系统中是很重要的,不谈操作系统本身对注册表依赖性的大小,仅从应用程序的角度来说,除了用来代替INI文件用做保存配置信息以外,有些功能是必须通过操作注册表来完成的,比如,要让一个程序在Windows启动的时候自动运行,那就必须在注册表的HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion \Run子键下面添加一个键值定义;如果要让应用程序和某种数据文件相关联,就必须在注册表的HKEY_CLASSES_ROOT键下面为相应的数据文件扩展名设置关联信息;再比如,编写好一个COM组件以后,要为组件的DLL文件在注册表中添加注册信息,组件才能被其他程序使用。另外,操作系统本身将绝大多数的配置信息保存于注册表中,应用程序在运行中往往需要查询这些系统信息来决定运行的方式。
要自如地使用注册表,必须解决两方面的问题,第一是如何在程序中对注册表进行读写与枚举等操作;第二就是要搜集整理关于注册表键值定义的资料。因为注册表是Windows中出了名的“数据迷宫”,很多键的定义只有Microsoft自己知道,公开的键定义也足够编成一本手册了,仅知道如何读写注册表却不知道该往哪里读写是没用的。由于本节并不是当做注册表的定义手册来写的,所以内容仅涉及第一个方面的问题。本节讨论的内容是:注册表的数据组织方式,以及如何用Win32汇编来读写和枚举注册表的内容。
15.3.1 注册表的结构
1.注册表的数据组织方式
INI文件中的数据是按照两层组织的——只能通过一些在结构上“平行”的小节来归类不同的键,这就像一个驱动器中只能建立一层目录来管理文件一样非常不便,与此相比,注册表的结构有很大改进。
注册表中的数据是分多个层次来组织的,组织的方式类似于磁盘目录的多层组织方式。与文件系统中根目录、子目录和文件这样的层次划分类似,注册表中的数据层次分为根键、键和键值项,其中根键就相当于文件系统中的根目录,键相当于子目录,键值项相当于文件。根键和子键是为了将不同的键值项分类组织而定义的,只有键值项中才包含真正的配置数据。
在Windows自带的注册表编辑器Regedit中就可以看出注册表的结构来。如图15.2所示,注册表中的根键有6个,其名称是Windows规定的,并且是固定不变的,它们分别是HKEY_CLASSES_ROOT,HKEY_CURRENT_USER,HKEY_LOCAL_MACHINE,HKEY_CURRENT_CONFIG,HKEY_DYN_DATA和HKEY_USERS。在每个根键下可以建立不同的键,以HKEY_LOCAL_MACHINE根键为例,下面有HARDWARE,SAM,SECURITY和SOFTWARE等子键,而SOFTWARE键下面又由各种应用程序创建了一些子键,如ACD Systems,Acer和Adobe等,键和子键的层次关系是相对的,就像一个目录既可以是其上层目录的子目录,又可以是其下层目录的父目录一样。
图15.2 注册表的结构
在一个键中既可以继续建立多个子键,也可以同时建立多个键值项,就像一个目录中既可以建立多个子目录,同时也可以存放多个文件一样。图15.2右边窗口中列出的就是ACDSee键中定义的键值项。
每个键值项由键值名称和键值数据组成(就像是文件名和文件中的数据的关系),如上图中键值名称AppMode中的数据是“00 00 00 00”、键值名称UserName中的数据是“aaa”。不像INI文件中的键值只能定义为字符串,注册表键值的数据类型要丰富得多,全部可用的键值类型如表15.1所示。
表15.1 注册表的键值类型
图15.2中的AppMode和UsageCount键值项是REG_BINARY类型的,WindowsPosition键值项是REG_DWORD类型的,其余的键值项是REG_SZ类型的,这3种类型的键值项在注册表中是最常见的。每个键下面还可以有一个没有名称的键值项,称为默认键,默认键必须是REG_SZ或REG_EXPAND_SZ类型的。
2.注册表中的根键
注册表的结构中大量采用“映射”关系,系统定义的6种根键其实存放在不同的文件中。在Windows 9x系统中,HKEY_LOCAL_MACHINE根键的内容存放在System.dat文件中,HKEY_USERS根键的内容存放在User.dat文件中。而在Windows NT系统中,注册表的内容存放得更分散,连HKEY_LOCAL_MACHINE根键中的不同子键SOFTWARE,SAM,SECURITY和SYSTEM等都分开存放在Windows\system32\config目录下的不同文件中。
HKEY_LOCAL_MACHINE和HKEY_USERS根键是注册表中的两大根键,其余的根键都是它们的派生,实际上它们都是这两大根键下面某些子键的映射,如HKEY_CLASSES_ROOT根键是HKEY_LOCAL_MACHINE根键下SOFTWARE\Classes子键的映射,HKEY_CURRENT_CONFIG根键是HKEY_LOCAL_MACHINE根键下Config子键的映射。
HKEY_USERS根键中的内容是用户配置信息,其内容取决于计算机是否激活了用户配置文件。若未激活用户配置文件,则里面只有名为.DEFAULT的单一子键,该子键包括与所有用户相关的各种设置。若激活了用户配置文件并且正确地执行了登录操作,则根键下还会有代表当前登录用户的子键,这时候HKEY_CURRENT_USER根键就是这个子键的映射。
由于HKEY_DYN_DATA保存了系统运行时的动态数据,它反映出系统的当前状态,所以它的数据在每次运行时都是不一样的。应用程序一般不使用这个根键。
对这些实际上是其他数据的映射的根键来说,操作根键上的数据和操作未经映射前的数据产生的效果是一样的,系统建立映射可以让键值数据的组织更清晰,操作起来更加快捷、方便。
15.3.2 管理子键
本节用一个例子来演示对注册表的操作方法,例子代码在所附光盘的Chapter15\Reg目录中,运行后的界面如图15.3所示。例子程序演示了子键的创建和删除,键值项的创建、读取和删除,以及枚举子键和键值项的功能。
图15.3 注册表操作
例子的运行界面例子程序对HKEY_LOCAL_MACHINE根键进行操作,当在“键名”一栏中输入子键名称字符串,并在“键值名”一栏输入键值名称后,按下“读取键值”按钮,如果指定子键中存在这个键值项,程序会读出键值数据并显示在“键值”一栏中;如果按下“删除键值”按钮,那么对应键值项被删除;读者也可以在“键值”一栏中输入其他数据并选定“类型”,然后按下“保存键值”按钮将新的键值数据设置到注册表中。
例子程序也可以对子键进行操作。在对话框最下面的“子键名”一栏中输入子键名称,按下“创建子键”的话,函数会在“键名”一栏指定的键下面创建子键,如果按下“删除子键”按钮,那么“键名”指定的键下面由“子键名”指定的子键会被删除。
每次进行操作以后,程序将“键名”指定的键下面的全部子键和键值项列在编辑框中,以便让用户看到操作的结果。
这里列出了资源脚本文件和源代码。Reg.rc文件定义了如图15.3所示的对话框,文件内容如下:
#include <c:/masm32/include/resource.h>#define ICO_MAIN 1000
#define DLG_MAIN 1000
#define IDC_KEY 1001
#define IDC_VALUENAME 1002
#define IDC_VALUE 1003
#define IDC_TYPE 1004
#define IDC_KEYLIST 1005
#define IDC_SUBKEY 1006
#define IDC_REMOVE_VALUE 1007
#define IDC_GET_VALUE 1008
#define IDC_SET_VALUE 1009
#define IDC_CREATE_SUBKEY 1010
#define IDC_REMOVE_SUBKEY 1011
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN ICON "Main.ico"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_MAIN DIALOG 205, 107, 245, 206
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "注册表操作"
FONT 9, "宋体"
{
RTEXT "键名HKEY_LOCAL_MACHINE\", -1, 9, 9, 105, 8
EDITTEXT IDC_KEY, 118, 7, 121, 12, ES_AUTOHSCROLL | WS_BORDER | WS_TABSTOP
RTEXT "键值名", -1, 4, 26, 30, 8
EDITTEXT IDC_VALUENAME, 39, 24, 139, 12, ES_AUTOHSCROLL | WS_BORDER |
WS_TABSTOP
RTEXT "键值", -1, 4, 43, 30, 8
EDITTEXT IDC_VALUE, 39, 41, 201, 12, ES_AUTOHSCROLL | WS_BORDER | WS_TABSTOP
RTEXT "类型", -1, 4, 59, 30, 8
COMBOBOX IDC_TYPE, 39, 58, 78, 77, CBS_DROPDOWNLIST | WS_TABSTOP
LTEXT "当前键下的所有的子键名:", -1, 8, 78, 141, 8
EDITTEXT IDC_KEYLIST, 6, 92, 232, 91, ES_MULTILINE | ES_AUTOVSCROLL |
ES_AUTOHSCROLL | ES_READONLY | WS_BORDER | WS_VSCROLL | WS_TABSTOP
RTEXT "子键名", -1, 7, 191, 30, 8
EDITTEXT IDC_SUBKEY, 42, 189, 73, 12, ES_AUTOHSCROLL | WS_BORDER | WS_TABSTOP
PUSHBUTTON "删除键值", IDC_REMOVE_VALUE, 183, 23, 57, 14
PUSHBUTTON "读取键值", IDC_GET_VALUE, 122, 57, 57, 14
PUSHBUTTON "保存键值", IDC_SET_VALUE, 183, 57, 57, 14
PUSHBUTTON "创建子键", IDC_CREATE_SUBKEY, 121, 188, 57, 14
PUSHBUTTON "删除子键", IDC_REMOVE_SUBKEY, 182, 188, 57, 14
}
汇编源代码Reg.asm的内容如下:
; Reg.asm -------------- 注册表操作的例子
; -------------------------------------------------------------------
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff Reg.asm
; rc Reg.rc
; Link /subsystem:windows Reg.obj Reg.res
.386
.model flat,stdcall
option casemap:none ; include 文件定义
include c:/masm32/include/windows.inc
include c:/masm32/include/user32.inc
includelib c:/masm32/lib/user32.lib
include c:/masm32/include/kernel32.inc
includelib c:/masm32/lib/kernel32.lib
include c:/masm32/include/Advapi32.inc
includelib c:/masm32/lib/Advapi32.lib ; equ 等值定义
ICO_MAIN equ 1000
DLG_MAIN equ 1000
IDC_KEY equ 1001
IDC_VALUENAME equ 1002
IDC_VALUE equ 1003
IDC_TYPE equ 1004
IDC_KEYLIST equ 1005
IDC_SUBKEY equ 1006
IDC_REMOVE_VALUE equ 1007
IDC_GET_VALUE equ 1008
IDC_SET_VALUE equ 1009
IDC_CREATE_SUBKEY equ 1010
IDC_REMOVE_SUBKEY equ 1011 ; 数据段
.data?
hInstance dword ?
hWinMain dword ?
.const
szTypeSz byte 'REG_SZ',0
szTypeDw byte 'REG_DWORD',0
szFmtSubkey byte '【子键】%s',0dh,0ah,0
szFmtSz byte '【键值】%s=%s (REG_SZ 类型)',0dh,0ah,0
szFmtDw byte '【键值】%s=%08X (REG_DWORD 类型)',0dh,0ah,0
szFmtValue byte '【键值】%s (其它类型)',0dh,0ah,0
szNotSupport byte '程序暂不显示其它类型的键值!',0; 代码段
.code
include _Reg.asm _EnumKey proc _lpKey local @hKey, @dwIndex, @dwLastTime:FILETIME local @szBuffer1[512]:byte local @szBuffer[256]:byte local @szValue[256]:byte local @dwSize, @dwSize1, @dwType mov @dwIndex, 0 invoke SetDlgItemText, hWinMain, IDC_KEYLIST, NULL ;枚举子键invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE, _lpKey, NULL, \KEY_ENUMERATE_SUB_KEYS, addr @hKey .if eax == ERROR_SUCCESS .while TRUE mov @dwSize, sizeof @szBuffer invoke RegEnumKeyEx, @hKey, @dwIndex, addr @szBuffer, addr @dwSize, \NULL, NULL, NULL, NULL.break .if eax == ERROR_NO_MORE_ITEMS invoke wsprintf, addr @szBuffer1, addr szFmtSubkey, addr @szBuffer invoke SendDlgItemMessage, hWinMain, IDC_KEYLIST, EM_REPLACESEL, 0, addr @szBuffer1 inc @dwIndex .endw invoke RegCloseKey, @hKey .endif ;枚举键值mov @dwIndex, 0invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE, _lpKey, NULL, \KEY_QUERY_VALUE, addr @hKey .if eax == ERROR_SUCCESS .while TRUE mov @dwSize, sizeof @szBuffer mov @dwSize1, sizeof @szValue invoke RegEnumValue, @hKey, @dwIndex, addr @szBuffer, addr @dwSize, \NULL, addr @dwType, addr @szValue, addr @dwSize1 .break .if eax == ERROR_NO_MORE_ITEMS mov eax, @dwType .if eax == REG_SZ invoke wsprintf, addr @szBuffer1, addr szFmtSz, addr @szBuffer, addr @szValue .elseif eax == REG_DWORD invoke wsprintf, addr @szBuffer1, addr szFmtDw, addr @szBuffer, dword ptr @szValue .else invoke wsprintf, addr @szBuffer1, addr szFmtValue, addr @szBuffer .endif invoke SendDlgItemMessage, hWinMain, IDC_KEYLIST, EM_REPLACESEL, 0, addr @szBuffer1 inc @dwIndex .endw invoke RegCloseKey, @hKey .endif ret
_EnumKey endp _ProcDlgMain proc uses ebx edi esi hWnd, wMsg, wParam, lParam local @szKey[256]:byte, @szSubkey[256]:byte local @szValueName[256]:byte, @szValue[256]:byte local @dwType, @dwSize mov eax, wMsg .if eax == WM_CLOSE invoke EndDialog, hWnd, NULL .elseif eax == WM_INITDIALOG push hWnd pop hWinMain invoke LoadIcon, hInstance, ICO_MAIN invoke SendMessage, hWnd, WM_SETICON, ICON_BIG, eax invoke SendDlgItemMessage, hWnd, IDC_TYPE, CB_ADDSTRING, 0, addr szTypeSz invoke SendDlgItemMessage, hWnd, IDC_TYPE, CB_ADDSTRING, 0, addr szTypeDw invoke SendDlgItemMessage, hWnd, IDC_TYPE, CB_SETCURSEL, 0, 0invoke _EnumKey, NULL .elseif eax == WM_COMMAND invoke GetDlgItemText, hWnd, IDC_KEY, addr @szKey, 256invoke GetDlgItemText, hWnd, IDC_SUBKEY, addr @szSubkey, 256 invoke GetDlgItemText, hWnd, IDC_VALUENAME, addr @szValueName, 256 mov eax, wParam .if ax >= IDC_KEY && ax <= IDC_SUBKEY mov eax, TRUE ret .elseif ax == IDC_REMOVE_VALUE invoke _RegDelValue, addr @szKey, addr @szValueName .elseif ax == IDC_GET_VALUE ;读取键值mov @dwSize, sizeof @szValue invoke RtlZeroMemory, addr @szValue, @dwSize invoke _RegQueryValue, addr @szKey, addr @szValueName, \addr @szValue, addr @dwSize, addr @dwType .if eax == ERROR_SUCCESS mov eax, @dwType .if eax == REG_SZ invoke SetDlgItemText, hWnd, IDC_VALUE, addr @szValue invoke SendDlgItemMessage, hWnd, IDC_TYPE, CB_SETCURSEL, 0, 0 .elseif eax == REG_DWORD invoke SendDlgItemMessage, hWnd, IDC_TYPE, CB_SETCURSEL, 1, 0invoke SetDlgItemInt, hWnd, IDC_VALUE, dword ptr @szValue, FALSE .else invoke SetDlgItemText, hWnd, IDC_VALUE, addr szNotSupport.endif .else invoke SetDlgItemText, hWnd, IDC_VALUE, NULL .endif ;设置键值.elseif ax == IDC_SET_VALUE invoke SendDlgItemMessage, hWnd, IDC_TYPE, CB_GETCURSEL, 0, 0.if !eax invoke GetDlgItemText, hWnd, IDC_VALUE, addr @szValue, 256 invoke lstrlen, addr @szValue inc eax invoke _RegSetValue, addr @szKey, addr @szValueName, \addr @szValue, REG_SZ, eax .else invoke GetDlgItemInt, hWnd, IDC_VALUE, NULL, FALSE mov dword ptr @szValue, eax invoke _RegSetValue, addr @szKey, addr @szValueName, \addr @szValue, REG_DWORD, 4.endif .elseif ax == IDC_CREATE_SUBKEY invoke _RegCreateKey, addr @szKey, addr @szSubkey .elseif ax == IDC_REMOVE_SUBKEY invoke _RegDelSubKey, addr @szKey, addr @szSubkey .endif invoke _EnumKey, addr @szKey .elsemov eax, FALSE ret .endif mov eax, TRUE ret
_ProcDlgMain endp main proc invoke GetModuleHandle, NULL mov hInstance, eax invoke DialogBoxParam, eax, DLG_MAIN, NULL, offset _ProcDlgMain, NULL invoke ExitProcess, 0
main endp
end main
注册表函数是一组函数名以Reg开头的函数,全部函数包含在Advapi32.dll文件中,所以在源程序的一开始要使用include和includelib语句将Advapi32.inc文件和Advapi32.lib文件包含进来。
例子程序中将把注册表的各种操作分别写成子程序,用_RegQueryValue,_RegSetValue和_RegDelValue子程序来查询、设置和删除键值,用_RegCreateKey和_RegDelSubKey完成子键的创建和删除工作,并在WM_COMMAND消息中根据不同的按钮消息调用相应的子程序。为了让读者不经修改就可以将这些子程序用在其他程序中,将这些子程序放在一个单独的_Reg.asm文件中并在主程序中使用include语句包含进来,文件内容如下:
; _Reg.asm ------几个注册表操作的公用子程序; 查询键值
_RegQueryValue proc _lpszKey,_lpszValueName,_lpszValue,_lpdwSize,_lpdwTypelocal @hKey,@dwReturnmov @dwReturn,-1invoke RegOpenKeyEx,HKEY_LOCAL_MACHINE,_lpszKey,NULL,\KEY_QUERY_VALUE,addr @hKey.if eax == ERROR_SUCCESSinvoke RegQueryValueEx,@hKey,_lpszValueName,NULL,_lpdwType,\_lpszValue,_lpdwSizemov @dwReturn,eaxinvoke RegCloseKey,@hKey.endifmov eax,@dwReturnret_RegQueryValue endp; 设置键值
_RegSetValue proc _lpszKey,_lpszValueName,_lpszValue,_dwValueType,_dwSizelocal @hKeyinvoke RegCreateKey,HKEY_LOCAL_MACHINE,_lpszKey,addr @hKey.if eax == ERROR_SUCCESSinvoke RegSetValueEx,@hKey,_lpszValueName,NULL,\_dwValueType,_lpszValue,_dwSizeinvoke RegCloseKey,@hKey.endifret_RegSetValue endp; 创建子键
_RegCreateKey proc _lpszKey,_lpszSubKeyNamelocal @hKey,@hSubkey,@dwDispinvoke RegOpenKeyEx,HKEY_LOCAL_MACHINE,_lpszKey,NULL,\KEY_CREATE_SUB_KEY,addr @hKey.if eax == ERROR_SUCCESSinvoke RegCreateKeyEx,@hKey,_lpszSubKeyName,NULL,NULL,\NULL,NULL,NULL,addr @hSubkey,addr @dwDispinvoke RegCloseKey,@hKeyinvoke RegCloseKey,@hSubkey.endifret_RegCreateKey endp; 删除键值
_RegDelValue proc _lpszKey,_lpszValueNamelocal @hKeyinvoke RegOpenKeyEx,HKEY_LOCAL_MACHINE,_lpszKey,NULL,\KEY_WRITE,addr @hKey.if eax == ERROR_SUCCESSinvoke RegDeleteValue,@hKey,_lpszValueNameinvoke RegCloseKey,@hKey.endifret_RegDelValue endp; 删除子键
_RegDelSubKey proc _lpszKey,_lpszSubKeyNamelocal @hKeyinvoke RegOpenKeyEx,HKEY_LOCAL_MACHINE,_lpszKey,NULL,\KEY_WRITE,addr @hKey.if eax == ERROR_SUCCESSinvoke RegDeleteKey,@hKey,_lpszSubKeyNameinvoke RegCloseKey,@hKey.endifret_RegDelSubKey endp
1.打开和关闭子键
注册表函数对注册表的操作是通过句柄来完成的,与文件操作一样,在对某个键下的子键或者键值项进行操作之前,需要先将这个键打开,然后使用键句柄来引用这个键,在操作完毕以后再将键句柄关闭。注册表的根键不需要打开,它们的句柄是固定不变的,要使用根键的时候只要把这些句柄直接拿来用就是了,Windows.inc中已经预定义了它们的数值:
HKEY_CLASSES_ROOT equ 80000000hHKEY_CURRENT_USER equ 80000001hHKEY_LOCAL_MACHINE equ 80000002hHKEY_USERS equ 80000003hHKEY_PERFORMANCE_DATA equ 80000004hHKEY_CURRENT_CONFIG equ 80000005hHKEY_DYN_DATA equ 80000006h
在程序中可以随时将这些助记符当做句柄来引用对应的根键。在程序结束的时候,不需要关闭这些根键句柄。
打开子键使用RegOpenKeyEx函数,在Win16中还存在一个RegOpenKey函数,虽然在Win32中这个函数仍然存在,但这仅是为了兼容的目的而设置的。API手册中推荐使用RegOpenKeyEx函数:
invoke RegOpenKeyEx,hKey,lpSubKey,dwOptions,samDesired,phkResult
函数的hKey参数指定父键句柄,lpSubKey指向一个字符串,用来表示要打开的子键名称,在系统中一个子键的全称是以“根键\第1层子键\第2层子键\第n层子键”类型的字符串表示的,中间用“\”隔开,字符串的最后以0字符结束,这和目录名的表示方法是很像的。
既然子键的全称是这样表示的,那么要打开一个子键的时候,下面的两种表示方法有什么不同呢?
(1)父键=HKEY_LOCAL_MACHINE,子键=Software\RegTest\MySubkey
(2)父键=HKEY_LOCAL_MACHINE\Software,子键=RegTest\MySubkey
答案是:这两种表示方法是完全相同的。在使用RegOpenKeyEx函数打开子键的时候,既可以将hKey参数设置为HKEY_LOCAL_MACHINE根键的句柄,并将lpSubKey参数指向“Software\RegTest\MySubkey”字符串;也可以将hKey参数设置为“HKEY_LOCAL_MACHINE\Software”的句柄,将lpSubKey参数指向“RegTest\MySubkey”字符串,得到的结果是一样的。但是,使用第一种方法时,hKey参数可以直接使用助记符HKEY_LOCAL_MACHINE来表示,因为根键的句柄是固定的,不需要打开;而使用第二种方法时,还需要先打开“HKEY_LOCAL_MACHINE\Software”键来获取它的句柄,所以具体使用哪种方法还要根据具体情况灵活选用。
函数的其他几个参数的含义如下。
● dwOptions参数——系统保留参数,必须指定为0。
● samDesired参数——子键的打开方式,根据使用子键的方式,可以设置为下列取值的组合,只有指定了打开的方式,才能在打开子键后进行相应的操作:
■ KEY_ALL_ACCESS——允许所有的存取。
■ KEY_CREATE_LINK——允许建立符号列表。
■ KEY_CREATE_SUB_KEY——允许建立下一层子键。
■ KEY_ENUMERATE_SUB_KEYS——允许枚举下一层子键。
■ KEY_EXECUTE——允许读操作。
■ KEY_QUERY_VALUE——允许查询键值数据。
■ KEY_READ—KEY_QUERY_VALUE,KEY_ENUMERATE_SUB_KEYS和KEY_NOTIFY的组合。
■ KEY_SET_VALUE——允许修改或创建键值数据。
■ KEY_WRITE——KEY_SET_VALUE和KEY_CREATE_SUB_KEY的组合。
● phkResult参数——指向一个双字变量,函数在这里返回打开的子键句柄。
如果函数执行成功,返回值是ERROR_SUCCESS,并且函数在phkResult参数指向的变量中返回子键句柄。
当不再需要继续使用键句柄的时候,可以使用RegCloseKey函数将它关闭:
invoke RegCloseKey, hKey
如果句柄被成功关闭,函数返回ERROR_SUCCESS。
2.创建和删除子键
创建一个子键可以使用RegCreateKeyEx函数:
invoke RegCreateKeyEx,hKey,lpSubKey,Reserved,lpClass,dwOptions,\samDesired,lpSecurityAttributes,phkResult,lpdwDisposition
函数中与RegOpenKeyEx函数中同名参数的含义和用法是相同的,hKey也是用来指定父键句柄,lpSubKey指向要创建的子键名称字符串,samDesired参数指明子键建立后的操作方式,phkResult指向用来返回键句柄的双字变量。
其余一些参数的含义如下:
● Reserved参数——保留参数,必须设置为0。
● lpClass参数——为创建的子键定义一个类名,这个参数一般设置为NULL。
● dwOptions参数——创建子键时的选项,它可以是以下取值之一:
■ REG_OPTION_NON_VOLATILE——默认值,子键被创建到注册表文件中。
■ REG_OPTION_VOLATILE——创建易失性的子键,子键被保存在内存中,当系统重新启动的时候,子键消失。这个选项仅对Windows NT系统有效,在9x系统中被忽略。
● lpSecurityAttributes参数——指向一个SECURITY_ATTRIBUTES结构,用来指定键句柄的继承性,如果句柄不需要被继承,可以使用NULL。
● lpdwDisposition参数——这个参数一般使用NULL。
当需要创建的子键已经存在的时候,函数仅起到RegOpenKeyEx函数的作用;如果子键不存在,那么函数将创建子键。如果函数执行成功,返回值是ERROR_SUCCESS。
如果要创建“HKEY_LOCAL_MACHINE\Key1\Key2\Key3”子键,既可以将hKey参数设置为HKEY_LOCAL_MACHINE,将lpSubKey参数指向“Key1\Key2\Key3”字符串;也可以先打开“HKEY_LOCAL_MACHINE\Key1”键,将hKey设置为打开的键句柄,然后将lpSubKey参数指向“Key2\Key3”字符串,这与RegOpenKeyEx函数中的用法是类似的。在第二种用法中,打开父键的时候注意要指定KEY_CREATE_SUB_KEY方式。
当被创建子键的上层键不存在的时候,函数连同上层的子键一起创建。如上面的例子中,假如Key2也不存在,那么函数先在“HKEY_LOCAL_MACHINE\Key1”下创建Key2,然后在Key2下继续创建Key3。
_Reg.asm文件中的大部分子程序首先用RegOpenKeyEx函数打开子键,以便进行下一步操作,但是保存键值用的_RegSetValue子程序中使用的是RegCreateKeyEx函数,这样当子键已经存在的时候,函数仅打开它,如果子键不存在的话则创建子键。
删除子键使用RegDeleteKey函数:
invoke RegDeleteKey,hKey,lpSubKey
hKey参数为父键句柄,lpSubKey参数指向要删除的子键名称字符串。函数仅删除最后一 层 子 键,以 及 下 面 的 全 部 键 值 项。比 如,在“HKEY_LOCAL_MACHINE\Key1\Key2\Key3”子键存在的情况下,当hKey指定为HKEY_LOCAL_MACHINE,lpSubKey指向“Key1\Key2\Key3”的时候,函数仅删除Key3子键,不会连同Key2,Key1全部删除。但如果Key3子键下有键值项的话,这些键值项会被一起删除。
如果要删除的子键下还存在下一层子键,比如,上例中的Key3子键下还存在Key4子键,那么对Key3子键进行删除时,Windows 9x和Windows NT系统的做法是不同的:在Windows 9x中,Key3子键本身、Key3子键下所有的键值项和下层子键(包括上面举例的Key4)会被全部删除;而在Windows NT中,只有在不存在下层子键的情况下删除才能成功,如果Key3子键下还存在Key4子键,那么对Key3子键的删除是不会成功的。
应用程序不能直接在HKEY_LOCAL_MACHINE根键下面创建和删除子键,只能在下一层由系统定义的子键下进行操作,如果要保存配置信息的话,用户应用程序一般在HKEY_LOCAL_MACHINE\SOFTWARE子键下再创建自己的子键,然后将键值项保存在自己的子键中。
15.3.3 管理键值
配置信息是存放在键值项中的,打开或者创建一个键的最终目的都是为了在键下面存取键值项,这就像磁盘上的目录是用来合理组织和管理文件用的,数据还是存放在文件中的。当使用打开或者创建键的函数得到键句柄后,就可以通过它来存取键值项了。
1.设置键值项
在一个键下面设置和创建键值项使用RegSetValueEx函数:
invoke RegSetValueEx,hKey,lpValueName,Reserved,dwType,lpData,cbData
● hKey参数指定一个键句柄,键值项将保存在这个键下,lpValueName参数指向定义键值项名称的字符串。假如lpValueName参数指向一个空串或者设置为NULL,并且设置的键值类型是REG_SZ的话,那么函数设置的是键的默认值(图15.2中所示的“默认”项)。
● Reserved参数是保留的,必须设置为0。
● dwType参数指出了要设置的键值数据的类型,可以使用的类型如表15.1所示。
● lpData参数是一个指针,指向包含键值数据的缓冲区,cbData参数为要保存的数据长度。缓冲区中的数据格式,以及cbData参数指定的数据长度需要和dwType参数指出的键值类型相对应,比如,要设置REG_SZ类型的键值项,就要将cbData参数设置为字符串的长度+1(加上尾部的0);同样对于REG_MULTI_SZ类型的键值项来说,最后的两个0的长度都必须包括到cbData参数中;对于REG_DWORD类型的键值项,需要将双字数据放在缓冲区中并将cbData参数设置为4(不像其他函数一样当参数是双字的时候一般将双字在参数中直接传递)。
当子键中的键值项不存在的时候,函数新建键值项;当键值项已经存在的时候,函数将新的键值数据写入。如果键值数据保存成功,函数返回ERROR_SUCCESS。
虽然键值数据的最大长度没有规定,其大小仅受限于可用的内存大小,应用程序甚至可以使用REG_BINARY格式的键值项将整个文件都保存到注册表中,但在实际的使用中还是建议不要将大于2 KB的数据放到注册表中,因为这将影响注册表的使用效率。
要在一个键中创建或修改键值项,键的打开方式中必须包括KEY_SET_VALUE方式。
2.查询键值数据
读取键值项中的数据或者查询键值项的属性使用RegQueryValueEx函数,用法如下:
invoke RegQueryValueEx,hKey,lpValueName,lpReserved,\lpType,lpData,lpcbData
参数hKey和lpValueName用来指定要读取的键值项所处的子键句柄和键值项的名称,lpReserved参数是保留参数,必须使用0。lpData参数指向一个缓冲区,用来接收返回的键值数据。
函数的其余几个参数使用时必须注意的是它们都是指向双字变量的指针,这一点和使用RegSetValueEx函数时是不同的:
● lpType参数——函数在这个参数指向的双字变量中返回读取的键值类型,如果不需要返回键值项的类型,可以将这个参数设置为NULL。
● lpcbData参数——在调用的时候,程序必须在这个参数指向的双字变量中放置缓冲区的长度(并不是直接用lpcbData参数指出缓冲区长度)。当函数返回的时候,双字变量被函数改为返回到缓冲区中的数据的实际长度。
当函数执行成功的时候,函数的返回值是ERROR_SUCCESS。当程序指定的缓冲区长度不足以容纳返回的数据的时候,函数的返回值是ERROR_MORE_DATA,这时lpcbData参数指向的双字变量中返回需要的长度。
如果仅需要查询键值长度而不需要返回实际的数据,可以将lpData参数设置为NULL,但是lpcbData参数不能为NULL,这时函数会在lpcbData参数指向的双字变量中返回键值数据的长度。如果仅想查询键值项的类型,也可以同时将lpcbData和lpData参数设置为NULL。在这些情况下如果函数查询成功,返回值也是ERROR_SUCCESS。
如果要在一个键中查询键值数据的话,键的打开方式中必须包括KEY_QUERY_VALUE方式。
3.删除键值项
删除一个键值项的操作则比较简单,使用RegDeleteValue函数就可以了:
invoke RegDeleteValue,hKey,lpValueName
hKey参数和lpValueName指定父键句柄和被删除键值项的名称。唯一需要注意的是父键句柄的打开方式必须包括KEY_SET_VALUE。如果键值项被成功删除,则函数返回ERROR_SUCCESS。
15.3.4 子键和键值的枚举
在实际的应用中往往需要对一个键下的子键或者键值项进行列表操作,就像在DOS系统下常用Dir命令一样,这就要用到子键和键值的枚举函数。在注册表函数中,枚举子键和枚举键值项使用的函数是不一样的,不像FindFirstFile等文件列表函数那样将文件连同子目录混在一起列出来。下面分别介绍这两种函数。
1.枚举子键
例子程序中枚举子键和键值项的操作是在_EnumKey子程序中完成的,读者可以参考一下相应的代码,在这个子程序中,程序首先使用RegEnumKeyEx函数来枚举子键:
invoke RegEnumKeyEx,hKey,dwIndex,lpName,lpcbName,lpReserved,\lpClass,lpcbClass,lpftLastWriteTime
● hKey参数指定被枚举的键句柄,dwIndex参数指定需要返回信息的子键索引编号,lpName指向一个缓冲区,函数在这里返回子键名称,lpClass指向用于返回子键类名的缓冲区,lpftLastWriteTime指向一个FILETIME结构,函数在这里返回子键上一次被写入的时间。lpReserved参数是保留参数,必须设置为0。
要注意的是:lpcbName和lpcbClass指向两个双字变量,调用函数前,这两个双字变量中必须放入lpName和lpClass指定的缓冲区的长度,当函数返回的时候,函数在里面返回实际返回到缓冲区中的字符串的长度。如果函数执行成功,返回值是ERROR_SUCCESS。
因为RegEnumKeyEx函数每次返回一个子键的名称信息,所以要枚举全部子键的话,必须用循环多次调用这个函数,并且每次将dwIndex参数指定的子键索引号递增,当子键全部被枚举后,继续调用函数将得到一个ERROR_NO_MORE_ITEMS返回值,这时就可以结束循环了。下面是循环的典型写法:
.data
dwIndex dd ?
dwSize dd ?
szBuffer db 256 dup (?)
.code... ...mov dwIndex,0.while TRUEmov dwSize,sizeof szBufferinvoke RegEnumKeyEx,hKey,dwIndex,addr szBuffer,\addr dwSize,NULL,NULL,NULL,NULL.break .if eax == ERROR_NO_MORE_ITEMS;处理获取的子键inc dwIndex.endw
在循环开始前,程序初始化当做索引用的dwIndex变量,每次调用RegEnumKeyEx后将索引加1,当检测到函数的返回值是ERROR_NO_MORE_ITEMS的时候,使用 .break语句退出循环。程序不使用 .break .if eax != ERROR_SUCCESS语句当做结束循环的条件是因为:当出现缓冲区不够长等意外情况时,函数的调用可能失败,但是这时子键可能还没有全部被枚举,所以只有判断返回值是ERROR_NO_MORE_ITEMS才能保证全部子键被枚举。
每次调用函数之前,程序必须重新将dwSize变量的值设置为szBuffer缓冲区的大小,这一点非常重要,笔者有很多次听朋友说他的程序只能枚举一部分子键,最后查出的原因就是忘了这一步。这是因为每次函数返回时,dwSize中会变成返回的子键名称字符串的长度,如果不重新设置,下一次调用时函数就会将这个长度认为是缓存区的长度。随着循环的进行,这个值会变得越来越小。
当进行枚举子键操作时,父键的打开方式中必须包括KEY_ENUMERATE_SUB_KEYS方式(KEY_READ方式中已经包括KEY_ENUMERATE_SUB_KEYS)。
2.枚举键值
RegEnumKeyEx函数仅枚举一个键下面的全部子键,对键下面的键值项则不会去理会。如果要枚举一个键下面的键值项,那么必须使用RegEnumValue函数:
invoke RegEnumValue,hKey,dwIndex,lpValueName,lpcbValueName,\lpReserved,lpType,lpData,lpcbData
函数的hKey,dwIndex和lpReserved参数的使用同RegEnumKeyEx函数中的同名参数。其余的一些参数中,lpValueName和lpData参数指向两个缓存区,函数在里面分别返回键值项的名称和数据。lpcbValueName和lpcbData参数指向两个双字变量,调用函数前里面必须放入键值项名称缓冲区和键值数据缓冲区的长度,函数返回后这两个变量的值被改为返回到缓冲区中的数据长度。lpType参数则指向一个用于返回键值数据类型的双字变量。
如果不需要返回键值数据,lpData和lpcbData参数可以设置为NULL,如果不需要返回键值数据类型,lpType参数也可以设置为NULL。
要进行枚举键值项的操作,父键的打开方式中必须包括KEY_QUERY_VALUE方式。
下面是一段典型的用于枚举键值项的循环代码:
.data
dwIndex dd ?
dwType dd ?
dwNameSize dd ?
szName db 256 dup (?)
dwDataSize dd ?
szData db 256 dup (?)
.code...mov dwIndex,0.while TRUEmov dwNameSize,sizeof szNamemov dwDataSize,sizeof szDatainvoke RegEnumValue,hKey,dwIndex,addr szName,\addr dwNameSize,NULL,addr dwType,\addr szData,addr dwDataSize.break .if eax == ERROR_NO_MORE_ITEMS;处理获取的键值项inc dwIndex.endw
这个循环的结构和使用RegEnumKeyEx函数的循环是大同小异的。同样要注意的是,在循环中每次要重新设置dwNameSize和dwDataSize变量的值,因为它们在RegEnumValue函数执行后也会被改写。
3.查询键属性
在枚举子键和键值项的时候往往会遇到这样一个问题:注册表函数对键值数据的长度并没有限制,在预留缓冲区的时候如果申请太大的内存比较浪费,申请太小的内存则无法枚举成功,对于返回的子键名称和键值项名称也是如此。那么,究竟该留多大的缓冲区呢?其实在枚举之前可以先用RegQueryInfoKey函数查看一下键的统计信息。
RegQueryInfoKey函数返回的信息有:一个键下面子键的数量、键值项的数量、子键名称和键值名称字符串的最大长度及键值数据的最大长度等。根据这些信息,就能方便地申请足够大的缓冲区来保证枚举成功。函数还能返回创建子键时指定的类名和最后一次写入子键的时间等信息。
RegQueryInfoKey函数的用法如下:
invoke RegQueryInfoKey,hKey,lpClass,lpcbClass,lpReserved,\lpcSubKeys,lpcbMaxSubKeyLen,lpcbMaxClassLen,\lpcValues,lpcbMaxValueNameLen,lpcbMaxValueLen,\lpcbSecurityDescriptor,lpftLastWriteTime
函数的参数比较多,但并不复杂,各参数的含义是:
● hKey — 指 定 要 获 取 信 息 的 键 句 柄,键 的 打 开 方 式 中 必 须 包 括KEY_QUERY_VALUE。
● lpClass——指向一个缓冲区,用来返回创建键时指定的Class字符串。
● lpcbClass——指向一个双字变量,调用函数时变量中必须放入lpClass指定的缓冲区的长度,函数返回时在这里放入返回到缓冲区中的字符串长度。
● lpReserved——保留参数,必须设置为0。
● lpcSubKeys——指向一个双字,用来返回键中的子键数量。
● lpcbMaxSubKeyLen——指向一个双字,用来返回所有子键中最长的名称字符串长度,返回的长度不包括字符串结尾的0字符。
● lpcbMaxClassLen——指向一个双字,用来返回所有子键中最长的Class字符串长度,返回的长度不包括字符串结尾的0字符。
● lpcValues——指向一个双字,用来返回键下面的键值项数量。
● lpcbMaxValueNameLen——指向一个双字,用来返回所有键值项中最长的名称字符串长度,返回的长度不包括字符串结尾的0字符。
● lpcbMaxValueLen——指向一个双字,用来返回所有键值数据的最大长度。
● lpcbSecurityDescriptor——指向一个双字,用来返回安全描述符的长度。
● lpftLastWriteTime——指向一个FILETIME结构,用来返回最后一次修改键的时间。
可以看到,除hKey外其他的参数都是指针,指向用来返回数据的变量或结构,如果不需要返回某种信息的话,可以将对应的指针参数设置为NULL。另外,所有返回的最长名称字符串长度中都不包括结尾的0字符。
除了前面介绍的函数外,系统中还存在一些不常用的注册表函数,比如,可以用RegLoadKey和RegReplaceKey函数从指定的文件中恢复注册表的子键信息,也可以通过RegSaveKey函数将键信息保存到指定的文件中。另外,可以通过RegConnectRegistry等函数操作远程注册表。对于这些函数,本节不再详细介绍。
15.3.5 注册表应用举例
曾经有一段时间,注册表修改和优化的工具层出不穷,如魔法兔子、侠客系统修改器和Windows优化大师等都是这方面的典型软件。看了前面的注册表函数后,读者现在一定知道,编写这些软件的大部分时间是花在界面设计和收集注册表的说明资料上,在这些工作的背后,使用的就是这么几个注册表函数。
本节用几个简单的例子来演示一些涉及注册表操作的常见应用,读者在收集到足够的注册表说明资料后,参考这些例子就可以写出类似的应用程序来。
1.设置开机自动运行
Windows在启动并执行登录操作后,会将HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run子键下的所有键值项枚举一遍,并将所有REG_SZ类型的键值数据字符串当做一个文件名自动执行,所以在这个子键下设置一个键值项,让它的键值数据是某个文件名字符串(也可以是数据文件名,Windows会自动打开关联的可执行文件),就可以让这个文件在Windows启动后自动运行。
由于Windows只关心键值数据,并不关心键值名称,所以在设置的时候只要保证键值名称是唯一的就可以了。下面的_SetAutoRun子程序就可以用来将程序设置为自动运行:
.const
szKeyAutoRun db 'Software\Microsoft\Windows\CurrentVersion\Run',0
szValueAutoRun db 'AutoRun Test',0 ;在不同程序中使用时修改此字符串!.code... ...
_SetAutoRun proc _dwFlaglocal @szFileName[MAX_PATH]:byte.if _dwFlaginvoke GetModuleFileName,NULL,addr @szFileName,MAX_PATHinc eaxinvoke _RegSetValue,addr szKeyAutoRun,addr szValueAutoRun,\addr @szFileName,REG_SZ,eax.elseinvoke _RegDelValue,addr szKeyAutoRun,addr szValueAutoRun.endifret
_SetAutoRun endp
当用TRUE参数调用_SetAutoRun子程序时,程序将被设置为自动运行,这时子程序先用GetModuleFileName函数获取当前执行文件的文件名,然后用.const段中定义的szValueAutoRun字符串当做键值项的名称,用文件名当做键值数据在Run键下设置一个键值项。当用FALSE参数调用的时候,程序将取消自动运行,这时子程序仅简单地将前面设置的键值项删除而已。
代码中用到的几个子程序在15.3.2小节中列出的_Reg.asm文件中。
2.设置文件关联
如果将一个数据文件与一个可执行文件关联,那么就可以通过双击数据文件来直接执行可执行文件,比如,双击以txt为扩展名的文本文件,系统就会自动执行Notepad.exe文件来编辑它,这就是因为txt文件是与Notepad.exe文件关联的。
文件关联是在注册表的HKEY_CLASSES_ROOT根键中设置的。要为某种扩展名设置关联,需要在HKEY_CLASSES_ROOT根键下设置两个子键,第一个子键的名称是“.扩展名”(和数据文件的扩展名相对应),这个子键需要设置默认值,默认值的数据为HKEY_CLASSES_ROOT根键下另一个子键的名称,在这个子键下可以继续设置与这种数据文件关联的可执行文件名。
如果关联的操作方式是“打开”,那么在第二个子键中,可以继续创建“shell\open\command”子键,并把command子键的默认值设为可执行文件名,这样双击数据文件,就会执行这个可执行文件;如果关联的操作方式是“打印”,那么可以在第二个子键中继续创建“shell\print\command”子键,同样将command子键的默认值设为执行打印操作的可执行文件名。当然,也可以只设置“open”操作的关联。经过这两个步骤,文件关联就设置好了。
以“*.test”数据文件为例,要在HKEY_CLASSES_ROOT根键下创建一个“.test”子键,把它的默认值设置为“testfile”,然后再创建一个“testfile\shell\open\command”子键,并把command键的默认值设置为指定的可执行文件名。
下面这段代码就是将“*.test”数据文件与当前可执行文件关联的子程序,调用_SetExt子程序就可以完成这个功能:
.const
szKeyEnter db 'testfile',0
szKeyExt1 db '.test',0
szKeyExt2 db 'testfile\shell\open\command',0
szParam db ' "%1"',0.data... ..._SetExt proclocal @hKeylocal @szFileName[MAX_PATH]:byteinvoke RegCreateKey,HKEY_CLASSES_ROOT,addr szKeyExt1,addr @hKey.if eax == ERROR_SUCCESSinvoke RegSetValueEx,@hKey,NULL,NULL,\REG_SZ,addr szKeyEnter,sizeof szKeyEnterinvoke RegCloseKey,@hKey.endifinvoke RegCreateKey, HKEY_CLASSES_ROOT, addr szKeyExt2, addr @hKey.if eax == ERROR_SUCCESSinvoke GetModuleFileName,NULL,addr @szFileName,MAX_PATHinvoke lstrcat,addr @szFileName,addr szParaminvoke lstrlen,addr @szFileNameinc eaxinvoke RegSetValueEx,@hKey,NULL,NULL,\REG_EXPAND_SZ,addr @szFileName,eaxinvoke RegCloseKey,@hKey.endifret
_SetExt endp
子程序的第一部分创建了“.test”子键,第二部分创建了“testfile\shell\open\command”子键,注意在可执行文件名字符串后要加上“%1”字符串,这样系统才会将数据文件的文件名放在命令行参数中传给可执行文件。由于系统必须将“%1”字符串替换成数据文件名,所以command子键默认值的数据类型被定义为REG_EXPAND_SZ类型。
由于在两次RegSetValueEx函数的调用中设置的都是子键的默认值,所以函数的lpValueName参数都被指定为NULL。
在所附光盘的Chapter15\Associate目录中有一个将“*.test”文件与Associate.exe文件关联的例子,这个例子是在第13章中的Cmdline例子中添加_SetExt子程序代码形成的。读者可以仔细分析源程序并执行一下,执行文件以后再双击目录中的Hello.test文件,就会发现Associate.exe文件被执行了,而且文件显示的第二个参数为“Hello.test”,这表示对“*.test”文件的关联生效了!
要撤销关联,只要进行逆向操作—将“.test”子键和“testfile\shell\open\command”子键删除即可。
【完整代码笔记】
; Associate.asm
; 设置注册表将 *.test 文件管理到本程序
; 程序从 Cmdline.asm 修改而来
; -------------------------------------------------------------------
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff Associate.asm
; Link /subsystem:windows Associate.obj
.386
.model flat,stdcall
option casemap:none ; include 文件定义
include c:/masm32/include/windows.inc
include c:/masm32/include/user32.inc
include c:/masm32/include/kernel32.inc
include c:/masm32/include/Advapi32.inc
includelib c:/masm32/lib/user32.lib
includelib c:/masm32/lib/kernel32.lib
includelib c:/masm32/lib/Advapi32.lib ; 数据段
.data?
szBuffer1 byte 4096 dup(?)
szBuffer2 byte 4096 dup(?)
szOutput byte 8192 dup(?).const
szCaption byte '命令行参数',0
szFormat1 byte '*.test 文件的关联被设置到本程序',0dh,0ahbyte '请双击目录中的Hello.test文件进行测试! 并注意下面的参数[1]',0dh,0ah,0ahbyte '可执行文件名称:',0dh,0ah,'%s',0dh,0ah,0ahbyte '参数总数:%d',0dh,0ah,0
szFormat2 byte '参数[%d]:%s',0dh,0ah,0
szKeyEnter byte 'testfile',0
szKeyExt1 byte '.test',0
szKeyExt2 byte 'testfile\shell\open\command',0
szParam byte ' "%1"',0; 代码段
.code include _Cmdline.asm_SetExt proc local @hKey local @szFileName[MAX_PATH]:byte invoke RegCreateKey, HKEY_CLASSES_ROOT, addr szKeyExt1, addr @hKey .if eax == ERROR_SUCCESS invoke RegSetValueEx, @hKey, NULL, NULL, \REG_SZ, addr szKeyEnter, sizeof szKeyEnter invoke RegCloseKey, @hKey .endif invoke RegCreateKey, HKEY_CLASSES_ROOT, addr szKeyExt2, addr @hKey .if eax == ERROR_SUCCESS invoke GetModuleFileName, NULL, addr @szFileName, MAX_PATH invoke lstrcat, addr @szFileName, addr szParam invoke lstrlen, addr @szFileName inc eax invoke RegSetValueEx, @hKey, NULL, NULL, \REG_EXPAND_SZ, addr @szFileName, eax invoke RegCloseKey, @hKey .endif ret
_SetExt endp main proc invoke _SetExt invoke GetModuleFileName, NULL, offset szBuffer1, sizeof szBuffer1 invoke _argc mov ebx, eax invoke wsprintf, addr szOutput, addr szFormat1, addr szBuffer1, eax xor esi, esi .while esi < ebx invoke _argv, esi, addr szBuffer2, sizeof szBuffer2 invoke wsprintf, addr szBuffer1, addr szFormat2, esi, addr szBuffer2 invoke lstrcat, addr szOutput, addr szBuffer1 inc esi .endw invoke MessageBox, NULL, addr szOutput, addr szCaption, MB_OK invoke ExitProcess, 0
main endp
end main
命令行参数分析的通用子程序
; _CmdLine.asm
; 命令行参数分析的通用子程序
; 功能:
; _argc ---> 对命令行参数进行数量统计
; _argv ---> 取某个命令行参数
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
CHAR_BLANK equ 20h ;定义空格
CHAR_DELI equ '"' ;定义分隔符
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 取命令行参数个数 (arg count)
; 参数个数必定大于等于 1, 参数 1 为当前执行文件名
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_argc proclocal @dwArgcpushadmov @dwArgc,0invoke GetCommandLinemov esi,eaxcld
_argc_loop:
;********************************************************************
; 忽略参数之间的空格
;********************************************************************lodsbor al,aljz _argc_endcmp al,CHAR_BLANKjz _argc_loop
;********************************************************************
; 一个参数开始
;********************************************************************dec esiinc @dwArgc
_argc_loop1:lodsbor al,aljz _argc_endcmp al,CHAR_BLANKjz _argc_loop ;参数结束cmp al,CHAR_DELIjnz _argc_loop1 ;继续处理参数内容
;********************************************************************
; 如果一个参数中的一部分有空格,则用 " " 包括
;********************************************************************@@:lodsbor al,aljz _argc_endcmp al,CHAR_DELIjnz @Bjmp _argc_loop1
_argc_end:popadmov eax,@dwArgcret_argc endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 取指定位置的命令行参数
; argv 0 = 执行文件名
; argv 1 = 参数1 ...
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_argv proc _dwArgv,_lpReturn,_dwSizelocal @dwArgv,@dwFlagpushadinc _dwArgvmov @dwArgv,0mov edi,_lpReturninvoke GetCommandLinemov esi,eaxcld
_argv_loop:
;********************************************************************
; 忽略参数之间的空格
;********************************************************************lodsbor al,aljz _argv_endcmp al,CHAR_BLANKjz _argv_loop
;********************************************************************
; 一个参数开始
; 如果和要求的参数符合,则开始复制到返回缓冲区
;********************************************************************dec esiinc @dwArgvmov @dwFlag,FALSEmov eax,_dwArgvcmp eax,@dwArgvjnz @Fmov @dwFlag,TRUE@@:
_argv_loop1:lodsbor al,aljz _argv_endcmp al,CHAR_BLANKjz _argv_loop ;参数结束cmp al,CHAR_DELIjz _argv_loop2cmp _dwSize,1jle @Fcmp @dwFlag,TRUEjne @Fstosbdec _dwSize@@:jmp _argv_loop1 ;继续处理参数内容_argv_loop2:lodsbor al,aljz _argv_endcmp al,CHAR_DELIjz _argv_loop1cmp _dwSize,1jle @Fcmp @dwFlag,TRUEjne @Fstosbdec _dwSize@@:jmp _argv_loop2
_argv_end:xor al,alstosbpopadret_argv endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
运行结果: