GDI+绘制自定义行距的文本的三种方法。

  在.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

  

  写了一段测试代码,分别测试三个方法的效率。代码如下:

  Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

    Dim mGDI As New clsDraw(Panel1)

    Dim tS1 As String = My.Computer.FileSystem.ReadAllText("t1.txt", System.Text.Encoding.Default)

    Dim t1 As Integer, t2 As Integer

  

    t1 = Environment.TickCount

    mGDI.Draw1(tS1)

    t2 = Environment.TickCount

    Debug.Print(t2 - t1)


    t1 = Environment.TickCount

    mGDI.Draw2(tS1)

    t2 = Environment.TickCount

    Debug.Print(t2 - t1)

 

    t1 = Environment.TickCount

    mGDI.Draw3(tS1)

    t2 = Environment.TickCount

    Debug.Print(t2 - t1)

 

    Panel1.Invalidate()

  End Sub

  在绘制象示意图中的文本的效果,测试了十次,三种方法的耗费的时间如下(单位是毫秒):

  第一次:方法一:125;方法二:94;方法三:15
  第二次:方法一:125;方法二:78;方法三:16
  第三次:方法一:125;方法二:79;方法三:15
  第四次:方法一:110;方法二:93;方法三:16
  第五次:方法一:125;方法二:78;方法三:16
  第六次:方法一:125;方法二:78:方法三:16
  第七次:方法一:125;方法二:78;方法三:15
  第八次:方法一:125;方法二:78;方法三:16
  第九次:方法一:110;方法二:94;方法三:15
  第十次:方法一:109;方法二:94;方法三:15

  可以看出,方法一和方法二由于要拆分字符串和反复调用GDI+的方法,所以效率有点低下,方法二由于采用DrawImage的方法,效率略有提升。而方法三不拆分字符串和只调用一次GDI+的方法,效率高得惊人。把前两种方法远远甩在后面。

  如果各位网友还有什么好的方法,欢迎交流,大家互相学习。

posted @ 2010-04-21 13:05  万仓一黍  阅读(8781)  评论(2编辑  收藏  举报