28. 其他技巧
筆者經常聽到一個對Visual Basic的批評就是Visual Basic不提供指標(Pointer)和結構(Structure),這兩個功能是C程式設計師不可或缺的工具。事實上,我們可以用Visual Basic的Variant型別來建立結構化的資料,也可以用集合物件(Collection)來模仿指標。本章舉一個由Variant變數和Collection物件所建立的仿鏈結串列作為範例。
另外兩個主題是介紹如何偵測作業系統的版本,以及如何從程式中重新啟動電腦。第四節介紹如何在應用程式中撥電話號碼。最後一節則告訴你一個錯誤處理常式的設計方法,這種方式遠優於傳統的On Error GoTo方法。
如何建立鏈結串列?
鏈結串列(Linked List)是C語言中一個非常具有威力的工具,它可以用來建立、管理、和重排序列性的資料。在C語言中建立鏈結串列時,必須依賴指標變數來存放串列元素的位址。Visual Basic雖然沒有明確的指標變數,但它提供了一些技巧可供我們以類似的方式引用物件和變數。Visual Basic實際上在幕後追蹤記錄著指標,但你不需要去操心Visual Basic處理指標的細節,只要注意層次較高的資料處理程序即可。
在下面這個例子中,我們將要利用Collection物件建立一個模仿的字串鏈結串列,你可以在這個串列中插入字串,而串列中的元素仍能保持按字母排列的順序。
請建立一個新專案,在表單中加入一個指令按鈕控制項cmdBuildList,然後加入下面的程式:
Option Explicit
Private colWords As New Collection
Sub Insert(V As Variant)
Dim i As Variant
Dim j As Variant
Dim k As Variant
`Determine whether this is first item to add
If colWords.Count = 0 Then
colWords.Add V
Exit Sub
End If
`Get the range of the collection
i = 1
j = colWords.Count
`Determine whether this should be inserted before first item
If V <= colWords.Item(i) Then
colWords.Add V, before:=i
Exit Sub
End If
`Determine whether this should be inserted after last item
If V >= colWords.Item(j) Then
colWords.Add V, after:=j
Exit Sub
End If
`Conduct binary search for insertion point
Do Until j - i <= 1
k = (i + j) \ 2
If colWords.Item(k) < V Then
i = k
Else
j = k
End If
Loop
`Insert item where it belongs
colWords.Add V, before:=j
End Sub
Private Sub cmdBuildList_Click()
Dim i As Integer
Insert "One"
Insert "Two"
Insert "Three"
Insert "Four"
Insert "Five"
Insert "Six"
Insert "Seven"
Insert "Eight"
Insert "Nine"
Insert "Ten"
For i = 1 To colWords.Count
Print colWords.Item(i)
Next i
End Sub
我們在模組層次宣告ColWords集合物件(Collection)的目的在於使這個物件在程式執行期間都能一直存在。如果在某個程序裡宣告了一個集合物件,那麼當程序結束時,物件就立刻被移除。如果在模組層次中宣告集合物件,只要表單存在,集合物件就會存在,而且所有的程序都可以存取這個集合物件。
集合物件可以包含兩種成員:Object和Variant。Variant變數可以包含各種資料型別的資料,因此集合物件事實上也能包含各種型別的資料。在本例中,我們把要插入到串列中的字串當作Variant型別的資料來處理。Visual Basic的線上手冊很清楚地解釋如何將物件類別模組中的集合物件加以包裝,以便控制可被加入到集合物件中的資料型別。這一點很重要,例如,如果要使用一個在ActiveX元件中的集合物件,你可能希望該集合物件只包含單一型別的資料。然而,在我們的範例中儘管字串以外的資料對程式毫無意義,我們卻不能防止字串以外的資料被傳入Insert程序中。如果程式中能夠控制被傳入的資料,這個物件類別會更完整。
我們可以透過索引值或是關鍵字直接存取物件的成員,這些索引值或關鍵字所代表的是資料成員在串列中的位置。在本例中,我們用索引值來控制成員被存取的順序。集合物件的索引讓你可以插入及刪除成員,以及執行一般真正的鏈結串列所要求的動作。集合物件就像是個鏈結串列(而不像陣列),因為成員的插入及刪除由Visual Basic自動且有效率地處理,不多浪費記憶體空間。
集合物件的Add方法被用來插入字串到集合物件中,Add方法有兩個引數,before和after,讓你可以指明要把資料插在某個特定成員的前面還是後面。在範例中,我們根據字母的順序,利用這兩個引數把字串插入到集合物件裡;這樣,當我們循序地使用索引值來存取集合物件中的成員時,成員才會按照字母順序被取得。圖28-1所顯示的是程式執行的情形。
| 圖28-1 在鏈結串列集合物件中使用插入排序的結果 |
請務必仔細地研讀線上手冊關於集合物件的資訊,這種強大而具無比彈性的結構與Variant資料型別結合,解除了許多過去Basic語言所設下的限制。
如何偵測作業系統版本的不同?
要能夠分辨16位元作業系統和32位元作業系統的差異,你必須用Visual Basic 4或更早版本的Visual Basic來開發16位元的應用程式。以下這段程式能夠根據目前的作業系統版本的不同來決定啟動16位元或32位元的安裝程式:
`LAUNCH.BAS
Option Explicit
#If Win16 Then
Declare Function GetVersion Lib "Kernel" () _
As Long
#Else
Declare Function GetVersion Lib "Kernel32" () _
As Long
#End If
`Demonstrates how to get Windows version information `for 16-bit and 32-bit systems Sub Main()
Dim lWinInfo As Long
Dim strWinVer As String
Dim strDosVersion As String
`Retrieve Windows version information
lWinInfo = GetVersion()
`Parse Windows version number from returned
`Long integer value
strWinVer = LoByte(LoWord(lWinInfo)) & "." & _
HiByte(LoWord(lWinInfo))
`If version number is earlier than 3.5 (Win NT 3.5).
If Val(strWinVer) < 3.5 Then
Shell "Setup1.EXE" `Run 16-bit setup;
Else `otherwise,
Shell "Setup132.EXE" `run 32-bit setup
End If
End Sub
Function LoWord(lArg)
LoWord = lArg And (lArg Xor &HFFFF0000)
End Function
Function HiWord(lArg)
If lArg > &H7FFFFFFF Then
HiWord = (lArg And &HFFFF0000) \ &H10000
Else
HiWord = ((lArg And &HFFFF0000) \ &H10000) Xor &HFFFF0000
End If
End Function
Function HiByte(iArg)
HiByte = (iArg And &HFF00) \ &H100
End Function
Function LoByte(iArg)
LoByte = iArg Xor (iArg And &HFF00)
End Function
我們在本例中採用條件式編譯的方式來編譯GetVersion API函式,這使得我們可以在16位元或32位元環境下都能發展以及測試這個程式。但本程式最終的版本必須用16位元的Visual Basic加以編譯,以產生一個16位元的應用程式。
注意:
如果要在32位元的系統中執行由16位元Visual Basic所編譯的應用程式,系統中必須要安裝16位元的Visual Basic執行時期動態連結程式庫VB40016.DLL。
如何結束以及重新啟動Windows?
從應用程式中重新啟動Windows並不是一件常見的事,但有時候重新啟動系統卻是無可避免的。例如,當應用程式偵測到系統安全性被破壞時,我們可以強迫系統重新啟動,以免系統受到更大的破壞。另外,某些安裝程式在安裝完成後,必須重新啟動系統才能更新目前的路徑和系統登錄。從程式中重新啟動系統很簡單:只要在執行下面這段程式之前,記得把已開啟的檔案儲存起來即可。
如果要試試這個程式,請新增一個專案,在表單上加入一個指令按鈕控制項cmdRestart,加入以下的程式,存檔後即可執行:
Option Explicit
Const EWX_SHUTDOWN = 1
Const EWX_REBOOT = 2
Const EWX_LOGOFF = 0
Const EWX_FORCE = 4
Private Declare Function ExitWindowsEx Lib "user32" _
(ByVal uFlags As Long, _
ByVal dwReserved As Long) _
As Long
Private Sub cmdRestart_Click()
`Restart Windows (works on Windows 95/NT)
ExitWindowsEx EWX_LOGOFF, 0
End Sub
一旦按下了指令按鈕,系統會隨即關閉然後重新啟動。
注意:
Windows NT會要求你的應用程式必須具備特殊的權限才能使用EWX_SHUTDOWN、EWX_REBOOT和EWX_FORCE旗標。有關AdjustTokenPrivileges函式的部份,請參考Windows API函式的技術文件。
如何從應用程式中撥出電話號碼?
設計通訊程式時最常見的一件工作就是撥號處理,我們可以利用MSComm控制項來幫我們完成撥號的工作。MSComm控制項是一個完整、功能強大而且易於使用的控制項,它讓你能夠處理所有序列通訊(Serial Communication)的需求。
第三十一章"日期與時間" 的NISTTime應用程式提供了一個使用MSComm控制項的範例,在這裡,我們要介紹一個簡短的撥號程式。
為了使這個範例可以執行,我們假定電話號碼已經事先被複製到「剪貼簿」裡,這樣你可以從許多其他應用程式中(如Word)複製電話號碼到「剪貼簿」裡,然後使用範例程式。
請建立一個新專案,在空白表單上加入一個指令按鈕cmdDial,然後加入一個MSComm控制項comOne,最後加入下列的程式。
如果在工具箱中找不到MSComm控制項,請在「專案」功能表中選取「設定使用元件」,然後在「設定使用元件」對話方塊中核取Microsoft Comm Control 6.0。
以下是詳細的程式內容:
Option Explicit
Private Sub cmdDial_Click()
Dim strA As String
strA = Clipboard.GetText(vbCFText)
If strA = "" Then
MsgBox "Mark and copy a number first."
Exit Sub
End If
comOne.CommPort = 1
comOne.Settings = "9600,N,8,1"
comOne.PortOpen = True
comOne.Output = "ATDT" & strA & vbCr
MsgBox "Dialing " & strA & vbCrLf & "Pick up the phoneDear John, How Do I... ", _
vbOKOnly, "Dial-A-Phone"
comOne.PortOpen = False
End Sub
圖28-2顯示程式執行的情形。
| 圖28-2 撥號程式正在等待使用者拿起話筒 |
如果你把數據機安裝在COM2而不是COM1,必須把CommPort屬性設為1,其他的屬性可以不必修改。
在PortOpen屬性被設定為False之前,我們用了一個MsgBox來延遲掛斷電話的動作。當使用者按下訊息方塊的「確定」按鈕後,MSComm控制項才會把電話掛掉,這樣使用者才能在斷線之前拿起話筒。
參考資料:
請參閱 第三十一章"日期與時間" 中的NISTTime應用程式,這個應用程式使用了更多MSComm控制項的功能。
如何使用行內錯誤處理技巧?
在Visual Basic程式中最常見的錯誤處理方式是:在程序的開端使用"On Error GoTo標籤"陳述式,讓程式執行權在錯誤發生時跳到標籤下的第一行程式碼,這裡有一個例子:
Private Sub cmdTest_Click()
On Error GoTo ErrorTrap
Print 17 / 0 `(math error)
Exit Sub
ErrorTrap:
Print "Illegal to divide by zero."
Resume Next
End Sub
錯誤處理常式的標籤讓人聯想起老式的程式設計方法──利用GoTo指令改變程式流程,現在的結構化程式已經捨棄這種設計方法了。雖然錯誤處理的GoTo指令不像許多從前的舊Basic程式那樣令人滿頭霧水,但是這種錯誤處理的方法仍然不比其他結構化的方法來得清楚明瞭。尤其是當你在程式中處理物件時,如果用這種錯誤處理方法想要找到錯誤在何處以及為何發生,你一定會覺得無比的困惑。雖然以前的Err變數已經被改良為Err物件,但Err物件所能提供給你的資訊仍然十分有限。
行內錯誤處理(Inline Error Trapping)
行內錯誤處理的原則是:當錯誤發生時,不讓程式執行權跳躍到別處進行錯誤處理。行內錯誤處理即是C程式中標準的錯誤處理方法──每次呼叫過函式之後立即檢查錯誤資訊,若程式執行有錯誤則採取相對的措施。在Visual Basic中我們也可以用這種方式來呼叫API函式,檢查錯誤。
例如,你可以在呼叫mciExcute API函式之後,立刻檢查函式的傳回值,判斷函式的呼叫是否成功:
x = mciExecute("Play c:\windows\tada.wav")
If x = 0 Then MsgBox "There was a problem."
這裡有個通用的方法可以用來建立行內錯誤處理程序,筆者較喜歡這個方法,而且Microsoft也建議,當你在處理物件時,最好也採用這種方法。這個方法的秘訣是在程序的開始處使用On Error Resume Next陳述式,然後在可能發生錯誤的程式碼後面以程式碼檢查是否有錯誤發生。以下這個範例即是使用這種方法:
Option Explicit
Private Sub cmdTest_Click()
Dim vntX As Variant
Dim vntY As Variant
For vntX = -3 To 3
vntY = Reciprocal(vntX)
Print "Reciprocal of "; vntX; " is "; vntY; ""
If IsError(vntY) Then Print Err.Description
Next vntX
End Sub
Function Reciprocal(vntX As Variant)
Dim vntY As Variant
On Error Resume Next
vntY = 1 / vntX
If Err.Number = 0 Then
Reciprocal = vntY
Else
Reciprocal = CVErr(Err.Number)
End If
End Function
我們在程式中運用了Variant可以接受任何型別資料的特性,把CVErr函式的傳回值以Variant型別傳回給原呼叫程序,這是讓原呼叫程序知道有錯誤發生的最佳方法。
在錯誤發生時,我們把傳回的VariantY的內容加以顯示。因為傳回值是一個Variant,因此列印出來的不是數字而是錯誤訊息。
| 圖28-3 行內錯誤處理程序偵測到一個除數為零的錯誤 |
Err物件提供了幾個有用的屬性及方法,這些屬性和方法強化了程式設計師處理錯誤的能力。例如,Err物件的Raise方法讓你建立自訂的錯誤種類,這種高明的技巧可以讓你從你建立的物件中傳回錯誤訊息。