65.9K
CodeProject 正在变化。 阅读更多。
Home

RichTextBox 的行号

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (39投票s)

2007年4月5日

BSD

9分钟阅读

viewsIcon

215559

downloadIcon

3512

对接 RichTextBox 或在其上方显示为叠加层的行号。

Screenshot - linenumbers_for_rtb_examples.jpg

引言

尽管市面上已经有一些LineNumbering控件,但我还是决定自己编写一个,它能为用户提供很大的自由度来创建个性化的外观,同时还能正确处理RichTextBox的动态内容。还有一个SeeThroughMode,允许LineNumbers作为覆盖层显示在RichTextBox本身之上。自动换行和行高差异都会被正确考虑,并且由于该控件只绘制可见文本行的LineNumberItem,因此即使是包含复杂布局的大量文本,其绘制速度也能保持很高。

Screenshot - linenumbers_for_rtb_overlay.jpg

Using the Code

下载的ZIP文件包含VB.NET解决方案文件夹。LineNumbers控件的所有代码都在LineNumbers_For_RichTextBox类代码中。打开LineNumbers.sln项目文件后,请使用解决方案资源管理器查找它。在打开窗体之前,请务必生成项目,否则会收到错误消息。如果发生这种情况,请关闭窗体的设计选项卡并重新生成解决方案。所有代码均按“原样”提供,不附带任何权利或责任。这意味着您可以自行承担风险,随意使用和修改它。

将类代码复制到您的项目中,然后生成或重新生成您的应用程序/解决方案。LineNumbers控件应该会出现在您的Toolbox中。将LineNumbers控件添加到窗体后,您会注意到它会显示一个垂直的提示消息:您需要先设置ParentRichTextBox属性,以便它知道要为哪个RTB显示LineNumbers。设置好后,LineNumbers控件将停靠在RTB的左侧——由DockSide属性控制——并且如果RTB中已有文本,它将开始显示行号,或者显示另一个提示,说明它连接到哪个RTB。

可用属性

您可以使用以下元素来自定义LineNumbers的外观。所有线条都可以更改其ColorLineStyle(点、虚线、实线等)和LineThickness。此LineNumbers控件继承自基本的Control类,因此也可以设置BackgroundImage

边框线

定义控件整体边框的元素。

GridLines

在每个LineNumber的项区域顶部绘制水平分隔线的元素。

边距线

定义可以出现在控件左侧、右侧或两侧垂直边框的元素。

背景渐变

每个LineNumber的项区域可以有一个渐变,该渐变会柔和地混合两种颜色,在public属性中称为alpha和beta颜色,在代码中称为Start/EndColor。所有颜色都可以是透明的,您还可以指定渐变方向,即水平、垂直、前/后对角线。它通过Drawing2D.LinearGradientBrush绘制。请参阅下面的代码片段。

' --- BackgroundGradient
If zGradient_Show = True Then
   zLGB = New Drawing2D.LinearGradientBrush(zLNIs(zA).Rectangle, _
              zGradient_StartColor, zGradient_EndColor, zGradient_Direction)
   e.Graphics.FillRectangle(zLGB, zLNIs(zA).Rectangle)
End If

行号

LineNumbers的颜色和字体通过常规的ForeColorFont属性设置,但也有额外的属性可供更改其外观和行为。

  • LineNrs_Alignment:您可以设置对齐点(TopLeftTopCenterTopRight等)来确定LineNumber相对于其项区域的哪个角/中心点绘制。这与普通Label上的TextAlign属性相同。
  • LineNrs_LeadingZeroes:根据RichTextBox中的文本总行数,用前导零填充LineNumber
  • LineNrs_AsHexadecimal:将LineNumbers显示为十六进制值(在这种情况下,没有前导零)。
  • LineNrs_Anti-Alias:一些字体在文本字符边缘与背景稍微融合时看起来更好。然而,其他字体在没有这种柔化效果的情况下可能看起来更清晰,尤其是小像素字体。
  • LineNrs_ClippedByItemRectangle:如果LineNumbers使用的是大字体,它们可能会溢出其自身的项区域。有时,这与部分透明的BackgroundGradient结合使用会产生炫酷的效果。此选项允许您裁剪LineNumbers,使其仅显示在其自身区域内。
  • LineNrs_Offset:虽然对齐会处理LineNumber的放置,但此属性允许您手动微调LineNumber的位置。使用负值可以使偏移量朝向TopLeft,使用正值可以将LineNumbers的位置移向BottomRight

LineNumbers_For_RichTextBox

LineNumbers_For_RichTextBox控件的行为由以下属性控制:

  • ParentRichTextBox:需要先设置此项,因为它允许您指向将显示LineNumbersRichTextBox控件。在设计模式下,如果未设置父RTB,或者RTB中还没有文本,会显示垂直提示消息。
  • _SeeThroughMode_LineNumbers控件可以显示在其父RichTextBox旁边,也可以作为覆盖层显示在RTB之上。LineNumbers中的空白部分将是透明且可点击的,因此您仍然可以使用下面的RTB。
  • AutoSizing:激活时,自动调整大小将自动调整LineNumbers控件的宽度和位置,以确保LineNumbers保持可见。
  • DockSide:您可以使用此属性将LineNumbers停靠在父RTB的左侧或右侧,或将其高度锁定为与RTB相同。当设置为none时,您可以像其他控件一样自由地定位LineNumbers控件。但是,标准的Dock会覆盖DockSide的行为。

关注点

虽然这是一个相当直接的控件,旨在只做一件事,但有一些问题需要一些关注才能使控件以良好的速度工作。核心Sub,即Update_VisibleLineNumberItems(),处理了其中的几个问题。其余的工作主要由重写的OnPaint sub完成。

对齐行号和RTB文本行

RichTextBox有一个简单的GetPositionFromCharIndex()方法,用于计算给定文本字符的位置——通过其在整个文本中的索引来识别——但该位置点是以客户端坐标表示的。因此,在Update_VisibleLineNumberItems()的开头,您可以看到一些到屏幕坐标的转换以及回溯,以确定RTB的(0,0)原点在LineNumbers控件中的位置。此外,还需要额外检查以找到控件在父RTB内的(0,0)原点,因为LineNumber控件的Top可能比RTB在窗体上低。这会影响计算应该为哪些文本行绘制LineNumberItem,因为只有可见的LineNumberItems才重要,以保持速度。Update_VisibleLineNumberItems() sub基本上构建了一个名为zLNIs的列表,其中只包含可见的LineNumberItems。每个LineNumberItem(它是一个结构 更新B:现在这是一个嵌套类)包含一个LineNumber和一个标记LineNumber项区域的矩形。

自动换行和行高

主要问题在于,当自动换行将一个文本行分成多个行时,这些新文本行会进入RichTextBoxLines集合——这在常规的TextBox中也会发生——而不会真正向集合添加项。例如,一个有5行实际文本且禁用自动换行的RTB将有一个包含5个项的正确Lines集合,其中每个项都是一个实际文本行。但是,当启用自动换行并且碰巧将第一行实际文本换成2行时,Lines集合仍然有5个项,但item2将是第一行实际文本的第二部分。为了应对这种特殊的行为,LineNumbers控件需要创建自己的Lines集合,一个不受自动换行和实际文本行影响的集合。这就是Update_VisibleLineNumberItems() sub中的string列表zSplit。行高(即LineNumberItem矩形的高度)将通过比较每个实际文本行与下一个实际行的Y坐标来计算。GetPositionFromCharIndex()方法将为我们提供Y坐标,但需要知道每个可见文本行的第一个字符的char索引。

计算哪些行号可见

控件需要找出RTB中的哪些文本行需要为它们绘制LineNumberItem。为了保持高绘制速度,只应绘制可见项。zStartIndex变量的初始值,即第一个(完全或部分)可见文本字符的char索引,将由FindStartIndex() sub计算。这是一个递归sub(即它会调用自身),它基本上会查找Y坐标最接近0或最接近目标值的文本字符。代码注释将确切解释其工作原理。

行号的绘制(仅数字)

下面是一个代码片段,显示了在重写的OnPaint sub中绘制LineNumbers的过程。为了确定zPoint的大型TextAlignment计算被省略了。您可以看到文本裁剪是如何通过使用Graphics.SetClip方法临时限制绘图区域来实现的。还请注意,一个基于LineNumber文本尺寸(是否裁剪)的矩形zItemClipRectangle被添加到zGP_LineNumbers对象中。这是一个GraphicsPath对象,将在SeeThroughMode中使用。有关更多信息,请参见下一篇文章部分。

' --- LineNumbers
If zLineNumbers_Show = True Then
    '   TextFormatting
    If zLineNumbers_ShowLeadingZeroes = True Then
        zTextToShow = IIf(zLineNumbers_ShowAsHexadecimal, _
            zLNIs(zA).LineNumber.ToString("X"), _
            zLNIs(zA).LineNumber.ToString(zLineNumbers_Format))
    Else
        zTextToShow = IIf(zLineNumbers_ShowAsHexadecimal, _
            zLNIs(zA).LineNumber.ToString("X"), _
            zLNIs(zA).LineNumber.ToString)
    End If
    '   TextSizing
    zTextSize = e.Graphics.MeasureString(zTextToShow, Me.Font, zPoint, zSF)

    ' ==TextAlignment computation here (large Select Case to build zPoint)==
    
    '   TextClipping
    zItemClipRectangle = New Rectangle(zPoint, zTextSize.ToSize)
    If zLineNumbers_ClipByItemRectangle = True Then
        '   If selected, the text will be clipped so that it doesn't spill out
        '   of its own LineNumberItem-area. Only the part of the text inside 
        '   the LineNumberItem.Rectangle should be visible, so intersect with 
        '   the ItemRectangle.
        '   The SetClip method temporary restricts the drawing area of the 
        '   control for whatever is drawn next.
        zItemClipRectangle.Intersect(zLNIs(zA).Rectangle)
        e.Graphics.SetClip(zItemClipRectangle)
    End If
   
    '   TextDrawing
    e.Graphics.DrawString(zTextToShow, Me.Font, zBrush, zPoint, zSF)
    e.Graphics.ResetClip()   
   
    '   The GraphicsPath for the LineNumber is just a rectangle behind the 
    '   text, to keep the paintingspeed high and avoid ugly artifacts.
    zGP_LineNumbers.AddRectangle(zItemClipRectangle)
    zGP_LineNumbers.CloseFigure()
End If

透明模式

我能想象人们对此感兴趣,因为它比简单的线条和矩形绘制要复杂一些。所以,这里有一些关于它是如何实现的:它使用一个Drawing2D.GraphicsPath对象,这与更常用的Graphics类型类似。然而,当您在GraphicsPath上绘制时,您基本上是在绘制哪些像素将是透明的,当该GraphicsPath——或者在这种情况下是几个GraphicsPaths的组合——被设置为控件的Region时。换句话说,您正在为控件创建一个自定义轮廓,以便您可以使控件具有任何想要的形状,甚至在其内部留下孔洞。

我在重写的OnPaint sub中同时在GraphicsPaths上进行绘制,与常规绘制相同。这是因为线条和矩形图形已经被计算出来了,所以最好使用它们两次。下面的代码片段清楚地显示了这一点:在常规Graphics上绘制的相同边框线(e.Graphics.DrawLines...)也被绘制到了GraphicsPath上(zGP_BorderLines.AddLines...)。

Dim zGP_BorderLines As New Drawing2D.GraphicsPath(Drawing2D.FillMode.Winding)

Dim zP_Left As New Point(Math.Floor(zBorderLines_Thickness / 2), _
    Math.Floor(zBorderLines_Thickness / 2))
Dim zP_Right As New Point(
    Me.Width - Math.Ceiling(zBorderLines_Thickness / 2), _
    Me.Height - Math.Ceiling(zBorderLines_Thickness / 2))

' --- BorderLines 
Dim zBorderLines_Points() As Point = { _
    New Point(zP_Left.X, zP_Left.Y), _
    New Point(zP_Right.X, zP_Left.Y), _
    New Point(zP_Right.X, zP_Right.Y), _
    New Point(zP_Left.X, zP_Right.Y), _
    New Point(zP_Left.X, zP_Left.Y)}
If zBorderLines_Show = True Then
   zPen = New Pen(zBorderLines_Color, zBorderLines_Thickness)
   zPen.DashStyle = zBorderLines_Style
   e.Graphics.DrawLines(zPen, zBorderLines_Points)

   '   And the same shape is added to the border's GraphicsPath
   zGP_BorderLines.AddLines(zBorderLines_Points)
   zGP_BorderLines.CloseFigure()

   '   BorderThickness and Style for SeeThroughMode
   zPen.DashStyle = Drawing2D.DashStyle.Solid
   zGP_BorderLines.Widen(zPen)
End If

OnPaint sub的最后,控件只是检查zSeeThroughMode是否处于活动状态。如果是,那么不同的GraphicsPaths(名为zGP_...)将被组合起来,在进行额外检查以确保控件不会变为空后,形成控件的Region

' --- SeeThroughMode
'   combine all the GraphicsPaths (= zGP_... ) and set them as the Region 
If zSeeThroughMode = True Then
    zRegion.MakeEmpty()
    zRegion.Union(zGP_BorderLines)
    zRegion.Union(zGP_MarginLines)
    zRegion.Union(zGP_GridLines)
    zRegion.Union(zGP_LineNumbers)
End If

' --- Region
If zRegion.GetBounds(e.Graphics).IsEmpty = True Then
    '   Note: If the control is in a condition that would show it as empty, 
    '   then a border-region is still drawn regardless of it's borders' 
    '   on/off state. This is added to make sure that the bounds of the 
    '   control are never lost (it would remain empty if this was not done).
    zGP_BorderLines.AddLines(zBorderLines_Points)
    zGP_BorderLines.CloseFigure()
    zPen = New Pen(zBorderLines_Color, 1)
    zPen.DashStyle = Drawing2D.DashStyle.Solid
    zGP_BorderLines.Widen(zPen)

    zRegion = New Region(zGP_BorderLines)
End If
Me.Region = zRegion

更新

固定

  • (A) 当第一个LineNumberItem的Y坐标为负值时,GridLinesGraphicsPath的矩形的底部线条会显示在控件内部。通过偏移-zLNIs(0).Rectangle.Y已修复此问题。

改进

  • (B) 通过提高Update_VisibleLineNumberItems()方法的效率,性能提高了一倍。这是通过将RTB的.GetPositionFromChar()方法调用次数减半来实现的,随着文本行数的增加,该方法变得越来越慢。
  • (B) 对于大型文档的滚动,现在有一个基于时间的截止日期来计算LineNumberItems,以确保滚动保持平滑。

结束

就是这些了,希望您喜欢这个LineNumbers_For_RichTextBox控件,并在您自己的项目中找到它。祝您使用愉快! 

历史

  • 2007年5月31日:文章已编辑并移至CodeProject.com主文章库
  • 2007年4月12日:已更新
  • 2007年4月5日:发布原始版本
© . All rights reserved.