Synchronize Scroll Position of Two Richtextboxes

Synchronize Scroll Position of two RichTextBoxes?

I did this for a small project a while ago, and here's the simplist solution I found.

Create a new control by subclassing RichTextBox:

   public class SynchronizedScrollRichTextBox : System.Windows.Forms.RichTextBox
{
public event vScrollEventHandler vScroll;
public delegate void vScrollEventHandler(System.Windows.Forms.Message message);

public const int WM_VSCROLL = 0x115;

protected override void WndProc(ref System.Windows.Forms.Message msg) {
if (msg.Msg == WM_VSCROLL) {
if (vScroll != null) {
vScroll(msg);
}
}
base.WndProc(ref msg);
}

public void PubWndProc(ref System.Windows.Forms.Message msg) {
base.WndProc(ref msg);
}
}

Add the new control to your form and for each control explicitly notify the other instances of the control that its vScroll position has changed. Somthing like this:

private void scrollSyncTxtBox1_vScroll(Message msg) {
msg.HWnd = scrollSyncTxtBox2.Handle;
scrollSyncTxtBox2.PubWndProc(ref msg);
}

I think this code has problems if all the 'linked' controls don't have the same number of displayable lines.

Synchronize the Scroll position of two Controls with different content

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
}

How to Synchronize Scroll of two RichTextBox without SystemOverflowException in WinForm?

Looks like your application is running as 32 bit and you're getting an Overflow because UInt can have a value which can't be fit in 32 bit signed int.

For instance, running your application as 64 bit should just work fine.

That said, you don't need that. You can simply avoid using uint and just use int which will work just fine.

int wParam = (int)ScrollBarCommands.SB_THUMBPOSITION | (int)nPos;

How can I sync the scrolling of two multiline textboxes?

Yes, you'll have to create a custom text box so you can detect it scrolling. The trick is to pass the scroll message to the other text box so it will scroll in sync. This really only works well when that other text box is about the same size and has the same number of lines.

Add a new class to your project and paste the code shown below. Compile. Drop two of the new controls from the top of the toolbox onto your form. Set the Buddy property to the other control on both. Run, type some text in both of them and watch them scroll in sync as you drag the scrollbar.

using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;

class SyncTextBox : TextBox {
public SyncTextBox() {
this.Multiline = true;
this.ScrollBars = ScrollBars.Vertical;
}
public Control Buddy { get; set; }

private static bool scrolling; // In case buddy tries to scroll us
protected override void WndProc(ref Message m) {
base.WndProc(ref m);
// Trap WM_VSCROLL message and pass to buddy
if (m.Msg == 0x115 && !scrolling && Buddy != null && Buddy.IsHandleCreated) {
scrolling = true;
SendMessage(Buddy.Handle, m.Msg, m.WParam, m.LParam);
scrolling = false;
}
}
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);
}

Powershell: Sync Scrolling of two Richtextboxes

ScrollBarType, ScrollBarCommands, and Message are members of User32.WinUtils namespace. Also, ptr should be System.IntPtr.

EDIT:
To scroll $rtb2 while dragging the scrollbar (and not just when mouse is released), you have to use GetScrollInfo

EDIT 2:
Fixed arrow scroll buttons not working

EDIT 3:
Fixed other bugs. Restructured code.

$typeDef = @"
using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;

public enum ScrollBarType : uint {
SbHorz = 0,
SbVert = 1,
SbCtl = 2,
SbBoth = 3
}

public enum Message : uint {
WmVScroll = 0x0115
}

public enum ScrollBarCommands : uint {
ThumbPosition = 4,
ThumbTrack = 5
}

[Flags()]
public enum ScrollBarInfo : uint {
Range = 0x0001,
Page = 0x0002,
Pos = 0x0004,
DisableNoScroll = 0x0008,
TrackPos = 0x0010,

All = ( Range | Page | Pos | TrackPos )
}

public class CustomRichTextBox : RichTextBox {
public Control Buddy { get; set; }

public bool ThumbTrack = false;

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

[DllImport( "User32.dll" )]
public extern static int SendMessage( IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam );
[DllImport( "User32.dll" )]
public extern static int GetScrollInfo( IntPtr hWnd, int fnBar, ref ScrollInfo lpsi );

public void CustomVScroll() {
int nPos;

ScrollInfo scrollInfo = new ScrollInfo();
scrollInfo.cbSize = (uint)Marshal.SizeOf( scrollInfo );

if (ThumbTrack) {
scrollInfo.fMask = (uint)ScrollBarInfo.TrackPos;
GetScrollInfo( this.Handle, (int)ScrollBarType.SbVert, ref scrollInfo );
nPos = scrollInfo.nTrackPos;
} else {
scrollInfo.fMask = (uint)ScrollBarInfo.Pos;
GetScrollInfo( this.Handle, (int)ScrollBarType.SbVert, ref scrollInfo );
nPos = scrollInfo.nPos;
}

nPos <<= 16;
uint wParam = (uint)ScrollBarCommands.ThumbPosition | (uint)nPos;
SendMessage( Buddy.Handle, (int)Message.WmVScroll, new IntPtr( wParam ), new IntPtr( 0 ) );
}

protected override void WndProc( ref System.Windows.Forms.Message m ) {
if ( m.Msg == (int)Message.WmVScroll ) {
if ( ( m.WParam.ToInt32() & 0xFF ) == (int)ScrollBarCommands.ThumbTrack ) {
ThumbTrack = true;
} else {
ThumbTrack = false;
}
}

base.WndProc( ref m );
}
}
"@

$assemblies = ("System.Windows.Forms", "System.Runtime.InteropServices")

Add-Type -ReferencedAssemblies $assemblies -TypeDefinition $typeDef -Language CSharp

### Form

$form = New-Object System.Windows.Forms.Form
$form.Size = "400,400"

### Rich text box 1 (Synchronized - master)

$rtb1 = New-Object CustomRichTextBox

$rtb1.Size = "190,350"
$rtb1.Location = "200,1"
$rtb1.Text = (1..300 | Out-String)

$form.Controls.Add($rtb1)

### Rich text box 2 (Synchronized - slave)

$rtb2 = New-Object system.Windows.Forms.RichTextBox

$rtb2.Size = "190,350"
$rtb2.Location = "1,1"
$rtb2.Text = (1..300 | Out-String)
$rtb2.ScrollBars = "none"

$form.Controls.Add($rtb2)

### Synchronization setup

$rtb1.Buddy = $rtb2

$rtb1.Add_VScroll({
$this.CustomVScroll()
})

### Run

$form.ShowDialog()


Related Topics



Leave a reply



Submit