在.NET中,繪制圖形和文本用的是GDI+。
在實際的應用中,繪制多行文本是比較常見的,而且有時還要求在繪制多行文本時能指定文本的行間距。如下圖:
注:由於圖太大,只截了左邊部分的圖,右邊有一小部分沒有截圖。
上面這個示意圖。一共18行文字,每行52個文字,行間距為1.5字符。
有關的GDI+的知識這裡不再詳細的介紹了。下面講的是如何實現上面這個圖的效果,給出三種實現方法。並比較他們的實現效率。
由於GDI+中沒有文本行間距的概念,所以,本文的三種方法都是自己實現行間距。
准備工作:自定義一個類clsDraw
有這幾個方法:
New(P as Control) 構造函數,根據P來構造文本繪制環境
Clear() 用背景色清楚畫布
DrawText(Text as String,P as Point) 在指定位置P,繪制文本Text,這個文本一般是單行文本
DrawText1(Text as String,P as Point) 在指定位置P,繪制文本Text,這個文本是帶有換行符的多行文本
Draw1(Text as String) 用方法一繪制文本在默認位置。
Draw2(Text as String) 用方法二繪制文本在默認位置。
Draw3(Text as String) 用方法三繪制文本在默認位置。
Refresh(G as Graphics) 刷新本畫布的內容到指定的控件上
有這幾個屬性
mG Graphics 本畫布的Graphics對象。
mFont Font 本畫布的Font對象
mForeColor Color 本畫布的前景色
mBackColor Color 本畫布的背景色
mBmp Bitmap 本畫布對應的Bitmap對象
mCP Point 畫布當前的繪制點
mTextHeight Integer Font對應的文字高度
mLineHeight Integer 行高,在本文中,是文本高度的1.5倍
下面詳細介紹三種方法的實現:
方法一:將文本截斷成多個文本,然後依次調用DrawText方法,將文本繪制到畫布。模擬出自定義行間距的效果。
Public Sub Draw1(ByVal Text As String)
Clear()
Dim i As Integer, j As Integer, tS() As String
j = Int(Text.Length / 52)
ReDim tS(j - 1)
For i = 0 To j - 1
tS(i) = Text.Substring(i * 52, 52)
Next
If Text.Length - j * 52 <> 0 Then
ReDim Preserve tS(j+1)
tS(j+1) = Text.Substring(j * 52)
End If
For i = 0 To tS.GetUpperBound(0)
DrawText(tS(i), New Point(3, 3 + i * mLineHeight))
Next
End Sub
Public Sub DrawText(ByVal Text As String, ByVal P As Point)
mCP = P
RenderText(Text)
End Sub
Private Sub RenderText(ByVal Text As String)
TextRenderer.DrawText(mG, Text, mFont, mCP, mForeColor, TextFormatFlags.NoPadding)
End Sub
幾點說明:
1、繪制文本采用TextRender類,這個類對GDI進行了封裝。在繪制文本的時候效率比Graphics的DrawString的效率高。
2、這個方法也是大家都能想到的。不過效率不敢恭維。原因有二,一是在繪制文本前,要拆分文本,會產生大量的臨時字符串。二是,每調用一次DrawText,CLR其實做了大量的PInvoke的工作,而這些工作很多是重復的。去看看它的Reflector後的代碼,每次調用,都要將mG、mFont、mForeColor對象轉化為GDI,繪制文本,然後銷毀GDI對象。而多次繪制,其實這三個對象是不變的,而多次的生成和銷毀自然影響了效率。
方法二:既然方法一的瓶頸在多次調用DrawText而產生的。那如果只調用一次該方法,是不是就能提升效率呢?答案是肯定的。本方法就是將文本拆分成多個字符串後,再用換行符(VbNewLine)串聯起來。這樣,調用一次DrawText就能繪制多行文本,不過這個多行文本是沒有行間距效果的,下一行文本緊挨著上一行文本。采用的辦法是將繪制好的文本再逐行下移到指定位置,產生行間距的效果。本方法過程分兩步,先在備用的畫布上一次繪制所有文本,將備用畫布上的文本再依次繪到畫布上的指定位置。產生行間距的效果。
Public Sub Draw2(ByVal Text As String)
Clear()
Dim i As Integer, j As Integer, tS() As String, tS1 As String
j = Int(Text.Length / 52)
ReDim tS(j - 1)
For i = 0 To j - 1
tS(i) = Text.Substring(i * 52, 52)
Next
If Text.Length - j * 52 <> 0 Then
ReDim Preserve tS(j+1)
tS(j+1) = Text.Substring(j * 52)
End If
tS1 = Join(tS, vbNewLine)
DrawText1(tS1, New Point(3, 3))
End Sub
Public Sub DrawText1(ByVal Text As String, ByVal P As Point)
mCP = P
RenderText1(Text)
End Sub
Private Sub RenderText1(ByVal Text As String)
Dim i As Integer, tR As Rectangle, tR1 As Rectangle
TextRenderer.DrawText(mG1, Text, mFont, mCP, mForeColor, TextFormatFlags.NoPadding)
tR.X = 3
tR.Height = mTextHeight
tR.Width = mBmp1.Width
tR1.X = 3
tR1.Height = mTextHeight
tR1.Width = mBmp1.Width
For i = 0 To MaxLines - 1
tR.Y = 3 + i * mLineHeight
tR1.Y = 3 + i * mTextHeight
mG.DrawImage(mBmp1, tR, tR1, GraphicsUnit.Pixel)
Next
End Sub
Public ReadOnly Property MaxLines() As Integer
Get
Return Int((mBmp.Height + mLineHeight - mTextHeight) / mLineHeight)
End Get
End Property
幾點說明:
1、本方法比方法一效率有所提高,約有20%的提高。
2、不過還是存在兩個問題。一是要拆分字符串,會產生大量的臨時字符串。二是將原來的多次調用DrawText的方法改為多次調用DrawImage的方法,效率有一定的提高,但還是多次PInvoke,大量的對象生成和銷毀,效率還是有問題。
方法三:利用GdipDrawDriverString函數。我們所有的GDI+對象其實都是封裝了Gdiplus.dll中的函數,只不過有的函數沒有封裝而已。GdipDrawDriverString就是其中一個。它的VB2005聲明為
<DllImport("Gdiplus.dll", CharSet:=CharSet.Unicode)> _
Friend Shared Function GdipDrawDriverString(ByVal graphics As IntPtr, _
ByVal text As String, _
ByVal length As Integer, _
ByVal font As IntPtr, _
ByVal brush As IntPtr, _
ByVal positions() As PointF, _
ByVal flags As Integer, _
ByVal matrix As IntPtr) As Integer
End Function
由於這個函數不能直接調用Graphics、Font、SolidBrush等對象。因此,在調用前還得自己先封裝一下:
Private Shared Sub DrawDriverString(ByVal graphics As Graphics, _
ByVal text As String, ByVal font As Font, _
ByVal brush As Brush, ByVal positions() As PointF)
DrawDriverString(graphics, text, font, brush, positions, Nothing)
End Sub
Private Shared Sub DrawDriverString(ByVal G As Graphics, _
ByVal T As String, ByVal F As Font, _
ByVal B As Brush, ByVal P() As PointF, ByVal M As Matrix)
If (G Is Nothing) Then Throw New ArgumentNullException("graphics")
If (T Is Nothing) Then Throw New ArgumentNullException("text")
If (F Is Nothing) Then Throw New ArgumentNullException("font")
If (B Is Nothing) Then Throw New ArgumentNullException("brush")
If (P Is Nothing) Then Throw New ArgumentNullException("positions")
Dim Field As FieldInfo
Field = GetType(Graphics).GetField("nativeGraphics", BindingFlags.Instance Or BindingFlags.NonPublic)
Dim hGraphics As IntPtr = Field.GetValue(G)
Field = GetType(Font).GetField("nativeFont", BindingFlags.Instance Or BindingFlags.NonPublic)
Dim hFont As IntPtr = Field.GetValue(F)
Field = GetType(Brush).GetField("nativeBrush", BindingFlags.Instance Or BindingFlags.NonPublic)
Dim hBrush As IntPtr = Field.GetValue(B)
Dim hMatrix As IntPtr = IntPtr.Zero
If (Not M Is Nothing) Then
Field = GetType(Matrix).GetField("nativeMatrix", BindingFlags.Instance Or BindingFlags.NonPublic)
hMatrix = Field.GetValue(M)
End If
Dim result As Integer = GdipDrawDriverString(hGraphics, T, T.Length, hFont, hBrush, P, DriverStringOptions.CmapLookup, hMatrix)
End Sub
Private Enum DriverStringOptions
CmapLookup = 1
Vertical = 2
Advance = 4
LimitSubpixel = 8
End Enum
上面這段代碼是我移植網上的一段C#的代碼。期間也碰到過陷阱,看看“使用GDI+繪制有間距的文本”“充滿魅惑的GetType(VB2005)”這兩篇文章就知道我指陷阱是什麼了。
這個函數在調用的時候要傳遞一個PointF的數組,指明每個字符的繪制位置。而且這個位置是指的是字符的左下角位置。那麼我在調用的時候就不需要拆分字符串,而是計算每個字符的位置就可以了。
Public Sub Draw3(ByVal Text As String)
Clear()
Dim i As Integer, tP() As PointF
ReDim tP(Text.Length - 1)
For i = 0 To Text.Length - 1
tP(i).X = (i Mod 52) * 16 + 3
tP(i).Y = 3 + Int(i / 52) * mLineHeight + 12
Next
DrawDriverString(mG, Text, mFont, New SolidBrush(mForeColor), tP)
End Sub