VB使用API的簡明教程

此頁沒有內(nèi)容條目
內(nèi)容

第一話 從消息說起

 

  由于這是《細(xì)水長流話API》的第一話,我必須注意到所講的內(nèi)容要簡單,并且讓你有耐心可以看到往后的文章,所以我希望可以通過一個(gè)比較特別的例子來引起你的注意(這樣的情況不會(huì)總是有的)。讓我們想想,VB里的CommandButton控件讓我們可以做什么?按下、彈起,還有呢?請看看圖3,這樣的情況在你的程序運(yùn)行時(shí)出現(xiàn)過嗎?

 

  Windows是以消息來傳遞信息的。當(dāng)出現(xiàn)某個(gè)操作,比如按鈕被按下,就產(chǎn)生按鈕被按下的消息。消息被傳送到被操作對象(按鈕),事件就產(chǎn)生了。應(yīng)注意不是按鈕產(chǎn)生消息,而是Windows知道這個(gè)操作的發(fā)生,向按鈕發(fā)送這個(gè)消息,按鈕收到后再做相應(yīng)的處理——如改變外觀成為按下的狀態(tài)。

 

  Windows允許第三者向某個(gè)對象發(fā)送消息,因此當(dāng)某個(gè)操作沒有發(fā)生時(shí),我們是可以讓對象如同收到消息一樣產(chǎn)生效果的,這就需要用到API函數(shù)——SendMessage了。

 

  SendMessage的聲明前面已經(jīng)說過(注意以Public開頭應(yīng)放在標(biāo)準(zhǔn)模塊中,否則用Private開頭),它的各個(gè)參數(shù)中,hwnd是對象的句柄,wMsg是消息的值(具體什么消息),另外兩個(gè)參數(shù)根據(jù)不同消息和不同應(yīng)用有不同的值。

 

  你看到的圖3的情況,是由于我的程序向Command Button控件發(fā)送了WM_NCLBUTTONDOWN消息。這個(gè)消息發(fā)生在鼠標(biāo)在窗口的非客戶區(qū)域上按下時(shí)。所謂非客戶區(qū)域,你可以理解成一個(gè)窗口的邊緣和標(biāo)題欄(當(dāng)然是指一般情況,這種情況是可以被程序改變的)。

 

  在我這個(gè)按鈕的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 之間,是讓鼠標(biāo)只在按鈕邊緣才可以拉動(dòng)按鈕

 

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

 

 

 

可以看到,我讓鼠標(biāo)拉動(dòng)按鈕時(shí),拉按鈕左邊是用 HTLEFT做參數(shù),拉右邊是用HTRIGHT做參數(shù)。這兩個(gè)都是常量,可以從API瀏覽器中得到值。同樣的,若想拉按鈕的上面和下面,可用HTTOP和 HTBOTTOM做參數(shù),而 HTTOPLEFT和HTBOTTOMRIGHT則分別是左上角和右下角。

 

  在發(fā)送消息之前有一個(gè) ReleaseCapture的API。這個(gè)API是讓W(xué)indows釋放對鼠標(biāo)的捕捉以便使鼠標(biāo)位置的信息不能被收到,CommandButton不知道鼠標(biāo)在哪里,也就不會(huì)發(fā)生按鈕在這時(shí)被按下的情況。當(dāng)然,可以放心,Windows釋放對鼠標(biāo)的捕捉只是暫時(shí)的,當(dāng)你放開鼠標(biāo)再次發(fā)生移動(dòng)時(shí),Windows又會(huì)捕捉鼠標(biāo)了——它是時(shí)時(shí)都在發(fā)生的。

 

  你可能希望如同我的程序一樣在按鈕邊緣光標(biāo)會(huì)變化,下面是我寫的程序段:

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

 

  作用很明顯,而且很簡單,所以我就不對這段代碼作解釋了。

 

  這個(gè)例子很簡單,但相信起的作用是不小的。SendMessage可以發(fā)送很多消息,當(dāng)然我不會(huì)對這些消息一一作解釋,但以后還是會(huì)經(jīng)常接觸到的,所以更多的知識(shí)就等慢慢再學(xué)吧。

 

 

================

用過VB5.0或者更早版本的讀者應(yīng)該知道VB有一個(gè)測試字符串長度的函數(shù): Len。但當(dāng)你升級(jí)到VB6時(shí),會(huì)發(fā)現(xiàn)這里的Len并沒有以前那么好用了——它變成了測試字符個(gè)數(shù)而不是字符串長度。就是說,當(dāng)你用以前版本的VB執(zhí)行Len("字符abc")時(shí),返回值是7,因?yàn)橹形淖址總€(gè)有2個(gè)字節(jié),所以總共有7個(gè)字節(jié);而在VB6中執(zhí)行,返回值是5。

  VB6不再有一個(gè)直接計(jì)算出字符串總字節(jié)數(shù)的函數(shù)了,因?yàn)閂B6內(nèi)部已經(jīng)把字符串轉(zhuǎn)換成了Unicode——一種比ANSI更新的字符編碼方式。

 

  Unicode把每一個(gè)字,無論是中文還是其他文字都當(dāng)成兩個(gè)字節(jié),如果是英文,則這兩個(gè)字節(jié)中第二個(gè)字節(jié)保留著不使用,如果是雙字節(jié)字符(如中文,雙字節(jié)日文以及韓文),而由這兩個(gè)字節(jié)的組合表示一個(gè)字符。所以Len可以方便地知道一共有多少個(gè)雙字節(jié)字符,多少個(gè)單字節(jié)字符,也就出現(xiàn)了上面所說的情況。

  不過既然VB內(nèi)部把ANSI字符轉(zhuǎn)換成Unicode,那么它一定有對應(yīng)方法轉(zhuǎn)換回來。所以這里提供一個(gè)比較方便的方法來得到總字節(jié)數(shù): LenB(StrConv("字符abc", vbFromUnicode))。

 

 

***   這里用到了一個(gè)LenB() 函數(shù),你可以自己試試它,比如 LenB("字符")、LenB("abc")、LenB("字符abc"),會(huì)發(fā)現(xiàn)返回值分別是4、6和10。

  為什么是4、6和10呢?

  我說過VB內(nèi)部把ANSI字符轉(zhuǎn)換為Unicode,每個(gè)Unicode字符用2個(gè)字節(jié)來表示,所以,LenB() 的作用是返回字符串的實(shí)際字節(jié)數(shù)。但是,這個(gè)實(shí)際字節(jié)數(shù)已經(jīng)不是我所輸入的字符串的,而是被VB轉(zhuǎn)換過的(我們無法讓VB函數(shù)在轉(zhuǎn)換之前先算好長度),所以我們需要先把字符串轉(zhuǎn)換回來,使用的是 StrConv() 函數(shù)。

  對于這個(gè)函數(shù)我不想太過詳細(xì)解釋它(一般應(yīng)用中比較少用),你可以參考MSDN,我只提一提它的第二個(gè)參數(shù):vbFromUnicode。

 

  StrConv()函數(shù)的第二個(gè)函數(shù)指定轉(zhuǎn)換的類型,vbFromUnicode 指定把字符串從Unicode轉(zhuǎn)換回來,如果是vbUnicode,則把字符串轉(zhuǎn)換為Unicode。注意,雖然你的程序中寫的是ANSI的字符而不是 Unicode字符,但當(dāng)這個(gè)函數(shù)執(zhí)行時(shí),它得到的卻是已經(jīng)被轉(zhuǎn)換成為Unicode的字符串了。

  現(xiàn)在問題可以算解決了,但我們還需要另一個(gè)解決方法,因?yàn)檫@種方法太費(fèi)時(shí)。想想看,每一次算長度都要進(jìn)行 Unicode->ANSI 的轉(zhuǎn)換,這將會(huì)花費(fèi)太多時(shí)間。對少量字符還可以,對長字符串,時(shí)間就變得更長了。

  所以我們再講一個(gè)API:lstrlen。

Public Declare Function lstrlen Lib "kernel32" Alias "lstrlenA" (ByVal lpString As String) As Long

 

  以上是lstrlen的聲明。lstrlen的作用只有一個(gè):

  得到字符串的字節(jié)數(shù)。所以執(zhí)行 lstrlen("字符abc") 將返回7。我們不需要知道它內(nèi)部是如何工作的,但它總是返回該字符串是ANSI時(shí)的長度,并且速度很快。

 

==============

 

這是一個(gè)顯示W(wǎng)indows的Temp目錄、Windows安裝目錄以及System目錄的路徑的程序。這里用到了三個(gè)API分別得到這三個(gè)目錄的路徑?! ”容^一下,可以看到這三個(gè)API都用到兩個(gè)參數(shù),一個(gè)是字符串緩存,用來保存得到的路徑,另一個(gè)是指定該緩存的大小。為什么這里要指定大小呢?我把我的代碼貼下來,你看一看。

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去賦值的,因此必須指定大小,以避免當(dāng)緩存比API要填充的字符串還小時(shí)出現(xiàn)錯(cuò)誤。它們的返回值都是API已經(jīng)填充了的字符個(gè)數(shù)。因?yàn)槎ㄩL字符串長度是一定的,所以沒被填充的空間仍留著,所以要用left來取出有用的部分。

 

***

  我在現(xiàn)在講這個(gè)例子除了它實(shí)用簡單,還因?yàn)槲蚁胱屇阒蓝x長字符串在API中的應(yīng)用,而且這里有個(gè)VB的知識(shí)要跟大家講。當(dāng)我們定義一個(gè)變長的字符串變量時(shí),VB并不會(huì)像其他變量一樣馬上為它分配內(nèi)存,而是當(dāng)賦值給它時(shí)才分配合適大小的內(nèi)存來存放。

  但是API并不會(huì)像VB一樣為你的變量分配內(nèi)存并賦值,它只是知道你想要得到一個(gè)字符串,那么它就給你,至于你的變量裝不裝得下,那是你的事。定長的字符在定義時(shí),由于已經(jīng)指定了大小,所以VB就同時(shí)分配了內(nèi)存給它,所以在使用API填充一個(gè)字符串變量時(shí)就要用定長字符串并指定字符的大小了。

 

  但是,是不是定義時(shí)是變長的字符串變量就無法用來讓API填充呢?其實(shí)是有辦法的,就是事先讓VB為它分配好足夠的內(nèi)存??聪旅?

Dim sPath As String

sPath=Space(260)

'或者

sPath=String(260,0)

 

  用這段代碼來代替前面定長字符串變量的聲明,得到的結(jié)果是一樣的。

 

  Space(260)把260個(gè)空格賦給了sPath變長字符串變量,因此VB此時(shí)為它分配了可容納260個(gè)空格的內(nèi)存,而String(260,0)則把260個(gè)NULL字符(ASCII碼為0的字符,在API中多數(shù)代表字符串的結(jié)尾)賦給sPath,它同樣因此而得到260個(gè)字節(jié)的內(nèi)存空間。當(dāng)然你也可以用 String(260," "),讓空格來填充這個(gè)空間,效果是一樣的。

 

 

 

經(jīng)過前幾期的連載,我們學(xué)到了幾個(gè)有用的API,也許有的讀者會(huì)希望我盡快介紹更多的API,不過有許多簡單的API的用法是相似甚至相同的,所以為了讓讀者學(xué)到真正有用的知識(shí),在連載的初期,我講的API將是比較簡單而又涉及到相關(guān)基礎(chǔ)知識(shí)的。至于那些用法極相似甚至相同的,我會(huì)在適當(dāng)?shù)臅r(shí)候再介紹它們,只是詳細(xì)程度和側(cè)重點(diǎn)不同而已。這點(diǎn)希望引起讀者的注意。

 

 

 

第四話 使用自定義類型

 

  我在前面已經(jīng)提到過自定義類型,這次我用一個(gè)簡單的API來說明一個(gè)自定義類型在API中的使用。

 

  VB中規(guī)定了自定義類型的變量傳遞給函數(shù)或子程序時(shí)必須按引用來傳遞(關(guān)于按引用傳遞與按值傳遞,將在以后的文章中做詳細(xì)介紹),因此下面這個(gè)API的聲明,你會(huì)發(fā)現(xiàn)和前面所介紹的幾個(gè)有少許不同。

 

 

Public Declare Function GetCursorPos Lib "user32" Alias "GetCursorPos" (lpPoint As POINTAPI) As Long

 

相比上一話中的一個(gè)API:

 

 

Public Declare Function GetSystemDirectory Lib "kernel32" Alias "GetSystemDirectoryA" (ByVal lpBuffer As String, ByVal nSize As Long) As Long

 

  可發(fā)現(xiàn)參數(shù)前面少了個(gè)ByVal。如果不加ByVal,或者把ByVal換成ByRef,就是按引用傳遞。POINTAPI不是VB的標(biāo)準(zhǔn)數(shù)據(jù)類型,它是一個(gè)自定義類型。從API瀏覽器中我們得到它的定義原形是這樣的:

 

 

Public Type POINTAPI

 

x As Long

 

y As Long

 

End Type

 

  這里應(yīng)該引起注意的是,你應(yīng)該把POINTAPI的定義寫在使用它的函數(shù)聲明之前,否則VB會(huì)認(rèn)為你的類型未定義。你也不可以把 x As Long 和 y As Long 的位置對調(diào),如果對調(diào)了,在這個(gè)API中最多只會(huì)使原本 x 的值變成 y 的值,y 的值變成 x 的值,但在更復(fù)雜的自定義類型中,結(jié)果就不可預(yù)知了。

 

  這個(gè)API的作用是得到鼠標(biāo)指針在屏幕中的坐標(biāo)(以像素為單位)。你可以在自己的程序中試驗(yàn)它,比如:

Dim tCursor As POINTAPI

 

GetCursorPos tCursor

 

Debug.Print tCursor.x, tCursor.y

 

將從調(diào)試窗口打印鼠標(biāo)指針的當(dāng)前坐標(biāo)

 

VB中的坐標(biāo)系統(tǒng)比較豐富,有Twip、Point、Pixel、 Character、Inch、Millimeter、Centimeter和User。很復(fù)雜吧?在這里我要說的是Twip和Pixel,至于剩下的,由于和本文所說的應(yīng)用無多大關(guān)系,請參考MSDN或相關(guān)書籍。

 

  VB中最常用的是Twip的坐標(biāo)系統(tǒng),按照微軟的說法, Twip是一種與屏幕無關(guān)的測量單位,就是說,當(dāng)我們使用Twip作為單位時(shí),(在打印時(shí))不需要擔(dān)心屏幕的分辨率??雌饋硎峭Ψ奖愕臏y量單位,但是在 API應(yīng)用中,它卻顯得有點(diǎn)多余,因?yàn)樵贏PI中使用的坐標(biāo)系統(tǒng)是Pixel。Pixel是以像素為單位的測量單位,像素是構(gòu)成屏幕的最小元素,因此它也是常用的一種測量單位。

 

  下面讓我們來看看如何在API中應(yīng)用這兩個(gè)常用的坐標(biāo)系統(tǒng)。我把上一話的示例擴(kuò)展了一下,將要用到一個(gè)新的 API:ScreenToClient。

 

 

Private Declare Function ScreenToClient Lib "user32" (ByVal hwnd As Long, lpPoint As POINTAPI) As Long

 

  ScreenToClient的作用是把屏幕中的坐標(biāo)轉(zhuǎn)換為客戶區(qū)的坐標(biāo)(關(guān)于什么是客戶區(qū),請參考前面的文章)。hwnd是客戶區(qū)對象的句柄,而 lpPoint則是已經(jīng)存放著屏幕坐標(biāo)的 POINTAPI類型,執(zhí)行該函數(shù)后,lpPoint的內(nèi)容將被轉(zhuǎn)換為客戶區(qū)坐標(biāo)值。

 

參考圖1,它顯示了當(dāng)Form1的坐標(biāo)系(ScaleMode)設(shè)置為Twip時(shí):

 

1.鼠標(biāo)在屏幕中的坐標(biāo)

 

2.鼠標(biāo)在Form1中的坐標(biāo)(即由VB計(jì)算出來的客戶坐標(biāo))

 

3.把鼠標(biāo)的屏幕坐標(biāo)轉(zhuǎn)換為Form1的客戶坐標(biāo)

 

4.把以Pixel為單位的客戶坐標(biāo)轉(zhuǎn)換為以Twip為單位的客戶坐標(biāo)

 

看看我是如何計(jì)算這4對坐標(biāo)值的:

 

 

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 '注意這里是在屏幕中的坐標(biāo)

 

Label2 = "2. Cursor on Form Coordinate: " & X & Space(5) & Y

 

ScreenToClient Me.hwnd, tC

Label3 = "3. ScreenToClient: " & tC.X & Space(5) & tC.Y '這里把屏幕中的坐標(biāo)轉(zhuǎn)換為在 Form1 中的坐標(biāo)

Label4 = "4. Coordinate after transform: " & tC.X * Screen.TwipsPerPixelX & Space(5) & tC.Y * Screen.TwipsPerPixelY

 

End Sub

 

  然后對比圖2,和上面同樣的代碼,把Form1的ScaleMode設(shè)置為 Pixel 時(shí)計(jì)算出來的坐標(biāo)值。

  在圖1中,F(xiàn)orm1的ScaleMode是Twip,當(dāng)把鼠標(biāo)的屏幕坐標(biāo)轉(zhuǎn)換為客戶坐標(biāo)時(shí),我們發(fā)現(xiàn)它和Form1本身提供的X、Y值不同(2和3不同),這是因?yàn)榇藭r(shí)VB程序給我們的坐標(biāo)值是以Twip為單位的。所以這里我提供了一個(gè)方法來把以像素為單位的客戶坐標(biāo)轉(zhuǎn)換為以Twip為單位,即把水平和豎直方向的坐標(biāo)值分別乘以Screen.TwipsPerPixelX和Screen.TwipsPerPixelY(所以2和4相同)。

 

  Screen.TwipsPerPixelX和Screen.TwipsPerPixelY是由VB本身提供的,它們的作用是得到屏幕中在水平和豎直方向上每個(gè)像素各等于多少個(gè)Twip。你也可以使用另一個(gè)VB提供的方法:ScaleX()和ScaleY(),它們可以幫你把某一坐標(biāo)系的值轉(zhuǎn)換成另一坐標(biāo)系的值。然而,作為一種習(xí)慣,我還是建議選擇第一種方法,它顯得直觀一些,并且許多時(shí)候當(dāng)看到這樣一段代碼時(shí),我們可以馬上就理解它的作用。

 

  再看圖2,F(xiàn)orm1的ScaleMode是Pixel,因此Form1本身提供的X、Y和我們用API計(jì)算出來的值是相同的(2和3相同),而不是圖1中和被轉(zhuǎn)換為Twip的4相同。

 

  看了上面的示例,我想你應(yīng)該知道如何在API中使用 Twip和Pixel了。另外我還想補(bǔ)充一句,在一般應(yīng)用中,我們使用得最多的還是Twip,原因之一是VB默認(rèn)是使用它的,之二是用它來控制長度比用Pixel更準(zhǔn)確,特別是在涉及到打印時(shí)——1 Point等于1/72英寸,1 Twip等于1/20 Point即1/1440⒋紓坷迕子?67 Twips; 而Pixel卻因屏幕顯示范圍的不同而改變,這必將使得難以掌握打印長度。

 

  程序在Windows98/2000+VB6下調(diào)試通過。工程文件下載地址是:

  http://www.cfan.net.cn/qikan/cxg/0204gwv.zip。

 

窗體和風(fēng)格

 

  在Windows中大部分東西都是一個(gè)窗口,窗體、菜單、工具欄、狀態(tài)欄、按鈕、文本框……不要覺得奇怪,它們都是窗口——Window(是否從一個(gè)側(cè)面說明了這個(gè)操作系統(tǒng)為何叫Windows,加了復(fù)數(shù)的Window)。

 

  從VB的IDE中你可以更改一個(gè)窗體的外觀,圖1是 IDE中各種外框風(fēng)格的窗體。

 

  你可以看到它們有的有邊框,有的沒有;有的有標(biāo)題欄,有的沒有;有的有最大最小化按鈕,有的沒有。這些窗體的邊框風(fēng)格都是在窗體被創(chuàng)建時(shí)就定下來的。我們在建立VB程序的窗體時(shí),不需要自己寫創(chuàng)建窗體的代碼,省去了許多重復(fù)的工作,但我們也因此失去了解其中秘密的機(jī)會(huì)。許多情況下窗體風(fēng)格是在運(yùn)行時(shí)就一直不變的,但有時(shí)我們要求在運(yùn)行時(shí)改變,然而,類似BorderStyle等許多設(shè)置外觀的屬性只能在設(shè)計(jì)時(shí)才有效,在這種情況下,我們的這項(xiàng)工作就無法完成。所幸的是,實(shí)際上窗體的風(fēng)格是能夠在運(yùn)行時(shí)被改變的,用SetWindowLong,我們就能解決這個(gè)問題。

 

  以前我寫過子類的文章,用的也是SetWindowLong,但這次我們不是要用子類,它比子類簡單得多。下面給出SetWindowLong的聲明:

Private Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long

 

  要改變窗體的風(fēng)格,我們需要用一個(gè)常量來使 SetWindowLong知道我們對窗體進(jìn)行風(fēng)格設(shè)置:GWL_STYLE。

 

  從API瀏覽器得到GWL_STYLE的值后,調(diào)用時(shí),它是作為第二個(gè)參數(shù)傳遞出去的。那么第三個(gè)參數(shù)呢?這里顯得有點(diǎn)復(fù)雜,因?yàn)樗皇且粋€(gè)單一的參數(shù),而是一組參數(shù)的組合。

 

  就如上面我所說的,一個(gè)窗體可能有邊框,可能有最大最小化按鈕,可能有標(biāo)題欄,但也有可能一部分或全部都沒有,如果我們在這里只用一個(gè)參數(shù)為其設(shè)置風(fēng)格,那么這么多風(fēng)格就需要一種特殊方法,使該API能夠知道我們包含了哪些風(fēng)格在里面。這就是Or運(yùn)算。Or運(yùn)算是把兩個(gè)數(shù)值進(jìn)行或運(yùn)算,而微軟為了可以方便分離進(jìn)行Or運(yùn)算的值,對這些值都精心設(shè)計(jì)過,因此我們可以放心地將它們組合。如,把 1 和 2 進(jìn)行Or運(yùn)算,然后傳遞給函數(shù),函數(shù)會(huì)自己分離出 1 和 2,就知道我們傳遞了 1 和 2 兩個(gè)值。但有時(shí)我們不僅是要組合幾個(gè)值,而且要把一個(gè)組里的某個(gè)值去除,所以還需要用另一種方法: And Not(這里的And 不是布爾運(yùn)算的And,而是位運(yùn)算的And)。比如把 1 和 2 進(jìn)行 Or 運(yùn)算后的值中的 1 去掉,則將其 And Not 1。如果想知道是否含有一個(gè)值,可以用And,如 If 64 And 3 Then ……這里只是提供一種方法讓你可以使用,如果你想知道它們是如何工作的,我建議你參考位運(yùn)算的相關(guān)書籍。

 

***

  我說過窗體、按鈕等許多東西都是一種窗口,那么這個(gè)函數(shù)也就理所當(dāng)然的是針對所有窗口而設(shè)計(jì)的了,因此可供設(shè)置的風(fēng)格非常多,并且新風(fēng)格在新操作系統(tǒng)出現(xiàn)時(shí)也可能被增加,這里只能給出大部分最常用的,更多的風(fēng)格請參考 MSDN的Window Styles部分。

 

WS_BORDER:窗口帶有一個(gè)薄邊框

 

WS_DLGFRAME:帶有一般對話框的風(fēng)格,但沒有標(biāo)題欄

 

WS_CAPTION:窗口帶有一個(gè)標(biāo)題欄,經(jīng)測試,實(shí)際上等于 (WS_BORDER Or WS_DLGFRAME)

 

WS_SIZEBOX 和 WS_THICKFRAME:窗口帶有一個(gè)可以調(diào)整窗口大小的邊框(即VB里的Sizable,其他地方的邊框均指不具調(diào)整大小功能的邊框)

 

WS_HSCROLL:窗口帶有一個(gè)水平滾動(dòng)條

 

WS_MAXIMIZEBOX:窗口帶有最大化按鈕,該窗口必須具有 WS_CAPTION 風(fēng)格

 

WS_MINIMIZEBOX:窗口帶有最小化按鈕,該窗口必須具有 WS_CAPTION 風(fēng)格

 

WS_SYSMENU:在窗口的標(biāo)題欄上增加一個(gè)系統(tǒng)菜單,該窗口必須具有 WS_CAPTION 風(fēng)格(即WS_BORDER和WS_DLGFRAME)

 

WS_OVERLAPPED 和 WS_TILED:窗口是一個(gè)交迭式窗口。交迭式窗口帶有一個(gè)標(biāo)題欄和一個(gè)邊框

 

WS_OVERLAPPEDWINDOW 和 WS_TILEDWINDOW:窗口是一個(gè)交迭式窗口,并且組合了 WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU,

 

WS_THICKFRAME, WS_MINIMIZEBOX 以及 WS_MAXIMIZEBOX 這些風(fēng)格

 

WS_VSCROLL:窗口帶有一個(gè)垂直滾動(dòng)條

 

  好了,說了這么多,下面該動(dòng)手了。在VB 里BorderStyle設(shè)置為NONE的窗體,我在上面加了8個(gè)CheckBox ,分別測試這些CheckBox上面所示的風(fēng)格,當(dāng)CheckBox按下時(shí),表示具有該風(fēng)格,彈起時(shí)表示不具有該風(fēng)格。

 

***

 

我把該示例所需的常量聲明列在下面:

 

 

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風(fēng)格,可以這樣寫:

SetWindowLong Me.hwnd, GWL_STYLE, WS_SIZEBOX

 

  但是這里仍有問題。這相當(dāng)于只給窗體WS_SIZEBOX 風(fēng)格,如果要其他風(fēng)格我們就得一起加上,但如果我們想在保留窗體原有風(fēng)格的基礎(chǔ)上增加一個(gè)風(fēng)格,還需要另一個(gè)API:

Private Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" (ByVal hwnd As Long, ByVal nIndex As Long) As Long

 

  GetWindowLong的調(diào)用方法和SetWindowLong相似,只不過不需要第三個(gè)參數(shù),因?yàn)檫@里的返回值是得到它的風(fēng)格的組合。你可以先這樣做:

Dim lStyle As Long

 

lStyle = GetWindowLong(Me.hwnd, GWL_STYLE)

 

然后你就可以放心地使用了。

SetWindowLong Me.hwnd, GWL_STYLE, lStyle Or WS_SIZEBOX

 

為窗體增加一個(gè)WS_SIZEBOX風(fēng)格而無需擔(dān)心其他風(fēng)格會(huì)丟失。如果想去掉WS_SIZEBOX,則使用:

SetWindowLong Me.hwnd, GWL_STYLE, lStyle And Not WS_SIZEBOX

 

  好了,到這里已為你講述了安全地為窗體更改風(fēng)格的方法,你可以把你想要的風(fēng)格(比如前面所列出的)應(yīng)用于你的窗體。但是,它還是不夠完美,當(dāng)你改了風(fēng)格之后,你會(huì)發(fā)現(xiàn)——雖然風(fēng)格實(shí)際上已經(jīng)改了,但外表完全沒變,就好像窗體忘了刷新一樣。

讓它刷新?或許你會(huì)這么認(rèn)為,不過這個(gè)可憐的窗體,無論你用什么方法去刷新,它都無動(dòng)于衷……很長一段時(shí)間以來我都使用了一個(gè)折衷的方法——改變窗體的大小,再改回去。當(dāng)窗體大小被改變之后,它就會(huì)刷新一下,這樣就沒事了。但是這種方法顯得笨了一點(diǎn),你也許希望就如發(fā)送消息一樣方便地讓它正常刷新,不過就如前面所說,它不領(lǐng)你的情。

 

  但是這種情況也并非無法解決,下一話,我將告訴你一個(gè)更好的辦法。

 

位置與常居頂端

 

  許多軟件,特別是占桌面面積不是很大的軟件(比如筆者的NaviEdit),通常都提供了一個(gè)常居頂端的功能(可能有的軟件不是這么叫法,但作用是相同的),它的作用是保持窗口一直在其他窗口的上面,可以省去頻繁切換窗口的動(dòng)作。

 

如果你想這么做,有一個(gè)API可以實(shí)現(xiàn): 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

 

  雖然參數(shù)很多,但實(shí)際用起來很簡單。hwnd是窗口的句柄,x、y、cx、cy分別是窗口的x和y坐標(biāo)、寬和高度。hWndInsertAfter用來指定窗口的Z位置(或稱Z順序)。如果你經(jīng)常接觸3D方面的軟件,你就知道Z代表深度。這個(gè)參數(shù)接受5種值:HWND_BOTTOM、 HWND_NOTOPMOST、HWND_TOP、HWND_TOPMOST或者另一個(gè)窗口的句柄。而wFlags用來指定附加的選項(xiàng)。

 

  你可以用它改變窗口的位置和大小,而且它允許你同時(shí)改變Z位置(當(dāng)然,在VB中不用API你也可以改變窗體大小和位置)。比如讓窗口退到最下面,可以這么使用:

SetWindowPos Me.hWnd, HWND_BOTTOM, 10&, 10&, 80&, 120&, 0&

 

  想要常居頂端,只需把HWND_BOTTOM改為 HWND_TOPMOST,而HWND_NOTOPMOST則是取消常居頂端,HWND_TOP是把窗口的Z位置改為最前。如果這個(gè)參數(shù)傳遞的是另一個(gè)窗口的句柄,則是把該窗口的Z 位置更改為在另一個(gè)窗口的下面。

 

***

  非常簡單的事情。不過如果像上面一樣做,是不是單單改個(gè)Z位置也要計(jì)算窗口位置和大小?最后一個(gè)參數(shù)又是干什么用的呢?wFlags可以讓SetWindowPos忽略或執(zhí)行某種行為。這里給出一部分:

 

SWP_DRAWFRAME和SWP_FRAMECHANGED:強(qiáng)制發(fā)送 WM_NCCALCSIZE消息給窗口

 

SWP_HIDEWINDOW:隱藏窗口

 

SWP_NOACTIVATE:不激活窗口

 

SWP_NOMOVE:保持當(dāng)前位置(忽略x和y)

 

SWP_NOREDRAW:窗口不自動(dòng)重畫

 

SWP_NOSIZE:保持當(dāng)前大?。ê雎詂x和cy)

 

SWP_NOZORDER:保持窗口在列表的當(dāng)前位置(忽略hWndInsertAfter)

 

SWP_SHOWWINDOW:顯示窗口

 

  這些參數(shù)可以使用Or運(yùn)算組合,所以如果你不希望改變窗口位置和大小,你只需要給最后一個(gè)參數(shù)傳遞(SWP_NOMOVE Or SWP_NOSIZE)即可。如下:

SetWindowPos Me.hWnd, HWND_TOPMOST, 0&, 0&, 0&, 0&, SWP_NOMOVE Or SWP_NOSIZE

 

  這里的x、y、cx、cy的值將被忽略。其他值的組合,你可以自己去試試。

  好了,這個(gè)看起來好像有點(diǎn)復(fù)雜的API已經(jīng)變得很清晰,那么輪到上一話的收尾。

 

  WM_NCCALCSIZE消息是在計(jì)算窗口的客戶區(qū)大小時(shí)被發(fā)送的,它主要是讓程序可以收到該消息后重新計(jì)算客戶區(qū)的大小。我們先不管它是不是也能像許多以WM_開頭的消息一樣由我們發(fā)送給程序讓它產(chǎn)生作用,但它使用起來的復(fù)雜程度讓我寧可選擇改變窗體大小再改回去。當(dāng)我們改變窗口的大小時(shí),很明顯的就是它一定會(huì)重新計(jì)算客戶區(qū)大小以調(diào)整外觀。既然這個(gè)函數(shù)可以強(qiáng)制發(fā)送WM_NCCALCSIZE消息,那么我們就應(yīng)該試一試。

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,讓它忽略前面所有的參數(shù),最后加個(gè) Or SWP_FRAMECHANGED 就是讓它重新計(jì)算客戶區(qū)大小。把上面的一行放到前一話中更改風(fēng)格的那一句下面,再執(zhí)行程序,成功了! 它已經(jīng)能夠正常刷新! 就是這樣,問題馬上變得這么簡單。

 

父與子

 

  在開始這一話之前,不知各位讀者有沒有使用過MDI Form呢?看看圖1,這是一個(gè)標(biāo)準(zhǔn)的MDI Form和其中一個(gè)子窗體在標(biāo)準(zhǔn)和最大化情況下的外觀。不過別誤會(huì),我不是想講MDI,你再看看圖2,我只是想讓你區(qū)別圖2的窗體不是MDI Form。圖2的兩個(gè)窗體都是一般的窗體,從最大化的外觀就可以看出區(qū)別了。是不是覺得很有意思?其實(shí)也沒有什么秘密。

 

  我說過 Windows中多數(shù)東西都是一種窗口,比如按鈕。一般情況下我們看到的按鈕都是在一個(gè)窗體的里面,這是因?yàn)榇绑w和按鈕有一種父與子的關(guān)系。當(dāng)一個(gè)窗口成為另一個(gè)窗口的子窗口(Child),那么它的位置的變化就只發(fā)生在另一個(gè)窗口里,另一個(gè)窗口就是這個(gè)窗口的父窗口(Parent)。平時(shí)我們建立的窗體都是相互獨(dú)立的,與其他的窗體沒有關(guān)系,但我們可以通過API使它們建立起父與子的關(guān)系。這要用到SetParent:

 

 

Private Declare Function SetParent Lib "user32" (ByVal hWndChild As Long, ByVal hWndNewParent As Long) As Long

 

  SetParent接收兩個(gè)參數(shù),第一個(gè)是將成為子窗口的窗口句柄,第二個(gè)是將成為父窗口的窗口句柄。它的使用很簡單,比如想把Form2作為Form1的子窗口,只需這樣使用:

SetParent Form2.hWnd, Form1.hWnd

 

  Windows會(huì)自動(dòng)把Form2在新的父窗口中的位置調(diào)整為原父窗口的位置(即使是桌面,也是一個(gè)父窗口)。即是說,假如原來在桌面的Form2,位置為10,10,則它在新的父窗口中的位置也為10,10。但這個(gè)新的10,10是以新父窗口為參照物的,無論怎么變化,都是在新父窗口中。

 

  不過應(yīng)該注意,并不是所有東西都適合當(dāng)父窗口。因?yàn)槊恳环N窗口都有為自己設(shè)計(jì)的行為,比如當(dāng)畫面重畫時(shí)要畫什么,如果我們?yōu)樗砑恿诵碌淖哟翱冢敲此鼈儗⒖赡墚a(chǎn)生沖突,因?yàn)楦复翱谠谠O(shè)計(jì)時(shí)并沒有考慮出現(xiàn)意外的子窗口的情況。為了說明這個(gè)問題,我做了一個(gè)示例。當(dāng)我把按鈕作為ListBox的子窗口時(shí),你會(huì)看到由于ListBox在選擇項(xiàng)目時(shí)進(jìn)行了畫面的重畫,導(dǎo)致按 鈕顯示變得不正常,但當(dāng)我按了一下按鈕時(shí),又因?yàn)榘粹o的重畫,顯示又正常了。

 

  值得一提的是,當(dāng)我們把Form1中的一個(gè)子窗口(比如按鈕)放置到Form2中,而我們又在Form1中為這個(gè)子窗口的某個(gè)事件寫了執(zhí)行代碼,那么 夠岜恢蔥新穡?Form2又需不需要為這個(gè)新的子窗口做特別處理呢?假如我的處理代碼都是寫在Form1中的,而所有控件都被我放到Form2中時(shí)(如圖 4),它們的點(diǎn)擊事件的代碼仍然能被執(zhí)行。由于無法得知實(shí)際上VB內(nèi)部是如何處理控件的消息循環(huán)的,所以我也無法對此中秘密進(jìn)行解釋,特別是一個(gè)應(yīng)該注意的問題——當(dāng)你把按鈕(這里以按鈕為例,但其實(shí)其他東西也一樣)放到 Form2中后,如果這個(gè)按鈕在Form2中獲得了焦點(diǎn),那么你就無法從Form2切換回Form1,除非這時(shí)你可以讓Form1中某個(gè)控件重新獲得焦點(diǎn) ——比如通過使某個(gè)控件從Form2中成為Form1的子窗口,或者使用 SetFocus讓Form1的某個(gè)控件獲得焦點(diǎn)。所以,實(shí)際應(yīng)用中應(yīng)該避免這種情況的發(fā)生。如果新的父窗口不是由VB所建立的窗體,那么這種事就不會(huì)發(fā)生,不過這已不是本話的內(nèi)容了。

 

  在我寫的示例源程序里,還有一個(gè)GetParent的API這里沒有講到,我用它判斷當(dāng)前的子窗口是哪個(gè)窗體的子窗口。它的作用是返回指定子窗口的父窗口的句柄。

 

尋找子窗口

 

  這里又是一個(gè)特別的例子,圖像處理我還會(huì)兩下,不過這可不是處理來的,而是真實(shí)的抓圖。我把開始按鈕移到這里來了。再看看圖6,怎么樣?有意思吧?

 

這里我要介紹幾個(gè)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可以根據(jù)所給的條件,從桌面上尋找一個(gè)窗口,lpClassName是窗口的類名,而lpWindowName是窗口的標(biāo)題。我們可以傳遞lpClassName,讓它找符合的類名的窗口,或傳遞 lpWindowName,讓它找符合的標(biāo)題的窗口,如果我們不需要兩個(gè)條件都符合,則另一個(gè)參數(shù)可以傳遞vbNullString,讓它忽略。它的返回值就是找到的窗口的句柄。

 

  那么什么是類名?避開C++的相關(guān)術(shù)語來說,其實(shí)Windows的窗口都是某種類中的一種,這個(gè)“類”可以是Textbox、 Combobox,也可以是由用戶來定義的,這個(gè)窗口是屬于哪一類的,它的類名就是什么。GetWindow也可以用來尋找某個(gè)窗口并返回其句柄,但它只限于在某個(gè)窗口中尋找子窗口,因此它需要傳遞hWnd以表示在哪個(gè)窗口里尋找。而 wCmd用來描述要找的子窗口與父窗口的關(guān)系。它的值如下:

 

GW_CHILD:尋找第一個(gè)子窗口

 

GW_HWNDFIRST:尋找第一個(gè)同級(jí)窗口,或?qū)ふ业谝粋€(gè)頂級(jí)窗口

 

GW_HWNDLAST:尋找最后一個(gè)同級(jí)窗口,或?qū)ふ易詈笠粋€(gè)頂級(jí)窗口

 

GW_HWNDNEXT:尋找下一個(gè)同級(jí)窗口

 

GW_HWNDdivV:尋找前一個(gè)同級(jí)窗口

 

GW_OWNER:尋找窗口的所有者(即父窗口)

 

  我們先來理解什么是同級(jí)窗口和頂級(jí)窗口。打個(gè)比方,如果一個(gè)窗口有三個(gè)子窗口,則這三個(gè)窗口都是同一級(jí)的,互為同級(jí)窗口。如果我們從沒尋找過一個(gè)子窗口,那么API 不知道我們要找的是和哪個(gè)窗口同級(jí),那么此時(shí)它找的是頂級(jí)窗口,頂級(jí)窗口即是子窗口,但這個(gè)子的關(guān)系是直接的,而不會(huì)是子窗口的子窗口(即孫子,別笑,這里的術(shù)語不是我自己造的)。最后一個(gè)GetClassName和以前講過的幾個(gè)字符串相關(guān)的API用法差不多,hWnd是窗口句柄,lpClassName是用來接收窗口類名的緩沖區(qū),nMaxCount則是說明緩沖區(qū)的大小。

 

***

那么接下來我是如何用它們的呢?看這里:

 

 

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從桌面上找到了一個(gè)類名為 “Shell_traywnd”的窗口,它就是任務(wù)欄(不要問我是怎么知道它的類名的)。然后我又用GetWindow函數(shù),從任務(wù)欄找到第一個(gè)子窗口。接下來,我用一個(gè)Do…Loop結(jié)構(gòu)的循環(huán)為上一次找到的子窗口檢查其類名,如果類名是button,則說明是個(gè)按鈕,一般來說,任務(wù)欄上只有一個(gè)是button類的,所以一找到,它勢必就是“開始”按鈕了。如果沒找到,則仍使用GetWindow,但這次和第一次不同,我傳遞的不是任務(wù)欄的句柄,而是上一次找到的子窗口的句柄,為的是找下一個(gè)同級(jí)窗口,就這樣一次次循環(huán)直到找到開始按鈕。

 

  那么,開始按鈕就被我這么找到了,然后我就可以像對待其他窗口一樣對待它:比如將它移動(dòng)。不要忘了上一期所講的內(nèi)容,SetWindowPos將在這里產(chǎn)生作用,你可以移動(dòng)它,或者為最后一個(gè)參數(shù)組合上SWP_HIDEWINDOW,讓開始按鈕變得不可見,或者組合SWP_SHOWWINDOW重新顯示……

 

  接下來輪到任務(wù)欄了,你從圖6中可以看到在開始按鈕的位置有另一個(gè)“厲害”的按鈕取代它,這是上一話的內(nèi)容:SetParent。我用SetParent為原本在Form1上的按鈕指定了新的父窗口——任務(wù)欄。如果你查看我的示例源程序,你會(huì)發(fā)現(xiàn)在此按鈕的GotFocus事件中,我把焦點(diǎn)轉(zhuǎn)移給了另一個(gè)按鈕,原因在上一話已經(jīng)說了。

 

  在示例源程序中,我還演示了隱藏和顯示任務(wù)欄,仍然是SetWindowPos的功勞,提醒一下,為了不改變窗口的一些屬性,要在最后一個(gè)參數(shù)組合上合適的值。

 

  好了,這一期的內(nèi)容就這么多,我想這一次你應(yīng)該好好研究我的源程序,里面的東西涉及到上一期和本期的內(nèi)容,把它消化下去吧。

 

 

摘自 田志良 的博文