第一话 从消息说起
由于这是《细水长流话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分别得到这三个目录的路径。 比较一下,可以看到这三个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
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
将从调试窗口打印鼠标指针的当前坐标
VB中的坐标系统比较丰富,有Twip、Point、Pixel、 Character、Inch、Millimeter、Centimeter和User。很复杂吧?在这里我要说的是Twip和Pixel,至于剩下的,由于和本文所说的应用无多大关系,请参考MSDN或相关书籍。
VB中最常用的是Twip的坐标系统,按照微软的说法, Twip是一种与屏幕无关的测量单位,就是说,当我们使用Twip作为单位时,(在打印时)不需要担心屏幕的分辨率。看起来是挺方便的测量单位,但是在 API应用中,它却显得有点多余,因为在API中使用的坐标系统是Pixel。Pixel是以像素为单位的测量单位,像素是构成屏幕的最小元素,因此它也是常用的一种测量单位。
下面让我们来看看如何在API中应用这两个常用的坐标系统。我把上一话的示例扩展了一下,将要用到一个新的 API:ScreenToClient。
Private Declare Function ScreenToClient Lib "user32" (ByVal hwnd As Long, lpPoint As POINTAPI) As Long
ScreenToClient的作用是把屏幕中的坐标转换为客户区的坐标(关于什么是客户区,请参考前面的文章)。hwnd是客户区对象的句柄,而 lpPoint则是已经存放着屏幕坐标的 POINTAPI类型,执行该函数后,lpPoint的内容将被转换为客户区坐标值。
参考图1,它显示了当Form1的坐标系(ScaleMode)设置为Twip时:
1.鼠标在屏幕中的坐标
2.鼠标在Form1中的坐标(即由VB计算出来的客户坐标)
3.把鼠标的屏幕坐标转换为Form1的客户坐标
4.把以Pixel为单位的客户坐标转换为以Twip为单位的客户坐标
看看我是如何计算这4对坐标值的:
Private Sub Form_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single)
Dim tC As POINTAPI
GetCursorPos tC
Label1 = "1. Cursor Position: " & tC.X & Space(5) & tC.Y '注意这里是在屏幕中的坐标
Label2 = "2. Cursor on Form Coordinate: " & X & Space(5) & Y
ScreenToClient Me.hwnd, tC
Label3 = "3. ScreenToClient: " & tC.X & Space(5) & tC.Y '这里把屏幕中的坐标转换为在 Form1 中的坐标
Label4 = "4. Coordinate after transform: " & tC.X * Screen.TwipsPerPixelX & Space(5) & tC.Y * Screen.TwipsPerPixelY
End Sub
然后对比图2,和上面同样的代码,把Form1的ScaleMode设置为 Pixel 时计算出来的坐标值。
在图1中,Form1的ScaleMode是Twip,当把鼠标的屏幕坐标转换为客户坐标时,我们发现它和Form1本身提供的X、Y值不同(2和3不同),这是因为此时VB程序给我们的坐标值是以Twip为单位的。所以这里我提供了一个方法来把以像素为单位的客户坐标转换为以Twip为单位,即把水平和竖直方向的坐标值分别乘以Screen.TwipsPerPixelX和Screen.TwipsPerPixelY(所以2和4相同)。
Screen.TwipsPerPixelX和Screen.TwipsPerPixelY是由VB本身提供的,它们的作用是得到屏幕中在水平和竖直方向上每个像素各等于多少个Twip。你也可以使用另一个VB提供的方法:ScaleX()和ScaleY(),它们可以帮你把某一坐标系的值转换成另一坐标系的值。然而,作为一种习惯,我还是建议选择第一种方法,它显得直观一些,并且许多时候当看到这样一段代码时,我们可以马上就理解它的作用。
再看图2,Form1的ScaleMode是Pixel,因此Form1本身提供的X、Y和我们用API计算出来的值是相同的(2和3相同),而不是图1中和被转换为Twip的4相同。
看了上面的示例,我想你应该知道如何在API中使用 Twip和Pixel了。另外我还想补充一句,在一般应用中,我们使用得最多的还是Twip,原因之一是VB默认是使用它的,之二是用它来控制长度比用Pixel更准确,特别是在涉及到打印时——1 Point等于1/72英寸,1 Twip等于1/20 Point即1/1440⒋纾坷迕子?67 Twips; 而Pixel却因屏幕显示范围的不同而改变,这必将使得难以掌握打印长度。
程序在Windows98/2000+VB6下调试通过。工程文件下载地址是:
http://www.cfan.net.cn/qikan/cxg/0204gwv.zip。
窗体和风格
在Windows中大部分东西都是一个窗口,窗体、菜单、工具栏、状态栏、按钮、文本框……不要觉得奇怪,它们都是窗口——Window(是否从一个侧面说明了这个操作系统为何叫Windows,加了复数的Window)。
从VB的IDE中你可以更改一个窗体的外观,图1是 IDE中各种外框风格的窗体。
你可以看到它们有的有边框,有的没有;有的有标题栏,有的没有;有的有最大最小化按钮,有的没有。这些窗体的边框风格都是在窗体被创建时就定下来的。我们在建立VB程序的窗体时,不需要自己写创建窗体的代码,省去了许多重复的工作,但我们也因此失去了解其中秘密的机会。许多情况下窗体风格是在运行时就一直不变的,但有时我们要求在运行时改变,然而,类似BorderStyle等许多设置外观的属性只能在设计时才有效,在这种情况下,我们的这项工作就无法完成。所幸的是,实际上窗体的风格是能够在运行时被改变的,用SetWindowLong,我们就能解决这个问题。
以前我写过子类的文章,用的也是SetWindowLong,但这次我们不是要用子类,它比子类简单得多。下面给出SetWindowLong的声明:
Private Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
要改变窗体的风格,我们需要用一个常量来使 SetWindowLong知道我们对窗体进行风格设置:GWL_STYLE。
从API浏览器得到GWL_STYLE的值后,调用时,它是作为第二个参数传递出去的。那么第三个参数呢?这里显得有点复杂,因为它不是一个单一的参数,而是一组参数的组合。
就如上面我所说的,一个窗体可能有边框,可能有最大最小化按钮,可能有标题栏,但也有可能一部分或全部都没有,如果我们在这里只用一个参数为其设置风格,那么这么多风格就需要一种特殊方法,使该API能够知道我们包含了哪些风格在里面。这就是Or运算。Or运算是把两个数值进行或运算,而微软为了可以方便分离进行Or运算的值,对这些值都精心设计过,因此我们可以放心地将它们组合。如,把 1 和 2 进行Or运算,然后传递给函数,函数会自己分离出 1 和 2,就知道我们传递了 1 和 2 两个值。但有时我们不仅是要组合几个值,而且要把一个组里的某个值去除,所以还需要用另一种方法: And Not(这里的And 不是布尔运算的And,而是位运算的And)。比如把 1 和 2 进行 Or 运算后的值中的 1 去掉,则将其 And Not 1。如果想知道是否含有一个值,可以用And,如 If 64 And 3 Then ……这里只是提供一种方法让你可以使用,如果你想知道它们是如何工作的,我建议你参考位运算的相关书籍。
***
我说过窗体、按钮等许多东西都是一种窗口,那么这个函数也就理所当然的是针对所有窗口而设计的了,因此可供设置的风格非常多,并且新风格在新操作系统出现时也可能被增加,这里只能给出大部分最常用的,更多的风格请参考 MSDN的Window Styles部分。
WS_BORDER:窗口带有一个薄边框
WS_DLGFRAME:带有一般对话框的风格,但没有标题栏
WS_CAPTION:窗口带有一个标题栏,经测试,实际上等于 (WS_BORDER Or WS_DLGFRAME)
WS_SIZEBOX 和 WS_THICKFRAME:窗口带有一个可以调整窗口大小的边框(即VB里的Sizable,其他地方的边框均指不具调整大小功能的边框)
WS_HSCROLL:窗口带有一个水平滚动条
WS_MAXIMIZEBOX:窗口带有最大化按钮,该窗口必须具有 WS_CAPTION 风格
WS_MINIMIZEBOX:窗口带有最小化按钮,该窗口必须具有 WS_CAPTION 风格
WS_SYSMENU:在窗口的标题栏上增加一个系统菜单,该窗口必须具有 WS_CAPTION 风格(即WS_BORDER和WS_DLGFRAME)
WS_OVERLAPPED 和 WS_TILED:窗口是一个交迭式窗口。交迭式窗口带有一个标题栏和一个边框
WS_OVERLAPPEDWINDOW 和 WS_TILEDWINDOW:窗口是一个交迭式窗口,并且组合了 WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU,
WS_THICKFRAME, WS_MINIMIZEBOX 以及 WS_MAXIMIZEBOX 这些风格
WS_VSCROLL:窗口带有一个垂直滚动条
好了,说了这么多,下面该动手了。在VB 里BorderStyle设置为NONE的窗体,我在上面加了8个CheckBox ,分别测试这些CheckBox上面所示的风格,当CheckBox按下时,表示具有该风格,弹起时表示不具有该风格。
***
我把该示例所需的常量声明列在下面:
Private Const GWL_STYLE = (-16)
Private Const WS_BORDER = &H800000
Private Const WS_CAPTION = &HC00000 ' WS_BORDER Or WS_DLGFRAME
Private Const WS_DLGFRAME = &H400000
Private Const WS_SIZEBOX = &H40000
Private Const WS_MAXIMIZEBOX = &H10000
Private Const WS_MINIMIZEBOX = &H20000
Private Const WS_SYSMENU = &H80000
Private Const WS_HSCROLL = &H100000
Private Const WS_VSCROLL = &H200000
如果你要让窗体具有WS_SIZEBOX风格,可以这样写:
SetWindowLong Me.hwnd, GWL_STYLE, WS_SIZEBOX
但是这里仍有问题。这相当于只给窗体WS_SIZEBOX 风格,如果要其他风格我们就得一起加上,但如果我们想在保留窗体原有风格的基础上增加一个风格,还需要另一个API:
Private Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" (ByVal hwnd As Long, ByVal nIndex As Long) As Long
GetWindowLong的调用方法和SetWindowLong相似,只不过不需要第三个参数,因为这里的返回值是得到它的风格的组合。你可以先这样做:
Dim lStyle As Long
lStyle = GetWindowLong(Me.hwnd, GWL_STYLE)
然后你就可以放心地使用了。
SetWindowLong Me.hwnd, GWL_STYLE, lStyle Or WS_SIZEBOX
为窗体增加一个WS_SIZEBOX风格而无需担心其他风格会丢失。如果想去掉WS_SIZEBOX,则使用:
SetWindowLong Me.hwnd, GWL_STYLE, lStyle And Not WS_SIZEBOX
好了,到这里已为你讲述了安全地为窗体更改风格的方法,你可以把你想要的风格(比如前面所列出的)应用于你的窗体。但是,它还是不够完美,当你改了风格之后,你会发现——虽然风格实际上已经改了,但外表完全没变,就好像窗体忘了刷新一样。
让它刷新?或许你会这么认为,不过这个可怜的窗体,无论你用什么方法去刷新,它都无动于衷……很长一段时间以来我都使用了一个折衷的方法——改变窗体的大小,再改回去。当窗体大小被改变之后,它就会刷新一下,这样就没事了。但是这种方法显得笨了一点,你也许希望就如发送消息一样方便地让它正常刷新,不过就如前面所说,它不领你的情。
但是这种情况也并非无法解决,下一话,我将告诉你一个更好的办法。
位置与常居顶端
许多软件,特别是占桌面面积不是很大的软件(比如笔者的NaviEdit),通常都提供了一个常居顶端的功能(可能有的软件不是这么叫法,但作用是相同的),它的作用是保持窗口一直在其他窗口的上面,可以省去频繁切换窗口的动作。
如果你想这么做,有一个API可以实现: SetWindowPos,声明是这样的:
Private Declare Function SetWindowPos Lib "user32" Alias "SetWindowPos" (ByVal hwnd As Long, ByVal hWndInsertAfter As Long, ByVal x As Long, ByVal y As Long, ByVal cx As Long, ByVal cy As Long, ByVal wFlags As Long) As Long
虽然参数很多,但实际用起来很简单。hwnd是窗口的句柄,x、y、cx、cy分别是窗口的x和y坐标、宽和高度。hWndInsertAfter用来指定窗口的Z位置(或称Z顺序)。如果你经常接触3D方面的软件,你就知道Z代表深度。这个参数接受5种值:HWND_BOTTOM、 HWND_NOTOPMOST、HWND_TOP、HWND_TOPMOST或者另一个窗口的句柄。而wFlags用来指定附加的选项。
你可以用它改变窗口的位置和大小,而且它允许你同时改变Z位置(当然,在VB中不用API你也可以改变窗体大小和位置)。比如让窗口退到最下面,可以这么使用:
SetWindowPos Me.hWnd, HWND_BOTTOM, 10&, 10&, 80&, 120&, 0&
想要常居顶端,只需把HWND_BOTTOM改为 HWND_TOPMOST,而HWND_NOTOPMOST则是取消常居顶端,HWND_TOP是把窗口的Z位置改为最前。如果这个参数传递的是另一个窗口的句柄,则是把该窗口的Z 位置更改为在另一个窗口的下面。
***
非常简单的事情。不过如果像上面一样做,是不是单单改个Z位置也要计算窗口位置和大小?最后一个参数又是干什么用的呢?wFlags可以让SetWindowPos忽略或执行某种行为。这里给出一部分:
SWP_DRAWFRAME和SWP_FRAMECHANGED:强制发送 WM_NCCALCSIZE消息给窗口
SWP_HIDEWINDOW:隐藏窗口
SWP_NOACTIVATE:不激活窗口
SWP_NOMOVE:保持当前位置(忽略x和y)
SWP_NOREDRAW:窗口不自动重画
SWP_NOSIZE:保持当前大小(忽略cx和cy)
SWP_NOZORDER:保持窗口在列表的当前位置(忽略hWndInsertAfter)
SWP_SHOWWINDOW:显示窗口
这些参数可以使用Or运算组合,所以如果你不希望改变窗口位置和大小,你只需要给最后一个参数传递(SWP_NOMOVE Or SWP_NOSIZE)即可。如下:
SetWindowPos Me.hWnd, HWND_TOPMOST, 0&, 0&, 0&, 0&, SWP_NOMOVE Or SWP_NOSIZE
这里的x、y、cx、cy的值将被忽略。其他值的组合,你可以自己去试试。
好了,这个看起来好像有点复杂的API已经变得很清晰,那么轮到上一话的收尾。
WM_NCCALCSIZE消息是在计算窗口的客户区大小时被发送的,它主要是让程序可以收到该消息后重新计算客户区的大小。我们先不管它是不是也能像许多以WM_开头的消息一样由我们发送给程序让它产生作用,但它使用起来的复杂程度让我宁可选择改变窗体大小再改回去。当我们改变窗口的大小时,很明显的就是它一定会重新计算客户区大小以调整外观。既然这个函数可以强制发送WM_NCCALCSIZE消息,那么我们就应该试一试。
SetWindowPos Me.hwnd, 0&, 0&, 0&, 0&, 0&, SWP_NOSIZE Or SWP_NOZORDER Or SWP_NOMOVE Or SWP_FRAMECHANGED
为了不改变窗口大小、位置和Z顺序(就是要窗口保持原状),我使用SWP_NOSIZE Or SWP_NOZORDER Or SWP_NOMOVE,让它忽略前面所有的参数,最后加个 Or SWP_FRAMECHANGED 就是让它重新计算客户区大小。把上面的一行放到前一话中更改风格的那一句下面,再执行程序,成功了! 它已经能够正常刷新! 就是这样,问题马上变得这么简单。
父与子
在开始这一话之前,不知各位读者有没有使用过MDI Form呢?看看图1,这是一个标准的MDI Form和其中一个子窗体在标准和最大化情况下的外观。不过别误会,我不是想讲MDI,你再看看图2,我只是想让你区别图2的窗体不是MDI Form。图2的两个窗体都是一般的窗体,从最大化的外观就可以看出区别了。是不是觉得很有意思?其实也没有什么秘密。
我说过 Windows中多数东西都是一种窗口,比如按钮。一般情况下我们看到的按钮都是在一个窗体的里面,这是因为窗体和按钮有一种父与子的关系。当一个窗口成为另一个窗口的子窗口(Child),那么它的位置的变化就只发生在另一个窗口里,另一个窗口就是这个窗口的父窗口(Parent)。平时我们建立的窗体都是相互独立的,与其他的窗体没有关系,但我们可以通过API使它们建立起父与子的关系。这要用到SetParent:
Private Declare Function SetParent Lib "user32" (ByVal hWndChild As Long, ByVal hWndNewParent As Long) As Long
SetParent接收两个参数,第一个是将成为子窗口的窗口句柄,第二个是将成为父窗口的窗口句柄。它的使用很简单,比如想把Form2作为Form1的子窗口,只需这样使用:
SetParent Form2.hWnd, Form1.hWnd
Windows会自动把Form2在新的父窗口中的位置调整为原父窗口的位置(即使是桌面,也是一个父窗口)。即是说,假如原来在桌面的Form2,位置为10,10,则它在新的父窗口中的位置也为10,10。但这个新的10,10是以新父窗口为参照物的,无论怎么变化,都是在新父窗口中。
不过应该注意,并不是所有东西都适合当父窗口。因为每一种窗口都有为自己设计的行为,比如当画面重画时要画什么,如果我们为它添加了新的子窗口,那么它们将可能产生冲突,因为父窗口在设计时并没有考虑出现意外的子窗口的情况。为了说明这个问题,我做了一个示例。当我把按钮作为ListBox的子窗口时,你会看到由于ListBox在选择项目时进行了画面的重画,导致按 钮显示变得不正常,但当我按了一下按钮时,又因为按钮的重画,显示又正常了。
值得一提的是,当我们把Form1中的一个子窗口(比如按钮)放置到Form2中,而我们又在Form1中为这个子窗口的某个事件写了执行代码,那么 够岜恢葱新穑?Form2又需不需要为这个新的子窗口做特别处理呢?假如我的处理代码都是写在Form1中的,而所有控件都被我放到Form2中时(如图 4),它们的点击事件的代码仍然能被执行。由于无法得知实际上VB内部是如何处理控件的消息循环的,所以我也无法对此中秘密进行解释,特别是一个应该注意的问题——当你把按钮(这里以按钮为例,但其实其他东西也一样)放到 Form2中后,如果这个按钮在Form2中获得了焦点,那么你就无法从Form2切换回Form1,除非这时你可以让Form1中某个控件重新获得焦点 ——比如通过使某个控件从Form2中成为Form1的子窗口,或者使用 SetFocus让Form1的某个控件获得焦点。所以,实际应用中应该避免这种情况的发生。如果新的父窗口不是由VB所建立的窗体,那么这种事就不会发生,不过这已不是本话的内容了。
在我写的示例源程序里,还有一个GetParent的API这里没有讲到,我用它判断当前的子窗口是哪个窗体的子窗口。它的作用是返回指定子窗口的父窗口的句柄。
寻找子窗口
这里又是一个特别的例子,图像处理我还会两下,不过这可不是处理来的,而是真实的抓图。我把开始按钮移到这里来了。再看看图6,怎么样?有意思吧?
这里我要介绍几个API:
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Private Declare Function GetWindow Lib "user32" (ByVal hwnd As Long, ByVal wCmd As Long) As Long
Private Declare Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hwnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
首先是FindWindow。FindWindow可以根据所给的条件,从桌面上寻找一个窗口,lpClassName是窗口的类名,而lpWindowName是窗口的标题。我们可以传递lpClassName,让它找符合的类名的窗口,或传递 lpWindowName,让它找符合的标题的窗口,如果我们不需要两个条件都符合,则另一个参数可以传递vbNullString,让它忽略。它的返回值就是找到的窗口的句柄。
那么什么是类名?避开C++的相关术语来说,其实Windows的窗口都是某种类中的一种,这个“类”可以是Textbox、 Combobox,也可以是由用户来定义的,这个窗口是属于哪一类的,它的类名就是什么。GetWindow也可以用来寻找某个窗口并返回其句柄,但它只限于在某个窗口中寻找子窗口,因此它需要传递hWnd以表示在哪个窗口里寻找。而 wCmd用来描述要找的子窗口与父窗口的关系。它的值如下:
GW_CHILD:寻找第一个子窗口
GW_HWNDFIRST:寻找第一个同级窗口,或寻找第一个顶级窗口
GW_HWNDLAST:寻找最后一个同级窗口,或寻找最后一个顶级窗口
GW_HWNDNEXT:寻找下一个同级窗口
GW_HWNDdivV:寻找前一个同级窗口
GW_OWNER:寻找窗口的所有者(即父窗口)
我们先来理解什么是同级窗口和顶级窗口。打个比方,如果一个窗口有三个子窗口,则这三个窗口都是同一级的,互为同级窗口。如果我们从没寻找过一个子窗口,那么API 不知道我们要找的是和哪个窗口同级,那么此时它找的是顶级窗口,顶级窗口即是子窗口,但这个子的关系是直接的,而不会是子窗口的子窗口(即孙子,别笑,这里的术语不是我自己造的)。最后一个GetClassName和以前讲过的几个字符串相关的API用法差不多,hWnd是窗口句柄,lpClassName是用来接收窗口类名的缓冲区,nMaxCount则是说明缓冲区的大小。
***
那么接下来我是如何用它们的呢?看这里:
Dim hTaskbar As Long, hStartbutton As Long
Dim sClass As String * 250
hTaskbar = FindWindow("Shell_traywnd", vbNullString)
hStartbutton = GetWindow(hTaskbar, GW_CHILD)
Do
GetClassName hStartbutton, sClass, 250
If LCase(Left$(sClass, 6)) = "button" Then Exit Do
hStartbutton = GetWindow(hStartbutton, GW_HWNDNEXT)
Loop
我使用FindWindow从桌面上找到了一个类名为 “Shell_traywnd”的窗口,它就是任务栏(不要问我是怎么知道它的类名的)。然后我又用GetWindow函数,从任务栏找到第一个子窗口。接下来,我用一个Do…Loop结构的循环为上一次找到的子窗口检查其类名,如果类名是button,则说明是个按钮,一般来说,任务栏上只有一个是button类的,所以一找到,它势必就是“开始”按钮了。如果没找到,则仍使用GetWindow,但这次和第一次不同,我传递的不是任务栏的句柄,而是上一次找到的子窗口的句柄,为的是找下一个同级窗口,就这样一次次循环直到找到开始按钮。
那么,开始按钮就被我这么找到了,然后我就可以像对待其他窗口一样对待它:比如将它移动。不要忘了上一期所讲的内容,SetWindowPos将在这里产生作用,你可以移动它,或者为最后一个参数组合上SWP_HIDEWINDOW,让开始按钮变得不可见,或者组合SWP_SHOWWINDOW重新显示……
接下来轮到任务栏了,你从图6中可以看到在开始按钮的位置有另一个“厉害”的按钮取代它,这是上一话的内容:SetParent。我用SetParent为原本在Form1上的按钮指定了新的父窗口——任务栏。如果你查看我的示例源程序,你会发现在此按钮的GotFocus事件中,我把焦点转移给了另一个按钮,原因在上一话已经说了。
在示例源程序中,我还演示了隐藏和显示任务栏,仍然是SetWindowPos的功劳,提醒一下,为了不改变窗口的一些属性,要在最后一个参数组合上合适的值。
好了,这一期的内容就这么多,我想这一次你应该好好研究我的源程序,里面的东西涉及到上一期和本期的内容,把它消化下去吧。
摘自 田志良 的博文