Tie the scrolling between two controls
Join the DZone community and get the full member experience.
Join For FreeToday I encountered an interesting question on Dream.In.Code. The original poster was curious whether it is possible to tie two TextBox controls in a way so that the scrolling of one TextBox control would trigger the scrolling of another. Initially, I tried to look at .NET events but there was nothing available for me, so I had to look at Windows API to directly hook to the scrolling event. For now, I only had to look at vertical scrolling so I had to implement the most basic hooks.
What I did first is re-implement the TextBox control as a custom class that inherits the base properties and capabilities of the TextBox class.
public class ExtendedTextBox : TextBox
{
private int oldValue = 0;
private int newValue = 0;
private ExtendedTextBox textBox = null;
public event ScrollEventHandler VerticalScrolled = null;
private System.ComponentModel.Container components = null;
protected override void WndProc(ref Message m)
{
if (m.Msg == 0x115)
{
if (VerticalScrolled != null)
{
SCROLLINFO info = new SCROLLINFO();
info.cbSize = Marshal.SizeOf(info);
info.fMask = 1| 2| 3 | 4 | 10;
WinApi.GetScrollInfo(m.HWnd, 1, out info);
if (m.WParam.ToInt32() == 8)
{
ScrollEventArgs args = new ScrollEventArgs(ScrollEventType.EndScroll, info.nPos);
VerticalScrolled(this, args);
oldValue = newValue;
newValue = args.NewValue;
if (text != null)
{
WinApi.SendMessage(text.Handle, 182, IntPtr.Zero, (IntPtr)(newValue - oldValue));
}
}
}
}
base.WndProc(ref m);
}
public ExtendedTextBox()
{
}
public ExtendedTextBox(ExtendedTextBox textBox)
{
this.textBox = textBox;
}
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose(disposing);
}
}
The overall class structure might seem confusing, so I will explain each part. The standard Dispose and InitializeComponent methods are standard for Windows controls - these allow the developer to control memory allocations for a specific control when that one is in use or not.
There are two constructors defined - one for the un-linked ExtendedTextBox that can be used to track scrolling when you don't need another ExtendedTextBox scrolling simultaneously, and another one for a linked ExtendedTextBox control:
public ExtendedTextBox()
{
}
public ExtendedTextBox(ExtendedTextBox textBox)
{
this.textBox = textBox;
}
If the second constructor is used, I am using an internal instance of ExtendedTextBox to later use its handle for scrolling purposes.
Also, I now have an event handler that can be used for tracking vertical scrolling:
public event ScrollEventHandler VerticalScrolled = null;
This event is not specific to a TextBox control, so that's why I have to manually include it in the extended class.
The next step is overriding the WndProc method that executes a call to the WindowProc function. This is the gateway to event tracking since WindowProc handles the messages sent to a window/control with a handle identifier (hWnd). The vertical scroll message is identified by an integer value equal to 115 - same as the WM_VSCROLL constant.
protected override void WndProc(ref Message m)
{
if (m.Msg == 0x115)
{
}
base.WndProc(ref m);
}
This checks the current message processed by the control and in case this is the vertical scrolling message, it needs to be handled.
If the event handler is null (therefore it is not possible to handle the scrolling), there is no need to actually try to process the scrolling even if it happens. So let's say that there is an event handler. If that's the case, I need to instantiate a SCROLLINFO struct that will keep some information related to the current scrolling progress.
The struct itself has the following structure:
[StructLayout(LayoutKind.Sequential)
public struct SCROLLINFO
{
public int cbSize;
public int fMask;
public int nMin;
public int nMax;
public int nPage;
public int nPos;
public int nTrackPos;
}
I am required to specify the size of the struct and also set the mask that will define the returned values:
SCROLLINFO info = new SCROLLINFO();
info.cbSize = Marshal.SizeOf(info);
info.fMask = 1| 2| 3 | 4 | 10;
The numbers used in the mask are also associated with struct-specific constants:
- SIF_RANGE = 0x1
- SIF_PAGE = 0x2
- SIF_POS = 0x4
- SIF_TRACKPOS = 0x10
- SIF_ALL = SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS
In this case, I need SIF_ALL that will return all possible data related to the scroll bar in the control.
NOTE: This will only apply to the scroll bar and not to the control itself. Therefore, the returned struct can only be used to refer to the scroll bar instance.
It is now time to get the actual data via GetScrollInfo:
WinApi.GetScrollInfo(m.HWnd, 1, out info);
This is a native WinApi function referenced via DllImport:
public static class WinApi
{
[DllImport("user32.dll")]
public extern static bool GetScrollInfo(IntPtr hWnd, int nBar, out SCROLLINFO lpsi);
}
I created a separate static class to hold WinApi function references. It is possible to have those separated inside the actual working class, but the way I have it makes it more organized and easier to access and extend later on.
The second parameter in the GetScrollInfo call is the constant that represents the current scroll bar that should be tracked. An integer value of 1 represents SB_VERT, also known as the vertical scroll bar.
The wParam I am checking next should represent SB_ENDSCROLL - the marker that shows that the scrolling is over, and when this happens, I am defining the ScrollEventArgs instance and trigger the scrolling event handler:
if (m.WParam.ToInt32() == 8)
{
ScrollEventArgs args = new ScrollEventArgs(ScrollEventType.EndScroll, info.nPos);
VerticalScrolled(this, args);
}
In the same if statement I am storing the current line and the old line, so that I can later on calculate the difference and scroll the linked text box according to the number of lines used in the first one:
oldValue = newValue;
newValue = args.NewValue;
If the textBox field is not empty, which means that the second constructor is used, the SendMessage function can be used to scroll the linked text box to a number of lines represented by the difference I mentioned above:
if (textBox != null)
{
WinApi.SendMessage(textBox.Handle, 182, IntPtr.Zero, (IntPtr)(newValue - oldValue));
}
The message I am sending is representing the EM_LINESCROLL constant, equal to 182 (hex: &HB6). The lParam here is the number of lines.
The SendMessage is also referenced in the static WinApi class:
[DllImport("user32.dll")]
public extern static bool SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
Trying it out
Now, in the working form, make sure you add the needed ExtendedTextBox instances:
// First extended text box - will be tied to the second one.
ExtendedTextBox textBox = new ExtendedTextBox();
textBox.ScrollBars = ScrollBars.Vertical;
for (int i = 0; i < 100; i++)
{
textBox.Text += i.ToString() + Environment.NewLine;
}
textBox.Width = 100;
textBox.Height = 100;
textBox.Multiline = true;
this.Controls.Add(textBox);
// The "master" extended text box.
ExtendedTextBox textBox2 = new ExtendedTextBox(textBox);
textBox2.ScrollBars = ScrollBars.Vertical;
for (int i = 0; i < 100; i++)
{
textBox2.Text += i.ToString() + Environment.NewLine;
}
textBox2.Width = 100;
textBox2.Height = 100;
textBox2.Multiline = true;
textBox2.Location = new Point(200, 0);
textBox2.VerticalScrolled += new ScrollEventHandler(textBox2_VerticalScrolled);
this.Controls.Add(textBox2);
Here I am also adding some sample data to show the scrolling procedures - that's what the for loops are there for. The second text box constructor receives the first one as a reference and links it to itself so that the scrolling will be symmetrical.
Opinions expressed by DZone contributors are their own.
Comments