'Synchronize the Scroll position of two Controls with different content

I use this simple code to set the position of two Scrollbars of different RichTextBox Controls, at same time.
The trouble comes when the text of a RichTextBox is longer that the other.

Any suggestion? How can I calculate the percentage of the difference, to synchronize the scroll position of the two Controls, e.g., at the start/middle/end, at same time?

Const WM_USER As Integer = &H400
Const EM_GETSCROLLPOS As Integer = WM_USER + 221
Const EM_SETSCROLLPOS As Integer = WM_USER + 222
Declare Function SendMessage Lib "user32.dll" Alias "SendMessageW" (ByVal hWnd As IntPtr, ByVal msg As Integer, ByVal wParam As Integer, ByRef lParam As Point) As Integer

Private Sub RichTextBox1_VScroll(sender As Object, e As EventArgs) Handles RichTextBox1.VScroll
    Dim pt As Point
    SendMessage(RichTextBox1.Handle, EM_GETSCROLLPOS, 0, pt)
    SendMessage(RichTextBox2.Handle, EM_SETSCROLLPOS, 0, pt)
End Sub

Private Sub RichTextBox2_VScroll(sender As Object, e As EventArgs) Handles RichTextBox2.VScroll
    Dim pt As Point
    SendMessage(RichTextBox2.Handle, EM_GETSCROLLPOS, 0, pt)
    SendMessage(RichTextBox1.Handle, EM_SETSCROLLPOS, 0, pt)
End Sub


Solution 1:[1]

The procedure is described here:
How to scroll a RichTextBox control to a given point regardless of caret position

  • You need to calculate the maximum Scroll value of your Controls

  • Consider the ClientSize.Height and the Font.Height: both play a role when we define the maximum scroll position. The max Vertical Scroll Value is defined by:

    MaxVerticalScroll = Viewport.Height - ClientSize.Height + Font.Height - BorderSize  
    

    where Viewport is the overall internal surface of a Control that includes all its content.
    It's often returned by the PreferredSize property (which belongs to the Control class), but, e.g., the RichTextBox, sets the PreferredSize before text wrapping, so it's just relative to the unwrapped text, not really useful here.
    You determine the base distance manually (as described in the link above), or use the GetScrollInfo() function. It returns a SCROLLINFO structure that contains the absolute Minimum and Maximum Scroll value and the current Scroll Position.

  • Calculate the relative difference of the two maximum scroll positions: this is the multiplier factor used to scale the two scroll positions, to generate a common relative value.

Important: using the VScroll event, you have to introduce a variable that prevents the two Control from triggering the Scroll action of the counterpart over and over, causing a StackOverflow exception.
See the VScroll event handler and the use of the synchScroll boolean Field.

? The SyncScrollPosition() method calls the GetAbsoluteMaxVScroll() and GetRelativeScrollDiff() methods that calculate the relative scroll values, then calls SendMessage to set the Scroll position of the Control to synchronize.
Both accept TextBoxBase arguments, since RichTextBox derives from this base class, as the TextBox class, so you can use the same methods for both RichTextBox and TextBox Controls without any change.

? Use the SendMessage declaration you find here, among the others.

Private synchScroll As Boolean = False

Private Sub richTextBox1_VScroll(sender As Object, e As EventArgs) Handles RichTextBox1.VScroll
    SyncScrollPosition(RichTextBox1, RichTextBox2)
End Sub

Private Sub richTextBox2_VScroll(sender As Object, e As EventArgs) Handles RichTextBox2.VScroll
    SyncScrollPosition(RichTextBox2, RichTextBox1)
End Sub

Private Sub SyncScrollPosition(ctrlSource As TextBoxBase, ctrlDest As TextBoxBase)
    If synchScroll Then Return
    synchScroll = True

    Dim infoSource = GetAbsoluteMaxVScroll(ctrlSource)
    Dim infoDest = GetAbsoluteMaxVScroll(ctrlDest)
    Dim relScrollDiff As Single = GetRelativeScrollDiff(infoSource.nMax, infoDest.nMax, ctrlSource, ctrlDest)

    Dim nPos = If(infoSource.nTrackPos > 0, infoSource.nTrackPos, infoSource.nPos)
    Dim pt = New Point(0, CType((nPos + 0.5F) * relScrollDiff, Integer))
    SendMessage(ctrlDest.Handle, EM_SETSCROLLPOS, 0, pt)
    synchScroll = False
End Sub

Private Function GetAbsoluteMaxVScroll(ctrl As TextBoxBase) As SCROLLINFO
    Dim si = New SCROLLINFO(SBInfoMask.SIF_ALL)
    GetScrollInfo(ctrl.Handle, SBParam.SB_VERT, si)
    Return si
End Function

Private Function GetRelativeScrollDiff(sourceScrollMax As Integer, destScrollMax As Integer, source As TextBoxBase, dest As TextBoxBase) As Single
    Dim border As Single = If(source.BorderStyle = BorderStyle.None, 0F, 1.0F)
    Return (CSng(destScrollMax) - dest.ClientSize.Height) / (sourceScrollMax - source.ClientSize.Height - border)
End Function

Win32 methods declarations:

Imports System.Runtime.InteropServices

Private Const WM_USER As Integer = &H400
Private Const EM_GETSCROLLPOS As Integer = WM_USER + 221
Private Const EM_SETSCROLLPOS As Integer = WM_USER + 222

<DllImport("user32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
Friend Shared Function SendMessage(hWnd As IntPtr, msg As Integer, wParam As Integer, <[In], Out> ByRef lParam As Point) As Integer
End Function

<DllImport("user32.dll")>
Friend Shared Function GetScrollInfo(hwnd As IntPtr, fnBar As SBParam, ByRef lpsi As SCROLLINFO) As Boolean
End Function

<StructLayout(LayoutKind.Sequential)>
Friend Structure SCROLLINFO
    Public cbSize As UInteger
    Public fMask As SBInfoMask
    Public nMin As Integer
    Public nMax As Integer
    Public nPage As UInteger
    Public nPos As Integer
    Public nTrackPos As Integer

    Public Sub New(mask As SBInfoMask)
        cbSize = CType(Marshal.SizeOf(Of SCROLLINFO)(), UInteger)
        fMask = mask : nMin = 0 : nMax = 0 : nPage = 0 : nPos = 0 : nTrackPos = 0
    End Sub
End Structure

Friend Enum SBInfoMask As UInteger
    SIF_RANGE = &H1
    SIF_PAGE = &H2
    SIF_POS = &H4
    SIF_DISABLENOSCROLL = &H8
    SIF_TRACKPOS = &H10
    SIF_ALL = SIF_RANGE Or SIF_PAGE Or SIF_POS Or SIF_TRACKPOS
    SIF_POSRANGE = SIF_RANGE Or SIF_POS Or SIF_PAGE
End Enum

Friend Enum SBParam As Integer
    SB_HORZ = &H0
    SB_VERT = &H1
    SB_CTL = &H2
    SB_BOTH = &H3
End Enum

This is how it works:
Note that the two Controls contain different text and also use a different Font:

  • Segoe UI, 9.75pt the Control above
  • Microsoft Sans Serif, 9pt the other

ScrollBars Sychronize


C# Version:

private bool synchScroll = false;

private void richTextBox1_VScroll(object sender, EventArgs e)
{
    SyncScrollPosition(richTextBox1, richTextBox2);
}

private void richTextBox2_VScroll(object sender, EventArgs e)
{
    SyncScrollPosition(richTextBox2, richTextBox1);
}

private void SyncScrollPosition(TextBoxBase ctrlSource, TextBoxBase ctrlDest) { 
    if (synchScroll) return;
    synchScroll = true;

    var infoSource = GetAbsoluteMaxVScroll(ctrlSource);
    var infoDest = GetAbsoluteMaxVScroll(ctrlDest);
    float relScrollDiff = GetRelativeScrollDiff(infoSource.nMax, infoDest.nMax, ctrlSource, ctrlDest);

    int nPos = infoSource.nTrackPos > 0 ? infoSource.nTrackPos : infoSource.nPos;
    var pt = new Point(0, (int)((nPos + 0.5F) * relScrollDiff));
    SendMessage(ctrlDest.Handle, EM_SETSCROLLPOS, 0, ref pt);
    synchScroll = false;
}

private SCROLLINFO GetAbsoluteMaxVScroll(TextBoxBase ctrl) {
    var si = new SCROLLINFO(SBInfoMask.SIF_ALL);
    GetScrollInfo(ctrl.Handle, SBParam.SB_VERT, ref si);
    return si;
}

private float GetRelativeScrollDiff(int sourceScrollMax, int destScrollMax, TextBoxBase source, TextBoxBase dest) {
    float border = source.BorderStyle == BorderStyle.None ? 0F : 1.0F;
    return ((float)destScrollMax - dest.ClientSize.Height) / ((float)sourceScrollMax - source.ClientSize.Height - border);
}

Declarations:

using System.Runtime.InteropServices;

private const int WM_USER = 0x400;
private const int EM_GETSCROLLPOS = WM_USER + 221;
private const int EM_SETSCROLLPOS = WM_USER + 222;

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern int SendMessage(IntPtr hWnd, int msg, int wParam, [In, Out] ref Point lParam);


[DllImport("user32.dll")]
internal static extern bool GetScrollInfo(IntPtr hwnd, SBParam fnBar, ref SCROLLINFO lpsi);


[StructLayout(LayoutKind.Sequential)]
internal struct SCROLLINFO {
    public uint cbSize;
    public SBInfoMask fMask;
    public int nMin;
    public int nMax;
    public uint nPage;
    public int nPos;
    public int nTrackPos;

    public SCROLLINFO(SBInfoMask mask)
    {
        cbSize = (uint)Marshal.SizeOf<SCROLLINFO>();
        fMask = mask; nMin = 0; nMax = 0; nPage = 0; nPos = 0; nTrackPos = 0;
    }
}

internal enum SBInfoMask : uint {
    SIF_RANGE = 0x1,
    SIF_PAGE = 0x2,
    SIF_POS = 0x4,
    SIF_DISABLENOSCROLL = 0x8,
    SIF_TRACKPOS = 0x10,
    SIF_ALL = SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS,
    SIF_POSRANGE = SIF_RANGE | SIF_POS | SIF_PAGE
}

internal enum SBParam : int {
    SB_HORZ = 0x0,
    SB_VERT = 0x1,
    SB_CTL = 0x2,
    SB_BOTH = 0x3
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1