You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何同步WinForms RichTextBox客户区与非客户区的绘制

WinForms RichTextBox行号同步问题解决方案

我需要给WinForms的RichTextBox添加行号这类上下文信息,一开始选择了非客户区绘制的方案,避免客户区绘制在横向滚动时出现的严重闪烁问题,但实际实现中遇到了几个核心问题:

  • 滚动(横向/纵向)时非客户区不触发重绘,行号无法跟随文本滚动
  • 调整非客户区大小后,控件绘制异常
  • 操作非客户区时滚动条会消失,调用Invalidate()修复又导致性能下降
  • 文本内容更新(比如新增行)时,非客户区无法同步更新

下面分别给出非客户区方案的修复方法,以及客户区绘制方案的闪烁优化方案。


一、修复非客户区绘制同步问题

1. 捕获滚动相关消息触发重绘

RichTextBox滚动时会发送WM_VSCROLL、WM_HSCROLL和WM_MOUSEWHEEL消息,这些消息不会默认触发WM_NCPAINT,所以需要在WndProc中手动捕获,强制非客户区重绘。同时监听WM_PAINT消息,确保文本更新时同步非客户区。

修改后的WndProc代码:

Imports System.Runtime.InteropServices

Public Class LineNumberRichTextBox
    Inherits RichTextBox

    ' Windows消息常量
    Private Const WM_NCCALCSIZE As Integer = &H83
    Private Const WM_NCPAINT As Integer = &H85
    Private Const WM_VSCROLL As Integer = &H115
    Private Const WM_HSCROLL As Integer = &H114
    Private Const WM_MOUSEWHEEL As Integer = &H20A
    Private Const WM_PAINT As Integer = &HF
    Private Const SB_HORZ As Integer = 0
    Private Const SB_VERT As Integer = 1

    ' 外部API声明
    <DllImport("user32.dll")>
    Private Shared Function SendMessage(hWnd As IntPtr, msg As Integer, wParam As IntPtr, lParam As IntPtr) As IntPtr
    End Function

    <DllImport("user32.dll")>
    Private Shared Function GetWindowDC(hWnd As IntPtr) As IntPtr
    End Function

    <DllImport("user32.dll")>
    Private Shared Function ReleaseDC(hWnd As IntPtr, hdc As IntPtr) As Integer
    End Function

    <DllImport("user32.dll")>
    Private Shared Function GetScrollPos(hWnd As IntPtr, nBar As Integer) As Integer
    End Function

    Protected Overrides Sub WndProc(ByRef m As Message)
        Select Case m.Msg
            Case WM_NCCALCSIZE
                OnNcCalcSize(m)
            Case WM_NCPAINT, WM_VSCROLL, WM_HSCROLL, WM_MOUSEWHEEL
                ' 滚动或鼠标滚轮时强制重绘非客户区
                OnNcPaint(m)
            Case WM_PAINT
                ' 文本更新时同步非客户区
                InvalidateNonClientArea()
        End Select

        ' 必须调用基类方法,保证滚动条等系统元素正常绘制
        MyBase.WndProc(m)
    End Sub

    ' 强制非客户区无效并重绘
    Private Sub InvalidateNonClientArea()
        Dim nonClientRect As Rectangle = Me.Bounds
        nonClientRect.Offset(-Me.Location.X, -Me.Location.Y)
        Me.Invalidate(nonClientRect, False)
        SendMessage(Me.Handle, WM_NCPAINT, IntPtr.Zero, IntPtr.Zero)
    End Sub

2. 同步滚动位置绘制行号

在OnNcPaint方法中,获取当前滚动偏移,计算可见行范围,确保行号跟随文本滚动:

Protected Sub OnNcPaint(ByRef m As Message)
        Dim hdc As IntPtr = GetWindowDC(Me.Handle)
        Using g As Graphics = Graphics.FromHdc(hdc)
            ' 获取当前滚动位置
            Dim scrollX As Integer = GetScrollPos(Me.Handle, SB_HORZ)
            Dim scrollY As Integer = GetScrollPos(Me.Handle, SB_VERT)

            ' 行号区域宽度(可自定义)
            Dim lineNumberWidth As Integer = 40
            Dim lineNumberRect As New Rectangle(0, 0, lineNumberWidth, Me.Height)

            ' 填充行号区域背景,和控件背景保持一致
            g.FillRectangle(New SolidBrush(Me.BackColor), lineNumberRect)

            ' 计算可见的起始行和结束行
            Dim firstVisibleChar As Integer = Me.GetCharIndexFromPosition(New Point(0, scrollY))
            Dim firstVisibleLine As Integer = Me.GetLineFromCharIndex(firstVisibleChar)
            Dim lastVisibleChar As Integer = Me.GetCharIndexFromPosition(New Point(0, scrollY + Me.ClientSize.Height))
            Dim lastVisibleLine As Integer = Me.GetLineFromCharIndex(lastVisibleChar)

            ' 绘制行号
            Using lineFont As New Font(Me.Font.FontFamily, Me.Font.Size, FontStyle.Regular)
                Dim lineHeight As Integer = CInt(g.MeasureString("X", lineFont).Height)
                ' 计算行号的Y轴起始位置,抵消滚动偏移
                Dim yPos As Integer = (firstVisibleLine * lineHeight) - scrollY

                For lineIndex As Integer = firstVisibleLine To lastVisibleLine
                    g.DrawString((lineIndex + 1).ToString(), lineFont, Brushes.DarkGray, 5, yPos)
                    yPos += lineHeight
                Next
            End Using
        End Using
        ReleaseDC(Me.Handle, hdc)
    End Sub

3. 正确调整非客户区大小并同步绘制

在OnNcCalcSize中,调整客户区位置给行号留出空间,调整后强制全控件重绘,避免绘制异常:

' 非客户区大小计算相关结构
    <StructLayout(LayoutKind.Sequential)>
    Private Structure NCCALCSIZE_PARAMS
        Public rgrc As Rectangle()
        Public lppos As WINDOWPOS
    End Structure

    <StructLayout(LayoutKind.Sequential)>
    Private Structure WINDOWPOS
        Public hwnd As IntPtr
        Public hwndInsertAfter As IntPtr
        Public x As Integer
        Public y As Integer
        Public cx As Integer
        Public cy As Integer
        Public flags As Integer
    End Structure

    Protected Sub OnNcCalcSize(ByRef m As Message)
        ' 先让系统计算默认布局
        MyBase.WndProc(m)
        If m.WParam <> IntPtr.Zero Then
            Dim ncParams As NCCALCSIZE_PARAMS = DirectCast(Marshal.PtrToStructure(m.LParam, GetType(NCCALCSIZE_PARAMS)), NCCALCSIZE_PARAMS)
            ' 左侧留出40px作为行号区域
            ncParams.rgrc(0).Left += 40
            Marshal.StructureToPtr(ncParams, m.LParam, True)
            m.Result = IntPtr.Zero
        End If
        ' 调整后强制重绘整个控件,确保所有区域正常显示
        Me.Invalidate(True)
    End Sub

4. 文本更新时同步非客户区

监听TextChanged事件,文本更新时立即触发非客户区重绘:

Private Sub LineNumberRichTextBox_TextChanged(sender As Object, e As EventArgs) Handles Me.TextChanged
        InvalidateNonClientArea()
    End Sub
End Class

二、优化客户区绘制的闪烁问题

如果不想使用非客户区方案,可以通过以下方式优化客户区绘制的闪烁问题:

核心优化点

  1. 开启双缓冲:继承RichTextBox后手动开启双缓冲
  2. 局部重绘:只重绘行号区域,而非整个控件
  3. 减少无效触发:滚动时仅重绘行号区域,避免全控件刷新

示例代码:

Public Class BufferedLineNumberRichTextBox
    Inherits RichTextBox

    Private Const LineNumberWidth As Integer = 40

    Public Sub New()
        ' 开启双缓冲和全区域绘制优化
        Me.SetStyle(ControlStyles.OptimizedDoubleBuffer Or ControlStyles.AllPaintingInWmPaint Or ControlStyles.UserPaint, True)
        Me.UpdateStyles()
    End Sub

    Protected Overrides Sub OnPaint(e As PaintEventArgs)
        ' 先绘制RichTextBox原有内容
        MyBase.OnPaint(e)

        ' 绘制行号
        Dim firstVisibleChar As Integer = Me.GetCharIndexFromPosition(New Point(0, 0))
        Dim firstVisibleLine As Integer = Me.GetLineFromCharIndex(firstVisibleChar)
        Dim lastVisibleChar As Integer = Me.GetCharIndexFromPosition(New Point(0, Me.ClientSize.Height))
        Dim lastVisibleLine As Integer = Me.GetLineFromCharIndex(lastVisibleChar)

        Dim lineHeight As Integer = CInt(e.Graphics.MeasureString("X", Me.Font).Height)
        Dim yPos As Integer = 0

        Using lineFont As New Font(Me.Font.FontFamily, Me.Font.Size, FontStyle.Regular)
            For lineIndex As Integer = firstVisibleLine To lastVisibleLine
                e.Graphics.DrawString((lineIndex + 1).ToString(), lineFont, Brushes.DarkGray, 5, yPos)
                yPos += lineHeight
            Next
        End Using
    End Sub

    ' 滚动时仅重绘行号区域
    Protected Overrides Sub OnVScroll(e As EventArgs)
        MyBase.OnVScroll(e)
        Me.Invalidate(New Rectangle(0, 0, LineNumberWidth, Me.Height))
    End Sub

    Protected Overrides Sub OnHScroll(e As EventArgs)
        MyBase.OnHScroll(e)
        Me.Invalidate(New Rectangle(0, 0, LineNumberWidth, Me.Height))
    End Sub

    ' 文本更新时重绘行号区域
    Protected Overrides Sub OnTextChanged(e As EventArgs)
        MyBase.OnTextChanged(e)
        Me.Invalidate(New Rectangle(0, 0, LineNumberWidth, Me.Height))
    End Sub
End Class

内容的提问来源于stack exchange,提问作者mike

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 23:05:56