'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 theFont.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 theControl
class), but, e.g., the RichTextBox, sets thePreferredSize
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 aboveMicrosoft Sans Serif, 9pt
the other
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 |