27. 進階程式設計技巧

本章將要介紹幾個進階的程式設計技巧,幫助你建立更有效率、更穩固可靠的Visual Basic應用程式。

第一個要告訴你的技巧是如何建立動態連結程式庫(Dynamic Link Library,DLL)。DLL對Visual Basic應用程式很重要,因為DLL可以加快Visual Basic應用程式執行的速度。在Visual Basic 4之前,建立DLL檔唯一的途徑是透過其他的語言,如C語言。但現在你可以利用ActiveX技術,以Visual Basic本身來建立快速的DLL檔案。本章會介紹這兩種建立DLL的方法。

第二個進階技巧與Remote Automation有關。Remote Automation不是Visual Basic的新功能,但是本書第一次提到它。Remote Automation讓你能夠從你使用的電腦取得在遠端伺服器上執行的物件。Visual Basic企業版能夠建立在遠端Windows NT和Windows 95伺服器上面執行的應用程式,我們將帶領你走過遠端應用程式的建立、除錯、安裝和應用等每一個階段,並且告訴你一些關於對遠端應用程式進行故障排除的小偏方。

接著我們要介紹另一個Visual Basic的特性:利用Visual Basic本身,在Visual Basic的整合發展環境中建立增益功能(Add-in)。

我們也要討論如何讓應用程式與VBScript寫成的巨集互動,這些巨集可以用Windows Scripting Host (WSScript.OCX)在應用程式外面執行,也可以用Microsoft Script控制項(MSScript.OCX)在應用程式裡面執行。巨集可以讓使用者把重複性的工作予以自動化,也可以為ActiveX應用程式產生測試套件。

最後,我們的主題是介紹如何定義及使用含有UDT型別屬性的物件。

如何以Visual Basic建立ActiveX DLL?
 

你可以用Visual Basic建立可供32位元視窗應用程式使用的DLL,Visual Basic以in-process ActiveX技術建立這種DLL。以這種方式建立的DLL是一種真實的DLL,其檔案名稱的附屬檔名為DLL。

In-process ActiveX DLL和傳統的DLL有何不同呢?答案是:原呼叫程式對這兩種DLL的界面使用方式有所不同。舉例來說,你可以用ActiveX DLL裡的物件在程式中宣告一個物件變數,有了這個物件之後,便可以運用物件的屬性和方法來做你想完成的工作;而當你使用傳統的DLL時,你必須用Declare陳述式在你的程式中宣告DLL裡的函式。ActiveX DLL不允許這種使用函式的方式,ActiveX提供的是完整的物件(包含屬性和方法),而不是只有程序和函式。

範例:Fraction物件
 

在本例中,我們要一步一步地建立一個簡單的in-process ActiveX DLL,這個名為Math的ActiveX DLL元件提供了一個Fraction物件類別,透過Fraction物件,原呼叫程式可以執行簡單的分數運算。

Fraction物件提供了兩個公用屬性Num和Den,以及四個執行加減乘除運算的方法,Fraction物件內部還有一個私有方法負責約分的動作。

請新增一個專案,在「新增專案」對話方塊中連續點選兩下ActiveX DLL圖像,Visual Basic會在專案中加入一個物件類別模組,並且會把專案類型設為ActiveX DLL。在「屬性」視窗中,請把物件類別模組的Name屬性設為Fraction,別的應用程式將會以這個名稱用來產生物件執行實體。接下來,請你從「檔案」功能表中選取「另存Fraction為」,然後將物件類別模組存成MATHFRAC.CLS。

這個專案只包含Fraction物件類別,但是我們假定日後會增加一些與數學計算相關的物件,因此,我們把專案的Name屬性設為Math (而非Fraction)。要設定專案的Name屬性,請從「專案」功能表中選擇「專案屬性」,在「專案屬性」對話方塊中輸入專案名稱,然後按下「確定」按鈕。接下來在「檔案」功能表中選取「另存新專案」,把專案存成MATHPROJ.VBP。

請分辨清楚專案的檔案名稱與專案的Name屬性有所不同,不要將兩者弄混了。本範例的專案檔名是MATHPROJ.VBP,這個檔名只對專案管理具有意義,無關乎程式設計;而專案的Name屬性值Math則對程式很重要,我們在程式中需要用到Math來識別Fraction物件。假設某個應用程式用了許多個ActiveX DLL,那麼可能會有兩個以上的Fraction物件在不同的DLL中;在這種情況下,如果要引用某個特定的Fraction物件,你必須在Fraction物件前冠以專案的名稱。以下這個例子將會產生一個Math專案中的Fraction物件:

Public Frac As New Math.Fraction

如果在所有的被引用的ActiveX DLL中只有一個Fraction物件,我們就不需要使用專案名稱。請看下例:

Public Frac As New Fraction

下一步我們要做的是完成Fraction物件類別的內容。請將下列的程式碼加入到MATHFRAC.CLS檔中:

Option Explicit

Public Num As Integer
Public Den As Integer

Public Sub Add(Num2, Den2)
    Num = Num * Den2 + Den * Num2
    Den = Den * Den2
    Reduce
End Sub

Public Sub Sbt(Num2, Den2)
    Num = Num * Den2 - Den * Num2
    Den = Den * Den2
    Reduce
End Sub

Public Sub Mul(Num2, Den2)
    Num = Num * Num2
    Den = Den * Den2
    Reduce
End Sub

Public Sub Div(Num2, Den2)
    Mul Den2, Num2
End Sub

Private Sub Reduce()
    Dim s As Integer
    Dim t As Integer
    Dim u As Integer

    s = Abs(Num)
    t = Abs(Den)
    If t = 0 Then Exit Sub
    Do
        u = (s \ t) * t
        u = s - u
        s = t
        t = u
    Loop While u > 0
    Num = Num \ s
    Den = Den \ s
    If Den < 0 Then
        Num = -Num
        Den = -Den
    End If
End Sub

公用屬性Num和Den讓原呼叫程式可以設定及讀取分子和分母的值,而公用方法Add、Sbt、Mul和Div以另一個分數來對Num和Den屬性值進行分數運算(Sub是Visual Basic的保留字,因此我們把執行減法運算的方法取名為Sbt)。Reduce方法是只在這個DLL內部運作的私有方法,它的功用是把分數約分為最簡分數。

我們可以準備編譯並且測試這個DLL了,但首先請打開「專案」功能表,選擇「 Math屬性」。在「專案屬性」對話方塊中,你可以看到「啟動物件」欄中是"(無)",因為我們並沒有在專案中加入Sub Main程序作為專案執行的起始點。如果你的物件需要做初始化的動作,你必須加入一個Sub Main程序,然後設定Sub Main為啟動物件。在本範例中,Fraction物件不需要初始化,因此啟動物件可以設為"(無)"。

「專案屬性」對話方塊中的「專案描述」欄是空白的,現在請輸入一段簡短的描述。當使用者在設定這個DLL作為引用項目時,他將可以看到「專案描述」中的文字。因此,請輸入"Demonstration of Fraction Object"到專案描述欄中,然後按下「確定」。

現在到了最後一個步驟──編譯Math物件,以建立一個完整的ActiveX DLL。從「檔案」功能表中選擇「製成MATHPROJ.DLL 」;在「製成執行檔」對話方塊中選擇欲存放DLL檔的目錄,然後按下「確定」。這樣,你就完成了DLL的編譯,同時也向你的系統註冊了這個DLL。

在發展環境中進行測試
 

要測試我們剛完成的ActiveX DLL,請在「檔案」功能表中選擇「新增專案」,然後在「新增專案」對話方塊中點選兩下「標準執行檔」圖像。這時你可以看到ActiveX DLL專案和標準執行檔專案一起在「專案」視窗中。

這個新增的專案其主要目的在於測試Math元件中的Fraction物件,因此我們不用大費周章地幫它命名,採用Visual Basic預設的Project1和Form1即可。現在我們要把Project1設為啟動專案:在「專案」視窗的Project1上面按下滑鼠右鍵,然後選擇「設為啟動專案」。

現在我們要讓測試專案認識Math元件以及Fraction物件。請從「專案」功能表中選擇「設定引用項目」,在「設定引用項目」對話方塊中點選Math核取方塊,然後按下「確定」。

在我們繼續進行下一步測試專案的建構之前,請將整個發展環境中目前所有的專案設定存成一個專案群組。只有專案群組才能儲存有關目前已載入專案的資訊。專案檔的附屬檔名是VBP,而專案群組的附屬檔名是VBG。請從「檔案」功能表中選取「另存新專案群組」,把FORM1存成FORM1.FRM,把Project1存成PROJECT1.VBP,最後,把專案群組存成MATHDEMO.VBG。

在Form1上面加入六個文字方塊和四個指令按鈕,它們的排列情形看起來就像圖27-1,另外我們再畫上一些線條加以美化。


 

 圖27-1 用來測試Fraction物件的表單

現在把六個文字方塊控制項命名為txtN1、txtD1、txtN2、txtD2、txtN3和txtD3 (依由上而下,由左而右的順序),然後將四個指令按鈕控制項命名為cmdAdd、cmdSubtract、cmdMultiply和cmdDivide,並把它們的Caption屬性依序改為"Add"、"Subtract"、"Multiply"、"Divide"。最後,在表單Form1中加入下列的程式:

Option Explicit

Public Frac As New Math.Fraction

Private Sub cmdAdd_Click()
    Frac.Num = txtN1.Text
    Frac.Den = txtD1.Text
    Frac.Add txtN2.Text, txtD2.Text
    txtN3.Text = Frac.Num
    txtD3.Text = Frac.Den
End Sub

Private Sub cmdDivide_Click()
    Frac.Num = txtN1.Text
    Frac.Den = txtD1.Text
    Frac.Div txtN2.Text, txtD2.Text
    txtN3.Text = Frac.Num
    txtD3.Text = Frac.Den
End Sub

Private Sub cmdMultiply_Click()
    Frac.Num = txtN1.Text
    Frac.Den = txtD1.Text
    Frac.Mul txtN2.Text, txtD2.Text
    txtN3.Text = Frac.Num
    txtD3.Text = Frac.Den
End Sub

Private Sub cmdSubtract_Click()
    Frac.Num = txtN1.Text
    Frac.Den = txtD1.Text
    Frac.Sbt txtN2.Text, txtD2.Text
    txtN3.Text = Frac.Num
    txtD3.Text = Frac.Den
End Sub

在表單層次中,我們宣告了一個物件變數Frac,用來引用定義在Math DLL中的Fraction物件,而Frac變數所引用的Fraction物件則是定義在模組層次;這個物件並不是程式一開始執行就存在,一直要等到使用者按下第一個運算按鈕時,這個物件才會真正的產生。當系統將表單載出時,Frac所引用的物件就會自動破壞。

到了這裡,你已經可以進行對Fraction物件的測試了,請輸入數字到等號左邊的文字方塊中,然後進行四則運算。圖27-2所顯示的是3/4乘以5/6的結果。


 

 圖27-2 執行中的Fraction Math測試程式

建立及使用完成的DLL模組
 

當DLL檔通過了偵錯階段後,你必須把專案編譯成可以使用的DLL模組。在這裡你所要做的就是從「檔案」功能表中選擇「製成MATHPROJ.DLL 」,就這麼簡單。

當你的DLL模組到了使用者手中,使用者必須向他的系統註冊這個DLL。我們可以用Visual Basic的「封裝暨部署安裝精靈」建立一個安裝程式存在磁片中,DLL註冊的工作就交給安裝程式來完成。

如何使用C語言建立DLL?
 

Visual Basic有一個很大的優點,它讓程式設計師能夠在很短的時間內完成一個專案;以專案發展生產力的角度來看,Visual Basic的程式發展環境實在少有對手能與之抗衡。在另一方面,C語言所帶來的優點則在於C程式無人能比的執行速度。我們可以用C語言來發展傳統的DLL。

透過最新版的Microsoft Visual C++,建立DLL的工作比以前簡單了許多。我們以下面的範例來說明如何用C語言建立傳統的DLL,希望能讓你很容易地了解建立DLL工作的重點(我們假設程式只在32位元作業系統中執行)。

兩個C檔案
 

以下所列出的是在Visual C++ 中你唯一需要的兩個檔案。請在32位元版本的Visual C++ 中新增一個專案,選擇Dynamic Link Library作為專案類型。在專案中建立一個DEF檔,在DEF檔中加入下面這幾行,然後將檔案命名為MYDLL.DEF。

; Mydll.def
LIBRARY Mydll

CODE PRELOAD MOVEABLE DISCARDABLE
DATA PRELOAD MOVEABLE

EXPORTS
    TestByte           @1
    TestInteger        @2
    TestLong           @3
    TestSingle         @4
    TestDouble         @5
    ReverseString      @6

這個DEF檔列出了外界所能使用的函式名稱,Visual Basic應用程式可以呼叫DEF檔所列出的函式。

這個DLL專案只有一個C程式檔,請把下面這段C程式存成MYDLL.C,並且確定它包含在DLL專案中。

#include <windows.h>
#include <ole2.h>

BYTE _stdcall TestByte( BYTE a, LPBYTE b )
{
    *b = a + a;
    return( *b + a );
}

short _stdcall TestInteger( short a, short far * b )
{
    *b = a + a;
    return( *b + a );
}

LONG _stdcall TestLong( LONG a, LPLONG b )
{
    *b = a + a;
    return( *b + a );
}
float _stdcall TestSingle( float a, float far * b )
{
    *b = a + a;
    return( *b + a );
}

double _stdcall TestDouble( double a, double far * b )
{
    *b = a + a;
    return( *b + a );
}

void _stdcall ReverseString( BSTR a )
{
    int i, iLen;
    BSTR b;
    LPSTR pA, pB;

    iLen = strlen( (LPCSTR)a );
    b = SysAllocStringLen( NULL, iLen );
    
    pA = (LPSTR)a;
    pB = (LPSTR)b + iLen -1;
    
    for ( i = 0; i < iLen; i++ )
        *pB-- = *pA++;
    pA = (LPSTR)a;
    pB = (LPSTR)b;
    
    for ( i = 0; i < iLen; i++ )
        *pA++ = *pB++;
    SysFreeString( b );
}

在Visual C++ 的「 Build 」功能表中選擇「 Build All 」,Visual C++ 會將專案中的兩個檔案加以連結,並且產生一個DLL檔MYDLL.DLL。把這個DLL檔移到Windows的SYSTEM目錄中,這樣,Visual Basic的Declare陳述式就可以找到MYDLL.DLL檔了。

測試DLL
 

若要測試MYDLL.DLL中的六個函式,請在Visual Basic中建立一個新專案,在表單中加入一個指令按鈕控制項cmdGo,然後加入下列程式到表單中。

Option Explicit

Private Declare Function TestByte _
Lib "mydll.dll" ( _
    ByVal a As Byte, _
    ByRef b As Byte _
) As Byte

Private Declare Function TestInteger _
Lib "mydll.dll" ( _
    ByVal a As Integer, _
    ByRef b As Integer _
) As Integer

Private Declare Function TestLong _
Lib "mydll.dll" ( _
    ByVal a As Long, _
    ByRef b As Long _
) As Long

Private Declare Function TestSingle _
Lib "mydll.dll" ( _
    ByVal a As Single, _
    ByRef b As Single _
) As Single

Private Declare Function TestDouble _
Lib "mydll.dll" ( _
    ByVal a As Double, _
    ByRef b As Double _
) As Double

Private Declare Sub ReverseString _
Lib "mydll.dll" ( _
    ByVal a As String _
)

Private Sub cmdGo_Click()
    Dim bytA As Byte
    Dim bytB As Byte
    Dim bytC As Byte
    Dim intA As Integer
    Dim intB As Integer
    Dim intC As Integer
    Dim lngA As Long
    Dim lngB As Long
    Dim lngC As Long
    Dim sngA As Single
    Dim sngB As Single
    Dim sngC As Single
    Dim dblA As Double
    Dim dblB As Double
    Dim dblC As Double
    Dim strA As String
        
    bytA = 17
    bytC = TestByte(bytA, bytB)
    Print bytA, bytB, bytC
        
    intA = 17
    intC = TestInteger(intA, intB)
    Print intA, intB, intC
        
    lngA = 17
    lngC = TestLong(lngA, lngB)
    Print lngA, lngB, lngC
        
    sngA = 17
    sngC = TestSingle(sngA, sngB)
    Print sngA, sngB, sngC
        
    dblA = 17
    dblC = TestDouble(dblA, dblB)
    Print dblA, dblB, dblC
    
    strA = "This string will be reversed"
    Print strA
    ReverseString (strA)
    Print strA
End Sub

執行本程式並且按下了「執行」按鈕之後,每一個MYDLL.DLL中的函式都會被呼叫,結果顯示在表單上,如圖27-3。


 

 圖27-3 測試MYDLL.DLL函式的結果

從MYDLL.DLL中的五個數值函式中,你可以看到同一資料型別的資料以兩種傳遞方式傳遞。數值函式的第一個參數以ByVal的方式由Visual Basic程式傳遞到函式裡,而第二個參數以ByRef的方式傳遞;以ByVal方式傳遞的參數在函式中不能被修改,而以ByRef方式傳遞的函式則可以被修改。在測試程式中,我們傳數值17給每個函式的第一個參數,每個函式會將這個值加倍後存入第二個參數,因而改變了測試程式中的第二個引數的內容。另外,每個數值函式都有一個傳回值,這是第一個參數和第二個參數加總的結果,如圖27-3所示。

在測試用的Visual Basic程式中,我們用了ByRef關鍵字在函式的宣告部分。由於ByRef是預設的資料傳遞方法,你可以把它拿掉,但ByVal不是預設的,因此必須保留。在函式宣告部分寫出ByRef的好處是它讓函式更易於被了解,不容易令人覺得混淆。

在MYDLL.DLL中的TestInteger函式裡,我們並沒有用C的int型別來接收Visual Basic程式傳遞的Integer資料,反而用了short。是因為在32位元的Microsoft C語言裡,int是一個32位元的整數,而非如Visual Basic的Integer是16位元,因此,使用short才不致產生錯誤。另外提醒你一點:short在所有版本的Microsoft C中都是16位元。

字串不管在Visual Basic或Microsoft C中目前都是以BSTR型別來處理(在C語言裡要宣告字串為BSTR)。在Visual Basic程式中,我們以ByVal的方式傳遞一個字串給MYDLL.DLL中的ReverseString函式;由於函式中的BSTR字串是以字串的位址來進行處理,因此由ByVal方式傳來的字串仍然可以被改變。你也可以用ByRef方式來傳遞字串,但其效果並沒有多大的差別。

在ReverseString函式裡,我們使用了SysAllocStringLen和SysFreeString函式,這兩個函式可以用來處理BSTR字串,並且使得C語言的DLL更容易處理由Visual Basic傳來的字串。


參考資料:

請參閱 第三十四章"進階應用程式" 的BitPack應用程式,這個應用程式介紹了另一個DLL的建立過程及使用的情形。


如何建立一個在遠端執行的應用程式?
 

Visual Basic企業版可以建立一個在遠端執行的應用程式,簡稱遠端應用程式;遠端應用程式可以提供公用物件屬性和方法給近端(客戶端)應用程式使用。這種利用Remote Automation ActiveX科技使工作分散給不同CPU處理的運作方式,稱為分散式計算(Distributed Computing)。

只要是能夠提供公用屬性和方法的應用程式都可以被建構成遠端應用程式。建立遠端應用程式所涉及的觀念與建立近端應用程式的觀念相同,只是編譯時必須多設一些編譯選項,而且,應用程式必須向遠端及近端系統註冊,這使得專案發展稍微複雜了一些。

在這一節中,我們要帶領你走過遠端應用程式的建立及使用等步驟,文中以一個簡單的應用程式來說明這些步驟。

建立遠端應用程式
 

如果要建立一個遠端應用程式,你需要建立一個新的ActiveX執行檔專案,然後在「專案屬性」對話方塊中的「元件」頁籤下核取「遠端伺服器檔案」,如圖27-4所示。「遠端伺服器檔案」選項要求Visual Basic產生一些相關檔案,這些檔案使得近端的電腦能夠使用遠端應用程式。應用程式經過編譯之後,Visual Basic會產生登錄項目檔(Registration File,VBR)以及型別庫(Type Library,TLB),供客戶端電腦使用。


 

 圖27-4 「專案屬性」對話方塊中的「元件」頁籤

這個範例程式──質數應用程式(PRIME.VBP)告訴你如何將一個佔用大量CPU資源的工作轉載給遠端系統。本應用程式只提供一個物件,叫做Number,Number物件有一個Value屬性。如果指定一個正數給Value,應用程式會從小於或等於這個正數的範圍內,找出最接近此正數的質數。

質數應用程式有一個很有趣的功能──非同步處理(Asynchronous Processing)。當我們要找一個很大的質數時(例如數值1,000,000左右的質數),程式本來應該要花很長的時間來處理,但這個應用程式讓我們可以用詢問(Poll)的方式來取得屬性Value的值──如果傳回值是大於零的整數,表示已經找到了我們所要的質數──因此,在找尋大的質數時就不會使客戶 端電腦停滯不動。

以下就是Number物件類別的內容:

`NUMBER.CLS
Dim mlMaxNumber As Long
Dim mlFound As Long

Property Get Value() As Long
    `Return prime number
    `Note that Value is 0 until number is
    `found
    Value = mlFound
End Property

Property Let Value(Setting As Long)
    `Initialize module-level variables
    mlMaxNumber = Setting
    mlFound = 0
    `Launch asynchronous calculation
    frmLaunch.Launch Me
End Property

Friend Sub ProcFindPrime()
    Dim Count As Long
    For Count = 2 To mlMaxNumber \ 2
        DoEvents
        If mlMaxNumber Mod Count = 0 Then
            mlMaxNumber = mlMaxNumber - 1
            ProcFindPrime
            Exit Sub
        End If
    Next Count
    mlFound = mlMaxNumber
End Sub

當程式執行權交回給原呼叫程式之後,本應用程式用一個計時器控制項啟動ProcFindPrime程序。以下是啟動表單frmLaunch的程式內容,表單上的計時器控制項取名為tmrLaunch:

`LAUNCH.FRM
Dim mnumObject As Number

Public Sub Launch(numObject As Number)
    Set mnumObject = numObject
    tmrLaunch.Enabled = True
    tmrLaunch.Interval = 1
End Sub

Private Sub tmrLaunch_Timer()
    `Turn off timer
    tmrLaunch.Enabled = False
    `Launch calculation within object
    mnumObject.ProcFindPrime
End Sub

遠端應用程式的註冊
 

遠端應用程式只安裝在遠端伺服器電腦上,但必須向遠近兩端的電腦註冊。如果要向遠端系統註冊應用程式,只要在遠端系統執行一次應用程式的執行檔即可,Visual Basic應用程式有自行註冊的功能。

如果要向近端系統註冊應用程式,必須複製該應用程式的VBR檔和TLB檔到近端系統中,並且執行Visual Basic光碟中COMMON\TOOLS\CLIREG目錄下的CLIREG32.EXE公用程式。以下這行指令會把質數應用程式註冊到近端系統中,並且指明伺服器的名稱為WOMBAT2:

CLIREG32 PRIME.VBR -t PRIME.TLB -s WOMBAT2

CLIREG32公用程式會顯示出一個對話方塊,讓你修改應用程式的登錄項目記錄,如圖27-5所示。


 

 圖27-5 使用CLIREG32對話方塊設定網路通訊協定

Remote Automation支援好幾種網路通訊協定,包括:TCP/IP、IPX和NetBEUI。一般而言,如果某個網路通訊協定無法通連遠端系統,Remote Automation會自動使用下一個近端系統所支援的協定。CLIREG32對話方塊中的網路通訊協定選項決定了網路通訊協定使用的先後順序。

CLIREG32所做的工作很簡單,它只是提供一個前端界面給Visual Basic產生的登錄項目檔,方便程式設計師修改檔案的內容。大部分在Windows裡的登錄檔檔名中都有附屬檔名REG;你也可以自己把VBR附屬檔名改為REG,將原VBR檔中的前面兩行改由REGEDIT4和空行來代替,然後用登錄編輯程式來登錄應用程式,不過,你需要在登錄檔中加入伺服器和網路通訊協定的資訊。以下是質數應用程式的VBR檔內容:

VB5SERVERINFO
VERSION=1.0.0
HKEY_CLASSES_ROOT\Typelib\{9311AADB-B46F-11D1-8E5B-000000000000}\
    1.0\0\win32 = Prime.exe
HKEY_CLASSES_ROOT\Typelib\{9311AADB-B46F-11D1-8E5B-000000000000}\
    1.0\FLAGS = 0
HKEY_CLASSES_ROOT\Prime.Number\CLSID = 
    {9311AADD-B46F-11D1-8E5B-000000000000}
HKEY_CLASSES_ROOT\CLSID\{9311AADD-B46F-11D1-8E5B-000000000000}\
    ProgID = Prime.Number
HKEY_CLASSES_ROOT\CLSID\{9311AADD-B46F-11D1-8E5B-000000000000}\
    Version = 1.0
HKEY_CLASSES_ROOT\CLSID\{9311AADD-B46F-11D1-8E5B-000000000000}\
    Typelib = {9311AADB-B46F-11D1-8E5B-000000000000}
HKEY_CLASSES_ROOT\CLSID\{9311AADD-B46F-11D1-8E5B-000000000000}\
    LocalServer32 = Prime.exe
HKEY_CLASSES_ROOT\INTERFACE\
    {9311AADC-B46F-11D1-8E5B-000000000000} = Number
HKEY_CLASSES_ROOT\INTERFACE\
    {9311AADC-B46F-11D1-8E5B-000000000000}\ProxyStubClsid = 
    {00020420-0000-0000-C000-000000000046}
HKEY_CLASSES_ROOT\INTERFACE\
    {9311AADC-B46F-11D1-8E5B-000000000000}\ProxyStubClsid32 = 
    {00020420-0000-0000-C000-000000000046}
HKEY_CLASSES_ROOT\INTERFACE\{9311AADC-B46F-11D1-8E5B-000000000000}\
    Typelib = {9311AADB-B46F-11D1-8E5B-000000000000}
HKEY_CLASSES_ROOT\INTERFACE\
    {9311AADC-B46F-11D1-8E5B-000000000000}\Typelib\"version" = 1.0

執行遠端應用程式
 

如果要透過Remote Automation使用遠端應用程式,伺服器中必須執行Automation Manager應用程式(AUTMGR32.EXE),而且伺服器電腦上的安全性設定必須容許使用者執行必要的動作。

如果要啟動Automation Manager,請從Windows的Visual Basic啟動功能表中選取Automation Manager,Automation Manager會叫出一個小小的視窗,標示目前伺服器的狀態,如圖27-6所示。


 

 圖27-6 Automation Manager視窗

Windows NT為每一位使用者提供完整的安全檔案。因此,如果要使用遠端Windows NT伺服器中的物件,使用者對伺服器必須要有適當的存取權利(Access Privileges)。若要啟動一個尚未執行的應用程式,該應用程式必須允許由遠端產生物件。「Remote Automation連結管理員」(RACMGR32.EXE)提供了一個前端介面來設定系統登錄中的Remote Automation的內容,如圖27-7。


 

 圖27-7 在伺服器電腦上使用「Remote Automation連結管理員」

為了達到偵錯的目的,請將「客戶端存取」頁籤下的「系統安全方針」設定為「可產生所有的物件」。


注意:

因為安全模式有所不同,在「 Remote Automation連結管理員」中可供使用的存取控制清單(Access Control List,ACL)功能只適用於Windows NT,在Windows 95中並不適用。


使用遠端應用程式
 

一旦安裝好遠端應用程式並且完成登錄之後,就可以使用遠端伺服器中的物件了。以下的程式是一個簡單的測試程式,用來測試質數應用程式。圖27-8顯示了範例程式執行的結果。

Option Explicit

Private Sub Form_Load()
    Dim x As New Prime.Number
    x.Value = 42
    Debug.Print x.Value
End Sub


 

 圖27-8 範例程式執行的結果

從遠端應用程式中傳回錯誤訊息
 

發生在遠端程式中的錯誤會被傳回給近端用戶程式。當錯誤發生時,你可以顯示自訂的錯誤訊息,通知用戶程式錯誤已經發生。我們把修改後的Value屬性程序列在這裡:

Property Let Value(Setting As Long)
    `Raise error for negative values
    If Setting <= 0 Then
        Err.Raise 8001, "Prime.Number", _
            "Value must be a positive integer."
        Exit Property
    End If
    `Initialize module-level variables
    mlMaxNumber = Setting
    mlFound = 0
    `Launch asynchronous calculation
    frmLaunch.Launch Me
End Property

圖27-9顯示了由遠端應用程式傳回的錯誤訊息。


 

 圖27-9 如果負數指定給Value屬性,近端用戶程式會收到錯誤訊息

對遠端應用程式進行偵錯
 

在將應用程式安裝到遠端系統之前,應該在目前的系統中對應用程式徹底地偵錯;一旦遠端應用程式被安裝且完成登錄之後,對應用程式作任何改變,都必須向遠端及近端系統重新登錄。

遠端應用程式被使用後,它可能仍然在伺服器電腦的記憶體中。因此,在重新登錄應用程式之前,請記得按Ctrl-Alt-Del叫出Windows NT Task Manager,如圖27-10,停止該應用程式。

Remote Automation的故障排除
 

由Remote Automation傳回的錯誤訊息通常都不夠明確,無法告訴我們確切的問題在哪裡,而且追蹤所有可能造成錯誤的原因是一件令人傷腦筋的大工程。以下這張表列出了部分與Automation相關的錯誤,以及筆者認為可能發生的原因。


 

 圖27-10 在重新登錄應用程式前,檢查Windows Task Manager中該應用程式是否已經停止執行

Remote Automation不是運作正常就是完全無法運作,而且通常發生的問題都是因為很簡單的問題,例如,伺服器名稱包含了 \\,或者在系統登錄中錯誤地將share name和不相干的伺服器連在一起。

錯誤代碼(&H) 訊息本文 可能發生的原因
&H80076ba The RPC server is unavailable. 在用戶端系統登錄中的記錄裡,遠端應用程式所在的伺服器名稱錯誤。遠端應用程式已經關閉。在伺服器中找不到遠端應用程式的執行檔。
&H800706be The remoteprocedure call failed. 對伺服器的網路連結已經中止或者等待時間超過上限。
&H800706d9 There are no more endpoints from the endpoint mapper. Automation Manager(AUTMAG32.EXE) 並未在伺服器中執行。

如何建立Visual Basic發展環境的增益功能?
 

Visual Basic的增益功能(Add-in)是一種特殊的程式,它可以附加到Visual Basic的發展環境中,為發展環境增加一些自訂的功能。例如,你可以用增益功能表來增加Visual Basic發展環境的主功能表項目;或者,你可以利用增益功能幫你處理一些表單載入或儲存時必須做的瑣事。程式碼管理員,如Microsoft Visual Source Safe,就是一種十分實用的增益功能。

在MSDN CD ROM裡的Visual Basic SAMPLES目錄中有兩個範例增益功能應用程式之範例,TabOrder和VisData,但筆者認為這些範例有點令人覺得混淆,因為它們把Visual Basic Extensibility物件模型使用得過於複雜。Visual Basic也提供了增益功能專案的範本,你可以在建立增益功能時利用這個專案範本。

在這一節裡,我們將介紹一個簡單的範例,幫助你了解如何建立及使用增益功能應用程式。一旦清楚地了解了這個範例,就可以較容易地掌握Visual Basic所提供的範例。

基本觀念
 

增益功能是一個特別建構的Visual Basic應用程式,我們將以一個範例來說明。這個範例增益功能可以把設計階段表單上的所有按鈕都設定為同樣大小。我們的範例程式將由Main程序啟動,Main程序在程式模組中(BAS檔),這個程序的主要目的在於確使VBADDIN.INI加入正確的註冊記錄,使得Windows能夠找到該增益功能,這個動作我們稱之為註冊。一個新的增益功能只要註冊一次即可;要完成增益功能的註冊手續,最好的方式就是在安裝增益功能時執行增益功能的執行檔。因為你是在建立自己發展的增益功能而不是安裝一個買來的增益功能,因此,在完成增益功能的建立工作之後,必須執行一次增益功能的執行檔,以完成增益功能的註冊。

從Visual Basic的「增益集」功能表中選擇「增益功能管理員」之後,所有已註冊的增益功能都會在備用狀態下,讓你將某個增益功能載入到目前的Visual Basic發展環境中,也可以用「增益功能管理員」從目前的發展環境中取消某個增益功能。在本例中,我們要在增益功能應用程式中建立一個特別的物件類別,這個物件類別沿用(Implement)了Visual Basic的延伸功能(Extensibility)界面,IDTExtensibility;若要沿用這個界面,必須用Implements關鍵字,如下所示:

Implements IDTExtensibility

界面(Interface)可被視為你的程式與另一個程式(在本例中,另一個程式是Visual Basic)之間的契約。因此,Implements也就是表示你的程式將會使用可以被Visual Basic呼叫的程序。如果要使用Visual Basic的IDTExtensibility,你的程式必須完全沿用下列四個程序,如下表所描述。

程序名稱 發生的時機
IDTExtensibility_OnAddInsUpdate Visual Basic發展環境啟動並且載入該增益功能時;這發生在Visual Basic剛啟動時。
IDTExtensibility_OnStartupComplete 增益功能已經完成載入至Visual Basic環境中;這發生在Visual Basic已經完成啟動之後。
IDTExtensibility_OnConnection 使用者從「增益功能管理員」對話方塊中選用了該增益功能。
IDTExtensibility_OnDisconnection 使用者從「增益功能管理員」中取消了該增益功能。

IDTExtensibility_OnConnection程序提供了一個參數VBInst,透過這個參數我們可以使用Visual Basic延伸功能的根物件(VBIDE.VBE)。透過VBInst,你可以改變Visual Basic的功能表和工具列,將程序和Visual Basic發展環境中的事件予以連結,以及執行一些其他的動作。


注意:

Visual Basic 4使用ConnectEvents方法將物件類別模組中的事件程序與Visual Basic環境中的事件連結在一起。現在,Visual Basic新的延伸功能物件模型為我們提供了方便的管道,讓我們可以在應用程式中使用更多Visual Basic發展環境的功能。


在完成增益功能的註冊以及載入增益功能到Visual Basic環境等動作之後,剩下的工作就由增益功能的事件程序MenuHandler_Click來負責。本例中的MenuHandler_Click透過部分Visual Basic物件的層級,執行這個增益功能的核心任務-設定設計階段作用中表單上指令按鈕的大小。

建立新的增益功能
 

請新增一個ActiveX執行檔專案,然後在專案中另外加入一個物件類別模組和程式模組(簡稱模組);把程式模組取名為Myaddin,並且把物件類別模組分別命名為Connect和Sizer。

接著請加入以下的程式到Myaddin模組中。Main程序唯一的工作是向Windows註冊這個增益功能,因此它執行的速度很快,外界幾乎察覺不到它做了什麼。

`Declare API to write to INI file
Declare Function WritePrivateProfileString _
Lib "Kernel32" Alias "WritePrivateProfileStringA" ( _
    ByVal AppName$, _
    ByVal KeyName$, _
    ByVal keydefault$, _
    ByVal FileName$ _
) As Long

`Declare API to read from INI file
Declare Function GetPrivateProfileString _
Lib "Kernel32" Alias "GetPrivateProfileStringA" ( _
    ByVal AppName$, _
    ByVal KeyName$, _
    ByVal keydefault$, _
    ByVal ReturnString$, _
    ByVal NumBytes As Long, _
    ByVal FileName$ _
) As Long

Sub Main()
    Dim strReturn As String
    Dim strSection As String
    `Be sure you are in the VBADDIN.INI file
    strSection = "Add-Ins32"
    strReturn = String$(255, Chr$(0))
    GetPrivateProfileString strSection, _
        "cmdSizer.Connect", "NotFound", _
        strReturn, Len(strReturn) + 1, "Vbaddin.Ini"
    If InStr(strReturn, "NotFound") Then
        WritePrivateProfileString strSection, "cmdSizer.Connect", _
            "0", "vbaddin.ini"
    End If
End Sub

Connect物件類別模組定義了IDTExtensibility_OnConnection和IDTExtensibility_OnDisconnection兩個事件程序的內容。(儘管IDTExtensibility_OnAddinUpdate和IDTExtensibility_OnStartupComplete這兩個事件程是空的程序,還是要把它們定義在模組中,因為我們必須完全沿用這四個事件程序)當使用者將增益功能載入Visual Basic發展環境或從發展環境中取消增益功能時,Visual Basic會自動呼叫這兩個程序。IDTExtensibility_OnConnection事件程序執行後,Visual Basic 「增益集」功能表中會增加一個新的功能表選項,而IDTExtensibility_OnDisconnection則負責移除該功能表選項。

有一點值得注意:對我們的範例增益功能而言,每次安裝增益功能時,Main程序都會執行一次;Connect物件類別模組裡的程序在每次載入或載出增益功能時都會執行一次;Sizer物件類別裡的程序,則在每次功能表中該增益功能選項被點選時執行。

請在Connect物件類別模組中加入下面的程式碼:

Option Explicit

`Indicate that this class implements the extensibility
`interface for Visual Basic
Implements IDTExtensibility

Dim VBInstance As VBIDE.VBE
Dim mnuSize As Office.CommandBarControl
Dim SizerHandler As Sizer

`Set these constants as desired
Const CMDBTNWIDTH = 1200
Const CMDBTNHEIGHT = 400

Private Sub IDTExtensibility_OnConnection _
    (ByVal VBInst As Object, _
    ByVal ConnectMode As vbext_ConnectMode, _
    ByVal AddInInst As VBIDE.AddIn, _
    custom() As Variant)
    `Save this instance of Visual Basic so you can refer to it later
    Set VBInstance = VBInst
    `Add menu item to Visual Basic's Add-Ins menu
    Set mnuSize = VBInstance.CommandBars("Add-Ins").Controls.Add(1)
    mnuSize.Caption = "&Size Command Buttons"
    `Create Sizer object
    Set SizerHandler = New Sizer
    `Establish a connection between menu events
    `and Sizer object
    Set SizerHandler.MenuHandler = _
        VBInst.Events.CommandBarEvents(mnuSize)
    `Pass VBInstance to Sizer object
    Set SizerHandler.VBInstance = VBInstance
    `Set command button sizing properties
    SizerHandler.ButtonWidth = CMDBTNWIDTH
    SizerHandler.ButtonHeight = CMDBTNHEIGHT
End Sub

`Removes menu item when user deselects this add-in in
`the Add-In Manager
Private Sub IDTExtensibility_OnDisconnection _
    (ByVal RemoveMode As VBIDE.vbext_DisconnectMode, _
    custom() As Variant)
    `Remove menu item
    VBInstance.CommandBars("Add-Ins").Controls _
        ("&Size Command Buttons").Delete
End Sub

`The following empty procedures are required because this
`class implements the IDTExtensibility interface
Private Sub IDTExtensibility_OnAddInsUpdate(custom() As Variant)

End Sub
Private Sub IDTExtensibility_OnStartupComplete(custom() As Variant)
End Sub

在上面的模組中,我們定義了兩個常數,CMDBTNWIDTH和CMDBTNHEIGHT。所有經過增益功能處理過的指令按鈕,都會按照這兩個常數來設定其尺寸。你可以自行修改這兩個常數的值,如果你覺得意猶未盡的話,你可以用一個對話方塊來讓使用者自行設定指令按鈕的大小。

如果使用者同時開啟了好幾個Visual Basic發展環境,系統的記憶體中也只能有一份增益功能的執行實體(instance);我們把由Visual Basic發展環境傳給IDTExtensibility_OnConnection程序的VBIDE.VBE物件存放在VBInstance物件變數中,就可以透過VBInstance存取到特定的Visual Basic執行實體中的表單和控制項。這樣,當使用者點選「增益集」功能表中的新增益功能選項時,增益功能就可以對這些表單和控制項做該執行的動作。

另一個物件模組Sizer,只定義了一個ManuHandler_Click事件程序。當使用者點選了新的功能表選項時,系統就會呼叫ManuHandler_Click事件程序。現在請加入下列的程式碼到Sizer物件類別模組中:

Option Explicit

`Sizer object properties
Public ButtonWidth As Long
Public ButtonHeight As Long
Public VBInstance As VBIDE.VBE

`Declare menu event handler
Public WithEvents MenuHandler As CommandBarEvents

`This event fires when menu is clicked in IDE
Private Sub MenuHandler_Click _
    (ByVal CommandBarControl As Object, _
    Handled As Boolean, CancelDefault As Boolean)
    Dim AllControls
    Dim Control As Object
    `Get collection containing all controls on form
    Set AllControls = _
        VBInstance.SelectedVBComponent.Designer.VBControls
    `For each control on the active formDear John, How Do I... 
    For Each Control In AllControls
        `Dear John, How Do I... if the control is a command buttonDear John, How Do I... 
        If Control.ClassName = "CommandButton" Then
            `Dear John, How Do I... resize it
            With Control.Properties
                .Item("Width") = ButtonWidth
                .Item("Height") = ButtonHeight
            End With
        End If
    Next Control
End Sub

當使用者點選「增益集」功能表中的新功能表選項時,ManuHandler_Click程序會對目前使用者專案的作用中表單逐一檢查所有的控制項;若控制項是指令按鈕,則依照程式預設的尺寸(Connect物件類別模組的CMDBTNWIDTH和CMDBTNHEIGHT)來設定其Width和Height屬性。

在執行這個增益功能應用程式之前,你必須設定一些重要的專案屬性,如圖27-11。請從「專案」功能表中選擇「專案屬性」,在「專案屬性」對話方塊中點選「一般」頁籤。把啟動物件設定為Sub Main,這樣程式第一次執行時才能找到正確的啟動程序。接下來,在「專案名稱」欄中打入cmdSizer。這必須和Sub Main程序用來向系統註冊的字串"cmdSizer.Connect"相吻合。如果兩者有所不同,你沒有辦法用「增益功能管理員」將增益功能載入到Visual Basic環境中,這是筆者遇到的第一個陷阱。下一步請在「專案描述」一欄中填入"Command Button Sizer Add-in",這段文字將會出現在「瀏覽物件」對話方塊中,幫助你找到物件。

現在請點選「專案屬性」對話方塊中的「元件」頁籤,然後在「啟動模式」的選項中選取「 ActiveX元件」。

增益功能的執行檔包含著ActiveX的程式碼,這些程式碼由一個外部的應用程式所呼叫,在我們的例子中,這個外部應用程式就是Visual Basic。Visual Basic發展環境利用ActiveX技術連接增益功能應用程式中的物件。

最後,請按下「確定」按鈕完成專案屬性的設定。


 

 圖27-11 cmdSizer增益功能的專案屬性設定內容

這裡又有另一個筆者遇到的陷阱。請不要忘記設定Microsoft Office物件和VBIDE.VBE物件為專案的引用項目,我們的增益功能對這兩者依賴甚重。請從「專案」功能表中選取「設定引用項目」,核取「 Microsoft Office 8.0 Object Library 」以及「Microsoft Visual Basic 6.0 Extensibility 」,然後按下「確定」。如果忘了引用這些項目,當你執行這個程式時,你會收到一個錯誤訊息。

最後一個步驟,我們要設定物件類別模組的屬性,然後就可以執行程式。我們必須把Connect物件類別模組的Instancing屬性設為"5-MultiUse",這個屬性設定值允許增益功能程式載入到多個並存的Visual Basic執行實體中。Sizer物件類別模組的Instancing屬性可以被設為"1-Private"。

執行應用程式
 

新增益功能應用程式的執行檔必須執行一次,以便向Windows註冊。當你還在對程式進行偵錯時,可以從Visual Basic環境中執行增益功能的執行檔;一旦增益功能編譯成執行檔之後,就可以從Windows工作列的「開始」按鈕選擇的「執行」選項執行該執行檔。

在偵錯階段執行增益功能的步驟如下:按下Visual Basic環境中的"執行"按鈕,然後按下"中斷"按鈕。在「立即運算」視窗中打入Main,然後按下Enter鍵,以便在VBADDIN.INI中註冊增益功能。接下來,按下"繼續"按鈕,然後將整個Visual Basic最小化。

啟動另一個Visual Basic環境,用「增益集」中的「增益功能管理員」載入新的增益功能。如果沒有任何錯誤發生的話,你會看到cmdSizer增益功能被列在「增益功能管理員」對話方塊中。


注意:

因為該增益功能的專案啟動模式被設為ActiveX元件,所以當你在Visual Basic環境中執行程式時,Main程序不會被執行,你必須親自去執行Main程序才能完成增益功能的註冊手續。


另一個執行增益功能的方法就是利用執行檔。當你完成對增益功能的編譯之後,請關閉Visual Basic環境,然後在Windows中執行CMDSIZER.EXE一次(請注意:在註冊的過程中你看不到任何事情發生,註冊程序很快就完成了),然後再次啟動Visual Basic環境,看看是否能載入增益功能。請記住,你只需要執行一次增益功能的執行檔就可以完成向Windows註冊的工作了。

使用增益功能
 

如果要試用這個增益功能,請啟動Visual Basic,然後從「增益集」功能表中選取「增益功能管理員」,選擇cmdSizer.Connect,然後按下「確定」。如圖27-12所示,在「增益集」的底下,應該會有我們剛剛載入的增益功能「Size Command Buttons 」。


 

 圖27-12 增加的增益功能

現在新增一個專案,在表單中產生一些大小不一的指令按鈕,然後到「增益集」中點選「 Size Command Buttons 」,你會看到表單中的指令按鈕都變成了同樣大小。圖27-13顯示的是未經增益功能處理前的表單,圖27-14顯示的是處理後的結果。


 

 圖27-13 尚未調整大小的指令按鈕


 

 圖27-14 由增益功能調整後的指令按鈕

如何使應用程式可腳本化(Scriptable)?
 

Microsoft Windows Scripting Host (WScript.EXE)可以執行以VBScript和JScript寫成的文字檔,任何提供ActiveX物件的應用程式都能以Windows Scripting Host予以腳本化。


注意:

你可以從 http://www.microsoft.com/scripting/windowshost 這個網站下載Windows Scripting Host (WScript.EXE)。


腳本(Script)可以把使用者的工作和應用程式的測試工作變得可自動化。例如,以下這個腳本會產生一個Prime物件,並且顯示一列質數:

`TstPrime.VBS
dim i, prime, wscript, last, results
`Create objects
set prime = createobject("prime.number")
set wscript = createobject("wscript.shell")

for i = 1 to 20
    prime.value = i
    `Pause until answer is found
    do until prime.value > 0 : loop
    if prime.value <> last then
        `Add unique numbers to results
        results = results & " " & prime.value
    end if
    last = prime.value
next

`Display results
wscript.popup   result,,"Prime Test Script"

如果要執行這個腳本,請在「檔案總管」中點選兩下它的檔名或用以下這行命令:

wscript tstprime.vbs

tstprime.vbs的最後一行用PopUp物件方法顯示圖27-15中的結果,PopUp方法相當於Visual Basic的MsgBox指令。

對於類似Prime這種無使用者界面的應用程式而言,以腳本來做測試工具真的非常好用。然而,腳本的最大用處並不止於此;你可以在你的應用程式中使用腳本,讓使用者撰寫巨集,將他們的工作自動化。

使用Script控制項
 

Microsoft Script控制項(MSScript.OCX)讓你可以在應用程式中執行巨集。下幾個步驟告訴你如使用Script控制項:


 

 圖27-15 以WScript.EXE執行測試腳本
  1. 把Script控制項放進表單中。
  2. 根據巨集所使用的腳本語言,設定Script控制項的Language屬性。
  3. 在執行階段,用Script控制項的AddObject方法把應用程式裡的物件放進Script控制項中。這些物件可以是private物件(如表單或private物件類別),也可以是Public物件 ( 如Public ActiveX物件 )。
  4. 在執行階段,以Script控制項的AddCode物件方法載入巨集程序,載入後,用Script控制項的Run物件方法執行巨集程序。

注意:

你可以從 http://www.microsoft.com 網站下載Microsoft Script控制項(MSScript.OCX)。


使用Script控制項時,有以下幾點你必須記住:

在接下來的幾個小節中,我們會用Editor範例程式介紹如何處理上述的各種情況。Editor範例程式收錄在隨書光碟中。


 

 圖27-16 Editor範例程式(Editor.VBP)讓你撰寫可以修改文字檔的巨集

加入物件和巨集程序
 

你要用Script控制項的AddObject方法把在應用程式中的物件加入到Script控制項裡,這些被加入的物件不一定非得是Public物件,你可以放入Private表單和物件類別模組。被放進Script控制項的物件可以在VBScript裡使用,在VBScript裡的物件名稱就和AddObject方法使用的物件名稱一樣。例如,以下這段程式把Editor物件放到Script控制項裡:

Sub LoadObject()
    scrVB.AddObject "Editor", medtObject, True
End Sub

另外,你必須使用Script控制項裡的AddCode方法把要執行的巨集放進Script控制項裡。你可以一次放入一個巨集,也可以一次把所有的巨集都載入。例如,以下這段程式碼將整個Macros.VBS載入到Script控制項中:

Sub LoadMacros()
    scrVB.AddCode GetText("Macros.VBS")
End Sub

`Used by the preceding code to load file
Function GetText(FileName) As String
    Dim filScript As New Scripting.FileSystemObject
    Dim texScript As TextStream
    `Create a text stream from the FileSystemObject
    Set texScript = filScript.OpenTextFile(FileName, ForReading, True)
    `If file is empty or doesn't exit, don't read it
    If texScript.AtEndOfStream Then
        `Return an empty string
        GetText = ""
    Else
        `Return the text from the text stream into the text box
        GetText = texScript.ReadAll
    End If
    `Close the text stream
    texScript.Close
End Function

當Script控制項載入巨集時,會同時編譯程式碼。因此,如果程式中有任何錯誤都會在AddCode方法被呼叫時發生。在下一節中我們要介紹如何使用Script控制項的Error物件來處理編譯時期的錯誤。

在應用程式中,當AddCode物件方法載入一個含錯誤語法的巨集時,巨集的編譯時期錯誤就會發生。以下這段程式可以偵測到編譯時期的錯誤,並且在錯誤發生時會呼叫MacroError程序:

Private Sub UpdateMacros()
    `Unload all objects and macros
    scrVB.Reset
    `Add the Editor object to the Script control
    scrVB.AddObject "Editor", medtObject, True
    `Check for errors when code is parsed during
    `loading
    On Error Resume Next
    `Add new code to the Script control
    scrVB.AddCode medtObject.GetText(mstrMacroFile)
    `Call error handler if compile-time error occurs
    If Err Then MacroError
    `Add macros to the menu list
    UpdateMenus
End Sub

Sub MacroError()
    `Using the Script control's Error object
    With scrVB.Error
        `Create a new instance of this form
        Dim frmMacro As New frmEdit
        `Set MacroMode property
        frmMacro.MacroMode = True
        `Show modeless
        frmMacro.Show vbModeless, Me
        `Move the cursor to the error line
        frmMacro.medtObject.MoveDown .Line
        `Move the cursor to the error column
        frmMacro.medtObject.MoveRight .Column
        `Display the error information
        MsgBox Join(Array(.Source, .Description), _
            vbCrLf), vbCritical
        `Hide form so you can show it as modal
        frmMacro.Hide
        `Show form as modal so this code waits for
        `the user to fix the macro
        frmMacro.Show vbModal, Me
        `Update the macro list after corrections are made
        UpdateMacros
    End With
End Sub

上面這段程式做了許多動作,其中第一項是把Script控制項中先前載入的巨集和物件以Reset物件方法清除掉。接下來是載入Editor物件,然後再載入巨集。如果有任何錯誤發生,MacroError就會被呼叫,將巨集檔的內容顯示在另一個視窗中。

Editor範例程式用同一張表單來編輯巨集檔和其他文字檔。MoveDown和MoveRight物件方法被定義在Editor物件類別模組中,模組中的其他物件方法也同樣地可以在巨集中使用。Script控制項的Error物件所提供的Line和Column屬性,可供上述的物件方法用來把游標移到編譯錯誤發生的那一行,如圖27-17所示。


 

 圖27-17 當使用者修改巨集時,Editor範例程式就會編譯巨集檔,並能把游標移到錯誤發生處

Editor範例程式首先以非強制回應模式顯示巨集編輯視窗,這樣MoveDown和MoveRight方法才能在視窗內移動游標。接下來Editor將視窗隱藏,然後再以強制回應模式顯示視窗,這樣UpdateMacro會等到使用者修正錯誤之後才繼續執行。

執行巨集
 

你可以用Script控制項的Procedures物件集合一一存取已經載入的所有巨集,Procedures物件集合能讓你取得每一個Sub或Function的名稱、引數數目和傳回值等資訊。例如,以下的程式碼把巨集名稱放入Editor應用程式的Macros功能表裡:

`Unloads all the names on the Macro menu, then
`reloads names from the Procedures list of the
`Script control

Private Sub UpdateMenus()
    Dim proItem As Procedure
    Dim intCount As Integer
    `Unload Macro menu items
    For intCount = 1 To mnuProcedures.UBound
        Unload mnuProcedures(intCount)
    Next intCount
    intCount = 0
    `Add Macro names to the menu
    For Each proItem In scrVB.Procedures
        intCount = intCount + 1
        Load mnuProcedures(intCount)
        mnuProcedures(intCount).Caption = proItem.Name
    Next proItem
End Sub

Script控制項的Run方法可以執行巨集,Run物件方法把巨集名稱和巨集所需的引數作為它自己的引數,而如果巨集是一個Function,則巨集會有傳回值。Editor範例並不處理引數和傳回值,它只是簡單地執行巨集,如以下所示:

Private Sub mnuProcedures_Click(Index As Integer)
    `Turn on error handling
     On Error Resume Next
    `Run the selected macro
    scrVB.Run mnuProcedures(Index).Caption
    `If an error occurs, call MacroError
    If Err Then MacroError mnuProcedures(Index).Caption
End Sub

任何執行時期的錯誤只發生在Run物件方法執行時,下面這一節就專門討論處理執行時期錯誤的事宜。

處理執行時期的錯誤
 

像巨集的編譯時期錯誤一樣,執行時期錯誤在傳回時會傳回完整的錯誤資訊;然而,在某些情形下,Script控制項並無法辨識到底是哪裡出了錯。為了處理這種情況,請務必要把巨集名稱傳給錯誤處理程序,以便顯示最基本的錯誤資訊。

以下這段程式比前面提到的MacroError程序多了一些動作,改變的部分主要在於Automation錯誤發生時會把程序的名稱顯示出來。

Sub MacroError(Optional Procedure As String = "")
    `Using the Script control's Error object
    With scrVB.Error
        `Create a new instance of this form
        Dim frmMacro As New frmEdit
        `Set MacroMode property
        frmMacro.MacroMode = True
        `Show modeless
        frmMacro.Show vbModeless, Me
        `If the error number is zero, then it's an
        `Automation error, not a script error
        If .Number = 0 Then
            `If it's an Automation error, display the
            `procedure that caused the error
            If Err Then
                MsgBox "The procedure " & Procedure & _
                    " caused an Automation error.", vbCritical
            End If
        `Otherwise, there is more specific information
        `about the error, so display the line with the error
        Else
            `Move the cursor to the error line
            frmMacro.medtObject.MoveDown .Line
            `Move the cursor to the error column
            frmMacro.medtObject.MoveRight .Column
            `Display the error information
            MsgBox Join(Array(.Source, .Description), _
                vbCrLf), vbCritical
        End If
        `Hide form so you can show it as modal
        frmMacro.Hide
        `Show form as modal so that this code waits for
        `the user to fix the macro
        frmMacro.Show vbModal, Me
        `Update the macro list after corrections are made
        UpdateMacros
    End With
End Sub

傳遞字串給巨集
 

巨集所使用的所有參數都是Variant型別,因此,如果你建立的物件會在巨集中被使用,就必須考慮這一點。舉例來說,如果嘗試從巨集中傳遞一個變數給以下這個函式,這個函式會產生一個Automation錯誤:

`~~~.Find - Finds a string within the current text and
`selects the found string
`Returns True if found, False if not found
Public Function Find(FindText As String) As Boolean
    Dim lngFound As Long
    lngFound = InStr(Position + 1, mfrmParent.txtSource, FindText)
    `If there is a current selection, limit the search
    If mfrmParent.txtSource.SelLength Then
        If lngFound > mfrmParent.txtSource.SelStart + _
            mfrmParent.txtSource.SelLength Then
            Find = False
            Exit Function
        End If
    ElseIf lngFound <> 0 Then
        mfrmParent.txtSource.SelStart = lngFound - 1
        mfrmParent.txtSource.SelLength = Len(FindText)
        Find = True
    Else
        Find = False
    End If
End Function

這種問題很微妙,它只發生在傳遞變數時,至於字串則沒有這種困擾。看看以下這段程式碼便知道。

第一個對Find方法的呼叫沒有問題,但第二個呼叫就產生了錯誤:

`FindTest
Sub FindTest()
    FindString = "Howdy"
    Editor.Find "Howdy"     `Works as expected
    Editor.Find FindString     `Causes Automation error!
End Sub

如果要解決上述的問題,你必須把Find物件方法引數列中的As String拿掉,像下面這一行:

Public Function Find(FindText) As Boolean   'Remove As String

撰寫巨集程式碼
 

撰寫在Script控制項中執行的VBScript有點不同於撰寫一般的Visual Basic程式碼。例如,VBScript不支援換行接續符號,VBScript也沒有Visual Basic處理檔案與目錄的相關函式。

以下這段巨集程式碼是用Editor範例所寫成的,它告訴你如何用Editor的物件方法和屬性來修改檔案、列出檔名以處理VBScript的錯誤:

`Macros.VBS
`Counts actual lines (including line wraps)
Sub CountLines()
    Do 
        `Record starting position
        Start = Editor.Position
        `Move cursor down 1 line
        Editor.MoveDown
        `Keep track of number of lines
        NumLines = NumLines + 1
    `Repeat until cursor doesn't move
    Loop Until Editor.Position = Start
    `Show result
    MsgBox Editor.FileName & " has " & NumLines & " lines."
End Sub

`Changes tabs to 4 spaces
Sub TabsToSpaces()
    `Replace all tabs with spaces
    Editor.ReplaceAll vbTab, "    "
End Sub

`Displays a list of the files in the current directory
Sub BuildFileList()
    `Get a list of the files in the current directory
    Set CurrentDirFiles =  FileList(Editor.Directory)
    For Each FileItem In CurrentDirFiles
        `Display the filename
        Editor.Insert FileItem.Name
        `Add a carriage return and line feed
        Editor.Insert vbCrLf
    Next
    `Move back 1
    Editor.MoveLeft
    `Delete the last carriage return and line feed
    Editor.Delete
End Sub

`Returns the collection of files in a specified directory
Function FileList(Directory)
    `Create an object to get folder and file information
    Set FileSys = CreateObject("Scripting.FileSystemObject")
    `Return the collection of files in the directory
    Set FileList = FileSys.GetFolder(Directory).Files
End Function

`Causes a deliberate runtime error
Sub HandledRunTimeErr()
    On Error Resume Next
    Infinity = 1 / 0
    If Err Then
        MsgBox "VBScript error: " & Err.Number & " " & Err.Description
    End If
End Sub

請參考MSDN裡的主題"VBScript Language Reference",這裡有更多有關於VBScript的資訊。

如何傳遞使用者自訂型別資料到物件中?
 

Visual Basic現在已經可以讓函式和物件屬性傳遞陣列和使用者自訂型別(User-Defined Type,UDT)結構,在 第五章"物件導向程式設計" 裡我們已經介紹過傳遞陣列的例子,現在讓我們來看看如何傳遞UDT結構。

請建立一個新的ActiveX DLL專案,然後把物件類別模組的Name屬性設為MidPoint。把以下這段程式碼加入到物件類別模組中,存成MidPoint.cls。

`MIDPOINT.CLS
Option Explicit

Public Type typeCoordinate
    X As Double
    Y As Double
End Type

'Keep track of the most recent two coordinates
Private mcoordOne As typeCoordinate
Private mcoordTwo As typeCoordinate

'~~~Property (W/O): XY
Property Let XY(coordTest As typeCoordinate)
    `Bump previous coordinate
    mcoordTwo = mcoordOne
    `Store away this coordinate
    mcoordOne = coordTest
End Property

'~~~Property (R/O): XYMid
Property Get XYMid() As typeCoordinate
    `Return the midpoint
    XYMid.X = (mcoordOne.X + mcoordTwo.X) / 2
    XYMid.Y = (mcoordOne.Y + mcoordTwo.Y) / 2
End Property

請留意一下我們用Public Type來定義typeCoordinate UDT結構,所有MidPoint物件屬性所傳入或傳出的變數都是屬於這個型別。為了簡單起見,我們定義了一個唯寫的屬性XY,用來把一組座標值傳入MidPoint物件,另外也定義了一個唯讀屬性XYMid,用來傳出另一組經過計算後的座標值。

如果要測試MidPoint物件,你需要建立一個專案群組,其中包括一個標準執行檔專案和一個ActiveX DLL專案。請從「檔案」功能表中選取「新增專案」選項,然後選擇「標準執行檔」,接下來在「專案總管」視窗中以滑鼠右鍵點選這個標準執行檔專案,將這個專案設為啟動專案。把這個專案的Form1改名為frmPoint,加入以下這段程式,把Caption屬性設為Please Click on This Form,最後存成Point.frm。

`POINT.FRM
Option Explicit

Private Sub Form_Click()
    `Create a UDT variable
    Dim coordTest As typeCoordinate
    `Create an object
    Dim midpointTest As New MidPoint
    `Send the first coordinate to the object
    coordTest.X = 3
    coordTest.Y = 4
    midpointTest.XY = coordTest
    `Send the next coordinate to the object
    coordTest.X = 7
    coordTest.Y = 8
    midpointTest.XY = coordTest
    `Get the midpoint from the object
    coordTest = midpointTest.XYMid
    Print "Midpoint coordinate is ";
    Print coordTest.X;
    Print ", ";
    Print coordTest.Y
End Sub

在測試這個應用程式之前,這個標準執行檔專案必須引用這個ActiveX DLL專案。請在「專案總管」視窗中選取標準執行檔專案,然後從「專案」功能表中選取「設定引用項目」,在「設定引用項目」對話方塊中選取這個ActiveX DLL專案,按下「確定」後結束。

應用程式執行時,如果你在表單上按下滑鼠左鍵,一個新的MidPoint物件midpointTest以及一個UDT結構變數coordTest就會產生出來。coordTest的X值和Y值由第一組座標值 (3,4) 填入,然後coordTest的內容便會被傳進midpointTest物件的XY屬性裡,接著第二組座標值也會被傳到物件裡,midpointTest物件會記住這兩組座標值。

midpointTest的XYMid屬性會傳出一組座標值,指定給coordTest變數,這組座標值就是前面兩組座標點的中點座標值。執行的結果請看圖27-18。


 

 圖27-18 MidPoint物件的輸出結果

本例中的物件屬性所傳入和傳出的UDT結構很簡單,只包含兩個代表XY座標的數字。然而,在傳遞一個巨大的結構時,你會發現使用這個傳遞資料的技巧有多麼方便!