14. 繪圖技巧
在本章中你會看到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控制項" 。