14. 繪圖技巧

Visual Basic提供了一組相當豐富的繪圖工具,包括了頗具威力的PaintPicture方法。PaintPicture方法提供了一個相當友善的界面,漂亮地將普受歡迎的BitBlt Windows API函式包裝起來。

在本章中你會看到PaintPicture好幾次。另外,你也會看到我們直接使用BitBlt函式來產生動畫,因為這是產生動畫最快的方法。本章也展示了其他幾個實用的API函式,我們把這些API函式封裝在物件中,這是使用這些函式最好的方法。

如何在RGB、HSL或HSV定義中計算顏色常數?
 

在Visual Basic中,每一種顏色由一個數字來代表,就是所謂的RGB顏色值。這個數值的位元結構由三個值所組成,這三個值的範圍由0到255,分別對應紅、綠、藍三個顏色的深淺度。Visual Basic的RGB函式結合這三組數值,變成一個單一的顏色值,但Visual Basic並沒有提供任何函式讓我們可以從RGB顏色值中擷取出代表三種顏色深淺的數值。以下這段程式定義了一個物件類別,它可以從一個單一的顏色值中取得紅、綠、藍的數值。請把這段程式放進一個物件類別模組中,並取名為RGB。

`RGB.CLS
Option Explicit

Private mlngColor As Long

`~~~Color
Property Let Color(lngColor As Long)
    mlngColor = lngColor
End Property
`~~~Red
Property Get Red() As Byte
    Red = mlngColor And &HFF
End Property

`~~~Green
Property Get Green() As Byte
    Green = (mlngColor \ &H100) And &HFF
End Property

`~~~Blue
Property Get Blue() As Byte
    Blue = (mlngColor \ &H10000) And &HFF
End Property

為了使這個物件類別符合簡單的原則,我們把Color屬性設計成唯寫屬性,而代表三原色的屬性則設計成唯讀屬性。當然,你可以把所有的屬性改為可讀又可寫。

以下這段程式展示了如何使用RGB物件來擷取紅藍綠三原色的值。

Option Explicit

Private Sub Form_Click()
    Dim rgbTest As New RGB
    Dim lColor As Long
    'Combine R,G,and B to create a known color value
    lColor = RGB(17, 53, 220)
    Print "Color:", lColor
    'Use RGB object to extract component colors
    rgbTest.Color = lColor
    Print "Red:", rgbTest.Red
    Print "Green:", rgbTest.Green
    Print "Blue:", rgbTest.Blue
End Sub

除了RGB顏色模式之外,另外兩種顏色的表示方法是用色調(Hue)、飽和度(Saturation)和值(Value)來代表的HSV模式,以及用色調(Hue)、飽和度(Saturation)和亮度(Luminosity)來代表的HSL模式。對某些人而言,這兩種模式較具直覺性,而且某些種類的圖形較適合以HSV模式來處理。例如,日落景觀最好是用一組色調及飽和度不變、但亮度不同的紅色來描繪。色調表示某個顏色在光譜中的相對位置,其值從0到360;飽和度是代表顏色純度的百分比,範圍從0(無色)到100(純色);亮度則是代表某個顏色亮度的百分比,範圍也是從0(黑)到100(白)。這裡有一個實用的物件類別可以轉換RGB值到HSV值:

`HSV.CLS
Option Explicit

`RGB color properties
Private mintRed As Integer
Private mintGreen As Integer
Private mintBlue As Integer

`HSV color properties
Private msngHue As Single
Private msngSaturation As Single
Private msngValue As Single

`Keep track of implied conversion
Private mintCalc As Integer
Private Const RGB2HSV = 1
Private Const HSV2RGB = 2

`~~~ Hue
Property Let Hue(intHue As Integer)
    msngHue = intHue
    mintCalc = HSV2RGB
End Property

Property Get Hue() As Integer
    If mintCalc = RGB2HSV Then CalcHSV
    Hue = msngHue
End Property

`~~~ Saturation
Property Let Saturation(intSaturation As Integer)
    msngSaturation = intSaturation
    mintCalc = HSV2RGB
End Property

Property Get Saturation() As Integer
    If mintCalc = RGB2HSV Then CalcHSV
    Saturation = msngSaturation
End Property

`~~~ Value
Property Let Value(intValue As Integer)
    msngValue = intValue
    mintCalc = HSV2RGB
End Property

Property Get Value() As Integer
    If mintCalc = RGB2HSV Then CalcHSV
    Value = msngValue
End Property

`~~~ Red
Property Let Red(intRed As Integer)
    mintRed = intRed
    mintCalc = RGB2HSV
End Property

Property Get Red() As Integer
    If mintCalc = HSV2RGB Then CalcRGB
    Red = mintRed
End Property

`~~~ Green
Property Let Green(intGreen As Integer)
    mintGreen = intGreen
    mintCalc = RGB2HSV
End Property

Property Get Green() As Integer
    If mintCalc = HSV2RGB Then CalcRGB
    Green = mintGreen
End Property

`~~~ Blue
Property Let Blue(intBlue As Integer)
    mintBlue = intBlue
    mintCalc = RGB2HSV
End Property

Property Get Blue() As Integer
    If mintCalc = HSV2RGB Then CalcRGB
    Blue = mintBlue
End Property

`Converts RGB to HSV
Private Sub CalcHSV()
    Dim sngRed As Single
    Dim sngGreen As Single
    Dim sngBlue As Single
    Dim sngMx As Single
    Dim sngMn As Single
    Dim sngDelta As Single
    Dim sngVa As Single
    Dim sngSa As Single
    Dim sngRc As Single
    Dim sngGc As Single
    Dim sngBc As Single
    sngRed = mintRed / 255
    sngGreen = mintGreen / 255
    sngBlue = mintBlue / 255
    sngMx = sngRed
    If sngGreen > sngMx Then sngMx = sngGreen
    If sngBlue > sngMx Then sngMx = sngBlue
    sngMn = sngRed
    If sngGreen < sngMn Then sngMn = sngGreen
    If sngBlue < sngMn Then sngMn = sngBlue
    sngDelta = sngMx - sngMn
    sngVa = sngMx
    If sngMx Then
        sngSa = sngDelta / sngMx
    Else
        sngSa = 0
    End If
    If sngSa = 0 Then
        msngHue = 0
    Else
        sngRc = (sngMx - sngRed) / sngDelta
        sngGc = (sngMx - sngGreen) / sngDelta
        sngBc = (sngMx - sngBlue) / sngDelta
        Select Case sngMx
        Case sngRed
            msngHue = sngBc - sngGc
        Case sngGreen
            msngHue = 2 + sngRc - sngBc
        Case sngBlue
            msngHue = 4 + sngGc - sngRc
        End Select
        msngHue = msngHue * 60
        If msngHue < 0 Then msngHue = msngHue + 360
    End If
    msngSaturation = sngSa * 100
    msngValue = sngVa * 100
    mintCalc = 0
End Sub

`Converts HSV to RGB
Private Sub CalcRGB()
    Dim sngSaturation As Single
    Dim sngValue As Single
    Dim sngHue As Single
    Dim intI As Integer
    Dim sngF As Single
    Dim sngP As Single
    Dim sngQ As Single
    Dim sngT As Single
    Dim sngRed As Single
    Dim sngGreen As Single
    Dim sngBlue As Single
    sngSaturation = msngSaturation / 100
    sngValue = msngValue / 100
    If msngSaturation = 0 Then
        sngRed = sngValue
        sngGreen = sngValue
        sngBlue = sngValue
    Else
        sngHue = msngHue / 60
        If sngHue = 6 Then sngHue = 0
        intI = Int(sngHue)
        sngF = sngHue - intI
        sngP = sngValue * (1! - sngSaturation)
        sngQ = sngValue * (1! - (sngSaturation * sngF))
        sngT = sngValue * (1! - (sngSaturation * (1! - sngF)))
        Select Case intI
        Case 0
            sngRed = sngValue
            sngGreen = sngT
            sngBlue = sngP
        Case 1
            sngRed = sngQ
            sngGreen = sngValue
            sngBlue = sngP
        Case 2
            sngRed = sngP
            sngGreen = sngValue
            sngBlue = sngT

        Case 3
            sngRed = sngP
            sngGreen = sngQ
            sngBlue = sngValue
        Case 4
            sngRed = sngT
            sngGreen = sngP
            sngBlue = sngValue
        Case 5
            sngRed = sngValue
            sngGreen = sngP
            sngBlue = sngQ
        End Select
    End If

    mintRed = Int(255.9999 * sngRed)
    mintGreen = Int(255.9999 * sngGreen)
    mintBlue = Int(255.9999 * sngBlue)
    mintCalc = 0
End Sub

HSL模式被用在Microsoft小畫家裡的調色板。HSL模式很近似HSV模式,差別只在亮度部份的計算方式有些不同。以下有一個物件類別可供RGB模式和HSL模式的相互轉換:

`HSL.CLS
Option Explicit

`RGB color properties
Private mintRed As Integer
Private mintGreen As Integer
Private mintBlue As Integer

`HSL color properties
Private msngHue As Single
Private msngSaturation As Single
Private msngLuminosity As Single
`Keep track of implied conversion
Private mintCalc As Integer
Private Const RGB2HSL = 1
Private Const HSL2RGB = 2

`~~~ Hue
Property Let Hue(intHue As Integer)
    msngHue = (intHue / 240!) * 360!
    mintCalc = HSL2RGB
End Property
Property Get Hue() As Integer
    If mintCalc = RGB2HSL Then CalcHSL
    Hue = (msngHue / 360!) * 240!
End Property

`~~~ Saturation
Property Let Saturation(intSaturation As Integer)
    msngSaturation = intSaturation / 240!
    mintCalc = HSL2RGB
End Property

Property Get Saturation() As Integer
    If mintCalc = RGB2HSL Then CalcHSL
    Saturation = msngSaturation * 240!
End Property

`~~~ Luminosity
Property Let Luminosity(intLuminosity As Integer)
    msngLuminosity = intLuminosity / 240!
    mintCalc = HSL2RGB
End Property

Property Get Luminosity() As Integer
    If mintCalc = RGB2HSL Then CalcHSL
    Luminosity = msngLuminosity * 240!
End Property

`~~~ Red
Property Let Red(intRed As Integer)
    mintRed = intRed
    mintCalc = RGB2HSL
End Property

Property Get Red() As Integer
    If mintCalc = HSL2RGB Then CalcRGB
    Red = mintRed
End Property

`~~~ Green
Property Let Green(intGreen As Integer)
    mintGreen = intGreen
    mintCalc = RGB2HSL
End Property

Property Get Green() As Integer
    If mintCalc = HSL2RGB Then CalcRGB
    Green = mintGreen
End Property

`~~~ Blue
Property Let Blue(intBlue As Integer)
    mintBlue = intBlue
    mintCalc = RGB2HSL
End Property
Property Get Blue() As Integer
    If mintCalc = HSL2RGB Then CalcRGB
    Blue = mintBlue
End Property

Private Sub CalcHSL()
    Dim sngMx As Single
    Dim sngMn As Single
    Dim sngDelta As Single
    Dim sngPctRed As Single
    Dim sngPctGrn As Single
    Dim sngPctBlu As Single
    sngPctRed = mintRed / 255
    sngPctGrn = mintGreen / 255
    sngPctBlu = mintBlue / 255
    sngMx = sngMaxOf(sngMaxOf(sngPctRed, sngPctGrn), sngPctBlu)
    sngMn = sngMinOf(sngMinOf(sngPctRed, sngPctGrn), sngPctBlu)
    sngDelta = sngMx - sngMn
    msngLuminosity = (sngMx + sngMn) / 2
    If sngMx = sngMn Then
        msngSaturation = 0
    Else
        msngSaturation = 1
    End If
    If msngLuminosity <= 0.5 Then
        If msngSaturation > 0 Then
            msngSaturation = sngDelta / (sngMx + sngMn)
        End If
    Else
        If msngSaturation > 0 Then
            msngSaturation = sngDelta / (2 - sngMx - sngMn)
        End If
    End If

    If msngSaturation Then
        If sngPctRed = sngMx Then
            msngHue = (sngPctGrn - sngPctBlu) / sngDelta
        End If
        If sngPctGrn = sngMx Then
            msngHue = 2 + (sngPctBlu - sngPctRed) / sngDelta
        End If
        If sngPctBlu = sngMx Then
            msngHue = 4 + (sngPctRed - sngPctGrn) / sngDelta
        End If
        msngHue = msngHue * 60
    End If
    If msngHue < 0 Then msngHue = msngHue + 360
    mintCalc = 0
End Sub

Private Sub CalcRGB()
    Dim sngM1 As Single
    Dim sngM2 As Single
    Dim sngPctRed As Single
    Dim sngPctGrn As Single
    Dim sngPctBlu As Single
    If msngLuminosity <= 0.5 Then
        sngM2 = msngLuminosity * (1! + msngSaturation)
    Else
        sngM2 = (msngLuminosity + msngSaturation) _
            - (msngLuminosity * msngSaturation)
    End If
    sngM1 = 2! * msngLuminosity - sngM2
    If msngSaturation = 0! Then
        sngPctRed = msngLuminosity
        sngPctGrn = msngLuminosity
        sngPctBlu = msngLuminosity
    Else
        sngPctRed = rgbVal(sngM1, sngM2, msngHue + 120!)
        sngPctGrn = rgbVal(sngM1, sngM2, msngHue)
        sngPctBlu = rgbVal(sngM1, sngM2, msngHue - 120!)
    End If
    mintRed = Int(255.9999 * sngPctRed)
    mintGreen = Int(255.9999 * sngPctGrn)
    mintBlue = Int(255.9999 * sngPctBlu)
    mintCalc = 0
End Sub

Private Function rgbVal(sngN1 As Single, sngN2 As Single, _
sngHue As Single) As Single
   If sngHue > 360 Then
      sngHue = sngHue - 360
   ElseIf sngHue < 0 Then
      sngHue = sngHue + 360
   End If
   If sngHue < 60 Then
      rgbVal = sngN1 + (sngN2 - sngN1) * sngHue / 60
   ElseIf sngHue < 180 Then
      rgbVal = sngN2
   ElseIf sngHue < 240 Then
      rgbVal = sngN1 + (sngN2 - sngN1) * (240 - sngHue) / 60
   Else
      rgbVal = sngN1
   End If
End Function

Private Function sngMaxOf(sngV1 As Single, sngV2 As Single) As Single
    sngMaxOf = IIf(sngV1 > sngV2, sngV1, sngV2)
End Function

Private Function sngMinOf(sngV1 As Single, sngV2 As Single) As Single
   sngMinOf = IIf(sngV1 < sngV2, sngV1, sngV2)
End Function

如果要在應用程式中使用HSV物件類別或是HSV物件類別,請撰寫一個執行專案引用該物件類別,然後設定Red/Green/Blue屬性值、Hue/Saturation/Value屬性值,或是Hue/Saturation/Luminosity屬性值,然後直接讀取你想要的顏色模式資料,就可以把顏色從一個顏色模式轉換到另一個顏色模式裡。在 第二十九章"圖形" 裡的範例應用程式替HSV物件和HSL物件的使用方式提供了完整的說明。

如果想看這HSL顏色模式的表現方式,你可以看一下Windows 95和Windows NT所提供的系統顏色。在控制台中點選兩下「顯示器」圖像,然後在「內容-顯示器」對話方塊中點選「外觀」頁籤;按下「顏色」按鈕,在所有列出的顏色中,按下「其他」按鈕,這時系統會叫出「色彩」對話方塊,這個方塊讓你選擇某個顏色並且可以顯示該顏色的RGB值和HSL值。HSL模式使用從0到240的整數,因此我們的HSL物件類別也用這個數值範圍。

如何在Twips、點、像素、字元、英吋、公厘、公分之間相互轉換?
 

表單、圖片方塊或是印表機物件都有一個ScaleMode屬性用來設定衡量尺寸大小的單位,你可以用自訂的單位來衡量這些物件的大小,或者也可以用Twips、點、像素、字元、英吋、公厘和公分的近似值為單位來衡量物件的尺寸大小。為什麼是近似值呢?因為在許多機器上,Windows只能以這些單位計算出螢幕的約略尺寸。當這些單位應用在Printer物件上而且列印在高品質的印表機上時,這些單位就能夠表現得更為精準。

以下我們列出了這些單位之間的關係:

1英吋等於1440 Twips

1公分等於567 Twips

1英吋等於72 Points

1英吋等於2.54公分

1公分等於10公釐

字元是一個較特殊的單位,因為一個字元有兩個尺寸,水平尺寸和垂直尺寸:

水平尺寸:一個字元等於120 Twips

垂直尺寸:一個字元等於240 Twips

Visual Basic提供了兩個很有用的屬性,如下表,可以幫助你了解在某個物件上一個像素(Pixel)所含的Twips數目。因為每部顯示器實際的螢幕像素解析度有所不同,因此這些Twips數目的差異可能很大。另外,由於每個像素在水平方向所含的Twips數與垂直方向的Twips數目不同,所以Visual Basic為兩個方向各別提供了一個屬性。

屬性 傳回值
TwipsPerPixelX 每個像素在水平方向所含的Twips數目
TwipsPerPixelY 每個像素在垂直方向所含的Twips數目

藉由結合這些屬性和以上列出的單位運算關係,你可以很容易地在任意兩種不同的單位之間做轉換運算。

如何產生一個由藍轉黑的漸層效果背景?
 

以下這段程式以變化的藍色陰影方塊(由亮藍到純黑)來描繪表單的背景。這個程式最微妙的部分是它可以產生連續且平滑的漸層效果,而且這種效果在256色模式、High Color模式(16 bit)和True Color模式(24 bit)都能表現出來。在256色模式中,顏色斑點是無可避免的,因為要製造從亮藍褪變為純黑的效果。Visual Basic的Line方法不容許直線中有顏色斑點,但它卻容許我們填滿小方塊(顏色斑點是由小方塊造成的)。為了達到這個效果,我們用以下的程序把表單的DrawStyle屬性改為vbInvisible並且把表單ScaleMode屬性改為vbPixel。DrawStyle屬性決定線的樣式,把它設為vbInvisible可以防止每個藍色格子被畫上黑色邊框。把ScaleMode設為vbPixel,讓我們以像素為單位計算每個方格大小而且不會有捨位誤差(Round-Off Error),這樣就可以防止方格的重疊以及防止方塊與方塊之間出現空白。

Option Explicit

Private Sub Form_Paint()
    Dim lngY As Long
    Dim lngScaleHeight As Long
    Dim lngScaleWidth As Long
    ScaleMode = vbPixels
    lngScaleHeight = ScaleHeight
    lngScaleWidth = ScaleWidth
    DrawStyle = vbInvisible
    FillStyle = vbFSSolid
    For lngY = 0 To lngScaleHeight
        FillColor = RGB(0, 0, 255 - (lngY * 255) \ lngScaleHeight)
        Line (-1, lngY - 1)-(lngScaleWidth, lngY + 1), , B
    Next lngY
End Sub

不管表單的大小,以上這個程序都能以藍黑色的漸層效果填滿整張表單。如果要製造一個全螢幕的背景效果,請把表單的BorderStyle屬性設為"0-無",並把WindowStyle屬性設為"2-最大化"。

這裡仍然有很大的空間可以讓你隨心所欲地以這個程序作實驗。例如,FillColor的計算式可以被修改為產生紅色的漸層效果,或者,你可以把藍色放在下半部而黑色在上半部。圖14-1展示了這個漸層效果。


 

 圖14-1 從藍色到黑色漸層效果的表單

參考資料:

請參閱 第三十四章"進階應用程式" 的Dialogs應用程式,這個應用程式展示了一個漸層效果的螢幕。


如何產生一個橡皮筋式的選擇方框?
 

DrawFocusRect Windows API函式是產生一個橡皮筋式選擇方框的最佳工具,以下這段程式展示了它的用法。請建立一張新表單,把表單的AutoRedraw屬性設為True,然後加入下列的程式碼,執行程式。

程式執行時,如果按住滑鼠左鍵不放並且同時拖曳滑鼠,可以看到一個由虛線構成的選擇方框,這個方框會隨著滑鼠游標的移動而改變大小;一旦鬆開滑鼠鍵,原來的虛線就被固定的實線所替代,而這個實線方框所圍住的範圍就代表最後選擇的區域。

Option Explicit

Private Type RECT
    Left As Long
    Top As Long
    Right As Long
    Bottom As Long
End Type

Private Declare Function DrawFocusRect _
Lib "user32" ( _
    ByVal hdc As Long, _
    lpRect As RECT _
) As Long

Private FocusRec As RECT
Private sngX1 As Single
Private sngY1 As Single
Private sngX2 As Single
Private sngY2 As Single

Private Sub Form_Load()
    `Use units expected by the API function
    Me.ScaleMode = vbPixels
End Sub

Private Sub Form_MouseDown( _
    Button As Integer, _
    Shift As Integer, _
    X As Single, _
    Y As Single _
)
    `Be sure left mouse button is used
    If (Button And vbLeftButton) = 0 Then Exit Sub
    `Set starting corner of box
    sngX1 = X
    sngY1 = Y
End Sub

Private Sub Form_MouseMove( _
    Button As Integer, _
    Shift As Integer, _
    X As Single, _
    Y As Single _
)
    `Be sure left mouse button is pressed
    If (Button And vbLeftButton) = 0 Then Exit Sub
    `Erase focus rectangle if it exists
    If (sngX2 <> 0) Or (sngY2 <> 0) Then
        DrawFocusRect Me.hdc, FocusRec
    End If


    `Update coordinates
    sngX2 = X
    sngY2 = Y
    `Update rectangle
    FocusRec.Left = sngX1
    FocusRec.Top = sngY1
    FocusRec.Right = sngX2
    FocusRec.Bottom = sngY2
    `Adjust rectangle if reversed
    If sngY2 < sngY1 Then Swap FocusRec.Top, FocusRec.Bottom
    If sngX2 < sngX1 Then Swap FocusRec.Left, FocusRec.Right
    `Draw focus rectangle
    DrawFocusRect Me.hdc, FocusRec
    Refresh
End Sub

Private Sub Form_MouseUp( _
    Button As Integer, _
    Shift As Integer, _
    X As Single, _
    Y As Single _
)
    `Be sure left mouse button is pressed
    If (Button And vbLeftButton) = 0 Then Exit Sub
    `Erase focus rectangle if it exists
    If FocusRec.Right Or FocusRec.Bottom Then
        DrawFocusRect Me.hdc, FocusRec
    End If
    `Draw indicated rectangle in red
    Line (sngX1, sngY1)-(sngX2, sngY2), vbRed, B
    `Zero the rectangle coordinates
    sngX1 = 0
    sngY1 = 0
    sngX2 = 0
    sngY2 = 0
End Sub

Private Sub Swap(vntA As Variant, vntB As Variant)
    Dim vntT As Variant
    vntT = vntA
    vntA = vntB
    vntB = vntT
End Sub

圖14-2所顯示的是執行中的程式。

DrawFocusRect Windows API函式繪出一個虛線選擇方框,當滑鼠被拖曳時,程式在每次繪出虛線方格之前,必須把前一次的方格擦掉。幸好,DrawFocusRect API函式內建的Xor動作使得這個工作容易多了。如果你用同樣的座標值呼叫這個函式兩次,第二次的函式呼叫會把第一次呼叫函式時所畫的方格擦掉。在Form_MouseMove事件程序中,你可以看到DrawFocusRect函式被呼叫了兩次,一次畫出方框,另一次擦掉方框。


 

 圖14-2 互動的橡皮筋式選擇方框

Visual Basic中滑鼠的動作被分為三個事件:鬆開按鍵、按下按鍵和移動游標,這個範例程式使用了這三個事件。MouseDown所指的是方框的起點,MouseMove代表方框正在改變大小,而MouseUp則表示選擇的動作已經完成。

在本例中,以滑鼠選擇方框只能在表單上執行,在其他物件上則不行,即使是表單內的控制項亦然。然而,你也可以把同樣的程式碼放進圖片方塊控制項中,在圖片方塊控制項上畫出選擇方框。

如何產生圖形焦點?
 

影像控制項(Image Control)是在圖形上產生焦點(Hot Spots)最有效率的工具。讓我們以一個實例來介紹所有的步驟。

我們首先把一個圖像檔WORLD.BMP載入一個影像控制項imgWorld裡,然後在這張圖上放置另外四個影像控制項,取名imgNAmerica、imgSAmerica、imgEurope以及imgAferica,每個方形蓋住地圖上相對的地理區域,然後加入一個標籤控制項lblHotSpot到表單中。


 

 圖14-3 在設計階段的焦點

以下即是本範例的所有程式碼,當每個焦點被點選時,標籤上就會顯示出相關的資訊。

Option Explicit

Private Sub imgAfrica_Click()
    lblHotSpots.Caption = "Africa"
End Sub

Private Sub imgEurope_Click()
    lblHotSpots.Caption = "Europe"
End Sub

Private Sub imgEurope_Click()
    lblHotSpots.Caption = "North America"
End Sub

Private Sub imgEurope_Click()
    lblHotSpots.Caption = "South America"
End Sub

Private Sub imgEurope_Click()
    lblHotSpots.Caption = "The World"
End Sub

圖14-4 顯示當"Africa"焦點被點選的結果。


 

 圖14-4 由滑鼠按鍵事件啟動一個圖形焦點

Image控制項在設計階段可以被看見,但在執行階段則無法看得見。因為這些代表焦點的影像控制項被放置在顯示著世界地圖的影像控制項上面,滑鼠事件只會作用在這些焦點控制項上。

為了達到更好的互動效果,我們把每一個Image控制項的ToolTips屬性都填入了適當的大陸名稱,這樣可以給使用者更好的視覺輔助。


參考資料:

請參閱 第三十一章"日期與時間" 中的VBClock應用程式,這個應用程式也展示了圖形焦點的功能。


如何快速地繪出多邊形?
 

你可以用Line方法畫出幾個線段,讓這些線段連成一個多邊形。但是,使用Polygon Windows API函式是一個更快的方式,而且這個函式可以讓多邊形內的區域被填滿指定的顏色。

下面這個Polygon物件類別把Polygon函式及其所需用到的資料型別封裝在物件內,大幅地簡化了描繪多邊形的工作。

`POLYGON.CLS
Option Explicit

Private Type POINTAPI
    x As Long
    y As Long
End Type

Private Declare Function Polygon _
Lib "gdi32" ( _
    ByVal hdc As Long, _
    lpPoint As POINTAPI, _
    ByVal nCount As Long _
) As Long

`Module-level private variables
Private mobjDevice As Object
Private msngSX1 As Single
Private msngSY1 As Single
Private msngXRatio As Single
Private msngYRatio As Single
Private maPointArray() As POINTAPI

`~~~Device
Property Set Device(objDevice As Object)
    Dim sngSX2 As Single
    Dim sngSY2 As Single
    Dim sngPX2 As Single
    Dim sngPY2 As Single
    Dim intScaleMode As Integer
    Set mobjDevice = objDevice
    With mobjDevice
        `Grab current scaling parameters
        intScaleMode = .ScaleMode
        msngSX1 = .ScaleLeft
        msngSY1 = .ScaleTop
        sngSX2 = msngSX1 + .ScaleWidth
        sngSY2 = msngSY1 + .ScaleHeight
        `Temporarily set pixels mode
        .ScaleMode = vbPixels
        `Grab pixel scaling parameters
        sngPX2 = .ScaleWidth
        sngPY2 = .ScaleHeight
        `Reset user's original scale
        If intScaleMode = 0 Then
            mobjDevice.Scale (msngSX1, msngSY1)-(sngSX2, sngSY2)
        Else
            mobjDevice.ScaleMode = intScaleMode
        End If
        `Calculate scaling ratios just once
        msngXRatio = sngPX2 / (sngSX2 - msngSX1)
        msngYRatio = sngPY2 / (sngSY2 - msngSY1)
    End With
End Property

`~~~Point X,Y
Public Sub Point(sngX As Single, sngY As Single)
    Dim lngN As Long
    lngN = UBound(maPointArray) + 1
    ReDim Preserve maPointArray(lngN)
    maPointArray(lngN).x = XtoP(sngX)
    maPointArray(lngN).y = YtoP(sngY)
End Sub

`~~~Draw
Public Sub Draw()
    Polygon mobjDevice.hdc, maPointArray(1), UBound(maPointArray)
    ReDim maPointArray(0)
End Sub

`Scales X value to pixel location
Private Function XtoP(sngX As Single) As Long
    XtoP = (sngX - msngSX1) * msngXRatio
End Function

`Scales Y value to pixel location
Private Function YtoP(sngY As Single) As Long
    YtoP = (sngY - msngSY1) * msngYRatio
End Function
`Initialization
Private Sub Class_Initialize()
    ReDim maPointArray(0)
End Sub

Visual Basic容許使用者用不同的衡量單位來為圖形輸出設備規劃尺度,但Visual Basic在呼叫任何畫線或畫圓等繪圖函式之前,它會自動地將使用者選定的衡量單位一律轉換為像素,這是Visual Basic在處理圖形時暗中所做的動作,程式設計師看不到這個功能。由於Polygon Windows API函式所期待的參數只能以像素作為衡量單位,因此,為了使Polygon物件也能允許使用者使用不同的衡量單位,我們在Polygon物件中借用了Visual Basic處理繪圖函式時所運用的概念。Polygon物件在內部也和Visual Basic一樣,將使用者選定的衡量單位轉換為像素。

我們在Property Set Device屬性程序中計算出單位轉換的比例值(像素 / 使用者選定的單位),然後依據這個比例值將Point方法所收到的座標值X和Y轉換為以像素為單位的座標值。在這部分要注意的是:只要輸出設備的尺度單位有所改變,就應立刻重新設定Device屬性。

在Polygon物件中執行繪圖動作時,我們不需要做額外的設定動作,因為Polygon API函式會自動使用輸出設備的現有設定值。也就是說,你可以用Ploygon物件的FillStyle和FillColor屬性,很容易地畫出各種形狀的多邊形,裡面填滿有顏色的對角線。

以下這段程式展示了如何在應用程式中使用Polygon物件。在本例中,我們畫了一個17個端點的多邊形,然後在裡面填滿隨機選取的顏色。

若要執行本範例,請在一張表單中加入一個圖形方塊控制項picTest,然後加入以下的程式碼:

`POLYTEST.FRM
Option Explicit

Dim polyTest As New Polygon

Private Sub Form_Load()
    `Create unique polygon each time
    Randomize

    `Use any desired units and graphics settings
    With picTest
        .Move 0, 0, ScaleWidth, ScaleHeight
        .ScaleMode = vbInches
        .FillStyle = vbSolid
    End With
End Sub
Private Sub picTest_Click()
    Dim intI As Integer
    `Connect picture box as polygon output device
    Set polyTest.Device = picTest
    With picTest
        `Clear output with each click
        .Cls
        `Build 17-point random polygon
        For intI = 1 To 17
            polyTest.Point Rnd * .ScaleWidth, Rnd * .ScaleHeight
        Next intI
        `Create unique fill color each time
        .FillColor = RGB(Rnd * 256, Rnd * 256, Rnd * 256)
        `Draw polygon, filling the interior
        polyTest.Draw
    End With
End Sub

圖14-5所顯示的是一個以Polygon物件畫出的17邊多邊形。


 

 圖14-5 以Polygon物件繪出多邊形

如何畫出橢圓形?
 

Visual Basic的Circle方法可以畫出橢圓形,但在許多情況下,以橢圓形外圍的長方形為依據來畫出橢圓會比較方便。用Circle方法必須要計算出中心點、半徑和長寬比,反而比較複雜而且容易算錯。

這裡有一個好方法讓你在長方形中畫出一個橢圓:Ellipse Windows API函式。我們把Ellipse API函式封裝在Ellipse物件類別中,讓你在應用程式中可以很容易地以長方形來繪出橢圓。

`ELLIPSE.CLS
Option Explicit

Private Declare Function Ellipse _
Lib "gdi32" ( _
    ByVal hdc As Long, _
    ByVal X1 As Long, _
    ByVal Y1 As Long, _
    ByVal X2 As Long, _
    ByVal Y2 As Long _
) As Long

`Module-level private variables
Private mobjDevice As Object
Private msngSX1 As Single
Private msngSY1 As Single
Private msngXRatio As Single
Private msngYRatio As Single


`~~~Device
Property Set Device(objDevice As Object)
    Dim sngSX2 As Single
    Dim sngSY2 As Single
    Dim sngPX2 As Single
    Dim sngPY2 As Single
    Dim sngScaleMode As Integer
    Set mobjDevice = objDevice
    With mobjDevice
        `Grab current scaling parameters
        sngScaleMode = .ScaleMode
        msngSX1 = .ScaleLeft
        msngSY1 = .ScaleTop
        sngSX2 = msngSX1 + .ScaleWidth
        sngSY2 = msngSY1 + .ScaleHeight
        `Temporarily set pixels mode
        .ScaleMode = vbPixels
        `Grab pixel scaling parameters
        sngPX2 = .ScaleWidth
        sngPY2 = .ScaleHeight
        `Reset user's original scale
        If sngScaleMode = 0 Then
            mobjDevice.Scale (msngSX1, msngSY1)-(sngSX2, sngSY2)
        Else
            mobjDevice.ScaleMode = sngScaleMode
        End If
        `Calculate scaling ratios just once
        msngXRatio = sngPX2 / (sngSX2 - msngSX1)
        msngYRatio = sngPY2 / (sngSY2 - msngSY1)
    End With
End Property

`~~~Draw X1,Y1,X2,Y2
Public Sub Draw( _
    sngX1 As Single, _
    sngY1 As Single, _
    sngX2 As Single, _
    sngY2 As Single _
)
    Ellipse mobjDevice.hdc, XtoP(sngX1), YtoP(sngY1), _
        XtoP(sngX2), YtoP(sngY2)
End Sub

`Scales X value to pixel location
Private Function XtoP(sngX As Single) As Long
    XtoP = (sngX - msngSX1) * msngXRatio
End Function
`Scales Y value to pixel location
Private Function YtoP(sngY As Single) As Long
    YtoP = (sngY - msngSY1) * msngYRatio
End Function

Ellipse物件類別模組中有很多程式碼和Polygon物件類別中的程式碼相同,例如,它們的Device屬性都執行相同的尺度單位轉換工作。你可以把Ellipse物件類別模組或Polygon物件類別模組當作範本,然後用這個範本為基礎,加上一些其他函式,建立其他的繪圖物件;甚至可以把所有的繪圖函式都封裝在一個Graphic物件類別裡,那麼這個Graphic物件類別就會有一個Device屬性,以及好幾個繪圖方法,如Polygon和Ellipse。

以下這段程式展示了Ellipse物件的用法。我們畫了三個套在一起的橢圓,這如果用Circle方法將會很難辦到,因為必須計算出準確的長寬比。

若要測試Ellipse物件,請在一張新表單上加入一個圖片方塊控制項picTest,然後加入下列的程式碼:

`ELLITEST
Option Explicit

Dim ellipseTest As New Ellipse

Private Sub picTest_Click()
    picTest.ScaleMode = vbCentimeters
    Set ellipseTest.Device = picTest
    ellipseTest.Draw 1, 1, 7, 4
    ellipseTest.Draw 2, 1, 6, 4
    ellipseTest.Draw 2, 2, 6, 3
End Sub

圖14-6所顯示的是三個套疊的橢圓。


 

 圖14-6 以長方形座標所畫出的橢圓

如何在一個不規則狀的區域中填滿顏色?
 

如果你已經知道某塊區域各個端點的座標值,那麼便可以用前面提到的PolygonWindows API函式將這塊區域填滿顏色。然而,即使只知道這個區域的邊線顏色,不知道端點座標,還是可以用這個資訊將這塊區域填滿顏色。只要知道這塊區域邊緣的顏色,就可以以使用FloodFill Windows API函式將這塊區域著色。FloodFill函式模仿在早期Basic中的Paint指令,它可以把一塊由顏色邊線圍成的區域填滿顏色。

以下的Paint物件類別模組展示了FloodFill API函式的用法,我們把一個由方形和圓形交集所圍成的區域以FloodFill API函式來填滿顏色:

`PAINT.CLS
Option Explicit



Private Declare Function FloodFill _
Lib "gdi32" ( _
    ByVal hdc As Long, _
    ByVal x As Long, _
    ByVal y As Long, _
    ByVal crColor As Long _
) As Long

`Module-level private variables
Private mobjDevice As Object
Private msngSX1 As Single
Private msngSY1 As Single
Private msngXRatio As Single
Private msngYRatio As Single

Property Set Device(objDevice As Object)
    Dim sngSX2 As Single
    Dim sngSY2 As Single
    Dim sngPX2 As Single
    Dim sngPY2 As Single
    Dim nScaleMode As Integer
    Set mobjDevice = objDevice
    With mobjDevice
        `Grab current scaling parameters
        nScaleMode = .ScaleMode
        msngSX1 = .ScaleLeft
        msngSY1 = .ScaleTop
        sngSX2 = msngSX1 + .ScaleWidth
        sngSY2 = msngSY1 + .ScaleHeight
        `Temporarily set pixels mode
        .ScaleMode = vbPixels
        `Grab pixel scaling parameters
        sngPX2 = .ScaleWidth
        sngPY2 = .ScaleHeight
        `Reset user's original scale
        If nScaleMode = 0 Then
            mobjDevice.Scale (msngSX1, msngSY1)-(sngSX2, sngSY2)
        Else
            mobjDevice.ScaleMode = nScaleMode
        End If
        `Calculate scaling ratios just once
        msngXRatio = sngPX2 / (sngSX2 - msngSX1)
        msngYRatio = sngPY2 / (sngSY2 - msngSY1)
    End With
End Property

`~~~Flood x,y
Public Sub Flood(sngX As Single, sngY As Single)
    FloodFill mobjDevice.hdc, XtoP(sngX), YtoP(sngY), _
        mobjDevice.ForeColor
End Sub

`Scales X value to pixel location
Private Function XtoP(sngX As Single) As Long
    XtoP = (sngX - msngSX1) * msngXRatio
End Function

`Scales Y value to pixel location
Private Function YtoP(sngY As Single) As Long
    YtoP = (sngY - msngSY1) * msngYRatio
End Function

以下這段程式用紅色線條畫了一個正方形和圓形,然後把正方形和圓形交集的區域填滿綠色:

Option Explicit

Dim paintTest As New Paint

Private Sub picTest_Click()
    picTest.ScaleMode = vbInches
    `Draw overlapping box and circle in red
    picTest.Line (0.5, 0.5)-(2, 2), vbRed, B
    picTest.Circle (2, 1), 0.7, vbRed
    `Prepare to paint the overlapping area
    picTest.FillStyle = vbFSSolid    `Paint style
    picTest.FillColor = vbGreen      `Paint color
    picTest.ForeColor = vbRed        `Paint boundary color
    Set paintTest.Device = picTest
    paintTest.Flood 1.7, 0.9
    `Reset fill style to default
    picTest.FillStyle = vbFSTransparent
End Sub

有顏色的邊線是由ForeColor屬性定義的,而填入的顏色則由FillStyle和FillColor屬性所定義。填顏色的動作從一個被指定的座標位置開始,把所有的像素以被指定的顏色和樣式予以填滿,一直填到由ForeColor所定義的邊界才停止。這種方法可以有效率地為不規則的區域填滿顏色,尤其是圓形與方形重疊的部分。

若要執行這個範例,請在表單中加入一個圖形方塊控制項picTest,並加入前面的程式碼。圖14-7顯示程式執行的結果。


 

 圖14-7 以Paint物件為一塊不規則區域填滿顏色

如何旋轉一張點陣圖?
 

32位元的PlgBlt函式的主要目的是協助程式設計師改變圖案的形狀,雖然Windows 95的技術文件中已有了這個函式的說明,但它尚未建置在Windows 95上。當筆者嘗試去呼叫PlgBlt函式時,程式並無任何結果產生,也就是說,PlgBlt函式目前仍然只是個空函式。

以下這段程式可以使一個圖形依任意指定的角度旋轉,同時也把這個圖形逐一地按照每個像素,從一個圖片方塊複製到另一個圖片方塊中。這個程式跑起來很慢,因此你可以把它當作自己平時產生圖形的工具,不要把它放在交給使用者的應用程式中。

Point方法和Pset方法是用來讀寫像素的,請記得在使用Point和PSet方法之前把Scale Mode屬性設為vbPixels。

如果要測試這個技巧,請新增一個專案,在表單中加入兩個Picture控制項,picOne和picTwo,以及加入一個指令按鈕控制項cmdRotate。接下來,把一個點陣圖的檔名指定給picOne的Picture屬性,並調整picOne的大小以便能完整地顯示載入的點陣圖。在這張圖中,只有顯示在圖片方塊中的部分才會被複製下來。最後,調整picTwo的大小,加入以下的程式碼到表單中。

執行這個程式時,按下指令按鈕,程式就會使圖形旋轉並且複製到另一個圖片方塊中。這段程式將點陣圖旋轉了45度︰

Option Explicit

Const PI = 3.14159265358979
Const ANGLE = 45

Private Sub cmdRotate_Click()
    Dim intX As Integer
    Dim intY As Integer
    Dim intX1 As Integer
    Dim intY1 As Integer
    Dim dblX2 As Double
    Dim dblY2 As Double
    Dim dblX3 As Double
    Dim dblY3 As Double
    Dim dblThetaDeg As Double
    Dim dblThetaRad As Double
    `Initialize rotation angle
    dblThetaDeg = ANGLE
    `Compute angle in radians
    dblThetaRad = dblThetaDeg * PI / 180
    `Set scale modes to pixels
    picOne.ScaleMode = vbPixels
    picTwo.ScaleMode = vbPixels
    For intX = 0 To picTwo.ScaleWidth
        intX1 = intX - picTwo.ScaleWidth \ 2
        For intY = 0 To picTwo.ScaleHeight
            intY1 = intY - picTwo.ScaleHeight \ 2
            `Rotate picture by dblThetaRad
            dblX2 = intX1 * Cos(-dblThetaRad) + _
                intY1 * Sin(-dblThetaRad)
            dblY2 = intY1 * Cos(-dblThetaRad) - _
                intX1 * Sin(-dblThetaRad)
            `Translate to center of picture box
            dblX3 = dblX2 + picOne.ScaleWidth \ 2
            dblY3 = dblY2 + picOne.ScaleHeight \ 2
            `If data point is in picOne, set its color in picTwo
            If dblX3 > 0 And dblX3 < picOne.ScaleWidth - 1 _
                And dblY3 > 0 And dblY3 < picOne.ScaleHeight - 1 Then
                picTwo.PSet (intX, intY), picOne.Point(dblX3, dblY3)
            End If
        Next intY
    Next intX
End Sub

你可以用SavePicture陳述式來儲存旋轉後的點陣圖;如果要這麼做,請記得把picTwo的AutoRedraw屬性設為True。以下這行陳述式告訴你如何使用SavePicture:

SavePicture picTwo.Image, "C:\FINISHROT.BMP"

圖14-8所顯示的是設計階段的表單,圖14-9顯示的則是旋轉了45度的圖形。


 

 圖14-8 旋轉前的點陣圖


 

 圖14-9 旋轉45度後的點陣圖

如何捲動圖形影像?
 

Visual Basic的PaintPicture方法簡化了許多處理圖形的技巧,在下面的範例中,我們用PaintPicture方法將一張較大的點陣圖放在一個具有捲軸的小視窗裡,讓使用者可以捲動這張圖。

如圖14-10所示,整張圖形完全顯示在一個較大的圖片方塊中,而小視窗裡有另一張複製的圖。


注意:

左邊那張圖能否被使用者看見無關乎程式能不能執行。在設計階段,你可以載入這張圖到圖片方塊中,然後把Visible屬性設為False,小的圖片方塊中仍然可以顯示出這張圖。


若要測試這個程式,請在表單中加入兩個大小不同的圖片方塊,把大的圖片方塊取名為picOne,小的取名為picTwo。然後在表單中加入一個垂直捲軸vsbScroll和一個水平捲軸hsbScroll;讓這兩個捲軸緊臨著picTwo,如圖14-10,因為它們要被用來控制picTwo中的內容。

現在請載入一張點陣圖到picOne裡,調整picOne的大小。

在這張點陣圖中,只有在picOne中可被顯示的部分才會在picTwo中顯示。這個程式以PaintPicture方法複製picOne中的一塊長方形區域,然後畫在picTwo中,造成圖形捲動的效果。


 

 圖14-10 在較小的視窗中捲動一個較大的圖形

最後,請加入下列的程式碼到表單中。

請注意,這個程式在捲軸的Scroll事件中驅動捲軸的Change事件,這讓你可以拖曳捲軸滑鈕並且平順地捲動圖形。

Option Explicit

Private Sub Form_Load()
    hsbScroll.Max = picOne.ScaleWidth - picTwo.ScaleWidth
    hsbScroll.LargeChange = hsbScroll.Max \ 10
    hsbScroll.SmallChange = hsbScroll.Max \ 25
    vsbScroll.Max = picOne.ScaleHeight - picTwo.ScaleHeight
    vsbScroll.LargeChange = vsbScroll.Max \ 10
    vsbScroll.SmallChange = vsbScroll.Max \ 25
End Sub

Private Sub hsbScroll_Change()
    UpdatePicTwo
End Sub

Private Sub hsbScroll_Scroll()
    hsbScroll_Change
End Sub

Private Sub vsbScroll_Change()
    UpdatePicTwo
End Sub

Private Sub vsbScroll_Scroll()
    vsbScroll_Change
End Sub

Private Sub UpdatePicTwo()
    picTwo.PaintPicture picOne.Picture, 0, 0, _
        picTwo.ScaleWidth, picTwo.ScaleHeight, _
        hsbScroll.Value, vsbScroll.Value, _
        picTwo.ScaleWidth, picTwo.ScaleHeight, _
        vbSrcCopy
End Sub

如何用BitBlt函式產生動畫?
 

要想在螢幕上四處移動一個不規則形狀的圖形而又不致影響到背景,Visual Basic的PaintPicture方法是一個方便又快速的好方法。Visual Basic中的PaintPicture方法就等於Windows所提供的BitBlt API函式(事實上,PaintPicture在執行了錯誤檢查和尺度單位轉換後,它接著就呼叫了BitBlt函式),但PaintPicture唯一的缺點就是速度較慢。如果直接呼叫BitBlt函式,就可以省下PaintPicture在呼叫BitBlt之前所做的額外動作。用BitBlt可以產生快速而且動作平順的動畫效果,這就是為什麼我們要使用BitBlt函式來產生動畫的原因。

BitBlt函式可以快速地從一個圖片方塊或表單上移動一整塊(長方形)的像素到另一個圖片方塊、表單或印表機物件上。在圖形的來源端或是目的端,BitBlt函式都需要一個hDC (設備識別代碼)才能進行圖形的搬移。圖片方塊控制項、表單和印表機物件都有hDC屬性。然而,請注意,當圖片方塊控制項的AutoRedraw屬性被設為True時,hDC屬性所產生影響的不是圖片方塊控制項的Picture屬性,而是Image屬性。

Image屬性所引用的影像是一份存放在記憶體中、看不見的影像,這份看不見的影像對映著我們在螢幕看到的圖片。如果AutoRedraw屬性被設為True,那麼當你以BitBlt函式在處理圖片方塊控制項的內容時,這個看不見的影像才是BitBlt函式真正的處理對象。只有在系統重新載入(Refresh)圖片方塊控制項時,這份背後的影像才會被移轉到看得見的畫面上。

這個功能剛好符合了範例程式的需求,避免了螢幕閃動和其他的問題。

以下這段程式製造了一個幽浮動畫的效果。程式中需要三個圖形方塊才能製造出動畫的效果:第一個用來存放幽浮的影像,第二個存放幽浮影像的遮罩(Mask),第三個則存放使背景復原所需的影像。

`BITBLT
Option Explicit

Private Declare Function BitBlt _
Lib "gdi32" ( _
    ByVal hDestDC As Long, _
    ByVal x As Long, ByVal y As Long, _
    ByVal nWidth As Long, ByVal nHeight As Long, _
    ByVal hSrcDC As Long, _
    ByVal xSrc As Long, ByVal ySrc As Long, _
    ByVal dwRop As Long _
) As Long

 Private Sub cmdAnimate_Click()
    Static lngX As Long
    Static lngY As Long
    Static lngW As Long
    Static lngH As Long
    Static blnBackSaved As Boolean
    Dim lngRtn As Long
    `Display hourglass pointer while busy
    Screen.MousePointer = vbHourglass
    `Provide starting location
    lngX = -picUfo.ScaleWidth
    lngY = picClouds.ScaleHeight
    `Save sizes in local variables once for speed
    lngW = picUfo.ScaleWidth
    lngH = picUfo.ScaleHeight
    `Loop to animate the UFO
    Do
        `Restore background unless this is first time object is drawn
        If blnBackSaved = True Then
            lngRtn = BitBlt(picClouds.hDC, lngX, lngY, lngW, lngH, _
                picBack.hDC, 0, 0, vbSrcCopy)
            `Stop UFO's motion when it gets to the edges
            If lngX > picClouds.ScaleWidth Then
                blnBackSaved = False
                
                picClouds.Refresh
                Exit Do
            End If
        End If
        `Move UFO to a new location
        lngX = lngX + 1
            If lngX < 0.5 * picClouds.ScaleWidth _
                Or lngX > 0.8 * picClouds.ScaleWidth Then
                lngY = lngY - 1
        Else
            lngY = lngY + 1
        End If
        `Save background at new location
        lngRtn = BitBlt(picBack.hDC, 0, 0, lngW, lngH, _
            picClouds.hDC, lngX, lngY, vbSrcCopy)
        blnBackSaved = True
        `Apply mask
        lngRtn = BitBlt(picClouds.hDC, lngX, lngY, lngW, lngH, _
            picUfoMask.hDC, 0, 0, vbSrcAnd)
        `Draw UFO
        lngRtn = BitBlt(picClouds.hDC, lngX, lngY, lngW, lngH, _
            picUfo.hDC, 0, 0, vbSrcPaint)
        picClouds.Refresh
    Loop
    `Restore pointer
    Screen.MousePointer = vbDefault
End Sub

如果要測試這個程式,請新增一個專案,在表單中加入指令按鈕控制項cmdAnimate,以及四個圖片方塊控制項picClouds、picUfo、picUfoMask和picBack。現在你需要三張點陣圖:一張較大的點陣圖當背景(我們用CLOUD.BMP,它在Windows 95的安裝光碟裡),一張幽浮影像,以及一張幽浮影像的遮罩(幽浮影像和遮罩可以用Windows的小畫家來產生)。把picClouds的Picture屬性設為CLOUD.BMP,並把它的AutoRedraw屬性設為True。接下來,把picUfo的Picture屬性設為幽浮影像的檔名,把picUfoMask的Picture屬性設為幽浮影像遮罩的檔名,picBack的Picture屬性則仍然設為幽浮影像的檔名。設定完成後,調整這三個控制項的大小,使它們的尺寸一致;接著再把這三個控制項擺到表單上方,讓你可以了解程式執行的動作。

把以上所有圖片方塊控制項的AutoSize屬性設為True,ScaleMode屬性設為"3-像素"。圖14-11所顯示的是設計階段的表單。

在圖形編輯軟體(如Windows 95小畫家)裡繪製幽浮影像和幽浮影像遮罩這兩張點陣圖時,必須遵守一些限制,才能使動畫效果顯現。在幽浮影像點陣圖中,幽浮的形狀必須用黑色以外的顏色繪製,而幽浮形狀的外圍(或者說是背景)必須是黑色。另一方面,幽浮影像遮罩必須是幽浮影像的負像(Negative Image),也就是說,只要是幽浮影像點陣圖中的黑色部分,在遮罩點陣圖中的相對位置上就必須是白色,而幽浮影像點陣圖中的彩色(非黑色)部分,在遮罩點陣圖中的相對位置上就必須是黑色。

這些點陣圖是如何一起運作才會製造出動畫效果呢?

在解釋幽浮飛行路徑上畫出第一個幽浮之前,我們先從處理動畫的第一格畫面開始談起。我們先把第一個位置上會被幽浮影像遮住的部分背景先儲存起來,放在picBack中備用。接著,用BitBlt函式將幽浮影像遮罩畫在第一個位置上,同時也用BitBlt函式讓每一個像素一一作Boolean And運算── BitBlt函式的最後一個參數決定這個運算。這個And運算的結果是遮罩上黑色的部分保持黑色,而遮罩白色的部分則讓背景透出。


 

 圖14-11 在設計階段的BitBlt動畫範例

下一步,我們用BitBlt函式讓幽浮影像(存放在picUfo控制項中)畫在剛才的位置上,但這次BitBlt函式執行的是一個Or位元運算。在這個位置上,剛剛被遮罩塗黑的部分現在被畫上了彩色的幽浮形狀,但幽浮影像在點陣圖中的黑色部分則不會對背景造成影響(因為作了Or位元運算)。至此,我們就完成了動畫中的"第一格"動作。

接下來,我們把幽浮往下一個位置移動,也就是說我們要在"第二格"畫上幽浮。在這裡,我們只要多做一個動作──用先前被保留下來的一小塊背景(在放在picBack中)蓋掉前一個位置上的幽浮影像。接下來的動作就和前面第一格的動作一樣。

這幾個連續的步驟被放在一個迴圈中反覆執行,製造出幽浮由左至右飛過天空的效果,如圖14-12所示。

如果在使用影像遮罩時遇到困難,問題可能是出在你的顯示卡上,這時不妨換塊顯示卡試試看。


參考資料:

請參閱 第二十九章"圖形" 中的動畫示範



 

 圖14-12 幽浮圖形在背景上移動

如何以圖片物件製造動畫效果?
 

從Visual Basic 4開始,Visual Basic就提供了圖片物件(Picture Object)來加強使用者對圖形處理的能力。圖片物件是一個獨立的個體,你可以用圖片物件來載入和儲存影像,而不需透過圖片方塊控制項或影像控制項。當你想要存取影像時,可以很容易地把影像從圖片物件中複製到可以顯示影像的控制項裡。


注意:

除了圖片物件以外,Visual Basic另外提供了ImageList控制項讓你從一個資源檔中載入多個影像。使用圖片物件和ImageList控制項可以大幅增進影像顯示的速度。


在這裡,我們提供一個範例程式讓你實驗圖片物件的用法。這個程式建立了一個圖片物件的陣列,然後把一系列的影像一一存放到這些圖片物件中。程式中運用了一個計時器控制項tmrAnimate,它的Interval屬性被設為1微秒,這是程式將影像物件陣列的影像一一播放在picTest圖片方塊控制項上的速度。這個程式執行的結果是一個平順而且不閃動的動畫。

Option Explicit

Const NUMFRAMES = 15
Dim picArray(1 To NUMFRAMES) As Picture

Private Sub Form_Load()
    Dim strFile As String
    Dim intI As Integer
    For intI = 1 To NUMFRAMES
        strFile = App.Path & "\GLOBE" & _
            Format(intI) & ".BMP"
        Set picArray(intI) = LoadPicture(strFile)
    Next intI
End Sub

Private Sub tmrAnimate_Timer()
    Static intN As Integer
    intN = (intN Mod NUMFRAMES) + 1
    picTest.Picture = picArray(intN)
End Sub

我們把AniGlobe應用程式(請看 第二十九章"圖形" )加以修改,產生15個地球影像,再把這些影像依序儲存為GLOBE1.BMP、GLOBE2.BMP、GLOBE3.BMP...... 依此類推,這15個影像被一一放進一個含有15個圖片物件的陣列中,等待循序地被複製到一個圖片方塊控制項裡。圖片方塊控制項picTest的AutoSize屬性應該被設為True,這樣所顯現的影像才會和picTest的大小一致。圖14-13所顯示的是動畫的一個定格畫面。


 

 圖14-13 用圖片物件陣列所產生的動畫

如何使用動畫控制項?
 

Windows 95提供了許多有趣的小動畫,如清理資源回收筒、檔案複製、檔案搜尋以及其他的系統動畫,Visual Basic的動畫控制項(Animation Control)現在讓你可以很容易地在應用程式中加入這些動畫。以下這段程式將會載入並且執行兩個動畫:檔案搜尋──手電筒來回地照射檔案夾,以及清理資源回收筒──垃圾從資源回收筒中飛出後消失。

Option Explicit

Private Sub Form_Load()
    anmOne.Open App.Path & "\SEARCH.AVI"
    anmOne.Play
    anmTwo.Open App.Path & "\FILENUKE.AVI"
    anmTwo.Play
End Sub

如果要測試這個程式,請在一張新表單上加入兩個動畫控制項,取名為anmOne和anmTwo。動畫控制項包含在Windows Common Controls-2 6.0 (MSCOMCT2.OCX) 裡。

我們用Open方法載入動畫檔,然後用Play方法(不使用任何參數)播放動畫,動畫會持續地播放,直到表單被關閉。圖14-14顯示這兩個動畫執行的情形。


 

 圖14-14 用動畫控制項播放系統動畫

這些動畫在它們自己的執行緒(Thread)中執行,這表示程式可以進行其他的處理程序而不受影響。

並非所有的AVI檔都能用動畫控制項來播放,能在動畫控制項上播放的AVI檔必須沒有音效,如果播放有音效的AVI檔,你會收到錯誤訊息。

如何將文字定位在圖片方塊中的特定位置上?
 

圖片方塊控制項有一個優於文字方塊控制項或標籤控制項的優點,就是它可以容許你把文字準確地擺在某個位置上,而且可以使用不同的字型與顏色,甚至可以和圖形混合在一起。

你可以用圖片方塊控制項的標準屬性來改變文字的字型;若要設定文字在圖片方塊中的位置,可以把ScaleWidth和ScaleHeight屬性以及TextWidth和TextHeight方法混合併用。

以下這段程式展示如何在圖片方塊的正中央及右下角各印出一個字串。若要執行這個程式,請在一張新表單上加入一個圖片方塊控制項picTest,然後加入下面的程式:

Option Explicit

Private Sub Form_Resize()
    Dim intX As Integer
    Dim intY As Integer
    Dim strA As String
    `Reposition the picture box
    picTest.Move 0, 0, ScaleWidth, ScaleHeight
    `Erase previous contents of picture box
    picTest.Cls
    `Determine center of picture box
    intX = picTest.ScaleWidth \ 2
    intY = picTest.ScaleHeight \ 2
    `Draw circle at center for reference
    picTest.Circle (intX, intY), 700
    `Print string centered in picture box
    strA = "CENTER"
    picTest.CurrentX = intX - picTest.TextWidth(strA) \ 2
    picTest.CurrentY = intY - picTest.TextHeight(strA) \ 2
    picTest.Print strA
    `Determine lower-right corner of picture box
    intX = picTest.ScaleWidth
    intY = picTest.ScaleHeight
    `Print string at lower-right corner
    strA = "Lower-right cornerDear John, How Do I... "
    picTest.CurrentX = intX - picTest.TextWidth(strA)
    picTest.CurrentY = intY - picTest.TextHeight(strA)
    picTest.Print strA
End Sub

TextWidth方法會傳回整個字串的有效長度,這個有效長度是依字型、字串內的字元數和字元間的距離等因素合併計算的。因此,你必須在設定字型之後、印出字串之前傳遞字串給TextWidth方法。

圖14-15顯示了這個程式執行的結果。


 

 圖14-15 在圖片方塊中的確切位置上印出字串

如何無限制地調整字型大小?
 

即使系統上安裝了"可以無限制地調整字型大小"的TrueType字型,早期的Visual Basic也只能讓我們使用一組大小固定的字型。現在Visual Basic在圖形輸出設備上提供了Font物件,利用設定Font物件的Size屬性,我們可以無限制地調整TrueType字型或PostScript字型的大小。

下面這段程式介紹了Font物件的用法。請把空白表單的Font屬性設為TrueType字型,然後加入下面的程式:

Option Explicit

Private Sub Form_Click()
    Dim sngSize As Single
    sngSize = 1
    Do
        sngSize = sngSize * 1.2
        Me.Font = "Garamond"
        Me.Font.Size = sngSize
        Me.Print "Garamond - "; sngSize
    Loop Until sngSize > 100!
End Sub

圖14-16顯示了程式執行的結果。


 

 圖14-16 當表單被點選時,由小而大的Garamond TrueType字型會顯示在表單上

如何以任意角度旋轉文字?
 

Visual Basic本身不容許直接旋轉文字,但是藉由幾個Windows API函式的輔助,我們還是可以旋轉文字。下面這個Rotator物件類別封裝了五個API函式、一個LOGFONT資料型別和幾個必要的常數在它的物件中,讓我們可以在螢幕上或報表上輸出旋轉的文字。

`ROTATOR.CLS
Option Explicit

`API constants
Private Const LF_FACESIZE = 32
Private Const LOGPIXELSY = 90

Private Type LOGFONT
    lfHeight As Long
    lfWidth As Long
    lfEscapement As Long
    lfOrientation As Long
    lfWeight As Long
    lfItalic As Byte
    lfUnderline As Byte
    lsngStrikeOut As Byte
    lfCharSet As Byte
    lfOutPrecision As Byte
    lfClipPrecision As Byte
    lfQuality As Byte
    lsngPitchAndFamily As Byte
    lfFaceName(LF_FACESIZE - 1) As Byte
End Type

Private Declare Function SelectObject _
Lib "gdi32" ( _
    ByVal hdc As Long, _
    ByVal hObject As Long _
) As Long

Private Declare Function DeleteObject _
Lib "gdi32" ( _
    ByVal hObject As Long _
) As Long

Private Declare Function CreateFontIndirect _
Lib "gdi32" Alias "CreateFontIndirectA" ( _
    lpLogFont As LOGFONT _
) As Long

Private Declare Function TextOut _
Lib "gdi32" Alias "TextOutA" ( _
    ByVal hdc As Long, _
    ByVal x As Long, _
    ByVal y As Long, _
    ByVal lpString As String, _
    ByVal nCount As Long _
) As Long

Private Declare Function GetDeviceCaps _
Lib "gdi32" ( _
    ByVal hdc As Long, _
    ByVal intIndex As Long _
) As Long

`Module-level private variables
Private mobjDevice As Object
Private msngSX1 As Single
Private msngSY1 As Single
Private msngXRatio As Single
Private msngYRatio As Single
Private mlfFont As LOGFONT
Private mintAngle As Integer

`~~~Angle
Property Let Angle(intAngle As Integer)
    mintAngle = intAngle
End Property
Property Get Angle() As Integer
    Angle = mintAngle
End Property

`~~~Label
Public Sub Label(sText As String)
    Dim lngFont As Long
    Dim lngOldFont As Long
    Dim lngRes As Long
    Dim bytBuf() As Byte
    Dim intI As Integer
    Dim strFontName As String
    `Prepare font name, decoding from Unicode
    strFontName = mobjDevice.Font.Name
    bytBuf = StrConv(strFontName & Chr$(0), vbFromUnicode)
    For intI = 0 To UBound(bytBuf)
        mlfFont.lfFaceName(intI) = bytBuf(intI)
    Next intI
    `Convert known font size to required units
    mlfFont.lfHeight = mobjDevice.Font.Size * _
        GetDeviceCaps(mobjDevice.hdc, LOGPIXELSY) \ 72
    `Set italic or not
    If mobjDevice.Font.Italic = True Then
        mlfFont.lfItalic = 1
    Else
        mlfFont.lfItalic = 0

    End If
    `Set underline or not
    If mobjDevice.Font.Underline = True Then
        mlfFont.lfUnderline = 1
    Else
        mlfFont.lfUnderline = 0
    End If
    `Set strikethrough or not
    If mobjDevice.Font.Strikethrough = True Then
        mlfFont.lsngStrikeOut = 1
    Else
        mlfFont.lsngStrikeOut = 0
    End If
    `Set bold or not (use font's weight)
    mlfFont.lfWeight = mobjDevice.Font.Weight
    `Set font rotation angle
    mlfFont.lfEscapement = CLng(mintAngle * 10#)
    mlfFont.lfOrientation = mlfFont.lfEscapement
    `Build temporary new font and output the string
    lngFont = CreateFontIndirect(mlfFont)
    lngOldFont = SelectObject(mobjDevice.hdc, lngFont)
    lngRes = TextOut(mobjDevice.hdc, XtoP(mobjDevice.CurrentX), _
        YtoP(mobjDevice.CurrentY), sText, Len(sText))
    lngFont = SelectObject(mobjDevice.hdc, lngOldFont)
    DeleteObject lngFont
End Sub

`~~~Device
Property Set Device(objDevice As Object)
    Dim sngSX2 As Single
    Dim sngSY2 As Single
    Dim sngPX2 As Single
    Dim sngPY2 As Single
    Dim intScaleMode As Integer
    Set mobjDevice = objDevice
    With mobjDevice
        `Grab current scaling parameters
        intScaleMode = .ScaleMode
        msngSX1 = .ScaleLeft
        msngSY1 = .ScaleTop
        sngSX2 = msngSX1 + .ScaleWidth
        sngSY2 = msngSY1 + .ScaleHeight
        `Temporarily set pixels mode
        .ScaleMode = vbPixels

        `Grab pixel scaling parameters
        sngPX2 = .ScaleWidth
        sngPY2 = .ScaleHeight
        `Reset user's original scale
        If intScaleMode = 0 Then
            mobjDevice.Scale (msngSX1, msngSY1)-(sngSX2, sngSY2)
        Else
            mobjDevice.ScaleMode = intScaleMode
        End If
        `Calculate scaling ratios just once
        msngXRatio = sngPX2 / (sngSX2 - msngSX1)
        msngYRatio = sngPY2 / (sngSY2 - msngSY1)
    End With
End Property

`Scales X value to pixel location
Private Function XtoP(sngX As Single) As Long
    XtoP = (sngX - msngSX1) * msngXRatio
End Function

`Scales Y value to pixel location
Private Function YtoP(sngY As Single) As Long
    YtoP = (sngY - msngSY1) * msngYRatio
End Function

即使Rotator物件裡封裝了一堆程式碼和複雜的執行細節,在應用程式中使用Rotator物件卻十分簡單。下面這段程式告訴你使用Rotator物件的基本步驟。

在程式中,我們產生了一個Rotator物件實體,把一個叫作picTest的圖片方塊控制項指定給Rotator物件的Device屬性,作為這個物件的輸出設備;另外,我們把輸出的角度指定給Angle屬性,再把欲輸出的字串傳給Label方法。

輸出的字型和大小以及輸出的位置(CurrentX和CurrentY)都由圖片方塊的Font屬性群來設定,Rotator物件則利用這些設定值來作為輸出的依據。為了舉例說明,我們故意把Bold、Italic、Underline、Strikethrough和Weight等屬性的設定變成註解,你可以自己去實驗設定這些屬性的效果。

Option Explicit

Dim rotTest As New Rotator
Private Sub picTest_Click()
    Dim intA As Integer
    `Prepare the font in the picture box
    picTest.Scale (-1, -1)-(1, 1)
    With picTest
        .CurrentX = 0
        .CurrentY = 0
        With .Font
            .Name = "Courier New"
            .Size = 11
            `.Bold = True
            `.Italic = True
            `.Strikethrough = True
            `.Underline = True
            `.Weight = 1000
        End With
    End With
    `Connect Rotator object to the picture box
    Set rotTest.Device = picTest
    `Label strings at a variety of angles
    For intA = 10 To 359 Step 15
        rotTest.Angle = intA
        rotTest.Label Space(4) & picTest.Font.Name & Str(intA)
    Next intA
End Sub

圖14-17顯示了程式執行的結果。


 

 圖14-17 利用Rotator物件將字串以多個角度旋轉

如何在圖片方塊中使用多種字型?
 

圖片方塊控制項有完整的屬性群供你設定字型的特性。不像文字方塊控制項和標籤控制項,圖片方塊控制項讓你在程式執行中可以設定這些屬性,而且不會影響到已經輸出的文字。只要設定字型屬性群,將文字印出,又可以繼續設定屬性、繼續列印,就是這麼簡單。

圖14-18顯示了列印在圖片方塊上的不同字型。


 

 圖14-18 列印在圖片方塊上的不同字型

為了與前面版本的Visual Basic相容,現在Visual Basic的圖片方塊控制項也支援早期版本中Visual Basic的字型屬性,如FontName和FontSize,但是這些字型屬性的使用方式有了改變。在現在的Visual Basic裡,Font屬性已不再是一個屬性了,它本身成了一種物件,而且提供了許多屬於它自己的屬性。以下這段程式就是我們產生圖14-18所使用的程式,在這個程式裡,我們用Font物件設定這些字型屬性。

請記住,你可以產生一個獨立的Font物件,設定這個物件的所有屬性,然後只要用一個指令就可以把這些屬性全部指定給含有Font屬性的物件。

Option Explicit

Private Sub picTest_Click()
    picTest.Print "Default font properties"
    picTest.Font.Name = "WingDings"
    picTest.Font.Size = 18
    picTest.Print "WingDings"
    picTest.Font.Name = "Arial"
    picTest.Print "Arial"
    picTest.Font.Name = "Garamond"
    picTest.Print "Garamond"
    picTest.Font.Bold = True
    picTest.Font.Italic = True
    picTest.Font.Underline = True
    picTest.Font.Name = "Arial"
    picTest.Print "Bold, Italic, Underline, and so on"
End Sub

如果要測試這個程式,請在一張空白表單中加入一個圖片方塊控制項picTest,然後加入上述的程式碼。


注意:

Visual Basic的RichTextBox控制項也提供了一些控制字型的功能。雖然RichTextBox控制項不具備將文字準確定位的能力,但是你可以使文字左靠、右靠或向中對齊,也可以在同一個RichTextBox控制項中選擇不同的字型。在使用RichTextBox控制項之前,必須把Microsoft Rich Textbox Control 6.0設定為使用元件。



參考資料:

請參閱 第二十一章"TextBox控制項與RichTextBox控制項" 。