Introduction
I have noticed a lot of posts out there describing how to synchronize the scrolling of multiple RichTextBox
es. Most examples use the GetScrollPos()
call and/or respond to VScroll
/HScroll
events. The problem I have experienced with this method is that the client area does not always match the scroll bar position and it is difficult to distinguish whether to use the trackPos
or Pos
value returned by the GetScrollInfo()
when the scroll bar is being dragged. (This may be due to my ignorance on how to use these methods/APIs so please let me know�)
The RichTextBox
does offer a way to determine and set the position of the client area as opposed to the position of the scroll bar using the EM_GETSCROLLPOS
and EM_SETSCROLLPOS
messages. Using this method allows you to accurately sync the client area instead of the scroll bar position which is really what we want.
Code usage
First declare the SendMessage
API which will actually send the messages to the RTF control and declare the API constants. We will modify the lParam
parameter in the SendMessage
declaration to accept a POINT
structure by reference:
Private Declare Auto Function SendScrollPosMessage _
Lib "user32.dll" Alias "SendMessage" ( _
ByVal hWnd As IntPtr, _
ByVal Msg As Integer, _
ByVal wParam As IntPtr, _
ByRef lParam As POINT) As Integer
Private Const WM_USER = &H400
Private Const EM_GETSCROLLPOS = WM_USER + 221
Private Const EM_SETSCROLLPOS = WM_USER + 222
Private Structure POINT
Public x As Integer
Public y As Integer
End Structure
To retrieve the position of the client area you will need to send an EM_GETSCROLLPOS
message to the RichTextbox
. You will have to specify the handle to the target RichTextBox
and pass the POINT
structure which will contain the position after the message is sent:
Dim stcScrollPoint As New Point
SendMessage(rtfText.Handle(), EM_GETSCROLLPOS, _
New IntPtr(0), stcScrollPoint)
Debug.WriteLine(stcScrollPoint.y)
Debug.WriteLine(stcScrollPoint.x)
To set the position of the client area you will need to send an EM_SETSCROLLPOS
message to the RichTextBox
. As mentioned above you need to specify the handle of the target RichTextBox
and pass the POINT
structure which will set the position after the message is sent:
Dim stcScrollPoint As New Point
stcScrollPoint.x = 20
stcScrollPoint.y = 40
SendMessage(rtfText.Handle(), EM_SETSCROLLPOS, _
New IntPtr(0), ptrScrollPoint)
Implementation
I have implemented this into a class that simply allows you to add RichTextBox
es and set the synchronization to include the horizontal, vertical or both scrollbars and it takes care of the rest. The usage of this class is as follows:
Private objScrollSync As New clsRTFScrollSync
Public Sub FormLoad(...)...
objScrollSync.ScrollBarToSync = ScrollBars.Vertical
objScrollSync.AddControl(rtfML)
objScrollSync.AddControl(rtfGutter)
End Sub
The full source code:
Imports System.Runtime.InteropServices
Public Class clsRTFScrollSync
Private Declare Auto Function SendScrollPosMessage _
Lib "user32.dll" Alias "SendMessage"( _
ByVal hWnd As IntPtr, _
ByVal Msg As Integer, _
ByVal wParam As IntPtr, _
ByRef lParam As POINT) As Integer
Private Const WM_USER = &H400
Private Const EM_GETSCROLLPOS = WM_USER + 221
Private Const EM_SETSCROLLPOS = WM_USER + 222
Private Structure POINT
Public x As Integer
Public y As Integer
End Structure
Private aControls As New ArrayList
Private sbScrollBarType As Windows.Forms.ScrollBars
Public Property ScrollBarToSync() As Windows.Forms.ScrollBars
Get
Return sbScrollBarType
End Get
Set(ByVal Value As Windows.Forms.ScrollBars)
sbScrollBarType = Value
End Set
End Property
Public Sub AddControl(ByVal RTFControl As Object)
Dim objControlSubClass As New clsWindowSubClass(RTFControl.Handle, _
RTFControl, Me)
aControls.Add(objControlSubClass)
End Sub
Public Sub SyncScrollBars(ByVal Handle As IntPtr, _
ByVal SubClass As clsWindowSubClass, _
ByVal Window As Object, _
ByRef WindowsMessage As Message)
Static blnIgnoreMessages As Boolean
If blnIgnoreMessages = True Then Exit Sub
blnIgnoreMessages = True
Dim blnChangeVertPos As Boolean = False
Dim blnChangeHorizPos As Boolean = False
Dim lngVertPos, lngHorizPos As Long
Dim stcScrollPoint As New Point
Dim ptrScrollPoint As IntPtr
SendScrollPosMessage(Handle, EM_GETSCROLLPOS, _
New IntPtr(0), stcScrollPoint)
lngVertPos = stcScrollPoint.y
lngHorizPos = stcScrollPoint.x
If (sbScrollBarType = RichTextBoxScrollBars.Both Or _
sbScrollBarType = RichTextBoxScrollBars.Vertical) Then
blnChangeVertPos = True
If (sbScrollBarType = RichTextBoxScrollBars.Both Or _
sbScrollBarType = RichTextBoxScrollBars.Horizontal) Then
blnChangeHorizPos = True
If blnChangeVertPos = True Or blnChangeHorizPos = True Then
Dim objSubClass As clsWindowSubClass
Dim objWindowMessage As New Windows.Forms.Message
For Each objSubClass In aControls
If objSubClass.Handle.ToInt32 <> Handle.ToInt32 Then
SendScrollPosMessage(objSubClass.Handle, EM_GETSCROLLPOS, _
New IntPtr(0), stcScrollPoint)
If blnChangeHorizPos = True Then
stcScrollPoint.x = lngHorizPos
If blnChangeVertPos = True Then
stcScrollPoint.y = lngVertPos
SendScrollPosMessage(objSubClass.Handle, EM_SETSCROLLPOS, _
New IntPtr(0), stcScrollPoint)
End If
Next
End If
blnIgnoreMessages = False
End Sub
End Class
Public Class clsWindowSubClass
Inherits System.Windows.Forms.NativeWindow
Private objWindow As Object
Private objParent As clsRTFScrollSync
Public Sub New(ByVal Handle As IntPtr, ByVal Window As Object,
ByVal Parent As clsRTFScrollSync)
objWindow = Window
objParent = Parent
MyBase.AssignHandle(Handle)
End Sub
Protected Overrides Sub WndProc(ByRef WindowMessage As _
system.Windows.Forms.Message)
MyBase.WndProc(WindowMessage)
objParent.SyncScrollBars(MyBase.Handle, Me, _
objWindow, WindowMessage)
End Sub
End Class
Credits
Special thanks to Georgi Atanasov for his input in this article.