3. 變 數

從BASICA時代開始,程式設計師們一直都必須為字串的處理費盡苦心,現在Visual Basic 6的新的字串函式減輕了這些負擔。常見的工作諸如去掉字串中的空白字元、取代Tab字元、分解字串等,現在都可以由Visual Basic語言完全代勞了。

Visual Basic 6 也提供了一些好用的控制項,讓我們可以用月曆或List Box的形式選取或顯示日期。在顯示日期資訊給使用者時,Calendar和DTPicker控制項讓我們可以做得更漂亮也更省事。

本章所要討論的就是如上述的新特色以及一些處理變數的技巧,如Variant和模擬不帶正負號整數的方法。另外,本章也要討論預先定義的常數和建立使用者自定型別 (User-Defined Type,UDT) 結構等課題。

如何模擬不含正負號的整數?
 

Visual Basic的整數資料型別有三種:長整數Long、一般整數Integer和單位元組整數Byte。Long是32位元、含正負號的整數型別,它的有效範圍是從-2,147,483,648到+ 2,147,483,647;Integer是16位元、含正負號的整數型別,有效範圍在-32,768和+32,767之間;而Byte則是用來存放不含正負號、8個位元長的整數,其有效範圍在0到255之間,它是在Visual Basic中唯一不含正負號的整數型別。

許多API函式需要傳遞不帶正負號的16位元整數(unsigned 16-bit integer),但是,很可惜Visual Basic並不支援unsigned 16-bit整數型別。雖然還是可以用帶正負號的整數(如Integer )來呼叫這些API函式,但是當被傳遞的整數值落入了帶正負號整數的負數範圍(如Integer的-1和-32768之間)時,你必須用一些特殊的方法將需要傳給API函式的值加以轉換。在這裡,我們要介紹一些方法來模擬unsigned 16-bit整數。


注意

在編譯程式時,如果選擇將程式編譯成機器碼,你必須小心決定是否要關閉整數的溢位檢查的功能。因為關閉這個功能在某些情況下會造成不正確的整數計算結果,但關閉它的好處是可以加快整數的運算,並且有效地得到不含正負號整數的計算結果。


利用長整數的型別轉換技巧
 

從資料型別的位元長度來說,Integer型別的位元長度是16位元,可以完全容納unsigned 16-bit整數變數內含的資料;但是,unsigned16-bit整數的有效範圍是在0到65,535之間,而Integer型別有效範圍卻在-32,768和+32,767之間,因此,如果用Integer變數來模擬unsigned 16-bit整數,我們必須針對落入Integer負數範圍的整數特別加以處理,不能貿然地直接把unsigned 16-bit整數指定給Integer變數。我們可以先把unsigned 16-bit整數指定給一個Long整數變數,然後用以下這個計算式把數值指定給Integer變數:

intShort=(intLong And &H7FFF&)-(intLong And &H8000&)

在上式中,intLong存放的是原本要傳給API函式的unsigned 16-bit整數值,intShort雖然是一個Integer,但它所內含的值是經過轉換後的unsigned 16-bit整數值,因此,intShort可作為API函式的引數。

以上這個計算式運用邏輯運算子And和16進位的位元遮罩(Bit Mask)來進行位元運算,以達成把32位元整數(Long)轉換為unsigned 16位元整數的目的。

如果你要將經過轉換後的數值還原為Long整數,你可以使用以下這個運算式:

intLong = intShort And &HFFFF&

在呼叫API函式之前,應該把unsigned 16-bit整數轉換為Integer整數intShort,然後把intShort傳給API函式;而對於API函式傳回的unsigned 16-bit整數,應該把它用Integer整數intShort加以接收,然後把intShort還原為Long整數。

在這裡我們要特別提醒你,unsigned16-bit整數雖然可以被存放在含正負號的Integer變數裡傳給API函式,但是如果直接將它計算或列印的話,它會被視為負數。

利用資料結構包裝只含正值的Byte數值
 

Visual Basic不提供C語言中的Union結構,但是你可以在Visual Basic中模擬類似Union結構的特性─在兩個UDT結構之間以LSet指令交換內含的資料。這個技巧讓我們可以很容易地在Byte和Integer之間進行資料的互相轉換,因此,我們可以把一個帶正負號的Integer分為兩個不帶正負號的Byte,這兩個Byte分別代表Integer的高低位元組,以下的範例在說明這個技巧:

Option Explicit 

Private Type UnsignedIntType
    lo As Byte
    hi As Byte
End Type 

Private Type SignedIntType
    n As Integer
End Type

這兩個UDT結構各自佔用了兩個位元組的記憶空間,透過這兩個UDT結構所宣告的變數,我們可以在這兩種UDT結構之間相互傳遞資料,請看下例:

Private Sub Form_Click()    
'Create variables of user-defined types
Dim intU As UnsignedIntType    
Dim intS As SignedIntType
'Assign high and low bytes to create integer    
intU.hi = 231
intU.lo = 123    
'Copy binary data into the other structure
LSet intS = intU    
Print intS.n, intU.hi; 
intU.lo
'Assign integer and extract high and low bytes
intS.n = intS.n - 1  'Decrement integer for new value
'Copy back into the other data structure    
LSet intU = intS
Print intS.n, intU.hi; 
intU.loEnd 
Sub

把這兩段程式放在一個表單裡加以執行,你看見第一個Print的結果是:含正負號的intS.n存放的內容是-6277,另外兩個Byte元素─高位元組intU.hi及低位元組intU.lo各別存放著231及123。第二個Print執行的結果則是-6278、231和122。在這個例子裡,兩個由不同UDT結構所宣告的變數intU和intS,透過LSet完成了二進位資料複製的工作。在倒數第三行的陳述式中,intS裡的元素n減了1,所以intS.n最後的值變成了-6278;這個減1之後的intS,再透過LSet指定給intU,intU的高位元組元素intU.hi保持著原值,但低元組intU.lo比原來的值少了1。這個例子告訴你帶正負號的Integer可以被切割為兩個不帶正負號的Byte。

你可以將這個例子加入一些文字方塊控制項,標籤控制項以及指令按鈕控制項等物件加以美化,如圖3-1。

LSet可以把UDT結構中的二進位資料複製給另一個UDT結構所定義的變數,這讓我們可以有效地把記憶體中的二進位資料,當作各種型別的資料加以處理。


警告:

在Windows 95和Windows NT的環境下,UDT結構中的元素在記憶體中不是緊密排列的。Windows 95和Windows NT以每四個位元組作為一個記憶體區段,UDT結構中的每一個元素都會佔用一個到數個記憶體區段,如果某個記憶體區段無法由結構中的某個元素完全填滿時,則這個區段剩餘的空間會被塞入充填位元(padding),以維持系統記憶體配置的一致性。因此,使用LSet來指定某個UDT結構中的資料給另一個結構變數時,你必須小心地觀察搬移後的結果看看有沒有問題。



 

 圖3-1 以一個帶正負號的Integer包含兩個不帶正負號的Byte

參考資料:

•C語言很適合把整數資料中的位元組加以包裝(Pack)以及解包裝(Unpack),有關於以Visual C++ 產生DLL這方面的資訊,請另外參考 第二十七章"進階程式設計技巧" 。


如何處理True/False資料?
 

Boolean資料型別變數只能含True或是False,通常這個型別的變數是用來存放比較運算或邏輯運算的結果,下面這個例子把一個比較運算的結果放入一個Boolean變數blnTestResult中,然後把blnTestResult加以顯示。

Private Sub Form_Click()
    Dim blnTestResult As Boolean
blnTestResult = 123 < 246
Print blnTestResult
End Sub

當你直接列印或顯示一個Boolean變數的內容時,其結果必定是True或者是False。在本例中,這個程序輸出的結果是True。

Visual Basic支援許多資料型別的自動強制轉換,幾乎可以任由你無限制的指定任何型別的資料給另一種型別的變數。因此,即使Boolean變數只能含有True或是False,仍然可以指定數值或字串給Boolean變數,但是請小心,資料型別自動強制轉換的結果,可能不是你所期望的。

下列的例子告訴你,在不同型別變數之間作資料轉移,可能會造成不可預期的結果。這個例子列印結果是True、True和False,也就是說,bytA是True,intB也是True,但是bytA卻不等於intB!這就是資料型別不同而進行內部的強制型別轉換時所造成的不可預期結果。

Private Sub Form_Click()
    Dim bytA As Byte
    Dim intB As Integer
    bytA = True
    intB = True
    Print bytA = True
    Print intB = True
    Print bytA = intB
End Sub

Boolean型別運用的正則是:使用And、Or和Not等運算子,在同一個運算式中進行運算,使運算的結果不是True就是False。如下例:

If blnExit And Not blnChange Then End

你也可以用算術運算子來作相同的工作,但是邏輯運算子可以使程式更簡短、易讀,而且可以避免不必要的型別強制轉換。

如何使用Byte陣列?
 

Byte陣列最大的用處是,它讓我們可以傳遞二進位的資料給32位元的API函式。16位元的VISUAL BASIC應用程式和32位元的Visual Basic應用程式有一個很大的差別:在32位元的程式裡,字串是由Unicode字元所組成,每一個字元的長度是2個位元組,而16位元應用程式中的字元只有一位元組長,我們稱之為ANSI字元。Visual Basic在處理字串時,會把Unicode字串轉成ANSI字串,但是如果字串中含有二進位資料,轉換後的結果會變得無法辨識。因此,請養成一個好習慣:傳遞可列印的字串資料用字串變數,傳遞二進位資料用Byte陣列。

傳遞Byte陣列給API函式
 

Visual Basic在處理字串時會將字串做適當的轉換,但是Byte陣列中所存放的資料不會被系統轉換,因此你可以傳遞Byte陣列給許多API函式。在以下的範例中,我們用兩組程式來比較傳遞字串和傳遞Byte陣列的不同之處。這兩組程式都使用GetWindowsDirectory API函式來取得Windows目錄所在的路徑,如圖3-2。


 

 圖3-2 用API函式取得Windows目錄所在的路徑

基本上,這兩個例子都使用同樣的API函式,但它們的相異之處在GetWindowsDirectory函式的宣告部分,第一個例子用字串作為參數,第二個例子則用Byte陣列,首先我們先看第一個例子:

Option Explicit 

Private Declare Function GetWindowsDirectory _
Lib "kernel32" _
Alias "GetWindowsDirectoryA" ( _
    ByVal lpBuffer As String, _
    ByVal nSize As Long _
) As Long 

Private Sub Form_Click()
    Dim n As Integer
    Dim strA As String
    'Size the string variable
    strA = Space$(256)
    n = GetWindowsDirectory(strA, 256)
    'Strip off extra characters
    strA = Left$(strA, n)
    Print strA
End Sub

這個範例利用字串參數lpBuffer傳回Windows目錄的路徑。在呼叫GetWindowsDirectory函式之前,我們先將strA的長度預設為256個字元,在呼叫函式之後,我們把strA多餘的空白部分截掉。


警告:

在呼叫函式前,請務必要將接收函式傳回值的字串變數或Byte陣列作長度設定,否則程式很可能會當掉。


我們再看看使用Byte陣列的例子:

Option Explicit

Private Declare Function GetWindowsDirectory _
Lib "kernel32" _
Alias "GetWindowsDirectoryA" ( _
    ByRef lpBuffer As Byte, _
    ByVal nSize As Long _
) As Long 

Private Sub Form_Click()
    Dim intN As Integer
    Dim bytBuffer() As Byte
    Dim strA As String
    'Size the Byte array
    bytBuffer = Space$(256)
    intN = GetWindowsDirectory(bytBuffer(0), 256)
    strA = StrConv(bytBuffer, vbUnicode)
    'Strip off extra characters
    strA = Left$(strA, intN)
    Print strA
End Sub

請比較一下這兩個例子在函式宣告部分的差異。在第二個例子裡,我們把原來在第一個例子函式宣告部分的ByVal改成了ByRef。原來在第一個例子函式宣告部分是:

ByVal lpBuffer As String

在第二個例子裡,我們把它改成:

ByRef lpBuffer As Byte

因為字串變數的值就是字串的起始位址,因此,在第一個例子裡我們用ByVal把字串的起始位址傳給API函式,讓函式可以改變這個位址中的資料,達到資料傳遞的目的。然而,在第二個例子裡呼叫GetWindowsDirectory函式時,我們則以ByRef的方式傳遞bytBuffer(0) 給函式,這表示我們把bytBuffer陣列的第一個元素的位址傳給函式,以便讓該函式可以傳回資料。函式呼叫完畢之後,我們用下面這行程式碼把Byte陣列的二進位資料轉換成合法的Visual Basic字串:

strA = StrConv(bytBuffer, VbUnicode)

在這裡我們要對字串與Byte陣列之間的資料相互指定作更深入的說明。

動態的Byte陣列是可以直接指定給字串變數的,就像這樣:

bytBuffer=strA
strB=bytBuffer

當你指定某個字串給一個Byte陣列時,Byte陣列中所包含的位元組數目是字串裡字元數目的兩倍,因為字串中的每一個Unicode字元都是兩個位元組。而且,當ASCII字元所形成的字串被轉成Byte陣列後,Byte陣列裡每隔一個位元就有一個0。

在第二個例子裡,GetWindowsDirectory函式的引數不是Unicode格式,必須把它由Byte陣列轉成Unicode字串,因此我們用了StrConv函式。

上述的轉換把在陣列Buffer裡的每一個位元組都變成了兩位元組長的資料,存放在字串變數裡。如果用Len(strA) 和Ubound(bytBuffer) 來檢查資料長度,得到的結果是一樣的,但是如果用LenB(strA),你會發現LenB(strA) 告訴你的資料長度是Len(strA) 計算結果的2倍。這是因為Len告訴你Unicode字元的數目,而LenB告訴你位元組的總數。

總而言之,把API函式中的字串型別參數改成Byte陣列參數時,要把ByVal改成ByRef,並且必須傳遞Byte陣列的第一個位元;另外,如果要把Byte陣列中的資料轉換成字串,請記得使用vbUnicode常數來呼叫StrConv函式。

Byte陣列和字串之間的資料轉換
 

為了簡化Byte陣列和字串之間的資料轉換,Visual Basic特別允許使用者在任何動態的Byte陣列和任何字串之間相互指定資料。


注意:

只有當Byte陣列被宣告為動態陣列的情況下,才可以指定字串給Byte陣列。如果Byte陣列的長度是固定的,那麼就不可以作這種指定。


宣告動態Byte陣列最簡單的方式就像這樣:

Dim bytBuffer() As Byte

而下面的例子則是宣告一個固定長度的Byte陣列,Visual Basic不允許把字串指定給這種Byte陣列。

Dim bytBuffer(80) As Byte

如何處理Date和Time型別的資料?
 

Date型別的變數是一個8位元組的變數,它包含了系統目前的日期和時間,如果把Date變數的內容列印出來,你可以看到年、月、日、時、分、秒等資料,列印的格式則根據系統的設定。

使用日期相關的控制項
 

DTPicker和Calendar控制項提供了十分方便的方式讓我們顯示或者是取得日期資訊。DTPicker包括在Microsoft Windows Common Control第二部分(MSCOMCT2.OCX),而Calendar控制項則獨立定義於MSCAL.OCX檔中。

我們可以利用DTPicker控制項在一個清單方塊 (List Box) 中顯示或是取得日期資料,點選DTPicker控制項後,使用者可以在一個小月曆中選取某個日期,如圖3-3所示。


 

 圖3-3 DTPicker控制項也允許使用者直接在DTPicker的文字方塊中鍵入日期資料。

Calendar控制項顯示出來的是一個更完整的月曆,它比DTPicker控制項佔更多的空間,但也提供了較豐富的選項,如圖3-4所示。


 

 圖3-4 Calendar控制項讓設計者可以選擇不同字型及其他顯示選項。

以上這兩種控制項被使用時,它們的預設日期都是當天日期,如果想在程式中改變這個日期,只要把日期資料指定給Value屬性即可。不過,你必須先把時間資訊去掉,因為指定時間資訊給Calendar控制項會造成錯誤。以下這兩行程式碼告訴你正確和錯誤的示範:

calDate.Value = Date        'The right way
calDate.Value = Now         'The wrong way;cause an error

DTPicker控制項本身並不會有上述的問題,問題在於DTPicker和Calendar一起使用時可能會有問題。我們用以下這四行程式碼說明這個情況。我們把dptDate(DTPicker控制項)資料傳給calDate(Calendar控制項),程式就發生了錯誤:

dtpDate.Value = Date
calDate.Value = dtpDate.Value  'This line runs fine,
dtpDate.Value = Now
calDate.Value = dtpDate.Value  'but causes an error here!

指定資料給Date變數
 

如果要直接指定含日期時間的資料給一個Date變數,你必須在這項資料的前後加上#,而且指定的日期時間在內容和格式上都必須正確,否則Visual Basic會告訴你資料錯誤。請看下面這個例子:

Dim dtmD As Date
dtmD = #11/17/96 6:19:20 PM#

在Visual Basic裡有一些函式可供設計者把數字轉換Date型別的資料。DateSerial函式可以把三個數字合併為Date型別資料的年月日部份,TimeSerial則可以把三個數字合併為Date型別資料的時分秒部份。如果要同時合併這兩組資料,你可以把它們加(+)在一起:

dtmD = DateSerial(1996,11,17) + TimeSerial(18,19,21)

如果要把代表日期時間的字串轉換成Date型別資料,就要用DateValue和Time Value函式。

dtmD = Date Value ("11/17/96")
dtmD = Time Value ("18:19:20")

同樣的,它們也可以"加"在一起。

dtmD = DateValue("Nov-17-1996")+ TimeValue("6:19:20 PM")

顯示Date或Time型別的資料
 

Format函式提供了很大的彈性讓我們可以方便地列印Date或Time型別的資料,以下就是一些Visual Basic已經預先為我們定義好的格式:

Print Format(dtmD, "General Date")  '11/17/96 6:19:20 PM
Print Format(dtmD, "Long Date")     'Sunday, November 17, 1996
Print Format(dtmD, "Medium Date")   '17-Nov-96
Print Format(dtmD, "Short Date")    '11/17/96
Print Format(dtmD, "Long Time")     '6:19:20 PM
Print Format(dtmD, "Medium Time")  '06:19 PM
Print Format(dtmD, "Short Time")   '18:19

除了這些現成的格式之外,你也可以自行定義自己想要的輸出格式,請看下例:

strA = Format(dtmD, "m/d/yyyy  hh:mm AM/PM") '11/17/1996 06:19 PM

我們再看一個顯示月份名稱的範例格式:

strMonthName = Format (D,"mmmm")  'November

你可以在線上說明中找到更多有關如何合併日期時間格式的資訊。

從日期或時間資料中擷取部份資料
 

Visual Basic提供了許多函式可以讓我們擷取Date變數中部份的資料,以下就是這些相關的函式:

Print Month(dtmD)   '11
Print Day(dtmD)     '17
Print Year(dtmD)    '1996
Print Hour(dtmD)    '18
Print Minute(dmtD)  '19
Print Second(dtmD)  '20
Print WeekDay(dtmD) '1

另外值得一提的是,Visual Basic有一些內建的常數可以用來代替WeekDay運算的結果:vbSunday代表1,vbMonday代表2,以此類推,一直到vbSunday代表7。

計算Date和Time資料
 

Date型別的變數可以直接作數學運算,例如,你可以設計一個程式以"多少天"來表示你的年紀,如圖3-5。只要把你的出生年月日放在一個Date變數中,然後用Now這個函式去減該變數就可以得到這個結果。


 

 圖3-5 使用Date變數計算兩個日期之間的日數

如果要在"時分秒"之間互相換算,通常你會用24或60來作乘除運算以得到結果,但Visual Basic的DateSerial和TimeSerial函式提供了一個更好的方法幫你做"時分秒"的換算。例如,如果你想知道目前時刻過後第10000分鐘的正確日期,你可以用這個方法:

dtmD = Now + TimeSerial(0, 10000, 0)

把dtmD加以顯示,就會看到正確的日期。

Date和Time型別資料的正確性檢查
 

如果指定的日期或時間是不合法的,那麼Visual Basic會給你一個"Type Mismatched"的錯誤訊息。如果要防止使用者輸入不合法的日期到你的程式裡,最好的方法是給使用Calendar控制項,提供一個類似月曆的輸入界面,這樣,在某個月份中,使用者只能選擇那個月才會有的日期。


參考資料:

• 第三十一章"日期和時間" 提供了一個實用的範例─VBCal應用程式,這個範例告訴你如何在應用程式中提供選擇日期的對話方塊。


如何處理Variant型別的資料?
 

Variant是一種極具彈性的資料型別,幾乎可以說任何型別的資料都可以存入Variant變數裡。舉凡陣列、物件、UDT結構甚至其他的Variant變數,無一不能存放在Variant變數裡。

如果在宣告某個變數時未宣告其型別,Visual Basic會自動把它當成Variant變數。

由Variant所組成的陣列,更展現了Variant型別驚人的彈性。一般陣列中所含的每一個元素都必須屬於同一種資料型別,不容許字串及數值同時存在於同一個陣列裡,但Variant陣列就沒有這個限制。下一個例子就是一個存放整數、字串及另一個Variant陣列的Variant陣列。

Option Explicit 

Private Sub Form_Click()
    Dim i      'Note that this defaults to Variant
    Dim vntMain(1 To 3) As Variant
    Dim intX As Integer
    Dim strA As String
    Dim vntArray(1 To 20) As Variant
    'Fill primary variables
    strA = "This is a test."
    For i = 1 To 20
        vntArray(i) = i ^ 2
    Next i
    'Store everything in main Variant array
    vntMain(1) = intX
    vntMain(2) = strA
    vntMain(3) = vntArray()
    'Display sampling of main Variant's contents
    Print vntMain(1)         '0
    Print vntMain(2)         'This is a test.
    Print vntMain(3)(17)     '289
End Sub

最後一行陳述式告訴我們要如何去取得雙重Variant陣列裡的某一個元素,這個方法就類似我們去取得一個二維陣列裡的元素,只是語法上有所不同。

For Each迴圈
 

Variant在For Each...Next迴圈中扮演了一個極重要的角色,我們可以用這個迴圈一一存取在一般陣列或物件集合(collection)裡的每一個元素或物件成員。以這個迴圈逐一存取陣列元素時,必須用Variant變數,而存取物件集合裡的物件成員時,你可以用Object變數,也可以用Variant變數。

Variant參數
 

Variant是一種很好用的參數型別,尤其是把Variant當作物件屬性時,你可以傳遞各種不同型別的資料給物件。只要把各種不同型別的資料指定給Variant變數,然後就可以把這個Variant變數當作引數傳給函式。

與Variant相關的函式
 

有幾個和Variant相關的函式是你必要知道的:TypeName函式傳回一個字串,告訴你Variant變數內含的資料屬於哪一種型別;IsNumeric和IsObject傳回邏輯的真偽值,告訴你Variant變數裡是否存放數值或者是否存放物件。

空Variant和Null Variant的不同之處
 

空的(Empty)Variant和Null Variant有所不同:一個Variant變數經過宣告後,如果不指定任何值給它,它就是一個空的Variant變數;經過指定之後它就不是空的Variant變數了。Null是一個值,可以指定給Variant變數。(指定Null給空的Variant變數之後,這個變數就不再是空的Variant變數。)一個含有Null值的Variant變數表示它不存放任何合法的資料。Null經常被使用在資料庫應用程式中,它代表未知或者遺漏的資料。

資料型別的強制轉換
 

Variant相當的有彈性,但你要小心防範資料型別強制轉換時所可能發生的問題。下面這個例子執行的結果可能會讓你覺得很訝異。

Option Explicit 

Private Sub Form_Click()
    Dim vntA, vntB
    vntA = "123"
    vntB = True
    Print vntA + vntB            '122
    Print vntA & vntB            '123True
    Print vntA And vntB = 0      '0
    Print vntB And vntA = 0      'False
End Sub

第一個Print陳述式把vntA和vntB兩個Variant變數當成數字,第二個Print陳述式把它們都當都成字串,第三個和第四個Print陳述式所產生的結果,就不是三言兩語就能解釋得清楚了。給你的建議是:小心檢查輸出的結果,看看結果是不是如你所預期的。

如何處理字串?
 

Visual Basic增加了五種處理字串的函式:

  • Replace函式可以用一組字元取代某個字串中的另一組字元,也可以讓我們在文字方塊或其他文字資料來源中取代所有的特定字串。
     
  • Split函式讓我們以分隔字元,如空白或Tab,將某個字串分割為數個字串。
     
  • Join函式與Split的功能相反,它將幾個字串或字串陣列加上指定的分隔字元,合併成一個較大的字串。
     
  • Filter函式可以和Split函式及Join函式並用。你可以用Filter函式在字串陣列中搜尋某個子字串,Filter函式會將所有含此子字串的字串放在一個陣列中傳回。
     
  • InStrRev函式(與InStr函式反向操作的函式)讓你由字串尾端開始搜集目標字串。
     

圖3-6所顯示的是一個叫做StringFun.VBP的應用程式,這個程式展示了上述的每一個新函式。以下幾節我們就來討論StringFun.VBP的各項功能。


 

 圖3-6 StringFun.VBP所展示的各個字串處理新功能

在字串中取代某些字元
 

轉換字串的格式是一項常用的程式設計技巧,通常字串轉換都會牽涉以一個分隔字元分開的字串去取代另一個字串,Replace函式正可以一步就解決字串取代的問題。以下這個例子用Replace函式將空白字元與換行字元相互置換。

'cmdReplace_Click uses Replace to
'do a global search and replace
Private Sub cmdReplace_Click()
    Static blnReplaced As Boolean
    'Replace spaces with carriage returns
    If Not blnReplaced Then
        txtString = Replace(txtString, " ", vbCrLf)
    'Replace carriage returns with spaces
    Else
        txtString = Replace(txtString, vbCrLf, " ")
    End If
    blnReplaced = Not blnReplaced
End Sub

分解以及合併字串
 

當我們從文字方塊中取得字串或將字串重組時,分解字串和合併字串是最常用的技巧。Split函式和Join函式可以用來將一段文章分解為個別獨立的字串,改變這些字串的順序,然後加以顯示,就如以下這個例子:

'cmdReverse uses Split and Join to
'reverse the order of words in a text box
Private Sub cmdReverse_Click()
    Dim strForward() As String
    Dim strReverse() As String
    Dim intCount As Integer
    Dim intUpper As Integer
    'Tokenize the text
    strForward = Split(txtString, " ")
    'Initialize the other array
    intUpper = UBound(strForward)
    ReDim strReverse(intUpper)
    'Reverse the order of words in strReverse
    For intCount = 0 To intUpper
        strReverse(intUpper - intCount) = strForward(intCount)
    Next
    'Detokenize the text
    txtString = Join(strReverse, " ")
End Sub

你可以把Split函式和Join函式視為轉換函式,Split將某個字串轉成字串陣列,而Join則將字串陣列合併為單一字串。既然可以將字串轉成陣列,那麼你便可以運用標準的陣列排序方法將陣列元素排序,也可以運用過濾器 (Filter) 從中選出想要取得的字串元素。

運用Filter函式
 

Filter函式接收的是一個字串陣列,傳回的則是含有指定字串的字串陣列。Filter函式經常和Split以及Join函式併用,處理文字方塊中的資料。以下這個例子告訴你如何使用Filter函式。

'cmdFilter_Click uses Filter to display only
'the words containing a specific set of characters
'This is useful when narrowing a list of items based
'on what a user types
Private Sub cmdFilterClick()
    Dim strFilter As String
    Dim strWords() As String
    'Get a substring to check for
    strFilter = InputBox("Show only words containing:")
    'If cancelled, then exit
    If strFilter = "" Then Exit Sub
    'Get rid of carriage returns
    txtString = Replace(txtString, vbCrLf, " ")
    'Split string into an array of single words
    strWords = Split(txtString, " ")
    'Get a list of the words containing strFilter
    strWords = Filter(strWords, strFilter)
    'Display the list in the text box
    txtString = Join(strWords)
End Sub

搜尋字串
 

大部分的文字編輯器使用者都需要文字搜尋的功能。InStr函式提供順向搜尋文字的功能,而InStrRev函式則提供反向搜尋文字的功能,請看下例參考這兩個函式的用法。

'cmdBack_Click and cmdForward_Click
'use InStr and InStrRev to perform forward and
'backward searches through a text box.
'These techniques can be used to selectively
'replace text
Private Sub cmdBack_Click()
    Dim strFind As String
    On Error GoTo errNotFoundRev
    'Get a string to find
    strFind = InputBox("String to find:", "Search Backward")
    txtString.SelStart = _
        InStrRev(txtString, strFind, txtString.SelStart) - 1
    txtString.SelLength = Len(strFind)
    Exit Sub
errNotFoundRev:
    MsgBox strFind & " not found.", vbInformation
End Sub 
Private Sub cmdForward_Click()
    Dim strFind As String
    On Error GoTo errNotFoundFor 
    'Get a string to find
    strFind = InputBox("String to find:", "Search Forward")
    txtString.SelStart = _ 
        InStr(txtString.SelStart + 1, txtString, strFind)  1
    txtString.SelLength = Len(strFind)
    Exit Sub
errNotFoundFor:
    MsgBox strFind & " not found.", vbInformation
End Sub

請注意,在InStr函式的引數中,第一個引數是開始搜尋的位置,而在InStrRev函式中,第三個引數才是開始搜尋的位置;這個差異很容易讓人產生混淆。

如何處理物件變數?
 

物件(Object)是一種很特殊的變數,它們不僅存放著資料,也可以執行一些動作,因此,在處理物件變數時,我們要多花一些心思。

新物件
 

宣告物件變數不同於宣告一般變數。例如,我們以Dim宣告一個Integer變數,當我們初次使用這個變數時,系統會對這個變數加以初值化,自動指定0給它。在另一方面,系統則不會對物件變數進行初值化的工作──在宣告一個物件變數後,如果直接顯示其型別名稱,你得到的答案是"Nothing",如下面這個例子:

Dim frmX As Form1
Debug.Print TypeName(frmX)    'Displays "Nothing"

如果不指定一個物件的執行實體(Instance)給物件變數,就不能對這個變數作任何動作。使用New這個關鍵字就可以產生一個物件實體,請看這個例子:

Dim frmX As Form1
Debug.Print TypeName(frmX)    'Displays "Nothing"
Set frmX = New Form1
Debug.Print TypeName(frmX)    'Displays "Form1"

New指令是個相當重要的指令,它用來產生一個物件變數。不過這個物件變數的內容在起始時是Nothing,一直要等到你指定一個物件給它,物件變數才會有真正的值。

你也可以在宣告物件變數的同時使用New指令,如下所示:

Dim frmX As New Form1
Debug.Print TypeName(frmX)    'Displays "Form1"

宣告物件變數時使用New指令是告訴Visual Basic,將Nothing指定給該物件變數,在遇見可執行指令時才產生物件;上例中的Dim指令並不是可執行指令,Visual Basic會等到遇見Debug.Print時才產生物件。看來這似乎並不是什麼重點,但忽略了這點卻可能造成非預期的結果。下例中的輸出結果並不是我們預期的 "Nothing":

Dim frmX As New Form1
Set frmX = Nothing
Debug.Print TypeName(frmX)    'Displays "Form1"

上面這個例子在產生了Form 1物件後又立刻將之銷毀,但Debug.Print這一行卻又再產生了另一個新的Form1物件。我們可以避免這樣的錯誤,只要別在宣告物件變數時使用As New即可,請看下例:

Dim frmX As Form1
Set frmX = New Form1          'Create an instance of the form
Debug.Print TypeName(frmX)    'Displays "Form1"
Set frmX = Nothing            'Destroy the form
Debug.Print TypeName(frmX)    'Displays "Nothing"

一般而言,若物件在它的程式碼有效範圍中會一直存在,你應該用:

Dim... As  New

而如果物件在其有效範圍中會被產生並且銷毀,那麼你該用:

Dim... As ...
Set... New ...

現存物件
 

一個物件變數只是用來引用(Refer)某個物件的執行實體,以便讓我們操控這個存在的物件,物件變數本身不表示它就是一個物件。我們可以用Set指令重新指定另一個物件執行實體給這個物件變數,如此一來,我們就可以透過同一個變數來控制另一個物件了。

Dim frmX As New Form1    'Create a new object
Dim frmY As New Form1    'Create another object
Set frmX = frmY          'frmX and frmY both refer to the same object

物件的運作方法
 

你可以使用Is運算子來比較兩個物件變數,藉以得知這兩個變數是否引用同一個物件,如下例:

Dim frmX As New Form1     'Create a new object
Dim frmY As New Form1     'Create another object 
Debug.Print frmX Is frmY  'Displays "False"
Set frmX = frmY           'frmX and frmY both refer to the same object
Debug.Print frmX Is frmY  'Displays "True"

失效物件(Dead Object)
 

一個物件可以存在多久?直到它們變成Nothing為止。

只要有物件變數引用著物件,或是這個物件可以在表單上讓使用者看見,則物件就會一直存在。藉由改變表單上某個物件的Visible屬性,你可以控制該物件的可見性,也就等於控制著物件的生命期。另外,你可以把物件A指定給引用物件B的物件變數,或是指定"Nothing"給引用物件B的物件變數,這樣就可以拋棄物件B。

Dim frmX As New Form1 'Create a new object
Dim frmY As Object    'Declare a variable of type Object
Set frmY = frmX       'frmX and frmY both refer to same object
Set frmX = Nothing    'frmY is still valid; object persists
Set frmY = Nothing    'Object goes away

基本上,只要按照上述的方法就可以將物件銷毀,但是事實上有些早期版本的物件並非這麼容易就可以銷毀(包括一些由Microsoft提供的物件)。現在大部份的物件都較能夠被控制了,但在面對一些看不見的物件時,還是謹慎一點較好。


參考資料:

• 第五章"物件導向程式設計" 有更多有關於物件的資訊。


如何使用預先定義的常數?
 

編譯常數
 

如果要發展既能在16位元環境(Windows 3.1)也能在32位元環境(Windows 95和Windows NT )下執行的軟體,你必須要用Visual Basic 4。在Visual Basic的各個版本中,Visual Basic 4是唯一能夠支援16位元和32位元環境的版本。要產生這樣的軟體,你需要使用Win16和Win32編譯常數。這兩個編譯常數可以用來指示目前的發展環境是16位元還是32位元,讓編譯器可以因應不同的環境,選擇性地編譯部份的程式碼。這些編譯常數會在程式中和 #IF... THEN... #ELSE等編譯指令一起使用。

雖然Visual Basic 5和Visual Basic 6也支援這些編譯常數,但因為它們只能在32位元Windows環境下執行,所以Win32被定義為True不能改變,而Win16也被定義為False不能改變。

Visual Basic預先定義的常數
 

Visual Basic所提供的常數十分的豐富,可以說應有盡有,如果要看看有哪些常數可供使用,要在主功能表的「檢視」底下選擇「瀏覽物件」,在「瀏覽物件」視窗中的第一個下拉式清單中選擇「VBA」或「VBRUN」,然後點選「物件類別」中你想要看的常數集合,就可以在右邊的顯示區域看到一些以vb作為名稱開頭的常數,如圖3-7。


 

 圖3-7 從「瀏覽物件」視窗中瀏覽Visual Basic預先定義的常數

Visual Basic幾乎為所有的控制項和函式的參數都預先定義了常數,例如vbModal和vbModeless可供表單的Show方法使用;vbRed、vbBlue和vbGreen是表示顏色的常數,可以給繪圖函式使用;vbCr、vbLf、vbCrlf和vbTab可以當作一般的文字字元。下面的例子告訴你如何插入一個換行 (carrige return/line peed) 字元到某個字串裡:

strA = "Line One" & vbCrLf & "Line two"

在你每次想要設定某個屬性的值或是想傳入數值給某個方法之前,別忘了先去找一找Visaul Basic已經定義好的常數,這樣你的程式本身就會變得更文件化,更易讀也易於維護。

使用者自定的常數
 

早期版本的Visual Basic可以讓你用Const關鍵字來定義你自己的常數,現在Visual Basic則加入了新的 #Const,由 #Const定義的編譯常數只能和 #IF... THEN... #ELSE等編譯指令一起使用,以便讓編譯器作條件式的編譯。但是請注意,用Const關鍵字定義的常數不能和編譯指令一起使用。

在某些情況下,預先定義的常數可以幫助我們處理較複雜的字串。在下面這個例子裡,我們列出一個字串常數,這個字串常數輸出的結果是一直列的數字。如果是早期的Visual Basic,就必須用好幾行陳述式才能做得到,在這裡我們只用了一個Const和Print就辦到了。

Option Explicit 

Private Sub Form_Click()
    Const DIGITS = _
        "1" & vbCrLf & _
        "2" & vbCrLf & _
        "3" & vbCrLf & _
        "4" & vbCrLf & _
        "5" & vbCrLf & _
        "6" & vbCrLf & _
        "7" & vbCrLf & _
        "8" & vbCrLf & _
        "9" & vbCrLf
    Print DIGITS
End Sub

對於在模組中的常數,我們可以用Private和Public關鍵字來限定它們的有效範圍:如果把常數宣告成Private,則這個常數只能在模組中使用;如果宣告成Public,那麼常數就可以在整個專案範圍裡被使用。請注意,在程序裡面的常數不能用Public或Private來宣告,而且程序中宣告的常數的有效範圍只限於該程序。

列舉集合
 

利用列舉集合Enum可以讓你把一些常用的整數加以命名並把它們集中在一起。列舉集合對於屬性程序(property procedure)特別有幫助。在下面的例子裡,屬性SpellOption只有三個可能值:0、1、2,我們把這些可能值加以命名並用下面這個CheckedState列舉集合把它們集中在一起:

'MailDialog class module-level declarations
Public Enum CheckedState
    Unchecked            'Enums start at 0 by default
    Checked              '1
    Grayed               '2, and so on
    'CantDeselect = 255  (You can set specific values too)
End Enum

對於以下這兩個屬性程序,我們用上述的Enum CheckedState來宣告它們的引數。

Public Property Let SpellOption(Setting As CheckedState)
    'Set the state of the Check Spelling check box
    frmSendMail.chkSpelling.Value = Setting
End Property 

Public Property Get SpellOption() As CheckedState
    'Return the state of the Check Spelling check box
    SpellOption = frmSendMail.chkSpelling.Value
End Property

你可以在「瀏覽物件」視窗中看到Enum在左邊的物件類別顯示區域裡面,而Enum的成員則在右邊的顯示區域裡一一列出。最後要提醒你,只有在Public物件類別模組裡的Public Enum才具有涵蓋整個專案的有效範圍。

旗標(Flag)和位元遮罩(Bit Mask)
 

And、Or和Not運算子最常被用在邏輯運算式裡,如下面這個例子:

If blnExit And Not blnChanged Then End

這三個運算子可以作位元運算(Bitwise Operation)。所謂位元運算是把某個數值裡的每一個位元和另一個數值裡的每一位元─作比較運算,這些用來作位元運算的數值就稱為旗標(Flags)。以下這個例子把兩個旗標比較運算後的結果指定給「列印」對話方塊的Flags屬性:

dlgPrint.Flags = cdlPDCollate Or cdlPDNoSelection

在大多數的情況下,用Or對兩個旗標作位元運算,相當於把兩個值相加(+),但是,當兩個旗標的值相同時,加法運算的結果和Or位元運算的結果就會有出入了,例如:1 + 1 = 2,但1 Or 1 = 1。在設定旗標值時,如果設想周延一些,就可以避免這個情況發生,基本上,使用Or是很安全的。

用And可以把旗標歸零,也可以檢查某個旗標的值。以下這個例子檢查「列印至檔案」選項是否被選取:

'Check whether Print To File is selected
If dlgPrint.Flags And cdlPDPrintToFile Then

上例中的And檢查第5個位元是不是1(&H20是cdlPDPrintToFile的值);如果這個位元是1,運算的結果就是True。以這種方法檢查旗標叫作使用位元遮罩(Bit Mask)。所謂位元遮罩就是把所有待檢查的旗標集合在一起,成為一個單一的檢查值,如01101101(十六進位值為 &H6D)。在位元遮罩裡每一個出現1的位置上,被檢查的數值在這些位置上的值也都必須是1,才能讓被檢查的值通過遮罩檢查。使用位元遮罩,就是本章一開始教你如何把帶正負號整數轉換成為不帶正負號整數的方法,帶正負號整數的數值範圍是從 &H8000(-32768) 到 &H7FFF(+32767),藉著過濾掉某些位元,我們就可以把帶負號的整數轉換為不帶正負號的整數:

intShort = (intLong And &H7FFF&)-(intLong And &H8000&)

如何產生使用者自訂型別結構?
 

Type關鍵字是用來宣告使用者自訂型別(User-Defined Type, UDT)資料結構的指令,它只告訴系統某個UDT結構所包含的內容,但並不實際產生一個結構的實體,要產生一個結構的實體,必須用Dim來產生一個UDT結構變數。

你可以在標準模組或Public物件模組中宣告一個Public UDT結構,而在表單中或是物件類別模組中,你只能把Type宣告為Private。當UDT結構和Variant資料型別在一起使用時,它們可以產生極具動態性的資料結構。下面這段程式就充分的表現了這個特色。

Option Explicit 

Private Type typX
    a() As Integer
    b As String
    c As Variant
End Type
Private Sub Form_Click()
    'Create a variable of type typX
    Dim X As typX
    'Resize dynamic array within the structure
    ReDim X.a(22 To 33)
    'Assign values into the structure
    X.a(27) = 29
    X.b = "abc"
    'Insert entire array into the structure
    Dim y(100) As Double
    y(33) = 4 * Atn(1)
    X.c = y()
    'Verify a few elements of the structure   
    Print X.a(27)    '29
    Print X.b        'abc
    Print X.c(33)    '3.14159265358979
End Sub

typX中的第三個元素被宣告為Variant。我們知道Variant變數可以存放任何資料,因此,我們可以指定一整個陣列給X裡的c元素。

記憶空間配置的對齊排列( Memory Alignment)
 

32位元的Visual Basic為了配合32位元的作業系統,按照每四位元為一個記憶區隔 (4-byte boundary) 的方式,把UDT結構中的元素作對齊排列(又稱DWORD Alignment)。這表示:第一,UDT結構實際上用到的記憶空間比預期中佔用的空間還大;第二,用LSet從一個UDT結構中搬移二進位資料給另一個UDT結構時,可能會產生無法預期的結果。

再一次提醒你,這種資料搬移的方法,不但可能會產生不可預期結果,更可能會造成不容易抓到的程式錯誤。

如何用物件類別產生新的資料型別?
 

物件類別(Class)或物件類別模組(Class module)定義物件的內容,告訴系統這種物件有哪些屬性(Property)及物件方法(Method),而真正的物件則是物件類別的一個執行實體(Instance)。

我們在這一節中將要介紹如何以簡單的物件類別來建立一個不帶正負號的Integer資料型別,到了 第五章"物件導向程式設計" ,我們將會討論更複雜的物件導向相關課題。

產生新的資料型別
 

本章的第一節告訴你如何模擬不帶正負號的整數,我們可以把這段程式放到一個叫UInt的物件類別裡,產生一種新的基本資料型別。

在UInt物件類別的模組層次中,物件類別模組定義了兩個UDT結構和兩個分別用來傳回高低位元組的Private資料成員。在這個例子裡,我們把這個物件類別的用途及其他相關的資料都寫在程式的註解部分,這樣可以讓物件類別模組更易於被了解以及更易於被重複使用到別的程式裡。

'Class UInt module level.
'Provides an unsigned integer type.
'
'Methods:
'   None 
'Properties:
'   Value (default)
'   HiByte
'   LoByte 

'Type structures used for returning high/low bytes
Private Type UnsignedIntType
    lo As Byte
    hi As Byte
End Type 

Private Type SignedIntType
    n As Integer
End Type 

'Internal variables for returning high/low bytes
Private mnValue As SignedIntType
Private muValue As UnsignedIntType

這個物件類別使用了模組層級的變數mnValue來存放屬性的設定值,但如果要存取這個變數則只能由Let Value和Set Value屬性程序控制。這些屬性程序實際上在做模擬整數所需的轉換工作:

Property Let Value(lngIn As Long)
    mnValue.n = (lngIn And &H7FFF&) - (lngIn And &H8000&)
    LSet muValue = mnValue
End Property

Property Get Value() As Long
    Value = mnValue.n And &HFFFF&
End Property

物件類別模組中的每一個屬性,都可以有一個Let屬性程序以及一個Get屬性程序;Let屬性程序用來設定屬性值,Get屬性程序用來傳出屬性值。下面這段程式中,屬性HiByte和LoByte都各有它們所屬的Get屬性程序和Set屬性程序。

Property Let HiByte(bytIn As Byte)
    muValue.hi = bytIn
    LSet mnValue = muValue
End Property 

Property Get HiByte() As Byte
    HiByte = muValue.hi
End Property 

Property Let LoByte(bytIn As Byte)
    muValue.lo = bytIn
    LSet mnValue = muValue
End Property 

Property Get LoByte() As Byte
    LoByte = muValue.lo
End Property

最後,UInt物件類別裡有一個公用函式,它會把帶正負號的整數值傳出。

Function Signed() As Integer
    Signed = mnValue.n
End Function

使用新的資料型別
 

用UInt物件類別定義一個物件變數時,要記得使用New關鍵字,這樣系統才會產生一個物件執行實體指定給該變數,就像這樣:

Dim uNewVar As New UInt

UInt物件類別中的Value屬性是這個物件類別的預設屬性(Default Property)(在 第五章 裡,我們會討論到如何設定預設屬性),因此,在存取預設屬性時,我們可以省略掉預設屬性的名稱,就像這樣:

UNewVar = 64552   '設定一個值給預設屬性

而一般的屬性如HiByte和LoByte被叫用時,就如同叫用任何其他Visual Basic裡的屬性一樣:

'Set the high byte
uNewVar.HiByte = &HFF 

'Return the low byte
Print Hex(uNewVar.LoByte)