使用 .net 框架类替代 api 调用
升级到 microsoft .net
ken getz
mcw technologies
2002 年 2 月
摘要:通过学习 microsoft .net 框架中某些特定而有用的类,可以减少您对 win32 api 调用的依赖。本文讨论的每个类都可以代替一个或多个 win32 api 调用,而在 microsoft visual basic 6.0 中,您必须调用一个或多个 win32 api 才能完成相同的任务。
目标
- 查找现有 win32 api 调用的特定替代品。
- 了解 registry 类。
- 使用 fileversioninfo 类。
- 使用环境信息和系统信息。
目录
避免使用 win32 api
如果您是一位 microsoft visual basic® 6.0 开发人员,您就无法避免调用 win32 api。开发人员有太多的任务需要完成,而 visual basic 却不能提供任何实现方法。例如,在 visual basic 6.0 中,您很难完成以下任务:
- 确定文件版本信息。
- 在注册表的任何位置进行读取和写入操作。
- 确定用户的特定文件夹,例如 microsoft windows® 收藏夹或个人文件夹。
- 检索所有可用驱动器的列表。
- 查找用户的登录名或计算机名。
- 检索所有打开窗口的列表。
如果仅使用 visual basic 6.0 中提供的工具,您不可能解决上述任何问题。对于每个问题,开发人员都需要使用 windows api。许多开发人员使用 windows api 已经找到了完成这些(以及许多其他)任务的方法。
windows api 存在什么问题?
为什么不继续在 .net 环境中使用 windows api 呢?如果使用 .net 平台调用服务(称为“p/invoke”),您当然可以这样做。从 visual basic 开发人员的角度来说,调用 windows api 并不比使用他们所熟悉的 declare 语句困难。不过,在 .net 环境中使用 windows api 存在一些比较严重的缺陷,您可能需要考虑采取任何可行的措施来避免这些问题。例如:
- .net 公共语言运行时不会受平台影响。当您使用 windows api 调用时,您将代码绑定到编写代码的特定平台上(即,相对于其他操作系统的某个特定 windows 版本或 windows 本身)。必要时,您需要将代码转换到另一个平台上,而这样做就需要修改使用 api 调用的每行代码。
- 从 .net 中调用 windows api(或 dll 中的任何非托管代码)不像在 visual basic 6.0 中那样简单。例如,对结构的工作方式的限制使得很难将结构传递给 api 调用。此外,由于数据类型的更改以及更严格的类型转换,visual basic 6.0 的 api 声明也需要进行更改。
- 根据语言的不同,使用 windows api(以及通常情况下使用的外部代码)的技巧也不尽相同。如果您打算在多 .net 语言环境中工作,则需要掌握各种语言的不同技巧。
- 调用 windows api 的代码要求调用这些代码的用户具有执行此操作的权限。这将影响应用程序的安全保护方案,您需要对此要求提前做出安排。
这个问题很简单:尽管您可以在 visual basic .net 应用程序中继续使用 windows api,但通常情况下,您应当尽可能寻找由 .net 框架提供的替代品。虽然 .net 框架的目的并不是要阻止您直接使用 windows 的功能,但框架的确提供了大量的类,可以帮助您放弃对 windows api 调用的依赖。
如果能够给出一个完整列表,列出 win32 api 调用以及在 .net 框架中完成相同任务的相应方法(如果有),可能会很方便,不过本文不涉及此任务。在本文中,您将了解到一些由 .net 框架提供的特定且非常有用的类,它们可以解决您的问题。在每个示例中,本文所讨论的类都可以用来替代一个或多个 win32 api 调用,而在 microsoft visual basic 6.0 中,您必须调用一个或多个 win32 api 才能完成相同的任务。
使用注册表
如果您与大多数 visual basic 6.0 开发人员一样,您会发现 microsoft visual basic for applications (vba) 中内置的 savesetting、getsetting、getallsettings 和 deletesetting 方法有点儿用处,但却很可能被它们的局限性弄得精疲力尽。所有这些方法都只能在注册表的 hkey_current_user\software\vb 和 vba program settings 下的项中使用。如果您要在注册表的其他地方读取或写入注册表项或注册表值,则必须使用复杂的 api 调用,或依靠别人的代码来处理此问题。
.net 框架在 microsoft.win32 名称空间中提供了一对功能强大的类(registry 和 registrykey),从而简化了注册表的使用,即不再需要 api 调用!
作为演示,请在示例项目的主窗体上单击 work with the registry(使用注册表)按钮。此窗体提供了 software\microsoft\windows\currentversion\run 项的 hkey_local_machine 配置单元中所有注册表值的列表。您可以右键单击列表中的任何项,然后选择插入新项,或者编辑或删除选定项,如图 1 所示。
提示:示例窗体也已经过设计,在列表框中按下 enter 键时,可以编辑当前选定的项。按下 delete 键可以删除选定项,按下 insert 键可以添加一个新值。这些项对应于列表框的上下文菜单中的项。

图 1:使用 registry 和 registrykey 类轻松检索和修改 windows 注册表中的信息
.net 框架提供了两个非常有用的类,使您可以轻松使用 windows 注册表。第一个类是 registry,它提供的字段与标准 registry 配置单元的各字段相对应:
- classesroot (hkey_classes_root)
- currentconfig (hkey_current_config)
- currentuser (hkey_current_user)
- dyndata (hkey_dyn_data)
- localmachine (hkey_local_machine)
- performancedata (hkey_performance_data)
- users (hkey_users)
要使用 registry 类,只需检索所需配置单元的引用。示例窗体的 loadlist 过程中包含如下代码,以便使用注册表中的 hkey_local_machine 配置单元:
imports microsoft.win32
dim reg as registrykey = registry.localmachine
另一个类是 registrykey,它可以完成所有工作。它提供了一组使用 registry 的方法。表 1 列出了 registrykey 类的所有有用方法。
表 1:registrykey 类方法
| 方法 |
说明 |
| createsubkey |
创建新子项或打开现有子项 |
| deletesubkey |
删除指定子项。 |
| deletesubkeytree |
以递归方式删除子项和该子项的所有子项。 |
| deletevalue |
从该项中删除指定项值。 |
| getsubkeynames |
检索包含所有子项名称的字符串数组。 |
| getvalue |
检索指定的值。 |
| getvaluenames |
检索包含与此项相关联的所有项值名称的字符串数组。 |
| opensubkey |
检索指定子项,具有可选的写入权限。 |
| setvalue |
设置指定的值。 |
registrykey 类还提供以下三个属性:
- name:检索项的名称。
- subkeycount:检索与该项相关联的子项的数量。
- valuecount:检索与该项相关联的项值的数量。
示例窗体的 listload 过程将检索所请求项中的所有值,并将检索到的值添加到窗体的列表框中:
private const conregkey as string = _
"software\microsoft\windows\currentversion\run"
private structure regdata
public value as string
public data as string
public overrides _
function tostring() as string
return me.value
end function
end structure
private sub listload()
dim reg as registrykey = registry.localmachine
dim astrvalues() as string
dim strvalue as string
dim rd as regdata
' 清除列表框中的现有项。
lstitems.beginupdate()
lstitems.items.clear()
' 打开注册表项,然后使用
' 该项的值加载列表框。
reg = reg.opensubkey(conregkey)
astrvalues = reg.getvaluenames()
for each strvalue in astrvalues
rd.value = strvalue.tostring
rd.data = reg.getvalue(strvalue)
lstitems.items.add(rd)
next
lstitems.endupdate()
end sub
要编辑示例窗体中的值或添加新值,需要运行以下代码:
private sub addoredit( _
byval rd as regdata, _
byval mode as frmaddvalue.accessmode)
dim reg as registrykey = registry.localmachine
dim frm as new frmaddvalue(mode)
frm.keyname = rd.value
frm.keydata = rd.data
if frm.showdialog() = dialogresult.ok then
if frm.keyname <> string.empty then
reg = reg.opensubkey(conregkey, true)
reg.setvalue(frm.keyname, frm.keydata)
listload()
end if
end if
end sub
此代码将再次打开注册表项,这次将请求写入项值的权限(此请求由 opensubkey 的第二个参数发出)。然后,代码将调用 setvalue 方法,传递图 1 所示的对话框窗体中的项名和项值。为简化工作,可以使用 setvalue 方法添加新值或修改现有值。如果项值不存在,setvalue 方法将添加一个项值。
要删除项值,示例窗体将调用以下代码:
private sub deletekey(byval rd as regdata)
dim strtext as string
dim reg as registrykey = registry.localmachine
if lstitems.selectedindex = -1 then
exit sub
end if
' 删除选定的项。
strtext = string.format( _
"are you sure you want to delete ""{0}""?", _
rd.value)
if messagebox.show(strtext, _
"delete registry value", _
messageboxbuttons.yesno, _
messageboxicon.question) = dialogresult.yes then
' 打开项,允许写入。
reg = reg.opensubkey(conregkey, true)
reg.deletevalue(rd.value)
' 重新加载列表框。
listload()
end if
end sub
此代码将打开项并请求对其执行写入操作,然后将调用 deletevalue 方法删除选定的值。
有了示例窗体提供的信息和 .net 框架附带的文档,便可以轻松地完成与注册表相关的任何任务,而不必使用 windows api。这是一个简单的对象模型,但它提供的功能比 visual basic 6.0 开发人员先前所拥有的功能更强大。
提示:如果具有必要的权限,您还可以使用远程计算机上的注册表。您可以调用 registrykey.openremotebasekey 方法检索其他计算机上的基本项,而不是简单地使用其中一个 registry 类属性,来代表您自己计算机上的 registry 配置单元。
使用常用对话框
windows 提供了一组常用对话框,使开发人员可以方便地请求用户信息。您肯定见过并且使用过打开和保存文件、颜色、打印机和字体设置等常用对话框。visual basic 6.0 开发人员在使用这些对话框时有两种选择。他们可以:
- 使用一个 microsoft activex® 控件,该控件提供了一个包含常用对话框的简单对象模型,但是由于控件有多个不同的版本且底层 dll 不同,因此该控件存在严重的部署问题。(对于很多 visual basic 6.0 开发人员来说,这个问题是最致命的 dll 问题。)
- 直接使用 windows api 发送消息并提供回叫功能,以管理各个常用对话框。
两个解决方案都不是完美无缺的,而且由于对 .net 框架进行了增补,这两个解决方案当前都不是必需的。查看 system.windows.forms 名称空间时,您会发现 colordialog、filedialog、fontdialog 和 printdialog 类。这些类都集成在框架中(也就是说,既不需要使用 api 调用,也不需要使用 activex 控件),这样可以方便地将这些标准功能合并到您的应用程序中。
每个类都提供了一系列属性,您可以使用类的 showdialog 方法在显示对话框之前设置这些属性。本文将不对每个类进行详细讨论,但在示例项目中使用了 filedialog 类,使您可以选择文件。如果您在主窗体上选择了 file version info(文件版本信息)按钮,则可以使用演示窗体上的 select a file(选择文件)按钮显示 file open(打开文件)对话框,如图 2 所示。

图 2:使用 filedialog 类显示 windows 常用对话框
虽然 filedialog 类不像直接使用 windows api 那样灵活,但它提供了大量的属性,您可以使用这些属性控制对话框的操作。您可以决定文件的来源,选择一个或多个文件,还可以检索选定的文件名。表 2 列出了可能会用到的 filedialog 类的部分属性和方法。
表 2:filedialog 对象的属性和方法
| 属性/方法 |
说明 |
| addextension |
指示当用户省略扩展名时,对话框是否自动为文件添加一个扩展名。 |
| checkfileexists |
指示当用户指定的文件名不存在时,对话框是否显示警告信息。 |
| checkpathexists |
指示当用户指定的路径不存在时,对话框是否显示警告信息。 |
| defaultext |
默认文件扩展名。 |
| dereferencelinks |
指示对话框是否返回快捷方式所引用的文件的位置,或者是否返回快捷方式 (.lnk) 所在的位置。 |
| filename |
在文件对话框中选定的文件名。 |
| filenames |
(只读)对话框中所有选定文件的文件名。 |
| filter |
当前文件名筛选字符串,确定对话框的“save as file type”(另存为文件类型)或“files of type”(文件类型)框中显示的选项。 |
| filterindex |
文件对话框中当前选定的筛选器的索引。 |
| initialdirectory |
文件对话框显示的初始目录。 |
| restoredirectory |
指示对话框在关闭之前是否还原当前目录。 |
| showhelp |
指示文件对话框中是否显示 help(帮助)按钮。 |
| title |
文件对话框标题。 |
| validatenames |
指示对话框是否只接受有效文件名。 |
| reset (method) |
将所有属性重置为默认值。 |
| showdialog (method) |
显示文件对话框。如果用户按 ok(确定),将返回 dialogresult.ok,否则返回 dialogresult.cancel。 |
单击 select a file(选择文件)时,示例窗体 frmfileversioninfo 将调用以下代码:
private sub btnselectfile_click( _
byval sender as system.object, _
byval e as system.eventargs) _
handles btnselectfile.click
dim ofd as openfiledialog = new openfiledialog()
ofd.filter = _
"executable files (*.exe;*.dll;*.ocx)|" & _
"*.exe;*.dll;*.ocx|" & _
"drivers (*.sys;*.drv;*.fnt)|" & _
"*.sys;*.drv;*.fnt|" & _
"all files (*.*)|*.*"
ofd.filterindex = 1
ofd.showreadonly = false
ofd.restoredirectory = true
if ofd.showdialog() = dialogresult.ok then
if ofd.filename.length > 0 then
' 显示文件版本信息。
displayfileinfo(ofd.filename)
end if
end if
end sub
提示:如同使用 visual basic 6.0 commondialog activex 控件一样,您可以将 filedialog 类的 filter 属性设置为一个字符串,在其中包含一对以竖线分隔的值,如下所示:“description|filespec”。
在上面的示例中,一旦您选择了一个文件名,示例窗体就会在窗体的 listview 控件中显示有关该文件的信息。下一节将讨论 fileversioninfo 类,它可以实现上述操作。
检索文件版本信息
开发人员和编译人员可以将版本信息嵌入到可执行文件、dll 文件和驱动程序文件中。您可能需要检索部分或全部版本信息以用作应用程序的一部分,在 visual basic 6.0 中执行此操作需要大量的 api 调用。您需要调用 getversioninfosize、verqueryvalue 和 getfileversioninfo win32 api 函数,在 visual basic 中使用上述任何函数都不容易。
这种情况再一次显示出 .net 框架的强大:使用 fileversioninfo 对象使得这些工作变得非常简单。您只需调用 fileversioninfo 对象的共享 getversioninfo 方法,传递一个文件名,一切问题就都迎刃而解了。示例窗体使用了以下代码:
dim fvi as fileversioninfo = _
fileversioninfo.getversioninfo(strfile)
完成以上操作之后,检索 fileversioninfo 对象的属性就是一件非常简单的事情了。示例窗体使用下面所示的一小段程序,将每个属性名称和值都添加到窗体的 listview 控件中:
private sub additem( _
byval strproperty as string, _
byval strvalue as string)
with lvwinfo.items.add(strproperty)
.subitems.add(strvalue)
end with
end sub
真正起作用的是代码,而代码只需反复调用 additem,每个属性调用一次即可:
additem("comments", fvi.comments)
additem("companyname", fvi.companyname)
additem("language", fvi.language)
' 此处删除了一些行...
additem("product", fvi.productname)
additem("productprivatepart", _
fvi.productprivatepart.tostring())
additem("productversion", fvi.productversion)
additem("specialbuild", fvi.specialbuild)
图 3 所示的结果窗体显示了可通过编程使用的所有版本信息。

图 3:使用 fileversioninfo 类检索文件版本信息
检索环境信息
win32 api 提供了一系列函数,使您可以确定用户的环境设置。getsystemmetrics 和 systemparametersinfo 是其中的两个函数。您可以继续从 .net 应用程序中调用这些 api 函数,但很可能您并不需要这样做。environment 类(位于 system 名称空间)和 systeminformation 类(位于 system.windows.forms 名称空间)提供了许多与 api 函数相同的信息。在本节中,我们将通过示例来演示这两个类的功能。
警告:不要浪费时间从这些类中寻找设置用户环境设置的方法。此处显示的所有信息均为只读。如果您仍然希望修改环境设置,则需要寻找其他方法。
使用 system.environment 类
system.environment 类提供了若干不同的信息,如果没有这些信息,就需要进行多次 windows api 调用。使用 system.environment 可以检索:
- 有关可用驱动器的信息(getlogicaldrives 方法)
- windows 启动后的毫秒数(tickcount 属性)
- 一般环境设置(由 currentdirectory、machinename、osversion、systemdirectory、userdomainname、userinteractive、username 和 workingset 属性提供)
- 特定文件夹列表(使用 getfolderpath 方法提供)
如果您使用过 windows api,您将知道选择方法和属性将代替很多 api 调用,包括 gettickcount、getlogicaldrives、getsystemdirectory、getcomputername、getusername 以及 getversionex 等等。
在图 4(单击主窗体上的 environment info [环境信息])所示的示例窗体中,在靠近顶部的列表框中显示了执行 getlogicaldrives 方法的结果,其中包括所有特定文件夹的列表(使用 getfolderpath 方法检索)。在窗体下部的列表框中,显示了该类的许多属性的运行结果。

图 4:显示 system.environment 类所使用的属性和方法的窗体
另外,要测试 tickcount 属性,请单击 test clickcount(测试 clickcount),以显示使用示例窗体中定义的 stopwatch 类的结果,并请使用以下代码:
public class stopwatch
private mintstart as integer
public sub start()
mintstart = environment.tickcount
end sub
public function elapsed() as integer
return environment.tickcount - mintstart
end function
public overrides function tostring() as string
return string.format( _
"started: {0}, elapsed: {1}", _
mintstart, me.elapsed)
end function
end class
此处显示的 fillproperties 方法使用先前显示过的 additem 方法的一个副本,将属性名称和结果填入 listview 控件,如下所示:
private sub fillproperties()
additem("currentdirectory", _
environment.currentdirectory)
additem("machinename", environment.machinename)
additem("osversion.platform", _
environment.osversion.platform.tostring)
additem("osversion.version", _
environment.osversion.version.tostring)
additem("systemdirectory", _
environment.systemdirectory)
additem("userdomainname", environment.userdomainname)
additem("userinteractive", _
environment.userinteractive)
additem("username", environment.username)
additem("workingset", environment.workingset)
end sub
除了使用 environment 类之外,fillfolderlist 实际上还包含一些有趣的代码。这个过程的目的是循环使用 environment 类提供的 specialfolder 枚举中的所有成员。(此枚举包含物理文件夹 [如收藏夹和历史记录等] 的逻辑名称。)该过程将每个枚举值的名称添加到窗体上的 listview 控件中,同时将传递枚举值的结果添加到 environment 对象的 getfolderpath 方法中。下面所示的过程能够完成所有工作:
private sub fillfolderlist()
dim strname as string
dim astrnames() as string
dim aintvalues as array
dim i as integer
' 使用 specialfolder 枚举中的名称
' 填写 listview 控件的第一列。
astrnames = system.enum.getnames( _
gettype(environment.specialfolder))
aintvalues = system.enum.getvalues( _
gettype(environment.specialfolder))
for i = 0 to astrnames.length - 1
with lvwfolders.items.add(astrnames(i))
.subitems.add( _
environment.getfolderpath(aintvalues(i)))
end with
next
end sub
此示例没有对发送到 environment 对象的参数进行硬编码,而是使用了 enum 类的共享 getnames 和 getvalues 方法。通过将调用 visual basic .net gettype 函数的结果传递给 getnames(传递指定的枚举类型),您可以检索使用所有枚举成员的名称填写的数组。重复包含 getvalues 方法的过程将返回一个 array 对象,其中包含所有枚举值。
astrnames = system.enum.getnames( _
gettype(environment.specialfolder))
aintvalues = system.enum.getvalues( _
gettype(environment.specialfolder))
给出这两个数组后,程序的其余部分将在这两个数组中循环运行,将 astrnames 中的值添加到 listview,然后调用 environment 类的 getfolderpath 方法来检索相应的路径:
for i = 0 to astrnames.length - 1
with lvwfolders.items.add(astrnames(i))
.subitems.add( _
environment.getfolderpath(aintvalues(i)))
end with
next
图 4 中的上部 listview 控件包含这些代码的输出内容。
提示:enum 为您提供了一些 visual basic 6.0 中没有的技巧,例如示例中所示的 getnames 和 getvalues 方法。有关利用 enum 类的功能的详细信息,请参阅 .net 框架文档。
使用 windows.forms.systeminformation 类
在提供精心设计的用户界面时,您常常需要确定当前的 windows 设置,例如图标的高度和宽度,或者滚动条的宽度。在 visual basic 6.0 中,您可以使用 getsystemmetrics 和 systemparametersinfo windows api 函数来确定许多此类设置。在 .net 框架中,您可以使用 windows.forms 名称空间提供的 systeminformation 类。
尽管图 5 中 frmsysteminformation 使用的代码不是很有趣,但它确实显示了该类提供的所有属性。(单击主窗体上的 systeminformation info [系统信息] 可以测试此示例窗体。)示例窗体对 systeminformation 类的所有属性进行操作,将每个属性的名称和当前值显示在窗体的 listview 控件中。

图 5:显示 windows.forms.systeminformation 类的所有属性的示例窗体
示例窗体使用了前面介绍过的 additem 方法,在 systeminformation 类所有 60 多个属性中简单地循环运行并显示输出内容:
additem("arrangedirection", _
systeminformation.arrangedirection)
additem("startingposition", _
systeminformation.arrangestartingposition)
additem("bootmode", systeminformation.bootmode)
' 等等...
总结
- 虽然您可以使用 visual basic .net 中的平台调用服务 (p/invoke) 来处理非托管代码,然后直接调用 windows api,但在创建 visual basic .net 应用程序时,您还是应该寻找其他替代方法。不过,您不用担心 p/invoke 的细节问题,因为 declare 语句会为您处理这些细节。
- .net 框架并不封装所有的 windows api 功能,但是您在 visual basic 6.0 中可能要使用 api 调用才能实现的操作在 .net 框架中也可以实现。
- registry 和 registrykey 类简化了对 windows 注册表的操作,可以帮助开发人员避免大量的 api 调用。
- filedialog、colordialog、fontdialog 和 printerdialog 类使您可以很方便地使用 windows 中的常用对话框。您不需要直接调用 windows api,也不需要使用可怕的 commondialog activex 控件。
- 您可以使用 fileversioninfo 类检索内置在可执行文件、驱动程序文件和 dll 文件中的所有文件版本信息。此类可以替代 visual basic 6.0 中所需的某些复杂的 windows api 代码。
- environment 和 systeminformation 类使您可以方便地检索系统设置,如果没有这些类,则需要调用许多不同的 api 函数。
关于作者
ken getz 是 mcw technologies 的资深顾问,他的工作涉及编程、著书和培训。他精通用 microsoft access、visual basic 以及 office 和 backoffice 套件编写的工具和应用程序。ken 还与其他人一起编写了许多书籍,包括:与 paul litwin 和 mike gilbert 合著的《access 97 developer's handbook》;与 paul litwin 和 mike gilbert 合著的《access 2000 developer's handbooks》;与 paul litwin 和 mike gunderloy 合著的《access 2002 developer's handbooks》;与 mike gilbert 合著的《visual basic language developer's handbook》;以及与 mike gilbert (sybex) 合著的《vba developer's handbook》。他还参与了 appdev 培训资料的编写工作,并从事这方面的教学工作。ken 经常在技术会议上发言,自 1994 年以来,每一届的 microsoft tech*ed 会议上他都会发表演讲。ken 是 access/vb/sql advisor 杂志的技术编辑和 informant communication group 属下的《microsoft office solutions》杂志的电子文档撰稿人。
关于 informant communications group
informant communications group, inc. (www.informant.com) 是一家专注于信息技术行业的多媒体公司。icg 成立于 1990 年,致力于与软件开发有关的出版物、会议、目录发布和 web 站点等领域。icg 在美国和英国均设有办事处,目前已成为享有盛誉的媒体和营销内容集成商,并以高质量的技术信息满足 it 人员不断增长的需求。
© 2002 informant communications group 和 microsoft corporation 版权所有。
技术编辑:kng consulting