Categories
Programming

Scrolling textbox when text is added, but not if the user has scrolled

You may (or may not) have read my recent article How to watch your log through your application in Log4Net which allows you to append Log4Net events direct in to a textbox – if so, you may then also have come across a frustration that I also had, which is that whenever the texbox is updated it scrolls incorrectly and what you actually want is for it to scroll to the bottom unless the user has scrolled themselves and therefore it should remain in place (so that the user can carry on reading where they are).

This issue is easy to solve when you know how, but a bit of a bitch when you don’t so I thought I’d share my solution here. This issue cannot be fully and easily resolved within your standard C# tools, instead you need to invoke the power of the Windows API. To do this we need some declarations:

// Constants for extern calls to various scrollbar functions
private const int SB_VERT = 0x1;
private const int WM_VSCROLL = 0x115;
private const int SB_THUMBPOSITION = 4;
private const int SIF_TRACKPOS = 10;

// Structure to hold scroll bar info
[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;
}

// The SetScrollPos function sets the position of the scroll box (thumb) in the specified scroll bar and, if requested, 
// redraws the scroll bar to reflect the new position of the scroll box
[DllImport("user32.dll")]
static extern int SetScrollPos(IntPtr hWnd, int nBar, int nPos, bool bRedraw);

        // The GetScrollPos function retrieves the current position of the scroll box (thumb) in the specified scroll bar
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int GetScrollPos(IntPtr hWnd, int nBar);

// Retrieves the parameters of a scroll bar, including the minimum and maximum scrolling positions, the page 
        // size, and the position of the scroll box (thumb)
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int GetScrollInfo(IntPtr hWnd, int fnBar, ref ScrollInfo lpScrollInfo);

// Places (posts) a message in the message queue associated with the thread that created the specified window 
// and returns without waiting for the thread to process the message
[DllImport("user32.dll")]
private static extern bool PostMessageA(IntPtr hWnd, int nBar, int wParam, int lParam);

// Retrieves the current minimum and maximum scroll box (thumb) positions for the specified scroll bar
[DllImport("user32.dll")]
static extern bool GetScrollRange(IntPtr hWnd, int nBar, out int lpMinPos, out int lpMaxPos);

Now that we have made all of our declarations we need to put some code in to the place where our textbox is being updated. As this is a follow on from my previous article I will explain how it fits in to the relevant code – within your UpdateLogTextbox function after the “if (InvokeRequired) {” block but before you update the textbox insert the following code:

// Variables to store whether or not the scroll bar is at the bottom, the min and max positions, offset between characters and textbox and the current scroll position
bool scrollbarIsAtBottom = false;
int verticalScrollMin;
int verticalScrollMax;
int savedVerticalPos;

try {
    // Get the current scroll position
    savedVerticalPos = GetScrollPos(this.LogTextbox.Handle, SB_VERT);

    // Get the minimum and maximum scroll positions
    GetScrollRange(this.LogTextbox.Handle, SB_VERT, out verticalScrollMin, out verticalScrollMax);

    ScrollInfo scrollInfo = new ScrollInfo();

    // Set size of bytes to be returned and mask (to return current position of scroll box)
    scrollInfo.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(scrollInfo);
    scrollInfo.fMask = SIF_TRACKPOS;

    // Get the scroll info so that we can find out the size of the scroller
    GetScrollInfo(this.LogTextbox.Handle, SB_VERT, ref scrollInfo);

    // Determine whether the scroll bar is currently at the bottom
    if (savedVerticalPos >= (verticalScrollMax - scrollInfo.nPage - 1)) {
        scrollbarIsAtBottom = true;
    }

This will effectively get the current position of the scrollbar before it is updated. The next bit of code goes after your update of the textbox i.e. “LogTextbox.Text = value”, and will calculate the difference between the new position of the scrollbar and what was previously the case; it will then either move the scrollbar to the bottom or leave it where it is depending on whether the scrollbar was previously at the bottom:

    // If the scroll bar is currently at the bottom
    if (scrollbarIsAtBottom) {
        // Get the minimum and maximum scroll positions
        GetScrollRange(this.LogTextbox.Handle, SB_VERT, out verticalScrollMin, out verticalScrollMax);

        // Calculate the difference
        savedVerticalPos = verticalScrollMax - scrollInfo.nPage;

        // Set the carat position to be at the end
        LogTextbox.Select(LogTextbox.TextLength, 0);

        // Set the scroll position
        SetScrollPos(this.LogTextbox.Handle, SB_VERT, savedVerticalPos, true);

        // Post the message so that Windows actually changes the scroll position
        PostMessageA(this.LogTextbox.Handle, WM_VSCROLL, SB_THUMBPOSITION + 0x10000 * savedVerticalPos, 0);
    }
} catch (ObjectDisposedException) {}

Now please bear in mind that playing with the Windows API can sometimes cause you errors that can be a pig to resolve so I wouldn’t recommend using this code unless you are prepared to have to debug these. Otherwise this code is pretty solid and should fit the need of most people.

About Stephen Pickett


Stephen Pickett is a programmer, IT strategist and architect, project manager and business analyst, Oracle Service Cloud and telephony expert, information security specialist, all-round geek. He is currently Technical Director at Connect Assist, a social business that helps charities and public services improve quality, efficiency and customer engagement through the provision of helpline services and CRM systems.

Stephen is based in south Wales and attended Cardiff University to study Computer Science, in which he achieved a 2:1 grading. He has previously worked for Think Consulting Solutions, a leading voice on not-for-profit fundraising, Fujitsu Services and Sony Manufacturing UK as a software developer.

Stephen is the developer of ThinkTwit, a WordPress plugin that allows you to display multiple Twitter feeds within a blog.

By Stephen Pickett

Stephen Pickett is a programmer, IT strategist and architect, project manager and business analyst, Oracle Service Cloud and telephony expert, information security specialist, all-round geek. He is currently Technical Director at Connect Assist, a social business that helps charities and public services improve quality, efficiency and customer engagement through the provision of helpline services and CRM systems.

Stephen is based in south Wales and attended Cardiff University to study Computer Science, in which he achieved a 2:1 grading. He has previously worked for Think Consulting Solutions, a leading voice on not-for-profit fundraising, Fujitsu Services and Sony Manufacturing UK as a software developer.

Stephen is the developer of ThinkTwit, a Wordpress plugin that allows you to display multiple Twitter feeds within a blog.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: