同步两个内容不同的控件的滚动位置

Synchronize the Scroll position of two Controls with different content

我使用这个简单的代码同时设置不同 RichTextBox 控件的两个滚动条的位置。
当 RichTextBox 的文本比另一个长时,问题就来了。

有什么建议吗?如何计算差异的百分比,以同步两个控件的滚动位置,例如,同时在 start/middle/end?

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

程序描述如下:

  • 您需要计算控件的最大滚动值

  • 考虑 ClientSize.HeightFont.Height:当我们定义最大滚动位置时,两者都起作用。最大垂直滚动值定义为:

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

    其中 Viewport 是包含其所有内容的控件的整体内表面。
    它通常由 PreferredSize 属性(属于 Control class)返回,但是,例如 RichTextBox,在文本环绕之前设置 PreferredSize,所以它只是相对于展开的文本,在这里并不是很有用。
    您可以手动确定基本距离(如上文link中所述),或使用包含绝对最小和最大滚动值以及当前滚动值的GetScrollInfo() function. It returns a SCROLLINFO结构滚动位置。

  • 计算两个最大滚动位置的相对差异:这是用于缩放两个滚动位置的乘数,以生成一个共同的相对值。

重要:使用VScroll事件,必须引入一个变量,防止两个Control反复触发对方的Scroll动作,导致Whosebug异常
请参阅 VScroll 事件处理程序和 synchScroll 布尔字段的使用。

SyncScrollPosition() 方法调用 GetAbsoluteMaxVScroll()GetRelativeScrollDiff() 计算相对滚动值的方法,然后调用 SendMessage 设置要同步的控件的滚动位置。
两者都接受 TextBoxBase 个参数,因为 RichTextBox 派生自这个基础 class,作为 TextBox class,因此您可以对 RichTextBox 和 TextBox 控件使用相同的方法而无需任何更改。

▶ 使用您在此处找到的 SendMessage 声明等。

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 方法声明:

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

它是这样工作的:
请注意,这两个控件包含不同的文本,并且还使用了不同的字体:

  • Segoe UI, 9.75pt上面的控件
  • Microsoft Sans Serif, 9pt另一个


C#版本:

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);
}

声明:

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
}