17. ActiveX 控制項

ActiveX控制項為當時與Visual Basic 4一起發表的OCX控制項的後代,雖然它們有著一樣的副檔名,但是骨子裡卻是截然不同的。最初的OCX控制項包含了許多低階化的功能(且理所當然的,它還必須支援COM介面),也因此當時的OCX控制項既龐大且執行速度緩慢。而新的ActiveX控制項則是特別重新設計用來被嵌進一份HTML文件中,並且針對了它們的收納器,如Microsoft Internet Explorer,Visual Basic表單或是支援ActiveX的環境支援許多的功能。藉由這樣的方式,ActiveX控制項比早先的OCX控制項體積更小,下載時間更少,且更快載入記憶體中。

ActiveX控制項之基礎架構
 

Visual Basic 5和6提供了所有開發ActiveX控制項所需要用到的工具,可以將開發完成的ActiveX控制項應用在所有以後的專案中。更確切的說,藉由Visual Basic,可以開發兩種不同型態的ActiveX控制項:

  • 私有的(Private)ActiveX控制項可包含在任何型態的專案中,控制項的副檔名為.ctl,檔案格式為一般的文字檔。要將控制項加進Visual Basic專案中,只要將檔案加到專案裡即可。(此乃原始碼重複使用)
     
  • 公用的(Public)ActiveX控制項只能包含於ActiveX控制項專案,必須先將其編譯為OCX檔,之後就可以在任何以Visual Basic、Visual C++,或其他支援ActiveX控制項的開發環境所開發出來的Windows應用程式使用它。(此種控制項格式為binary檔,故稱為二進位檔重複使用)
     

Visual Basic 5是第一個讓我們以視覺化的方式開發ActiveX控制項的程式語言,如同待會兒會看到的,可以將多個基本控制項組合成一個功能強大的新控制項。這些控制項稱為組成控制項(constituent controls)。例如,藉由一個圖片顯示框及兩個捲軸控制項,可建立一個能夠捲動圖片內容的新控制項。此外,Visual Basic也允許使用者不使用任何組成控制項來建立一個新的控制項。此即所謂的owner-drawn ActiveX控制項。

當在設計ActiveX控制項的時候,也應該將下面的東西謹記在心:一般把使用程式的人分為兩類─開發者與使用者。但為了更了解ActiveX控制項的行為,還需要考慮另一個角色,那就是控制項本身的作者。作者的工作是是準備一個將被開發者所用來開發程式的控制項。如您所見,作者與開發者是有那麼點不同,即使常常這兩者為同一人。(亦即,一開始您是控制項的作者,然後又成為使用它的開發者)。

建立使用者控制項模組
 

在這一節裡,筆者要告訴您如何建立一個超級文字盒(super-Textbox),比正常的文字盒(Textbox)多出一些額外功能,比方說過濾錯誤的字元。下面是每次當要建立一個新的ActiveX時,必須做的步驟:

  1. 加入一個新的ActiveX控制項專案,此新專案裡已經包含了一個使用者控制項模組。(若建立的是個Private ActiveX控制項,則需手動在專案選單中選擇加入一個使用者控制項模組)
  2. 給予此專案一個有意義的名字與敘述,稍後表單的名字變成了控制項的函式庫,且以字串的型態出現在以後使用此控制項的專案之元件對話盒中。在這個範例裡,我們使用SuperTB及An enhanced TextBox control作為控制項的名稱及敘述。
  3. 點選使用者控制項表單以取得駐點(focus),在屬性視窗中輸入控制項名稱。在此範例中,請輸入SuperTextBox。
  4. 在使用者控制項表單上放置一或多個組成控制項。在此我們需要放置一個標籤(Label)控制項和一個文字方塊(TextBox)控制項,如圖17-1。


 

圖17-1 設計階段的SuperTextBox控制項

除了OLE Container控制項(其文字方塊圖示取得聚點時會被呈現disabled狀態)外,可以使用任何基本的控制項作為ActiveX控制項的組成控制項。也可以使用非系統內建的控制項(例如其他協力廠商開發的控制項)來作為組成控制項,但應該確定您有其合法的使用權來包裝在您自己的控制項中。除了DBGrid以外,所有Visual Basic提供的控制項都可以自由地在ActiveX控制項中重複使用。在包裝控制項之前,請先仔細地閱讀協力廠商的控制項版權宣告文件。在本章最後會有更多有關版權的建議。最後,可建立不使用組成控制項的ActiveX控制項,像是SuperLabel控制項:可在本書附贈光碟的SuperText專案相同目錄下找到它。

現在可關閉使用者控制項設計視窗,然後切換到Visual Basic標準執行檔專案中。此時會有個新圖示出現在工具箱裡。點選它,將其拖曳到表單上,如圖17-2所示。

恭喜您!您已經建立了第一個ActiveX控制項了。

之前的說明有個特別的地方需要注意。在測試專案中使用控制項前,必須先將ActiveX控制項設計畫面正確地關掉。如果忽略了此動作,則在測試專案內的控制項會成為無法使用狀態。事實上,Visual Basic會在您關掉使用者控制項設計視窗後,才允許在測試專案中選取及使用您的控制項。

必須記住,必須處理此控制項的兩種執行個體(Instance),一個為設計階段個體,另一個為執行階段個體。不同於Visual Basic的其他模組,使用者控制項即使在測試專案的設計模式中也必須為可使用狀態,因為控制項必須針對程式設計者的動作做出事件回應,像是在屬性視窗中鍵入屬性值或在表單中改變控制項的大小。然而,當開啟了使用者自訂控制項設計視窗,控制項本身正處於設計模式中,因此不能在任何表單中使用。要使用一個ActiveX控制項,必須關閉設計視窗,如同之前筆者所提的。


 

圖17-2 測試平台上的一個SuperTextBox執行個體。屬性視窗裡包含了一些Visual Basic定義好的屬性。

使用ActiveX控制項介面精靈
 

我們第一個版本的SuperTextBox控制項還沒有做任何有用的事,但是可以執行應用程式並且沒有任何錯誤產生。要將這樣的控制項改成一個有用的控制項,必須加入一些屬性和方法,及撰寫新功能所用到的程式碼。

為了要完成SuperTextBox控制項,需要加入所有使用者希望找到的屬性,例如ForeColor、Text及SelStart。少數幾個屬性必須出現在屬性視窗,其餘的則為執行階段屬性。也需要加入一些其他屬性及方法來擴增原本基本TextBox的功能─例如FormatMask屬性(此屬性影響到text屬性的格式)或Copy方法(複製控制項的內容到剪貼簿中)。

在大部分例子中,這些屬性和方法直接對應到組成控制項的屬性及方法。例如, ForeColor和Text屬性對應到Text1組成控制項的相同名字的屬性;Caption屬性則和Label1組成控制項的Caption屬性關聯。這種連結型態和第七章的繼承觀念十分類似。

為了讓建立ActiveX控制項的公用介面及程式碼更為容易,Visual Basic提供了ActiveX控制項介面精靈。這個外掛程式與Visual Basic一起被安裝在開發環境中,但可能需要在外掛程式管理員對話方塊中將此精靈載入。

在精靈的第一步驟中,可選擇介面的成員,如圖17-3所示。精靈列出了組成控制項所顯露出來的屬性,方法及事件讓來您選擇對外的項目。在範例裡,右列裡的項目除了BackStyle以外,其餘皆保留,然後加入以下的項目:Alignment、Caption、Change、hWnd、Locked、MaxLength、MouseIcon、MousePointer、PasswordChar、SelLength、SelStart、SelText、Text,所有OLE開頭的屬性,方法及事件,這些成員讓SuperTextBox控制項和一般的TextBox控制項的成員相同﹔少數的屬性被省略掉,如MultiLine和ScroolBars。稍後會說明這樣的理由。


 

圖17-3 ActiveX控制項介面精靈的第一步驟。也可以一次選取多個要加入的屬性,方法及事件。

說明

很不幸的,ActiveX控制項介面精靈讓您加入許多屬性、方法以及事件,但不見得都是您想要加入的項目─例如,ToolTipText、CausesValidation、WhatsThisHelpId和Validate事件。實際上,Visual Basic會自動地加入這些成員到任何您所建立的ActiveX控制項,所以不需要指定要加入它們,除非打算將這些控制項使用在其他非Visual Basic的環境裡。後面會有更多有關這方面的話題。


下一個步驟,要為您的ActiveX控制項定義所有顯露出來的屬性,方法及事件。應該將下列的成員加入您的控制項中:FormatMask、FormattedText、CaptionFont、CaptionForeColor、CaptionBackColor等屬性;Copy、Clear、Cut和Paste等方法以及SelChange事件。

第三個步驟,定義了控制項的公用成員跟組成控制項的對應關係。例如,Alignment公用屬性應該與Text1組成控制項的Alignment屬性對應﹔對清單中大部分的成員來說也是同樣的道理。可以直接選取所有的清單中的成員,並且指定它們對應到Text1控制項,以加快建立對應的工作,如圖17-4。


 

圖17-4 ActiveX控制項介面精靈的第三個步驟中,可在左邊的清單中一次選取多筆成員,然後選取右邊的下拉式清單某一控制項建立成員的對應關係。

讓我們從幾個簡單的例子來看,比如說Caption,對應到組成控制項Label1的屬性。當這兩個屬性名稱不同時,必須指出原本在組成控制項中的成員名稱﹔比方說CaptionForColor、CaptionBackColor及CaptionFont屬性相對於Label1的ForeColor、BackColor和Font屬性。其他方面,必須將公用成員對應給使用者自訂控制項本身,像是Refresh方法。

也許會有些不能直接對應至任何組成控制項的成員,如在第四個步驟的時候,定義這樣的幾個成員。比如說,宣告了Copy、Cut、Clear以及Paste方法。同樣地,宣告的FormatMask是一個可以在設計階段及執行階段具有讀寫的屬性﹔反之,FormattedText則在設計階段無法改變,且只能唯讀於執行階段。您必須給予這兩個屬性空字串以作為預設值,然而即使改變了屬性的格式為字串,精靈並不會自動改變對初設給屬性的預設值0。而且必須為每個方法及事件輸入參數列,如同圖17-5所示。

儘管ActiveX控制項介面精靈是個易學易用的,但仍有一些限制。不能定義擁有參數的屬性,也不可以針對所有的屬性輸入敘述─例如CaptionFont及CaptinoForeColor對應到組成控制項的屬性。


注意

如果您是使用Visual Basic撰寫國際性軟體的程式設計師,請注意,如果您控制台的系統時區設定不是英文的話,ActiveX控制項介面精靈繼承了一個奇異的臭蟲(Bug)。當布林常數True和False轉換成字串時,得到的值會依據當時控制台的區域設定而異(例如,如果區域時間為義大利的話,得到的字串會變「Vero」和「Falso」)。因此在這樣的環境下,精靈無法產生正確的程式碼,而您也許必須手動去修正它。或者是說,如果您願意只在一種環境下執行精靈的話,可以將區域時間只設定為英文。



 

圖17-5 在ActiveX控制項介面精靈的第四個步驟中,將決定方法及事件的語法,以及屬性在執行階段是否為可讀寫或唯讀。

最後只要按下 Finish 按鈕,精靈便會產生SuperTextBox控制項的所有程式碼了。如果回到測試平台,會發現您的控制項變成了灰色的。每當改變了這個控制項的公用介面時,便會有這種情形。可以在控制項的父表單上按下右鍵然後選擇更新使用者控制項選項來使您的控制項恢復正常狀態。

加入缺少的程式片段
 

藉由觀察ActiveX控制項使用者介面精靈所產生的程式碼,可以將控制項的內部運作方式徹底的了解。大部分的情況下,使用者自訂控制項的模組和一般的類別模組沒什麼兩差別。有一個重點要注意:精靈在模組中加上了許多的註解行讓使用者了解類別成員是如何運作的。您應該遵循這些註解行的告誡及避免刪掉或修改它們。

代表屬性、方法及事件
 

筆者之前說過,大部分精靈產生的程式碼並沒有特別的作用,但是它們代表了內部組成控制項的實際動作。例如,下面這段程式碼告訴我們Text屬性是如何運作的:

Public Property Get Text() As String
    Text = Text1.Text
End Property
Public Property Let Text(ByVal New_Text As String)
    Text1.Text() = New_Text
    PropertyChanged "Text"
End Property

PropertyChanged方法告訴了收納器屬性是否被更動過,在這裡的收納器是Visual Basic。這樣的做法有兩個目的,第一,在設計階段,Visual Basic應該知道控制項是否被更改過,然後將更動過的內容存放於FRM檔中。第二,在執行階段,如果Text屬性跟資料庫欄位結合,Visual Basic必須更新資料錄的資料。我們會在下一章討論資料感知ActiveX控制項。

代表的機制同樣也運用在方法及事件上。例如下面的例子,看看SuperTextBox模組捕捉Text1控制項的KeyPress事件,然後將其對外發布。同樣注意其如何代替使用者控制項的Refresh運作。

' The declaration of the event
Event KeyPress(KeyAscii As Integer) 

Private Sub Text1_KeyPress(KeyAscii As Integer)
    RaiseEvent KeyPress(KeyAscii)
End Sub
Public Sub Refresh()
    UserControl.Refresh
End Sub

屬性
 

ActiveX控制項介面精靈對於無法對應到組成控制項屬性的公用屬性,會自動幫它們建立私有的變數成員來存放外界指定的值。讓我們看看下面FormatMask屬性的例子:

Dim m_FormatMask As String
Public Property Get FormatMask() As String
    FormatMask = m_FormatMask
End Property
Public Property Let FormatMask(ByVal New_FormatMask As String)
    m_FormatMask = New_FormatMask
    PropertyChanged "FormatMask"
End Property

在比較特別的例子如FormattedText中,這個屬性會改變其他屬性的值。所以應該修正這個由精靈產生的程式碼,如下所示:

Public Property Get FormattedText() As String
    FormattedText = Format$(Text, FormatMask)
End Property

因為FormattedText屬性被定義為在執行階段為唯讀,所以精靈只產生了Property Get副程式而沒有產生Property Let副程式。

方法
 

對於每個加入的方法,精靈只產生簡單的Sub或Function基本架構。其餘的程式碼則隨您的意思加入。下面的例子,可以決定Copy和Clear方法要如何實作出來:

Public Sub Copy()
    Clipboard.Clear
    Clipboard.SetText IIf(SelText <> "", SelText, Text)
End Sub
Public Sub Clear()
    If SelText <> "" Then SelText = "" Else Text = ""
End Sub

在上面的程式碼中,您也許比較喜歡用Text1.Text及Text1.SelText來代替Text及SelText,但是筆者建議不要這樣做。如果使用公用名稱的話,程式碼執行起來稍微比較慢一點,但是如果之後決定對Text屬性做一些運作上的改變的話,那會節省相當多的時間。

事件
 

透過ActiveX控制項介面精靈,精靈只會完成事件宣告的程式碼,因為它無法得知您欲在何時且何處引發事件。

SuperTextBox控制項宣告了SelChange事件,它只在SelStart屬性或SelLength屬性(或兩者)改變時才會引發。如果想要在狀態列上秀出目前選取文字,或依據是否任何選取文字來打開或關閉工具列的功能,此事件是非常有用的。要實作這個事件,必須加入兩個私有變數和一個被此控制項中多個事件呼叫的私有副程式:

Private saveSelStart As Long, saveSelLength As Long
' Raise the SelChange event if the cursor moved.
Private Sub CheckSelChange()
    If SelStart <> saveSelStart Or SelLength <> saveSelLength Then
        RaiseEvent SelChange
        saveSelStart = SelStart
        saveSelLength = SelLength
    End If
End Sub
Private Sub Text1_KeyUp(KeyCode As Integer, Shift As Integer)
    RaiseEvent KeyUp(KeyCode, Shift)
    CheckSelChange
End Sub
Private Sub Text1_Change()
    RaiseEvent Change
    CheckSelChange
End Sub

在隨書光碟中,可以找到完整的示範專案,CheckSelChange副程式被Text1裡的MouseMove和MouseUp事件呼叫,同時也在屬性SelStart和SelLength裡的副程式中被呼叫。

對應到多個控制項的屬性
 

有的時候針對某幾個事件,可能需要撰寫程式碼來控制您的控制項。比如說,Click及DblClick事件﹔可能需要將其對應到Text1組成控制項上,但是當使用者單擊在Label控制項上面時,在使用者控制項模組中應該也產生一個同樣的事件。這表示必須為這些事件撰寫程式碼:

Private Sub Label1_Click()
    RaiseEvent Click
End Sub
Private Sub Label1_DblClick()
    RaiseEvent DblClick
End Sub

有的時候可能需要撰寫額外的程式碼來使某些屬性可以對應到多個控制項上面。想讓ForeColor屬性影響Text1及Label1兩個控制項,因為精靈只能將屬性對應到一個控制項上,所以勢必在屬性副程式Let中自己加上一些程式碼(在下面的例子粗體的部分),讓其他的組成控制項也可以一起接受的到同樣的屬性值。

Public Property Let ForeColor(ByVal New_ForeColor As OLE_COLOR)
    Text1.ForeColor = New_ForeColor
    Label1.ForeColor = New_ForeColor
    PropertyChanged "ForeColor"
End Property

然而,卻不需要更改相對的屬性副程式Get裡的程式碼。

保留控制項的屬性值
 

對於控制項的屬性值,ActiveX控制項介面精靈會自動幫我們產生保留屬性值於FRM檔中的程式碼。這種保留機制與persistable ActiveX元件相同(我 在十六章 有提過)。然而在這裡,不用要求ActiveX控制項去儲存它自己的屬性,因為在Visual Basic的編輯環境下,只要有任何屬性被修改更動,Visual Basic會自動將所有的控制項屬性儲存起來。

當控制項被放置在表單上時,Visual Basic引發其UserControl_InitProperties事件。在這個事件中,控制項應該給予自己的屬性一些初始值。下面是精靈產生給SuperTextBox控制項的程式碼:

Const m_def_FormatMask = ""
Const m_def_FormattedText = ""

Private Sub UserControl_InitProperties()
    m_FormatMask = m_def_FormatMask
    m_FormattedText = m_def_FormattedText
End Sub

當Visual Basic將目前的表單儲存到FRM檔中時,它會引發ActiveX控制項的UserControl_WriteProperties事件將控制項自己的屬性儲存起來。

Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
    Call PropBag.WriteProperty("FormatMask", m_FormatMask, m_def_FormatMask)
    Call PropBag.WriteProperty("FormattedText", m_FormattedText, _
        m_def_FormattedText)
    Call PropBag.WriteProperty("BackColor", Text1.BackColor, &H80000005)
    Call PropBag.WriteProperty("ForeColor", Text1.ForeColor, &H80000008)
    ' Other properties omitted....
End Sub

在傳給PropertyBag物件的WriteProperty方法的第三個參數為屬性的預設值。如果所處理的屬性為顏色值時,通常參數會為系統標準十六進位常數﹔比如,&H80000005為vbWindowBackground常數值(系統預設背景顏色)﹔&H80000008為vbWindowText常數值(系統預設字型顏色)。但是很不幸精靈並不會產生這些系統常數。如果需要了解這些支援系統參數的常數值,可以使用物件瀏覽工具列出所有VBRUN函式庫中的SystemColorConstants常數值。

當Visual Basic重新讀取一個FRM檔時,同時會引發UserControl_ReadProperties事件來讓ActiveX控制項讀取它自己的屬性:

Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
    m_FormatMask = PropBag.ReadProperty("FormatMask", m_def_FormatMask)
    m_FormattedText = PropBag.ReadProperty("FormattedText", _
        m_def_FormattedText)
    Text1.BackColor = PropBag.ReadProperty("BackColor", &H80000005)
    Text1.ForeColor = PropBag.ReadProperty("ForeColor", &H80000008)
    Set Text1.MouseIcon = PropBag.ReadProperty("MouseIcon", Nothing)
    ' Other properties omitted....
End Sub

同樣地,PropertyBag的ReadProperty方法的最後一個參數為該屬性的預設值。如果手動修改這些由精靈產生的程式碼的話,請確定在InirPeroperties、WriteProperties及ReadProperties事件中使用的常數是一樣的。

雖然精靈快速地產生了許多屬性的程式碼,但是有時候還是得自己做一些必要的修正。比方說,產生的程式碼直接指定屬性值給某些組成控制項,在大部分的專案中,這樣是沒問題的。但是在某些專案中,這些屬性可能對應到多個組成控制項,而應該將這些值指定給公用屬性才對。另一方面,使用公用屬性會引發控制項的屬性副程式Let和Set,這樣會使得PropertyChanged事件被引發而導致屬性值又被儲存一次,即使它們並未被修改。筆者會本章後面告訴您如何避免這樣的問題。

除此之外,精靈所產生的程式碼並非全是我們需要的。像是在SelStart、SelText、SelLength及FormattedText中的儲存及讀取屬性的程式碼,在設計階段並不是正確的。將這些相關的程式碼從ReadProperties及WriteProperties副程式拿掉能讓FRM檔更短且加速讀取。

使用者控制項的Resize事件
 

在ActiveX控制項的生命週期中,使用者自訂控制項物件會引發許多事件,筆者為在本章後面將它們全部討論。在這裡,筆者要告訴各位一個特別重要的事件:Resize事件。在設計階段,程式設計師將控制項由工具箱拖曳到表單上時便會引發它﹔如果控制項大小更動的話,也會引發Resize事件。在設計控制項的時候,必須針對此事件做特別的處理,使得所有的組成控制項能相對應的移動與改變大小。在我們的範例中,組成控制項的位置及大小是倚賴於SuperTextBox控制項是否有非空值的Caption屬性。

Private Sub UserControl_Resize()
    On Error Resume Next
    If Caption <> "" Then
        Label1.Move 0, 0, ScaleWidth, Label1.Height
        Text1.Move 0, Label1.Height, ScaleWidth, _
            ScaleHeight - Label1.Height
    Else
        Text1.Move 0, 0, ScaleWidth, ScaleHeight
    End If
End Sub

為了避免ActiveX控制項小於Label1組成控制項而發生錯誤,我們在前面加上了On Error敘述句。由於當Caption屬性改變時,前面的程式碼也必須被執行,所以也需要在屬性副程式Let中加入下面的敘述句:

Public Property Let Caption(ByVal New_Caption As String)
    Label1.Caption = New_Caption
    PropertyChanged "Caption"
    Call UserControl_Resize
End Property

使用者控制項物件
 

使用者控制項物件是放置組成控制項的容器。聽起來好像與表單物件類似,但實際上,使用者控制項物件與表單物件共享許多屬性,方法以及事件。比方說,可以透過ScaleWidth及ScaleHeight屬性得知控制項物件中物件的尺寸﹔使用AutoRedraw屬性在使用者控制物件上建立固定的圖像﹔也可以利用BorderStyle來增加物件的邊寬。使用者控制項物件也支援所有form物件支援的圖形屬性與方法,包括Cls、Line、Circle、DrawStyle、DrawWidth、ScaleX以及ScaleY。

使用者控制項也支援大部分Form物件的事件,例如Click、DblClick、MouseDown、MouseMove及MouseUp事件。當使用者在使用者控制項非組成控制項區域上移動滑鼠時便會引發這些事件。另外使用者控制項也支援KeyDown、KeyUp和KeyPress事件,但只有指點落在控制項本身而非組成控制項或當使用者控制項的KeyPreview屬性設定為True時。

使用者控制項物件的生命週期
 

使用者控制項身為物件,因此在它們的生命週期中會接收到數種不同的事件。ActiveX控制項則是有兩個生命週期,因為它們也必須在設計階段存活著。

建立
 

Initialize為使用者控制項第一個接收到的事件。在這個事件中,還沒有任何Windows的資源會分配給控制項,所以不該使用任何組成控制項元件。這與避免在form物件的Initialize事件中參考任何控制項是同樣的道理。基於同樣的理由,Extender和AmbientProperties物件在這個事件中也不允許存取(這些物件會在下面的部分討論到)。

過了Initialize事件後,使用者控制項建立了它所有的組成控制項和準備被放置在使用者的表單上。當被放置完畢後,Visual Basic引發了InitProperties或ReadProperties事件,而引發哪個事件取決於控制項是從工具箱拖曳到表單上或表單是被開啟的。在這些事件中,Extender和Ambient物件被允許存取。

當使用者控制項成為可視狀態之前,控制項會接收到Resize事件,然後才是Show事件。這個事件與Activate事件十分類似,但Activate事件並不支援使用者控制項。最後使用者控制項會接收到一個Paint事件(除非它的AutoRedraw屬性設為True)。

在設計階段,當控制項的父表單被關閉後然後再開啟時,控制項會被重新建立。 InitProperties在這樣的過程中並不會被引發,而是在Resize事件後以ReadProperteis事件來取代。

終止
 

當開發人員在設計階段關閉了父表單或當程式切換到執行模式時,Visual Basic會將設計階段ActiveX控制項的實體摧毀掉。如果開發人員更改了一個或多個控制項的屬性,使用者控制項便會接收到WriteProperties事件。在這個事件中,Visual Basic並不會在FRM檔中寫入任何東西,而是簡單的紀錄這些值於記憶體中的PropertyBag物件。這個事件只有在程式設計師更改了表單上任何控制項的屬性(或者是表單自己本身)才會發生﹔但是當使用使用者控制項時,這些動作是不必要的。控制項會透過呼叫PropertiChanged方法來告訴您它有個屬性被改變而需要去更新FRM檔案。當控制項從收納器中被移除時,會發生Hide事件(在HTML網頁中的ActiveX控制項會在使用者瀏覽別的網頁時發生此事件)。這個事件與表單的Deactive事件相同﹔ActiveX控制項雖然已不可見,但是依然存在於記憶體中。

AictveX控制項生命週期的最後一個事件為Terminate﹔通常會在此事件裡將所有開啟的檔案關閉,然後將在Initialize事件裡所取得的系統資源釋放回去Windows。在這個事件中,無法存取Extender及AmbientPropertes物件。

其他的事件順序
 

當開發人員執行程式時,Visual Basic會摧毀掉ActiveX控制項設計階段的實體,然後建立一個執行階段的實體,讓控制項可以接收到所有先前提到的事件。在設計階段與執行階段的實體中大的差異在於,後者不會有WriteProperties事件發生。

當重新開啟專案時,會引發另一個特別的事件順序:一個新控制項的instance會被建立起來,在建立的過程中會引發所有通用的事件,包括WriteProperties事件來更新記憶體中PropertyBag物件。

最後,當一個表單模組被編譯時,Visual Basic會建立一個表單隱藏的實體,這樣編譯程式便可以藉此查詢到所有ActiveX控制項最新的屬性值。每個ActiveX控制項都會接收到Initialize、Resize、ReadProperties、Show、WriteProperties、Hide及Terminate事件。不必對這些事件做任何特別的處理,筆者提及這個問題的原因是因為如果程式碼包含了中斷點或者是MsgBox函式的話,有可能會中斷編譯程式的程序。

Extender物件
 

當建立一個使用者控制項模組,然後將之放置在表單上時,也許會注意到屬性視窗中並非是空的,如同圖17-2所示。這些屬性是從哪來的呢?

原來是Visual Basic並不會直接使用ActiveX控制項,而是以所謂的Extender物件將使用者及控制項區隔開來。Extender物件將ActiveX控制項所有的屬性顯露給程式設計師,另外再加上Visual Basic幫我們處理的屬性。比方說,Name、Left、Top及Visible為Extender屬性,所以您使用者控制項模組中不必去理會這些屬性的處理方式。其餘Extender屬性為Height、Width、Align、Negotiate、Tag、Parent、Container、TooTipText、DragIcon、DragMode、CauseValidation、TabIndex、TabStop、HelpContextID及WhatThisHelpID。

Extender物件也提供它自己的方法及事件。例如Move、Drag、SetFocus、ShowWhatsThis及Zorder方法為收納器所提供﹔GotFocus、LostFocus、Validate、DragDrop及DragOver為事件。使用ActiveX控制項的程式設計師所看到的屬性實際上與控制項的設計者不同,後者看到更少的屬性、方法以及事件。


 

讀取Extender屬性
 

然而有時候,需要從使用者控制項模組中存取Extender屬性。可藉由控制項的Extender屬性來取得,透過此屬性,可以得到一個與程式設計師在使用控制項時相同的Extender物件參照。一個典型的例子,當Visual Basic控制項被建立時,在物件上會顯示出它的物件名稱。如果要在SuperTextBox加上這樣的特色時,可在InitProperties事件副程式中加上下面的敘述:

Private Sub UserControl_InitProperties()
    On Error Resume Next
    Caption = Extender.Name
End Sub

也許您會覺得奇怪,為什麼需要加上錯誤處理來保護這樣一個簡單的指定敘述呢?理由是,不能預料您的ActiveX控制項是在什麼樣的環境下被使用,所以不能保證Name屬性會在當時的執行環境被支援。如果不幸Name並沒有被支援,那使用者便無法使用您的控制項了。一般來說,不同的執行環境下會產生不同的Extender成員。Visual Basic也許是支援最多Extender屬性的執行環境了。

由於Extender物件是在設計階段的執行環境中被建立,所以Extender屬性便會傳回一個物件。基於某些理由,所有的Extender成員例如Name或Tag是透過後期連結來讀取的。所以在您的使用者控制項中存取這些Extender成員時會將使得速度將低。因為不能確定什麼樣的Extender成員會在執行階段被顯露出來,不該讓您的ActiveX控制項過度地依賴它們,應該儘量減少這樣的情況,以免在不適用的環境下導致效率降低。

最後,要注意少數Extender屬性只在某些情況係會建立。例如,Align與Negotiate屬性只在使用者控制項的Alignable屬性為True時才會有,且Default與Cancel屬性只會當使用者控制項的DefaultCancel屬性為True時方會存在。同樣地,如果InvisibleAtRuntime屬性為True時,Visible屬性是不可用的。

設定Extender屬性
 

一般來說,從使用者控制項中更改Extender屬性是一種不好的習慣。我發現在Visual Basic 6中,所有的Extender屬性都可以被寫入,但是在別的環境下或先前的Visual Basic版本中不見得相同。某些場合中,我們會透過新增的函式來設定Extender的屬性。例如在下面的例子裡,可以看到如何透過一個方法來使得您的ActiveX控制項與父表單大小完全吻合。

Sub ResizeToParent()
    Extender.Move 0, 0, Parent.ScaleWidth, Parent.ScaleHeight
End Sub

這個程序只保證在Visual Basic環境下可以正常執行,因為其他的執行環境下也許不支援Move Extender方法,而也無法確定是否父表單已然存在,或是否支援ScaleWidth及ScaleHeight。如果前面的任何一種狀況發生時,會產生代碼為438的錯誤訊息「物件不支援此屬性或方法」。

從收納器的觀點來看,Extender的屬性比使用者控制項的屬性有更高的優先權。舉例來說,如果使用者控制項模組包含一個Name的屬性,使用者的程式碼─最起碼是Visual Basic的程式碼─會先參照Extender屬性的Name屬性。基於這個理由,應該小心地選擇您自己的屬性名稱。


小秘訣

也可以定義一些與Extender物件屬性名稱相同的屬性,這樣使用者就可以藉由這些屬性而不用理會它們所使用的程式語言了。舉例來說,可以定義一個Tag屬性(String或Variant型態),這樣您的控制項即使在非Visual Basic的環境中,也可以使用Tag屬性了。


Object屬性
 

您也許會問這樣的問題:我是否可以直接跳過Extender物件而直接存取我的ActiveX控制項呢?答案是肯定的。這樣歸功於Object屬性,透過此屬性,我們可以得到使用者控制項物件內部的參照。這對於使用ActiveX控制項的開發人員來說是非常有用的。

' Set the Tag property exposed by the UserControl module.
' Raises an error if such property isn't implemented
SuperTextBox1.Object.Tag = "New Tag"

不需要在使用者控制項中使用Extender.Object屬性,因為透過Me關鍵字也可以傳回同樣的參照物件。

AmbientProperties物件
 

ActiveX控制項常需要收集一些它所屬的表單資訊﹔比方說,可能想要讓您的ActiveX控制項使用字型與表單一致。在某些情況下,可以使用Extender或Parent物件(例如,Parent.Font),但是這並不是一個好方法。

確認父表單的設定
 

使用者控制項物件的Ambient屬性傳回一個AmbientProperties的物件參照,此物件記載許多ActiveX控制項執行環境下的屬性。例如,可以透過Ambient.Font來找出被父表單所使用的字型﹔或者可透過Ambient.ForeColor和Ambient.BackColor屬性決定父表單所使用的顏色。當要建立與父表單目前設定一致的控制項時,這些資訊是特別有用的。下面的範例說明如何改進SuperTextBox控制項,讓它感覺像是Visual Basic自己的控制項:

Private Sub UserControl_InitProperties()
    ' Let the label and the text box match the form's font.
    Set CaptionFont = Ambient.Font
    Set Font = Ambient.Font
    ' Let the label's colors match the form's colors.
    CaptionForeColor = Ambient.ForeColor
    CaptionBackColor = Ambient.BackColor
End Sub

AmbientProperties物件在控制項開發及執行階段裡便一直伴隨著ActiveX控制項,不像是Extender物件只有在支援的執行環境裡存在。Visual Basic會藉由後期連結的方式來取得AmbientProperites物件的參照,然後自動地給予AmbientProperties物件預設的屬性。詳細情形可分為兩種結果:Ambient屬性速度比Extender屬性快,且在參照Ambient時,不需要撰寫錯誤攔截。比方說,AmbientProperties包含一個DisplayName屬性,此屬性會傳回執行環境下ActiveX控制項的名稱,可透過此屬性來初始控制項的標題。

Private Sub UserControl_InitProperties()
    Caption = Ambient.DisplayName
End Sub

這段程式碼比使用Extender.Name屬性為佳,因為在任何環境下它都會傳回有理的結果,而不需要使用On Error敘述來攔截錯誤。

另一個可能會覺得有用的Ambient屬性是TextAlign,用來指定表單上控制項文字的對齊方式。此屬性會傳回下列常數:0-一般、1-靠左、2-置中、3-靠右、4-齊行。如果執行環境不支援這些常數,Ambient.TextAlign則會傳回0-一般(文字靠左對齊,數字靠右對齊)。

若您的控制項包含了圖片盒,如果可能的話,應該將Ambient.Palette值設給圖片盒的Palette屬性。這樣您控制項的點陣圖在沒有駐點的時候才不會看起來怪怪的。

UserMode屬性
 

UserMode屬性也許是Ambient屬性中最重要的了,因為開發者可以藉由此屬性得知控制項是否正在被使用(UserMode = False)與否(UserMode = True)。藉由此屬性可以在設計階段及執行階段處理各種不同的控制項行為。如果很難去記住UserMode屬性傳回值的意義的話,可假想在UserMode裡的「使用者」為使用者。本章後面的〈唯讀屬性〉一節裡,可以看到此屬性如何使用的例子。

AmbientChanged事件
 

透過AmbientChanged事件,可以很快地知道某個Ambient屬性已改變了。這個事件會接收一個與被改變的屬性名稱相同的字串傳回值。舉個例子,透過下面的程式碼,可讓使用者控制項的背景顏色自動的與父表單的背景色相同:

Private Sub UserControl_AmbientChanged(PropertyName As String)
    If PropertyName = "BackColor" Then BackColor = Ambient.BackColor
End Sub

不過這裡有個例外情況:如果改變了父表單的FontTransparent或Palette屬性的話,父表單上的ActiveX控制項並不會接收到任何訊息。AmbientChanged事件在設計階段與執行階段都會被引發,所以可能需要使用Ambient.UserMode屬性來區分這兩種情況。

在使用者自繪(user-drawn)控制項中,AmbientChanged事件是最重要事件了。這些屬性在DisplayAsDefault屬性被改變時,就必須重繪它們自己:

Private Sub UserControl_AmbientChanged(PropertyName As String)
    If PropertyName = "DisplayAsDefault" Then Refresh
End Sub

ActiveX控制項的區域化
 

Ambient LocaleId屬性所傳回的屬性值為一長整數值,此數值依據使用控制項的程式所位在的區域來決定。可以依此傳回值來決定使用何種語言─比如說,一個字串表,資源檔,或者是附屬的DLL檔案。但必須要對各語言所顯示出來的結果做些修飾。

當編譯程式時,Visual Basic的區域碼會變成應用程式的區域碼。但使用您的控制項的應用程式也許會自動的根據其所在的區域更換區域碼。在使用者自訂控制項中的Initialize事件中,設定區域碼的副程式並未完整,所以ambient的LocaleId屬性傳回Visual Basic編譯時的的預設區域碼。基於這種情形,如果想要透過此屬性來更換控制項所使用的語言的話,應該遵循下面的結構來撰寫程式:

Private Sub UserControl_Initialize()
    ' Load messages in the default (Visual Basic's) locale.
    LoadMessageTable Ambient.LocaleID
End Sub    
Private Sub UserControl_InitProperties()
    ' Load messages in the user's locale.
    LoadMessageTable Ambient.LocaleID
End Sub
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
    ' Load messages in the user's locale.
    LoadMessageTable Ambient.LocaleID
End Sub
Private Sub UserControl_AmbientChanged(PropertyName As String)
    ' Load messages in the new user's locale.
    If PropertyName = "LocaleID" Then LoadMessageTable Ambient.LocaleID 
End Sub
Private Sub LoadMessageTable(LocaleID As Long)
    ' Here you load localized strings and resources.
End Sub

需要在InitProperties和ReadProperties兩個事件中讀取區域碼資料,因為前者在控制項被放置在表單上便會引發﹔後者在專案開啟時或應用程式執行時皆會被引發。

其他的ambient屬性
 

Ambient.ScaleMode屬性傳回收納器目前單位度量的計量單位(例如:twip)。此屬性對於使用者或者是開發人員都可能是有用的資訊。在 〈轉換計量單位〉 一節中,將告訴各位如何輕鬆地轉換表單與使用者之間度量單位的方法。

Ambient.DisplayAsDefault屬性只有在使用者自繪控制項中屬性值設為true時才真正發揮作用。在控制項的Extender.Default屬性值為true時,這些控制項的邊框一定會變厚,應該在AmbientChanged事件中檢查該屬性是否被更動過。

Ambient.SupportsMnemonics屬性會傳回應用程式環境是否支援熱鍵功能,即Caption屬性裡使用And(&)字元所定義的熱鍵。大部分的收納器都會支援此功能,但也可在Show事件中測試不支援此功能的環境裡。

Ambient.RightToLeft屬性指定控制項的文字是否由右到左對齊,也許在希伯來文或阿拉伯語系的Windows版本會需要用到它。其餘的ambient屬性─MessageReflect、ShowGrabHandles、ShowHatching與UIDead-則對於控制項的開發無實際的用處,可以將它們忽略。

實作方面
 

使用者控制項中包含許多與表單模組不太一樣的屬性,方法及事件。在這一節中,筆者會說明大部分,且先做簡單地提示,本章後面會再做深入探討。

處理輸入的駐點
 

了解使用者控制項如何處理駐點可說是一件重要的工作。有許多事件跟駐點有著密切的關聯:

要了解執行階段發生了什麼事情,最簡單的方法就是當使用者按下Tab鍵的動作進入組成控制項的時候檢視這些事件的動作。筆者建立了一個簡單的使用者控制項,名叫做MyControl1,控制項中有兩個TextBox組成控制項(分別是Text1及Text2)─然後在所有有關駐點的事件程序中加上了除錯碼。下面是筆者在Immediate視窗中所發現的(註解的部分是筆者加上的):

UserControl_EnterFocus   ' The user has tabbed into the control.
MyControl1_GotFocus
Text1_GotFocus
Text1_Validate           ' The user has pressed the Tab key a second time.
Text1_LostFocus
Text2_GotFocus
MyControl1_Validate      ' The user has pressed the Tab key a third time.
Text2_LostFocus
UserControl_ExitFocus
MyControl1_LostFocus
...                      ' The user has pressed Tab several times
UserControl_EnterFocus   ' until the focus reenters the UserControl
MyControl1_GotFocus      ' and the sequence is repeated.
Text1_GotFocus

如您所見到的,使用者控制項第一個引發的事件為EnterFocus,且是在ActiveX控制項的GotFocus事件之前。同樣地,在ActiveX控制項在表單中引發LostFocus事件之前,使用者控制項會搶先接收到一個ExitFocus事件。

當駐點組成控制項移動到另一個組成控制項時,失去駐點的控制項會引發Validate事件,但當駐點離開使用者控制項時,並不會引發此事件。如果要強迫最後一個離開駐點的控制項引發Validate事件的話,必須在使用者控制項的ExitFocus事件中呼叫ValidateControls方法。如果ActiveX控制項包含了數個控制項時,使用Validate事件來確認最後離開駐點的控制項有時可能會無效。此外,如果使 用ValidateControls方法,也許會在表單關閉的時候錯誤地強迫組成控制項回報狀態(比方說,當使用者按下 Cancel 時)。基於這些理由,建議您使用向父表單的要求回報,或者是更聰明的方法,使用在父表單中的ActiveX控制項的Validate事件。如果控制項比較複雜,也許可以使用下面的方式,提供一個確認控制項的方法來減輕程式設計師確認控制項的負擔,如下面程式碼所示:

Private Sub MyControl1_Validate(Cancel As Boolean)
    If MyControl1.CheckSubFields = False Then Cancel = True
End Sub

小秘訣

Visual Basic的文件中遺漏了ActiveX控制項中多個組成控制項駐點的處理方法。如果ActiveX控制項是表單中唯一可接收駐點的控制項,當使用者在最後一個組成控制項上按下Tab鍵時,駐點並不會如使用者所想的移到第一個組成控制項上。所以如果要讓這個控制項正常的話,最少要在表單上加入另外一個控制項。如果不想在表單中放其他的控制項的話,可透過下面的技巧:建立一個CommandButton(或者其他可以得到駐點的控制項),然後給予Top或Left屬性極大的負值,讓此控制項無法出現在可視區域裡,然後將下面的程式碼加在其LostFocus事件中:

Private Sub Command1_GotFocus()
    MyControl1.SetFocus   ' Manually move the focus
                          ' to the ActiveX control.
End Sub

看不見的控制項
 

InvisibleAtRuntime屬性允許建立只在設計階段看得見的控制項,如同Timer和CommonDialog控制項。當InvisibleAtRuntime屬性設為True時,Extener物件不會包含Visible屬性。通常希望此控制項在設計階段時有固定的大小,且在使用者控制項的Resize事件中使用Size方法來保證這個結果:

Private Sub UserControl_Resize()
    Static Active As Boolean
    If Not Active Then Exit Sub        ' Avoid nested calls.
    Active = True
    Size 400, 400
    Active = False
End Sub

熱鍵
 

如果您的ActiveX控制項包含一個以上支援Caption屬性的控制項,那麼就可以使用「&」字元來為該控制項設定熱鍵,如同在一般的表單做的動作一樣,即使控制項未得到駐點,熱鍵可讓您透過按鍵來迅速處理該控制項。記住要避免一個不好的習慣──為一個ActiveX控制項提供一個固定的標題。原因有兩個,一是這樣的控制項無法被區域化,另外則是這樣的標題有可能會與父表單中的某些控制項的熱鍵衝突到。

如果您的ActiveX並無包含任何具有Caption屬性的組成控制項,則可透過AccessKeys屬性來指定熱鍵。例如,也許要建立一個有Caption屬性的使用者自繪控制項,而想指定Alt+char為它的熱鍵(char為Caption的第一個字元)。在這種情況裡,必須在Property Let程序中指定AccessKeys的屬性值,如下所示:

Property Let Caption(New_Caption As String)
    m_Caption = New_Caption
    PropertyChanged "Caption"
    AccessKeys = Left$(New_Caption, 1)
End Property

當使用者按下某個熱鍵時,使用者控制項的AccessKeyPressed事件會被引發,事件接收到了熱鍵的字元碼。透過此字元碼,可以將一包含兩個以上字元的字串指定給AccessKeys屬性,讓您的ActiveX控制項擁有多個熱鍵功能。

Private Sub UserControl_AccessKeyPress(KeyAscii As Integer)
    ' User pressed the Alt + Chr$(KeyAscii) hot key.
End Sub

可以利用ForwardFocus屬性來模擬Label控制項,當控制項得到駐點時,它會自動地將駐點移動到TabIndex順序為下一個的控制項。但是如果ForwardFocus屬性設定為True,則使用者控制項便不會接受到AccessKeyPress事件了。

存取父表單上的控制項
 

ActiveX控制項要存取到父表單上的控制項有兩種不同的方式。第一種方式根基於父表單的控制項集合物件。如下面的範例所示:

' Enlarge or shrink all controls on the parent form except this one.
Sub ZoomControls(factor As Single)
    Dim ctrl As Object
    For Each ctrl In Parent.Controls
        If Not (ctrl Is Extender) Then
            ctrl.Width = ctrl.Width * factor
            ctrl.Height = ctrl.Height * factor
        End if
    Next
End Sub

因為位在Parent.Controls裡的項目都是Extender物件,所以如果想要挑出執行程式碼的控制項時,必須比較過每個項目的Extender屬性。這種方式的缺點是,只能在VB的環境下處理父表單上的物件(更明確的說法是,只有在Parent物件包含Controls屬性的環境下時)。

第二種方法是利用ParentControls屬性。不像Parent.Controls,ParentControls保證能在任何收納器中執行。在Parent.Controls集合包含了父表單本身,但是可使用Parent物件(如果有的話)來過濾它。

轉換計量單位
 

在與收納器互動時,常常需要將使用者控制項的座標系統轉換成父表單的座標系統,尤其是在滑鼠事件。這時就要利用ScaleX和ScaleY這兩個方法了。可以使用Parent.ScaleMode屬性來取得Visual Basic父表單的ScaleMode,但是如果此控制項是在別的收納器裡執行─例如Internet Explorer,則這個方法便會失敗。很幸運地,ScaleX和SacleY也支援vbContainerPosition常數:

' Forward the MouseDown event to the container, but convert measure units.
Private Sub UserControl_MouseDown(Button As Integer, Shift As Integer, _
    X As Single, Y As Single)
    RaiseEvent MouseDown(Button, Shift, _
        ScaleX(X, vbTwips, vbContainerPosition), _
        ScaleY(Y, vbTwips, vbContainerPosition))
End Sub

當在組成控制項中啟動滑鼠事件時,事情可能會變的較為複雜,因為需要保留控制項從左上角到控制項位置的偏移量:

Private Sub Private Sub Text1_MouseDown(Button As Integer, _
    Shift As Integer, X As Single, Y As Single)
    RaiseEvent MouseDown(Button, Shift, _
        ScaleX(Text1.Left + X, vbTwips, vbContainerPosition), _
        ScaleY(Text1.Top + Y, vbTwips, vbContainerPosition))
End Sub

ScaleX和ScaleY兩個方法支援列舉參數vbContainerSize。當再轉換控制項大小時,應該會使用到它(相對於座標值)。VbContainerPosition及vbContainerSize常數在使用時所得到的結果會依據收納器的ScaleMode不同而改變。ActiveX控制項介面精靈不會提供有關這方面的支援,所以必須手動修改產生出來的程式碼。

其他屬性
 

如果控制項的Alignable屬性設定為True時,ActiveX控制項─更精確的說,它的Extender物件─會顯露出Align屬性。同樣地,應該設定DefaultCancel為True,如果要您的控制項顯露出Default和Cancel屬性的話。當ActiveX控制項的類型像是標準的CommandButton且只有在ForwardFocus設定為False時才可運作時,這些設定則是必須的。如果ActiveX控制項的Default屬性為True而使用者按下了Enter鍵時,則Default設定為True的組成控制項會接收到Click事件。如果並沒有任何組成控制項支援Default或Cancel屬性時,則可在AccessKeyPress事件中擷取Enter或Escape鍵。

如果控制項的CanGetFocus屬性設定為False,則控制項本身便無法得到任何輸入駐點,且ActiveX控制項便不會顯露出TabStop屬性。如果有一個或多個組成控制項可以得到駐點的話,便不能將此值設定為False﹔反之亦然,當CanGetFocus屬性設定為True時,則不能放置任何可得到駐點的控制項。

EventFrozen屬性為一個執行階段的屬性,其功用在於當父表單忽略掉使用者控制項所引起的事件時,則此屬性值會傳回True。例如,當在執行階段時,可透過它來得知是否您的RaiseEvent指令會被忽略掉,所以可以決定延遲它們。不幸的,沒有安全的方法去得知收納器何時要接受事件,但是可以從AmbientChanged事件的UIDead屬性中得知暫停的程式是否又開始運作了。

可以利用EditAtDesignTime屬性來建立一個可以在設計階段編輯的控制項。可在設計階段時在控制項上按下右鍵來選擇Edit指令進入編輯模式。當控制項處於編輯模式時,其會有如執行階段時般的運作,儘管它沒有在其收納器中引發任何事件(EventsFrozen屬性傳回True)。可點選表單在控制項以外的地方來離開編輯模式。一般來說,要建立一個可以在設計階段編輯的控制項並不是一項簡單的工作。舉例來說,必須處理所有在設計階段無法使用的屬性,且在Ambient.UserMode傳回False時產生一錯誤訊息。

ToolboxBitmap屬性指定在工具項視窗裡控制項的圖示。可以使用16X15 pixels的點陣圖,但是不同大小的圖會自動地被縮放。不應該使用icon,原因是因為icon縮放後的效果不太好。

ContainerHwnd屬性只允許在傳回收納器視窗的handle時才會運作。如果控制項處於Visual Basic程式中,此屬性值則與Extender.Container.hWnd屬性的傳回值有關聯。

使用者控制項物件還顯露出了其他少數屬性,讓您建立非視窗類型的控制項,收納器控制項,透明控制項。筆者會在本章後面再提到它們。

改善您的ActiveX控制項
 

加入一個使用者控制項物件到目前的專案中,再置放一些組成控制項,對建立一個具有完整羽翼,商業價值的ActiveX控制項來說,只是第一步而已。在這一節裡,筆者要告訴您如何建立一個健全的使用者介面,加入連結功能與屬性頁,建立使用者自繪控制項,和準備將控制項放到網際網路上去。

屬性
 

您已經看過如何利用一對屬性副程式來為您的控制項加入自訂屬性了。這一節中會解釋一些特別型態的屬性如何被應用。

設計階段與執行階段屬性
 

並非所有的屬性皆可以在設計階段與執行階段運作,很有趣的是,它們的可視性限制在您使用者控制項中程式碼的撰寫。要建立一個只在執行階段運作的屬性,像是TextBox的SelText或ListBox的ListIndex,最簡單的方法就是勾選程序屬性設定對話盒裡的 不要顯示在瀏覽屬性視窗 屬性(可以在工具選單中找到這個對話盒)。如果這個選項被打勾的話,那此屬性便不會在屬性頁視窗中被看到了。

然而這個簡單的方法有個問題,就是它同樣也會把Visual Basic所提供的區域變數 視窗裡的屬性給隱藏起來。如果要讓屬性列在執行階段在區域變數視窗中列出而不是在屬性視窗的話,必須在設計階段時在Property Get副程式中引發一個錯誤訊息,如下面所展示的:

Public Property Get SelText() As String
    If Ambient.UserMode = False Then Err.Raise 387
    SelText = Text1.SelText
End Property

錯誤387「設定不允許」是在這個情況下會引發的錯誤,但是任何錯誤皆可利用這個技巧來引發。如果Visual Basic─或者是說,在開發環境中─當控制項在設計階段接收到一個有關於讀取屬性值錯的訊息時,屬性頁視窗中的屬性並不會被顯示出來,這正是我們所想要的。同樣地,要建立一個只有在執行階段唯讀的屬性而再執行階段無法運作的屬性也是類似地情況,因為只需要忽略不撰寫Property Let副程式,可以利用這種方式建立任何的唯讀屬性。藉此Visual Basic不會將這些屬性顯示在 Properties 視窗中,因為這些屬性皆無法被修改。

另外關於屬性的存取情形則是在設計階段可讀取而在執行階段為唯讀的屬性。這與TextBox控制項的MultiLine和ScrollBar屬性很類似。可在Property Let 副程式中應用引發錯誤382「不支援執行時的設定」來達到這樣的效果,如下面的程式碼所示:

' This property is available at design time and read-only at run time.
Public Property Get ScrollBars() As Integer
    ScrollBars = m_ScrollBars 
End Property
Public Property Let ScrollBars(ByVal New_ScrollBars As Integer)
    If Ambient.UserMode Then Err.Raise 382
    m_ScrollBars = New_ScrollBars
    PropertyChanged "ScrollBars"
End Property

如果有在設計階段可視而執行階段唯讀的屬性,便無法在ReadProperties事件中呼叫Property Let副程式,因為可能會得到錯誤訊息。在這種情形,必須透過存取私有成員變數或組成控制項的屬性,或者必須提供一個模組層次的布林變數,讓您的控制項在進入ReadProperties事件設定為True,而離開時設定為False﹔可在Property Let副程式中引發錯誤之前檢查此變數。也可以使用相同的變數來略過沒必要的PropertyChanged方法,如下面的範例:

Public Property Let ScrollBars(ByVal New_ScrollBars As Integer)
    ' The ReadingProperties variable is True if this routine is being
    ' called from within the ReadProperties event procedure.
    If Ambient.UserMode And Not ReadingProperties Then Err.Raise 382
    m_ScrollBars = New_ScrollBars
    If Not ReadingProperties Then PropertyChanged "ScrollBars"
End Property

列舉屬性
 

透過Enum區塊或Visual Basic自己的列舉型別,可以自行定義列舉屬性。舉例來說,可以更改精靈所產生的程式碼,改善MousePointer屬性:

Public Property Get MousePointer() As MousePointerConstants
    MousePointer = Text1.MousePointer
End Property
Public Property Let MousePointer(ByVal New_MousePointer _
    As MousePointerConstants)
    Text1.MousePointer() = New_MousePointer
    PropertyChanged "MousePointer"
End Property

列舉型屬性對開發人員來說可增加效率,因為可在屬性視窗裡的下拉式選單中看到它們的值,如圖17-6所示。然而要記住的是,應該保護您的ActiveX控制項被輸入錯誤的參數值,所以上面的範例應該要改成下面的樣子:

Public Property Let MousePointer(ByVal New_MousePointer _
    As MousePointerConstants)
    Select Case New_MousePointer
        Case vbDefault To vbSizeAll, vbCustom
            Text1.MousePointer() = New_MousePointer
            PropertyChanged "MousePointer"
        Case Else
            Err.Raise 380   ' Invalid Property Value error
    End Select
End Property


 

圖17-6 使用列舉屬性來建立屬性視窗的下拉式清單。

然而有個理由來建議您不要使用Visual Basic和VBA的列舉常數:如果在非Visual Basic的環境裡使用此控制項的話,使用者的程式裡便看不到這些常數值了。


小秘訣

您可能想要讓屬性視窗的列舉項目更具可讀性。例如說,FillStyle屬性包含了像是Horizontal Line或Diagonal Cross。要讓您的ActiveX控制項顯露出這樣的值時,必須在Enum區塊中利用方括弧將您的參數值刮起來,如下面例子所示:

Enum MyColors
    Black = 1
    [Dark Gray]
    [Light Gray]
    White 
End Enum


小秘訣

這裡還有另外一個您可能覺得有用的點子:如果使用一個名稱為底線開頭的列舉常數,像是 [_HiddenValue] ,則此值不會在物件瀏覽器中出現。然而,此值卻會出現在屬性視窗中,所以這個技巧對想要讓屬性不在設計階段的人特別有用。


Picture和Font屬性
 

Visual Basic利用特別的方法讓屬性傳回一個Picture或Font物件。在前者的例子裡,屬性視窗顯示一個按鍵讓您選擇硬碟中的圖檔,後者則在屬性視窗裡包含一個按鍵顯示字型的一般對話盒。

當使用Font屬性的時候,要記住此屬性所傳回的是個物件的參照。比方說,如果有個或多個組成控制項被指定同樣的字型參照時,當改變了其中一個字型,同樣也會改變所有其他控制項的字型。基於這個理由,Ambient.Font所傳回的物件為副表單字型物件的複本,所以即使父表單上的字型有所改變,並不會影響到使用者的組成控制項的字型,反之亦然(如果要讓您控制項的字型與父表單同步的話,只要追蹤AmbientChanged事件即可)。當您將這些物件參照與其他控制項分享時,也可能不知不覺會引起一些無法察覺的錯誤。看看下面的例子:

' Case 1: Label1 and Text1 use fonts with identical attributes.
Set Label1.Font = Ambient.Font
Set Text1.Font = Ambient.Font

' Case 2: Label1 and Text1 point to the *same* font.
Set Label1.Font = Ambient.Font
Set Text1.Font = Label1.Font

這兩段程式碼看起十分相似,但前者的兩個組成控制項被指定的字型物件為不同的複本,所以即使改變某個物件的字型,也不會對另外一個產生影響。但後者兩個控制項皆指向同樣的字型物件,所以每當改變了其中一個控制項的字型時,連帶地另外一個控制項也會跟著改變。

習慣上,Visual Basic也提供了其他的字型存取屬性,早期的Fontxxxx屬性,FontName、FontSize、FontBold、FontItalic、FontUnderline及FontStrikethru。但應該設定讓這些屬性在設計階段時無法運作,且如果使用了Font物件的話,您就不應該在WriteProperties事件中使用它們。如果決定了要使用個別的Fontxxxx屬性時,提醒您一件很重要的是,按照正確的順序來讀取它們(FontName優先,然後才是其他的屬性)。

當處理字型屬性的時候,有件事要牢記在心:如果Font屬性顯露在屬性視窗時,不可以限制程式設計師使用哪一類的字型。唯一要限制使用者選擇的字型的時機為當我們要顯示屬性視窗的一般字型對話盒時。可以參考稍後〈屬性頁〉 這一節所介紹的如何建立屬性頁。

Font屬性的使用給了ActiveX控制項設計師一個特別的挑戰。如果您的控制項顯露了Font屬性且使用者的程式更動了Font的一或多個屬性時,Visual Basic會呼叫Property Get Font副程式,但不會呼叫Property Set Font。如果Font屬性關聯到某個單一個組成控制項時,則這就不會有上述的問題,因為控制項的字型是同步更新的。在使用者自繪控制項中則就不太一樣了,因為在這個情況中控制項不會得到任何它應該重繪的告知,這個問題在Visual Basic 6裡可透過StdFont物件的FointChanged事件以得到解決。下面是一個從類似Label的使者自繪控制項中擷取的程式碼片段,裡面介紹了當使用者程式更動了Font某個屬性時,控制項如何正確地重新繪製自己的方式。

Private WithEvents UCFont As StdFont
Private Sub UserControl_InitProperties()
    ' Initialize the Font property (and the UCFont object).
    Set Font = Ambient.Font
End Sub
Public Property Get Font() As Font
    Set Font = UserControl.Font
End Property
Public Property Set Font(ByVal New_Font As Font)
    Set UserControl.Font = New_Font
    Set UCFont = New_Font         ' Prepare to trap events.
    PropertyChanged "Font"
    Refresh                       ' Manually perform the first refresh.
End Property

' This event fires when the client code changes a font's attribute.
Private Sub UCFont_FontChanged(ByVal PropertyName As String)
    Refresh                       ' This causes a Paint event.
End Sub
' Repaint the control.
Private Sub UserControl_Paint()
    Cls
    Print Caption;
End Sub

物件屬性
 

可建立具有可以傳回物件的屬性的ActiveX控制項,例如TreeView這類會傳回節點集合的控制項。我們可以這樣做的原因是因為ActiveX控制項專案可以包含PublicNotCreatable類別,所以您的控制項可以透過一唯讀屬性自內部使用New運算元自行建立和回傳物件。在大部分的情況下物件屬性可以被視為是一般標準的屬性,但是當產生這些物件且希望讓它們持續存在或在WriteProperties及ReadProperties載入它們時,就要特別注意這些屬性的行為了。

即使Visual Basic 6支援persistable類別,但還是不能儲存非creatable的物件。然而沒有東西可以阻止您手動地建立PropertyBag物件然後載入所有相關物件。讓筆者用一個例子來展示這樣的技術。

假設有個專門紀錄使用者輸入的個人姓名及住址資料的ActiveX控制項AddressOCX,如 圖17-7 所示。此控制項並無多個屬性,而是顯露出一個在控制項中定義的物件屬性,稱為Address。不同於一般的使用者控制項在模組中儲存及載入個別的屬性,應該在PublicNotCreatable類別中建立一個Friend屬性。筆者通常稱呼這樣的屬性為AllProperties,因為它是利用一個位元陣列中處理所有的設定與回傳動作。為了要讓屬性連續地存入陣列中,筆者使用了一個私有獨立的PropertyBag物件。下面是Address類別模組完整的程式碼(為了要讓範例簡化,筆者將所有的屬性皆宣告為公用變數)。

' The Address.cls class module
Public Name As String, Street As String
Public City As String, Zip As String, State As String

Friend Property Get AllProperties() As Byte()
    Dim PropBag As New PropertyBag
    PropBag.WriteProperty "Name", Name, ""
    PropBag.WriteProperty "Street", Street, ""
    PropBag.WriteProperty "City", City, ""
    PropBag.WriteProperty "Zip", Zip, ""
    PropBag.WriteProperty "State", State, ""
    AllProperties = PropBag.Contents
End Property
Friend Property Let AllProperties(value() As Byte)
    Dim PropBag As New PropertyBag
    PropBag.Contents = value()
    Name = PropBag.ReadProperty("Name", "")
    Street = PropBag.ReadProperty("Street", "")
    City = PropBag.ReadProperty("City", "")
    Zip = PropBag.ReadProperty("Zip", "")
    State = PropBag.ReadProperty("State", "")
End Property

這時您應該是簡單地使用Address物件的AllProperties屬性來存取地址等資料而不是個別地使用WriteProperties和ReadProperties事件程序來存取這些資料了。


 

圖17-7 AddressOCX ActiveX控制項。其將每一個Address屬性顯露成個別的屬性,PublicNotCreatable物件。
' The AddressOCX code module (partial listing)
Dim m_Address As New Address

Public Property Get Address() As Address
    Set Address = m_Address
End Property
Public Property Set Address(ByVal New_Address As Address)
    Set m_Address = New_Address
    PropertyChanged "Address"
End Property

Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
    m_Address.AllProperties = PropBag.ReadProperty("Address")
End Sub
Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
    Call PropBag.WriteProperty("Address", m_Address.AllProperties)
End Sub

每個個別地組成控制項必須參照其在Address物件裡相關的屬性。例如,下面是txtName控制項在Change事件中的程式片斷:

Private Sub txtName_Change()
    Address.Name = txtName
    PropertyChanged "Address"
End Sub

這個ActiveX控制項也應該要顯露出Refresh方法來重新讀取所有的Address物件的所有欄位值。另外您也許要做一個當有屬性值被更動時,便會引發的事件。這個問題與筆者在第九章裡的 〈以表單作為物件顯示器〉 所提及的很類似。

傳回使用者自訂型態的屬性
 

ActiveX控制項還可以顯露出可以傳回使用者自訂型態(UDT)的屬性及方法或者把使用者自訂型態做為參數使用。因為ActiveX控制項為行程內COM元件,所以可以隨意安排UDT而不用理會作業系統的版本。若想要得到更多有關這方面的細節,可以參考十六章的 〈在應用程式間傳遞資料〉 一節。

然而,目前不能在With區塊中傳回UDT,否則會導致Visual Basic環境當機。此問題尚未完整地被消除,希望這個臭蟲能在下一版的修正中被修正。

特別的OLE資料型態
 

ActiveX控制項的屬性也可以傳回少數特別的資料型態。例如,由精靈所宣告的顏色屬性就是使用OLE_COLOR型態,如下所示:

Public Property Get BackColor() As OLE_COLOR
    BackColor = Text1.BackColor
End Property

當一個屬性被宣告成為一OLE_COLOR型態值時,程式設計師便能夠從屬性視窗中的調色盤點選它的值,藉由此方法來點選ForeColor及BackColor的屬性值。此外,OLE_COLOR屬性在Visual Basic內部則為Long型態。

Visual Basic另外還支援了其他三種特別的資料型態:

副程式編號
 

少數的ActivieX控制項屬性有著特殊的意義。您可在副程式屬性對話盒(Procedure Attributes dialog box)的進階部分(Advanced section)指定特別的副程式編號來定義這樣的屬性。

如同第六章〈屬性〉一節裡所說的,可從Procedure ID欄位裡選擇出(預設的)選項或者直接輸入0來建立類別預設的屬性或方法。必須讓OLE_OPTEXCLUSIVE屬性成為預設屬性,這樣您的ActiveX控制項才會像OptionButton控制項一樣地運作。

如果您的控制項有著像Text或Caption這樣的屬性時,應要個別指定Text或Caption屬性Procedure ID。這些設定會讓這些屬性的行為在Visual Basic下運作正常:當屬性在程序視窗中被更新時,控制項的屬性也隨即被更新。而在背後實際運作方面,程序視窗會在使用者每敲一次鍵盤時,便會呼叫一次Property Let副程式,取代當使用者敲下Enter鍵才呼叫。可在任何的屬性中使用這些procedure ID來取代屬性的名子。不過屬性不可以使用超過一個以上的procedure ID。


小秘訣

因為在procedure ID欄位裡只能選擇一個項目,所以似乎是不可能複製Visual Basic的TextBox及Label控制項這種可立即隨屬性視窗更動屬性值,以及同時身為預設屬性的行為。此時定義一個隱藏的屬性,讓其成為預設屬性,然後將其與Text或Caption屬性關聯起來,藉以解決上面問題的要求。

' Make this property the default property, and hide it.
Public Property Get Text_() As String
    Text_ = Text
End Property
Public Property Let Text_(ByVal newValue As String)
    Text = newValue
End Property

應要指定Enabled屬性編號給予您的ActiveX控制項的Enabled屬性讓其工作正常。這是一個必要的步驟,因為Enabled屬性的行為與其他的屬性不相同。當取消一個表單時,表單會將它所有控制項的Extender的Enabled屬性設為False(因此該控制項顯示成Disabled的狀態),但是卻不會將控制項內部的Enabled屬性設為False(所以當它們為Enabled時,會自動重繪自己)。為了要讓Visual Basic建立一個Extender的Enabled屬性,您的使用者控制項必須包含一個共用的Enabled屬性並且此Enabled屬性擁有一個Enabled procedure ID:

Public Property Get Enabled() As Boolean
    Enabled = Text1.Enabled
End Property
Public Property Let Enabled(ByVal New_Enabled As Boolean)
    Text1.Enabled() = New_Enabled
    PropertyChanged "Enabled"
End Property

ActiveX控制項介面精靈繪正確地建立這些相關的程式碼,但是必須手動地指定Enabled副程式編號。

最後,可建立一個關於對話方塊來顯示版權宣告資訊。透過下面的程式碼可以輕易地做到:

Sub ShowAboutBox()
    MsgBox "The SuperTextBox control" & vbCr _
        & "(C) 1999 Francesco Balena", vbInformation
End Sub

當ActiveX有了這樣的一個procedure ID時,在屬性視窗中會出現一個(關於)項目。一般我們習慣將此項目隱藏起來,這樣程式設計師才不會沒事在程式中呼叫它。

副程式屬性對話盒
 

在屬性對話盒中加入些許的欄位會讓您的ActiveX控制項使用起來更為親切。並不是所有的這些設定都會影響控制項的功能。

筆者在本章的前面部分已經提過,千萬不要在設計階段以及執行階段顯示屬性瀏覽器。當勾選盒被選取時,此屬性就不會在設計階段中被顯示在屬性視窗中了。

 在屬性瀏覽器中使用此頁 下拉式選單讓您將此屬性關聯到某一個Visual Basic提供的屬性頁(比方說StandardColor、StandardDataFormat、StandardFont及StandardPicture)或ActiveX控制項專案所定義的屬性頁。當一個屬性被關聯到了一個屬性頁時,它便會以一個按鈕的方式出現在屬性視窗中。當我們按下該按鈕,便會帶出該屬性頁。我們將會在本章後面繼續討論屬性頁。

使用屬性分類欄位來選擇屬性在屬性視窗中被歸類的頁籤。Visual Basic提供數種種類供我們選擇─Appearance、Behavior、Data、DDE、Font、List、Misc、Position、Scale及Text─也可以自己建立一個新的種類,只要在下拉式選單中鍵入新種類的名稱即可。

「使用者介面預設值」依據其針對的是屬性或者是事件,可有許多不同的意思。當屬性標記著此屬性時,則意味著該屬性是在建立了控制項後才會出現在屬性視窗中讓您選取的。若是事件標記著「使用者介面預設值」時,則表示該屬性的樣版是當雙擊Active控制項時,由Visual Basic的程式碼視窗建立的。

限制與使用範圍
 

當我們利用簡單的組成控制項建立ActiveX控制項是一個有效的方法,但是它還是有它的限制。其中一項困擾我的問題是,沒有其他簡單的方法可以建立詳述TextBox或ListBox控制項,以及可以正確的顯露出所有它們原本屬性的控制項。像這樣的控制項有少許的屬性在執行階段為唯讀的,例如MultiLine、ScrollBars及Sorted。但在設計階段將一個ActiveX控制項放置在表單上時,此ActiveX控制項已經在執行了,所以不能更改那些特殊的屬性。

可以使用些許技巧來解決這個問題,但沒有一個技巧可以徹底解決這個問題。例如有時候可以使用程式碼來模擬這些屬性,像是模擬ListBox的Sorted屬性。另外一個廣為人知的技巧則是利用組成控制項陣列。舉例說明,可以應用MultiLine屬性來作為單行編輯及多行編輯兩者之間的判別,另外只讓符合條件的屬性出現在屬性視窗中。使用此方式而衍伸出的問題是,每當需要使用二或更多個屬性時會有大量需要的控制項以指數數量增加。需要五個TextBox控制項來應用MultiLine及ScrollBars屬性(一個為單行編輯TextBox,另外四個為ScrollBars的所有可能設定),而如果要應用HideSelection屬性時,則是需要10個TextBox控制項。

第三個可能的解決方法為利用相似的簡單控制項來模擬另外的控制項。舉例來說,可使用PictureBox及VscrollBar來製作一個像似ListBox的ActiveX控制項。因為使用了PictureBox的方法來模擬ListBox,所以可自由地改變它的圖形樣式,或加入一個水平捲軸諸如此類。但不用說,這通常不是簡單的方法。

關於第四個方法,筆者想要儘量少提示大家使用,不用懷疑地,這是最複雜的方式。有別使用Visual Basic的控制項,第四個方法使用CreateWindowEx API函數。這是C語言的方式,在Visual Basic的環境中使用時也許會更比在C裡使用時更為複雜,因為Visual Basic語言中並不提供像是指標這種較低階的語言才有的語法能力。

在聽完了所有這些抱怨後,您會很高興在Visual Basic 6中這些問題都獲得了解決。事實上,新的Windowless控制項函式庫中(請參閱 第九章 )並不會在執行階段中顯露出唯讀的屬性,這個方式唯一的缺點是,該函式庫控制項並不顯露出hWnd屬性,所以不能夠呼叫API來增強它們的功能,筆者在附錄會提到。

收納器(Container)控制項
 

可建立像是PictureBox及Frame控制項這些類似收納器的控制項。要建立一個收納器控制項,所需要做的只是將使用者控制項的ControlContainer屬性設為True。然而請記住,並不是所有的環境都支援這個屬性。如果其收納器並不支援IsimpleFrame介面時,您的ActiveX控制項便無法收納其他的控制項。Visual Basic的表單支援這個介面,如同PictureBox及Frame一般。換句話說,可以設計任何ActiveX收納器控制項而運作起來不會有任何問題。

可在設計階段時(從工具箱拖放)或執行階段(透過Container屬性)在收納器控制項上放置控制項。以上兩種情況,ActiveX控制項都可以透過查詢它自己的ContainedControls屬性來得知哪個控制項被放置在它表面。這個屬性會傳回一個參考到被收納的控制項Extender介面的集合。

在隨書光碟中,可找到一個簡單的收納器ActiveX控制項,名為Stretcher。Stretcher會在它縮放時自動地調整其所收納的控制項的大小。這個控制項的程式碼則是令人無法相信的簡單:

' These properties hold the previous size of the control.
Private oldScaleWidth As Single
Private oldScaleHeight As Single

' To initialize the variables, you need to trap both these events.
Private Sub UserControl_InitProperties()
    oldScaleWidth = ScaleWidth
    oldScaleHeight = ScaleHeight
End Sub

Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
    oldScaleWidth = ScaleWidth
    oldScaleHeight = ScaleHeight
End Sub
Private Sub UserControl_Resize()
    ' When the UserControl resizes, move and resize all container controls.
    Dim xFactor As Single, yFactor As Single
    ' Exit if this is the first resize.
    If oldScaleWidth = 0 Then Exit Sub
    ' This accounts for controls that can't be resized.
    On Error Resume Next
    ' Determine the zoom or factor along both axis.
    xFactor = ScaleWidth / oldScaleWidth
    yFactor = ScaleHeight / oldScaleHeight
    oldScaleWidth = ScaleWidth
    oldScaleHeight = ScaleHeight
    
    ' Resize all controls accordingly.
    Dim ctrl As Object
    For Each ctrl In ContainedControls
        ctrl.Move ctrl.Left * xFactor, ctrl.Top * yFactor, _
            ctrl.Width * xFactor, ctrl.Height * yFactor
    Next
End Sub

ContainedControls集合只包含了被使用者控制項收納的控制項。例如,如果ActiveX控制項收納了一個PictureBox,但是PictureBox收納了一個TextBox,則PictureBox會出現在ContainedControls集合中,但是TextBox則不會。參考圖17-8,這表示前面的程式碼將Stretcher ActiveX控制項裡的Frame1控制項給放大或縮小了,但在裡面的OptionButton兩個控制項並沒有被影響。為了要resize的程式碼可以正常地工作,需要更改UserControl_Resize事件副程式中的程式碼(新加入的程式碼以粗體表示):

Dim ctrl As Object, ctrl2 As Object
    For Each ctrl In ContainedControls
        ctrl.Move ctrl.Left * xFactor, ctrl.Top * yFactor, _
            ctrl.Width * xFactor, ctrl.Height * yFactor
        For Each ctrl2 In Parent.Controls
            ' Look for controls on the form that are contained in Ctrl.
            If ctrl2.Container Is ctrl Then
                ctrl2.Move ctrl2.Left * xFactor, ctrl2.Top * yFactor,_
                    ctrl2.Width * xFactor, ctrl2.Height * yFactor
            End If
        Next
    Next


 

圖17-8 Stretcher ActiveX控制項在設計階段以及執行階段時調整所有它所收納的控制項的大小。

您應該要知道一些關於Visual Basic收納器ActiveX控制項的寫作觀念:

另外一個與收納器控制項伴隨而來的問題是,當在設計階段時新增或移除控制項時,使用者控制項模組並不會接收到任何事件。如果需要得知這些動作的話,比如要自動地調整被收納的控制項大小,必須使用一個Timer控制項週期性地查詢ContainedControls集合。當這個方法並無法生效時,也許只能在設計階段時使用Timer控制項,也因此會體會出在執行階段時並沒有任何的效果。

透明(Transparent)控制項
 

Visual Basic提供許多方法建立形狀不規則的控制項。一開始,如果將使用者控制項的BackStyle屬性設為0-Trasparent,則控制項的背景─也就是說,控制項部份未被組成控制項覆蓋的背景色─會變成透明的。當控制項擁有透明的背景色時,所有滑鼠的事件會直接地由收納器表單或ActiveX控制項下方的收納器接收。除此之外,Visual Basic會對這樣的ActiveX控制項忽略BackColor及Picture屬性,且所有圖形方法的輸出皆會變成不可視。毫無驚訝地,透明控制項也較更為要求CPU時間,因為當重繪時,Visual Basic必須將不屬於該控制項的部分修剪掉。

使用Label及Shape控制項
 

如果您的透明控制項包含了一或多個使用TrueType字型的Label控制項且它的BackStyle屬性也被設為0-Transparent,Visual Basic會修剪掉Label的字元周圍的區域。只有Label的標題被視為ActiveX控制項的一部份,且所有位於Label的其他圖素(pixel)為透明的。舉例來說,如果單擊標題裡的一個O字元,則父表單或者該ActiveX控制項裡的Click事件便被引發。然而,筆者注意到了該細節只會發生於較大的字型。

可使用Shape控制項作為組成控制項來建立一個非矩形的控制項。(可參考隨書光碟中的範例。)如果將Shape控制項的BackStyle屬性設為0-Transparent,則所有位於Shape控制項外的圖素便會變成透明的。舉例來說,在表單上放置一個Shape1組成控制項,然後將其Shape屬性設為2-Oval,在將收納表單及Shape控制項本身的BackStyle屬性設為0-Transparent。然後只需一些程式碼來控制表單縮放時Shape控制項的大小,以及當Value屬性更動時更新控制項的外觀。下面列出該使用者控制項部分的程式碼:

' Change the color when the control is clicked.
Private Sub UserControl_Click()
    Value = True
    RaiseEvent Click
End Sub

Private Sub UserControl_Resize()
    Shape1.Move 0, 0, ScaleWidth, ScaleHeight
End Sub
Public Sub Refresh()
    ' TrueColor and FalseColor are Public properties. 
    Shape1.BackColor = IIf(m_Value, TrueColor, FalseColor)
    Shape1.FillColor = Shape1.BackColor
End Sub
' Value is also the default property.
Public Property Get Value() As OLE_OPTEXCLUSIVE
    Value = m_Value
End Property
Public Property Let Value(ByVal New_Value As OLE_OPTEXCLUSIVE)
    m_Value = New_Value
    Refresh
    PropertyChanged "Value"
End Property

使用Shape控制項來建立不規則形狀的控制項伴隨而來的問題是,不能簡單地使用圖形方法在其上繪圖。原因是Visual Basic會在Paint事件後重繪Shape控制項,所以Shape控制項覆蓋了在Paint事件中產生的繪圖部分。有個簡單的方法可以跨越這樣的限制,可在Paint事件中使用Timer控制項,然後讓Timer的Timer事件副程式在標準的Paint事件幾個百萬毫秒後發生。使用下面的程式碼作為指導方針:

Private Sub UserControl_Paint()
    Timer1.Interval = 1        ' One millisecond is enough.
    Timer1.Enabled = True
End Sub
Private Sub Timer1_Timer()
    Timer1.Enabled = False     ' Fire just once.
    ' Draw some lines, just to show that it's possible.
    Dim i As Long
    For i = 0 To ScaleWidth Step 4
        Line (i, 0)-(i, ScaleHeight)
    Next
End Sub

就筆者所知的,這個問題唯一其他的解決方法只有在Paint事件後應用使用者控制項的SubClass來執行一些程式碼。(SubClass技術在〈附錄〉中有提及)

使用MaskPicture及MaskColor屬性
 

如果您的透明控制項形狀太不規則而無法使用一個Shape控制項來做到(或即使使用了一群Shape控制項),下一個最好的選擇是指定一個點陣圖給MaskPicture屬性然後指定一個認定是背景色的顏色給MaskColor屬性。這裡的點陣圖被用來作為遮罩,而點陣圖裡的每一個顏色與MaskColor一樣的圖素則會變成透明顏色。(組成控制項無法成為透明,即使它位於遮罩區之外)且需要將BackStyle屬性設為0-Transparent來讓您的控制項正確運作。

使用這個方式,可建立任何形狀的ActiveX控制項,包含中間有洞的。也許最嚴重的問題只不過是不能輕易地建立可隨著控制項大小調整的點陣圖遮罩,因為可以指定點陣圖、Gif或JPEG圖檔給MaskPicture屬性,但不能指定特殊格式檔案。

輕量(Lightweight)控制項
 

Visual Basic 6允許建立在執行階段消耗較少資源且讀取及釋放時速度較快的ActiveX控制項。使用者控制項物件顯露出兩個新的屬性讓您微調這些能力。

HasDC及Windowless屬性
 

HasDC屬性決定了使用者控制項是否建立一個永久固定的視窗裝置內容(Windows device context)或當控制項重繪時建立一個模板裝置。將使屬性設為False時可以減少記憶體的使用且增進系統效率。若要更多關於此屬性的資訊,請參考第二章的 〈調整表單的效率〉 。

若將Windowless屬性設為True時則會建立一個無視窗介面,因而減少資源使用量的ActiveX控制項。無視窗控制項有兩個使用限制,且它無法以收納器的型態運作。不能放置一般正規的控制項於一個無視窗ActiveX控制項上,也不能在使用者控制項已經包含了非無視窗組成控制項的同時將Windowless屬性設為True。Image、Label、Shape、Line及Timer是唯一可以放置在無視窗使用者控制項的控制項。如果需要的特色這些控制項並無提供,請參閱之前提過的 〈限制與使用範圍〉 一節。

並非所有的收納器都支援無視窗控制項。很有趣的是,當一個無視窗控制項在一個不支援此特色的環境中執行時,此無視窗控制項會自動地變成一般的有視窗控制項。

無視窗控制項並不會顯露出hWnd屬性,所以不能呼叫API函數來擴增它的功能。(在某些情況下,可使用ContainerHwnd屬性來取代hWnd)除此之外, EditAtDesign及BorderStyle屬性對於無視窗控制項來說是無效的。HasDC屬性則通常會被忽略掉,因為無視窗控制項決不會有固定的裝置內容。但是應該將這些屬性設為False,因為如果當控制項在不支援無視窗ActiveX控制項的環境中執行時,至少這樣不會使用到固定裝置內容的資源。

透明輕量控制項
 

可建立透明背景的無視窗控制項,只要將其BackStyle屬性設為0-Transparent和指定一個適當的點陣圖給MaskPicture屬性。但應該也要考慮新的HitTest事件及HiBehavior及ClipBehavior屬性。

在筆者說明如何使用這些新的成員之前,您需要先了解關於一個控制項的四個區域為何。(圖17-9)Mask區域為控制項的非透明區域,其包含了所有的組成控制項及其他所有關於圖形方法的輸出區域。(在一般的控制項中,這是唯一存在的區域)Outside區域則是遮罩區域以外的部分。Transparent則是任何位於Mask區域中而不屬於控制項的部分。(控制項中的洞)最後,Close區域則是環繞著Mask區域的部分,其寬度則視ActiveX控制項作者的決定。


 

圖17-9 背景透明控制項的四個相關區域。

在背景透明控制項處理滑鼠事件會發現的問題是,Visual Basic並不知道任何關於Close及Transparent區域的東西,且它只能決定滑鼠是否位於Mask區域或Outside區域。這個問題在當有多個重疊的控制項的情況下會變得更糟糕,每個控制項有它自己的Close或Transparent區域,因為Visual Basic必須決定哪一個會接收到滑鼠事件。為了讓控制項決定是否它要處理滑鼠的動作,Visual Basic在位於滑鼠指標下的所有的控制項中製造了一個或多個HitTest事件,(也就是,它在最上方的控制項中製造了HitTest事件)HitTest事件接收到了滑鼠的x及y座標軸及HitTest的參數:

Sub UserControl_HitTest(X As Single, Y As Single, HitResult As Integer)
    ' Here you manage the mouse activity for the ActiveX control.
End Sub

HitResult可能的值有0-vbHitResultOutside、1-vbHitResultTransparent、2-vbHitResultClose及3-vbHitResultHit。Visual Basic會依據下面的架構產生HitTest事件:

因為Visual Basic只知道有關於Mask及Outside區域,所以傳遞給HitTest事件的HitResult的值只能是0或3。如果想要警告Visual Basic您的控制項擁有Close或Transparent區域時,必須透過程式碼來完成。在實作上,可以測試x及y座標軸,然後指定適當的值給HitResult,如下面範例所示:

' A control with a circular transparent hole in it.
Sub UserControl_HitTest(X As Single, Y As Single, HitResult As Integer)
    Const HOLE_RADIUS = 200, CLOSEREGION_WIDTH = 10
    Const HOLE_X = 500, HOLE_Y = 400
    Dim distance As Single
    distance = Sqr((X _ HOLE_X) ^ 2 + (Y _ HOLE_Y) ^ 2)
    If distance < HOLE_RADIUS Then
        ' The mouse is over the transparent hole.
        If distance > HOLE_RADIUS _ CLOSEREGION_WIDTH Then
            HitResult = vbHitResultClose
        Else
            HitResult = vbHitResultTransparent
        End If
    Else
        ' Otherwise use the value passed to the event (0 or 3).
    End If
End Sub

毋庸置疑地,所有這些指令皆會增加系統的負擔,且讓應用程式執行更慢。此外,Visual Basic需要修飾mask定義的MaskPicture的輸出部分,包括組成控制項以及圖形方法的輸出部分。為了讓這些負擔減到最小,可以透過ClipBehavior及HitBehavior屬性更改Visual Basic的預設行為。

ClipBehavior屬性會影響Visual Basic如何剪裁圖形方法輸出。此預設值為1-UseRegion,意思是說圖形方法的輸出要剪裁成適合Mask區域的大小。0-Note則不會執行剪裁的動作,而圖形輸出也會在Mask及Transparent區域中呈現出來。

HitBehavior屬性決定了呼叫HitTest事件之前HitResult參數的數值。當HitBehavior=1-UseRegion(預設值)時,Visual Basic會在只當滑鼠位於Mask區域時將HitResult設為3。如果將HitBehavior設為2-UsePaint時,Visual Basic會認為由圖形方法產生的圖素位於Paint事件中。最後,如果HitBehavior設為0-None時,Visual Basic甚至不會看HitResult的值且總是傳遞給HitTest事件值0。

如果Mask區域並不複雜且可簡單地在程式碼中使用它,通常可將HitBehavior設定為0-UseNote來增加您的ActiveX控制項的效能。在這個情況,Visual Basic總是會傳遞給HitTest事件HitResult=0的參數值,而可改變它來計算您的Mask、Close及Transparent區域。如果Mask區域是很複雜的且包含了不規則的圖形,應該將ClipBehavior設定為0-Note,因而降低了Visual Basic區別Mask及Outside區域的負擔。

資料連結
 

只要稍微點選一下滑鼠就可為您的ActiveX控制項加入資料連結的能力。不同於一般的控制項,您可以建立具有多重連結屬性至資料庫欄位的控制項。所要做的只是點選 屬性 對話盒的資料連結區的 屬性具資料連結功能 項目,如圖17-10所示,可以將任何想要與資料庫連結的欄位與該屬性連結起來。

只要您喜歡,要建立多少個資料連結屬性就建多少,但是必須先個別地選取每個屬性的 屬性會連結到DataField 選項。


 

圖17-10 屬性對話盒中包含了所有建立資料感知屬性的選項。

PropertyChanged及CanPropertyChange方法
 

要在程式碼中支援資料連結的能力,不需要為屬性副程式作任何的更改。在每一個Property Let副程式中,必須呼叫PropertyChanged方法來告知Visual Basic屬性已經被更動過了且資料庫欄位也應該在資料錄指標指向下一筆資料前更新資料。如果忽略了此動作,則資料庫欄位就不會被更新了。如果在屬性對話盒中選取了 立即更新 選項的話,資料庫欄位便會在有更動的時候立即被更新。

Visual Basic也提供了CanPropertyChange方法,讓您查詢更新的時機是否安全。您可以利用下面「CustomerName」Property Let副程式的程式碼來達到同樣的目的。(由精靈產生的程式碼以粗體表示)

Public Property Let CustomerName(New_CustomerName As String)
    If CanPropertyChange("CustomerName") Then    
        TxtCustomerName.Text = New_CustomerName
        PropertyChanged "CustomerName"
    End If
End Sub

然而要注意的是,不需要全部只呼叫CanPropertyChanged方法。因為在Visual Basic 6及5的環境下,其傳回值始終為True,即使資料庫欄位無法更新時。應該在相容的程式語言下才使用該功能。針對所有在更新前呼叫此方法的屬性,應該將 屬性 對話盒中的 在屬性值改變前,先呼叫CanPropertyChange 選項也勾選起來。當然,如果不勾選也不會造成任何傷害,您可以自由選擇。

要正確地支援資料連結功能,您的組成控制項必須在當它們的內容被改變時,更新相關聯的資料庫欄位。以往這些動作都是在Change或Click事件副程式中動作的,如下面的程式碼所示:

Private Sub txtCustomerName_Change()
    PropertyChanged "CustomerName"
End Sub

DataBindings集合
 

筆者之前有提過,只有一個屬性可以連結到DataField Extender屬性。因為您的控制項可以連結多個資料庫欄位,所以需要提供開發人員關聯每個屬性到相關資料庫欄位的方法。此關聯的方式可在設計階段或執行階段做到。

針對每個在執行階段可連結的屬性,必須選取屬性對話盒中的 設計階段時  會顯示於DataBindings集合物件 選項。如果有一個或多個屬性的該選項被選取的話,則DataBindings項目便會出現在屬性視窗中。當點選它時,Visual Basic會帶出如圖17-11般的對話盒。要注意的是連結到DataField屬性的屬性也會出現在DataBindings集合中。

Visual Basic 6允許在DataBindings集合物件中連結不同資料來源的屬性,且也可為每個屬性選擇不同的DataFormat屬性。在Visual Basic 5環境中,只可以將屬性連結到相同的資料來源。


 

圖17-11 DataBindings對話盒讓開發人員在設計階段將屬性連結資料庫欄位。

所有的連結到資料庫的屬性都會出現在DataBindings集合物件中。不能透過程式碼來新增該集合物件中的項目,但是可以改變屬性所連結的資料庫欄位:

' Bind the CustomerName property to the CompanyName database field.
Customer1.DataBindings("CustomerName").DataField = "CompanyName"

另外一個在DataBindings中常見的工作是取消掉資料的改變,讓資料錄取消更新動作:

Dim dtb As DataBinding
For Each dtb In Customer1.DataBindings
    dtb.DataChanged = False
Next

關於其他DataBindings集合物件的資料,請參閱Visual Basic的線上說明文件。

DataRepeater控制項
 

在Visual Basic 6中使用DataRepeater控制項可讓您自行建立類似grid的控制項。此控制項會如同收納器般的運作:其可以收納任何型態的控制項,且對於使用者自訂的ActiveX控制項特別有用。

若想要以表格形式顯示資料路中的資料,但是不想使用標準的Visual Basic grid控制項─像是DataGrid或Hierachical FlexGrid控制項─因為對於與使用者互動的介面需要最大的彈性,或者因為想要顯示的資料無法嵌入一般的grid控制項(比方圖形)。圖17-12顯示一個由DataRepeater所建立的自訂grid,其上顯示出Bible.mdb資料庫中的出版者表格的資料。為了要建立如此的自訂grid,需要依照下面這些步驟來作:

  1. 建立一個包含所有需要的欄位的AddressOCX控制項﹔這是要放置在DataRepeater控制項中的物件。
  2. 建立所有想要顯露在DataRepeater控制項中的屬性─也就是說,Name、Street、City、Zip及State─讓所有的屬性與資料庫連結起來,然後在它們在設計階段時顯示在DataBindings集合物件中。
  3. 將專案儲存起來,將其編譯成一個獨立OCX檔,然後開啟想要放置您控制項的使用者的應用程式。
  4. 放置一個ADO資料控制元件在使用者的表單上,然後設定其ConnectionString及RecordSource屬性指向資料庫中提供資料的表格。(也可以使用任何其他的ADO資料來源,包括DataEnvironment物件)
  5. 放置一個DataRepeater控制項於表單上,設定其DataSource指向ADO資料元件,然後在RepeatedContorlName屬性中設定選許AddressOCX ActiveX控制項。(清單中包含了所有註冊在您系統中的OCX元件)
  6. 帶出DataRepeater控制項的屬性頁,切換到 RepeaterBindings 頁籤,然後將ActiveX控制項內部顯露出來的屬性關聯到資料庫欄位。也可以在 Format 頁籤中設定每個欄位的DataFormat屬性。


 

圖17-12 DataRepeater控制項讓您可建立自訂的資料庫表格瀏覽介面。

完整的程式碼請參閱隨書附贈的光碟。

由於DataRepeater控制項尚有一些未完善之處,所以必須花些注意力在這些問題上,讓其正常運作:

  • 使用者自訂控制項必須編譯成OCX檔;否則便無法放置在DataRepeater控制項中了。此外不能放置Visual Basic內建的控制項在DataRepeater控制項中。
     
  • 所有ActiveX控制項內部資料連結的屬性都應該要傳回一個字串值﹔可以使用DataRepeater控制項所提供的DataFormat選項來將這些值格式化。此外,所有的屬性在設計階段時都必須為可視的﹔否則DataRepeater控制項便看不見它們了。
     
  • 每當位於子表單的組成控制項資料有改變時,其應該呼叫PropertyChanged方法﹔否則資料庫便不會正確地被更新。
     
  • DataRepeater控制項只會建立一個控制項的參照﹔這個控制項是用來讓使用者編輯目前資料錄的欄位值得,反之其他的資料列都只是該控制項的圖形而已。也許注意到編輯時偶而會有些正確的重繪現象。
     

DataRepeater控制項顯露出數個屬性,方法及事件來增進其彈性。比方說可以直接存取其子控制項參照的屬性(RepeatedControl屬性),找出目前資料錄是位於第幾行(ActiveRow屬性),改變DataRepeater控制項的外觀(指定Caption、CaptionStyle、ScrollBars,、RowIndicator及RowDividerStyle屬性),取得或設定目前或目前看的的資料錄的書籤(使用CurrentRecord及VisibleRecords屬性),以及諸如此類之功能等等。也可監視使用者的動作─舉例來說,當它們捲動列表中的內容(ActiveRowChanged及VisibleRecordsChanged事件)或選取其他資料行(CurrentRecordChanged事件)。

很有趣的是,DataRepeater控制項可藉由給予RepeatedControlName屬性一個新的屬性值,讓其在執行階段可以載入一個不同的子ActiveX控制項。在這種情形下,必須使用RepeaterBindings集合物件來關聯連結的屬性。(可利用PropertyNames屬性提供使用者一個可連結屬性的清單)。每當一個新的子控制項在執行階段被載入時,DataRepeater會引發RepeatedControlLoaded事件,程式設計師可以利用該事件來正確地初始化新的控制項。

什麼被忽略掉了
 

儘管Visual Basic提供的資料連結機制有少數的特色並無直接地被支援,且必須自己去設法達成,但已經相當完整了。

舉例來說,控制項並不直接支援連結第二個資料來源的特色,比方利用list及combol控制項下拉選取那般。可以顯露自訂屬性─像是RowSource這種讓開發人員可以指定第二個資料來源的屬性─來達到同樣的效果(或者其他像是ADO-compliant資料來源)。這裡要解決的問題是:不可在屬性視窗中顯示自訂的清單,所以要如何讓開發人員在設計階段選擇這些資料來源呢?答案在於使用者自訂的屬性頁,我們會在下一節裡提到它。

有件事似乎是不太可能做到,那就是在執行階段時決定哪個屬性要與哪個DataField Extender屬性連結。在這個情形下,答案卻是出奇的簡單:建立一個連結至DataField的額外屬性,該屬性代表著該控制項所顯露出來的其他屬性之一。這個方法可以藉由新的函數CallByName來做到。舉例說明,假設想要給開發者有能力連結任何使用者自訂控制項顯露出來的屬性。需要建立兩個額外的屬性:BoundPropertyName,該屬性會保留被連結的屬性的名稱,另一個為BoundValue,其負責實際的連結。下面為CallByName函數的使用範例:

' BoundValue binds directly to DataField, but the value actually stored
' in the database depends on the BoundPropertyName property.
Public Property Get BoundValue() As Variant
    BoundValue = CallByName(Me, BoundPropertyName, vbGet)
End Property
Public Property Let BoundValue (New_BoundValue As Variant)
    CallByName Me, BoundPropertyName, vbLet, New_BoundValue
End Property

您應該讓BoundValue屬性隱藏起來,這樣開發人員才不會直接存取到該值。

屬性頁
 

大多數在Visual Basic或協力廠商那裡獲得的ActiveX控制項都具備一或多個屬性頁。所以在本節中,會發現建立自訂屬性頁會有多麼的簡單輕鬆。

即使Visual Basic的屬性視窗已經足夠讓使用者在設計階段時輸入所有的屬性,但是至少有三個理由可以告訴您為什麼應該要為您的控制項建立自己的屬性頁。第一理由,讓使用者使用控制項自己的自訂屬性頁會更為簡化他們的工作,因為所有的屬性可以被有邏輯地分類清楚。第二個理由更為重要,自訂的屬性頁對於在設計階段時屬性的設定有很大的影響。例如,不能在屬性視窗中動態地顯示下拉式選單,秀出一系列屬性值讓開發人員選擇,也不能讓開發人員在屬性視窗中拉出一個編輯畫面來輸入多個屬性值(就如同他們在ListBox及ComboBox控制項中的List屬性所做的相同)。這些限制可以利用自訂屬性頁輕易的克服。第三個理由,自訂屬性頁可讓您地區化您控制項的設計階段使用者介面而沒有語系的問題。

所以您可在筆者所建立的範例控制項(SuperListBox)看到屬性頁的運作,SuperListBox ActiveX控制項為一個ListBox的運用,其顯露出一個AllItems屬性(傳回所有項目,以換行字元分隔開來)讓您在執行階段使用彈跳式選單來輸入新的項目。筆者的控制項也讓程式設計師能夠連結Text或ListIndex屬性至資料欄位,也因此可以克服Visual Basic裡幾個資料連結限制其中一項限制。此控制項運用了一些有趣的程式設計技術─像是API函數來達成圓柱格式─可在隨書光碟中找到其完整的原始程式碼。

使用屬性頁精靈
 

在專案選單中,可以選擇「增加屬性頁」指令來為您的控制項專案加入新的屬性頁,但是可使用屬性頁精靈幫您節省更多功夫。(必須先在外掛程式管理員中先將屬性頁精靈給加進來)屬性頁精靈的第一步,所需要做的是先建立自訂屬性頁,然後選擇其順序,還有是否要保留原本標準屬性頁(見圖17-13)。Visual Basic會自動第加入StandardColor、StandardFont及StandardPicture屬性頁(這是為了可以使用OLE_COLOR、StdFont及StdPicture屬性值等),但您也可以決定是否要使用它們。


 

圖17-13 利用屬性頁精靈建立屬性頁的第一個步驟是:點選哪些想要建立的屬性頁及改變它們的順序。

第二個步驟,要決定每個屬性頁要顯示的屬性。所有留在左邊選單(如圖17-14)中的屬性將不會顯示在任何屬性頁中。


 

圖17-14 在屬性頁精靈的第二個步驟中,要決定哪些屬性要顯示在哪個屬性頁中。

當點選完成按鈕時,精靈會建立一或多頁屬性頁模組。精靈會依據您指定給每個屬性頁的屬性分別產生Label控制項(其Caption屬性即該屬性的名稱)以及TextBox或CheckBox控制項。如果想要建立一個更為友善的介面的話─舉例來說,利用ComboBox控制項來輸入列舉型的屬性─必須自行更改精靈產生的介面。圖17-15顯示出SuperListBox控制項的一般屬性頁,這是筆者在精靈產生後自行將原本兩個TextBox換成兩個ComboBox的。


 

圖17-15 屬性頁精靈所產生的屬性頁經過作者修飾後的樣子。

PropertyPage物件
 

瀏覽屬性頁精靈所產生的程式碼,相信您一定很快會了解屬性頁是如何運作的。PropertyPage物件就類似一張表單,支援許多表單物件的屬性,方法以及事件,包含Caption、Font及所有鍵盤和滑鼠的事件。如果需要的話,也可以在屬性頁中加入物件拖曳的功能。

當然,屬性頁也有它們奇特的地方。舉例來說,可以使用StandardSize屬性來控制屬性頁的大小,該值可以為0-Custom(其大小由物件的大小來決定),1-Small(101x375pixels),或者是2-Large(179X375pixels)。Microsoft建議我們使用使用者自訂大小的屬性頁,因為在不同銀幕解析度環境中所呈現出來的大小可能不盡相同。

應該有注意到圖17-15的屬性頁中並沒有包含通常在標準屬性頁裡會看到的確定,取消以及套用等按鈕。事實上這些按鈕是由執行環境所提供的,所以並不需要親自加入它們。屬性頁藉由PropertyPage物件的屬性及事件與執行環境傳達訊息。如果專案具有連結好的說明檔的話,屬性頁上也會出現 說明 的按鈕。

當屬性頁開啟時,PropertyPage物件便會接收到SelectionChanged事件。在此事件中,程式碼應該讀取出此屬性頁裡所有控制項目前所關聯的屬性的屬性值。SelectedControls集合物件會傳回目前屬性頁中所有被選取的控制項的集合物件參照。舉例來說,下面的程式碼為SuperListBox控制項一般屬性頁的SelectionChanged事件中的程式碼:

Private Sub PropertyPage_SelectionChanged()
    TxtCaption.Text = SelectedControls(0).Caption
    TxtAllItems.Text = SelectedControls(0).AllItems
    ChkEnabled.Value = (SelectedControls(0).Enabled And vbChecked)
    CboShowPopupMenu.ListIndex = SelectedControls(0).ShowPopupMenu
    CboBoundPropertyName.Text = SelectedControls(0).BoundPropertyName
    Changed = False
End Sub

當任何一個項目的內容被改變時,在Change或Click事件中應該將PropertyPage的Changed屬性設為True,如下面範例所示:

Private Sub txtCaption_Change()
    Changed = True
End Sub
Private Sub cboShowPopupMenu_Click()
    Changed = True
End Sub

將Change的屬性設為True時便會自動地將套用按鈕功能啟動。當使用者點選此按鈕時(或者是切換到別的屬性頁時),PropertyPage物件便會接收到一個ApplyChanges事件。在此事件中,必須將屬性頁裡的控制項的值指定到控制項相關的屬性,如下面範例所示:

Private Sub PropertyPage_ApplyChanges()
    SelectedControls(0).Caption = txtCaption.Text
    SelectedControls(0).AllItems = txtAllItems.Text
    SelectedControls(0).Enabled = chkEnabled.Value
    SelectedControls(0).ShowPopupMenu = cboShowPopupMenu.ListIndex
    SelectedControls(0).BoundPropertyName = cboBoundPropertyName.Text
End Sub

另外一個與PropertyPage物件相關的事件為─EditProperties事件。這個事件會因為開發人員點選了屬性視窗中的小按鈕開啟屬性頁而引發。(若該屬性與特定的屬性頁有關聯的換便會自動地出現在屬性視窗中)。通常可在開啟屬性頁時利用此事件來將駐點移到相關屬性的控制項上:

Private Sub PropertyPage_EditProperty(PropertyName As String)
    Select Case PropertyName
        Case "Caption"
            txtCaption.SetFocus
        Case "AllItems"
            txtAllItems.SetFocus
        ' etc. (other properties omitted...)
    End Select
End Sub

也可以將屬性頁上的控制項隱藏或disable掉,但這樣做通常沒什麼意義。

多個控制項的屬性頁選取
 

屬性頁精靈為我們所產生的程式碼只能應付簡單的單一控制項─也就是說,只給目前表單上所選取的控制項使用。為了要建立可以供多個控制項使用的屬性頁,要記住屬性頁不是為單一控制項所設計的,開發人員應該可以選取多個或不選取表單上的控制項們。每次有新的控制項從SelectedControls集合物件中被加入或移除時,SelectionChanged事件便會被引發:

處理多重選取控制項的標準方式如下。如果表單上被選取的控制項有著共通的屬性要被編輯的話,便可在屬性頁中填入屬性值﹔反之該欄位會留白。下面是針對多重選取而修改過的SelectionChanged事件的程式碼:

Private Sub PropertyPage_SelectionChanged()
    Dim i As Integer
    ' Use the property of the first selected control.
    txtCaption.Text = SelectedControls(0).Caption
    ' If there are other controls, and their Caption property differs from
    ' the Caption of the first selected control, clear the field and exit.
    For i = 1 To SelectedControls.Count - 1
        If SelectedControls(i).Caption <> txtCaption.Text Then
            txtCaption.Text = ""
            Exit For
        End If
    Next
    ' The AllItems property is dealt with in the same way (omitted ...).
    ' The Enabled property uses a CheckBox control. If values differ, use
    ' the special vbGrayed setting. 
    chkEnabled.Value = (SelectedControls(0).Enabled And vbChecked)
    For i = 1 To SelectedControls.Count - 1
        If (SelectedControls(i).Enabled And vbChecked) <> chkEnabled.Value 
            Then
            chkEnabled.Value = vbGrayed
            Exit For
        End If
    Next
    ' The ShowPopupMenu enumerated property uses a ComboBox control.
    ' If values differ, set the ComboBox's ListIndex property to _1.
    cboShowPopupMenu.ListIndex = SelectedControls(0).ShowPopupMenu
    For i = 1 To SelectedControls.Count - 1
        If SelectedControls(i).ShowPopupMenu <> cboShowPopupMenu.ListIndex 
            Then
            cboShowPopupMenu.ListIndex = -1
            Exit For
        End If
    Next
    ' The BoundPropertyName property is dealt with similarly (omitted ...).
    Changed = False
    txtCaption.DataChanged = False
    txtAllItems.DataChanged = False
End Sub

因為必須在ApplyChange事件中決定開發人員是否輸入值在這些欄位中,所以在這裡的兩個TextBox控制項的DataChange屬性被設為False:

Private Sub PropertyPage_ApplyChanges()
    Dim ctrl As Object
    ' Apply changes to Caption property only if the field was modified.
    If txtCaption.DataChanged Then
        For Each ctrl In SelectedControls
            ctrl.Caption = txtCaption.Text
        Next
    End If
    ' The AllItems property is deal with in the same way (omitted ...).
    ' Apply changes to the Enabled property only if the CheckBox control
    ' isn't grayed out.
    If chkEnabled.Value <> vbGrayed Then
        For Each ctrl In SelectedControls
            ctrl.Enabled = chkEnabled.Value
        Next
    End If
    ' Apply changes to the ShowPopupMenu property only if an item 
    ' in the ComboBox control is selected.
    If cboShowPopupMenu.ListIndex <> -1 Then
        For Each ctrl In SelectedControls
            ctrl.ShowPopupMenu = cboShowPopupMenu.ListIndex
        Next
    End If
    ' The BoundPropertyName property is dealt with similarly (omitted ...).
End Sub

進一步的技術
 

這裡筆者要談論幾個可能在屬性頁設計中會使用到的技巧。例如,不需要等到ApplyChanges事件發生了才去改變ActiveX控制項的屬性值:可以在Change或Click事件發生時立即更新ActiveX控制項相關的屬性值。

另一個簡單易用的技巧是,PropertyPage物件可以呼叫使用者自訂控制項模組的Friend屬性及方法,因為它們都處於同一個專案之中。這技巧可以給您一些設計上的彈性:比方說,使用者自訂控制項可以顯露出組成控制項的Friend Property Get副程式,這樣的話屬性頁便可以直接處理它的屬性,如下面範例所示:

' In the SuperListBox UserControl module
Friend Property Get Ctrl_List1() As ListBox
    Set Ctrl_List1 = List1
End Property

上面這個方法會衍生出一個小問題,那就是PropertyPage利用程式碼透過SelectedControls集合物件存取使用者自訂控制項﹔反之,Friend成員只能被特定的物件變數存取。可以利用下面的範例試著將集合物件裡的項目指定給特定的物件變數:

' In the PropertyPage module
Dim ctrl As SuperListBox
' Cast the generic control to a specific SuperListBox variable.
Set ctrl = SelectedControls(0)
' Now it is possible to access Friend members.
Ctrl.Ctrl_List1.AddItem "New Item"

最後筆者要說明的技巧似乎在建立複雜的使用者自訂控制項時比較有幫助,像是在本章前面我介紹的使用者自訂ActiveX控制項。很令人驚訝的是,可以在使用自訂控制項的屬性頁上使用使用者自動控制項自己本身。圖17-16顯示了一個關於此技巧的例子:再一般屬性頁中使用了一個使用者自訂ActiveX控制項的參照來讓開發人員制定自訂控制項本身的屬性!


 

圖17-16 目前專案中使用者自定控制項的屬性頁。

這個方式漂亮之處在於在該屬性頁中不用寫什麼程式碼。圖17-16中屬性頁的程式碼如下所示:

Private Sub Customer1_Change(PropertyName As String)
    Changed = True
End Sub
Private Sub PropertyPage_ApplyChanges()
    ' Read all properties in one loop.
    Dim propname As Variant
    For Each propname In Array("CustomerName", "Address", "City", _
        "ZipCode", "Country", "Phone", "Fax")
        CallByName SelectedControls(0), propname, VbLet, _
            CallByName(Customer1, propname, VbGet)
    Next
End Sub
Private Sub PropertyPage_SelectionChanged()
    ' Assign all properties in one loop.
    Dim propname As Variant
    For Each propname In Array("CustomerName", "Address", "City", _
        "ZipCode", "Country", "Phone", "Fax")
        CallByName Customer1, propname, VbLet, _
            CallByName(SelectedControls(0), propname, VbGet)
    Next
End Sub

注意程式碼中利用了CallByName函數來簡化屬性值給予的動作。

高手的技巧
 

到這裡為止,您已經知道了所有建立ActiveX控制項時應該要了解的觀念。然而還有一些更進一步的技巧可能是一些有經驗的程式設計師還不知道的。當筆者再這一節中證明這些技巧時,也沒有必要去了解Windows及ActiveX程式設計的一些錯綜複雜的環節來增加控制項的效率,因為大部分的時間,Visual Basic已足以應付這些問題了。

Callback回呼方法
 

在ActiveX控制項中產生父表單中的事件的確很容易,但是這不是唯一使兩個物件互相溝通的方法。在 第十六章 ,筆者說明了使用Callback回呼方法讓一個物件告訴另外一個事件的發生。回呼方法對於事件來說有許多好處:它們比一般的事件平均快五到六倍的速度,且更重要的是,它們不會在當使用者表單秀出訊息方塊時將應用程式停滯住。

在隨書光碟中,可以找到SuperTimer控制項完整的原始碼。SuperTimer控制項使用了ISuperTimerCBK介面(一個ActiveX控制項專案中的PublicNotCreatable類別)的回呼方法與它的父表單溝通。當一張表單或其他收納器使用了該介面時,SuperTimer控制項便會送出一個訊息給該介面的Timer方法。下面是SuperTimer控制項裡表單的原始碼:

Implements ISuperTimerCBK
Private Sub Form_Load()
    Set SuperTimer1.Owner = Me
End Sub
Private Sub ISuperTimerCBK_Timer()
    ' Do whatever you want here.
End Sub

SuperTimer控制項包含了一個Timer1組成控制項,在使用者自訂控制項模組中產生Timer事件﹔在此副程式中,此控制項會決定它是要產生一個事件或者是呼叫回呼方法:

Public Owner As IsuperTimerCBK
Private Sub Timer1_Timer()
    If Owner Is Nothing Then
        RaiseEvent Timer      ' Fire a regular event.
    Else
        Owner.Timer           ' Fire a callback method.
    End If
End Sub

很有趣的是,如果使用者表單正在顯示對話盒時,在直譯環境裡的一個標準的Timer控制項的Timer事件並不會被引發。(儘管Timer不會在編譯好的應用程式中暫停)如果使用了SuperTimer OCX控制項的ISuperTimerCBK介面時,便不用忍受這樣的限制了,同時此法也能夠提供了比一般Timer控制項更強大的功能(見圖17-17)。


小秘訣

若應用程式執行在IDE界面或或已編譯過的程式時,SuperTimer控制項的範例程式顯示不同訊息。Visual Basic語言缺少分辨兩種模式的函數,但可善用不會被編譯進EXE檔,只會在IDE環境下執行的的Debug物件的所有方法。底下為此技術的範例:

Function InterpretedMode() As Boolean
    On Error Resume Next
    Debug Print 1/0                 ' This causes an error
    InterpretedMode = (Err <> 0)    ' but only inside the IDE.
    Err Clear                       ' Clear the error code. 
End Function

上述程式碼是根據Visual Basic Programming刊物內的「Tech Tips」來修改的。


 

圖17-17 編譯過的SuperTimer控制項可傳送CallBack方法給其父表單,即使對話方塊已顯示。

有著Vtable連結的快速呼叫
 

所有外在ActiveX控制項的參考-非內建的Visual Basic控制項-隱含著使用其Extender物件。您可能不知道,對於Extender的所有引用使用早期的ID連結,而非最有效率的Vtable連結。這表示若物件是包在ActiveX DLL元件內的話,在ActiveX控制項內呼叫方法會比呼叫相同的方法還要慢,因為在DLL內的物件是透過Vtable連結來引用的。

一般而言,ID連結不會嚴重地降低ActiveX控制項的效能,因為大部分屬性與方法都夠快,即使在低等級機器上。但有時候會需要更快的速度。假設現有個ListBox控制項,用來儘快地填入自資料庫讀出的資料,此時需要呼叫屬性或方法數以千次,此時ID連結的負荷就不可忽略了。

此問題的解法在概念上是不難的。加入一個PublicNoCreatable類別到ActiveX控制項專案,此專案包含與ActiveX控制項包含相同的屬性與方法。此類別沒作什麼事,只是表示主要UserControl模組的屬性與方法的執行。一但ActiveX控制項被使用時,他會建立相關的Public物件與將他變成唯獨屬性。客戶端表單可儲存此屬性的傳回值,用一種特別的物件變數來指定之,且透過第二個物件來呼叫ActiveX控制項的內容。此物件並未使用Extender物件,因此可透過Vtable連結而非ID連結來存取。

筆者發現透過此相關物件來存取UserControl的屬性可比透過ActiveX控制項的正規引用還快約15次。在書附光碟,有個範例專案,其目的為說明各種方法的效率。可以此為模型用在您的ActiveX專案。

第二層界面
 

另一個使用Vtable連結的方法為讓ActiveX控制項有第二層界面,且讓客戶端表單存取第二層界面而非主要界面。此方法也比立基於第二PublicNotCreatable物件還快,因為不需要代表主要ActiveX控制項模組的分割類別。此方法另一個效益是相同界面可被多個ActiveX控制項共享,所以可對不同但相關的ActiveX控制項實作VTable-based的多態性。

此方法的實作並不難,除了一點外。假設在程式模組開頭,建立一個包含Implements Icontrollnterface敘述的ActiveX控制項。目的是藉由指定一個ActiveX控制項實體給一個界面變數,從而善用此通用界面的優點。不幸的是,下列程式碼會導致錯誤:

' In the client form
Dim ctrl As IcontrolInterface
Set ctrl = MyControl1                      ' Error "Type Mismatch"

當然,問題是MyControl1物件使用ActiveX控制項的Extender界面,其被為涵蓋在Icontrollnterface界面內。為了存取此界面,需要繞過Extender物件,如下:

Set ctrl = MyControl1.Object

捕抓Multicasting事件
 

Multicasting可捕抓被任何物件(這些物件可透過物件變數來獲得)所引發的事件。(第七章筆者已提過Multicasting,所以在閱讀本節前,可先複習該章節)。好消息是Multicasting也可在ActiveX控制項上運作,即使控制項已被編譯為單獨存在的OCX檔。換言之,您的ActiveX控制項可捕抓被父表單(或在表單上其他控制項)所引發的事件。

為了讓您稍微了解此技術能作什麼,筆者準備了一個簡單的ActiveX控制項,其會自動地改變其大小使之填滿其父表單的整個版面。若它沒有Multicasting,此特性會很難實作,因為當它改變大小時,需要由父表單來告知。由於Multicasting,所需要撰寫的程式碼就變得很少:

Dim WithEvents ParentForm As Form
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
    On Error Resume Next         ' In case parent isn't a form.
    Set ParentForm = Parent
End Sub
' This event fires when the parent form resizes.
Private Sub ParentForm_Resize()
    Extender.Move 0, 0, Parent.ScaleWidth, Parent.ScaleHeight
End Sub

Multicasting技術沒有任何應用上的限制。例如,可建立一個總是顯示TextBox內容值總和的ActiveX控制項。對於此項作業,需要捕抓這些控制項的Change事件。當捕抓內建控制項的事件時,您的UserControl模組必須對物件宣告為WithEvents變數,但當捕抓外在控制項的事件時-例如,TreeView或MonthView控制項-可用VBComtrolExtender物件變數,並依靠其ObjectEvent事件。

在Internet上使用ActiveX控制項
 

許多程式設計師相信網際網路是給ActiveX控制項最自然棲息地了,所以也許會驚訝說為什麼到了本章末筆者才開始提到網際網路上發展的ActiveX控制項。事實上,當筆者還在撰寫本書時,Microsoft Internet Explorer還是目前唯一支援ActiveX控制項最徹底的瀏覽器。所以如果在您的網頁中大量地使用了ActiveX控制項時,便自動地減少了網站可能的使用者數量。您知道的,即使在企業網路中因為管理者可以確知使用者端所使用的瀏覽器品牌,而可能大量使用ActiveX控制項在其企業網路中,但ActiveX控制項也許不見得對於網際網路很有幫助。然而就網際網路而言,動態HTML及Active Server Page似乎提供了一個建立動態且「更聰明」網頁的解決方案。

程式部份
 

一般來說,在網頁中使用的ActiveX控制項可以利用瀏覽器所提供的額外功能而增加本身的特色。在本節中,筆者會簡單地描述怎樣的控制項具有新的方法及事件可以使用。但是首先您必須要了解的一個ActiveX控制項是如何被放置在網頁上的。

將ActiveX控制項放置在HTML網頁上
 

可以使用市面上一些網頁編輯器來將控制項放置於網頁上。舉例來說,下面的程式碼為Microsoft公司的網頁編輯工具FrontPage所產生的HTML網頁,裡頭放置著隨書光碟裡的ClockOCX,此控制項的完整原始碼於隨書光碟裡可以找到。注意網頁是藉由CLSID來使用此控制項而不是使用較有可讀性的ProgID名稱。(控制項的HTML碼以粗體字形表示)

<HTML>
<HEAD>
<TITLE>Home page</TITLE>
</HEAD>
<BODY BGCOLOR="#FFFFFF">
<H1>A web page with an ActiveX Control on it.</H1>
<OBJECT CLASSID="clsid:27E428E0-9145-11D2-BAC5-0080C8F21830"
    BORDER="0" WIDTH="344" HEIGHT="127">
    <PARAM NAME="FontName" VALUE="Arial">
    <PARAM NAME="FontSize" VALUE="24">
</OBJECT>
</BODY>
</HTML>

如同您所看見的,所有有關於此控制項的資訊皆被<OBJECT>及</OBJECT>兩個標籤所包圍起來,且所有的初始值則規定由<PARAM>來定義。這些值會在控制項的ReadProperties事件中被讀取(如果網頁中沒有<PARAM>標籤時,控制項便會接收InitProperties事件取代ReadProperties事件,但實際上會發生的情況則要是瀏覽器而定)在網頁上所使用的ActiveX控制項應該要顯露出Fontxxxx屬性來取代或一起與Font物件屬性運作,這是因為在HTML網頁中指定物件的屬性並不是件簡單的工作。

當在網站中使用ActiveX控制項時,可能會弄錯一些事情─舉例來說,在瀏覽器中不允許使用者存取Extender屬性。Visual Basic 6提供了兩個方法讓使用者在適當的時機避免這樣的問題。第一個方法是從IDE裡面啟動元件,然後等到瀏覽器建立了該控制項的參照。第二個方法是從Visual Basic中建立一個裡面只有ActiveX控制項的HTML網頁,然後使用瀏覽器開啟它。可以在專案屬性的除錯頁籤中選取這些選項,如圖17-18所示:


 

圖17-18 專案屬性對話盒中的除錯頁籤。

超連結
 

使用者控制項使用一超連結屬性Hyperlink來傳回一個超連結物件,讓使用者藉以瀏覽其他網頁。Hyperlink物件顯露出三個方法,其中最重要的方法為NavigateTo:

Hyperlink.NavigateTo Target, [Location], [FrameName]

Target是所要瀏覽網頁的網址,Location為一非必要的參數,其作用在於指向網頁中的一特定位置,而FrameName則是在網頁中的分頁名稱,亦為一非必要參數。如果ActiveX控制項是在瀏覽器中執行的話,則新的網頁會在原本的瀏覽器中被瀏覽,如果控制項不在瀏覽器中執行,則使用者所預設的瀏覽器便會被啟動。

Hyperlink物件所顯露出的另外兩個方法,GoBack及GoForard,可讓您檢視瀏覽器所瀏覽過網頁的清單。除非絕對確定歷史清單不是空的,否則應該使用On Error敘述句來防止錯誤發生:

Private Sub cmdBack_Click()
    On Error Resume Next
    Hyperlink.GoBack
    If Err Then MsgBox "History is empty!"
End Sub

小秘訣

當然也可以瀏覽其他格式的文件而不是只能瀏覽HTML網頁。例如說,Internet Explorer可以顯示Microsoft Word及Microsoft Excel檔案,所以可使用它當作文件的瀏覽器,如下面範例所示:

Hyperlink.NavigateTo "C:\Documents\Notes.Doc"

非同步下載
 

由Visual Basic開發出來的ActiveX控制項支援屬性的非同步下載。假設說有一個類似PictureBox的ActiveX控制項可以顯示出GIF或BMP圖檔。可以使用非同步下載的方式取代以往等待圖檔完全下載完的方式。若要使用非同步下載的功能,可以使用ActiveX控制項物件的AsyncRead方法,其語法如下:

AsyncRead Target, AsyncType, [PropertyName], [AsyncReadOptions]

Target為網頁要下載的位置。AsyncType為下面幾種非同步的型態:0-vbAsyncTypePicture (可以指定圖檔給Picture屬性),1-vbAsyncTypeFile(Visual Basic所產生的檔案),或2-vbAsyncTypeByteArray(位元組陣列)。PropertyName是要被非同步下載屬性的名稱,這對於當有多個可做非同步下載的屬性時很有用。但是記住一次只能有一個AsyncRead指令被執行。

AsyncRead方法支援一個新的參數─AsyncReadOtions ,其接受的值列於表17-1中。可使用此值來調整非同步下載時的效率及決定控制項是否可以使用使用者端快取記憶體中的資料。

常數 AsyncRead行為
vbAsyncReadSynchronousDownload 1 當(同步下載)下載完成時才傳回
vbAsyncReadOfflineOperation 8 只使用本地端的版本
vbAsyncReadForceUpdate 16 強迫從遠端Web伺服器下載,忽略本地端的版本
vbAsyncReadResynchronize 512 只有當遠端Web伺服器的版本較新時,才更新本地端的版本
vbAsyncReadGetFromCacheIfNetFail &H80000 若連到遠端Web伺服器失敗的話,則使用本地端的版本
表17-1 AsyncRead方法的AsyncReadOptions參數可接受的值。

在隨書光碟中,可以找到ScrollablePictureBox ActiveX控制項完整的原始碼,當大型圖檔從網際網路上下載時,此控制項還支援圖檔的捲動。(見圖17-19)非同步下載的特色可以從PicturePath屬性中看出,當我們指定該屬性值時,控制項便開始啟動下載的動作:

Public Property Let PicturePath(ByVal New_PicturePath As String)
    m_PicturePath = New_PicturePath
    PropertyChanged "PicturePath"
    If Len(m_PicturePath) Then
        AsyncRead m_PicturePath, vbAsyncTypePicture, "Picture"
    End If
End Property

可在任何時候使用CancelAsyncRead指令取消一個非同步下載的動作:

CancelAsyncRead "Picture"


 

圖17-19 ScrollablePictureBox在Internet Explorer中執行的情形。

當非同步下載動作終止時,Visual Basic會引發一個AsyncReadComplete事件。此事件會接收到一個AsyncProperty物件,而該物件最重要的屬性為PropertyName及Value:

Private Sub UserControl_AsyncReadComplete(AsyncProp As AsyncProperty)
    If AsyncProp.PropertyName = "Picture" Then
        Set Image1.Picture = AsyncProp.Value
    End If
End Sub

在Visual Basic 6的版本中,AsyncProperty物件已經增加了其他的供能,包括新加入的屬性像是BytesMax、ByteRead、Status及StatusCode。關於這些屬性請參閱程式語言手冊。Visual Basic 6也包含了AsyncReadProgress事件,在每當使用者端的資料就緒時便會被引發。可使用此事件建立一個進度表來告訴使用者目前指令執行的狀態:

Private Sub UserControl_AsyncReadProgress(AsyncProp As AsyncProperty)
    If AsyncProp.PropertyName = "Picture" Then
        Dim percent As Integer
        If AsyncProp.BytesMax > 0 Then
            percent = (AsyncProp.BytesRead * 100&) \ AsyncProp.BytesMax
        End If
    End If
End Sub

每當使用者端有資料被儲存於磁碟上時,AsyncReadProgress及AsyncReadComplete事件便會隨即被引發(在我們的範例中,PicturePath是檔案的路徑)或是儲存於使用者端的快取資料中。如果您下載的不是圖檔(因此,AsyncProp.AsyncType為1-vbAsyncTypeFile或2-vbAsyncTypeByteArray),可以在資料還在下載的時候讀取及處理它。雖然這會使得執行效率稍微降低,但是通常使用者並不會察覺得出來。如果是開啟一個檔案,則必須在離開此事件前將檔案給關閉,且必須避免使用DoEvents事件來避免原來的問題。 AsyncReadProgress及AsyncReadComplete事件會在下載完畢時引發:可在AsyncReadPress事件中檢查AsyncProp.StatusCode屬性的傳回值6-vbAsyncStatusCodeEndDownloadData來得知何時下載完畢。

存取瀏覽器
 

在網頁中的控制項可以輕易地改變其顯示外觀及行為:它可以修改網頁本身及其上的其他控制項的屬性。也可以使用Parent物件來存取收納器物建,如範例所示:

' Changing the HTML page's foreground and background colors
With Parent.Script.document
    .bgColor = "Blue"
    .fgColor = "White"
End With

也可以使用ParentControls集合物件來存取及改變網頁上所有的控制項。這個設定是絕對必要的,因為Internet Explorer所顯露出來的Extender物件不能夠被Visual Basic的程式碼所使用。

筆者沒有以較多版面說明在包含ActiveX控制項網頁上所有可做的事。如果您對這感到興趣的話,應該可以在微軟網站上的Internet Explorer Scripting Object Model網頁中找到這方面的資料。


小秘訣

如果您寫了一個可以在一般的表單及網頁上使用的控制項,則需要知道您的控制項究竟是在哪個收納器中執行。可在Parent物件所傳回的參照中查看:

' Test if the control runs in an HTML page.
If TypeName(Parent) = "HTMLDocument" Then ...

Show及Hide事件
 

Show事件引發於每次網頁出現的時候﹔Hide事件則相反,在網頁被隱藏時會被引發。最後網頁可能又會出現,因此再度引發Show事件,或者是瀏覽器將網頁從快取資料中移除掉。(比如當瀏覽器被關閉時),在這個情況下,控制項會接收到 Terminate 事件。

多執行緒ActiveX控制項
 

如果要在Microsoft Internet Explorer或一個多執行緒的Visual Basic應用程式中使用ActiveX控制項,應該將此控制項編譯成為一apartment-threaded執行緒模型,可以在專案屬性對話盒中的一般頁簽裡選取該選項。然而要當心的是文件所提供的錯誤:在Internet Explorer 4.0中執行時,多執行緒控制項並不會引發Hide事件。為了讓您的ActiveX控制項能夠正常的運作,必須將其編譯成單執行緒控制項且開啟主動式桌面選項。可在微軟知識庫中的文件Q175907取得這方面的消息。

元件下載
 

當建立包含一或多個ActiveX控制項的HTML網頁時,若使用者端並無註冊您的控制項的話,必須提供使用者端瀏覽器安裝或下載控制項的方法。

建立部署套件
 

要在使用者端部署您的控制項的機制乃基於Cabinet(CAB)檔案。CAB檔案是一可包含多個ActiveX控制項的壓縮檔(或其他格式的檔案,像是EXE及DLL檔)且如果必要的話還可以給予數位認證的機制。可使用封裝暨部署精靈來製作CAB檔案,在第二個步驟中選擇要建立Internet Package。機靈還會產生一個HTM範例讓您做為CAB檔放置在網頁上的範本。此範例檔包含了CODEBASE屬性的正確值,讓瀏覽器知道CAB檔的名稱及ActiveX控制項的版本。如果控制項的CLSID並未註冊在使用者端的註冊區或者是控制項的版本比網頁上還舊的話,瀏覽器便會開始下載網頁裡指定的CAB檔。下面是ClockOCX控制項所產生的範例HTML檔案:

<OBJECT ID="Clock"
CLASSID="CLSID:27E428E0-9145-11D2-BAC5-0080C8F21830"
CODEBASE="ClockOCX.CAB#version=1,0,0,0">
</OBJECT>

CAB檔案可以存放所有ActiveX控制項執行時所需相關的附屬檔案,包含資料檔及附屬的DLL檔案。ActiveX控制項所需要的附屬檔案的清單於INF檔案中有詳細的描述,INF也是由封裝暨部署精靈所產生的,且也包含在CAB檔案中。

由Visual Basic所開發出來的ActiveX控制項同樣也需要Visual Basic執行階段檔案。在封裝暨部署精靈裡的預設下載執行階段物件的位址為微軟的網站。這樣的設定是為了確保使用者總是下載這些檔案的最新版本,同時也降低了您網站的負荷。

安全性
 

當ActiveX控制項在瀏覽器中執行時,它可對使用者的系統中幹各種的邪惡的壞事,像是刪除系統檔案,毀損註冊區資料,或者是偷取秘密的資料。因此,必須向使用者保證不僅您的控制項不會做這些不法的行為,而且開發人員也不能夠藉由您的控制項破壞使用者的系統。

為了保證您的控制項不會(也不行)做出這些不當的行為,可將其標示為「安全初始化」或「安全的Script」。如果宣告了您的控制項為「安全初始化」,那麼就是告訴瀏覽器,網頁的設計人員無法有意或無意地藉由給予<PARAM>標籤不當的屬性值來傷害系統。如果將您的控制項宣告為「安全的Script」,那麼就更進一步地宣告了您的控制項沒有辦法藉由網頁上的腳本語言來進行破壞系統的動作。在瀏覽器方面,Microsoft Internet Explorer預設自動拒絕下載為標示為「安全初始化」或「安全的Script」的控制項。

讓您的控制項標示為安全並不代表您的控制項就完全不會對使用者的系統造成傷害。即使您的控制項標示安全,事實上也可能無意之間破壞了使用者的系統。這裡有幾點是您該注意的地方:

  • 提供讓開發人員可以在任何地方儲存資料的方法。此控制項對於腳本程式來說並不是安全的,因為一個惡意的開發人員也許會使用著各方法來覆寫重要的系統檔案。
     
  • 決定使用者端暫存資料的存放位置,但是卻讓開發人員自由地在該站存區寫入無限制大小的資料。同樣地,這也不是個針對腳本程式安全的控制項,因為開發人員也許故意消耗掉磁碟所有的暫存空間導致Windows突然地當機。
     

可以參考圖17-20,看看如何將您的控制項標示成為「安全初始化」或「安全的Script」。


小秘訣

可以利用Visual Studio裡附贈的OleView工具程式快速地得知哪些ActiveX控制項在您的機器上面為初始安全或腳本安全。下面是在系統註冊區裡標示為安全控制項的部分資料。

HKEY_CLASSES_ROOT
  \CLS
    \<your control's CLSID>
      \Implemented Categories
        \{7DD95802-9882-11CF-9FA9-00AA006C42C4}
        \{7DD95801-9882-11CF-9FA9-00AA006C42C4}

上面最後兩行資料表示該控制項分別為是初始安全及腳本安全。一旦知道這些資料是如何紀錄在註冊區裡後,便可以使用Regedit工具程式來修改這些設定或者是新增移除這些機碼了。



 

圖17-20 安裝暨部署精靈標示您的控制項為初始安全及安全性描述。

有一個更複雜的方式可以讓此安全問題獲得解決─IObjectSafety ActiveX介面,其可讓您的元件有條理地指示哪個方法及屬性為安全地。這個方式比標記安全提供了更大彈性。然而因為這牽涉到進階的主題,所以筆者不打算在此書中介紹它。

數位簽章
 

很明顯的,光是標記控制項為安全並不能夠滿足大部分的使用者。畢竟,每個人接可以標記他們自己的控制項為安全。即使他們信任您的目的及您身為程式設計師的能力,但是他們還是無法肯定該控制項是真的出於您手中,且在您編譯它後沒被竄改過。

微軟針對此問題提出了解決的方案─可以在控制項中加入一個使用公開金匙加密的數位簽章。為了要將數位簽章加入控制項中,需要一個私密金匙,可在提供數位認證的公司取得─比方說,VeriSign Inc。必須付一些費用來取得這樣的認證,但是即使是一位個體戶也負擔得起這樣的費用。若想要多了解有關這方面的資訊,請參考http://www.versign.com。一旦取得了一個數位任症候,便可透過ActiveX SDK中的SignCode公用程式為您的控制項─或者是控制項的CAB檔─中加入數位簽章了。也可以將數位簽帳加入EXE,DLL及OCX檔中,如果不想要透過CAB檔案來散播您的控制項的話。

版權宣告
 

ActiveX控制項可以作為商業用途的一部份販售,也可以獨立販售給開發人員。在稍後的日子裡,使用者應該能夠在設計階段時使用此控制項且在他們自己的應用程式中重新散播該控制項。如果不想讓他們的使用者能夠在重新散播您的控制項的話,便需要在您的控制項中加入版權宣告。

需要版權碼選項
 

如果在專案屬性對話盒中的一般頁籤中點選了 需要版權碼 選項後,Visual Basic便會產生一個VBL檔案包含在控制項的版權宣告中。比方說,下面的VBL控制項是ClockOCX控制項所產生的:

REGEDIT
HKEY_CLASSES_ROOT\Licenses = Licensing: Copying the keys may be a violation
of established copyrights.
HKEY_CLASSES_ROOT\Licenses\27E428DE-9145-11D2-BAC5-0080C8F21830 = 
Geierljeeeslqlkerffefeiemfmfglelketf

如同您所看到的,VBL檔案是一個為註冊區所撰寫的腳本程式。當建立了一個標準的安裝程式後,精靈會將此檔案包含在此安裝程式裡。當其他的開發人員購買了您的控制項且安裝在他們的機器後,安裝程式的程序便會使用這個檔案在註冊區中安裝版權宣告,但不會將該檔案複製到硬碟中。因此,當他們要在他們開發的應用程式中散播您的控制項的時候,VBL檔案便不會包括在他們的安裝套件中,所以他們的客戶也就不能夠在設計時其中使用此控制項了(當然除非他們向您購買了版權)。

一個需要註冊碼的控制項會在每次被參照的時候檢查它的註冊碼是否存在。如果此控制項是在一個編譯好的程式中被使用的話,則註冊碼便會被包含在可執行檔EXE中。但是如果控制項是在一個直譯的環境中被使用的話,沒有任何執行檔可以提供註冊碼,而控制項必須自己到註冊區中找。這表示說在Visual Basic或者是Office應用程式(或者其他VBA環境)中使用的控制項,都需要在註冊區登記它的註冊碼。

如果您的控制項使用其他的ActiveX控制項作為組成控制項的話,為了散播他們,應該要註冊他們的版權宣告﹔否則,您的控制項在設計階段將不會正常運作。在Visual Basic套件中所有的控制項中,不能再散播出去的只有DBGrid控制項。然而請注意,微軟版權同意書中規定,只能在「顯著地」擴展微軟控制項的功能時,才可以在您的ActiveX控制項中使用作為組成控制項。然而我從來沒聽說「顯著地」意義該怎麼衡量。

網頁控制項的註冊碼
 

前面說明的版權宣告問題並不是針對網頁上的控制項來說。事實上,要求使用者在其機器上註冊區安裝控制項的註冊碼一點意義也沒有。筆者想您也不願意將註冊碼寫在可讀的HTML網頁中。這個難題的解決方法是利用一個版權封裝檔案(或者簡稱LPK)。您可以在\Common\Tool\Vb\Lpk_Tool子目錄中找到建立LPK檔的工具程式。(見圖17-21)一旦建立了一個LPK檔案後,將可透過<PARAM>標籤來使用此檔案,如範例所示:

<PARAM NAME="LPKPath" VALUE="ClockOCX.lpk">

此參數告訴瀏覽器到哪裡下載ActiveX控制項的註冊碼﹔註冊碼會在每次網頁被下載的時候被重新解讀,這是因為在網頁上的ActiveX控制項的註冊碼並不會被安裝在使用者端的機器上。LPKPath參數值可以為絕對或相對的URL路徑,但是在後面的例子中,可能會因網頁位置的改變而發生問題。網頁的管理者必須要購買了ActiveX控制項的版權後才可以將控制項放置在他們的網頁中。換句話說,就版權宣告而言,網頁管理者就如同開發人員一樣需要針對控制項的使用權負責的。


 

圖17-21 Lpt_Tool工具程式可以幫我們建立包含一或多個ActiveX控制項註冊碼的LPK檔案。

說明

這裡要說明的是,Visual Basic所提供的版權保護方式並不是絕對安全的。畢竟一個心懷惡義的開發人員只要將VBL檔案拷貝至安裝磁片或─如果該檔案並不存在的話─從註冊區中取得相關的註冊資訊然後重新建立VBL檔─便可取得該控制項的註冊版權了。實際上只可以確定的一件事是,註冊碼不會被包含在安裝程式中。如果需要更多安全上的保護的話,應該設計避免註冊區或版權檔案被讀取的方式來避免上述這些問題。


如果您有仔細地研讀過十六章以及本章節,應該會很驚訝地發現Visual Basic對於ActiveX控制項所新增的特色十分的少。在下一個章節中我們將討論如何建立資料感知類別及元件。