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 StringByVal 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 StringByVal 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 StringByVal 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 As Graphics, _

          ByVal As StringByVal F As Font, _

          ByVal B As Brush, ByVal P() As PointF, ByVal M As Matrix)

    If (G Is NothingThen Throw New ArgumentNullException("graphics")

    If (T Is NothingThen Throw New ArgumentNullException("text")

    If (F Is NothingThen Throw New ArgumentNullException("font")

    If (B Is NothingThen Throw New ArgumentNullException("brush")

    If (P Is NothingThen 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 NothingThen

      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+的方法,效率高得惊人。把前两种方法远远甩在后面。

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


    本文转自万仓一黍博客园博客,原文链接:http://www.cnblogs.com/grenet/archive/2010/04/21/1717135.html,如需转载请自行联系原作者

相关文章
|
存储 缓存 弹性计算
2024年阿里云最便宜云服务器出炉:61元、165元、99元、199元
2024年截止目前阿里云最便宜的云服务器已经出炉,轻量应用服务器2核2G3M带宽61元1年、2核4G4M带宽165元1年;云服务器经济型e实例2核2G3M带宽99元1年;云服务器通用算力型u1实例2核4G5M带宽199元1年。除此之外,还有幻兽帕鲁Palworld专用服务器4核16G10M带宽只要26.52元/1个月、79.56元/3个月、149.00元/6个月,8核32G10M带宽只要90.60元/1个月、271.80元/3个月。本文为大家分享2024年阿里云最便宜的各个云服务器。
9125 4
2024年阿里云最便宜云服务器出炉:61元、165元、99元、199元
|
网络安全 数据安全/隐私保护 网络架构
ABCDE类网络的划分及保留网段
ABCDE类网络的划分及保留网段
4242 7
|
安全 数据安全/隐私保护
同态加密含义以及应用场景
文章探讨了同态加密技术的含义、发展历程、技术路线以及在安全求交、隐匿查询、多方联合计算和建模等隐私计算场景中的应用,并分析了其在实际应用中面临的关键问题和研究发展方向,同时指出了同态加密可能导致的计算精度损失和效率降低。
1243 0
同态加密含义以及应用场景
|
存储 SQL 关系型数据库
OceanBase与MySQL有何区别?
【8月更文挑战第12天】OceanBase与MySQL有何区别?
3594 3
|
关系型数据库 MySQL 数据安全/隐私保护
|
C# 图形学
Winform控件优化之Paint事件实现圆角组件(提取绘制圆角的扩展方法)
Paint事件方法中实现圆角控件不要通过事件参数`e.ClipRectangle`获取控件区域范围,原因见最后介绍;注意设置控件背景透明(参见[Winform控件优化之背景透明那些事2...
1077 0
Winform控件优化之Paint事件实现圆角组件(提取绘制圆角的扩展方法)
【UI】 element ui 表格没有数据时用--填充
【UI】 element ui 表格没有数据时用--填充
373 2
|
C++
c++ set、map的四种自定义排序方法
c++ set、map的四种自定义排序方法
1354 0
|
Android开发 开发工具 git
Android USB转串口通信
一、引用 1、Git上最火的USB转串口通信 2、Android之USB转串口通信 3、安卓开发中的USB转串口通讯 二、截图 废话不多说,先上图,micro usb -> usb 连接的测试温度的外设(其实是个测试粮油品质的,还有TPM值等等) usb_connect.png 三、流程 1、mainfest中注册监听USB拔插动作并且过滤对应vid,pid的设备。
5085 0
|
Java 程序员 区块链
怎样通过java用web3j查询以太坊交易信息?
刚开始使用web3j,我有一些基本的麻烦。 我已经可以成功如何获得一个EthBlock,并检索里面的所有信息。我想看看这个块中的交易列表,我该怎么做? 我可以调用: List transactions = ethBlock.getBlock().getTransactions(); 我应该能够浏览这个列表并获得有关每笔交易的信息。
6781 0