RichTextBox 的行号
对接 RichTextBox 或在其上方显示为叠加层的行号。
引言
尽管市面上已经有一些LineNumbering
控件,但我还是决定自己编写一个,它能为用户提供很大的自由度来创建个性化的外观,同时还能正确处理RichTextBox
的动态内容。还有一个SeeThroughMode
,允许LineNumbers
作为覆盖层显示在RichTextBox
本身之上。自动换行和行高差异都会被正确考虑,并且由于该控件只绘制可见文本行的LineNumberItem
,因此即使是包含复杂布局的大量文本,其绘制速度也能保持很高。
Using the Code
下载的ZIP文件包含VB.NET解决方案文件夹。LineNumbers
控件的所有代码都在LineNumbers_For_RichTextBox
类代码中。打开LineNumbers.sln项目文件后,请使用解决方案资源管理器查找它。在打开窗体之前,请务必生成项目,否则会收到错误消息。如果发生这种情况,请关闭窗体的设计选项卡并重新生成解决方案。所有代码均按“原样”提供,不附带任何权利或责任。这意味着您可以自行承担风险,随意使用和修改它。
将类代码复制到您的项目中,然后生成或重新生成您的应用程序/解决方案。LineNumbers
控件应该会出现在您的Toolbox
中。将LineNumbers
控件添加到窗体后,您会注意到它会显示一个垂直的提示消息:您需要先设置ParentRichTextBox
属性,以便它知道要为哪个RTB显示LineNumbers
。设置好后,LineNumbers
控件将停靠在RTB的左侧——由DockSide
属性控制——并且如果RTB中已有文本,它将开始显示行号,或者显示另一个提示,说明它连接到哪个RTB。
可用属性
您可以使用以下元素来自定义LineNumbers
的外观。所有线条都可以更改其Color
、LineStyle
(点、虚线、实线等)和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
的颜色和字体通过常规的ForeColor
和Font
属性设置,但也有额外的属性可供更改其外观和行为。
LineNrs_Alignment
:您可以设置对齐点(TopLeft
、TopCenter
、TopRight
等)来确定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
:需要先设置此项,因为它允许您指向将显示LineNumbers
的RichTextBox
控件。在设计模式下,如果未设置父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
项区域的矩形。
自动换行和行高
主要问题在于,当自动换行将一个文本行分成多个行时,这些新文本行会进入RichTextBox
的Lines
集合——这在常规的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坐标为负值时,GridLines
的GraphicsPath
的矩形的底部线条会显示在控件内部。通过偏移-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日:发布原始版本