增强型 RichTextBox - IRTB






4.73/5 (16投票s)
本文介绍了一种实现行号显示、高亮当前行以及显示当前行和列的简单方法。
介绍
本文介绍了我称之为改进的 RichTextBox
控件。为何改进?嗯,浏览互联网,你会发现很多评论指出微软提供的控件缺少各种功能,但其中三个最明显的功能是:
- 行号显示
- 高亮当前行
- 显示当前行和列
第一个功能已在多篇文章中介绍,例如以下几篇:
- https://codeproject.org.cn/Articles/14566/Line-Numbering-of-RichTextBox-in-NET-2-0 Michael Elly 撰
- https://codeproject.org.cn/Articles/12152/Numbering-lines-of-RichTextBox-in-NET-2-0 Petr Minarik 撰
- https://codeproject.org.cn/Articles/18294/LineNumbers-for-the-RichTextBox nogChoco 撰
- https://codeproject.org.cn/Articles/38858/Line-Numbers-for-RichText-Control-in-C Damian J. Suess 撰
当然,你还可以找到更多与行号显示相关的文章和代码片段。但尽管这个问题已经解决,高亮当前行和显示当前行/列仍然是悬而未决的问题。对于所有使用 Notepad++ 或 PSPad 的用户来说,很明显,一个很棒的功能是高亮显示当前光标所在的行,当然,能够指示行/列信息也非常有帮助。
那么,本文能带给你什么?我将通过这三个方面,展示我如何为自己的项目解决了这些问题。请注意,我刚开始接触 .NET 世界,如果有什么可以改进或用其他方式实现的地方,请告诉我。我希望这里的信息对那些为自己的项目寻求一个简单小巧的解决方案的人有所帮助。
代码分为以下“区域”:
- 区域“ImprovedRTB 属性”
- 区域“通用子例程”
- 区域“IRTB 事件”
- 区域“附加功能”
实现
该控件由两个组件组成:一个 RichTextBox
和一个 PictureBox
。PictureBox
将用于绘制行号,而 RichTextBox
将提供我们作为文本编辑器所需的所有功能。
IRTB 功能包括:
- 行号显示。运行时可启用/禁用。
- 高亮当前行。运行时可启用/禁用。
LineInformation
事件,提供当前行和列的信息。DragDropFileInformation
事件,允许将文件直接拖放到控件中。BMLInformation
事件,用于通知书签已添加/移除。- 高亮当前行的颜色可在运行时更改。
- 行号显示的前缀可运行时启用/禁用。
- 字体可在运行时更改。
- 缩放
- 支持拖放。
- 书签行。新增于 2013.04.02。
- XML 验证。新增于 2013.04.02。
- 无需
LineInformation
事件即可获取行/列信息。新增于 2013.04.02。
已知问题
- 对于大于 1500KB 的文件,性能不佳,而对于大于 2500KB 的文件,当同时启用行号显示和高亮显示时,性能会非常差。这主要是由于在
RichTextBox
上方添加数字和绘制高亮行所需的计算。 - 根据操作或文件大小的不同,可能会注意到一些闪烁。
- 如果设置自动换行(wrap)为 true,将无法正确显示行号。
行号显示
那么,让我们从第一个问题开始,行号显示。正如我之前所说,你可以找到几种解决方案,有些解决方案展示了非常漂亮的图形效果,但不幸的是,这使得控件对于大文件来说不可用;有些解决方案则不完全成熟;还有些解决方案则倾向于使用非托管代码(sendmessage)方法,这完全有效(我已经在其他项目中使用过)。考虑到这一点,我决定采取折衷方案,不太花哨以至于使控件不可用,并尽量不使用“sendmessage”选项;由于我已经采用了与 Michael Elly 提出的方法类似的方法,所以我决定继续沿用这条思路。Michael 代码的关键在于其计算行高的方式。为什么?因为正如大家所知,RichTextBox
使用平滑滚动,因此它滚动的是像素而不是行。这是由于该控件的特性,它可以支持不同的格式、字体、颜色等。因此,唯一的滚动方式是基于像素而不是行。所以,这是行号显示的核心。
Dim font_height As Single = _
IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexFromLine(2)).Y - _
IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexFromLine(1)).Y
If font_height <= 0 Then font_height = IRTBTextContainer.Font.Height + 1 'Exit Sub
'Get the first line index and location
Dim firstIndex As Integer = IRTBTextContainer.GetCharIndexFromPosition(New Point(0, g.VisibleClipBounds.Y + font_height / 3))
Dim firstLine As Integer = IRTBTextContainer.GetLineFromCharIndex(firstIndex)
Dim firstLineY As Integer = IRTBTextContainer.GetPositionFromCharIndex(firstIndex).Y
这样,就可以计算出行的实际高度,从而获得在 PictureBox
上绘制数字所需的精确值。现在,使用这个值,我们可以计算 Y 轴位置。
Dim y As Single
Do While y < g.VisibleClipBounds.Y + g.VisibleClipBounds.Height
If i > (IRTBLineCount) Or (i > (IRTBLineNumber + 1) And (IRTBLineNumber + 1) = _
IRTBLineCount) Then Exit Do
'**This line will avoid to paint all possible numbers in
' the VisibleClipBounds even if the line number on the Richtextbox is 0
y = firstLineY + 2 + font_height * (i - firstLine - 1) '
LineNumberingString = Format(i, PrefixFormat) '** To Add some format to the line numbering
Select Case PBAlign
Case IRTBAlignPos.Left
PBAlignNumbers = 0
Case IRTBAlignPos.Right
PBAlignNumbers = CInt(PBNumbering.Width - g.MeasureString(LineNumberingString, _
IRTBTextContainer.Font).Width * IRTBTextContainer.ZoomFactor)
End Select
If i > 0 Then '**To avoid painting the number 0 so in case we are zooming it won’t be shown
g.DrawString(LineNumberingString, PLNumberingFont, IRTBBrushes, PBAlignNumbers, y)
End If
i += 1
Loop
Me.TableLayoutPanel1.ColumnStyles.Item(0).Width = _
CSng(Math.Ceiling(IRTBTextContainer.Font.Size) * 2) + _
CInt(g.MeasureString(LineNumberingString, IRTBTextContainer.Font).Width * _
IRTBTextContainer.ZoomFactor)
好的,现在我们有了绘制数字的例程,太棒了!(感谢 Michael Elly),但我们如何使用它?什么时候调用它?现在我们需要检查 PictureBox
的 Paint
事件。如何触发这个事件?以及在什么条件下应该这样做?必须使用以下事件:
Me.SizeChanged
IRTBTextContainer.VScroll
IRTBTextContainer.MouseWheel
IRTBTextContainer.SelectionChanged
LoadFileAndNumbering
当任何这些事件被触发时,应该使用以下语句:
PBNumbering.Invalidate()
通过这样做,我们迫使 PictureBox
重新绘制,从而触发 Paint
事件并调用 DrawIRTBLineNumbers
。
这是用于此目的的代码:
Private Sub PLNumbering_Paint(ByVal sender As System.Object, _
ByVal e As System.Windows.Forms.PaintEventArgs) Handles PBNumbering.Paint
If EnableNumbering = True Then
DrawIRTBLineNumbers(e.Graphics)
If IRTBForceONPaint = True Then
OnPaint(Nothing)
IRTBForceONPaint = False
End If
End If
End Sub
现在,数字将根据 RichTextBox
的 VisibleClipBounds
中显示的行进行绘制。
高亮当前行
好的,既然我们已经绘制了数字,就可以尝试获得一个高亮显示当前行的矩形。要做到这一点,我们需要使用标准的 Windows GDI+ 库(System.Drawing
),并且我们需要重写 OnPaint
子例程。你可能已经注意到,RichTextbox
控件没有公开 Paint 事件,所以要控制我们想要绘制的内容,唯一的方法就是使用 Control 提供的 OnPaint
方法。我们重写该方法,并添加必要的代码来绘制矩形,使用 RichTextBox
的宽度和基于字体高度的行高。
为什么我们不使用 WndProc
来捕获 RichTextBox
的绘制事件?嗯,我决定选择更简单的解决方案。但对于对此方法感兴趣的人,这里有一个链接展示了如何捕获 WM_PAINT
消息:
回到绘制矩形的代码:
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
Dim RTBPoint As Point
IRTBTextContainer.Refresh()
RTBPoint = IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexOfCurrentLine)
IRTBDrawRectangle(My.Settings.HighLightColor, 1, RTBPoint.Y, _
IRTBTextContainer.Width - 1, CInt(IRTBTextContainer.Font.GetHeight))
IRTBTextContainer.Focus()
End Sub
首先,我们需要刷新 RichTextBox
以清除之前的所有绘制。
IRTBTextContainer.Refresh()
然后,我们需要获取当前行第一个字符的 Y 轴位置。
RTBPoint = IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexOfCurrentLine)
最后,我们可以绘制矩形。
IRTBDrawRectangle(My.Settings.HighLightColor, 1, RTBPoint.Y, _
IRTBTextContainer.Width - 1, IRTBTextContainer.Font.GetHeight, True)
这是绘制矩形的子例程:
Private Sub IRTBDrawRectangle(ByVal RTBDrawColor As Color, _
ByVal RTBPointX As Integer, ByVal RTBPointY As Integer, _
ByVal RTBWidth As Integer, ByVal RTBHeight As Integer)
If EnableHighLight = True Then
Dim MyPen As New System.Drawing.Pen(RTBDrawColor)
Dim FormGraphics As System.Drawing.Graphics
Dim MySolidBrush As SolidBrush
RTBHeight = RTBHeight * IRTBTextContainer.ZoomFactor + 2
If RTBDrawColor.A > 64 Then
MySolidBrush = New SolidBrush(Color.FromArgb(64, RTBDrawColor.R, RTBDrawColor.G, RTBDrawColor.B))
Else
MySolidBrush = New SolidBrush(RTBDrawColor)
End If
FormGraphics = IRTBTextContainer.CreateGraphics()
FormGraphics.DrawRectangle(MyPen, RTBPointX, RTBPointY, RTBWidth, RTBHeight)
FormGraphics.FillRectangle(MySolidBrush, RTBPointX, RTBPointY, RTBWidth, RTBHeight)
MyPen.Dispose()
FormGraphics.Dispose()
End If
End Sub
结果会是这样:
太棒了!现在我们有一个绘制在 RichTextBox
上方的半透明矩形,这样我们就可以看到它后面的内容了!但现在有一些新的场景需要验证,例如:
- 如果用户在
RichTextBox
中键入新字符会怎样? - 如果用户按箭头键会怎样?
- 如果用户决定缩放(Ctrl+滚轮)会怎样?
- 如果用户按 Page Up/Down 会怎样?
- 如果用户按 Back/Delete/Enter 会怎样?
所有这些操作都会触发 RichTextBox
的隐藏 Paint 事件,导致控件重绘并删除高亮当前行的矩形。要处理这个问题,我们需要使用一些我们已经用来绘制数字的事件。
Me.SizeChanged
IRTBTextContainer.MouseWheel
IRTBTextContainer.SelectionChanged
并且我们需要使用更多事件:
IRTBTextContainer.MouseClick
IRTBTextContainer.HScroll
IRTBTextContainer.GotFocus
IRTBTextContainer.KeyDown
IRTBTextContainer.KeyUp
现在,根据条件,我们需要调用 OnPaint
方法。通过这样做,矩形将在 RichTextBox
的 Paint 事件完成后绘制。
OnPaint(Nothing)
由于我们希望高亮显示当前光标所在的行,我们需要检查的主要事件是 SelectionChanged
。每次 RichTextBox
中的选择发生变化时,都会触发此事件,因此我们可以在这里根据用户执行的操作(按键、鼠标操作等)来决定该做什么。
为此,我们声明一个枚举来定义这些值:
Enum IRTBSelectionCase As Integer
KeysPageUpDown = 1
KeysLeftRight = 2
KeysSpecial = 3
KeysNormal = 4
Mouse = 5
End Enum
我们在按下/松开键盘事件触发时分配这些值。
Private Sub RTBTextContainer_KeyDown(ByVal sender As System.Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles IRTBTextContainer.KeyDown Dim RTBText As RichTextBox RTBText = Nothing RTBText = CType(sender, RichTextBox) '***Here you can implement different actions for different combination of keys Select Case e.KeyCode Case Keys.Up IRTBKeysSelect = IRTBSelectionCase.KeysPageUpDown Case Keys.Down IRTBKeysSelect = IRTBSelectionCase.KeysPageUpDown Case Keys.Left IRTBKeysSelect = IRTBSelectionCase.KeysLeftRight Case Keys.Right IRTBKeysSelect = IRTBSelectionCase.KeysLeftRight Case Keys.Next 'handles Keys.PageDown IRTBKeysSelect = IRTBSelectionCase.KeysPageUpDown IRTBForceONPaint = True PBNumbering.Invalidate() Case Keys.PageUp IRTBKeysSelect = IRTBSelectionCase.KeysPageUpDown Case Keys.PageDown 'Handles in case of Windows Vista IRTBKeysSelect = IRTBSelectionCase.KeysPageUpDown Case Keys.Enter IRTBKeysSelect = IRTBSelectionCase.KeysSpecial Case Keys.Back IRTBKeysSelect = IRTBSelectionCase.KeysSpecial Case Keys.Delete IRTBKeysSelect = IRTBSelectionCase.KeysSpecial RTBGetLineCol() IRTBTextContainer_SelectionChanged(Nothing, Nothing) Case CType(CInt(e.Control = True) And Keys.V, Keys) '*******Here is how to catch CTRL+KEYS IRTBKeysSelect = IRTBSelectionCase.KeysSpecial Case CType(CInt(e.Alt = True) And Keys.B, Keys) Dim LineColArray As Array LineColArray = Split(GetLineCol, ",") If MarkedLines.Contains(CInt(LineColArray.GetValue(0).ToString)) = False Then MarkedLines.Add(CInt(LineColArray.GetValue(0).ToString)) ElseIf MarkedLines.Contains(CInt(LineColArray.GetValue(0).ToString)) = True Then MarkedLines.Remove(CInt(LineColArray.GetValue(0).ToString)) End If RaiseEvent BMLInformation(True) IRTBForceONPaint = True PBNumbering.Invalidate() Case CType(CInt(e.Control) And Keys.Home, Keys) IRTBKeysSelect = IRTBSelectionCase.KeysSpecial Case CType(CInt(e.Control) And Keys.End, Keys) IRTBKeysSelect = IRTBSelectionCase.KeysSpecial Case Keys.ControlKey IRTBKeysSelect = IRTBSelectionCase.KeysSpecial If IRTBForceONPaint = True Then IRTBKeysSelect = IRTBSelectionCase.KeysSpecial PBNumbering.Invalidate() End If Case Keys.Escape '********To set the ZOOM to 1 again If RTBText.ZoomFactor > 1 Or RTBText.ZoomFactor < 1 Then RTBText.ZoomFactor = 1 IRTBForceONPaint = True PBNumbering.Invalidate() End If Case Else IRTBKeysSelect = IRTBSelectionCase.KeysNormal End Select End Sub
但是有一个特殊情况是“Delete”键。对于这个键,我们需要同时捕获 KeyUp
和 KeyDown
事件。为什么?因为如果我们不这样做,行号将不会更新,直到按键释放,或者更新会延迟,导致出现不存在的行号。
Private Sub IRTBTextContainer_KeyUp(ByVal sender As System.Object, _
ByVal e As System.Windows.Forms.KeyEventArgs) Handles IRTBTextContainer.KeyUp
'***Here you can implement different actions for different combination of keys
Select Case e.KeyCode
Case Keys.Delete
IRTBKeysSelect = IRTBSelectionCase.KeysSpecial
RTBGetLineCol()
IRTBTextContainer_SelectionChanged(Nothing, Nothing)
End Select
End Sub
到目前为止,我们已经能够捕获所有需要的按键操作。现在,我们可以检查当 SelectionChanged
事件触发时应该做什么。
Private Sub IRTBTextContainer_SelectionChanged(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles IRTBTextContainer.SelectionChanged
Dim IRTBCurrentLine As Integer
IRTBLineCount = IRTBTextContainer.Lines.Length
If IRTBTextContainer.Lines.Count = 0 Then
IRTBLineCount = 1
End If
Select Case IRTBKeysSelect
Case IRTBSelectionCase.KeysPageUpDown
RTBGetLineCol()
IRTBForceONPaint = True
PBNumbering.Invalidate()
Case IRTBSelectionCase.KeysLeftRight
IRTBCurrentLine = IRTBLineNumber
RTBGetLineCol()
If IRTBLineNumber <> IRTBCurrentLine Then
IRTBForceONPaint = True
End If
PBNumbering.Invalidate()
Case IRTBSelectionCase.KeysSpecial
RTBGetLineCol()
IRTBForceONPaint = True
PBNumbering.Invalidate()
Case IRTBSelectionCase.KeysNormal
RTBGetLineCol()
Case IRTBSelectionCase.Mouse
RTBGetLineCol()
PBNumbering.Invalidate()
End Select
End Sub
正如你所见,有一些条件,请让我一一解释。
IRTBSelectionCase.KeysNormal
选择此选项意味着用户正在输入普通字符,因此唯一需要执行的操作是获取行和列的位置。
RTBSelectionCase.KeysPageUpDown 或 IRTBSelectionCase.KeysSpecial
在这种情况下,按下的键可能是 BACK、ENTER、DELETE、PgDOWN、PgUP,因此必须更新行/列和高亮行。
RTBSelectionCase.KeysLeftRight
选择此选项是因为用户按了左键或右键,所以这部分代码确保正确高亮显示行并更新行/列信息。
Case IRTBSelectionCase.KeysLeftRight
IRTBCurrentLine = IRTBLineNumber
RTBGetLineCol()
If IRTBLineNumber <> IRTBCurrentLine Then
IRTBForceONPaint = True
End If
PBNumbering.Invalidate()
用于此目的的子例程称为 RTBGetLineCol
。它用于获取当前行和列,但我稍后会详细介绍,因为它是我在这篇文章中要介绍的第三个问题。
注意: 请注意,有一个场景下,行号和行/列信息不会被正确更新。当箭头键(上/下)被按住时,操作会执行,但由于需要计算才能绘制数字和高亮行,取决于文件大小(大于 350KB),信息将在按键释放后才更新。不幸的是,到目前为止我还没有解决这个问题。.
更正:现在,在按住箭头键(上/下)的同时,行号和高亮行也会被更新。为此,我更改了以下指令:
PBNumbering.Invalidate()
改为:
PBNumbering.Refresh()
到目前为止,我们已经能够根据按键、鼠标点击绘制矩形,但是当滚动并且光标超出可见范围时会发生什么?我们如何检测它何时再次进入可见范围?以下几行代码可以做到:
If EnableHighLight = True Then
'**Here it will redraw the highlight line when the caret position is coming inside the VisibleClipBounds
Dim CaretPos As Point = IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexOfCurrentLine)
If (CaretPos.Y > -10 And CaretPos.Y < 20) Or (CaretPos.Y < IRTBTextContainer.Height + _
30 And CaretPos.Y > IRTBTextContainer.Height - 100) Then
OnPaint(Nothing)
End If
End If
这段代码是处理 RichTextBox
的 VScroll
事件的子例程的一部分,仅在启用了高亮当前行时使用。
行和列信息
现在我们有了行号显示和高亮功能,最后一点是获取当前的行和列。这些值在这个子例程中计算:
Private Sub RTBGetLineCol()
Dim GetFirstCharIndex As Integer = IRTBTextContainer.GetFirstCharIndexOfCurrentLine
Dim GetRTBLine = IRTBTextContainer.GetLineFromCharIndex(GetFirstCharIndex)
Dim GetPosition As Integer = IRTBTextContainer.SelectionStart - GetFirstCharIndex
If GetPosition < 0 Then GetPosition = 0
If GetRTBLine >= IRTBTextContainer.Lines.Count Then GetRTBLine = IRTBTextContainer.Lines.Count - 1
If GetRTBLine = -1 Then GetRTBLine = 0
IRTBLineNumber = GetRTBLine
IRTBColumnNumber = GetPosition
RaiseEvent LineInformation(GetRTBLine + 1 & "," & GetPosition + 1)
End Sub
在计算获取行和列之后,我们需要“发布”它们,因此我们触发一个已声明的事件。
Public Event LineInformation(ByVal LineStatus As String)
所以,这个子例程的最后一行就是这样做的,它提供了显示行和列所需的信息。
RaiseEvent LineInformation(GetRTBLine + 1 & "," & GetPosition + 1)
以下是当在附带示例中使用此事件提供的信息时它的外观:
这是 IRTB 测试窗体中用于获取信息的代码:
Private Sub ImprovedRTB1_LineInformation(ByVal LineStatus As System.String) Handles Irtb1.LineInformation
Dim LineInfo As Array
LineInfo = Split(LineStatus, ",")
LineColInformation.Text = "Line:" & LineInfo(0) & " Col:" & LineInfo(1)
TextBox7.Text = LineStatus
End Sub
新增! 现在可以使用名为 IRTBLineCol
的属性读取行和列信息。要获取信息,可以使用名为 GetLineCol
的方法。例如,当一个窗体有多个选项卡并且在它们之间切换时,LineInformation
事件不会被触发,所以获取信息而不必等待事件的唯一方法是通过这个新属性。
附加功能
作为项目的一部分,我决定添加一些额外功能。让我们从拖放开始。
拖放
我决定实现文件的拖放功能,这样就不需要在项目中实现该选项了。因此,这部分代码位于“Extras”区域下,并且相当简单。
#Region "Extra Features" 'Drop Files
Private Sub IRTBDragDrop(ByVal sender As Object, ByVal e As _
System.Windows.Forms.DragEventArgs) Handles IRTBTextContainer.DragDrop
Dim myFiles() As String
myFiles = e.Data.GetData(DataFormats.FileDrop)
For Each mc_file In myFiles
Dim FileName As String = mc_file.ToString
LoadFileAndNumbering(FileName)
RaiseEvent DragDropFileInformation(FileName)
Next
End Sub
Private Sub IRTBDragEnter(ByVal sender As Object, ByVal e As _
System.Windows.Forms.DragEventArgs) Handles IRTBTextContainer.DragEnter
If e.Data.GetDataPresent(DataFormats.FileDrop) Then
e.Effect = DragDropEffects.All
End If
End Sub
#End Region
正如你所注意到的,有一个名为 DragDropFileInformation
的事件。通过在 Windows 窗体中使用此事件,我们可以获取文件名。因此,这是用于此目的的代码:
Private Sub DragDropnInformation(ByVal FileName As System.String) Handles Irtb1.DragDropFileInformation
If System.IO.File.Exists(FileName) Then
Irtb1.FileToLoad = FileName
Dim finfo As New FileInfo(FileName)
TextBox10.Text = finfo.Name
TextBox6.Text = Str(Math.Round(finfo.Length / 1024)) & "Kb"
TextBox9.Text = Irtb1.IRTBContainer.Lines.Count.ToString
TextBox8.Text = Str(Irtb1.ShowTotalChar)
End If
End Sub
但请记住,要在控件上使用拖放,需要将 IRTBTextContainer.AllowDrop
属性设置为 TRUE。在加载控件时已经完成了此操作。
缩放
还有一件事需要解释,那就是如何处理行号显示和高亮当前行启用时的缩放选项。正如我们所知,RichTextBox 中常用的缩放快捷键是 CTRL+滚轮。但如何处理呢?高亮行和行号显示会受到什么影响?
首先,我们需要检测滚动操作和 Ctrl 键。以下是检测鼠标滚轮的代码:
Private Sub IRTBCheckScoll(ByVal sender As Object, ByVal e As _
System.Windows.Forms.MouseEventArgs) Handles IRTBTextContainer.MouseWheel
If IRTBKeysSelect = IRTBSelectionCase.KeysSpecial Then
IRTBForceONPaint = True
End If
End Sub
我们需要检测是否按下了 CTRL 键,可以使用以下代码:
Case Keys.ControlKey
IRTBKeysSelect = IRTBSelectionCase.KeysSpecial
If IRTBForceONPaint = True Then
IRTBKeysSelect = IRTBSelectionCase.KeysSpecial
RTBGetLineCol()
PBNumbering.Invalidate()
OnPaint(Nothing)
IRTBForceONPaint = False
End If
这里检测到两个键:CTRL 和 Escape。检测到 CTRL 时,IRTBKeysSelect
设置为 IRTBSelectionCase.KeysSpecial
;当触发滚轮操作时,IRTBForceONPaint
设置为 true。通过这样做,我们可以确保在缩放过程中,数字和高亮行都会被刷新。请注意,书签图标不会被缩放或重绘。
按下 Escape 键时,缩放将设置为 1,行号、高亮行和书签图标将重新绘制。这是检测 Escape 键的代码:
Case Keys.Escape
'********To set the ZOOM to 1 again
If RTBText.ZoomFactor > 1 Or RTBText.ZoomFactor < 1 Then
RTBText.ZoomFactor = 1
PBNumbering.Invalidate()
RTBGetLineCol()
OnPaint(Nothing)
End If
书签行 - 新功能 2013.04.02
此功能不是 IRTB 最初开发的一部分。然而,随着时间的推移,在使用该控件的某些项目中,我感觉到这部分功能缺失了,所以我决定添加它并更新文章。
此新功能可以通过两种方式使用:
- 通过单击行号。
- 通过使用 Alt+B 组合键。如果该行不在标记行数组中,则会添加,否则会被删除。
Private Sub MarkLines(ByVal sender As System.Object, ByVal e As System.Windows.Forms.MouseEventArgs) Dim J As Integer Dim PBNumberingTemp As New PictureBox Dim StartLine As Integer Dim EndLine As Integer Dim GetLines As Array Dim CountLine As Integer = 1 Dim LineSelected As Integer = 0 GetLines = Split(FromLineToLine, ",") StartLine = CInt(GetLines.GetValue(0)) EndLine = CInt(GetLines.GetValue(1)) Dim font_height As Single = IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexFromLine(2)).Y - IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexFromLine(1)).Y If font_height <= 0 Then font_height = IRTBTextContainer.Font.Height + 1 PBNumberingTemp = CType(sender, PictureBox) PBNumberingTemp.Image = New Bitmap(PBNumberingTemp.ClientSize.Width, PBNumberingTemp.ClientSize.Height) Dim g As Graphics = Graphics.FromImage(PBNumberingTemp.Image) Dim firstIndex As Integer = IRTBTextContainer.GetCharIndexFromPosition(New Point(0, CInt(g.VisibleClipBounds.Y + font_height / 3))) Dim firstLineY As Integer = IRTBTextContainer.GetPositionFromCharIndex(firstIndex).Y Dim ClickPosition As Point ClickPosition.X = e.X ClickPosition.Y = e.Y Dim LineNumber As Integer = CInt(ClickPosition.Y / font_height) For J = StartLine To EndLine If CountLine = LineNumber Then If firstLineY < 0 Then LineSelected = J + 1 Else LineSelected = J End If If MarkedLines.Contains(LineSelected) = False Then MarkedLines.Add(LineSelected) RaiseEvent BMLInformation(True) Exit For ElseIf MarkedLines.Contains(LineSelected) = True Then MarkedLines.Remove(LineSelected) RaiseEvent BMLInformation(True) Exit For End If End If CountLine += 1 Next End Sub
这里的主要问题之一是找到一种方法,通过单击持有行号的 PictureBox 来获取行号。这花了我一些时间,但我终于想明白了(如果有人有更好的方法,请告诉我)。首先,我们需要知道字体高度:
Dim font_height As Single = IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexFromLine(1)).Y - IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexFromLine(0)).Y If font_height <= 0 Then font_height = IRTBTextContainer.Font.Height + 1
第一行的目的是找到当前使用的行号显示字体的字体高度值。第二行将在第一种计算小于或等于 0 的情况下进行校正。
完成此操作后,我们需要计算可见剪辑边界内的第一个行号的 Y 轴位置以及我们点击的点。一旦我们有了这些,我们就可以计算行号,但请记住,这个行号将是一个介于 0 到可见剪辑边界中显示的最多行号之间的值。
PBNumberingTemp = CType(sender, PictureBox)
PBNumberingTemp.Image = New Bitmap(PBNumberingTemp.ClientSize.Width, PBNumberingTemp.ClientSize.Height)
Dim g As Graphics = Graphics.FromImage(PBNumberingTemp.Image)
Dim firstIndex As Integer = IRTBTextContainer.GetCharIndexFromPosition(New Point(0, CInt(g.VisibleClipBounds.Y + font_height / 3)))
Dim firstLineY As Integer = IRTBTextContainer.GetPositionFromCharIndex(firstIndex).Y
Dim ClickPosition As Point
ClickPosition.X = e.X
ClickPosition.Y = e.Y
Dim LineNumber As Integer = CInt(ClickPosition.Y / font_height)
那么,我们如何获得正确的行号呢?使用这些代码行:
GetLines = Split(FromLineToLine, ",")
StartLine = CInt(GetLines.GetValue(0))
EndLine = CInt(GetLines.GetValue(1))
这些代码行给出图片框中显示的起始和结束行号,所以现在我们可以计算我们实际点击的行。
For J = StartLine To EndLine
If CountLine = LineNumber Then
If firstLineY < 0 Then
LineSelected = J + 1
Else
LineSelected = J
End If
If MarkedLines.Contains(LineSelected) = False Then
MarkedLines.Add(LineSelected)
RaiseEvent BMLInformation(True)
Exit For
ElseIf MarkedLines.Contains(LineSelected) = True Then
MarkedLines.Remove(LineSelected)
RaiseEvent BMLInformation(True)
Exit For
End If
End If
CountLine += 1
Next
MarkedLines
是一个 ArrayList,用于保存已标记的书签行。如果行号不在集合中,则会添加;否则会被移除。此集合可以通过名为 IRTBMarkedLines
的属性访问。
此过程的最后一部分是新的 BMLInformation
事件,它将在添加或移除新行时被触发,以通知用户。
那么,如何使用 ALT+B 来标记行?将以下几行添加到 RTBTextContainer
方法中:
Case CType(CInt(e.Alt = True) And Keys.B, Keys)
Dim LineColArray As Array
LineColArray = Split(GetLineCol, ",")
If MarkedLines.Contains(CInt(LineColArray.GetValue(0).ToString)) = False Then
MarkedLines.Add(CInt(LineColArray.GetValue(0).ToString))
ElseIf MarkedLines.Contains(CInt(LineColArray.GetValue(0).ToString)) = True Then
MarkedLines.Remove(CInt(LineColArray.GetValue(0).ToString))
End If
RaiseEvent BMLInformation(True)
IRTBForceONPaint = True
PBNumbering.Invalidate()
使用 GetLineCol 函数,我们可以获取当前光标所在的行。所以只需按下 ALT+B 即可将行添加到 MarkedLines
集合中或从中移除,并且会触发新的 BMLInformation
事件来通知已添加新行。
我为这个功能添加了另一个选项:可以使用以下属性检索书签行的列表:
- IRTBMarkedLines
一旦你有了行集合,你就可以使用以下方法跳转到 IRTB 控件中的行:
- GoToBookmark(linenumber)
Public Sub GoToBookmark(ByVal LineNumber As Integer) Dim FirstCHRInLine As Integer = IRTBTextContainer.GetFirstCharIndexFromLine(LineNumber - 1) IRTBContainer.SelectionStart = FirstCHRInLine IRTBContainer.SelectionLength = 0 IRTBContainer.ScrollToCaret() IRTBKeysSelect = 3 IRTBForceONPaint = True PBNumbering.Refresh() End Sub
在 IRTB_Demo.zip 中,你可以找到如何使用此选项。
XML 解析器
此选项不是我编写的,我只是添加了它作为额外的功能。所有代码均来自 MSDN,可以在这里找到:
http://code.msdn.microsoft.com/windowsdesktop/VBRichTextBoxSyntaxHighligh-2d18b6cc
请注意,此代码部分受 Microsoft 公共许可证约束。在此处查看:
据我所见,它工作得相当好。因此,如果你想进一步了解 XML 解析器的工作原理,请谷歌搜索“XML Parser”,你将获得大量信息,包括 CodeProject 上的一些文章链接,例如:
https://codeproject.org.cn/Articles/176236/Parsing-an-XML-file-in-a-C-C-program
如何使用?只需在你的窗体上放置一个按钮,然后在你的项目中编写类似以下代码:
Private Sub IRTBXMLParser_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles IRTBXMLParser.Click
Irtb1.Process(True)
End Sub
这里的 Irtb1 是 FORM1 上 IRTB 控件的名称。
结论
文章开头提到的三个点已经介绍完毕,我希望这些信息足以给你一些启发。不幸的是,你会注意到,当启用行号显示和/或高亮当前行时,RichTextBox 的性能会受到影响。为什么会这样?主要原因是使用了这些代码行:
Dim CaretPos As Point = IRTBTextContainer.GetPositionFromCharIndex(IRTBTextContainer.GetFirstCharIndexOfCurrentLine)
...
...
Dim firstIndex As Integer = IRTBTextContainer.GetCharIndexFromPosition(New Point(0, g.VisibleClipBounds.Y + font_height / 3))
Dim firstLine As Integer = IRTBTextContainer.GetLineFromCharIndex(firstIndex)
Dim firstLineY As Integer = IRTBTextContainer.GetPositionFromCharIndex(firstIndex).Y
当文件大小增加时,这些计算非常消耗 CPU 资源,因此控件的性能会受到影响。正如我在其他文章中读到的,并且我同意,如果你需要一个高性能控件,你有两个选择:从头开始构建你自己的控件(请查看 Code Project 上的这个项目:https://codeproject.org.cn/Articles/161871/Fast-Colored-TextBox-for-syntax-highlighting)或尝试 scintilla(scintilla)。
关于需要重新绘制数字和高亮行的操作,处理按键并不总是那么容易,但所有组合都可以被检测到,这样我们就可以选择应该执行哪些操作。
现在让我们看看如何使用该控件。
使用控件
本文中有两个 zip 文件:
- IRTB_CodeProject.zip
- IRTB_Demo_new.zip
要查看 IRTB 控件的功能,请下载 IRTB_Demo_new.zip,将其解压缩到任何你想要的地方,然后双击 IRTB_Test.exe。
IRTB_CodeProject.zip 包含解决方案。解压缩该文件并打开解决方案。我添加了两个项目,所以当文件打开时,你会看到以下内容:
名为 IRTB
的是控件,名为 IRTB_Test
的是一个 Windows 窗体,用于检查控件的基本功能。
IRTB 控件公开的属性有:
IRTBFileToLoad
IRTBFont
IRTBLNFontColor
IRTBHighLightColor
新名称ShowTotalChar
IRTBShowTotalChar
。IRTBEnableNumbering
IRTBEnableHighLight
RTBContainer
RTBnumbering
IRTBPrefix
IRTBAlignNumbers
IRTBMarkedLines
>> 包含书签行,因此应用程序可以读取信息并跳转到标记的行。请参阅 GoToBookmark 方法。IRTBLineCol
>> 行和列信息通过名为 LineInformation 的事件提供,但如果应用程序需要获取此信息而不必等待事件触发,则可以通过此属性完成。
添加 Windows 窗体的目的是展示该控件的功能,请检查代码,我希望它足够清晰,并展示了 IRTB 控件的功能。
如果你要在你的个人项目中使用 IRTB 控件,请不要忘记在引用选项卡中添加 DLL,否则你将无法使用它。请注意,新版本是 2.5.0.0。
这是在 IRTB_Test
Windows 窗体上使用的代码。我已经添加了新选项(2013.04.02)到窗体中。
Imports System.IO Public Class Form1 Private Sub ImprovedRTB1_LineInformation(ByVal LineStatus As System.String) Handles Irtb1.LineInformation Dim LineInfo As Array LineInfo = Split(LineStatus, ",") LineColInformation.Text = "Line:" & LineInfo(0) & " Col:" & LineInfo(1) TextBox7.Text = LineStatus End Sub Private Sub DragDropnInformation(ByVal FileName As System.String) Handles Irtb1.DragDropFileInformation If System.IO.File.Exists(FileName) Then Irtb1.IRTBMarkedLines.Clear() BMLinesTool.Items.Clear() Irtb1.IRTBFileToLoad = FileName Dim finfo As New FileInfo(FileName) TextBox10.Text = finfo.Name TextBox6.Text = Str(Math.Round(finfo.Length / 1024)) & "Kb" TextBox9.Text = Irtb1.IRTBContainer.Lines.Count.ToString TextBox8.Text = Str(Irtb1.IRTBShowTotalChar) End If End Sub Private Sub IRTBCheckLN_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles IRTBCheckLN.CheckedChanged Irtb1.IRTBEnableNumbering = IRTBCheckLN.Checked End Sub Private Sub IRTBCheckHL_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles IRTBCheckHL.CheckedChanged Irtb1.IRTBEnableHighLight = IRTBCheckHL.Checked End Sub Private Sub IRTBFont_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles IRTBFont.Click FontDialog1.ShowDialog() Irtb1.IRTBFont = FontDialog1.Font End Sub Private Sub IRTBHLColor_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles IRTBHLColor.Click SelectColor.Color = Irtb1.IRTBHighLightColor SelectColor.ShowDialog() Irtb1.IRTBHighLightColor = SelectColor.Color End Sub Private Sub Button4_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button4.Click TestLoadFile.ShowDialog() If System.IO.File.Exists(TestLoadFile.FileName) Then Irtb1.IRTBFileToLoad = TestLoadFile.FileName Irtb1.IRTBMarkedLines.Clear() BMLinesTool.Items.Clear() Dim finfo As New FileInfo(TestLoadFile.FileName) TextBox10.Text = finfo.Name TextBox6.Text = Str(Math.Round(finfo.Length / 1024)) & "Kb" TextBox9.Text = Irtb1.IRTBContainer.Lines.Count.ToString TextBox8.Text = Str(Irtb1.IRTBShowTotalChar) End If End Sub Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load IRTBCheckLN.Checked = Irtb1.IRTBEnableNumbering IRTBCheckHL.Checked = Irtb1.IRTBEnableHighLight BMLinesTool.Items.Clear() Irtb1.IRTBPrefix = False RadioButton1.Checked = True End Sub Private Sub RadioButton1_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles RadioButton1.CheckedChanged Irtb1.IRTBAlignNumbers = IRTB.IRTB.IRTBAlignPos.Left End Sub Private Sub RadioButton2_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles RadioButton2.CheckedChanged Irtb1.IRTBAlignNumbers = IRTB.IRTB.IRTBAlignPos.Right End Sub Private Sub CheckBox1_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles CheckBox1.CheckedChanged Irtb1.IRTBPrefix = CheckBox1.Checked End Sub Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click SelectColor.Color = Irtb1.IRTBLNFontColor SelectColor.ShowDialog() Irtb1.IRTBLNFontColor = SelectColor.Color End Sub Private Sub IRTBXMLParser_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles IRTBXMLParser.Click Irtb1.Process(True) End Sub Private Sub Irtb1_BMLInformation(ByVal NewEvent As System.Boolean) Handles Irtb1.BMLInformation BMLinesTool.Items.Clear() For Each BMLineTemp In Irtb1.IRTBMarkedLines BMLinesTool.Items.Add("Line " & BMLineTemp) Next End Sub Private Sub BMLinesTool_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BMLinesTool.SelectedIndexChanged If Not BMLinesTool.SelectedItem Is Nothing Then Dim LineNumber As Integer = CInt(Replace(BMLinesTool.SelectedItem.ToString, "Line ", "")) Irtb1.GoToBookmark(LineNumber) End If End Sub End Class
为了展示新功能,我在原始演示中添加了两个组件。第一个是名为 “Check XML” 的 button
,用于执行 XML 解析。请注意,如果文件不是 XML 格式,该过程将抛出错误。这可以被捕获以显示带有错误信息的 MessageBox。为此,这是使用的代码:
Private Sub IRTBXMLParser_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles IRTBXMLParser.Click
Try
Irtb1.Process(True)
Catch appException As ApplicationException
MessageBox.Show(appException.Message, "ApplicationException")
Catch ex As Exception
MessageBox.Show(ex.Message, "Exception")
End Try
End Sub
第二个是名为 BMLinesTool
的 ListBox
。这个列表框将显示书签行。通过单击列表中的任何一个项目,将使用 GoToBookmark
方法,该行将被高亮显示。
Private Sub BMLinesTool_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BMLinesTool.SelectedIndexChanged If Not BMLinesTool.SelectedItem Is Nothing Then Dim LineNumber As Integer = CInt(Replace(BMLinesTool.SelectedItem.ToString, "Line ", "")) Irtb1.GoToBookmark(LineNumber) End If End Sub
请审查项目,检查代码,如果你有任何疑问,请告诉我。我会尽力以最好的方式回答。
历史
- 2012年2月1日
- 2012年2月21日
- 根据收到的评论进行的小幅修正。
- 2013年4月3日
- 新增功能。书签行和 XML 解析器。
ShowTotalChar
属性名称已更改为IRTBShowTotalChar
。