Auto scroll horizontally while adding columns to a ListView

Viewed 262

I want to auto scroll the ListView horizontally to the last column every time a ColumnHeader is added.
I need only to scroll to the last Column, not to the item.

  • listView1.Alignment is set to Left
  • listView1.View is set to View.Details

I tried this, but it doesn't do anything:

listView1.AutoScrollOffset = new Point(listView1.AutoScrollOffset.X-10, 0);`

This only works if I add only items:

listView1.EnsureVisible(0);

string rowstr = "Test,";
for (var i = 0; i < 10; i++)
{
    Debug.WriteLine(i);
    
    ColumnHeader head = new ColumnHeader();
    head.Text = i.toString();
    listView1.Columns.Add(head);
    listView1.Columns[i].Width = 65;
    
    rowstr += "Test"+",";
    string[] row = rowstr.Split(",");
    var listViewItem = new ListViewItem(row);
    listViewItem.Font = new Font("Consolas", 10f);
    listView1.Items.Insert(0, listViewItem);

    //listView1.EnsureVisible(0);
    listView1.AutoScrollOffset = new Point(listView1.AutoScrollOffset.X-10, 0); 
}

I didn't try to override WndProc or to call a User32 function, but if this is the only option any help is appreciated.
If it's possibile without PInvoking, that would be better.

3 Answers

To scroll horizontally a ListView, you can send a LVM_SCROLL message to the Control, setting wParam to a value that correspond to position (in pixels) to scroll to.
The position is relative to the current offset.
Set lParam to scroll vertically.

Since you want to scroll to the last Column and the ListView is in Details mode, you can just pass int.MaxValue as the offset: the Win32 Control will do the adjustment (it does it anyway).

[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern int SendMessage(IntPtr hWnd, int uMsg, int wParam, int lParam);

private const int LVM_SCROLL = 0x1014;
SendMessage(listView1.Handle, LVM_SCROLL, int.MaxValue, 0);

EDIT:
Modify your code, adding an interval between the each Column/ListItem insertion, to see it in slow motion. You can use a Button.Click handler.

private async void SomeButton_Click(object sender, EventArgs e)
{
    for (int idx = 0; idx < 10; idx++) {
        var head = new ColumnHeader() {
            Text = idx.ToString(),
            Width = 65
        };
        listView1.Columns.Add(head);
        SendMessage(listView1.Handle, LVM_SCROLL, int.MaxValue, 0);
        var rowArray = new List<string>(Enumerable.Range(0, idx + 1).Select(n => $"Test{n}"));
        var listViewItem = new ListViewItem(rowArray.ToArray());
        listView1.Items.Insert(0, listViewItem);
        SendMessage(listView1.Handle, LVM_SCROLL, int.MaxValue, 0);
        await Task.Delay(500);
    }
}

Remove the async stuff to let it go free.


To get the current scroll position, if needed, use GetScrollInfo(). E.g.,

var scrollInfo = new SCROLLINFO(SBInfoMask.SIF_ALL);
bool result = GetScrollInfo(listView1.Handle, SBParam.SB_HORZ, ref scrollInfo);

The nPos member of the SCROLLINFO struct returns the current scroll position. nMax the maximum scroll value. Subtract nPage, corresponding to the Control's ClientSize.Width, to get the maximum possible scroll value.
It should be equal to nPos when the thumb is scrolled to the end.

Win32 declarations:

[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool GetScrollInfo(IntPtr hwnd, SBParam nBar, [In, Out] ref Point 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)
}

public enum SBParam : int
{
    SB_HORZ = 0x0,
    SB_VERT = 0x1,
    SB_CTL = 0x2,
    SB_BOTH = 0x3
}

You can create a CustomListView and Re_Define the EnsureVisible method

class DataFlowFilterListView : ListView
{
    /// <summary>
    /// margin from the selected column to the border of listview.
    /// </summary>
    const int MARGIN = 20;

    /// <summary>
    /// native windows message to scroll the listview.
    /// </summary>
    const Int32 LVM_FIRST = 0x1000;
    const Int32 LVM_SCROLL = LVM_FIRST + 20;

    [DllImport("user32")]
    static extern IntPtr SendMessage(IntPtr Handle, Int32 msg, IntPtr wParam,
    IntPtr lParam);

    private void ScrollHorizontal(int pixelsToScroll)
    {
       SendMessage(this.Handle, LVM_SCROLL, (IntPtr)pixelsToScroll,
       IntPtr.Zero);
    }

     /// <summary>
     /// Ensure visible of a ListViewItem and SubItem Index.
     /// 
     /// 
     /// </summary>
     /// <param name="item"></param>
     /// <param name="subItemIndex"></param>
     public void EnsureVisible(ListViewItem item, int subItemIndex)
     {
         if (item == null || subItemIndex > item.SubItems.Count - 1)
         {
           throw new ArgumentException();
         }

         // scroll to the item row.
         item.EnsureVisible();
         ScrollToRectangle(item.SubItems[subItemIndex].Bounds.Width);
     }

     /// <summary>
     /// Scrolls the listview.
     /// </summary>
     /// <param name="bounds"></param>
     private void ScrollToRectangle(int width)
     {
         this.ScrollHorizontal(width);
     }
}

Now you can use DataFlowFilterListView instead of ListView.


Execute the following code each time you add a column to the list

listView1.EnsureVisible(listView1.Items[0], 0);

My solution is using user32 function and SendMessage().

[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, int wParam, int lParam);

private const int WM_HSCROLL = 0x114; //for horizontal scroll
private const int WPARAM = 7; //to scroll to end

Implementation:

string rowstr = "Code,"
for (var i = 0; i < 10; i++)
{
    ColumnHeader head = new ColumnHeader();
    head.Text = i.toString();
    listView1.Columns.Add(head);
    listView1.Columns[i].Width = 65;
    
    rowstr += "Test"+",";
    string[] row = rowstr.Split(",");
    var listViewItem = new ListViewItem(row);
    listViewItem.Font = new Font("Consolas", 10f);
    listView1.Items.Insert(0, listViewItem);
    SendMessage(listView1.Handle, WM_HSCROLL, WPARAM, 0);
}
Related