VB 版 (精华区)

发信人: bloom (├┝┞┟┠┢┣), 信区: VB
标  题: VB API教程(王国荣版)(三)(转载)
发信站: 哈工大紫丁香 (2000年09月07日18:42:48 星期四), 转信

【 以下文字转载自 cnTemp 讨论区 】
【 原文由 bloom 所发表 】
发信人: Love1976 (狄飞惊), 信区: VisualBasic       
发信站: BBS 水木清华站 (Thu Apr  6 04:20:22 2000)

发信人: coolknight (酷骑士~找工作中), 信区: VB
标  题: VB 與 Windows API 講座(三)
发信站: 武汉白云黄鹤站 (Tue Nov  9 20:05:05 1999), 站内信件


VB 與 Windows API 講座(三)
 
Windows 的訊息系統
------------------------------------------------------------------------
----
----
/王國榮
VB 程式有兩種工作模式─主動模式及事件驅動模式,主動模式與傳統的 DOS 程式
很像
,程式載入系統後就會一直執行,直到結束為止,事件驅動模式則會在程式載入系
統後
,先暫時停止執行,直到事件發生時,才驅動對應的事件程序而執行某段程式,執
行此
段程式之後,又會暫停執行,等待下一次事件的發生。
 
主動模式的程式在 Windows 3.1 底下是被禁止的,主要的原因是 Windows 3.1 不
是真
正的多工作業系統,而為了讓不同程式能夠同時執行,Windows 規定每一個程式必
須不
定時地呼叫 Yield、GetMessage、PeekMessage 等 API 函數(VB 程式則是呼叫 
DoEven
ts),而當程式呼叫這些函數時,Windows 就會趁此一時機把 CPU 交給其他程式使
用,
也因此讓每一個程式都有機會使用 CPU 而達到多工作業的目的。
 
但如果有某一個程式忽略了此一規定,它就會一直霸佔著 CPU 不放,而使得整個
 Wind
但如果有某一個程式忽略了此一規定,它就會一直霸佔著 CPU 不放,而使得整個
 Wind
ows 的作業環境像當掉一樣,不過這個問題到了 Windows 95 及 NT 之後,已經不
再存
在,因為 Windows 95 及 NT 具有主動切換各個程式使用 CPU 權利的能力,所以
即使某
一程式進入了無窮迴圈,Windows 依然會在一小段時間後將 CPU 的使用權轉移給
其他程
式,我們可以把 Windows 與程式之間的關係表示成圖-1:
 
圖-1 程式使用 CPU 的時間是由 Windows 來主控的
 
儘管主動模式的程式在 Windows 底下不再有問題,但筆者必須強調的是事件驅動
模式才
是 Windows 程式的主流,為什麼呢?在 Windows 的多工作業環境底下,螢幕的輸
出、
鍵盤的輸入、滑鼠的輸入…等,都必須由 Windows 來統籌管理,如果每個程式都
想主動
控制這些輸出與輸入裝置,勢必造成你搶我搶的混亂現象,反觀事件驅動模式是以
物件
為核心,程式的執行是把負責不同工作的物件派到 Windows 的工作環境底下,接
著這些
物件就會靜候 Windows 所產生的事件,然後加以處理,由於事件是由 Windows 統
籌管
理以及產生的,因此不同程式之間便能夠在 Windows 的環境底下共同生存及工作

 
除了從多工作業的角度來看,事件驅動模式的程式也比較容易管理,由於我們會引
用不
同的物件以處理不同的工作,因此便比較不會把某一項工作的程式寫得太大,而造
成將
來偵錯及維護的困難。
 
------------------------------------------------------------------------
----
----
從 VB 的事件回溯到 Windows 的訊息
從 VB 的事件回溯到 Windows 的訊息
------------------------------------------------------------------------
----
----
 
以上是從 VB 的角度來看事件,儘管筆者說 Windows 會產生事件以驅動物件,但
比較正
確的說法是 Windows 先產生訊息,經由 VB 的轉化才成為事件,然後才驅動物件
的,如
圖-2。
 
圖-2 事件的產生乃源自於 Windows 的訊息
 
但是 VB 是如何把訊息轉化成事件的呢?而訊息又是什麼東西呢?請回憶我們在 
46 期
介紹的 hWnd(handle of window),它代表視窗的唯一識別碼,由於許多物件都有
 hWnd
 這個屬性,所以我們可以把物件表示成圖-3,而視窗是 Windows 傳遞訊息的對象
,這
便使得 VB 得以將訊息轉化成事件,進而驅動物件中的事件程序。
 
圖-3 物件與視窗的關係
 
至於 VB 怎樣把訊息轉化成事件的呢?在視窗的內部,有一樣最重要的東西稱為「
視窗
程序」(window procedure),它的用途是接收來自 Windows 的訊息,當 VB 建立
物件(
視窗)時,也會提供一個視窗程序,用以接受來自 Windows 的訊息、處理訊息、並
且會
將某些訊息轉化成為事件,進而驅動物件的事件程序,如圖-4,只是它始終是隱藏
的,
所以對大部分的 VB 程式設計人員來說,並不曉得有這一號人物存在。
 
 
圖-4 將訊息轉化成事件的藏鏡人是視窗程序
 
------------------------------------------------------------------------
----
----
認識視窗程序
------------------------------------------------------------------------
----
----
 
關於視窗程序,如果表示成 VB 的函數,則如下:
 
Function WndProcName(ByVal hWnd As Long, ByVal Msg As Long, ByVal wParam
 As
Long,
ByVal lParam As Long) As Long
    Select Case Msg
        Case WM_???? ' 訊息-1
            ...
        Case WM_???? ' 訊息-2
            ...
        Case Else
            ret = DefWindowProc(hWnd, Msg, wParam, lParam)
    End Select
    WndProcName = ret
    WndProcName = ret
End Function
 
首先來看視窗程序的四個參數:
hWnd:handle of window,這個不必再解釋了吧!
Msg:訊息編號,用來表示 Windows 所傳遞過來的是怎樣的訊息。在 Win32api.
txt 檔
案中,我們可以找到許多以 WM_(表示 Window Message) 為開頭的常數定義,例如
「Pu
blic Const WM_LBUTTONDOWN = &H201」,其中常數符號部分(例如 LBUTTONDOWN 
表示
Left Button Down)可以讓我們望文生義,而常數數值部分(&H201)則是訊息的編號
。而
視窗程序會利用 If Msg = WM_???? 或 Select Case Msg/Case WM_???? 敘述來判
斷收
到的訊息是否為某一種訊息,進而加以處理。
wParam 及 lParam:這兩個參數稱為訊息參數,其意義會隨著訊息(參數二 Msg)的
不同
而有所不同,想瞭解每一種訊息參數的意義,則必須查閱 SDK(Software 
Development
Kits) 參考手冊,不過 SDK 參考手冊蠻貴的,但您可以在以下兩個地方尋得 
SDK 的線
上參考手冊:(1) VC++ (2) MSDN/CD (Microsoft Developer Network/CD)。
 
至於視窗程序之中最值得注意的地方是 DefWindowProc API 函數,這是 
Windows 為了
減少視窗程序的負擔所提供的函數,由於 Windows 可能傳遞給視窗程序的訊息多
達數百
種,而大部分訊息是不需要特別處理的,此時若呼叫 DefWindowProc,可將訊息交
由 A
PI 代為處理。
 
VB 提供的視窗程序
------------------------------------------------------------------------
----
------------------------------------------------------------------------
----
----
 
如果是自己撰寫視窗程序,可以自由地處理收到的訊息,不過在 VB 程式中,視窗
程序
是由 VB 提供的,以下讓筆者先來介紹 VB 所提供的視窗程序是如何處理訊息、以
及如
何產生事件以驅動物件的。
 
剛才筆者說過 Windows 的訊息多達數百種,對 VB 所提供的視窗程序而言,也一
樣不處
理那麼多訊息,所以大部分的情況都是呼叫 DefWindowProc 交由 API 代勞。但由
於 V
B 提供的物件都含有某些事件,因此當 VB 的視窗程序收到訊息時,會判斷此一訊
息是
否應轉化成為事件,例如 LBUTTONDOWN 訊息應轉化成為 MouseDown 事件,如果是
,則
判斷程式中是否含有該事件的事件程序(例如 Form_MouseDown),如果有,則呼叫
此一事
件程序,參考圖-4 就不難瞭解其間的運作過程。
 
筆者覺得視窗程序與事件程序在概念上並沒有太大的差別,但不可否認的 VB 的視
窗程
序會吃掉了許多訊息(也就是說這些訊息並不會產生對應的事件),VB 這麼做也不
是沒有
道理,只保留最為常用的訊息可減少程式設計人員的負擔,而實際上,也可以提升
程式
的執行效能。不過話說回來,因為許多訊息被 VB 吃掉了,這也使得 VB 程式無法
完成
某些特殊的功能,過去 VB 一直被批評為容易使用但功能不足,這是重要的原因,
不過
這種現象到了 VB 5.0 以後,有了重大的改變,請繼續讀下去,稍後筆者就會說明

 
------------------------------------------------------------------------
----
----
----
讓 VB 程式具有 Callback 的能力
------------------------------------------------------------------------
----
----
 
想要強化 VB 的視窗程序是可能的,不過這必須先瞭解 Callback 的工作模式。
 
請回顧圖-4,Windows 會傳遞訊息給視窗程序,但何謂傳遞呢?其實就是 Windows
 準備
好 hWnd、Msg、wParam、lParam 參數,然後呼叫 VB 所提供的函數(視窗程序),
這原本
是沒什麼特別的事情,但由於平常都是由應用程式(或 VB)來呼叫 Windows 的,而
訊息
的傳遞則是由 Windows 倒回來呼叫應用程式,故稱為 Callback。
 
想要強化 VB 的視窗程序首先必撰寫好強化的程序,並且把這個程序設定成 
Windows 可
以 Callback 的程序,此時最重要的一件事情是告訴 Windows 此一程序的「位址
」,但
 VB 4.0 以前的版本卻無法取得程序的位址,因此舉凡與 Callback 有關的程式設
計都
與 VB 程式無緣,不過到5.0 版以後,VB 提供了 AddressOf 敘述可供程式取得程
序的
位址,也因此為 VB 程式開啟了 Callback 的後門。
 
Callback 程式範例
------------------------------------------------------------------------
----
----
 
接下來讓筆者舉個 VB 程式的 Callback 範例,首先請開啟 VB 的線上手冊(選取
 VB 功
接下來讓筆者舉個 VB 程式的 Callback 範例,首先請開啟 VB 的線上手冊(選取
 VB 功
能表的「說明/線上手冊」),然後尋找「AddressOf」關鍵字,在找到的幾個主題
中,請
選取「AddressOf 運算子範例」,然後依照指示將範例移植到程式中,最後執行程
式,
結果此一程式會列舉系統中的字型,並且顯示在表單上的 ListBox 中,如圖-5。
(註:
如果您無法順利將 AddressOf 範例移植到程式中,可進入筆者的網站下載完成的
程式)

 
圖-5 VB5 的 AddressOf 範例
 
此一範例的重點在於 EnumFontFamilies API 函數的呼叫(位於 Module1 的 
FillListW
ithFonts 的副程式),EnumFontFamilies 的用途是列舉系統的所有字型,但由於
系統的
字型數目是可變的,因此 Windows 規定呼叫此一函數時必須傳入一個 Callback 
程序讓
 Windows 把列舉得到的字型一一傳遞給這個程序,為了將 Callback 程序的位址
傳給
Windows,我們可以發現 EnumFontFamilies 的參數三如下:
 
AddressOf EnumFontFamProc
 
作用就是取得 EnumFontFamProc 程序的位址。
 
------------------------------------------------------------------------
----
----
強化 VB 的視窗程序
------------------------------------------------------------------------
----
------------------------------------------------------------------------
----
----
 
由於 VB 的視窗程序是隱藏起來的,所以我們無法改變其中的程式碼,但如果已知
 hWn
d,那麼 Windows 允許我們換掉既有的視窗程序,假設我們想將 Form1 預設的視
窗程序
改成我們所撰寫的視窗程序(假設名稱是 WndProc),則呼叫的 API 如下:
 
ret = SetWindowLong(Form1.hWnd, GWL_WNDPROC, AddressOf WndProc)
 
如此一來,VB 預設給 Form1 的視窗程序就會失效,而取而代之的是 WndProc 視
窗程序
,所以接下來就由 WndProc 來接收 Windows 的訊息了。
 
視窗程序的插隊遊戲
------------------------------------------------------------------------
----
----
 
利用以上方法來改變 Form 的視窗程序,乍看之下,好像還不錯,但實際卻很危險
,為
什麼呢?想想,當我們想換掉某一個視窗程序時,至少要提供與原視窗程序相同的
功能
,而 VB 的視窗程序到底提供了哪些功能,我們並不清楚,所謂破壞容易建設難啊
!解
決此一問題,其根本原理請參考圖-6,作法上是在原視窗程序前面插入另一個視窗
程序
,用以擷取我們希望處理的訊息,進而達到強化原視窗程序的效果,至於不處理的
訊息
,則呼叫 CallWindowProc 將訊息交由原視窗程序來處理,如此一來便不至於破壞
原視
窗程序的功能。此一作法 Windows 稱之為 SubClassing,筆者則稱之為視窗程序
的插隊
窗程序的功能。此一作法 Windows 稱之為 SubClassing,筆者則稱之為視窗程序
的插隊
遊戲,要玩插隊遊戲,還有一些細節要注意。
 
圖-6 在原有的視窗程序之前插入另一個視窗程序
 
記錄原視窗程序的位址
------------------------------------------------------------------------
----
----
 
由於我們必須將不處理的訊息丟回原視窗程序,所以呼叫:
 
ret = SetWindowLong(Form1.hWnd, GWL_WNDPROC, AddressOf WndProc)
 
改變 Form1 的視窗程序之前,必須先記錄原視窗程序的位址,方法如下:
 
Dim prevWndProc As Long
prevWndProc = GetWindowLong(Form1.hWnd, GWL_WNDPROC)
 
傳回值 prevWndProc 即為 Form1 原視窗程序的位址。
 
插隊用視窗程序的寫法
------------------------------------------------------------------------
----
----
----
 
首先假設插隊視窗程序的目的只為了插隊,但插隊之後什麼是也不做,那麼此一視
窗程
序收到訊息時,就原原本本地呼叫原視窗程序,則 WndProc 視窗程序如下:
 
Function WndProc(ByVal hWnd As Long, ByVal Msg As Long, ByVal wParam 
As Long
, ByVal lParam As Long) As Long
WndProc = CallWindowProc(prevWndProc, hWnd, Msg, wParam, lParam)
End Function
 
比較值得注意的是 CallWindowProc 的第一個參數必須傳入原視窗程序的位址 
prevWnd
Proc。
 
如果想要在插隊的視窗程序中處理某些訊息,則結構如下:
 
Function WndProc(ByVal hWnd As Long, ByVal Msg As Long, ByVal wParam 
As Long
,
ByVal lParam As Long) As Long
    Select Case Msg
        Case WM_xxxx
            ... 處理 WM_xxxx 的程式
        Case WM_xxxx
            ... 處理 WM_xxxx 的程式
            ... 處理 WM_xxxx 的程式
        Case Else
        WndProc = CallWindowProc(prevWndProc, hWnd, Msg, wParam, 
lParam)
    End Select
End Function
 
取消插隊行為
------------------------------------------------------------------------
----
----
 
一旦發生插隊的行為,在視窗結束以前,一定要取消插隊行為,否則程式會當掉,
此時
須將原視窗程序的位址設定回 Form1,如下:
 
ret = SetWindowLong(Form1.hWnd, GWL_WNDPROC, prevWndProc)
 
實例解析解說
------------------------------------------------------------------------
----
----
 
最後請直接參考筆者所完成的範例 WndProc.vbp,您可以進入筆者的網站下載此一
程式
,首先請檢視 WndProc.bas 檔案,如下:(筆者省略了 API 函數的宣告)
 
Option Explicit
Option Explicit
Public prevWndProc As Long
Function WndProc(ByVal hWnd As Long, ByVal Msg As Long, ByVal wParam 
As Long
, ByVal lParam As Long) As Long
Debug.Print hWnd, Msg, wParam, lParam
WndProc = CallWindowProc(prevWndProc, hWnd, Msg, wParam, lParam)
End Function
 
在此一 .bas 程式中有幾件事值得注意:
 
Option Explicit 敘述:撰寫呼叫 Windows API 的程式時,最好利用此一敘述禁
止使用
未宣告的變數,因為呼叫 Windows API 時,一旦資料型別不符合就很容易產生錯
誤。
prevWndProc 變數:用來記錄原視窗程序的位址。
視窗程序內容:在此一視窗程序中,暫時不處理任何訊息,因此直接呼叫 
CallWindowP
roc 將訊息傳給原視窗程序,此外也利用 Debug.Print 將視窗程序的幾個參數顯
示出來
,藉以瞭解訊息傳遞給視窗程序的情況。
 
接著請檢視 WndProc.frm 檔案,如下:
 
Option Explicit
Private Sub Form_Load()
    Dim ret As Long
    prevWndProc = GetWindowLong(Me.hWnd, GWL_WNDPROC)
    prevWndProc = GetWindowLong(Me.hWnd, GWL_WNDPROC)
    ret = SetWindowLong(Me.hWnd, GWL_WNDPROC, AddressOf WndProc)
End Sub
Private Sub Form_Unload(Cancel As Integer)
    Dim ret As Long
    ret = SetWindowLong(Me.hWnd, GWL_WNDPROC, prevWndProc)
End Sub
 
此一 .frm 程式的重點在於 Form_Load 及 Form_Unload 事件程序,其中筆者在 
Form_
Load 事件程序中(也就是 Form 載入時),將 WndProc.bas 的 WndProc 視窗程序
插在原
視窗程序之前,而在 Form_Unload 事件程序中則將原視窗程序還原回來。(特別注
意:
想結束程式,請按下 Form 的關閉鈕,不要使用 VB 的結束鈕,否則 Form_Unload
 將不
會被執行到)
 
------------------------------------------------------------------------
----
----
強化視窗程序的實例
------------------------------------------------------------------------
----
----
 
延續前面的 WndProc.vbp 程式,讓我們來觀察兩個強化 VB 原視窗程序的實例。

 
利用 WM_QUERYOPEN 訊息禁止視窗還原
利用 WM_QUERYOPEN 訊息禁止視窗還原
------------------------------------------------------------------------
----
----
 
WM_QUERYOPEN 訊息發生於視窗由「最小化」還原時,在此筆者想利用此一訊息讓
視窗一
直保留在最小化的狀態,首先假設我們不利用此一訊息,而想利用 VB 既有的事件
來達
成相同的目的,那麼想到的方法是在 Form_Resize 事件程序中撰寫以下程式:
 
Private Sub Form_Resize()
    If WindowState <> vbMinimized Then
        WindowState = vbMinimized
    End If
End Sub
 
這一段程式確實可以讓視窗一直保持在最小化的狀態,但使用者將視窗還原時,視
窗會
被先還原,然後才再縮小,結果可以看到視窗被還原然後又被縮小的過程。如果我
們利
用 WM_QUERYOPEN 訊息,則 WndProc 視窗程序只要如下修改即可:
 
Function WndProc(ByVal hWnd As Long, ByVal Msg As Long, ByVal wParam 
As Long
,
ByVal lParam As Long) As Long
    If Msg = WM_QUERYOPEN Then
        ' 吃掉此一訊息,不再傳遞給原視窗程序
        ' 吃掉此一訊息,不再傳遞給原視窗程序
        ' 也就是不讓原視窗程序得到視窗還原的訊息
    Else
        WndProc = CallWindowProc(prevWndProc, hWnd, Msg, wParam, 
lParam)
    End If
End Function
 
以上程式十分簡單,只要在 WM_QUERYOPEN 訊息發生時,不再呼叫原視窗程序,使
得原
視窗程序不知有 WM_QUERYOPEN 訊息發生,於是便不會由最小化還原回來。此一完
成之
程式筆者收錄於 queryopn.vbp 專案中。
 
非顯示區的滑鼠事件
------------------------------------------------------------------------
----
----
 
在 VB 既有的事件中,只含有視窗「顯示區」(client rect)的滑鼠事件,當我們
把滑鼠
移到非顯示區(例如視窗標題區),則收不到 MouseMove 事件,假設我們的需求是
:「滑
鼠移到視窗上面時,即讓視窗變成使用中」,若單純使用事件程序來撰寫程式,則
如下

 
Private Sub Form_MouseMove(Button As Integer, Shift As Integer, X As 
Single,
 Y As Single)
Me.SetFocus
Me.SetFocus
End Sub
 
但這個程式有點小瑕疵,那就是滑鼠移到非顯示區,而沒有移到顯示區時,程式並
沒有
作用,為了讓程式能夠收到滑鼠移到非顯示區的訊息,我們可以撰寫以下的視窗程
序:

 
Function WndProc(ByVal hwnd As Long, ByVal Msg As Long, ByVal wParam 
As Long
,
ByVal lParam As Long) As Long
    Dim ret As Long
    If Msg = WM_NCMOUSEMOVE Then
        Form1.SetFocus
    End If
    WndProc = CallWindowProc(prevWndProc, hwnd, Msg, wParam, lParam)
End Function
 
在以上程式中,判斷 Msg 是否等於 WM_NCMOUSEMOVE 訊息,便可得知使用者是否
將滑鼠
移到了非顯示區,而由於我們不想改變視窗的行為,所以緊接著又呼叫了 
CallWindowP
roc 將訊息傳給原視窗程序。以上完成之程式筆者收錄於 ncfocus.vbp 專案中。

 
------------------------------------------------------------------------
----
----
----
結語:平心而論
------------------------------------------------------------------------
----
----
雖然筆者本期介紹了視窗程序的強化功能,但說實在話,平常自己寫程式時,卻很
少這
麼做,主要原因是筆者覺得搞了一些花樣,未必會得到使用者的認同,倒不如採用
使用
者已經習慣的標準操作方式。這麼說來,筆者本期好像講了半天的廢話,浪費了 
Run!P
C 寶貴的篇幅,其實不然,訊息的運作模式在 Windows 的程式設計中是很重要的
觀念,
要使 Windows API 善盡其用,訊息的運作模式絕對不可不知,除了訊息之外,
Callbac
k 的功能則讓 VB 向前跨了一大步,它除了應用在視窗程序的插隊之外,呼叫許多
 Win
dows API 也少不了它,上一期筆者所寫的「螢幕保護程式」也曾經利用 Callback
 的功
能將偵測滑鼠與鍵盤的程式掛在系統之下,本期雖然沒有介紹什麼花俏的程式,但
建議
您不妨把它當作使用 Windows API 的墊底工作。

--
" The Matrix is everywhere, it's all around us, here even in this room.
 You
 can see it out your window, or  on your television. You feel it when 
you
 go to work, or go to church or pay your taxes. It is the world that has
 been
 pulled over your eyes to blind you from the truth... Unfortunately, 
no one
can be told what the Matrix is. You have to see it for yourself."
                                                                   
Morphe

※ 来源:.武汉白云黄鹤站 bbs.whnet.edu.cn.[FROM: 202.114.3.124]


--
我并不是在等待奇迹,因为我知道没有奇迹的。
有的,也只是爱情、意志和勇气。
是这些东西的重叠后,而成为奇迹的。
所以,我从未曾想过放弃。

※ 修改:·Love1976 於 Apr  6 04:26:20 修改本文·[FROM: 202.112.140.138]

--
☆ 来源:.哈工大紫丁香 bbs.hit.edu.cn.[FROM: blo0m.bbs@smth.org]
--
※ 转载:.哈工大紫丁香 bbs.hit.edu.cn.[FROM: 202.118.247.254]
[百宝箱] [返回首页] [上级目录] [根目录] [返回顶部] [刷新] [返回]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:204.076毫秒