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

增强型 RichTextBox - IRTB

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (16投票s)

2012 年 2 月 1 日

CPOL

17分钟阅读

viewsIcon

77480

downloadIcon

3728

本文介绍了一种实现行号显示、高亮当前行以及显示当前行和列的简单方法。

 

323059/IRTB.png

介绍 

本文介绍了我称之为改进的 RichTextBox 控件。为何改进?嗯,浏览互联网,你会发现很多评论指出微软提供的控件缺少各种功能,但其中三个最明显的功能是:

  • 行号显示
  • 高亮当前行
  • 显示当前行和列

第一个功能已在多篇文章中介绍,例如以下几篇:

当然,你还可以找到更多与行号显示相关的文章和代码片段。但尽管这个问题已经解决,高亮当前行和显示当前行/列仍然是悬而未决的问题。对于所有使用 Notepad++ 或 PSPad 的用户来说,很明显,一个很棒的功能是高亮显示当前光标所在的行,当然,能够指示行/列信息也非常有帮助。

那么,本文能带给你什么?我将通过这三个方面,展示我如何为自己的项目解决了这些问题。请注意,我刚开始接触 .NET 世界,如果有什么可以改进或用其他方式实现的地方,请告诉我。我希望这里的信息对那些为自己的项目寻求一个简单小巧的解决方案的人有所帮助。

代码分为以下“区域”:

  • 区域“ImprovedRTB 属性”
  • 区域“通用子例程”
  • 区域“IRTB 事件”
  • 区域“附加功能”

实现

该控件由两个组件组成:一个 RichTextBox 和一个 PictureBoxPictureBox 将用于绘制行号,而 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),但我们如何使用它?什么时候调用它?现在我们需要检查 PictureBoxPaint 事件。如何触发这个事件?以及在什么条件下应该这样做?必须使用以下事件:

  • 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

现在,数字将根据 RichTextBoxVisibleClipBounds 中显示的行进行绘制。

高亮当前行

好的,既然我们已经绘制了数字,就可以尝试获得一个高亮显示当前行的矩形。要做到这一点,我们需要使用标准的 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

结果会是这样:

IRTB Example

图 1. 高亮当前行

太棒了!现在我们有一个绘制在 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”键。对于这个键,我们需要同时捕获 KeyUpKeyDown 事件。为什么?因为如果我们不这样做,行号将不会更新,直到按键释放,或者更新会延迟,导致出现不存在的行号。

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

这段代码是处理 RichTextBoxVScroll 事件的子例程的一部分,仅在启用了高亮当前行时使用。

行和列信息

现在我们有了行号显示和高亮功能,最后一点是获取当前的行和列。这些值在这个子例程中计算:

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 Example

图 2. 行和列信息。

这是 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 最初开发的一部分。然而,随着时间的推移,在使用该控件的某些项目中,我感觉到这部分功能缺失了,所以我决定添加它并更新文章。

此新功能可以通过两种方式使用:

  1. 通过单击行号。
  2. 通过使用 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 公共许可证约束。在此处查看:

http://www.microsoft.com/opensource/licenses.mspx#Ms-PL 

据我所见,它工作得相当好。因此,如果你想进一步了解 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 Example

图 3. 在 Visual Studio 中打开的解决方案

名为 IRTB 的是控件,名为 IRTB_Test 的是一个 Windows 窗体,用于检查控件的基本功能。

IRTB 控件公开的属性有:

  • IRTBFileToLoad
  • IRTBFont
  • IRTBLNFontColor 
  • IRTBHighLightColor
  • ShowTotalChar 新名称 IRTBShowTotalChar
  • IRTBEnableNumbering
  • IRTBEnableHighLight 
  • RTBContainer
  • RTBnumbering
  • IRTBPrefix 
  • IRTBAlignNumbers 
有两个新属性(2013.04.02):
  • IRTBMarkedLines >> 包含书签行,因此应用程序可以读取信息并跳转到标记的行。请参阅 GoToBookmark 方法。
  • IRTBLineCol >> 行和列信息通过名为 LineInformation 的事件提供,但如果应用程序需要获取此信息而不必等待事件触发,则可以通过此属性完成。

IRTB Example

图 4. IRTB 属性

添加 Windows 窗体的目的是展示该控件的功能,请检查代码,我希望它足够清晰,并展示了 IRTB 控件的功能。

如果你要在你的个人项目中使用 IRTB 控件,请不要忘记在引用选项卡中添加 DLL,否则你将无法使用它。请注意,新版本是 2.5.0.0。

IRTB Example

图 5. 将 IRTB 添加到引用选项卡

这是在 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  

第二个是名为 BMLinesToolListBox。这个列表框将显示书签行。通过单击列表中的任何一个项目,将使用 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
© . All rights reserved.