当更新时防止ListBox滚动到顶部

4
我正在尝试构建一个简单的音乐播放器,其中包含一个ListBox播放列表。当向播放列表添加音频文件时,它首先用文件名填充ListBox,然后(在单独的线程上)提取ID3数据并使用正确的艺术家-标题信息覆盖文件名(类似于Winamp)。
但是,在更新ListBox时,它无法滚动,因为它总是跳到每个项目上方。
有没有办法防止这种情况发生?
编辑: 代码:
public Form1()
{
    //Some initialization code omitted here

    BindingList<TAG_INFO> trackList = new BindingList<TAG_INFO>();

    // The Playlist
    this.playlist = new System.Windows.Forms.ListBox();
    this.playlist.Location = new System.Drawing.Point(12, 12);
    this.playlist.Name = "playlist";
    this.playlist.Size = new System.Drawing.Size(229, 316);
    this.playlist.DataSource = trackList;
}

private void playlist_add_Click(object sender, EventArgs e)
{
    //Initialize OpenFileDialog
    OpenFileDialog opd = new OpenFileDialog();
    opd.Filter = "Music (*.WAV; *.MP3; *.FLAC)|*.WAV;*.MP3;*.FLAC|All files (*.*)|*.*";
    opd.Title = "Select Music";
    opd.Multiselect = true;

    //Open OpenFileDialog
    if (DialogResult.OK == opd.ShowDialog())
    {

        //Add opened files to playlist
        for (int i = 0; opd.FileNames.Length > i; ++i)
        {
            if (File.Exists(opd.FileNames[i]))
            {
                trackList.Add(new TAG_INFO(opd.FileNames[i]));
            }
        }

        //Initialize BackgroundWorker
        BackgroundWorker _bw = new BackgroundWorker();
        _bw.WorkerReportsProgress = true;
        _bw.DoWork += new DoWorkEventHandler(thread_trackparser_DoWork);
        _bw.ProgressChanged += new ProgressChangedEventHandler(_bw_ProgressChanged);

        //Start ID3 extraction
        _bw.RunWorkerAsync();
    }

}

void thread_trackparser_DoWork(object sender, DoWorkEventArgs e)
{
    BackgroundWorker _bw = sender as BackgroundWorker;

    for (int i = 0; i < trackList.Count; ++i)
    {
        //Pass extracted tag info to _bw_ProgressChanged for thread-safe playlist entry update
        _bw.ReportProgress(0,new object[2] {i, BassTags.BASS_TAG_GetFromFile(trackList[i].filename)});
    }
}

void _bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    object[] unboxed = e.UserState as object[];

    trackList[(int)unboxed[0]] = (unboxed[1] as TAG_INFO);
}

编辑2:
更简单的测试案例:
尝试在不选择项目的情况下向下滚动。变化的列表框将会再次滚动到顶部。

using System;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public class Form1 : Form
    {
        private System.ComponentModel.IContainer components = null;

        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        private void InitializeComponent()
        {
            this.components = new System.ComponentModel.Container();
            this.listBox1 = new System.Windows.Forms.ListBox();
            this.timer1 = new System.Windows.Forms.Timer(this.components);
            this.SuspendLayout();

            // listBox1
            this.listBox1.FormattingEnabled = true;
            this.listBox1.Location = new System.Drawing.Point(0, 0);
            this.listBox1.Name = "listBox1";
            this.listBox1.Size = new System.Drawing.Size(200, 290);

            // timer1
            this.timer1.Enabled = true;
            this.timer1.Tick += new System.EventHandler(this.timer1_Tick);

            // Form1
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(200, 290);
            this.Controls.Add(this.listBox1);
            this.Name = "Form1";
            this.Text = "Form1";
            this.ResumeLayout(false);
        }

        private System.Windows.Forms.ListBox listBox1;
        private System.Windows.Forms.Timer timer1;

        public Form1()
        {
            InitializeComponent();

            for (int i = 0; i < 45; i++)
                listBox1.Items.Add(i);
        }

        int tickCounter = -1;

        private void timer1_Tick(object sender, EventArgs e)
        {
            if (++tickCounter > 44) tickCounter = 0;
            listBox1.Items[tickCounter] = ((int)listBox1.Items[tickCounter])+1;
        }
    }

    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

你能发一些代码吗?更新方式可能与问题有关。 - Tim Lloyd
添加了一个更简单的代码来演示同样的问题。 - WDZ
3个回答

5
很好的复现代码,我没有任何困难就能诊断出问题源头。这是一项功能而非漏洞。请按下箭头向下键多次,然后滚动列表。注意现在它不会跳回去了。
这里发生的情况是,当更新后,列表框自动将具有焦点的项目滚动回视图中。通常情况下,这是期望的行为,您无法关闭它。像选择正在更新的项目这样的解决方法,在像这样更新列表时不会很好看,它会闪烁得很厉害。也许虚拟模式可以尝试一下,我没有试过。
ListView没有这种行为,考虑使用它。使用视图 = 列表或详细信息。

很棒的详尽回答。谢谢! - WDZ
完美运行!还使用了 https://dev59.com/AHRB5IYBdhLWcg3w-8Po 来消除更新期间的闪烁。 - WDZ

1

在更新时禁用任何列表框的绘制可能是明智的选择。使用 ListBox.BeginUpdate(),然后更新条目,完成后调用 ListBox.EndUpdate()。这应该确保在更新期间不会发生刷新。

您还应确保不要从线程刷新 ListBox,因为跨线程是不允许的,运行时将显示一条消息,指出“已发生跨线程异常”。

使用 ListBox 的 BeginInvoke(...) 方法解决跨线程问题。请参见 MSDN 上的 此处 了解如何执行此操作。


1

是的,.BeginUpdate() 是一个不错的解决方案:

公共类 Form1

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    Timer1.Start()

End Sub
Dim ix As Integer
Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick

    ix = ListBox1.TopIndex
    Label1.Text = ListBox1.TopIndex     'get most top displayed in the listbox
    ListBox1.BeginUpdate()              'do not refresh during update
    ListBox1.Items.Clear()              'do updates
    For i = 1 To 100
        ListBox1.Items.Add(i.ToString & "." & Int((101) * Rnd()).ToString)
    Next
    ListBox1.TopIndex = ix              'set the top item to be displayed
    ListBox1.EndUpdate()                'refresh the listbox

End Sub

Private Sub VScrollBar1_Scroll(ByVal sender As System.Object, ByVal e As System.Windows.Forms.ScrollEventArgs) _
Handles VScrollBar1.Scroll
    VScrollBar1.Maximum = ListBox1.Items.Count
    ListBox1.TopIndex = VScrollBar1.Value       ' link another scrollbar if you want
    Label2.Text = VScrollBar1.Value

End Sub

结束类

代码自解释。一个计时器正在生成随机数。这些数字被填充到列表框中。在更新之前,只需读取列表框的当前顶部索引。在更新期间,通过Begin Update关闭渲染。然后恢复顶部索引并调用End Update。

我添加了一个额外的滚动条,与列表框相连。


谢谢。TopIndex 对我很有帮助。而 Begin/EndUpdate 使刷新更加“平稳”。 - D.Kastier

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