如何同步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
二、优化客户区绘制的闪烁问题
如果不想使用非客户区方案,可以通过以下方式优化客户区绘制的闪烁问题:
核心优化点
- 开启双缓冲:继承RichTextBox后手动开启双缓冲
- 局部重绘:只重绘行号区域,而非整个控件
- 减少无效触发:滚动时仅重绘行号区域,避免全控件刷新
示例代码:
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
相关产品推荐
相关产品推荐

