ListView 滚动控制 - 如果用户没有滚动,则滚动到底部?

7

我有一个.NET 3.5 WinForm,其中包含一个以详细模式显示的ListView。它作为长时间后台任务上状态项目的可滚动列表。我将最新的ListViewItem(状态条目)添加到底部。为了确保它被看到,我在添加后确保新项目的可见性。这一切都很好;列表视图自动滚动到底部以显示最新的项目。

private void AddListItem(DateTime timestamp, string message, int index)
{
    var listItem = new ListViewItem(timestamp.ToString());
    listItem.SubItems.Add(message);
    statusList.Items.Insert(index, listItem);
    statusList.Items[statusList.Items.Count - 1].EnsureVisible();
}

问题是,如果用户向上滚动查看旧消息,则ListView将向下滚动以使新项目可见。有没有一种方法可以控制此行为,以检查用户是否与滚动条进行交互(特别是如果他们在滚动条上按住鼠标按钮)?如果始终在底部,则可能也可以检测滚动条是否在底部。如果不在底部,则我将不会确保最新项目的可见性。类似于:

private void AddListItem(DateTime timestamp, string message, int index)
{
    var listItem = new ListViewItem(timestamp.ToString());
    listItem.SubItems.Add(message);
    statusList.Items.Insert(index, listItem);
    if (!statusList.IsScrollbarUserControlled)
    {
        statusList.Items[statusList.Items.Count - 1].EnsureVisible();
    }
}

奇怪的是,当用户按住滚动条的“手柄”时,手柄不会移动(暗示视图实际上并没有通过程序滚动下来),但事实上确实滚动了。

更新: 是否有可能检测滚动条的位置,即是否在底部?

2个回答

5
解决这个问题的步骤如下:
  1. WinForms ListView没有滚动事件,我们需要定义一个。
  2. 确定ListView何时处于空闲状态,并在它空闲一段时间后调用EnsureVisible。
针对第一个问题,从ListView中继承一个新类,覆盖Windows消息循环,并在用户滚动它时触发一个事件:
public class MyListView : ListView
{
    public event EventHandler<EventArgs> Scrolled;

    protected override void WndProc(ref Message m)
    {
        base.WndProc(ref m);

        const int wm_vscroll = 0x115;
        if (m.Msg == wm_vscroll && Scrolled != null)
        {
            Scrolled(this, new EventArgs());
        }
    }
}

现在我们知道用户何时滚动了列表视图。您接下来需要解决的问题是确定列表视图是否处于空闲状态;也就是说,如果用户已经有一段时间没有滚动它了。
有多种方法可以实现这个目的。为此,我将只使用时间戳来指示上次滚动时间:
private DateTime lastScrollTime;

...

listView.Scrolled += delegate { lastScrollTime = DateTime.Now };

...

private void AddListItem(DateTime timestamp, string message, int index)
{
    var listItem = new ListViewItem(timestamp.ToString());
    listItem.SubItems.Add(message);
    statusList.Items.Insert(index, listItem);

    // Scroll down only if the list view is idle.
    var idleTime = TimeSpan.FromSeconds(5);
    var isListViewIdle = DateTime.Now.Subtract(this.lastScrollTime) > idleTime;
    if (isListViewIdle)
    {
       statusList.Items[statusList.Items.Count - 1].EnsureVisible();
    }
}

这会对性能造成多大的影响?你以前用过这个吗? - Stealth Rabbi
我很好奇这个0x115常量是从哪里来的。 - Julien Guertault
1
@JulienGuertault 这些常量来自于Windows SDK中的Windows消息常量,位于CommCtrl.h和WinUser.h文件中。您可以在MSDN以及P/Invoke.NET上找到它们:http://pinvoke.net/default.aspx/Constants.WM - Judah Gabriel Himango
好的,谢谢你提供链接。我以为那是某种神奇的数字。不使用宏的原因是让代码更易读吗? - Julien Guertault
宏是什么意思?这是C#。我将WM_SCROLL定义为常量,并使用该常量的名称。我不确定如何使其更清晰。 - Judah Gabriel Himango
与其查看滚动时间,不如检查视图是否已滚动到底部。如果是,则滚动;如果不是,则不要滚动。Fiddler将此作为其“SmartScroll”功能。 - EricLaw

3
与SysInternals的ProcMon相比,添加一个名为“自动滚动”的复选框,以便用户可以关闭它。

不确定为什么这个被-1了。虽然这并没有回答问题,但你提供了一个非常合理的替代方案。 - Stealth Rabbi
@Stealth - 有些人正在系统性地对我的答案进行负面评价。不知道为什么,不用担心。感谢你的投票。 - Hans Passant
这里的两个答案都看起来是正确的,但最终我会选择你的建议,因为我认为它更符合用户的需求,并且在功能上不会带来太多的意外。我按照你的建议使用了处理器监视器,这在用户体验方面效果不错。 - Stealth Rabbi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接