5. Visual Basic的應用程式與函式庫

大致上說來,Microsoft Visual Basic包含了Visual Basic應用程式函式庫,加上一群包含在Visual Basic函式庫和Visual Basic執行時期函式庫的物件。這一章將會著重描述VBA程式的函式和指令,以及一些較高階和較少見的技巧。藉由使用物件瀏覽器工具和選擇VBA函式庫便能瀏覽大部分在此章出現的物件。在本章最後,會介紹一些很重要的系統物件,像是VB函式庫中的App和Clipboard。

控制流程
 

所有的程式語言都必須提供一個以上的方式來執行一些非循序的敘述。除了呼叫副程式和函式外,所有基本控制流程敘述可分成:分支和迴圈敘述。

分支敘述
 

主要的分支敘述為If...Else...Else If...End If區塊。Visual Basic支援數種這類的敘述,包括單行和多行的版本:

' Single line version, without Else clause
If x > 0 Then y = x
' Single line version, with Else clause
If x > 0 Then y = x Else y = 0
' Single line, but with multiple statements separated by colons
If x > 0 Then y = x: x = 0 Else y = 0 

' Multiline version of the above code (more readable)
If x > 0 Then
    y = x
    x = 0 
Else
    y = 0 
End If
' An example of If..ElseIf..Else block
If x > 0 Then
    y = x
ElseIf x < 0 Then
    y = x * x
Else              ' X is surely 0, no need to actually test it.
    x = -1
End If

當IF後面的非零值成立時,會執行Then區塊:

' The following lines are equivalent.
If value <> 0 Then Print "Non Zero"
If value Then Print "Non Zero"

雖然後面可節省些打字功夫,但不該認為如此會讓程式更為快速,至少不一定會。Benchmark顯示若變數型態為Boolean,Integer或Long,則較短的方式並不會使程式執行得更快速。然而,若為其他數值型態,速率會有約百分之二十或更少的增加。如果使用這個技巧讓您感到舒服的話,那就去做吧。但必須瞭解在許多個案上,增進速率不值得減少程式的可讀性。

結合許多AND與OR運算元使得許多進階的最佳化的技術是可行的。下面的例子顯示可藉由改寫布林運算式來撰寫更簡明有效的程式:

' If two numbers are both zero, you can apply the OR operator
' to their bits and you still have zero.
If x = 0 And y = 0 Then ...
If (x Or y) = 0 Then ...

' If either value is <>0, you can apply the OR operator
' to their bits and you surely have a nonzero value.
If x <> 0 Or y <> 0 Then ...
If (x Or y) Then ...

' If two integer numbers have opposite signs, applying the XOR 
' operator to them yields a result that has the sign
'  bit set. (In other words, it is a negative value.)
If (x < 0 And y >= 0) Or (x >= 0 And y < 0) Then ...
If (x Xor y) < 0 Then ...

在程式碼中運作Boolean運算子且不慎引入不知不覺的錯誤時,常令我們感到憤恨不已。例如,你可能會猜想下面兩行的程式碼是相等的,事實不然。(要了解為什麼,想想數字在二進位中如何表示。)

' Not equivalent: just try with x=3 and y=4, whose binary
' representations are 0011 and 0100 respectively.
If x <> 0 And y <> 0 Then ...
If (x And y) Then ...
' Anyway, you can partially optimize the first line as follows:
If (x <> 0) And y Then ...

NOT運算子是另一個常用的運算子,它可以反轉數字裡的所有bits(即0變1,1變0)。在Visual Basic裡,只在其參數為True(-1)的情況下,此運算子才回傳False。因此除了比較Boolean結果或是Boolean變數外,你應從不使用它。

If Not (x = y) Then ...  ' The same as x<>y
If Not x Then ...        ' The same as x<>-1, don't use instead of x=0

若想取得更多相關資訊,可看此章後面的, "Boolean與Bit-Wise運算子" 單元。

許多由其它程式語言轉換到Visual Basic的程式設計者會驚訝If敘述句並不支援所謂的短路評估(short-circuit evaluation)。換言之,Visual Basic即使有足夠的資訊可決定結果是假或真,它仍總是會執行If子句的全部運算式,如下所示:

' If x<=0, it makes no sense to evaluate Sqr(y)>x
' because the entire expression is guaranteed to be False.
If x > 0 And Sqr(y) < z Then z = 0

' If x=0, it makes no sense to evaluate x*y>100.
' because the entire expression is guaranteed to be True.
If x = 0 Or x * y > 100 Then z = 0

雖然Visual Basic不夠聰明,不會自動地最佳化運算式,並不表示無法手動去做。可以參照下面的敘述句來改寫上面的If敘述句:

If x > 0 Then If Sqr(y) < z Then z = 0

可參照下面去改寫上面第二個If敘述句:

If x = 0 Then
    z = 0 
ElseIf x * y > 100 Then
    z = 0
End If

比起If而言,Select Case敘述句只能有一個測試運算式:

Select Case Mid$(Text, i, 1)
    Case "0" To "9"
        ' It's a digit.
    Case "A" To "Z", "a" To "z"
        ' It's a letter.
    Case ".", ",", " ", ";", ":", "?"
        ' It's a punctuation symbol or a space.
    Case Else
        ' It's something else. 
End Select

對於Select Case最佳化的技術大多是把常用到的情況移到區塊的最頂端。例如:在先前例子中,可能想在測試是否為數字前測試其是否為字母。假若正文包含的文字比數字多的話,這樣的改變會加快程式。

出乎意料的,Select Case區塊擁有一個有趣的特點,這在靈活的If敘述句中是缺乏的-其可執行短路評估。事實上,Case子運算式會被執行的情況只在回傳值是True時,之後其它在同一行上其餘的運算式都會被跳過。例如,在先前的程式碼片段中測試標點符號的Case語句,如果讀到的字元是".",則其它所有在該行的測試就不會被執行到。可利用這個有趣的特點改寫(且最佳化)某些由多樣Boolean子運算式所組成的複雜If敘述句。

' This series of subexpressions connected by the AND operator:
If x > 0 And Sqr(y) > x And Log(x) < z Then z = 0
' can be rewritten as:
Select Case False
    Case x > 0, Sqr(y) > x, Log(x) < z
        ' Do nothing if any of the above meets the condition,
        ' that is, is False.
    Case Else
        ' This is executed only if all the above are True.
        z = 0 
End Select
' This series of subexpressions connected by the OR operator:
If x = 0 Or y < x ^ 2 Or x * y = 100 Then z = 0 
' can be rewritten as:
Select Case True
    Case x = 0, y < x ^ 2, x * y = 100
        ' This is executed as soon as one of the above is found
        ' to be True.
        z = 0 
End Select

因為同樣是非正統化的最佳化技巧,建議為自己的程式認真的標上註解,說明你做的事,並總是把原始If敘述句作為註解。這技巧對於加快程式碼的速度是很有效的,但決不要忘記最佳化並不是唯一重要的事,如果你忘記曾經寫過的原始程式碼或讓程式碼看起來很難懂,將會讓維護它的同事們覺得很頭疼。

接著說明GoTo敘述句,許多人視其為瘟疫般地敬而遠之。但筆者必須承認,不管怎樣,對於這四個字並不那麼的否定。事實上,仍可使用一個GoTo敘述句跳出一連串的巢狀迴圈,取代連續的Exit Do或Exit For敘述句。建議:使用GoTo敘述句對於正規流程而言就像是個例外的事物般,且總是在程式碼中,用有意義的標籤名稱及有意義的言辭來解釋所做的事。

GoSub...Return比起GoTo而言是較好些,因為較有結構。在某些情況下,使用GoSub呼叫現行程序內部的一段程式比起呼叫外部的Sub或Function要來的好。既不用傳遞參數也不用接收回傳值;但另一方面,被呼叫的程式共用所有的參數與現行程序中的區域變數,所以在大部分的情況下,不須要傳遞任何東西。然而你得知道,當編譯成執行碼時,GoSub大約比在同一個模組內的外部函式呼叫要慢6到7倍,所以如果正在撰寫對時間很計較的程式碼時,總是要比較這兩種方法。

迴圈敘述
 

在Visual Basic中,毫無疑問的For...Next迴圈是我們最常用的迴圈結構。

For counter = startvalue To endvalue [Step increment]
    ' Statements to be executed in the loop...
Next

如果increment不等於一,就有必要詳細指明Step子句。可用Exit For敘述句離開迴圈,但不幸地,Visual Basic並不提供任何可跳過現行敘述後的部分,且重新開始迴圈的「重複」指定。最好的方法是使用(巢狀) If敘述句,或如果不想讓邏輯太過複雜的話,可使用GoTo指向迴圈的最末端。事實上,這是單一GoTo敘述句可增加程式的可讀性與可維護性的很少數時機之一:

For counter = 1 To 100
    ' Do your stuff here ...
    ' if you want to skip over what follows, just GoTo NextLoop.
    If Err Then Goto NextLoop
    ' more code that you don't want to enclose within nested IF blocks
    ' ...
NextLoop:
Next

小秘訣

如果有增加一個浮點變數的需要時,總是使用Integer或Long變數作為For...Next迴圈內的控制變數,因為其比Single或Double控制變數來得快多了。下個例子會解釋最有效的技術。



注意

避免使用浮點變數作為For...Next迴圈的控制變數的一個理由是因為小數點錯誤。當增加量是分數時,無法確保增加的浮點變數是無誤的,而且迴圈執行此數可能比預期要來得少或更多:

Dim d As Single, count As Long
For d = 0 To 1 Step 0.1
    count = count + 1
Next
Print count         ' Displays "10" but should be "11"

當要完全地確保迴圈的執行次數時,使用整數控制變數,並明確地增加迴圈內的浮點變數:

Dim d As Single, count As Long
' Scale start and end values by a factor of 10 
' so that you can use integers to control the loop.
For count = 0 To 10
    ' Do what you want with the D variable, and then increment it
    ' to be ready for the next iteration of the loop.
    d = d + 0.1
Next

筆者已在第四章有提到For Each...Next迴圈,在這裡不再重複其詳細內容。只想展示一個在此類迴圈與Array函式下的技巧。此技巧允許執行一個有不同控制變數的區塊敘述,且其不需有順序:

' Test if Number can be divided by any of the first 10 prime numbers.
Dim var As Variant, NotPrime As Boolean
For Each var In Array(2, 3, 5, 7, 11, 13, 17, 19, 23, 29)
    If (Number Mod var) = 0 Then NotPrime = True: Exit For
Next

此值並不一定得是數值:

' Test if SourceString contains the strings "one", "two", "three", etc.
Dim var2 As Variant, MatchFound As Boolean
For Each var2 In Array("one", "two", "three", "four", "five")
    If InStr(1, SourceString, var2, vbTextCompare) Then
        MatchFound = True: Exit For
    End If
Next

Do...Loop結構較For.. .Next迴圈有彈性,因為在迴圈的起端或末端可放置終止測試。(對後面而言,迴圈至少會被執行一次。)也可以使用While子句(當測試條件是True則重複)或Until子句(當測試條件是False則重複)。可隨時執行Exit Do敘述,離開Do迴圈,但如同For...Next迴圈-VBA並不提供可跳離迴圈剩餘的敘述句而直接重新開始迴圈的關鍵字。

' Example of a Do loop with test condition at the top.
' This loop is never executed if x <= 0.
Do While x > 0
    y = y + 1
    x = x \ 2
Loop

' Example of a Do loop with test condition at the bottom.
' This loop is always executed at least once, even if x <= 0.
Do 
    y = y + 1
    x = x \ 2
Loop Until x <= 0

' Endless loop: requires an Exit Do statement to get out.
Do
     ...
Loop

While...Wend迴圈在概念上相近於Do While...Loop。但只允許在迴圈的起端測試條件,並沒有Until子句,且也沒有Exit... While指令。因此,大部分的程式設計師寧願選擇較有彈性的Do... Loop結構,事實上,本書您將看不到一個While...Wend迴圈。

其他函式
 

幾個VBA函式接近於控制流程,雖然它們本身並不會改變執行流程。例如, Iif函式,可取代If...Else...End If區塊,如下所示:

' These lines are equivalent.
If x > 0 Then y = 10 Else y = 20
y = IIf(x > 0, 10, 20)

Choose函式可選擇群組中的一個值;可用來識別三個或更多例子。所以可代替程式碼:

' The classic three-choices selection
If x > y Then
    Print "X greater than Y"
ElseIf x < y Then
    Print "X less than Y"
Else
    Print "X equals Y"
End If

可使用這個較短的版本:

' Shortened form, based on Sgn() and Choose() functions.
' Note how you keep the result of Sgn() in the range 1-3.
Print "X " & Choose(Sgn(x _ y) + 2, "less than", "equals", _
    "greater than") & " Y"

Switch函式接受一串(condition, value)值,且回傳第一個condition為True時相對該condition的value。例如,可使用此函式來取代Select Case區塊:

Select Case x
    Case Is <= 10: y = 1
    Case 11 To 100: y = 2
    Case 101 To 1000: y = 3
    Case Else: y = 4
End Select

在剛才那行產生相同的結果。

' The last "True" expression replaces the "Else" clause.
y = Switch(x <= 10, 1, x <= 100, 2, x <= 1000, 3, True, 4)

當使用此函式時,要記得兩件事:第一,如果沒有運算式回傳True的值,則Switch函式會回傳Null。第二,即使只有一個值被回傳,所有運算式還是要被求值。因此,可能得到意想不到的錯誤或不希望得到的副作用。(例如,假設運算式引起溢位或除以零的錯誤。)


注意

雖然IIf、Choose及Switch函式有時有益於減低程式碼數量,但要知道它們總是較同等的If或Select Case結構要來得慢。因此,在時間就是金錢的迴圈中不該使用到它們。


數值的操作
 

Visual Basic提供豐富的數學運算元及函式。其中大部分的運算元意義上是「多形」的,它們可運作於任何型態的參數,包括Integer、Long、Single、Double、Date與Currency。依靠特定運算元或函式,Visual Basic編譯程式能將運算子轉換為更合適的資料型態。然而,這是語言的工作,不用多加操心,因為所有的事情都會為你先自動處理好。

數學運算元
 

如你所知,Visual Basic支援所有四個數學運算元。當組合兩個不同型態的值時,Visual Basic會自動應用強迫型別轉換,並把較窄的型態轉換成較廣的型態(例如,Integer轉換成Long或者是Single轉成Double)。有趣的是,除法運算元(/)總是把它兩個運算子轉換成Double,其會導致某些意想不到的狀況。如果把Integer或Long值拿來被另一個Integer或Long值做除法動作,而且不對商數的小數部分感興趣的話,則應使用執行較快的整數除法(\):

Dim a As Long, b As Long, result As Long
result = a / b          ' Floating point division
result = a \ b          ' This is about 4 times faster.

Visual Basic也支援次方的運算元(^),它可以求出一個數字的次方值。既然這樣,結果總是為Double型態,即使是求一個整數的次方值亦然。一般而言,^運算元相當慢,所以對於小次方數而言,可使用乘法運算的運算式作為替代:

Dim x As Double, result As Double 
x = 1.2345
result = x ^ 3
result = x * x * x      ' This is about 20 times faster.

MOD運算元得到整數值除法後的餘數部分。其常用於測試某數字是否為另一數字的倍數。此運算元非常有效率但卻有限制:其運算子型態被轉換為Long,因而無法用在非常大的數值上。其也截掉任何小數部分。這裡有個處理任意Double值的函式:

Function FPMod(ByVal Number As Double, ByVal divisor As Double) As Double
    ' Note: this differs from MOD when Number is negative.
    FPMod = Number - Int(Number / divisor) * divisor
End Function

當處理數值時,有幾個常被用到的函式:

  • Abs會回傳其參數之絕對值。
     
  • Sgn會回傳-1、0、或+1,假設其參數分別是負數、零、或正數。
     
  • Sqr會回傳數值之平方根。
     
  • Exp傳回以e(自然對數的基底)為底的次方數。
     
  • Log會回傳其參數的自然對數。可使用下面的函式來以10為底的自然對數:
     
Function Log10(Number As Double) As Double 
    Log10 = Log(Number) / 2.30258509299405
End Function

比較運算元
 

Visual Basic支援六個比較運算元,可應用在數值及字串型態上:

=   <   <=   >   >=   <>

這些運算元常被使用在If區塊,但要記住它們在概念上是不同於任何其它數學運算元的,意義上它們接受兩個型態且會傳送結果。這樣的結果會是False(0)或True(-1)。有時可利用這個觀點來撰寫更簡明的程式碼,如下所示:

' The following lines are equivalent.
If x > y Then x = x _ 1
x = x + (x > y)

注意

當在Single及Double值中使用=運算元時總是要謹慎小心,因為當在浮點數值上運算時,Visual Basic常會發生小的循環小數錯誤。例如,請看下面的程式碼:

Dim d As Double, i As Integer

For i = 1 To 10: d = d + 0.1: Next

Print d, (d = 1)        ' Displays "1  False" !!!

前面的結果看起來好像是不合理的,因為變數似乎包括正確的值,但其測試(d=1)回傳False。因為它是循環小數,所以不該信任Visual Basic的Print敘述顯示的資訊。實際上,d變數的值是稍微小於1的值,精確的差別在1.11022302462516E-16(在小數點後跟著15個零),可是卻足以造成等式測試失敗。建議絕不要在浮點數值上做=的運算。這兒有較好的方法:

' "equal" up to 10th decimal digit
Function AlmostEqual(x, y) As Boolean
    AlmostEqual = (Abs(x - y) <= 0.0000000001)
End Function

Boolean與Bit-Wise的運算元
 

Visual Basic的應用程式支援幾個Boolean運算元,特別有助於用來結合多重Boolean子運算式。其中較被常使用的是AND、OR、XOR及NOT。例如,下面的程式碼利用Boolean運算元來確定兩變數的正負號:

If (x > 0) And (y > 0) Then
    ' Both X and Y are positive.
ElseIf (x = 0) Or (y = 0) Then
    ' Either X or Y (or both) are zero.
ElseIf (x > 0) Xor (y > 0) Then
    ' Either X or Y (but not both of them) are positive.
ElseIf Not (x > 0) Then
    ' X is not positive.
End If

要記得,這些運算元實際上是bit-wise運算元,它們可對每個獨立的位元作用。實際上,如果運算子不是Boolean值(意思是說它們的值不是-1與0),則會有差別。可使用AND運算元來測試一個數字的一個或以上的位元:

If (number And 1) Then Print "Bit 0 is set (number is an odd value)"
If (number And 6) = 6 Then Print "Both bits 1 and 2 are set"
If (number And 6) Then Print "Either bits 1 and 2, or both, are set"

使用OR運算元來設定一個或以上的位元:

number = number Or 4          ' Set bit 2. 
number = number Or (8 + 1)    ' Set bits 3 and 0.

可結合AND與NOT運算元來重新設定一個或以上的位元:

Number = number And Not 4     ' Reset bit 2.

最後,XOR運算元可切換一個或以上位元的狀態:

Number = number Xor 2         ' Flip the state of bit 1.

如果不知道在編譯時間哪個位元該被設定、重設,或切換,可使用羃次運算元,如下所示:

Number = Number Or (2 ^ N)    ' Set Nth bit (N in range 0-30).

此方法有兩個缺點:當N=31時,會發生溢位,且由於太過依賴浮點運算,所以效率非常差。可用下面函式解決兩個問題:

Function Power2(ByVal exponent As Long) As Long
    Static result(0 To 31) As Long, i As Integer
    ' Evaluate all powers of 2 only once.
    If result(0) = 0 Then
        result(0) = 1
        For i = 1 To 30
            result(i) = result(i - 1) * 2
        Next
        result(31) = &H80000000        ' This is a special value. 
    End If
    Power2 = result(exponent)
End Function

小數化與整數化
 

Int函式截去小數部分成為整數值,且此整數值等於或小於其參數。和只說「截去數字的小數部分」是不同的。如果參數是負的,則差別就更顯而易見了:

Print Int(1.2)             ' Displays "1"
Print Int(-1.2)            ' Displays "-2"

Fix函式截去數字的小數部分:

Print Fix(1.2)             ' Displays "1"
Print Fix(-1.2)            ' Displays "-1"

Visual Basic 6引入新的數學函式,Round,其可把數值小數點後的幾位數後截掉(如果第二個參數省略的化,就到最接近的整數):

Print Round(1.45)          ' Displays "1"
Print Round(1.55)          ' Displays "2"
Print Round(1.23456, 4)    ' Displays "1.2346"

Round有個未文件化的花招:當小數部分恰好是0.5時,若其整數部分是奇數則進位,若為偶數則消去:

Print Round(1.5), Round(2.5)   ' Both display "2".

這個行為是必要的,以便於當進行統計時可避開錯誤,所以其不該被認為是個錯誤。

當取小數時,有時必須要確定大於或等於參數的最接近的整數,但Visual Basic缺少類似這樣的函式。可使用下面這段簡短的程式來達成這個問題:

Function Ceiling(number As Double) As Long
    Ceiling = -Int(-number)
End Function

轉換不同數字基底
 

VBA支援十進位、十六進位與八進位數字系統:

value = &H1234       ' The value 4660 as a hexadecimal constant
value = &O11064      ' The same value as octal constant

可使用Val函式將任何十六進位或八進位字串轉換成十進位值:

' If Text1 holds a hexadecimal value
value = Val("&H" & Text1.Text)

可做相對的轉換-由十進位到十六進位或八進位-使用Hex及Oct函式:

Text1.Text = Hex$(value)

特別的是,Visual Basic並不包括可轉換成二進位數字或由二進位數字轉換出去的函式,雖然其顯然比八進位更常見。可使用一對建立在Power2函式上的函式即可達到這個轉換,關於Power2函式請參閱本章稍前的章節〈 Boolean與Bit-Wise運算元 〉。

' Convert from decimal to binary.
Function Bin(ByVal value As Long) As String
    Dim result As String, exponent As Integer
    ' This is faster than creating the string by appending chars.
    result = String$(32, "0")
    Do
        If value And Power2(exponent) Then
            ' We found a bit that is set, clear it.
            Mid$(result, 32 - exponent, 1) = "1"
            value = value Xor Power2(exponent)
        End If
        exponent = exponent + 1
    Loop While value
    Bin = Mid$(result, 33 - exponent)  ' Drop leading zeros.
End Function
' Convert from binary to decimal.
Function BinToDec(value As String) As Long
    Dim result As Long, i As Integer, exponent As Integer
    For i = Len(value) To 1 Step -1
        Select Case Asc(Mid$(value, i, 1))
            Case 48      ' "0", do nothing.
            Case 49      ' "1", add the corresponding power of 2.
                result = result + Power2(exponent)
            Case Else
                Err.Raise 5  ' Invalid procedure call or argument
        End Select
        exponent = exponent + 1
    Next
    BinToDec = result
End Function

數值的格式選項
 

VBA語言的所有版本皆包含Format函式,是個強而有力的工具,適合你大部分的格式化需求。其語法有點複雜:

result = Format(Expression, [Format], _
    [FirstDayOfWeek As VbDayOfWeek = vbSunday], _
    [FirstWeekOfYear As VbFirstWeekOfYear = vbFirstJan1])

幸運地,對於大部分的工作而言,前兩個參數就夠用的,除非是要格式化日期,在本章稍後會談到。現在我就來介紹Format函式的多項特性,。雖然我猜你已看過Visual Basic文件內的內容了。

當格式化數值時,Format函式支援「指名格式」與「自訂格式」。指名格式包含以下字串: General Number(沒有特別的格式,必要時使用科學記號法),Currency(貨幣符號,以千為區分子與小數點後兩位),Fixed(小數點後兩位),Standard(以千為區分子與小數點後兩位),Percent(百分率,附加%符號),Scientific(科學記號法),Yes/No、True/False、On/Off(如果是0則為False或Off,否則為True或On)。Format是一個地域化的函式,且會自動在適當的狀況下使用貨幣標誌、千區分子與小數點。

如果指名格式無法完成你要的工作,可使用由特殊字元組成的格式化字串來建立自訂格式。(對於細節與這類格式化字元的意義,請參閱Visual Basic文件。)

' Decimal and thousand separators. (Format rounds its result.)
Print Format(1234.567, "#,##0.00")   ' "1,234.57"
' Percentage values
Print Format(0.234, "#.#%")          ' "23.4%"
' Scientific notation
Print Format(12345.67, "#.###E+")    ' "1.235E+4"
Print Format(12345.67, "#.###E-")    ' "1.235E4"

Format函式特別的地方是,如果數值是正、負、0,或是Null時,皆可支援不同的格式化字串。在自訂格式字串上可使用分號作為每區段的區分。(可指定一個、二個、三個、或四個不同的區段。)

' Two decimal digits for positive numbers, enclose negative numbers within
' a pair of parentheses, use a blank for zero, and "N/A" for Null values.
Print Format(number, "##,###.00;(##,###.00); ;N/A")

Visual Basic 6引入三個新的格式化函式給數值使用-FormatNumber、FormatPercent,與FormatCurrency-借用自VBScript。(另三個函式-FormatDate、MonthName與WeekdayName-在本章稍後的〈處理日期〉中會做解釋。)這些新的函式複製了最強all-in-one的格式化能力,但其語法如下列的程式碼般更淺而易見。

Result = FormatNumber(expr, [DecDigits], [InclLeadingDigit], _
    [UseParens], [GroupDigits] )
result = FormatPercent(expr, [DecDigits], [InclLeadingDigit], _
    [UseParens], [GroupDigits] )
result = FormatCurrency(expr, [DecDigits], [InclLeadingDigit], _
    [UseParens], [,GroupDigits] )

在所有的情況下,DecDigits是你要的小數位數(預設值是2);InclLeading說明在[-1,1]範圍內的數值是否開頭要出現0;UseParens表示負數是否要被括起來;GroupDigits告知是否使用千區分子。最後三個非必要參數每一個都可為:0-vbFalse、-1-vbTrue或-2-vbUseDefault(依據使用者的地區而預設)。如果省略,則表示為vbUseDefault。

亂數
 

有時,需要產生一個或多個亂數。遊戲常使用到此功能,但這功能也應用在包含模擬的商業應用上。Visual Basic只提供一個敘述句及函式來產生亂數。可使用Randomize敘述句初始化內部亂數產生器的種子。可傳給他一個數值作為亂數種子;Visual Basic會自動使用Timer函式傳回的值:

Randomize 10

每次呼叫Rnd函式,其會回傳一個亂數。此回傳值總是小於1且大於或等於0,因此為了得到想要範圍內的數值,必須對結果進行處理:

' Simple computerized dice Randomize
For i = 1 To 10
    Print Int(Rnd * 6) + 1
Next

有時,可能要一直重複一連串相同的亂數,特別是在對程式碼進行除錯時。似乎可以相同種子呼叫Randomize敘述的方法而獲得這樣的作用,但這是行不通的。要重複相同的亂數序列,則呼叫Rnd函數時給予一個負值作為參數。

dummy = Rnd(-1)            ' Initialize the seed. (No Randomize is needed!)
For i = 1 To 10            ' This loop will always deliver the same
    Print Int(Rnd * 6) + 1 ' sequence of random numbers.
Next

也可以藉著傳入0給Rnd當作參數來重新載入剛產生的亂數。

一般亂數的常用作用為產出特定範圍內數字的變換:舉例來說,在卡片遊戲中將會很有用。這裡有個簡易且有效的範例,會回傳介於first和last中的所有長整數陣列,並以亂數排列:

Function RandomArray(first As Long, last As Long) As Long()
    Dim i As Long, j As Long, temp As Long
    ReDim result(first To last) As Long
    ' Initialize the array.
    For i = first To last: result(i) = i: Next
    ' Now shuffle it.
    For i = last To first Step -1
        ' Generate a random number in the proper range.
        j = Rnd * (last - first + 1) + first
        ' Swap the two items.
        temp = result(i): result(i) = result(j): result(j) = temp
    Next
    RandomArray = result
End Function

字串的處理
 

VBA包含許多強大的字串函式,在第一次掃視過時難以確定哪個是適合所需的。在此節,筆者簡單地描述你可使用的所有字串函式,在某些一般情況下,提供選擇最合宜函式的提示,並且提供一些可在應用程式內重複使用的字串函式。

基本字串運算元及函式
 

基本字串運算元"&"執行字串連結的動作。結果是由第一個字串的所有字元加上第二個字串的所有字元組成:

Print "ABCDE" & "1234"       ' Displays "ABCDE1234"

許多來自QuickBasic的程式設計者仍然使用"+"運算元來執行字串連結。這是很危險的,會影響程式碼的可讀性,且當其中任一個運算子不為字串時,可能會引起無法預期的變化。

下面列出些較普遍的字串函式,其中包含Left$、Right$ 及Mid$,會由原始字串的開始、結尾,或者是中間截取出子字串。

Dim text As String
text = "123456789"
Print Left$(text, 3)         ' Displays "123"
Print Right$(text, 2)        ' Displays "89"
Print Mid$(text, 3, 4)       ' Displays "3456"

小秘訣

VBA文件一貫地皆省略所有字串函數的$字元,而使用少了$的新函式。別這樣做!少了$的函式回傳的包含字串結果的Variant,這表示在它可被重新使用於運算式或指派為String變數前,Variant必須被轉變為字串。非正式的評估顯示,例如Left$ 函式就比少了$的相同函式快兩倍。同樣地,其它函式都有兩個形式,包括:Lcase、Ucase、Rtrim、Trim、Chr、Format、Space以及String。


Mid$ 也可像指令般,讓你修改字串內的數個字元:

Text = "123456789"
Mid$(Text, 3, 4) = "abcd"    ' Now Text = "12abcd789"

Len函式傳回現行字串的長度。常用來測試字串中是否包含任何字元:

Print Len("12345")          ' Displays "5"
If Len(Text) = 0 Then ...   ' Faster than comparison with an empty string.

可使用Ltrim$、Rtrim$ 及Trim$ 函式,去掉結尾或開頭不要的空白處:

Text = "  abcde  "
Print LTrim$(Text)           ' Displays "abcde  "
Print RTrim$(Text)           ' Displays "  abcde"
Print Trim$(Text)            ' Displays "abcde"

這些函式對於固定長度的字串特別有用,固定長度字串會補滿空白以便使字串長度保持固定。可利用Rtrim$ 函式來削減那些額外空白:

Dim Text As String * 10
Text = "abcde"               ' Text now contains "abcde     ".
Print Trim$(Text)            ' Displays "abcde"

注意

當宣告固定長度字串,卻尚未使用它時,則它包含了Null字元,而非空白。這表示Rtrim$ 函式無法削減這類的字串:

Dim Text As String * 10
Print Len(Trim$(Text))       ' Displays "10", no trimming has occurred.

要避開這樣的問題,可在其宣告後及使用它們前分配一個空字串給應用程式內的所有固定長度字串。


Asc函式傳回字串內第一個字母的字元碼。其類似於使用Left$ 函式來抽出第一個字元,但Asc函式比它快多了。

If Asc(Text) = 32 Then        ' Test whether the fist char is a space.
If Left$(Text, 1) = " " Then  ' Same effect, but 2 to 3 times slower

使用Asc函式時應該確認此字串不是空的,因為這樣會引發錯誤。就某種意義來說,和Asc函式相對的Chr$ 函式會將數值碼轉換為相對應的字元:

Print Chr$(65)                ' Displays "A"

Space$ 和String$ 函式很相似。前者回傳長度自訂的空白字串,而後者回傳一個字串,此字串長度由第一個參數所指定,而字串內容則一再重複第二個參數:

Print Space$(5)               ' Displays "     " (five spaces)
Print String$(5, " ")         ' Same effect
Print String$(5, 32)          ' Same effect, using the char code
Print String$(50, ".")        ' A row of 50 dots

最後StrComp函式用來比較字串,且若第一個參數小於、等於或大於第二個參數時,依序回傳-1、0或1。第三個參數表示是否該已沒有分大小寫的方式比對:

Select Case StrComp(first, second, vbTextCompare)
    Case 0
        ' first = second   (e.g. "VISUAL BASIC" vs. "Visual Basic")
    Case -1
        ' first < second   (e.g. "C++" vs. "Visual Basic")
    Case 1
        ' first > second   (e.g. "Visual Basic" vs. "Delphi")
End Select

StrComp函式有時對於區分大小寫的比對方式滿方便的,因為不需要兩次不同的測試來決定一個字串是否小於、等於或大於另一個字串。

轉換函式
 

最常用到的字串轉換函式是UCase$ 和LCase$,他們會分別轉換字串為大寫或小寫:

Text = "New York, USA"
Print UCase$(Text)                 ' "NEW YORK, USA"
Print LCase$(Text)                 ' "new york, usa"

StrConv函式包含了前兩者的功能,並加入更多功能。可用它來轉換大寫、小寫和適當大小寫(每個單字的第一個字母為大寫,其餘為小寫):

Print StrConv(Text, vbUpperCase)   ' "NEW YORK, USA"
Print StrConv(Text, vbLowerCase)   ' "new york, usa"
Print StrConv(Text, vbProperCase)  ' "New York, Usa"

(合法的字串分隔元有空格、Null字元、跳行和回行首等。)此函式還能使用vbUnicode和vbFromUnicode參數來進行ANSI和Unicode間的轉換。但在標準的Visual Basic應用程式中很少使用這些函式。

Val函式能夠轉換字串為十進位數字。(可參閱前頭的 〈轉換數值的基底〉 。)Visual Basic還有能將字串轉換為數值的函式,如CInt、CLng、CSng、CDbl、Ccu r和CDate。他們和Val函式主要的差別為他們是地域化的。舉例來說,他們能正確地辨識出逗號為某些國家的小數點而不當成為千位數的分隔字元。相反地,Val函式僅能辨識小數點,而當發現不合法的字元時即停止運作(包括貨幣符號或用於千位區隔的逗號)。

Str$ 函式能轉換數值為字串。跟CStr主要的差別在於前者會在參數為正的情況下增加一個前導空白,而後者則不會。

尋找和取代字串
 

InStr函式會在一個字串內尋找一段子字串,無論有沒有分大小寫。若要傳遞參數指示要使用哪種搜尋時,不能省略開始註標值:

Print InStr("abcde ABCDE", "ABC")     ' Displays "7" (case sensitive)
Print InStr(8, "abcde ABCDE", "ABC")  ' Displays "0" (start index > 1)
Print InStr(1, "abcde ABCDE", "ABC", vbTextCompare)
                                      ' Displays "1" (case insensitive)

InStr函式易於建立其他VBA語言不提供的強大字串函式。例如:某函式要在一個搜尋表中尋找字元第一次出現的地方。對於分解被許多不同分隔字元所區分的字串是很有用的。

Function InstrTbl(source As String, searchTable As String, _
    Optional start As Long = 1, _
    Optional Compare As VbCompareMethod = vbBinaryCompare) As Long
    Dim i As Long
    For i = start To Len(source)
        If InStr(1, searchTable, Mid$(source, i, 1), Compare) Then
            InstrTbl = i
            Exit For
        End If
    Next
End Function

Visual Basic 6可利用新加入的InStrRev函式來執行反向搜尋。其語法類似最初的InStr函式,但其參數順序卻不同:

found = InStrRev(Source, Search, [Start], [CompareMethod])

這兒有幾個例子。說明如果省略start參數時,將會由字串的最末端開始搜尋:

Print InStrRev("abcde ABCDE", "abc")    ' Displays "1" (case sensitive)
Print InStrRev("abcde ABCDE", "abc", ,vbTextCompare )  
                                        ' Displays "7" (case insensitive)
Print InStrRev("abcde ABCDE", "ABC", 4, vbTextCompare )
                            ' Displays "1" (case insensitive, start<>0)

Visual Basic也包含易於操作的字串運算元,當分析字串與執行複雜的搜尋動作時,Like運算元往往扮演救生員的角色。其語法如下:

result = string Like pattern

其中string為要分析的字串,而pattern是定義搜尋條件,而由特殊字元組成的字串。最常使用的特殊字元有?(任何一個字元),*(一個或多個字元)和#(任何一位數字)。這裡有些例子:

' The Like operator is affected by the current Option Compare setting.
Option Compare Text                 ' Enforce case-insensitive comparisons.
' Check that a string consists of "AB" followed by three digits.
If value Like "AB###" Then ...      ' e.g. "AB123" or "ab987"
' Check that a string starts with "ABC" and ends with "XYZ".
If value Like "ABC*XYZ" Then ...    ' e.g. "ABCDEFGHI-VWXYZ"
' Check that starts with "1", ends with "X", and includes 5 chars.
If value Like "1???X" Then ...      ' e.g. "1234X" or "1uvwx"

也可藉由插入一列由中括弧括起來的字串來表示哪些字元是要被包含(或排除)在搜尋內的。

' One of the letters "A","B","C" followed by three digits
If value Like "[A-C]###" Then ...           ' e.g. "A123" or "c456"
' Three letters, the first one must be a vowel
If value Like "[AEIOU][A-Z][A-Z]" Then...  ' e.g. "IVB" or "OOP"
' At least three characters, the first one can't be a digit.
' Note: a leading "!" symbol excludes a range.
If value Like "[!0-9]??*" Then ...  ' e.g. "K12BC" or "ABHIL"

Visual Basic 6引入新的Replace函式,可迅速的搜尋並取代子字串。因為包含了幾個選擇性參數,使其語法看起來並不那麼簡單:

Text = Replace(Source, Find, Replace, [Start], [Count], [CompareMethod])

最簡單的模式可以不分大小寫方式進行搜尋,且取代所有找到的字串:

Print Replace("abc ABC abc", "ab", "123")         ' "123c ABC 123c"

依照其餘的參數,可以由不同的位置開始搜尋,限定替代次數,與進行大小寫區分的搜尋。記住start值大於1時,會在搜尋開始前,截掉source參數的前後端空白:

Print Replace("abc ABC abc", "ab", "123", 5, 1)                ' "ABC 123c"
Print Replace("abc ABC abc", "ab", "123", 5, 1, vbTextCompare) ' "123C abc"

也可用Replace函式來計算字串內子字串出現的次數:

Function InstrCount(Source As String, Search As String) As Long
    ' You get the number of substrings by subtracting the length of the 
    ' original string from the length of the string that you obtain by 
    ' replacing the substring with another string that is one char longer.
    InstrCount = Len(Replace(Source, Search, Search & "*")) - Len(Source)
End Function

新的StrReverse函式可快速的顛倒字串內字元的順序。此函數本身沒啥用處,但可對其他字串處理函式而言卻有價值:

' Replace only the LAST occurrence of a substring.
Function ReplaceLast(Source As String, Search As String, _
    ReplaceStr As String) As String
        ReplaceLast = StrReverse(Replace(StrReverse(Source), _
            StrReverse(Search), StrReverse(ReplaceStr), , 1))
End Function

Split函式可找出字串內所有的區隔的項目。其語法如下:

arr() = Split(Source, [Delimiter], [Limit], [CompareMethod])

其中delimiter表示區隔各自項目的字元。如果不想要過多項目,可傳給limit參數一個正值,且可讓最後一個參數值為vbTextCompare值表示執行大小寫不分的搜尋。由於預設的區隔字元為空白,可使用下列程式輕易地擷取句子中的所有單字:

Dim words() As String
words() = Split("Microsoft Visual Basic 6")
' words() is now a zero-based array with four elements.

Join函式與Split函式是互補的,因為它接受一個字串陣列與一個區隔字元,並重建原始字串:

' Continuing the preceding example ...
' The delimiter argument is optional here, because it defaults to " ".
Print Join(words, " ")       ' Displays "Microsoft Visual Basic 6"

記住Split與Join函式中的delimiter參數可多於一個字元。

Filter函式是另一個在VBA語言中受歡迎的函式,它可以很快地掃描一個尋找子字串的陣列,並回傳另一個包含(或不包含)搜尋子字串項目的陣列。其語法如下:

arr() = Filter(Source(), Search, [Include], [CompareMethod])

如果Include參數為True或省略,則結果陣列包括所有在source中包含search子字串的項目;若為False,則產生的陣列只包含不含search的項目。如往常,CompareMetod參數表示是否搜尋區分大小寫:

ReDim s(2) As String
s(0) = "First": s(1) = "Second": s(2) = "Third"
Dim res() As String
res = Filter(s, "i", True, vbTextCompare)
' Print the result array  ("First" and "Third").
For i = 0 To UBound(res): Print res(i): Next

若在原陣列中沒有項目合乎尋找的要求,當傳給Ubound函式時,Filter函式會傳送一個回傳值-1的特別陣列。

字串的格式選擇
 

你可以使用Format函式來格式化字串。只可以指定一個自訂格式(對於字串資料,沒有指名格式可用),且只有有限的特殊字元,但至少可得到許多彈性。可指定兩個區段,一個用於非空的字串值,一個用於空的字串值,如下所示:

' By default, placeholders are filled from right to left.
' "@" stands for a character or a space, "&" is a character or nothing.
Print Format("abcde", "@@@@@@@")                          ' "  abcde"
' You can exploit this feature to right align numbers in reports.
Print Format(Format(1234.567, "Currency"), "@@@@@@@@@@@") ' "  $1,234.57"
' "!" forces left to right fill of placeholders.
Print Format("abcde", "!@@@@@@@")                         ' "abcde  "
' ">" forces to uppercase, "<" forces to lowercase.
Print Format("abcde", ">& & & & &") ' "A B C D E"
' This is a good way to format phone numbers or credit-card numbers.
Print Format("6152127865", "&&&-&&&-&&&&")                ' "615-212-7865"
' Use a second section to format empty strings.
' "\" is the escape character.
Print Format("", "!@@@@@@@;\n\o\n\e")

日期和時間的運作
 

Visual Basic不僅可用特別的Date資料型態來儲存資料及時間資訊,也提供許多關於日期與時間的函數。這些函數在商業應用上相當重要,因此該深入的探討。

取得和設定現在日期與時間
 

嚴格地說,Date與Time並非是函式:他們是屬性。事實上,可使用它們來取得現在的日期與時間(為Date型態),或指派新值給它們來更新系統設定:

Print Date & " " & Time         ' Displays "8/14/98 8:35:48 P.M.".
' Set a new system date using any valid date format.
Date = "10/14/98"
Date = "October 14, 1998"

說明

為了幫助比較所有日期和時間的函式的結果,本節全部的例子假定日期時間為先前程式片段所顯示的:August 14, 1998, 8:35:48 p.m.。


舊的Date$ 和Time$ 屬性也可做同樣的工作。然而它們是String屬性,因此僅認得mm/dd/yy或mm/dd/yyyy格式和hh:mm:ss和hh:mm格式。基於這個理由,通常會使用新的無$函式。

Now函式會回傳現在的日期和時間值(Date型態):

Print Now                       ' Displays "8/14/98 8:35:48 P.M.".

Timer函式回傳從零時起算的秒數,且比Now精準,因為Timer函式包含秒數的小數部份。(實際精確度依照系統而定。)此函式常用在評估程式的執行速度:

StartTime = Timer
' Insert the code to be benchmarked here.
Print Timer - StartTime

先前的程式有些不準確:StartTime變數可能會在系統時鐘將過時前就被指定,所以程序可能比實際上更慢。這裡有個較好的方法:

StartTime = NextTimerTick
' Insert the code to be benchmarked here.
Print Timer _ StartTime

' Wait for the current timer tick to elapse.
Function NextTimerTick() As Single
    Dim t As Single
    t = Timer
    Do: Loop While t = Timer
    NextTimerTick = Timer
End Function

如果在程式中使用Timer函式,應要注意時間在零時會重設,所以貿然引入可能會造成潛在的嚴重錯誤。例如試著增加與CPU無關的暫停可能導致錯誤:

' WARNING: this procedure has a bug.
Sub BuggedPause(seconds As Integer)
    Dim start As Single
    start = Timer
    Do: Loop Until Timer _ start  >= seconds
End Sub

錯誤其實很少出現-舉個例子,如果程式在11:59:59 p.m.要求兩秒鐘的暫停。即使可能性很少,這小錯誤的影響還是很大的,且必須按下Ctrl+Alt+Del來刪掉編譯好的應用程式。這裡有個解決方法:

' The correct version of the procedure
Sub Pause(seconds As Integer)
    Const SECS_INDAY = 24! * 60 * 60    ' Seconds per day
    Dim start As Single
    start = Timer
    Do: Loop Until (Timer + SECS_INDAY - start) Mod SECS_INDAY >= seconds
End Sub

建立和取得日期時間值
 

有很多方法可以組合Date值。例如,使用Date常數,如下所示:

StartDate = #8/15/1998 9:20:57 PM#

但更多時候可以從很多VBA函式其中之一來建立Date值。DateSerial函式會建立年/月/日的日期值;同樣地,TimeSerial函式能建立時/分/秒的時間值。

Print DateSerial(1998, 8, 14)          ' Displays "8/14/98"
Print TimeSerial(12, 20, 30)           ' Displays "12:20:30 P.M."
' Note that they don't raise errors with invalid arguments.
Print DateSerial(1998, 4, 31)          ' Displays "5/1/98"

DateSerial函式也能夠有效間接地決定某年是否為閏年:

Function IsLeapYear(year As Integer) As Boolean
    ' Are February 29 and March 1 different dates?
    IsLeapYear = DateSerial(year, 2, 29) <> DateSerial(year, 3, 1)
End Function

DateValue和TimeValue函式會回傳其參數的日期或時間部分,可以是以字串或日期表示:

' The date a week from now
Print DateValue(Now + 7)            ' Displays "8/21/98"

VBA函式集能從日期表示式或變數取得日期和時間的資訊。Day、Month和Year函式會回傳日期值,如同Hour、Minute和Second函式能回傳時間值:

' Get information about today's date.
y = Year(Now): m = Month(Now): d = Day(Now)
' These functions also support any valid date format.
Print Year("8/15/1998 9:10:26 PM")    ' Displays "1998"

Weekday函式回傳介於1到7的數字,其值對應到給定Date參數的星期別:

Print Weekday("8/14/98")           ' Displays "6" (= vbFriday)

Weekday函式在日期為一週的第一天時會回傳1。此函式是地域化的,這表示在不同的Microsoft Windows下能判斷其一週的第一天(有些並非為vbSunday)。在大部分情況下,這種情況不會影響程式碼的結構。但如要確定1表示星期天、2表示星期一等等,可強迫在不同Windows系統皆傳回固定值,如下:

Print Weekday(Now, vbSunday)

雖然使用選擇性的第二個參數強迫函式回傳正確值,但並不會改變系統的地域性。如果再呼叫Weekday函式而不使用第二個參數,它將仍然視一週的第一天如同以往。

最後使用DatePart函式可從日期值或表示式中取得任何日期和時間的資訊,其語法為:

Result = DatePart(Interval, Date, [FirstDayOfWeek], [FirstWeekOfYear])

通常很少使用這個函式,因為使用之前的其他函式就能做到大部分的功能。然而在底下兩個案例中,此函式就真的很有用:

' The quarter we are in
Print DatePart("q", Now)           ' Displays "3"
' The week number we are in (# of weeks since Jan 1st)
Print DatePart("ww", Now)          ' Displays "33"

第一個參數可為表5-1所列的字串。更多關於這二個選擇性參數,請參閱下節 DateAdd函式 的介紹。

設定 說明
"yyyy" 西元年
"q" 季節
"m" 月數
"y" 一年中的天數(同d)
"d" 天數
"w" 星期別
"ww" 星期
"h" 小時
"n"
"s"
表5-1 DatePart、DateAdd和DateDiff函式中interval參數可能的值。

日期計算
 

在大部分情況下,不需要任何特別的函式來進行日期計算。所有需要知道的Date變數的整數部份包含日期資訊,而小數部分包含時間資訊:

' 2 days and 12 hours from now
Print Now + 2 + #12:00#        ' Displays "8/17/98 8:35:48 A.M."

要計算日期,可使用DateAdd函式,其語法如下所示:

NewDate = DateAdd(interval, number, date)

Interval是表示日期或時間單位的字串(見表5-1),number是所要增加的單位數量,而date則為起始日期。可使用此函式來增減日期和時間值:

' The date three months from now
Print DateAdd("m", 3, Now)            ' Displays "11/14/98 8:35:48 P.M."
' One year ago (automatically accounts for leap years)
Print DateAdd("yyyy", -1, Now)        ' Displays "8/14/97 8:35:48 P.M."
' The number of months since Jan 30, 1998
Print DateDiff("m", #1/30/1998#, Now)       ' Displays "7"
' The number of days since Jan 30, 1998 _ you can use "d" or "y".
Print DateDiff("y", #1/30/1998#, Now)       ' Displays "196"
' The number of entire weeks since Jan 30, 1998
Print DateDiff("w", #1/30/1998#, Now)       ' Displays "28"
' The number of weekends before 21st century - value <0 means
' future dates.
' Note: use "ww" to return the number of Sundays in the date interval.
Print DateDiff("ww", #1/1/2000#, Now)       ' Displays "-72"

當擁有兩個日期,且想要計算兩者個差距時就是兩者相差的時間應該使用DateDiff函式,其語法為:

Result = DateDiff(interval, startdate, enddate _
    [, FirstDayOfWeek[, FirstWeekOfYear]])

其中interval意義顯示如表5-1所示,FirstDatOfWeek是個用來指定星期幾為一週的開始的選擇性參數(可用vbSunday、vbMonday常數等等),而FirstWeekOfYear為另一個能指定哪週是一年的開始週的選擇性參數。(見表5-2)

常數 說明
vbUseSystem 0 使用NLS API設定。
vbFirstJan1 1 第一週包含為包含一月一日的那週。(這是此設定的預設值。)
vbFirstFourDays 2 第一週是在新的一年中擁有最少四天的第一個星期。
VbFirstFullWeek 3 第一週是第一個完整包含在新的一年的第一個星期。
表5-2 DateDiff函式中FirstWeekOfYear可能的值

日期和時間值的格式化選擇
 

最重要與最有彈性的格式化日期和時間值的函式為Format函式。此函式有七個關於日期時間不同的指名格式:

  • General Date(標準的日期和時間格式;若小數部分為0時只有日期;若整數部份為0時則只有時間)
     
  • Long Date(例如,Friday, August 14, 1998,但其結果依地域性而定)
     
  • Medium Date(例如,14-Aug-98)
     
  • Short Date(例如,8/14/98)
     
  • Long Time(例如,8:35:48)
     
  • Medium Time(例如,8:35 A.M.)
     
  • Short Time(例如,24小時制的8:35)
     

也可用一些特別字元來自訂日期和時間格式的字串,包括一個或兩個數字的日數和月數,完整或其縮寫的月份和星期名稱,a.m/p.m.顯示,星期別和四季數字等等:

' mmm/ddd = abbreviated month/weekday,
' mmmm/dddd = complete month/weekday
Print Format(Now, "mmm dd, yyyy (dddd)")  ' "Aug 14, 1998 (Friday)"
' hh/mm/ss always use two digits, h/m/s use one or two digits
Print Format(Now, "hh:mm:ss")             ' "20:35:48"
Print Format(Now, "h:mm AMPM")            ' "8:35 P.M."
' y=day in the year, ww=week in the year, q=quarter in the year
' Note how a backslash can be used to specify literal characters.
Print Format(Now, "mm/dd/yy (\d\a\y=y \w\e\e\k=ww \q\u\a\r\t\e\r=q)")
                      ' Displays "08/14/98 (day=226 week=33 quarter=3)"

Visual Basic 6引進新的FormatDateTime函式。它遠少於標準的Format函式要來得沒彈性,且僅允許部分的Format函式的指名格式。 ForamtDateTime函式唯一的優點即為VBScript也支援它,且讓Visual Basic與VBA到VBScript間的轉換很容易。它的語法為:

result = FormatDateTime(Expression, [NamedFormat])

其中VamesFormat可以是下列常數之一:0-vbGeneralDate (預設值)、1-vbLongDate、2-vbShortDate、3-vbLongTime或4-vbShortTime。這裡有些例子:

Print FormatDateTime(Now)                 ' "8/14/98 8:35:48 P.M."
Print FormatDateTime(Now, vbLongDate)     ' "Saturday, August 15, 1998"
Print FormatDateTime(Now, vbShortTime)    ' "20:35"

Visual Basic 6也提供兩個關於日期格式的新函式。MonthName函式會回傳完整或縮寫的月份名,而WeekdatName函式則回傳完整或縮寫的星期名稱。兩者都是地域化的,所以可用它們來列出作業系統語言設定的月份和星期名稱:

Print MonthName(2)                           ' "February"
Print MonthName(2, True)                     ' "Feb"
Print WeekdayName(1, True)                   ' "Sun"

檔案的處理
 

Visual Basic總是擁有許多強大的指令來處理文字和二進位檔案。雖然Visual Basic 6沒有擴充內建函式集,但它藉由增加新鮮且有趣的FileSystemObject物件來處理檔案和目錄,而間接地延伸語言的功能。本節介紹所有關於檔案的VBA函式和敘述的總覽,包含許多有用的技巧,以便讓您能獲得最多從而遠離一再發生的問題。

操作檔案
 

一般來說,不開啟檔案則無法做其他相關的事。但Visual Basic能刪除一個檔案(使用Kill指令),移動或更改名稱(使用Name...As指令)和複製到別處(使用FileCopy指令):

' All file operations should be protected against errors.
' None of these functions works on open files.
On Error Resume Next
' Rename a file--note that you must specify the path in the target,
' otherwise the file will be moved to the current directory.
Name "c:\vb6\TempData.tmp" As "c:\vb6\TempData.$$$"
' Move the file to another directory, possibly on another drive.
Name "c:\vb6\TempData.$$$" As "d:\VS98\Temporary.Dat"
' Make a copy of a file--note that you can change the name during the copy
' and that you can omit the filename portion of the target file.
FileCopy "d:\VS98\Temporary.Dat", "d:\temporary.$$$"
' Delete one or more files--Kill also supports wildcards.
Kill "d:\temporary.*"

使用GetAttr和SetAttr函式分別可以讀取和修改檔案的屬性。GetAttr函式會回傳一個bit-code值,所以需要藉由VBA內部的常數來測試各位元值。這裡有個可重用函式可以建立關於所有檔案屬性的描述字串:

' This routine also works with open files
' and raises an error if the file doesn't exist.
Function GetAttrDescr(filename As String) As String
    Dim result As String, attr As Long
    attr = GetAttr(filename)
    ' GetAttr also works with directories.
    If attr And vbDirectory Then result = result & " Directory"
    If attr And vbReadOnly Then result = result & " ReadOnly"
    If attr And vbHidden Then result = result & " Hidden"
    If attr And vbSystem Then result = result & " System"
    If attr And vbArchive Then result = result & " Archive"
    ' Discard the first (extra) space.
    GetAttrDescr = Mid$(result, 2)
End Function

同樣地,藉由傳給SetAttr指令一個組合值可以改變檔案或資料夾的屬性值,如下所示:

' Mark a file as Archive and Read-only.
filename = "d:\VS98\Temporary.Dat"
SetAttr filename, vbArchive + vbReadOnly
' Change a file from hidden to visible, and vice versa.
SetAttr filename, GetAttr(filename) Xor vbHidden

SetAttr函式無法用在已開啟的檔案。在沒有開啟檔案的情況下,可以分別使用FileLen和FileDateTime函式得知檔案的兩個資訊:檔案容量和建立的日期和時間。

Print FileLen("d:\VS98\Temporary.Dat")         ' Returns a Long value
Print FileDateTime("d:\VS98\Temporary.Dat")    ' Returns a Date value

FileLen函式一樣不能用在已開啟的檔案上,而其可在開啟檔案前取得正確的容量。

操作資料夾
 

使用CurDir$ 函式可以得知目前資料夾的名稱(或它的無$同等函式,CurDir)。當傳給此函式一個磁碟代號後,它會回傳此特定路徑正確的資料夾。在這個例子中,假設Microsoft Visual Studio安裝在磁碟D和Microsoft Windows NT安裝在磁碟C,但可能在您的系統上有不同的設定:

' Always use On Error--the current dir might be on a removed floppy disk.
On Error Resume Next
Print CurDir$                   ' Displays "D:\VisStudio\VB98"
' The current directory on drive C:
Print = CurDir$("c")            ' Displays "C:\WinNT\System"

Chdrive和Chdir分別能夠改變目前的磁碟和資料夾。如果ChDir指令的磁碟並非現行的磁碟時,則實際上只改變現行資料夾到該磁碟,所以必須使用這兩個指令來確保改變系統的現行資料夾:

' Make "C:\Windows" the current directory. 
On Error Resume Next
SaveCurDir = CurDir$
ChDrive "C:": ChDir "C:\Windows"
' Do whatever you need to do...
' ....
' and then restore the original current directory.
ChDrive SaveCurDir: ChDir SaveCurDir

也可以使用Mkdir和RmDir指令來建立和移除子目錄:

' Create a new folder in the current directory, and then make it current.
On Error Resume Next
MkDir "TempDir"
ChDir CurDir$ & "\TempDir"      ' (Assumes current dir is not the root)
' Do whatever you need to do...
' ....
' then restore the original directory and delete the temporary folder.
' You can't remove directories with files in them.
Kill "*.*"                      ' No need for absolute path.
ChDir ".."                      ' Move to the parent directory.
RmDir CurDir$ & "\TempDir"      ' Remove the temporary directory.

可用Name指令來改變資料夾的名稱,但無法移動資料夾:

' Assumes that "TempDir" is a subdirectory of the current directory
Name "TempDir" As "TempXXX"

列舉資料夾內所有檔案
 

VBA的Dir函式提供一個古老卻有效的方法來列舉資料夾中所有的檔案。用filespec參數呼叫Dir函式(包括萬用字元)和一個非必需的參數表示檔案的屬性。然後在每次反覆擷取中,單獨呼叫Dir直到回傳一個空的字串,下面的例子回傳特定資料夾中檔案名稱的陣列和示範正確建立迴圈的方法:

Function GetFiles(filespec As String, Optional Attributes As _
    VbFileAttribute) As String()
    Dim result() As String
    Dim filename As String, count As Long, path2 As String
    Const ALLOC_CHUNK = 50
    ReDim result(0 To ALLOC_CHUNK) As String
    filename = Dir$(filespec, Attributes)
    Do While Len(filename)
        count = count + 1
        If count > UBound(result) Then
            ' Resize the result array if necessary.
            ReDim Preserve result(0 To count + ALLOC_CHUNK) As String
        End If
        result(count) = filename
        ' Get ready for the next iteration.
        filename = Dir$
    Loop
    ' Trim the result array.
    ReDim Preserve result(0 To count) As String
    GetFiles = result
End Function

小秘訣

可以使用Dir$ 函式間接地去測試檔案或資料夾的存在,使用下面的函式:

Function FileExists(filename As String) As Boolean
    On Error Resume Next
    FileExists = (Dir$(filename) <> "")
End Function
Function DirExists(path As String) As Boolean
    On Error Resume Next
    DirExists = (Dir$(path & "\nul") <> "")
End Function

雖然在FileExists的程式明顯易懂,但仍可能對DirExists迷惑:"\nul"字串從何而來?這要回朔到MS-DOS時代以及其特殊檔案名稱"nul"和"con"等等。這些名稱實際上屬於特殊的裝置(null裝置、終端機等等)並出現在任何能搜尋到且實際存在的目錄中。這方法可用在任何資料夾,如要測試資料是否為空的而使用Dir$("*.*") 反而會失敗。


GetFiles程序能用在讀取一群檔案名稱到ComboBox控制項中。尤其是在控制項的Sorted屬性為True時更是有用:

Dim Files() As String, i As Long
' All files in C:\WINDOWS\SYSTEM directory, including system/hidden ones.
Files() = GetFiles("C:\windows\system\*.*", vbNormal + vbHidden _
    + vbSystem)
Print "Found " & UBound(Files) & " files."
For i = 1 To UBound(Files)
    Combo1.AddItem Files(i)
Next

如果Attribute參數包含vbDirectory位元,則Dir$ 函式也會回傳資料夾的名稱。使用這個特性來建立GetDirectories函式以回傳指定位置的所有子目錄名稱:

Function GetDirectories(path As String, Optional Attributes As _
    VbFileAttribute, Optional IncludePath As Boolean) As String()
    Dim result() As String
    Dim dirname As String, count As Long, path2 As String
    Const ALLOC_CHUNK = 50
    ReDim result(ALLOC_CHUNK) As String
    ' Build the path name + backslash.
    path2 = path
    If Right$(path2, 1) <> "\" Then path2 = path2 & "\"
    dirname = Dir$(path2 & "*.*", vbDirectory Or Attributes)
    Do While Len(dirname)
        If dirname = "." Or dirname = ".." Then
            ' Exclude the "." and ".." entries.
        ElseIf (GetAttr(path2 & dirname) And vbDirectory) = 0 Then
            ' This is a regular file.
        Else
            ' This is a directory.
            count = count + 1
            If count > UBound(result) Then
                ' Resize the result array if necessary.
                ReDim Preserve result(count + ALLOC_CHUNK) As String
            End If
            ' Include the path if requested.
            If IncludePath Then dirname = path2 & dirname
            result(count) = dirname
        End If
        dirname = Dir$
    Loop
    ' Trim the result array.
    ReDim Preserve result(count) As String
    GetDirectories = result
End Function

一般程式的目的是執行在一個目錄樹中所有的檔案。感謝所列的例子和遞迴的能力,這已變成(幾乎)小孩的遊戲:

' Load the names of all executable files in a directory tree into a ListBox.
' Note: this is a recursive routine.
Sub ListExecutableFiles(ByVal path As String, lst As ListBox)
    Dim names() As String, i As Long, j As Integer
    ' Ensure that there is a trailing backslash.
    If Right(path, 1) <> "\" Then path = path & "\"
    ' Get the list of executable files.
    For j = 1 To 3
        ' At each iteration search for a different extension.
        names() = GetFiles(path & "*." & Choose(j, "exe", "bat", "com"))
        ' Load partial results in the ListBox lst.
        For i = 1 To UBound(names)
            lst.AddItem path & names(i)
        Next
    Next
    ' Get the list of subdirectories, including hidden ones,
    ' and call this routine recursively on all of them.
    names() = GetDirectories(path, vbHidden)
    For i = 1 To UBound(names)
        ListExecutableFiles path & names(i), lst
    Next
End Sub

處理文字檔案
 

純文字檔是最容易處理的類型。使用包含For Input、For Output或For Appending參數的Open敘述開啟它們,然後開始讀資料、寫資料等。要開啟檔案─不論是文字檔或二進位檔─需要檔案號碼,如下列程式:

' Error if file #1 is already open
Open "readme.txt" For Input As #1

在單獨的應用程式中,通常可以分配唯一檔案編號給不同的程序。然而,這會嚴重影響到程式的重用性,所以建議使用FreeFile函式以查詢Visual Basic關於第一個可用檔案編號:

Dim fnum As Integer
fnum = FreeFile()
Open "readme.txt" For Input As #fnum

在輸入一個純文字檔後,通常使用Line Input敘述一次讀取一行文字直到EOF函式傳回True。當開啟檔案和讀取檔案的內容,都必須處理錯誤,但用LOF函式來判定檔案的長度和用Input$ 函式讀取所有字元來做這件事會更好。這裡有個常用且更完美的例子:

Function ReadTextFileContents(filename As String) As String
    Dim fnum As Integer, isOpen As Boolean
    On Error GoTo Error_Handler
    ' Get the next free file number.
    fnum = FreeFile()
    Open filename For Input As #fnum
    ' If execution flow got here, the file has been open without error.
    isOpen = True
    ' Read the entire contents in one single operation.
    ReadTextFileContents = Input(LOF(fnum), fnum)
    ' Intentionally flow into the error handler to close the file.
Error_Handler:
    ' Raise the error (if any), but first close the file.
    If isOpen Then Close #fnum
    If Err Then Err.Raise Err.Number, , Err.Description
End Function
' Load a text file into a TextBox control.
Text1.Text = ReadTextFileContents("c:\bootlog.txt")

當需要寫入資料到一個檔案時,使用For Output開啟要取代內容的檔案或用For Append附加新的資料到檔案中。通常用一連串的Print # 敘述將輸出結果傳送到輸出檔案,但如果集合起要輸出結果到一字串,然後再一次送出會更為快速。這裡有個常見的方法:

Sub WriteTextFileContents(Text As String, filename As String, _
    Optional AppendMode As Boolean)
    Dim fnum As Integer, isOpen As Boolean
    On Error GoTo Error_Handler
    ' Get the next free file number.
    fnum = FreeFile()
    If AppendMode Then
         Open filename For Append As #fnum
     Else
         Open filename For Output As #fnum
     End If
     ' If execution flow gets here, the file has been opened correctly.
     isOpen = True
     ' Print to the file in one single operation.
     Print #fnum, Text
     ' Intentionally flow into the error handler to close the file.
Error_Handler:
    ' Raise the error (if any), but first close the file.
    If isOpen Then Close #fnum
    If Err Then Err.Raise Err.Number, , Err.Description
End Sub

即使Visdual Basic 6沒有增加任何處理文字檔案的函式,但新的Split函式對於文字處理卻是非常有用。就是說包含項目的文字檔案被讀取到ListBox或ComboBox控制項。不能使用在先前直接讀取進控制項的ReadTextFileContents,但可以使用它讓程式更簡明:

Sub TextFileToListbox(lst As ListBox, filename As String)
    Dim items() As String, i As Long
    ' Read the file's contents, and split it into an array of strings.
    ' (Exit here if any error occurs.)
    items() = Split(ReadTextFileContents(filename), vbCrLf)
    ' Load all non-empty items into the ListBox.
    For i = LBound(items) To UBound(items)
        If Len(items(i)) > 0 Then lst.AddItem items(i)
    Next
End Sub

處理分隔的文字檔
 

分隔的文字檔案在每一行文字包含數個區段。即使沒有許多的程式設計者使用分隔文字檔來儲存重要的應用程式資料,但這些檔案仍然有著重要的角色,因為它們提供一個重要的方法在不同的資料庫格式交換資料。舉個例子,分隔文字檔通常是個可行的方法用來匯出與匯入資料到主機的資料庫。這裡是個簡易的用分號分離的文字檔案結構。(注意習慣上檔案的第一行是欄位名稱。)

Name;Department;Salary
John Smith;Marketing;80000
Anne Lipton;Sales;75000
Robert Douglas;Administration;70000

Split和Join函式特別用在匯入和匯出分隔文字檔。舉個例子,看看它有多容易將用分號隔離的資料檔輸入到一個陣列中:

' The contents of a delimited text file as an array of strings arrays
' NOTE: requires the GetTextFileLines routine
Function ImportDelimitedFile(filename As String, _
    Optional delimiter As String = vbTab) As Variant()
    Dim lines() As String, i As Long
    ' Get all lines in the file.
    lines() = Split(ReadTextFileContents(filename), vbCrLf)
    ' To quickly delete all empty lines, load them with a special char.
    For i = 0 To UBound(lines)
        If Len(lines(i)) = 0 Then lines(i) = vbNullChar
    Next
    ' Then use the Filter function to delete these lines.
    lines() = Filter(lines(), vbNullChar, False)
    ' Create a string array out of each line of text
    ' and store it in a Variant element.
    ReDim values(0 To UBound(lines)) As Variant
    For i = 0 To UBound(lines)
        values(i) = Split(lines(i), delimiter)
    Next
    ImportDelimitedFile = values()
End Function
' An example of using the ImportDelimitedFile routine
Dim values() As Variant, i As Long
values() = ImportDelimitedFile("c:\datafile.txt", ";")
' Values(0)(n) is the name of the Nth field.
' Values(i)(n) is the value of the Nth field on the ith record.
' For example, see how you can increment employees' salaries by 20%.
For i = 1 to UBound(values)
    values(i)(2) = values(i)(2) * 1.2
Next

使用陣列是相當好的方法,因為很容易建立新的記錄:

' Add a new record.
ReDim Preserve values(0 To UBound(values) + 1) As Variant
values(UBound(values)) = Split("Roscoe Powell;Sales;80000", ";")

或刪除已存在的陣列:

' Delete the Nth record
For i = n To UBound(values) - 1
    values(i) = values(i + 1)
Next
ReDim Preserve values(0 To UBound(values) _ 1) As Variant

寫入字串陣列到分隔檔也是很簡易的工作,感謝這個建立在Join函式的方法:

' Write the contents of an array of string arrays to a delimited
' text file.
' NOTE: requires the WriteTextFileContents routine
Sub ExportDelimitedFile(values() As Variant, filename As String, _
    Optional delimiter As String = vbTab)
    Dim i As Long
    ' Rebuild the individual lines of text of the file.
    ReDim lines(0 To UBound(values)) As String
    For i = 0 To UBound(values)
        lines(i) = Join(values(i), delimiter)
    Next
    ' Create CRLFs among records, and write them.
    WriteTextFileContents Join(lines, vbCrLf), filename
End Sub
' Write the modified data back to the delimited file.
ExportDelimitedFile values(), "C:\datafile.txt", ";"

此章節所有的範例都是依靠在假設分隔檔案小到可以儲存在記憶體中。這聽起來有點擔心,實際上文字檔通常用來建立小型資料庫或在不同的資料庫格式中移動少量的資料。如果發現有陣列容量大小的問題,就必須用大量的Line Input # 和Print # 敘述來讀寫。在大部分的情況下,即使大到1或2 megabytes的檔案(或更多,取決於RAM的大小)時仍沒有問題。

處理二進位檔案
 

為了開啟二進位檔,要使用包含For Random或For Binary選項的Open敘述。先來解釋後者,它是兩者中較為簡單的。在Binary模式下,使用Put敘述寫入檔案和使用Get敘述讀取檔案。Visual Basic會視最後的參數的變數結構來決定多少位元被寫入或讀取:

Dim numEls As Long, text As String
numEls = 12345: text = "A 16-char string"
' Binary files are automatically created if necessary.
Open "data.bin" For Binary As #1
Put #1, , numEls            ' Put writes 4 bytes.
Put #1, , text              ' Put writes 16 bytes (ANSI format).

當讀取資料時,必須重複同樣的變數長度字串。不需要關閉和重新開啟一個二進位檔,因為可以使用Seek敘述來改變檔案的指標至一特定的位元:

Seek #1, 1                  ' Back to the beginning (first byte is byte 1)
Get #1, , numEls            ' All Long values are 4 bytes.
text = Space$(16)           ' Prepare to read 16 bytes.
Get #1, , text              ' Do it.

你可以使用第二個參數在寫入或讀取資料前移動檔案指標,如同此程式:

Get #1, 1, numEls           ' Same as Seek + Get

注意

當開啟一個二進位檔,如果檔案不存在,Visual Basic會自動建立。因此,無法使用On Error敘述來判定檔案是否已存在。在這種情況下,可以在開啟檔案前使用Dir$ 函式確定檔案是否存在。


可以在一個步驟中快速地寫入全部的檔案到磁碟和讀取出來;在大部分的例子,必須在讀取前正確地切割陣列,而且也必須將資料前置數字:

' Store a zero-based array of Double.
Put #1, 1, CLng(UBound(arr)) ' First store the UBound value.
Put #1, , arr()              ' Then store all items in one shot.
' read it back
Dim LastItem As Long 
Get #1, 1, LastItem          ' Read the number of items.
ReDim arr2(0 To LastItem) As Double
Get #1, , arr2()             ' Read the array in memory in one operation.
Close #1

注意

如果用不同於寫入順序來讀取資料的話,可能讀取到錯誤的資料。在一些情況中,當試著去顯示這些變數內容時,可能引起Visual Basic環境損毀。基於這個原因,總是要按兩下滑鼠鍵來寫入和讀取。在不確定的情況下,在執行程式前先儲存起來。


當從一個二進位檔讀取時,將無法使用EOF函式判定是否為資料底端;因此,應先測試LOF函式傳回的值(檔案的長度)和使用Seek函式來判斷是否已經讀取所有的資料了:

Do While Seek(1) < LOF(1)
    ' Continue to read.
      ....
Loop

小秘訣

當儲存字串至磁碟時即使是文字或二進位檔Visual Basic會自動將它們從Unicode轉換為ANSI,其節省磁碟空間量和讓你可與16位元的Visual Basic應用程式交換。如果要寫入Unicode程式至國際市場,無論如何,此種行為會漏失掉部份資料,因為從檔案中讀取的字串將不會符合之前儲存的資料。為了修整此錯誤,必須轉換字串為Byte陣列然後再儲存:

Dim v As Variant, s As String, b() As Byte
s = "This is a string that you want to save in Unicode format"
b() = s: v = b()     ' You need this double step.
Put #1, , v          ' Write that to disk.

' Read it back.
Get #1, 1, v: s = v  ' No need for intermediary Byte array here.

用For Random敘述開啟二進位檔不同於到目前為止我所說的有著許多重要方面:

  • 資料被寫入和從檔案中讀取一個可固定長度的記錄。如此一個記錄長度能夠說明當開啟檔案(用Open敘述中的Len參數),或估計介於單獨的Put和Get敘述。如果實際資料傳遞到Put敘述較預期的記錄長度要短,Visual Basic會以亂數的字元填塞。如果較長,則產生錯誤。
     
  • Seek指令的敘述,如同Put和Get敘述的第二個參數,是記錄的數目,非二進位檔的位置。檔案的第一個記錄即記錄為1。
     
  • 不須擔心儲存和接收變數長度的資料,包括字串和陣列,因為Put和Get能正確地處理那些案例。但建議stay clear包含習慣性的(非固定長度)字串和動態陣列的UDT以便於記錄的長度才不會取決於它實際內容。
     

用For Random項開啟儲存在二進位檔的字串會預先置入一個顯示字元數的兩個位元值。這表示無法寫入包含超過32,767個字元的字串,也是合法記錄的最大範圍。要寫入更大的字串,應該使用For Binary項。

最後:所有目前的程式範例皆假設我們是在單一使用者的環境下工作,和不考慮由另一名使用者開啟檔案時的錯誤,或使用Lock敘述的所有或部份資料檔(和在之後用Unlock敘述解鎖)。更多的資訊,見Visual Basic的說明檔。


小秘訣

當在二進位檔寫入和讀取資料時不想要獲得複雜的額外估算,可以依照路徑使用較短的居中Variant變數。如果儲存任意形態(除了物件)的變數到Variant變數然後將變數寫入到二進位檔中,Visual Basic會寫入變數形態(因此,VarType會回傳值)和資料。如果變數擁有字串或陣列,Visual Basic也會儲存足夠的資訊去精確地讀取需要的位元數,將從額外的讀取敘述中釋放:

Dim v As Variant, s(100) As String, i As Long
' Fill the s() array with data... (omitted)
Open "c:\binary.dat" For Binary As #1
v = s()              ' Store the array in a Variant variable,
Put #1, , v          ' and write that to disk.
v = Empty            ' Release memory.

' Read data back.
Dim v2 As Variant, s2() As String
Get #1, 1, v2        ' Read data in the Variant variable,
s2() = v2            ' and then move it to the real array.
v2 = Empty           ' Release memory. 
Close #1

此方法也會處理多維的陣列。


FileSystemObject階層
 

Visual Basic 6引進新的檔案指令庫,能夠讓程式設計者容易地掃描磁碟和目錄,執行基本的檔案運作(包含複製、刪除、移動等等),和從一般的Visual Basic函式中取出資訊。但依我來看,新指令最特別的事可以使用新潮、一致和物件導向語法,此能讓程式更容易閱覽。


 

圖5-1 FileSystemObject階層

FileSystemObject根物件
 

階層的起源是FileSystemObject本身。其包含許多方法和唯一的屬性,Drive,回傳系統中所有裝置的集合。FileSystemObject物件(如同出現在接下來的文章和程式中的小型FSO)是層級中唯一可建立的物件因此,它是唯一可以用New關鍵字公開的物件。其他所有的物件都是取自這個而再包含許多方法或屬性。看看它有多容易將系統裝置填入一個陣列:

Dim fso As New Scripting.FileSystemObject, dr As Scripting.Drive
On Error Resume Next        ' Needed for not-ready drives
For Each dr In fso.Drives
    Print dr.DriveLetter & " [" & dr.TotalSize & "]"
Next

表5-3列出許多FSO物件所提供的方法。其中一些也涵蓋於Folder和File物件中(通常有著不相同的名稱和語法)。大部分的這些方法增強了Visual Basic指令的功能。舉個例子,只要使用一個指令就可以刪除非空的資料夾(非常仔細的!)以及複製和更名多數的檔案和目錄。

語法 說明
BuildPath (Path, Name) 回傳完整的檔案名稱,自訂的路徑(相關或完整的)和名稱。
CopyFile Source, Destination , [Overwrite] 複製一個或多個檔案:Source可以包含萬用字元而Destination則可以支援以反斜線做結束的目錄。除非設Overwrite為False否則會複寫掉檔案。
CopyFolder Source, Destination, [Overwrite] 同CopyFile,但會複製整個資料夾及其內容(子目錄和檔案)。如果Destination沒有相關的目錄存在,則會建立一個(但如果Source使用萬用字元則不會)。
CreateFolder(Path) As Folder 建立新的資料夾物件並回傳之;在資料夾已經存在時會發生錯誤。
CreateTextFile(FileName, [Overwrite],[Unicode]) As TextStream 建立新的純文字檔物件並回傳之;設定Overwrite=False來避免複寫掉以存在的檔案;設定Unicode=True以建立Unicode的純文字檔物件。
DeleteFile FileSpec, [Force] 刪除一個或多個檔案。FileSpec支援萬用字元;設定 Force=True以強制刪除唯讀檔案。
DeleteFolder(FolderSpec, [Force]) 刪除一個或多個資料夾以及其內容;設定Force=True以強制刪除唯讀檔案。
DriveExists(DriveName) 若給定的磁碟機存在時傳回True
FileExists(FileName) 如果指定的檔案已存在則回傳True。(其路徑必須符合目前的目錄。)
FolderExists(FolderName) 如果指定的資料夾檔案已存在則回傳True。(其路徑必須符合目前的目錄。)
GetAbsolutePathName(Path) 移動目前的目錄到指定的目錄。
GetBaseName(Filename) 執行基本檔案名稱(不包括路徑和延伸部份);並不會檢驗檔案和(或)路徑是否存在。
GetDrive(DriveName)As Drive 回傳符合所傳入參數的Drive物件相關的UNC路徑。(會檢驗drive是否存在)。
GetDriveName(Path) 從路徑中取得drive。
GetExtensionName(FileName) 從檔案名稱中取得延伸的字串。
GetFile(FileName) 回傳符合所傳入參數的檔案物件名稱。(能夠取得完全符合或相關的目錄。)
GetFileName( 取得檔案名稱(不須路徑但須延伸部份);並不會檢驗檔案和(或)路徑是否存在。
GetFolder(FolderName) As Folder 回傳符合所傳入參數的資料夾物件。(能夠取得完全符合或相關的目錄。)
GetParentFolderName(Path) 回傳符合所傳入參數的根目錄的名稱(或在根目錄不存在的狀態下回傳一個空字串)。
GetSpecialFolder(SpecialFolder) As Folder 回傳符合特殊的Windows目錄中的資料夾物件。SpecialFolder可以為0-WindowsFolder、1-SystemFolder、2-TemporaryFolder。
GetTempName() 回傳被用來當作暫存檔的非實質檔案。
MoveFile (Source, Destination) 同CopyFile,但它會刪除程式檔案。也可用在drives中移動,如果此函式被作業系統所支援的話。
MoveFolder(Source, Destination) 同MoveFile,但僅能處理目錄。
OpenTextFile(FileName, [IOMode], [Create], [Format])As TextStream 開啟一個純文字檔並回傳相關的TextStream物件。IOMode可以是一個或多個下列敘述的集合:1-ForReading、2-ForWriting、8-ForAppending;如果要建立新檔案則設Create為True;Format可以是0-TristateFalse(ANSI)、-1-TristateTrue(Unicode)或-2-TristateUseDefault(依系統決定determined by the system)。
表5-3 FileSystemObject物件的所有方法

Drive物件
 

此物件僅擁有屬性(沒有方法),其概要說明全在表5-4。所有的屬性都是唯讀的,除了VolumeName屬性外。這裡的程式片段能控制最少100 MB空間的本機裝置:

Dim fso As New Scripting.FileSystemObject, dr As Scripting.Drive
For Each dr In fso.Drives
    If dr.IsReady Then
        If dr.DriveType = Fixed Or dr.DriveType = Removable Then
            ' 2 ^ 20 equals one megabyte.
            If dr.FreeSpace > 100 * 2 ^ 20 Then
                Print dr.Path & " [" & dr.VolumeName & "] = " _
                    & dr.FreeSpace
            End If
        End If
    End If
Next
語法 說明
AvailableSpace 磁碟的剩餘空間,以位元組為單位;它通常和FreeSpace屬性在一起,直到作業系統達到磁碟空間。
DriveLetter 關聯到磁碟機的代號或關聯到網路磁碟的空字串。
DriveType 表示磁碟形態的常數:0-Unknown、1-Removable、2-Fixed、3-Remote、4-CDRom、5-RamDisk。
FileSystem 檔案系統所使用的格式:FAT、NTFS、CDFS。
FreeSpace 磁碟中剩餘的空間。(見AvailableSpace。)
IsReady 如果磁碟存在則為True,反之則False。
Path 由磁碟組成的路徑,不包含反斜線(如C:)。
RootFolder 符合根目錄的資料夾物件。
SerialNumber 符合連續的磁碟數目的長整數。
ShareName 網路分享的磁碟名稱或在如果沒有網路磁碟下為空字串。
TotalSize 全部的磁碟容量,以位元組為單位。
VolumeName 磁碟標籤(可以讀寫)。
表5-4 Drive物件的所有屬性

Folder物件
 

資料夾物件表示個別子目錄。獲得此物件有不同的方法:藉由FileSystemObject物件的GetFolder和GetSpecialFolder方法、經由裝置物件的RootFolder屬性、經由檔案物件或另一個資料夾物件的ParentFolder屬性或藉由反覆另一個資料夾物件的SubFolders集合。資料夾物件擁有一些有趣的屬性(見表5-5),但只有Attribute和Name屬性能被修改。最令人好奇的屬性應該就屬SubFolders和Files集合,能經由子目錄和檔案使用優美和簡潔的語法來反覆執行:

' Print the names of all first-level directories on all drives
' together with their short 8.3 names.
Dim fso As New Scripting.FileSystemObject
Dim dr As Scripting.Drive, fld As Scripting.Folder
On Error Resume Next
For Each dr In fso.Drives
    If dr.IsReady Then
        Print dr.RootFolder.Path       ' The root folder.
        For Each fld In dr.RootFolder.SubFolders
            Print fld.Path & " [" & fld.ShortName & "]"
        Next
    End If
Next
語法 說明 應用到
Attributes 檔案或資料夾的屬性,以下常數的集合:0- Normal、1-ReadOnly、2-Hidden、4-System、8-Volume、16-Directory32-Archive、64-Alias、2048-Compressed。屬性為Volume、Directory、Alias和Compressed不能修改。 資料夾和檔案
DateCreated 建立的日期(唯讀的日期值)。 資料夾和檔案
DateLastAccessed 最後存取的日期(唯讀的日期值)。 資料夾和檔案
DateLastModified 最後修改的日期(唯讀的日期值)。 資料夾和檔案
Drive 檔案或資料夾所在的Drive物件。 資料夾和檔案
Files 所有檔案物件的集合。 資料夾專屬
IsRootFolder 如果是該磁碟的根目錄則為True。 資料夾專屬
Name 資料夾或檔案的名稱。能分配新的名稱給物件 資料夾和檔案
ParentFolder 母資料夾的物件。 資料夾和檔案
Path 資料夾或檔案的路徑。(預設屬性。) 資料夾和檔案
ShortName 8.3 MS-DOS格式的名稱物件。 資料夾和檔案
ShortPath 8.3 MS-DOS格式的路徑物件。 資料夾和檔案
Size 以位元組為單位的檔案物件大小;其總和包括檔案和資料夾物件的子目錄。 資料夾和檔案
SubFolders 包含在該資料夾的所有子資料夾集合,包含系統或隱藏的資料夾。 資料夾專屬
Type 物件的敘述。例如:fso.GetFolder("C:\Recycled").Type會回傳「資源回收筒」;對於檔案物件,此值會表示他們的延伸情形(例如"Text Document"對於純文字) 資料夾和檔案
表5-5 Folder和File物件的所有屬性

資料夾物件也有一些方法,總括在表5-6。注意其常常能使用主要的FSO物件的適當方法來達到類似的結果。也可以要求SubFolders集合的Add方法來建立新的資料夾,如以下的遞迴範例,複製一個磁碟中的目錄結構到另一個磁碟中而不須複製檔案:

' Call this routine to initiate the copy process.
' NOTE: the destination folder is created if necessary.
Sub DuplicateDirTree(SourcePath As String, DestPath As String)
    Dim fso As New Scripting.FileSystemObject
    Dim sourceFld As Scripting.Folder, destFld As Scripting.Folder
    ' The source folder must exist.
    Set sourceFld = fso.GetFolder(SourcePath)
    ' The destination folder is created if necessary.
    If fso.FolderExists(DestPath) Then
        Set destFld = fso.GetFolder(DestPath)
    Else
        Set destFld = fso.CreateFolder(DestPath)
    End If
    ' Jump to the recursive routine to do the real job.
    DuplicateDirTreeSub sourceFld, destFld
End Sub
Private Sub DuplicateDirTreeSub(source As Folder, destination As Folder)
    Dim sourceFld As Scripting.Folder, destFld As Scripting.Folder
    For Each sourceFld In source.SubFolders
        ' Copy this subfolder into destination folder.
        Set destFld = destination.SubFolders.Add(sourceFld.Name)
        ' Then repeat the process recursively for all
        ' the subfolders of the folder just considered.
        DuplicateDirTreeSub sourceFld, destFld
    Next
End Sub
語法 說明 應用到
Copy Destination, [OverWriteFiles] 複製目前的檔案或資料夾物件到另一個路徑;類似FSO的CopyFolder和CopyFile方法,也能夠複製同個區域多數的物件。 資料夾和檔案
CreateTextFile(FileName, [Overwrite], [Unicode]) As TextStream 在目前的資料夾中建立純文字檔且回傳符合的TextStream物件。詳見相關的FSO方法中的參數說明。 資料夾專屬
Delete [Force] 刪除該檔案或資料夾物件(包含裡面的子目錄和檔案)。類似FSO的DeleteFile和DeleteFolder方法。 資料夾和檔案
Move DestinationPath 移動該檔案或資料夾到另一個路徑;類似FSO的MoveFile和MoveFolder方法。 資料夾和檔案
OpenAsTextStream([IOMode], [Format]) As TextStream 開啟純文字檔物件並回傳相關的TextStream物件。 資料夾專屬
表5-6 Folder和File物件的所有方法

File物件
 

File物件代表磁碟的單一檔案。有兩個方法使用此物件:藉由FSO物件的GetFile方法或反覆其原本資料夾物件的Files集合。儘管兩者有不同的特點,File和Folder物件都擁有很多的共同屬性和物件,因此就不重複敘述表5-5和5-6了。

FSO階層的限制在於沒有使用萬用字元的直接方法來過濾檔案名稱,因此可以試著和Dir$ 函式一起使用。所能做的就是反覆資料夾的Files集合和測試檔案的名稱、延伸或其他的屬性,如下所示。

' List all the DLL files in the C:\WINDOWS\SYSTEM directory.
Dim fso As New Scripting.FileSystemObject, fil As Scripting.File
For Each fil In fso.GetSpecialFolder(SystemFolder).Files
    If UCase$(fso.GetExtensionName(fil.Path)) = "DLL"  Then
        Print fil.Name
    End If
Next

FileSystemObject階層不允許檔案方面的許多操作。尤其在列出它們的屬性(包括在原本VBA檔案函式功能的許多特性)時,僅能夠將檔案以文字模式開啟,如同下個章節所解釋。

TextStream物件
 

TextStream物件代表以文字模式開啟的檔案。可以用下列方法獲得:藉由FSO物件的CreateTextFile或OpenTextFile方法、藉由資料夾物件的CreateTextFile方法或經由使用檔案物件的OpenAsTextStream方法。TextStream物件擁有一些方法和唯讀的屬性,其全部介紹在表5-7。TextStream物件提供一些檔案指令舉例來說,當讀寫文字檔時,計算行與列。此特性的好處在於重複掃描目錄中所有TXT檔裡面含有的字串,然後回傳包含所有擁有此字串的檔案名稱,以及字串所在行列的結果陣列(實際上是陣列中的陣列):

' For each TXT file that contains the search string, the function
' returns a Variant element that contains a 3-item array that holds
' the filename, the line number, and the column number.
' NOTE: all searches are case insensitive.
Function SearchTextFiles(path As String, search As String) As Variant()
    Dim fso As New Scripting.FileSystemObject
    Dim fil As Scripting.File, ts As Scripting.TextStream
    Dim pos As Long, count As Long
    ReDim result(50) As Variant

    ' Search for all the TXT files in the directory.    
    For Each fil In fso.GetFolder(path).Files
        If UCase$(fso.GetExtensionName(fil.path)) = "TXT" Then
            ' Get the corresponding TextStream object.
            Set ts = fil.OpenAsTextStream(ForReading)
            ' Read its contents, search the string, close it.
            pos = InStr(1, ts.ReadAll, search, vbTextCompare)
            ts.Close

            If pos > 0 Then
                ' If the string has been found, reopen the file
                ' to determine string position in terms of (line,column).
                Set ts = fil.OpenAsTextStream(ForReading)
                ' Skip all preceding characters to get where 
                ' the search string is.
                ts.Skip pos _ 1 
                ' Fill the result array, make room if necessary.
                count = count + 1
                If count > UBound(result) Then
                    ReDim Preserve result(UBound(result) + 50) As Variant
                End If
                ' Each result item is a 3-element array.
                result(count) = Array(fil.path, ts.Line, ts.Column)
                ' Now we can close the TextStream.
                ts.Close
            End If
        End If
    Next
    ' Resize the result array to indicate number of matches.
    ReDim Preserve result(0 To count) As Variant
    SearchTextFiles = result
End Function
' An example that uses the above routine: search for a name in all
' the TXT files in E:\DOCS directory, show the results in 
' the lstResults ListBox, in the format "filename [line, column]".
Dim v() As Variant, i As Long
v() = SearchTextFiles("E:\docs", "Francesco Balena")
For i = 1 To UBound(v)
    lstResults.AddItem v(i)(0) & " [" & v(i)(1) & "," & v(i)(2) & "]"
Next
屬性或方法 語法 說明
屬性 AtEndOfLine 如果檔案指標在最後一行則為True。
屬性 AtEndOfFile 如果檔案指標在檔案的結尾則為True(類似VBA的EOF函式)。
方法 Close 關閉檔案(類似VBA的Close敘述)。
屬性 Column 目前所在的列數。
屬性 Line 目前的行數。
方法 Read(Characters) 讀取字元的數目並回傳字串(類似VBA的 Input$ 函式)。
方法 ReadAll() 讀取全部的檔案到字串中(類似VBA中和 LOF函式一起使用的Input$ 函式)。
方法 ReadLine() 讀取下一行的文字並回傳字串(類似VBA的 Line Input敘述)
方法 Skip Characters 略過特定的字元數。
方法 SkipLine 略過一行文字。
方法 Write Text 寫入一串字元,不包含Newline字元(類似包含分號的Print$ 指令)。
方法 WriteBlankLines Lines 寫入指定的空白行數(類似沒有參數的一個或多個Print# 指令)。
方法 WriteLine [Text] 寫入一串字元,包含Newline字元(類似不包含分號的Print$ 指令)。
表5-7 TextStream物件的所有屬性和方法。

和視窗的互動
 

至目前為止,我們集中在介紹獨立的應用程式上而沒和外界接觸。但在許多時刻,會需要讓應用程式和外在環境做互動,包括和其他的應用程式一同執行。此節導入此主題並介紹一些技巧來管理互動的情形。

App物件
 

App物件由Visual Basic函式庫所提供,且代表正在執行的應用程式。App物件擁有許多屬性和方法,許多都是頗為進階,在此書後續章節會有說明。

EXEName和Path屬性回傳可執行檔案的名稱與路徑(如果只是單獨執行的EXE檔)或物件名稱(若在外在環境下執行)。這些屬性通常同時被使用 - 舉個例子,要找到跟可執行檔存放在同樣目錄且擁有同樣主檔名的INI檔:

IniFile = App.Path & IIf(Right$(App.Path, 1) <> "\", "\", "") _
    & App.EXEName & ".INI"
Open IniFile For Input As #1
' and so on.

App.Path屬性的另一個常用法則是設定目前的目錄為應用程式的目錄,以便於讓它的從屬檔案能被找到而不需要指定其完整路徑:

' Let the application's directory be the current directory.
On Error Resume Next
ChDrive App.Path: ChDir App.Path

注意

先前的片段程式在某些情況下可能會錯誤,尤其在當Visual Basic的應用程式在遠端網路伺服器上啟動時。發生的原因在於App.Path屬性會回傳UNC路徑(例如,\\servername\dirname\...),而ChDrive指令不會處理這類的路徑。基於這個原因,應該防止此程式引發意外的錯誤,且還要提供使用者別的方法來讓應用程式只到自己的目錄(例如,藉由在系統Registry設定一鍵值)。


PrevInstance屬性能夠判斷是否是應用程式的其他(被編譯的)實體運作於系統上。若要預防使用者意外地執行兩次應用程式時很有效:

Private Sub Form_Load()
    If App.PrevInstance Then
        ' Another instance of this application is running.
        Dim saveCaption As String
        saveCaption = Caption
        ' Modify this form's caption so that it isn't traced by
        ' the AppActivate command.
        Caption = Caption & Space$(5)
        On Error Resume Next
        AppActivate saveCaption
        ' Restore the Caption, in case AppActivate failed.
        Caption = saveCaption
        If Err = 0 Then Unload Me
    End If
End Sub

有一對屬性是可讀的,且於執行時期可被修改。TaskVisible Boolean屬性決定應用程式是否要顯示在工作列上。Title屬性則是定義應用程式在Windows工作列的字串。其初始值是在設計時期專案屬性視窗的製成頁籤中所輸入的字串。

App物件的其他屬性傳回於設計時期在專案屬性視窗的一般和製成頁籤內所輸入的文字。(見圖5-2)。例如,HelpFile屬性是相關的求助檔案名稱,如果有的話。UnattendedApp和RetainedProject屬性對應到對話視窗一般頁籤的相關核選鈕。(但其意義分別在 16 和 20 章中會做說明)。Major、Minor和Revision屬性會回傳關於執行程式的版本資訊。Comments、CompanyName、FileDescription、LegalCopyright、LegalTrademark和ProductName屬性可讓你於執行時期查詢在專案屬性視窗的製成頁籤中所輸入的資料。它們通常在建立關於對話方塊或開頭畫面時特別有用。


 

圖5-2 專案屬性視窗的一般和製成頁籤

Clipboard物件
 

在Windows 9x和Windows NT的32位元世界中,經由系統的剪貼簿跟其他的應用程式交換資料似乎顯得有點過時,但事實上對於使用者而言,剪貼簿依然是最簡單也最有效用來在應用程式間快速複製資料的方法。Visual Basic能用Clipboard廣域物件控制系統的剪貼簿。相對於其他的Visual Basic物件,它算是相當單純的,只有六個方法且沒有屬性。

複製和貼上文字
 

取代剪貼簿中一段文字,使用SetText方法:

Clipboard.SetText Text, [Format]

其中format可為1-vbCFText(純文字,預設值)、&HBF01-vbCFRTF(RTF文字格式)或&HBF00-vbCFLink(DDE資訊)。此參數是必需的,因為剪貼簿可以各種格式來片段資訊。舉個例子,如果有個RichTextBox控制項(一種Microsoft ActiveX控制項, 第12章 會有說明),則可將所選取的文字儲存成vbCFText或vbCFRTF格式,且讓使用者將這些文字用恰當的格式貼在合適的目標控制項內。

Clipboard.Clear
Clipboard.SetText RichTextBox1.SelText     ' vbCFText is the default.
Clipboard.SetText RichTextBox1.SelRTF, vbCFRTF

注意

在某些情況與外部應用程式情況下,取代剪貼簿中的文字不會正常運作,直到使用Clear方法重置Clipboard物件,如先前程式所示。


使用GetText方法可以取得剪貼簿中的文字。可使用下列語法指定所要獲得的格式:

' For a regular TextBox control
Text1.SelText = Clipboard.GetText()       ' You can omit vbCFText.
' For a RichTextBox control
RichTextBox1.SelRTF = Clipboard.GetText(vbCFRTF)

一般來說,不知道剪貼簿是否真儲存RTF格式的文字,所以應要使用GetFormat方法測試它們的內容,此函數需要一個格式作為參數,並回傳Boolean值來表示剪貼簿的格式是否為該傳入格式:

If Clipboard.GetFormat(vbCFRTF) Then
    ' The Clipboard contains data in RTF format.
End If

Format的值可以是1-vbCFText(純文字)、2-vbCFBitmap(點陣圖)、3-vbCFMetafile(metafile)、8-vbCFDIB(與設備無關的點陣圖)、9-vbCFPalette(色彩範本)、&HBF01-vbCFRTF(RTF文字格式)或&HBF00-vbCFLink(DDE溝通資料)。此為將文字貼入RichTextBox控制項的正確方法:

If Clipboard.GetFormat(vbCFRTF) Then
    RichTextBox1.SelRTF = Clipboard.GetText(vbCFRTF)
ElseIf Clipboard.GetFormat(vbCFText) Then
    RichTextBox1.SelText = Clipboard.GetText()
End If

複製和貼上圖像
 

當使用PictureBox和Image控制項時,可用GetData方法取得儲存在剪貼簿的圖像,此方法也需要格式參數(vbCFBitmap、vbCFMetafile、vbCFDIB或vbCFPalette、mage控制項,只能使用vbCFBitmap)。正確的方法為:

Dim frmt As Variant
For Each frmt In Array(vbCFBitmap, vbCFMetafile, _
    vbCFDIB, vbCFPalette)
    If Clipboard.GetFormat(frmt) Then
        Set Picture1.Picture = Clipboard.GetData(frmt)
        Exit For
    End If
Next

使用SetData方法可複製PictureBox或Image控制項的內容到剪貼簿中:

Clipboard.SetData Picture1.Picture
' You can also load an image from disk onto the clipboard.
Clipboard.SetData LoadPicture("c:\myimage.bmp")

通用的編輯選單
 

在許多Windows應用程式中,所有剪貼簿指令大都集合都在Edit選單中。依照哪個控制項為作用中來決定哪些指令能讓使用者使用(以及程式可處理它們)。這有兩個問題尚待解決:基於友善的使用者界面,應該要取消所有無法應用到現行控制項與剪貼簿的目前內容的選項,且必須設計能在所有情況下進行剪下-複製-貼上的機制。

當使用多重的控制項在表單上時,事情就很快地讓人感到困惑,因為必須考慮到一些潛在性的問題。這有個簡單但完整的範例程式(見圖5-3)。為了讓您能容易地在應用程式中重用,所有對控制項的引用皆透過表單的ActiveControl屬性。除了使用TypeOf或TypeName關鍵字測試控制項形態外,此程式使用On Error Resume Next敘述間接地測試某屬性是否支援(見下列程式的粗體部份。)這方法能處理任何形態的控制項,包括協力廠商的ActiveX控制項,而當增加新的控制項到工具箱時不用修改程式。


 

圖5-3 Clipbord.vbp示範專案顯示如何使用TextBox、RTF TextBox和PictureBox控制項建立通用編輯選單
' Items in Edit menu belong to a control array. These are their indices.
Const MNU_EDITCUT = 2, MNU_EDITCOPY = 3
Const MNU_EDITPASTE = 4, MNU_EDITCLEAR = 6, MNU_EDITSELECTALL = 7

' Enable/disable items in the Edit menu.
Private Sub mnuEdit_Click()
    Dim supSelText As Boolean, supPicture As Boolean
    ' Check which properties are supported by the active control.
    On Error Resume Next
    ' These expressions return False only if the property isn't supported.
    supSelText = Len(ActiveControl.SelText) Or True
    supPicture = (ActiveControl.Picture Is Nothing) Or True

    If supSelText Then
        mnuEditItem(MNU_EDITCUT).Enabled = Len(ActiveControl.SelText)
        mnuEditItem(MNU_EDITPASTE).Enabled = Clipboard.GetFormat(vbCFText)
        mnuEditItem(MNU_EDITCLEAR).Enabled = Len(ActiveControl.SelText)
        mnuEditItem(MNU_EDITSELECTALL).Enabled = Len(ActiveControl.Text)
    ElseIf supPicture Then
        mnuEditItem(MNU_EDITCUT).Enabled = Not (ActiveControl.Picture _
            Is Nothing)
        mnuEditItem(MNU_EDITPASTE).Enabled = Clipboard.GetFormat( _
            vbCFBitmap) Or Clipboard.GetFormat(vbCFMetafile)
        mnuEditItem(MNU_EDITCLEAR).Enabled = _
            Not (ActiveControl.Picture Is Nothing)
    Else
        ' Neither a text- nor a picture-based control
        mnuEditItem(MNU_EDITCUT).Enabled = False
        mnuEditItem(MNU_EDITPASTE).Enabled = False
        mnuEditItem(MNU_EDITCLEAR).Enabled = False
        mnuEditItem(MNU_EDITSELECTALL).Enabled = False
    End If
    ' The Copy menu command always has the same state as the Cut command.
    mnuEditItem(MNU_EDITCOPY).Enabled = mnuEditItem(MNU_EDITCUT).Enabled
End Sub
' Actually perform copy-cut-paste commands.
Private Sub mnuEditItem_Click(Index As Integer)
    Dim supSelText As Boolean, supSelRTF As Boolean, supPicture As Boolean
    ' Check which properties are supported by the active control.
    On Error Resume Next
    supSelText = Len(ActiveControl.SelText) >= 0
    supSelRTF = Len(ActiveControl.SelRTF) >= 0
    supPicture = (ActiveControl.Picture Is Nothing) Or True
    Err.Clear
    Select Case Index
        Case MNU_EDITCUT
            If supSelRTF Then
                Clipboard.Clear
                Clipboard.SetText ActiveControl.SelRTF, vbCFRTF
                ActiveControl.SelRTF = ""
            ElseIf supSelText Then
                Clipboard.Clear
                Clipboard.SetText ActiveControl.SelText
                ActiveControl.SelText = ""
            Else
                Clipboard.SetData ActiveControl.Picture
                Set ActiveControl.Picture = Nothing
            End If
        Case MNU_EDITCOPY
            ' Similar to Cut, but the current selection isn't deleted.
            If supSelRTF Then
                Clipboard.Clear
                Clipboard.SetText ActiveControl.SelRTF, vbCFRTF
            ElseIf supSelText Then
                Clipboard.Clear
                Clipboard.SetText ActiveControl.SelText
            Else
                Clipboard.SetData ActiveControl.Picture
            End If
        Case MNU_EDITPASTE
            If supSelRTF And Clipboard.GetFormat(vbCFRTF) Then
                ' Paste RTF text if possible.
                ActiveControl.SelRTF = Clipboard.GetText(vbCFText)
            ElseIf supSelText Then
                ' Else, paste regular text.
                ActiveControl.SelText = Clipboard.GetText(vbCFText)
            ElseIf Clipboard.GetFormat(vbCFBitmap) Then
                ' First, try with bitmap data.
                Set ActiveControl.Picture = _
                    Clipboard.GetData(vbCFBitmap)
            Else
                ' Else, try with metafile data.
                Set ActiveControl.Picture = _
                    Clipboard.GetData(vbCFMetafile)
            End If
        Case MNU_EDITCLEAR
            If supSelText Then
                ActiveControl.SelText = ""
            Else
                Set ActiveControl.Picture = Nothing
            End If
        Case MNU_EDITSELECTALL
            If supSelText Then
                ActiveControl.SelStart = 0
                ActiveControl.SelLength = Len(ActiveControl.Text)
            End If
    End Select
End Sub

Printer物件
 

許多應用程式需要去列印其結果在紙上。Visual Basic提供一個Printer物件,其包含一些屬性和方法可完美地控制列印文件的效果。

Visual Basic函式庫也有個和Printers集合,能收集所有安裝在系統上的印表機資訊。每個項目都是個印表機物件,且所有的屬性都是唯讀的。換句話說,僅能讀出所有已安裝印表機的特性但不能直接地修改他們。如果需要更改印表機的特性,則首先必須從集合之中將所選擇的印表機指定給Printer物件,然後再改變它的屬性。

取得已安裝印表機的資訊
 

Printer物件擁有許多的屬性讓人得知可用印表機的特性與其驅動程式。舉例來說,DeviceName屬性會回傳出現在控制台上的印表機名稱,且DriverName會回傳其所使用的驅動程式名稱。可將這些資訊填入ListBox和ComboBox控制項:

For i = 0 To Printers.Count _ 1
    cboPrinters.AddItem Printers(i).DeviceName & " [" & _
        Printers(i).DriverName & "]"
Next

Port屬性會回傳印表機的連接埠(如LPT1:)。ColorMode屬性判斷印表機能否列印彩色文件。(可以是1-vbPRCMMonochrome或2-vbPRCMColor)。 Orientation屬性表示列印頁的方向。(可以是1-vbPRORPortrait、2-vbPRORLandscape)。PrinterQuality屬性回傳目前的解析度。(可以是1-vbPRPQDraft、2-vbPRPQLow、3-vbPRPQMedium或4-vbPRPQHigh。)

其他屬性還有PaperSize(紙張大小)、PaperBin(紙張來源)、Duplex(紙張邊界)、Copies(複製份數)和Zoom(放大倍率)。關於這些屬性的資訊,請見Visual Basic說明文件。在附屬光碟中,可以找到範例程式(如圖5-4所示)列舉出系統中的所有印表機、瀏覽其屬性與每台各印一張測試。


 

圖5-4 執行展示程式來察看Printers集合和運作中的Printer物件

現行印表機的運作
 

一個新潮的應用程式應該讓使用者擁有和安裝在系統中的印表機一同運作的能力。在Visual Basic中,藉由印表機集合中的元件敘述所選取的印表機就可做到。舉例來說,如果你已經在ComboBox控制項中填滿所有已安裝的印表機,則可以讓使用者藉由點選Make Current按鈕來選擇其中之一:

Private Sub cmdMakeCurrent_Click()
    Set Printer = Printers(cboPrinters.ListIndex)
End Sub

相對於Printers集合內的Printer物件的屬性是唯讀的,你卻可修改Printer物件的屬性。理論上,到目前為止所介紹的所有屬性都可以被寫入,除了DeviceName、DriverName和Port。然而,實際上能寫入的屬性端視印表機和驅動程式而定。舉例來說,如果目前的印表機是單色的,則無法在ColorMode屬性中使用2-vbPRCMColor值。若這樣做的話,印表機可能會不理會或產生錯誤。一般來說如果屬性不支援,會回傳0。

有時候可能需要去Printer物件對應到Printers集合的哪個項目,例如要暫時用另一台印表機列印時,然後又要還原回原先的印表機。經由比較Printer物件DeviceName屬性與Printers集合的每個項目所回傳的值就可以做到:

' Determine the index of the Printer object in the Printers collection.
For i = 0 To Printers.Count _ 1
    If Printer.DeviceName = Printers(i).DeviceName Then
        PrinterIndex = i: Exit For
    End If
Next
' Prepare to output to the printer selected by the user.
Set Printer = Printers(cboPrinters.ListIndex)
' ...
' Restore the original printer.
Set Printer = Printers(PrinterIndex)

另一個讓使用者用他們所選擇的印表機列印的方法是設定印表機的TrackDefault屬性為True。這樣子做,印表機物件會自動指向於控制台中所選擇的印表機。

輸出資料到Printer物件
 

傳送結果給Printer物件不難,因為此物件支援表單和PictureBox物件中所有的圖形方法,包括有Print、Pset、Line、Circle和PaintPicture。也可用標準的屬性,如Font物件、獨立的Fontxxxx屬性、CurrentX和CurrentY屬性,來控制外觀。

Printer物件有三個獨特的方法。EndDoc方法通知Printer物件所有的資料已傳送,可開始列印。KillDoc方法則在傳送任何資料給印表機裝置前終止目前的列印工作。最後NewPage方法傳送目前的頁面給印表機(或印表工作列),並前進到下一頁。它也可以重設列印範圍的左上角位置和增加頁數。目前的頁數可由Page屬性取得。這裡有個列印兩頁文件的例子:

Printer.Print "Page One"
Printer.NewPage
Printer.Print "Page Two"
Printer.EndDoc

Printer物件也支援標準的ScaleLeft、ScaleTop、ScaleWidth和ScaleHeight屬性,其度量單位由ScaleMode屬性所決定(通常是twips)。預設情況是ScaleLeft和ScaleTop屬性回傳0,表示可列印範圍的左上角區域。 ScaleWidth和ScaleHeight屬性會回傳可列印範圍的右下角座標。

執行其他的應用程式
 

Visual Basic能用Shell指令執行其他的Windows應用程式,其語法為:

TaskId = Shell(PathName, [WindowStyle])

PathName可包含一個命令行。WindowStyle為以下其中一個常數:0-vbHide(隱藏視窗且失去焦點)、1-vbNormalFocus(視窗為作用中並還原微原先的大小和位置)、2-vbMinimizedFocus(視窗以圖示顯示並為作用中-此為預設值)、3-vbMaximizedFocus(視窗為最大化且為作用中)、4-vbNormalNoFocus(視窗已還原但失去焦點)或6-vbMinimizedNoFocus(視窗最小化且焦點停在原作用中視窗)。看看例子,如何執行記事本並讀取一個檔案:

' No need to provide a path if Notepad.Exe is on the system path.
Shell "notepad c:\bootlog.txt", vbNormalFocus

Shell函式以非同步來執行外部程式。這表示控制權很快地返回Visual Basic應用程式中,如此可以繼續執行它原本的程式。在大部分的情況下,這種行為是正常的,因為這正是Windows多工的特性。但有時候仍需要去等待外部程式執行結束(舉個例子,如果需要執行的結果),或只是要測試程式是否仍在執行。Visual Basic本身沒有函數來獲得這些資訊,但是可用一些Wuindows API來達成這項工作。我準備了一個多用途的函式來測試共享的程式是否仍在執行,等待的時間可自行調整(省略其參數會持續等待),然後若程式仍在執行則回傳True:

' API declarations
Private Declare Function WaitForSingleObject Lib "kernel32" _
    (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long
Private Declare Function OpenProcess Lib "kernel32" (ByVal dwAccess As _
    Long, ByVal fInherit As Integer, ByVal hObject As Long) As Long
Private Declare Function CloseHandle Lib "kernel32" _
    (ByVal hObject As Long) As Long
' Wait for a number of milliseconds, and return the running status of a 
' process. If argument is omitted, wait until the process terminates.
Function WaitForProcess(taskId As Long, Optional msecs As Long = -1) _
    As Boolean
    Dim procHandle As Long
    ' Get the process handle.
    procHandle = OpenProcess(&H100000, True, taskId)
    ' Check for its signaled status; return to caller.
    WaitForProcess = WaitForSingleObject(procHandle, msecs) <> -1
    ' Close the handle.
    CloseHandle procHandle
End Function

傳送到此範例的參數為Shell函式回傳的值:

' Run Notepad, and wait until it is closed.
WaitForProcess Shell("notepad c:\bootlog.txt", vbNormalFocus)

有幾個方法能與執行的程式互動。在 第16章 中,會提到如何以COM控制一個應用程式,但並非所有的應用程式皆能用此方法來控制。而且即使可以,有時候其結果並不值得這樣做。在較少要求的情況下,可用較為簡單,建構在AppActivate和SendKeys指令的方法來獲得。AppActivate指令會轉移輸入焦點到符合第一個參數的應用程式:

AppActivate WindowTitle [,wait]

WindowTitle可以是個字串或Shell函式的回傳值;若為前者,Visual Basic會比較此值與系統中所有啟動中視窗的標題。如果沒有完全符合,Visual Basic會重複尋找標題開頭符合傳入字串參數的視窗。若傳給Shell函式的taskid回傳值,則不需要第二次的傳送,因為taskid唯一指到一執行的程序。如果Visual Basic無法找到符合的視窗,則會產生執行時期錯誤。Wait是非必要的參數,用來指示Visual Basic應該等到在傳送到其他程式前,目前的應用程式已獲得焦點(Wait=True)或是否指令必須直接執行(Wait=False,預設行為)。

SendKeys敘述會傳送一個或多個鍵給正等待輸入的應用程式。此敘述有著稍微複雜的語法,讓人指派如Ctrl、Alt和Shift的控制鍵、移動游標、特殊功能鍵等等。(詳見Visual Basic的說明文件)。這段程式會執行記事本,且在它的視窗中貼入剪貼簿中的內容:

TaskId = Shell("Notepad", vbMaximizedFocus) 
AppActivate TaskId
SendKeys "^V"        ' ctrl-V

現在你可以執行外部程式,與他互動,知道它何時完成執行時。這裡有個範例程式能達成這功能,並讓你了解一些不同的設定(見圖5-5)。在附屬光碟中有完整的程式碼。


 

圖5-5 說明如何使用Shell、AppActivate和SendKeys敘述的範例程式

顯示Help
 

一個成功的Windows應用程式應該會提供使用手冊給初學者,最典型的是以Help檔案的形式表現。Visual Basic支援兩種不同的方法來顯示這些使用者資訊,兩者皆使用Help檔案。

撰寫Help檔案
 

這兩種情況皆必須先建立Help檔案。為了如此,需要一個能夠建立RTF檔案格式的文字處理裝置(如Microsoft Word)以及一個Help編譯器。在Visual Basic 6光碟可找到Microsoft Help Workshop,如圖5-6所示,其可讓您組合所有準備好的文件和點陣圖,並將之編譯成HLP檔。

撰寫Help檔是複雜的事情,超出此書範圍。Microsoft Help Workshop中的說明文件有關於此的資訊。然而依筆者的看法,最有效的方法是使用協力廠商的共享軟體或商業程式,如Blue Sky Software的RoboHelp或WexTech的Doc-to-Help,其讓建立Help檔案變得較簡單些。


 

圖5-6 Help Workshop工具放在Visual Basic光碟片中但須先安裝

一旦已建立HLP檔案,則可以在Visual Basic應用程式中引用它。除了在設計時期藉由在專案屬性視窗的一般頁籤中輸入檔案名稱外,還可以在執行時期藉由指定App.HelpFile屬性來達成。後者用於不確定Help檔案安裝於何處時使用。舉個例子,可以設定此路徑在應用程式的主要資料夾下:

' If this file reference is incorrect, Visual Basic raises an error
' when you later try to access this file.
App.HelpFile = App.Path & "\Help\MyApplication.Hlp"

標準視窗的Help
 

提供相關文章求助的第一個方法是F1鍵。這種求助方法使用HelpContextID屬性,其支援Visual Basic所有可視物件,包括表單、內建控制項和額外的ActiveX控制項。也可在設計時期於專案屬性視窗中輸入屬於應用程式層級的help context ID。(App物件在執行時期並未包含同等的屬性。)

當使用者按下F1時,Visual Basic會核對作用中控制項的HelpContextID屬性是否為非零值:若是的話,會顯示關於該ID的求助頁。否則,Visual Basic會檢查主表單是否擁有一非零值的HelpContextID屬性,然後顯示相關的求助頁。如果控制項和表單的HelpContextID屬性皆為0,Visual Basic就會顯示對應到專案help context ID相關的求助頁。

"What's This"Help
 

Visual Basic也支援另外一個顯示Help方法,稱之為"What's This"Help。藉由在表單右上角顯示What's This按鈕來支援此說明模式,如圖5-7所示。當使用者點選該按鈕時,滑鼠指標會改變為一個箭頭和問號的圖形,然後使用者可以點選表單上的任意控制項來快速獲得該控制項的說明。


 

圖5-7 表單最右上角的What's This按鈕被點選

要在程式中使用此特點,必須設定表單的WhatsThisButton屬性為True,使得What's This按鈕顯示在表單標題列。此屬性在執行時期是唯讀的,所以僅能在設計時期的屬性視窗中設定。此外,為了讓What's This按鈕顯現,還必須設定BorderStyle屬性為1-Fixed Single或3-Fixed Dialog,或必須設定MaxButton和MinButton屬性為False。

如果沒有符合此要求,則無法顯示What's This按鈕。但可藉由按鈕或選單選項來進入此模式,藉由執行表單的WhatsThisMode方法:

Private Sub cmdWhatsThis_Click()
    ' Enter What's This mode and change mouse cursor shape.
    WhatsThisMode
End Sub

表單上的每個控制項(不是表單本身)擁有WhastThisHelpID屬性。把此屬性設定為要顯示說明頁的help context ID,當在What's This模式下且使用者點選此控制項時。

最後表單的WhatsThisHelp屬性必須設為True來啟動What's This Help。如果此屬性設為False,則Visual Basic會回復到標準的F1鍵與HelpContextID屬性作為基準的說明機制。WhatThisHelp屬性僅能在設計時期設定。關於這方面,有三個方法來顯示What's This? 說明主題:

  • 使用者點選What's This按鈕,然後在某控制項點選一下;在這種情況下,Visual Basic會自動顯示隨同被點選控制項的WhatsThisHelpID屬性的What's This說明。
     
  • 透過WhatsThisMode方法(見前面的程式片段),使用者點選按鈕或或選擇某選項進入What's This help模式,然後點選某控制項。Visual Basic會再次顯示隨同被點選控制項的WhatsThisHelpID屬性的What's This說明。
     
  • 藉由執行控制項的ShowWhatsThis方法可以引入隨同控制項的WhatsThisHelpID屬性的What's This說明。(所有內建或外在控制項皆支援此方法)。
     

不論用哪種方法,別忘了必須準備好應用程式內每個表單中每個控制項的說明文件。多數控制項共用相同的說明文件是可行的,但這樣會帶給使用者相當的困惑。因此一般對於每個控制項皆會有不同的說明頁。

在前五章,我已展示關於Visual Basic環境和VBA語言的最多事項。現在,你已經有足夠的資訊寫出不錯的程式了。然而,這本書著重於物件導向的程式,在下兩個章節中,我希望讓您相信有多需要OOP來建立複雜運作的應用程式。