5. 物件導向程式設計

物件導向程式設計(Object-Oriented Programming,OOP)是Visual Basic最重要的新特性之一,在本章中,我們要討論幾個主要OOP的觀念,提供一些實例,讓你讀完之後能夠具備自行研究OOP的基本知識與能力。

在Visual Basic裡,物件類別模組(Class Module)可以讓你用物件集合(collection)建構應用程式,使你的物件可以很有彈性地組織起來;ActiveX技術則能讓你建立跨不同應用程式執行的物件。運用物件導向的方法,你將會較能夠掌握大型專案的結構,同時不論個人單獨發展程式或多人共同發展程式都會變得更容易。

本章中,我們將要發展一個示範物件以及一個示範的ActiveX執行檔,簡單介紹如何處理物件集合,並且討論Visual Basic的兩個新特色──同名異式(Polymorphism)和Friend方法。

如何區分ActiveX執行檔和ActiveX DLL?
 

Visual Basic提供了兩種ActiveX元件(Component):ActiveX執行檔和ActiveX DLL。Visual Basic讓你把物件放在ActiveX執行檔或是ActiveX DLL元件裡,透過ActiveX執行檔或ActiveX DLL,可以讓其他的應用程式(用戶端應用程式)使用你的物件。當一個應用程式使用ActiveX執行檔所提供的物件時,它會產生一份ActiveX執行檔的執行實體(Instance),應用程式是以"out-of-process"的方式執行ActiveX執行檔的程式碼,換句話說,ActiveX執行檔會在它自己的執行緒(Thread)和工作區(Workspace)中進行處理,不會和用戶端應用程式共用一個程式碼空間(Code Space)。

在另一方面,ActiveX DLL並不是一個可以獨立執行的應用程式,但它提供了包含著物件的動態連結程式庫(Dynamic Link Library, DLL)給應用程式,應用程式以"in-process"的方式把連結的物件放在自己的process裡執行。這表示ActiveX DLL裡的程式碼是在應用程式所擁有的單一執行緒程式碼空間(Single-threaded Code Space)裡執行,因此,整個應用程式執行起來會更快速,更有效率。

這兩種ActiveX元件都很有用,Visual Basic讓你能產生這兩種元件。

如何在外部的ActiveX元件中建立所有的自訂物件?
 

如果只是要在應用程式裡使用你自己產生的物件,不需要把物件放在ActiveX元件裡。建立和使用自訂物件最簡單的方法就是在「標準執行檔」專案裡加入物件類別模組。

對你的應用程式而言,由你的物件類別模組所定義的物件,是這個應用程式的私有物件(如果要讓其他的應用程式也可以使用這個物件,就必須使用ActiveX元件)。你的應用程式可以用同一個物件類別模組來產生一個以上的物件執行實體,每一個物件都有它自己的資料。使用物件有一個好處:當物件被捨棄不用之後,由這個物件所佔用的記憶空間立刻會由系統收回;但使用物件最大的好處應該是:物件強化了程式的組織結構,讓程式更易於被了解。

以下這兩節介紹一個相當簡單的例子來說明如何在標準執行檔中產生物件。

如何產生新物件?
 

對於這個問題最簡單的答案就是:建立一個物件類別模組。在這裡我們提供一個很簡單的範例,讓你對建立物件類別有基本的認識。

 物件類別模組範例─Loan(貸款) 
 

Loan物件可以讓你計算貸款的每期應繳金額,我們以這個物件為例,說明如何用物件類別模組來產生Loan物件。

如果要建立一個物件類別模組,請先點選「檔案」功能表中的「建立新專案」,然後在「建立新專案」對話方塊中選擇「標準執行檔」。接著,在專案視窗中按一下滑鼠右鍵,在「新增」選項中選取物件類別模組,等「新增物件類模組」對話方塊出現後,在「物件類別模組」圖像上面按兩下滑鼠左鍵,然後再加入下面的程式碼到「程式碼」視窗的編輯區。接下來,把「屬性」視窗中的Name屬性改為Loan,最後把模組存成LOAN.CLS。

'LOAN.CLS - This is a class module that provides a
'blueprint for creating Loan objects

Option Explicit

'This variable is known only within this class module
Private mintMonths As Integer     'Number of months of loan

'~~~Property (R/W): Principal
Public Principal As Currency

'~~~Property (R/W): AnnualInterestRate
Public AnnualInterestRate As Single

'~~~Property (R/W): Months
'Lets user assign a value to Months property
Property Let Months(intMonths As Integer)
    mintMonths = intMonths
End Property

'Gets current value of Months property for user
Property Get Months() As Integer
    Months = mintMonths
End Property

'~~~Property (R/W): Years
'Lets user assign a value to Years property
Property Let Years(intYears As Integer)
    mintMonths = intYears * 12
End Property
'Gets current value of Years property for user
Property Get Years() As Integer
    Years = Round(mintMonths / 12#, 0)
End Property

'~~~Property (R/O): Payment
'Gets calculated Payment for user
Property Get Payment() As Currency
    Dim sngMonthlyInterestRate As Single
    'Verify that all properties are loaded
    If PropertiesAreLoaded() Then
        sngMonthlyInterestRate = AnnualInterestRate / 1200
        Payment = (-sngMonthlyInterestRate * Principal) / _
            ((sngMonthlyInterestRate + 1) ^ (-mintMonths) - 1)
    Else
        Payment = 0
    End If
End Property

'~~~Property (R/O): Balance()
'Gets array of loan balances
Property Get Balance() As Currency()
    Dim intCount As Integer
    Dim curPayment As Currency
    ReDim curBalance(0) As Currency
    If PropertiesAreLoaded() Then
        ReDim curBalance(mintMonths)
        curBalance(0) = Principal
        curPayment = Round(Payment, 2)   'Rounds to nearest penny
        For intCount = 1 To mintMonths
            curBalance(intCount) = curBalance(intCount - 1) * _
                (1 + AnnualInterestRate / 1200)
            curBalance(intCount) = curBalance(intCount) - curPayment
            curBalance(intCount) = Round(curBalance(intCount), 2)
        Next intCount

    End If
    Balance = curBalance
End Property

'~~~Method: Reset
'Initializes all properties to start over
Public Sub Reset()
    Principal = 0
    AnnualInterestRate = 0
    mintMonths = 0
End Sub

'Private function to check if all properties are properly loaded
Private Function PropertiesAreLoaded() As Boolean
    If Principal > 0 And AnnualInterestRate > 0 And mintMonths > 0 Then
        PropertiesAreLoaded = True
    End If
End Function

在主程式中產生的Loan物件和其他的物件一樣,它們都有屬性(Property)和物件方法(Method)。建立屬性最簡單的方法是在物件類別模組中宣告Public變數,LOAN.CLS裡的Principal和AnnualInterestRate就是兩個公用屬性。

Public Principal As Currency
Public AnnualInterestRate As Single

在LOAN.CLS中的變數mintMonths被宣告為Private,如下所示,因此,外界並不知道有這兩個變數存在,然而在這兩個變數裡所存放的資料只有Loan物件內部的程式碼可以直接存取。

Private mintMonths As Integer     'Number of months of loan

注意:

你可能會對變數字首mint感到奇怪。事實上,我們用m代表該變數是定義在模組中,而int則代表它是個Integer型別的變數。另一方面,你可能也會很好奇,為什麼Public變數沒有Hungarian Notation的字首?這是因為Public變數就如同物件的屬性一樣,而對於外界可以存取的屬性,大家公認的標準是不要有縮寫的字首。變數的縮寫字首其最大的用途是增加模組內部程式碼的可讀性,而外界可以存取的屬性,講究的重點是它命名的清楚程度,而非程式內部的可讀性。


mintMonths存放的資料是貸款的期數。本來我們可以把mintMonths宣告成Public變數,但我們已經有兩個屬性─ Years和Months,因此,我們只要設定Years或Months就可以設定貸款期數mintMonths的值。

使用Property Let屬性程序(Property Procedure)是另一種定義屬性的方法。當你指定某個值給物件時,Property Let屬性程序就會被呼叫。簡單的屬性如Principal只是很單純的接收原呼叫程式指定給它的值;然而,當你指定某個值給屬性Months的時候,Property Let Months裡的程式碼就會被執行。在我們的例子中,屬性程序只是對mintMonths作最簡單的資料指定動作。

Property Let Months(intMonths As Integer)
    mintMonths = intMonths
End Property

Property Let屬性程序用來定義一個可寫入資料的屬性,而Property Get屬性程序則定義一個可讀出資料的屬性。Property Get Months讓使用者可以取得Months屬性目前的值(存放在Private變數mintMonths裡)。

Property Get Months() As Integer
    Months = mintMonths
End Property

從外面看來,Months屬性看起來不過像是個簡單的變數,不但可以存放資料,而且當你讀取它的內容時,它也會給你原來給的值;但是當你觀察Loan物件類別內部運作細節時,你會發現Months不只是個單純的變數─當你對Months作資料寫入或讀出的動作時,屬性程序裡的程式碼會被執行。明白了這一點,你應該不難想到你可以加入更多的動作到屬性程序裡。

對某個屬性而言,如果只定義了Property Let屬性程序,而沒有定義Property Get程序,這個屬性就是一個只能寫入資料的屬性;反之,則這個屬性就是一個唯讀屬性,在這個例子裡Payment屬性就是一個唯讀屬性。

Years屬性提供了第二個管道讓你設定貸款期數。Property Let Years屬性程序把指定給屬性Years的值乘以12,然後把結果存放在mintMonths裡,這個計算過程對外界而言是完全看不見的。

Property Let Years(intYears As Integer)
    mintMonths = intYears * 12
End Property

Property Get Years() As Integer
    Years = Round(mintMonths / 12#, 0)
End Property

注意:

Round函式是Visual Basic 6的一個新函式,我們在Property Get Years程序裡用它來做Years的四捨五入。


比較一下Years屬性程序和Months的屬性程序,就可以了解為什我們要用mintMonths來存放實際的貸款期數了。再次提醒你,Years屬性和Months屬性把內部的運作細節封裝(Encapsulate)在Loan物件裡,外界無從得知這兩個屬性內部是如何運作的。

當使用者讀取Payment屬性值時,如下所示,一連串複雜的計算就開始運作,Payment的屬性值就由其他屬性的現值計算而得到。這是一個很好的例子可以說明這種定義屬性的方式和單純用Public變數定義屬性的方式有什麼不同之處。

Property Get Payment() As Currency
    Dim sngMonthlyInterestRate As Single
    'Verify that all properties are loaded
    If PropertiesAreLoaded() Then
        sngMonthlyInterestRate = AnnualInterestRate / 1200
        Payment = (-sngMonthlyInterestRate * Principal) / _
            ((sngMonthlyInterestRate + 1) ^ (-mintMonths) - 1)
    Else
        Payment = 0
    End If 
End Property

Balance是一個唯讀屬性,因為它只有Property Get屬性程序,而沒有Property Let屬性程序。

Property Get Balance() As Currency()
    Dim intCount As Integer
    Dim curPayment As Currency
    ReDim curBalance(0) As Currency
    If PropertiesAreLoaded() Then
        ReDim curBalance(mintMonths)
        curBalance(0) = Principal
        curPayment = Round(Payment, 2)   'Rounds to nearest penny
        For intCount = 1 To mintMonths
            curBalance(intCount) = curBalance(intCount - 1) * _
                (1 + AnnualInterestRate / 1200)
            curBalance(intCount) = curBalance(intCount) - curPayment
            curBalance(intCount) = Round(curBalance(intCount), 2)
        Next intCount
    End If
    Balance = curBalance
End Property

Balance屬性表現了兩個Visual Basic的新特性:除了新的Round函式之外,Balance屬性的傳回值是一個數字陣列。Balance傳回值的資料型別由As Currency ( ) 所定義,這對括弧即表示其傳回值是一個陣列。在Balance屬性的程式中,我們宣告了一個叫做curBalance的局部陣列,資料型別為Currency;當這個動態陣列裝滿了資料後,我們便將它指定給Balance,做為Balance屬性的傳回值。如果有任何錯誤發生,例如,使用者沒有設定所有相關屬性,Balance屬性會傳回一個維度為0的陣列,內含一個0。這是一個簡化的錯誤處理方式,在此我們不針對錯誤處理多作討論。物件方法 (Method) 和屬性一樣,也是物件對外界呈現的重要界面 (Interface)。在這裡,我們在Loan物件中加了一個物件方法Reset,簡單說來,Reset只是一個用來清除所有屬性內含值的Sub程序。

Public Sub Reset()
    Principal = 0
    AnnualInterestRate = 0
    mintMonths = 0
End Sub

注意:

Class_Initialize和Class_Terminate事件函式讓應用程式在產生物件和銷毀物件時,自動做好初始化和善後的工作。


Private函式只能在物件類別模組中被呼叫,我們的PropertiesAreLoaded函式即是一例。在這個Private函式中,我們把所有檢查屬性的程式碼都放在這裡。

Private Function PropertiesAreLoaded() As Boolean
    If Principal > 0 And AnnualInterestRate > 0 And mintMonths > 0 Then
        PropertiesAreLoaded = True
    End If
End Function

注意:

在函式中如果不特別指定函式的傳回值,那麼函式會傳回其所屬資料型別的系統預設值。例如Variant函式會傳回Empty,Integer傳回0,而Boolean傳回False。


如何使用新物件?
 

Loan物件類別模組本身並不會產生一個物件,它只是提供了一個物件的藍圖,讓你的應用程式可以使用物件類別模組來產生真正的物件。在這一節裡我們將要完成一個應用LOAN.CLS的專案。

首先請建立一個只有一張表單的「標準執行檔」專案,改變表單的Caption屬性為"Please Click on This Form",加入LOAN.CLS物件類別模組到你的專案裡,然後加入以下的程式碼到表單裡。圖5-1顯示的是設計階段中的表單。

Option Explicit

Private Sub Form_Click()
    Dim loanTest As New Loan
    Dim intCount As Integer
    Dim curBalance() As Currency

    'Clear the face of the form
    Cls
    'Set loan parameters
    loanTest.Reset
    loanTest.Principal = 1000
    loanTest.Months = 12
    loanTest.AnnualInterestRate = 8.5
    'Display parameters used
    Print "Principal: ", , Format(loanTest.Principal, "Currency")
    Print "No. Months: ", , loanTest.Months
    Print "(Years:) ", , loanTest.Years
    Print "Interest Rate:", , Format(loanTest.AnnualInterestRate _
        / 100, "Percent")
    Print "Monthly Payment: ", Format(loanTest.Payment, "Currency")
    Print
    'Get array of monthly balances
    curBalance() = loanTest.Balance()
    'Display payment schedule
    For intCount = LBound(curBalance) To UBound(curBalance)
        Print "Month: "; intCount,
        Print "Balance: "; Format(curBalance(intCount), "Currency")
    Next intCount
End Sub


 

 圖5-1 用來展示Loan物件的表單

在表單的Form_Click事件程序中,我們產生了一個Loan物件,將這個Loan物件指定給物件變數loanTest,這個Loan物件正如同其他的變數一樣,當程序執行完畢後,物件就會被破壞掉:

Dim loanTest As New Loan

以下三行程式碼指定三個值給三個Loan物件的屬性。

loanTest.Principal = 1000
loanTest.Months = 12
loanTest.AnnualInterestRate = 8.5

當Form_Click程序輸出其計算的結果時,如下所示,Loan物件中的許多屬性都被用到了;當程式讀取Principal、Months和AnnualInterestRate時,物件變數loanTest只是很單純的把屬性的現存值傳出,而Payment屬性被讀取時,複雜的計算就被啟動了,原呼叫程式並不知道這些計算是如何進行的。

Print "Principal: ", , Format(loanTest.Principal, "Currency")
Print "No. Months: ", , loanTest.Months
Print "(Years:) ", , loanTest.Years
Print "Interest Rate:", , Format(loanTest.AnnualInterestRate _
    / 100, "Percent")
Print "Monthly Payment: ", Format(loanTest.Payment, "Currency")
Print

叫用Balance屬性,如下所示,等於是告訴loanTest在內部計算每月應付金額,然後把這些金額放在一個陣列中:

'Get array of monthly balances
curBalance() = loanTest.Balance()

最後這幾行程式碼用來將每月應付金額顯示在表單上。

Display payment schedule
For intCount = LBound(curBalance) To UBound(curBalance)
    Print "Month: "; intCount,
    Print "Balance: "; Format(curBalance(intCount), "Currency")
Next intCount

當Form_Click程序結束時,所有的變數以及Loan物件中的程式碼和資料都會被系統破壞。

圖5-2顯示了這個程式執行的結果。你可以改進輸出結果並且加強這個程式的功能,但是你不用動到Loan物件類別模組裡的程式碼。物件設計好了之後,在你要提供給使用者的文件中就只要說明這個物件有什麼樣的界面(Interface)─公共屬性和方法──就足夠了,物件內部運作的細節就當作一個"黑箱",可予以省略。你可以把物件想像成一個具有外部包裝的物體,外面的世界只看得到它可見的外表,物件本身負責執行它該執行的工作,而呼叫物件的程式則負責了解如何正確地使用這個物件,這就是物件導向程式設計裡所謂的資料封裝(Encapsulation)。這種資料封裝的技術讓我們可以很容易地發展大型的應用程式,也更易對程式進行偵錯,這就是物件導向程式設計的迷人之處。


 

 圖5-2 Loan物件輸出的結果

我們除了可以很單純地把Loan物件模組加到一個應用程式裡之外,還可以把這個物件類別模組和ActiveX元件結合,稍後我們會討論這個主題。


參考資料:

請參閱本章的 "如何建立和使用ActiveX執行檔?" 以及 第二十七章"進階程式設計技巧" ,這裡有更多關於ActiveX元件的資訊。

請參閱 二十九章"圖形" 裡的Lottery應用程式。


如何在物件中設定預設屬性?
 

什麼是預設屬性(Default Property)?舉文字方塊控制項(TextBox Control)為例,屬性Text就是文字方塊控制項的預設屬性,意思就是你可以明確指出要存取的屬性是Text,但也可以不要指明Text屬性,而主角同樣都是Text這個屬性。下面這兩行陳述式是一樣的:

Text1.Text="This string is assigned to the Text property."
Text1="This string is assigned to the Text property."

筆者本身比較喜歡把屬性名稱都明確地寫在程式中,因為這樣可以增加程式的可讀性,但是有許多人卻比較喜歡使用預設屬性,而且Visual Basic也提供了這個特性讓你可以為你的物件設定預設屬性。

我們來看看如何幫Loan物件設定它的預設屬性。打開Loan物件類別的「程式碼視窗」,從「工具」功能表項目中選擇「程序屬性」,從「程序屬性」對話方塊的「名稱」下拉式清單中選擇Principal,然後按一下「進階」按鈕。再從在「程序識別碼」下拉式清單裡選取(預設值),最後按「確定」,這樣,Principal就成了Loan的預設屬性了。

為了檢查Principal有沒有被設定成預設屬性,請改變一下我們用來測試Loan物件的程式──把testLoan.Principal = 1000改為testLoan = 1000,然後執行測試程式,看看能不能得到正確的結果。

如何建立和使用ActiveX執行檔﹖
 

ActiveX執行檔和ActiveX DLL是類似的產物,在 第二十七章"進階程式設計技巧" 裡,我們會帶你走過建立ActiveX DLL的每一步,在這裡我們先教你如何建立ActiveX執行檔。

ActiveX執行檔本身就是一個可以獨立執行的程式,但它裡面的物件可以被其他外部的程式使用,前面提過這些在ActiveX執行檔裡的物件是以out-of-process方式執行,它們不和用戶端應用程式共用程式碼區域和執行緒。

Chance──ActiveX執行檔實例
 

在這裡範例中,我們要建立一個相當簡單、只含一種物件類別的ActiveX執行檔,其元件名稱為Chance。包含在Chance裡的物件,我們把它命名為Dice (骰子),並且把Dice的物件類別存放在DICE.CLS。當這個執行檔可以執行而且自動向系統登錄之後,我們再建立一個一般的標準執行檔,這個標準執行檔將會引用放在Chance裡的物件類別來產生物件。

雖然這個物件類別叫做Dice(骰子),但只要設定這個骰子有幾個面,它可以用來當作擲"銅幣"以及擲"十二面的骰子"的遊戲程式。

DICE.CLS
 

讓我們開始建立這個ActiveX執行檔。首先建立一個新專案,當「建立新專案」對話方塊出現之後,在「ActiveX執行檔」圖像上按兩下滑鼠左鍵,加入以下的程式碼到物件類別模組的「程式碼」視窗,把物件類別模組的Name屬性改為Dice,接著把這個物件類別模組存成DICE.CLS。最後將這個專案存成CHANCE.VBP

Option Explicit
'Declare properties that define the range of possible values
Public Smallest As Integer
Public Largest As Integer

'Declare a read-only property and define its value
'as a roll of the die
Public Property Get Value() As Integer
    Value = Int(Rnd * (Largest - Smallest + 1)) + Smallest
End Property

'Use a method to shuffle the random sequence
Public Sub Shuffle()
    Randomize
End Sub

以上的物件類別模組為物件類別Dice定義了三個屬性及一個方法。Smallest和Largest是兩個屬性(公共變數),因此,用戶端應用程式(接下來我們會談到建立用戶端應用程式)可以設定傳回值的最小值和最大值(最大值等於是設定「骰子」有幾個面)。

Value是另一個屬性,但我們不把它宣告為公共變數,因為傳出Value屬性值以前,我們還要作一下計算。我們把運算式放在Property Get屬性程序中,當屬性Value被讀取時,Value的值就會被計算出來。從運算式裡,我們可以看到Value傳回的值是一個亂數(Random Number),它用來模擬擲骰子得到的值。(這個亂數並不算是個很嚴格的隨機亂數,但在這個例子裡它已經符合我們需要了)。

Value屬性是一個唯讀屬性,因為我們沒有定義Property Let Value屬性程序。如果加了Property Let屬性程序,那麼用戶端應用程式就可以指定Value的值了,在這個例子裡,我們不需要Property Let屬性程序。

Shuffle(洗牌)是一個很簡單的物件方法,把Shuffle方法加進Dice物件類別中是為了讓這個物件至少有一個物件方法可作為示範。當Shuffle被呼叫時,物件就會啟動亂數產生器,亂數化(Randomize)動作的效果就像洗牌一樣。

現在讓我們加入主程式部份到Chance ActiveX執行檔,讓它能夠獨立執行。從「專案」功能表中選擇「新增模組」,在「新增模組」對話方塊的「模組」圖像上連續按兩下滑鼠左鍵,接下來在「程式碼」視窗裡加入下面的程式,然後把這個模組存放在CHANCE.BAS檔案裡。

Option Explicit
Sub Main()
    Dim diceTest As New Dice
    diceTest.Smallest = 1
    diceTest.Largest = 6
    diceTest.Shuffle
    MsgBox "A roll of two dice: " & _
        diceTest.Value & "," & diceTest.Value, _
        0, "Message from the Chance ActiveX EXE"
End Sub

從「專案」功能表中選擇「Project1屬性」,在「專案屬性」視窗中,可以看到「啟動物件」下拉式清單,從清單裡選擇「Sub Main 」,再來把專案名稱改為Chance,在「專案描述」中輸入"Chance - ActiveX EXE Example",然後按「確定」。最後,在「檔案」功能表中選擇「製成Chance.exe 」,指明你要存放執行檔的目錄之後,一切就大功告成了。

最後這個動作,讓Visual Basic把程式碼加以編譯,編譯完成之後把這個Chance ActiveX執行檔向系統註冊。

測試ActiveX執行檔
 

現在Chance ActiveX執行檔已經完成編譯,也已經向系統註冊過了,接下來我們就可以發展用戶端應用程式,使用定義在Chance裡面的物件。我們一步一步來說明如何建立這個測試程式。

首先建立一個「標準執行檔」專案,把它存成DICEDEMO.VBP,改變表單的Caption屬性,鍵入"Taking a Chance with Our ActiveX EXE",然後加入一個指令按鈕控制項(ConmandButton Control),把指令按鈕控制項的Caption屬性改為"Roll'em",再把它放在表單的右上角。加入下面的程式碼到表單裡,最後存檔。

Option Explicit    
Dim diceTest As New Dice
Private Sub Command1_Click()
    diceTest.Shuffle
    
    diceTest.Smallest = 1
    diceTest.Largest = 2
    Print "Coin:" & diceTest.Value,
    diceTest.Largest = 6
    Print "Dice:" & diceTest.Value,
    diceTest.Largest = 12
    Print "Dodecahedron:" & diceTest.Value
End Sub

圖5-3顯示的是設計階段中的表單。


 

 圖5-3 設計階段中的DiceDemo表單

在這個表單中,我們產生了一個Dice物件,因此,我們必須引用Chance ActiveX執行檔,以便讓目前的專案知道到哪裡去找Dice的定義。從「專案」功能表中選擇「設定引用項目」,「設定引用項目」對話方塊出現後,你可以從「可引用項目」底下找到「Chance - ActiveX EXE Example」(回憶一下,這是先前我們鍵入的「專案描述」),在它前面打個勾,再按「確定」。

現在這個用戶端應用程式可以執行了,請注意觀察執行的結果。首先我們的表單先出現,等待你按「擲骰子」按鈕;當你按下按鈕後,Dice物件就產生了,你可以看到"Coin","Dice","Dodecahedron"的結果顯示在表單上,而且你每按一下「Roll'em」按鈕就有不同的結果,如圖5-4。


 

 圖5-4 DiceDemo應用程式執行的結果

當你第一次按下「擲骰子」按鈕時,有一個訊息方塊會出現,這是因為當系統把Chance ActiveX執行檔載入記體時,Chance ActiveX執行檔的主程式Sub Main被執行了一次,因此Chance ActiveX執行檔自己也產生了一個Dice物件,並且擲了一對骰子(讀取Value屬性兩次),如圖5-5。


 

 圖5-5 當Chance ActiveX執行檔載入時,這個訊息方塊就會出現

這個訊息方塊只會出現一次,就是當Chance ActiveX執行檔被載入的時候。而當我們結束DiceDemo應用程式時,在Chance ActiveX執行檔裡的物件不再被DiceDemo程式所引用,Chance ActiveX元件也因此被載出了(Unload)記憶體。

如何建立一個可以顯示表單的物件?
 

前面我們提到的Loan和Dice物件本身都不具備可以讓使用者看到的界面,例如表單;事實上有些時候,我們需要物件本身就能夠提供視覺界面,以便讓使用者輸入資料。

把表單加到ActiveX執行檔或ActiveX DLL並不是一件難事,但是這裡有個陷阱。你必須謹慎管理從物件裡面產生的表單。用戶端應用程式沒有辦法和這些表單互動(這些表單是屬於物件的私有元件),因此,物件本身的程式碼不但必須負責產生表單,更要負責銷毀這些表單,否則程式結束後,你的物件還會留在記體裡,浪費資源,有時候甚至會造成不可預期的結果。

以下的程式碼定義一個叫做User物件類別模組,這個物件將會顯示一張表單,表單上有兩個文字方塊控制項(txtName和txtPassword)和一個指令按鈕控制項cmdOK,可以讓使用者輸入名字和密碼。

'USER.CLS
'~~~.Name
Property Get Name()
    Name = frmPass.txtName
End Property

'~~~.Password
Property Get Password()
    Password = frmPass.txtPassword
End Property

'~~~.Show
Sub Show()
    frmPass.Show vbModal
End Sub

Private Sub Class_Terminate()
    'Unload form when object is destroyed
    Unload frmPass
End Sub

這張叫做frmPass的表單只有一個事件程序,如下所示。當使用者按下「確定」按鈕後,這個事件程序會把表單隱藏起來,我們可以用另一個程序來驗證使用者的名字和密碼,如果資料不正確,把這張表單再顯示出來,要求使用者重新輸入,我們把文字方塊控制項收到的資料保留起來,目的在於方便使用者局部修改輸入的名字或密碼。

'FRMPASS.FRM
Private Sub cmdOK_Click()
    Hide
End Sub

如果要使用這個物件,另一個應用程式只要產生一個User物件,然後用Show方法就可以了。以下的程式告訴你,如何在應用程式裡以一個指令按鈕控制項來叫用這個物件。

請注意我們如何引用在PasswordSample專案裡的User物件類別。

Private Sub cmdSignOn_Click()
    'Create an instance of the object
    Dim usrSignOn As New PasswordSample.User
    'Loop until user enters Guest password
    Do While usrSignOn.Password <> "Guest"
        'Display dialog box
        usrSignOn.Show
    Loop
End Sub

從圖5-6你可以看到User物件顯示表單的情形。


 

 圖5-6 一個可以顯示表單的物件

如果要了解表單的建立及銷毀的重要性,你可以把物件類別模組裡的Class_Terminate事件程序刪掉,然後再執行你的測試程式。當cmdSignOn_Click事件程序執行完畢後,usrSignOn所引用的物件就被破壞了,但是frmPass表單卻仍然留在記憶體裡,這表示有一個隱藏的物件實體還在執行著。

如果這個物件的應用程式是一個執行檔,你可以同時按--叫出Windows 95的「關閉程式」視窗,從視窗找到這個隱藏的物件,然後你必須用「結束工作」按鈕來解決這個問題。切記,不要讓你的物件產生這種問題,隱藏的物件實體有時很難偵查得出來。

一般而言,你要用Class_Initialize和Class_Terminate來做表單產生和銷毀的工作。因為我們介紹的範例十分簡單,我們直接呼叫物件方法來產生表單。

一般而言,較正規的做法是加入一個Class_ Initialize事件程序:

'Add to USER.CLS to be explicit
Private Sub Class_Initialize()
    'Create form when object is created
    'by calling Show method
    Show
End Sub

注意:

用最明確清楚的方式來產生和銷毀表單是一個好習慣,因為這樣子會省下你很多偵錯的時間。


事件、WithEvents和RaiseEvent
 

Visual Basic裡面有幾個新指令可以讓你把你自行定義的事件放到物件裡。在這裡提出這個話題是因為ActiveX執行檔中物件的私有表單可以運用"事件"和應用程式互動。這種表單可以驅動一些事件影響它專屬的物件,物件又可以因此而產生另一個事件來影響應用程式。

「線上手冊」裡的主題"Adding a Form To the ThingDemo Project"運用了許多事件、WithEvents和RaiseEvent指令,這是個很好的範例。

如何使用物件的集合?
 

Visual Basic的集合物件(Collection Object)讓你可以把多個物件或任何型態的資料項合併成一個單一的元件。Collection物件最常見的用途是用以管理數目不確定而屬於同一個物件類別的物件群。你可以把集合物件想像成一個無所不包(包括物件和其他集合物件)的陣列。

在你的物件裡使用集合物件的方式有很多種,端看你對你自己或別人的程式保護的程度,你可以用很簡單的方式也可以用較奇特卻又不失安全的方式,當然有些方式會比較"危險"一點,我們建議你徹底的研讀一下「線上手冊」裡有關於這方面的主題,了解Microsoft建議的方法之後再試試本書所建議的方法。

(在「線上手冊」中,請找下列這些主題:House of Straws,House of Sticks,以及House of Bricks)

在以下的範例中,筆者要介紹一個簡單的方法,讓你應用集合物件到你的物件裡,這個方法相當的可靠,而且裡面技巧很容易模仿、複製和修改。

我們要建立一個太陽系結構:其中包含一個最上層的Star物件,Star物件裡包含了一個Planets集合物件,Planets集合物件裡的每一個Planet物件又包含了一個Moons集合物件,而在Moons集合物件裡的每一個Moon物件則是一個只有Name屬性的簡單物件。

Collection實例──SolarSys
 

首先建立一個標準執行檔專案,把表單的Name屬性設為SolarSys,而Caption屬性改為"Collections Example";加入一個指令按鈕控制項cmdBulildSolarSystem,把它的Caption屬性改為"Build Solar System";把表單存成SOLARSYS.FRM,專案則存成SOLARSYS.VBP。加入下列程式碼到表單後,再存檔一次。

Option Explicit
Public starObject As New Star
Private Sub cmdBuildSolarSystem_Click()
    'Prepare working object references
    Dim planetObject As Planet
    Dim moonObject As Moon
    'Add planets and moons to the solar system
    With starObject.Planets
        'Create first planet
        Set planetObject = .Add("Mercury")
        'Set some of its properties
        With planetObject
            .Diameter = 4880
            .Mass = 3.3E+23
        End With
        'Create second planet
        Set planetObject = .Add("Venus")
        'Set some of its properties
        With planetObject
            .Diameter = 12104
            .Mass = 4.869E+24
        End With
        'Create third planet
        Set planetObject = .Add("Earth")
        'Set some of its properties
        With planetObject
            .Diameter = 12756
            .Mass = 5.9736E+24
            'Add moons to this planet
            With .Moons
                'Create Earth's moon
                Set moonObject = .Add("Luna")
                'Set some of its properties
                With moonObject
                    .Diameter = 3476
                    .Mass = 7.35E+22
                End With
            End With
        End With
        'Create fourth planet
        Set planetObject = .Add("Mars")
        'Set some of its properties
        With planetObject
            .Diameter = 6794
            .Mass = 6.4219E+23
            'Add moons to this planet
            With .Moons
                'Create Mar's first moon
                Set moonObject = .Add("Phobos")
                'Set some of its properties
                With moonObject
                    .Diameter = 22
                    .Mass = 1.08E+16
                End With
                'Create Mar's second moon
                Set moonObject = .Add("Deimos")
                'Set some of its properties
                With moonObject
                    .Diameter = 13
                    .Mass = 1.8E+15
                End With
            End With
        End With
    End With
    'Disable the command button
    cmdBuildSolarSystem.Enabled = False
    'Display the results
    Print "Planet", "Moon", "Diameter (km)", "Mass (kg)"
    Print String(100, "-")
    For Each planetObject In starObject.Planets
        With planetObject
            Print .Name, , .Diameter, .Mass
        End With
        For Each moonObject In planetObject.Moons
            With moonObject
                Print , .Name, .Diameter, .Mass
            End With
        Next moonObject
    Next planetObject
    'Directly access some specific properties
    Print
    Print "The Earth's moon has a diameter of ";
    Print starObject.Planets("Earth").Moons("Luna").Diameter;
    Print " kilometers."
    Print "Mars has";
    Print starObject.Planets("Mars").Moons.Count;
    Print " moons."
End Sub

圖5-7顯示的是設計階段的SolarSys表單。


 

 圖5-7 設計階段中的SolarSys表單

我們首先把要用到的物件和集合物件定義完畢。然後再回頭討論程式內容。

Star物件類別
 

現在我們來定義Star物件類別。從「專案」功能表中選取「新增物件類別模組」,然後點選兩下「物件類別模組」圖像,將這個物件類別模組的Name屬性設定為Star,加入以下這段程式碼,然後將這個模組存成STAR.CLS。

Option Explicit

Public Name As String

Private mPlanets As New Planets

Public Property Get Planets() As Planets
    Set Planets = mPlanets
End Property
The Star

Star物件包含了一個Planets集合物件,這個集合物件則是定義了多個Planet物件,我們待會兒會介紹Planets和Planet物件。

請注意,在上面的程式中,Planets物件變數mPlanets是一個Private變數,外界無法直接取得它,而另一方面,Property Get Planets程序則提供了一個唯讀界面,讓外界只能讀出mPlanets中的資訊。( 因為沒有Property Let Planets程序,故外界的程式不能寫入任何資料到mPlanets中。)

Planets物件類別
 

在這個模擬太陽系中,所有的行星都以獨立的Planet物件作為代表,而這些Planet物件由Planets物件來管理 ( 請注意Planet和Planets單複數的差別 )。現在讓我們來建立Planets物件。加入另一個物件類別模組到專案中,命名為Planets,加入下面這段程式碼,將模組存成PLANETS.CLS。

Option Explicit
Private mcolPlanets As New Collection
Public Function Add(strName As String) As Planet
    Dim PlanetNew As New Planet
    PlanetNew.Name = strName
    mcolPlanets.Add PlanetNew, strName
    Set Add = PlanetNew
End Function

Public Sub Delete(strName As String)
    mcolPlanets.Remove strName
End Sub

Public Function Count() As Long
    Count = mcolPlanets.Count
End Function

Public Function Item(strName As String) As Planet
    Set Item = mcolPlanets.Item(strName)
End Function

Public Function NewEnum() As IUnknown
    Set NewEnum = mcolPlanets.[_NewEnum]
End Function
When I first started

把每一個物件和其物件集合放在同一個物件類別模組裡,程式雖然也能運作,但卻隱藏著一些問題,這些問題和陷阱可以藉由將二者分開而避免。因此,在這個範例中,我們分別定義了Planet和Planets物件類別,同樣地,你會在後面看見我們也以同樣的方式定義Moon物件類別和Moons物件類別。

在本例中,Planets集合物件將Planet物件封裝在其中,並負責掌管每一個Planet物件。真正Collection物件mcolPlanets被定義為Private變數,以防止外界程式任意存取其資料,而Add、Delete、Count、Item和NewEnum等物件方法則提供物件的界面,以供外界程式操控Planets物件內含的所有Planet物件。

Add物件方法的功用在於建立集合內新的Planet物件。在本例中,我們把行星的名稱傳給Add物件方法,以便設定該Planet物件的Name屬性,並以這個名稱做為這個Planet物件在物件集合中的鍵值。每個物件集合中的元素都必須有一個獨一無二的字串鍵值,若要維持這些鍵值的唯一性,最好的辦法就是讓集合物件自動產生並維護它所有的鍵值。在本例中,如果用同樣的名稱去建立不同的物件,你會遇到程式錯誤。你可以在Add方法中加上一些防錯的程式碼,避免這種錯誤。

Count物件方法負責傳回Planets內含的Planet物件數目,而Delete物件方法則負責將指定的Planet物件從Planets中移除,當然,你可以自己加入防錯的動作,以避免刪除不存在的Planet物件。 Item物件方法負責將某個指定的Planet物件傳回給原呼叫程式。說到這裡,我們要特別提出一點來多作討論。如果把Item程序設定為Planets的預設屬性,那麼,在取得某個指定物件時,程式的語法會更自然。舉例而言,你可以用starObject.Planets.Item("Earth") 來取得Earth這個物件,但如果Item是預設屬性,語法就簡單多了:

 

starObject.Planets("Earth")。

 

注意:

如果要把某個程序設定為預設屬性,首先從「工具」功能表中選取「程序屬性」,在「名稱」下拉式清單中選取你想要的程序,點選「進階」按鈕,然後從「程序識別碼」中選取 ( 預設值 )。在每一個物件類別中只能有一個程序可以設定為預設程序。


此外,在這裡我們要告訴你另一個在Planets物件類別中的重要技巧。這個技巧讓你可以在外部程式中用For Each迴圈的方式存取集合物件內的每一個物件。這部分不太容易懂,請仔細地往下看。第一步,先在你的類別中加入一個特別的NewEnum IUnknown程序。在NewEnum函式中,我們使用了Collection物件的隱藏列舉集合屬性。因此,程式中的方括號和底線符號是規定的語法,一定要這樣寫。第二步,將NewEnum函式隱藏後啟用。請看以下的注意事項:


注意:

如果要把物件類別中的特殊函式NewEnum加以隱藏後啟用,首先,點選「工具」功能表中的「程序屬性」,在「名稱」中選取NewEnum程序,再點選「進階」按鈕。在「特性」中選取「隱藏此成員」,然後在「程序識別碼」中填入 -4。這樣就可以讓集合物件以For Each迴圈來存取了。


Planet物件類別
 

現在加入另一個物件類別模組,加入以下的程式,把它的Name屬性設定為Planet,存檔為PLANET.CLS。這個物件類別定義了在Planets中的Planet物件。

Option Explicit

Public Name As String
Public Diameter As Long
Public Mass As Single

Private mMoons As New Moons

Public Property Get Moons() As Moons
    Set Moons = mMoons
End Property
Each Planet

每一個Planet物件都有幾個簡單的屬性 (Name、Diameter和Mass) 以及一個Moons集合物件,Moons裡面則包含著零個以上的Moon物件。

再提醒你一次,Moons物件集合被宣告為Private是為了防止外界程式任意地直接存取Moons物件,Planet物件提供了唯讀的Property Get Moons程序讓你取得有關Moons的資訊。

Moons物件類別
 

Moons物件是一個集合物件,它的結構很像Planets集合物件。現在在SolarSys專案中加入另一個物件類別模組,將它的Name屬性設為Moons,加入以下的程式碼,取後存成MOONS.CLS。

Option Explicit

Private mcolMoons As New Collection

Public Function Add(strName As String) As Moon
    Dim MoonNew As New Moon
    MoonNew.Name = strName
    mcolMoons.Add MoonNew, strName
    Set Add = MoonNew
End Function

Public Sub Delete(strName As String)
    mcolMoons.Remove strName
End Sub

Public Function Count() As Long
    Count = mcolMoons.Count
End Function

Public Function Item(strName As String) As Moon
    Set Item = mcolMoons.Item(strName)
End Function

Public Function NewEnum() As IUnknown
    Set NewEnum = mcolMoons.[_NewEnum]
End Function

Moon物件類別
 

這是個最簡單的物件類別模組,每一個Moon物件都包含Name、Diameter和Mass屬性。請在專案中加入一個新的物件類別模組,將它的Name屬性設為Moon,加入以下的程式碼後存檔為MOON.CLS。

Option Explicit

Public Name As String
Public Diameter As Long
Public Mass As Single

巢狀式Collection組織的運作方式
 

讓我們回頭看看SolarSys表單中的程式。當表單中的cmdBuildSolarSystem被按下時,系統中就產生了一個叫做starObject的Star物件。我們不用去設定Star的Name屬性,以區分所有的Star物件,因為我們只有一個Star物件。(如果我們要發展一個將星系予以分類的程式,就需要產生好幾個Star物件,而每個Star物件都要設定不同的Name屬性值。)

為了簡化程式、方便說明起見,我們只加了四個行星(Planet物件)和幾個衛星(Moon物件),如果你有興趣的話,你可以建造出整個完整的太陽系。以下這段程式產生一個叫做Mecury的Planet物件。Mercury並沒有任何衛星,因為我們並未替它加入任何Moon物件。

'Add planets and moons to the solar system
With starObject.Planets
     'Create first planet
     Set planetObject = .Add("Mercury")
     'Set some of its properties
     With planetObject
          .Diameter = 4880
          .Mass = 3.3E+23
End With

在程式中,我們會引用到好幾個層次的物件,因此,我們使用With指令來簡化存取物件屬和方法的陳述式,事實上,我們仍然可以用完整表達的陳述式來存取Mercury物件的Diameter屬性,如下例:

starObject.Planets("Mercury").Diameter = 4880

用一句話來解釋這個陳述式:把4800指定給Star物件裡的Planets物件裡的Mercury的屬性Diameter。好長的一句話,因此,你應該可以了解使用With有多麼方便了!With指令除了可以讓人易於了解程式之外,它更能減少在系統內部引用物件的步驟,增加了程式的執行速度。

第二段程式重複以上的處理步驟,加入第二個叫"Venus"的Planet物件。Venus也沒有Moon物件。我們先跳到第三個Star物件Earth,Earth有一個Moon物件,以下是把Moon物件加入的程式:

'Create third planet
Set planetObject = .Add("Earth")
'Set some of its properties
With planetObject
    .Diameter = 12756
    .Mass = 5.9736E+24
    'Add moons to this planet
    With .Moons
        'Create Earth's moon
        Set moonObject = .Add("Luna")
        'Set some of its properties
        With moonObject
            .Diameter = 3476
            .Mass = 7.35E+22
        End With
    End With
End With
Once again,

這裡請你注意我們是如何在巢狀式的Collection組織裡,引用特定的Planet的物件和Moon物件。

一旦這個簡化的太陽系建造完成之後,下一步我們就可以用以下這幾行程式顯示出所有在這個太陽系中的星體名稱了。

'Display the results
Print "Planet", "Moon", "Diameter (km)", "Mass (kg)"
Print String(100, "-")
For Each planetObject In starObject.Planets
    With planetObject
        Print .Name, , .Diameter, .Mass
    End With
    For Each moonObject In planetObject.Moons
        With moonObject
            Print , .Name, .Diameter, .Mass
        End With
    Next moonObject
Next planetObject

從以上的程式中可以看到,我們用For Each迴圈取得所有Planet物件和Moon物件的各個屬性,程式簡單明瞭。為了和上一段程式作比較,我們在以下這段程式中以完整的物件表示法取得特定的物件屬性:

'Directly access some specific properties
Print
Print "The Earth's moon has a diameter of ";
Print starObject.Planets("Earth").Moons("Luna").Diameter;
Print " kilometers."
Print "Mars has";
Print starObject.Planets("Mars").Moons.Count;
Print " moons."

圖5-8顯示了程式執行的結果。


 

 圖5-8 由Planet集合物件和Moon集合物件所組成的太陽系

以上這個建構物件的方式提供了一個堅固的觀念基礎,由此一觀念延伸,你可以建構出複雜但有脈絡可循的物件結構,也足以應付各種程式設計的需求。

如何使用同名異式?
 

OOP的世界充滿了新的名詞和觀念,現在讓我們來討論同名異式 (Polymorphism)──它的意義是什麼?它能帶給你什麼樣的助益?

簡單的說,當多個不同的物件類別提供完全相同的界面元素(Interface Element)時,我們便稱之為"同名異式",而所謂的界面元素即是指可供用戶端程式使用的屬性和方法。

譬如說,你定義了幾個不同物件類別,每一個由這些物件類別所產生的物件都提供了一組"標準的"(相同的)檔案管理函式,這時你便可以應用同名異式的觀念到你專案裡。在每個不同物件類別裡,把和這些檔案管理函式相關的屬性和物件方法都取統一的名稱,如FileName, Read和Write,這樣就是應用了同名異式的特性。同名異式可以簡化、標準化以及協調這些物件,使物件更加地一致而且可以預測。

Visual Basic處理同名異式的方式與C++ 有些許不同。在OOP語言的發展歷程中,一個新物件可以"繼承"(Inherit)另一個既存物件的屬性和物件方法,也就是界面元素,新舊物件因而都擁有了相同的界面,而達成所謂的同名異式;在新物件裡,我們可以把界面元素原來的內容原封不動地提供給應用程式,也可以賦予這些界面元素新的行為及內涵,但重點是這些界面元素的"外觀"(屬性名稱和函式原型)不能改變。

在另一方面,Visual Basic則透過抽象物件類別(Abstract Class)來實踐同名異式的觀念,而不靠傳統OOP的繼承特性。所謂的抽象物件類別事實上是一些"空殼"的物件類別模組,它們只有屬性名稱和物件方法的原型,完全沒有內容定義。新的物件類別必須透過Implement關鍵字來告訴系統它要使用抽象物件類別的界面元素,但它可以定義專屬於自己的界面元素內涵。

我們可以把抽象物件類別想像成某種的"契約",不同的物件類別模組透過Implement來表示大家都願意遵循這個"契約"(使用外觀相同的界面元素)。如果在一個應用程式中,我們用以上"願意遵守契約"的物件類別產生了許多不同的物件(包括由抽象物件類別直接宣告所產生的物件),那麼,當我們對其中一個物件存取其透過Implement得來的某個界面時,(雖然每一個物件的界面元素完全相同) 系統會根據被使用的物件是什麼而自動找出專屬於這個物件的界面元素定義,得到正確執行的結果。

如果你想完全精通這些觀念,最好的方法就是實作。另外,「線上手冊」提供了兩個讓你了解同名異式的好例子- Tyrannosaur和Flea,這兩種物件彼此都同意Implement一個Bite方法。研究過這些例子之後,你會對Visual Basic的同名異式有更深入的了解。

如何使用Friend物件方法﹖
 

Visual Basic 4首先引進了物件類別模組、物件方法和屬性等新觀念,但是使用者很快就發現了一個小缺點:Visual Basic 4無法把某個的物件中的物件方法提供給另一個物件使用,同時又可以不將這個物件公佈給外界。

現在Visual Basic藉由Friend關鍵字修正了這個缺失。當我們在某個物件方法前加上了Friend關鍵字之後,這個專案裡的其他物件類別模組就可以叫用這個Friend物件方法,但是,外部的應用程式則仍無法叫用這個物件方法。

Friend物件方法,讓專案裡相關的物件得以相互溝通,而且這些物件方法不會被加入到ActiveX元件的程式庫裡,因此,這些作為物件間溝通管道的物件方法,無法讓用戶端應用程式叫用。