user1763295
user1763295

Reputation: 1098

Textbox - Maintain scroll bar position during text updates

I have to show information to the user that updates every 100 milliseconds, this means the contents of the textbox I show it in are constantly being changed and if they are scrolling through them when they are being changed the update will cause them to loose their scroll bar position

How do I prevent this? I've reduced the effect a lot by adding all text at once.

Current code:

string textboxStr = "";
foreach (string debugItem in debugItems)
{
    textboxStr += debugItem + Environment.NewLine;
}

debugForm.Controls[0].Text = textboxStr;

Update 1:

Used the solution provided below and it's not working, the scroll bar still resets to its default position meaning you loose your position and your pointer resets too.

Implementation:

In class:

[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern bool LockWindowUpdate(IntPtr hWndLock);

In function:

var originalPosition = ((TextBox)debugForm.Controls[0]).SelectionStart;

LockWindowUpdate(((TextBox)debugForm.Controls[0]).Handle);

debugForm.Controls[0].Text = textboxStr;

((TextBox)debugForm.Controls[0]).SelectionStart = originalPosition;
((TextBox)debugForm.Controls[0]).ScrollToCaret();

LockWindowUpdate(IntPtr.Zero);

Update 2: Used the 2nd solution provided below and it's not working. The scroll bar still jumps to the top even mid way while scrolling. Then sometimes when you're not even on it the scroll bar will start jumping up and down (Every 100 ms, when it updates the text).

Implementation: In class:

[DllImport("user32.dll")]
static extern int SetScrollPos(IntPtr hWnd, int nBar, int nPos, bool bRedraw);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int GetScrollPos(IntPtr hWnd, int nBar);
[DllImport("user32.dll")]
private static extern bool PostMessageA(IntPtr hWnd, int nBar, int wParam, int lParam);
private const int SB_VERT = 0x1;
private const int SB_THUMBPOSITION = 4;
private const int WM_VSCROLL = 0x115;

In function:

var currentPosition = GetScrollPos(debugForm.Controls[0].Handle, SB_VERT);

debugForm.Controls[0].Text = textboxStr;

SetScrollPos(debugForm.Controls[0].Handle, SB_VERT, currentPosition, false);
PostMessageA(debugForm.Controls[0].Handle, WM_VSCROLL, SB_THUMBPOSITION + 65535 * currentPosition, 0);

Example text:

Active Scene: Level0
--------------------------------------------------
Settings
    Fps: 60
    GameSize: {Width=600, Height=600}
    FreezeOnFocusLost: False
    ShowCursor: False
    StaysOnTop: False
    EscClose: True
    Title: 
    Debug: True
    DebugInterval: 100
--------------------------------------------------
Entities
    Entity Name: Player
        moveSpeed: 10
        jumpSpeed: 8
        ID: 0
        Type: 0
        Gravity: 1
        Vspeed: 1
        Hspeed: 0
        X: 20
        Y: 361
        Z: 0
        Sprites: System.Collections.Generic.List`1[GameEngine.Sprite]
        SpriteIndex: 0
        SpriteSpeed: 0
        FramesSinceChange: 0
        CollisionHandlers: System.Collections.Generic.List`1[GameEngine.CollisionHandler]
--------------------------------------------------
Key Events
    Key: Left
    State: DOWN
    Key: Left
    State: UP
    Key: Right
    State: DOWN
    Key: Right
    State: UP
    Key: Up
    State: DOWN
    Key: Up
    State: UP

Upvotes: 0

Views: 2568

Answers (2)

Joe
Joe

Reputation: 21

After searching and never finding a legitimate solution that works with and without focus as well as horizontally and vertically, I stumbled across an API solution that works (at least for my platform - Win7 / .Net4 WinForms).

using System;
using System.Runtime.InteropServices;

namespace WindowsNative
{
    /// <summary>
    /// Provides scroll commands for things like Multiline Textboxes, UserControls, etc.
    /// </summary>
    public static class ScrollAPIs
    {
        [DllImport("user32.dll")]
        internal static extern int GetScrollPos(IntPtr hWnd, int nBar);

        [DllImport("user32.dll")]
        internal static extern int SetScrollPos(IntPtr hWnd, int nBar, int nPos, bool bRedraw);

        [DllImport("user32.dll")]
        internal static extern int SendMessage(IntPtr hWnd, int wMsg, IntPtr wParam, IntPtr lParam);

        public enum ScrollbarDirection
        {
            Horizontal = 0,
            Vertical = 1,
        }

        private enum Messages
        {
            WM_HSCROLL = 0x0114,
            WM_VSCROLL = 0x0115
        }

        public static int GetScrollPosition(IntPtr hWnd, ScrollbarDirection direction)
        {
            return GetScrollPos(hWnd, (int)direction);
        }

        public static void GetScrollPosition(IntPtr hWnd, out int horizontalPosition, out int verticalPosition)
        {
            horizontalPosition = GetScrollPos(hWnd, (int)ScrollbarDirection.Horizontal);
            verticalPosition = GetScrollPos(hWnd, (int)ScrollbarDirection.Vertical);
        }

        public static void SetScrollPosition(IntPtr hwnd, int hozizontalPosition, int verticalPosition)
        {
            SetScrollPosition(hwnd, ScrollbarDirection.Horizontal, hozizontalPosition);
            SetScrollPosition(hwnd, ScrollbarDirection.Vertical, verticalPosition);
        }

        public static void SetScrollPosition(IntPtr hwnd, ScrollbarDirection direction, int position)
        {
            //move the scroll bar
            SetScrollPos(hwnd, (int)direction, position, true);

            //convert the position to the windows message equivalent
            IntPtr msgPosition = new IntPtr((position << 16) + 4);
            Messages msg = (direction == ScrollbarDirection.Horizontal) ? Messages.WM_HSCROLL : Messages.WM_VSCROLL;
            SendMessage(hwnd, (int)msg, msgPosition, IntPtr.Zero);
        }
    }
}

With a multiline textbox (tbx_main) use like:

int horzPos, vertPos;
ScrollAPIs.GetScrollPosition(tbx_main.Handle, out horzPos, out vertPos);

//make your changes
//i did something like the following where m_buffer is a string[]
tbx_main.Text = string.Join(Environment.NewLine, m_buffer);

tbx_main.SelectionStart = 0;
tbx_main.SelectionLength = 0;

ScrollAPIs.SetScrollPosition(tbx_main.Handle, horzPos, vertPos);

Upvotes: 2

Simon Whitehead
Simon Whitehead

Reputation: 65077

You can store the SelectionStart then use ScrollToCaret after you update. Use LockWindowUpdate to stop the flicker. Something like this:

[DllImport("user32.dll")]
public static extern bool LockWindowUpdate(IntPtr hWndLock);

var originalPosition = textBox.SelectionStart;

LockWindowUpdate(textBox.Handle);

// ---- do the update here ----

textBox.SelectionStart = originalPosition;
textBox.ScrollToCaret();

LockWindowUpdate(IntPtr.Zero);

As long as the textbox doesn't change size (which it doesn't sound like it will) this should work fine. The other option is to use EM_LINESCROLL to store and set the scrollbar value for the textbox.. but that's more involved.

EDIT:

Since that didn't work.. here's another option.

First, some Windows APIs:

[DllImport("user32.dll")]
static extern int SetScrollPos(IntPtr hWnd, int nBar, int nPos, bool bRedraw);

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int GetScrollPos(IntPtr hWnd, int nBar);

[DllImport("user32.dll")]
private static extern bool PostMessageA(IntPtr hWnd, int nBar, int wParam, int lParam);

..and some values:

private const int SB_VERT = 0x1;
private const int SB_THUMBPOSITION = 4;
private const int WM_VSCROLL = 0x115;

You can now do this:

var currentPosition = GetScrollPos(textBox.Handle, SB_VERT);

// ---- update the text here ----

SetScrollPos(textBox.Handle, SB_VERT, currentPosition, false);
PostMessageA(textBox.Handle, WM_VSCROLL, SB_THUMBPOSITION + 65535 * currentPosition, 0);

This works perfectly for me. The only problem I have is that it jumps around sometimes purely because my randomly generated string of characters has widely varying widths. As long as yours is roughly similar after each update, it should be fine.

Upvotes: 3

Related Questions