Painting on a RichTextBox Control

    General discussion

  • Drawing on top of a RichTextBox control has always been a pain due to the Paint event, and related OnPaint() method being suppressed.  Here is a bit of code that gets around this issue.

    The following derived RichTextBox will use a seperate NativeWindow control to hook into the message stream of the RichTextBox, listen for the WM_PAINT message, and then raise the Paint event on the RichTextBox for you.

    In this way, it is possible to use the Paint event handler of the RichTextBox control to paint on top of the control after it has performed its default rendering of the text.

    The following code is an initial implementation but appears to be pretty solid so far.

    'Based in part on code located at:
    ''' <summary>
    ''' A RichTextBox object which raises, and utilizes, its own Paint event.
    ''' </summary>
    ''' <remarks></remarks>
    Public Class PaintableRichTextBox
        Inherits RichTextBox
        'Re-exposes the hidden event in the designer
        Public Shadows Event Paint As EventHandler(Of PaintEventArgs)
        'Holds the native window which will provide paint messages
        Private mWindowExtender As WindowExtender
        Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        End Sub
        'Used to raise the shadowed paint event
        Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
            RaiseEvent Paint(Me, e)
        End Sub
        'Ensures underlaying graphic resources match control size
        Protected Overrides Sub OnResize(ByVal e As System.EventArgs)
        End Sub
        Public Sub New()
            mWindowExtender = New WindowExtender(Me)
        End Sub
        'Object which handles normally suppressed messages
        Public Class WindowExtender
            Inherits NativeWindow
            'The Win32 WM_PAINT message id
            Private Const WM_PAINT As Integer = 15
            'The underlying RichTextBox
            Private mBaseControl As PaintableRichTextBox
            'An off-screen image to draw to
            Private mCanvas As Bitmap
            'The graphics object associated with the canvas bitmap
            Private mBufferGraphics As Graphics
            'The clip region of the canvas graphics (visible control bounds)
            Private mBufferClip As Rectangle
            'The graphics object associated with the RichTextBox
            Private mControlGraphics As Graphics
            'Flag ensuring that the control can be drawn to
            Private mCanRender As Boolean = False
            'Handles the custom painting after a Win32 paint message
            Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
                Select Case m.Msg
                    Case WM_PAINT
                        'Refresh the control's display
                        'Perform default drawing
                        'Perform custom drawing
                    Case Else
                End Select
            End Sub
            Public Sub New(ByVal baseControl As PaintableRichTextBox)
                mBaseControl = baseControl
            End Sub
            'Draws the result of the event handler to the textbox
            Protected Sub OnPerformPaint()
                If mCanRender Then
                    'clear the canvas
                    'give the event handler a chance to draw to the buffer
                    mBaseControl.OnPaint(New PaintEventArgs(mBufferGraphics, mBufferClip))
                    'render the buffer contents to the RichTextBox
                    mControlGraphics.DrawImageUnscaled(mCanvas, 0, 0)
                End If
            End Sub
            'Builds up graphics resources
            Protected Friend Sub ReinitializeCanvas()
                SyncLock Me
                    If mBaseControl.Width > 0 AndAlso mBaseControl.Height > 0 Then
                        mCanRender = True
                        mCanvas = New Bitmap(mBaseControl.Width, mBaseControl.Height)
                        mBufferGraphics = Graphics.FromImage(mCanvas)
                        mBufferClip = mBaseControl.ClientRectangle
                        mBufferGraphics.Clip = New Region(mBufferClip)
                        mControlGraphics = Graphics.FromHwnd(mBaseControl.Handle)
                        mCanRender = False
                    End If
                End SyncLock
            End Sub
            'Cleans up resources
            Protected Friend Sub TearDown()
                If mControlGraphics IsNot Nothing Then
                End If
                If mBufferGraphics IsNot Nothing Then
                End If
                If mCanvas IsNot Nothing Then
                End If
            End Sub
        End Class
    End Class

    I may continue to expand upon this idea, but I wanted to get the initial code out here as this seems to be the minimum to get the painting functionality to work.

    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"
    Friday, October 16, 2009 10:43 PM