*** 基础知识
在讲API之前,让我先讲解一些与API相关的VB基础知识,后文如有涉及将不再详述。此处未提及的,将在本文中第一次接触时再做解释。
1.自定义类型
VB中可以使用Type关键字将已有的数据类型进行组合,成为一个新的类型,该类型就称为用户自定义类型。如:
Type NewType
sName As String
lNumber As Long
End Type
定义了一个名为NewType的自定义类型。以后可以用Dim MyType As NewType来定义一个NewType类型的变量。
sName As String类型的变量有两种,一种是变长,即运行时的字符串长度是可变的,另一种是定长,运行时字符串的长度是固定的。平常我们定义一个字符串变量: Dim strA As String 即定义了一个变长的字符串,但在使用API时经常要用到定长的字符串,应该这样定义: Dim strB As String * 30,即定义了一个可容纳30个字节字符的变量。
2.声明
VB中使用API之前,需要先对API进行声明,声明的方法是使用Declare关键字,如:
Public Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, lParam As Any) As Long
声明了一个名为SendMessage的API函数。许多API的声明可以在API浏览器中找到,而且本文在讲述一个API时也会给出声明,更深入的知识将在以后讲述。
3.句柄
API中使用得最多的一个词大概就是“句柄”了。如果要说得复杂些,句柄的确可以说上一大堆,不过作为VB的应用,我们可以更简单地去理解它。可以这么说,句柄是Windows系统赋予内存中每一个对象,包括窗口、按钮(其实也是一种窗口)或者文件、图标、菜单等等所有东西的标识。所谓标识就如身份证一样,是不会重复并且和实际对象是一一对应的。它的作用是让Windows知道将被操作的对象是谁。许多VB控件都提供了一个长型的hWnd属性,一般情况下,这个属性就是这个控件的句柄,用API控制这个控件时就需要用到这个属性了。
4.设备上下文
其实我觉得“设备上下文”这个词读起来很奇怪,不过就其字面来看,Device Context(DC,可不是直流电或DreamCast哦)就是这个意思,许多人也这么称它。不过我想翻译为“设备中介”大家应该更容易理解吧。它的作用是作为计算机设备和程序之间的中介,比如显示器和程序之间,或者打印机和程序之间。在对这些设备操作的时候,是需要通过这个中介操作的(与句柄相似),一些控件,如 Picture,想在上面画图时,就要用到hDC属性了。
*** 5.显式声明与自动保存
在默认情况下,Visual Basic会把未声明的变量认为是新变量(即使是你不小心打错字了),这样很容易出现错误,而且要找出这样的错误往往很不容易。加上API的操作基本上都会涉及到系统本身,一旦出错就有可能出现Visual Basic崩溃甚至系统崩溃。所以到Visual Basic的选项设置中选上Require Variable Declaration(需要变量声明)。在 Enviroment(环境)页中的When a program starts(当程序启动时)处选上Prompt To Save Changes(提示保存)或 Save Changes(自动保存)。这样Visual Basic会检查变量是否已经声明,不再允许没有声明的变量出现了。
6.API浏览器
前面讲“声明”的时候提到了API浏览器,这里也说一说它。在安装完VB时,安装程序会把API浏览器复制到你的计算机里。启动后界面如图1(以VB6为准)。可以看到,从API浏览器里我们可以得到API的声明、常量值以及与API相关的自定义类型的定义。其中Declare Scope(声明范围)是指该声明是公有的还是私有的。对于在标准模块中的声明,如果声明为私有,则只对该模块有效,如果为公有,则对整个程序都有效。在窗体模块中只能声明为私有。
7.MSDN
MSDN是微软发布的一套完整的Windows开发者技术文档(如图2)。里面不仅有VB、VC++、VFP、MSDEV、VSS等开发工具的完整帮助,而且包含了Windows平台开发的几乎所有的技术资料,并不断地更新。我们需要的API资料这里都有很详细的说明,包括使用平台、参数类型、参数作用等(当然它并非完全正确,错误的地方也时有出现)。不过作为以C为基础的API,这里的资料是英文的,而且格式也是C语言格式。想要读懂,那你的英文水平和C语言就要有两把刷子了。不过不必担心,它只是我建议一定要有的参考文档,最重要的还是我接下来要讲的内容,它不仅是中文的,还是VB的,又有使用示例,还有相关知识讲解。记住MSDN只是参考文档,作用就好像字典,可千万不要买了一套MSDN就跑去跟别人说你已经变得多厉害了,不然……
顺便说一说,以前的MSDN是双CD的,现在已经变成3CD了。如果买的话应尽量买最新的(但没有必要每次更新都换一套),因为更新的版本把一些错误改正了,并增加了新的内容,比如最新的MSDN已经增加了许多Windows XP的内容。你可以从微软的中国网站订到这套文档,不过一年的价钱从一万几到三万几,买不起的话……自己想办法吧。
好了,开篇写了这么多,无非也是想让读者在以后可以更容易理解所讲的内容。这些是基础,是一定要记住的。下面我们就先介绍一个比较简单的API,开始我们的API之旅吧!
*** 第一话 从消息说起
由于这是《细水长流话API》的第一话,我必须注意到所讲的内容要简单,并且让你有耐心可以看到往后的文章,所以我希望可以通过一个比较特别的例子来引起你的注意(这样的情况不会总是有的)。让我们想想,VB里的CommandButton控件让我们可以做什么?按下、弹起,还有呢?请看看图3,这样的情况在你的程序运行时出现过吗?
Windows是以消息来传递信息的。当出现某个操作,比如按钮被按下,就产生按钮被按下的消息。消息被传送到被操作对象(按钮),事件就产生了。应注意不是按钮产生消息,而是Windows知道这个操作的发生,向按钮发送这个消息,按钮收到后再做相应的处理——如改变外观成为按下的状态。
Windows允许第三者向某个对象发送消息,因此当某个操作没有发生时,我们是可以让对象如同收到消息一样产生效果的,这就需要用到API函数——SendMessage了。
SendMessage的声明前面已经说过(注意以Public开头应放在标准模块中,否则用Private开头),它的各个参数中,hwnd是对象的句柄,wMsg是消息的值(具体什么消息),另外两个参数根据不同消息和不同应用有不同的值。
你看到的图3的情况,是由于我的程序向Command Button控件发送了WM_NCLBUTTONDOWN消息。这个消息发生在鼠标在窗口的非客户区域上按下时。所谓非客户区域,你可以理解成一个窗口的边缘和标题栏(当然是指一般情况,这种情况是可以被程序改变的)。
在我这个按钮的MouseDown事件中,只写了短短的几句:
Private Sub cmdResize_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single)
Dim nParam As Long
With cmdResize
'之所以在0和100之间以及下面 .Width-100 和 .Width 之间,是让鼠标只在按钮边缘才可以拉动按钮
If X > 0 And X < 100 Then
nParam = HTLEFT
ElseIf X > .Width - 100 And X < .Width Then
nParam = HTRight
End If
If nParam Then
Call ReleaseCapture
Call SendMessage(.hwnd, WM_NCLBUTTONDOWN, nParam, 0)
End If
End With
End Sub
可以看到,我让鼠标拉动按钮时,拉按钮左边是用 HTLEFT做参数,拉右边是用HTRIGHT做参数。这两个都是常量,可以从API浏览器中得到值。同样的,若想拉按钮的上面和下面,可用HTTOP和HTBOTTOM做参数,而 HTTOPLEFT和HTBOTTOMRIGHT则分别是左上角和右下角。
在发送消息之前有一个ReleaseCapture的API。这个API是让Windows释放对鼠标的捕捉以便使鼠标位置的信息不能被收到,CommandButton不知道鼠标在哪里,也就不会发生按钮在这时被按下的情况。当然,可以放心,Windows释放对鼠标的捕捉只是暂时的,当你放开鼠标再次发生移动时,Windows又会捕捉鼠标了——它是时时都在发生的。
你可能希望如同我的程序一样在按钮边缘光标会变化,下面是我写的程序段:
Private Sub cmdResize_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single)
Dim NewPointer As MousePointerConstants
With cmdResize
If X > 0 And X < 100 Then
NewPointer = vbSizeWE
ElseIf X > .Width - 100 And X < .Width Then
NewPointer = vbSizeWE
Else
NewPointer = vbDefault
End If
If NewPointer <> .MousePointer Then
.MousePointer = NewPointer
End If
End With
End Sub
作用很明显,而且很简单,所以我就不对这段代码作解释了。
这个例子很简单,但相信起的作用是不小的。SendMessage可以发送很多消息,当然我不会对这些消息一一作解释,但以后还是会经常接触到的,所以更多的知识就等慢慢再学吧。
用过VB5.0或者更早版本的读者应该知道VB有一个测试字符串长度的函数: Len。但当你升级到VB6时,会发现这里的Len并没有以前那么好用了——它变成了测试字符个数而不是字符串长度。就是说,当你用以前版本的VB执行Len("字符abc")时,返回值是7,因为中文字符每个有2个字节,所以总共有7个字节;而在VB6中执行,返回值是5。
VB6不再有一个直接计算出字符串总字节数的函数了,因为VB6内部已经把字符串转换成了Unicode——一种比ANSI更新的字符编码方式。
Unicode把每一个字,无论是中文还是其他文字都当成两个字节,如果是英文,则这两个字节中第二个字节保留着不使用,如果是双字节字符(如中文,双字节日文以及韩文),而由这两个字节的组合表示一个字符。所以Len可以方便地知道一共有多少个双字节字符,多少个单字节字符,也就出现了上面所说的情况。
不过既然VB内部把ANSI字符转换成Unicode,那么它一定有对应方法转换回来。所以这里提供一个比较方便的方法来得到总字节数: LenB(StrConv("字符abc", vbFromUnicode))。
*** 这里用到了一个LenB() 函数,你可以自己试试它,比如 LenB("字符")、LenB("abc")、LenB("字符abc"),会发现返回值分别是4、6和10。
为什么是4、6和10呢?
我说过VB内部把ANSI字符转换为Unicode,每个Unicode字符用2个字节来表示,所以,LenB() 的作用是返回字符串的实际字节数。但是,这个实际字节数已经不是我所输入的字符串的,而是被VB转换过的(我们无法让VB函数在转换之前先算好长度),所以我们需要先把字符串转换回来,使用的是 StrConv() 函数。
对于这个函数我不想太过详细解释它(一般应用中比较少用),你可以参考MSDN,我只提一提它的第二个参数:vbFromUnicode。
StrConv()函数的第二个函数指定转换的类型,vbFromUnicode 指定把字符串从Unicode转换回来,如果是vbUnicode,则把字符串转换为Unicode。注意,虽然你的程序中写的是ANSI的字符而不是Unicode字符,但当这个函数执行时,它得到的却是已经被转换成为Unicode的字符串了。
现在问题可以算解决了,但我们还需要另一个解决方法,因为这种方法太费时。想想看,每一次算长度都要进行 Unicode->ANSI 的转换,这将会花费太多时间。对少量字符还可以,对长字符串,时间就变得更长了。
所以我们再讲一个API:lstrlen。
Public Declare Function lstrlen Lib "kernel32" Alias "lstrlenA" (ByVal lpString As String) As Long
以上是lstrlen的声明。lstrlen的作用只有一个:
得到字符串的字节数。所以执行 lstrlen("字符abc") 将返回7。我们不需要知道它内部是如何工作的,但它总是返回该字符串是ANSI时的长度,并且速度很快。
这是一个显示Windows的Temp目录、Windows安装目录以及System目录的路径的程序。这里用到了三个API分别得到这三个目录的路径。
Private Declare Function GetSystemDirectory Lib "kernel32" Alias "GetSystemDirectoryA" (ByVal lpBuffer As String, ByVal nSize As Long) As Long
Private Declare Function GetTempPath Lib "kernel32" Alias "GetTempPathA" (ByVal nBufferLength As Long, ByVal lpBuffer As String) As Long
Private Declare Function GetWindowsDirectory Lib "kernel32" Alias "GetWindowsDirectoryA" (ByVal lpBuffer As String, ByVal nSize As Long) As Long
比较一下,可以看到这三个API都用到两个参数,一个是字符串缓存,用来保存得到的路径,另一个是指定该缓存的大小。为什么这里要指定大小呢?我把我的代码贴下来,你看一看。
Private Sub Form_Load()
Dim sPath As String * 260, lLen As Long
lLen = GetTempPath(260, sPath)
Text1 = Left(sPath, lLen)
lLen = GetWindowsDirectory(sPath, 260)
Text2 = Left(sPath, lLen)
lLen = GetSystemDirectory(sPath, 260)
Text3 = Left(sPath, lLen)
End Sub
我的sPath是让API去赋值的,因此必须指定大小,以避免当缓存比API要填充的字符串还小时出现错误。它们的返回值都是API已经填充了的字符个数。因为定长字符串长度是一定的,所以没被填充的空间仍留着,所以要用left来取出有用的部分。
***
我在现在讲这个例子除了它实用简单,还因为我想让你知道定义长字符串在API中的应用,而且这里有个VB的知识要跟大家讲。当我们定义一个变长的字符串变量时,VB并不会像其他变量一样马上为它分配内存,而是当赋值给它时才分配合适大小的内存来存放。
但是API并不会像VB一样为你的变量分配内存并赋值,它只是知道你想要得到一个字符串,那么它就给你,至于你的变量装不装得下,那是你的事。定长的字符在定义时,由于已经指定了大小,所以VB就同时分配了内存给它,所以在使用API填充一个字符串变量时就要用定长字符串并指定字符的大小了。
但是,是不是定义时是变长的字符串变量就无法用来让API填充呢?其实是有办法的,就是事先让VB为它分配好足够的内存。看下面:
Dim sPath As String
sPath=Space(260)
或者
sPath=String(260,0)
用这段代码来代替前面定长字符串变量的声明,得到的结果是一样的。
Space(260)把260个空格赋给了sPath变长字符串变量,因此VB此时为它分配了可容纳260个空格的内存,而String(260,0)则把260个NULL字符(ASCII码为0的字符,在API中多数代表字符串的结尾)赋给sPath,它同样因此而得到260个字节的内存空间。当然你也可以用 String(260," "),让空格来填充这个空间,效果是一样的。
好了,这一期的内容已经讲完,记得复习,我们下期再继续。
经过前几期的连载,我们学到了几个有用的API,也许有的读者会希望我尽快介绍更多的API,不过有许多简单的API的用法是相似甚至相同的,所以为了让读者学到真正有用的知识,在连载的初期,我讲的API将是比较简单而又涉及到相关基础知识的。至于那些用法极相似甚至相同的,我会在适当的时候再介绍它们,只是详细程度和侧重点不同而已。这点希望引起读者的注意。
第四话 使用自定义类型
我在前面已经提到过自定义类型,这次我用一个简单的API来说明一个自定义类型在API中的使用。
VB中规定了自定义类型的变量传递给函数或子程序时必须按引用来传递(关于按引用传递与按值传递,将在以后的文章中做详细介绍),因此下面这个API的声明,你会发现和前面所介绍的几个有少许不同。
Public Declare Function GetCursorPos Lib "user32" Alias "GetCursorPos" (lpPoint As POINTAPI) As Long
相比上一话中的一个API:
Public Declare Function GetSystemDirectory Lib "kernel32" Alias "GetSystemDirectoryA" (ByVal lpBuffer As String, ByVal nSize As Long) As Long
可发现参数前面少了个ByVal。如果不加ByVal,或者把ByVal换成ByRef,就是按引用传递。POINTAPI不是VB的标准数据类型,它是一个自定义类型。从API浏览器中我们得到它的定义原形是这样的:
Public Type POINTAPI
x As Long
y As Long
End Type
这里应该引起注意的是,你应该把POINTAPI的定义写在使用它的函数声明之前,否则VB会认为你的类型未定义。你也不可以把 x As Long 和 y As Long 的位置对调,如果对调了,在这个API中最多只会使原本 x 的值变成 y 的值,y 的值变成 x 的值,但在更复杂的自定义类型中,结果就不可预知了。
这个API的作用是得到鼠标指针在屏幕中的坐标(以像素为单位)。你可以在自己的程序中试验它,比如:
Dim tCursor As POINTAPI
GetCursorPos tCursor
Debug.Print tCursor.x, tCursor.y
将从调试窗口打印鼠标指针的当前坐标