像十六进制查看器一样使用 ListView

Use ListView like a hex viewer

我正在开发一个应用程序,该应用程序具有用作十六进制查看器的列表视图,但存在重要的性能问题。我不是一个非常有经验的程序员,所以我不知道如何优化代码。

这个想法是为了避免在将项目添加到列表视图时应用程序冻结。我正在考虑将项目添加到或多或少的 100 个项目组中,但我不知道如何处理上下滚动,我不确定这是否会解决这些性能问题。

控件将始终具有相同的高度,795px。这是我正在使用的代码:

private void Button_SearchFile_Click(object sender, EventArgs e)
{
    //Open files explorer.
    OpenFileDialog_SearchFile.Filter = "SFX Files (*.sfx)|*.sfx";
    DialogResult openEuroSoundFileDlg = OpenFileDialog_SearchFile.ShowDialog();
    if (openEuroSoundFileDlg == DialogResult.OK)
    {
        //Get the selected file 
        string filePath = OpenFileDialog_SearchFile.FileName;
        Textbox_FilePath.Text = filePath;

        //Clear list.
        ListView_HexEditor.Items.Clear();

        //Start Reading.
        using (BinaryReader BReader = new BinaryReader(File.OpenRead(filePath)))
        {
            //Each line will contain 16 bits.
            byte[] bytesRow = new byte[16];
            while (BReader.BaseStream.Position != BReader.BaseStream.Length)
            {
                //Get current offset.
                long offset = BReader.BaseStream.Position;

                //Read 16 bytes.
                byte[] readedBytes = BReader.ReadBytes(16);

                //Sometimes the last read could not contain 16 bits, with this we ensure to have a 16 bits array.
                Buffer.BlockCopy(readedBytes, 0, bytesRow, 0, readedBytes.Length);

                //Add item to the list.
                ListView_HexEditor.Items.Add(new ListViewItem(new[]
                {
                    //Print offset
                    offset.ToString("X8"),

                    //Merge bits
                    ((bytesRow[0] << 8) | bytesRow[1]).ToString("X4"),
                    ((bytesRow[2] << 8) | bytesRow[3]).ToString("X4"),
                    ((bytesRow[4] << 8) | bytesRow[5]).ToString("X4"),
                    ((bytesRow[6] << 8) | bytesRow[7]).ToString("X4"),
                    ((bytesRow[8] << 8) | bytesRow[9]).ToString("X4"),
                    ((bytesRow[10] << 8) | bytesRow[11]).ToString("X4"),
                    ((bytesRow[12] << 8) | bytesRow[13]).ToString("X4"),
                    ((bytesRow[14] << 8) | bytesRow[15]).ToString("X4"),

                    //Get hex ASCII representation
                    GetHexStringFormat(bytesRow)
                }));
            }
        }
    }
}

private string GetHexStringFormat(byte[] inputBytes)
{
    //Get char in ascii encoding
    char[] arrayChars = Encoding.ASCII.GetChars(inputBytes);
    for (int i = 0; i < inputBytes.Length; i++)
    {
        //Replace no printable chars with a dot.
        if (char.IsControl(arrayChars[i]) || (arrayChars[i] == '?' && inputBytes[i] != 63))
        {
            arrayChars[i] = '.';
        }
    }
    return new string(arrayChars);
}

这是程序的图片:

这是一个常见问题,主 UI 线程上的任何长时间 运行ning 操作都会冻结应用程序。正常的解决方案是 运行 在后台线程上的操作或作为异步方法......或者更常见的是作为两者的混合。

这里的主要问题是 ListView 在加载大量数据时速度很慢。即使是最快的方法——批量加载 ListViewItem 的整个集合——也很慢。使用 340KB 文件进行快速测试 ~0.18s 加载项目,然后~2.3s 将项目添加到控件。由于最后一部分必须在 UI 线程上发生,因此死区时间约为 2.3 秒。

ListView处理大型列表的最流畅解决方案是使用虚拟列表模式。在此模式下,ListView 会在列表滚动时请求可见项目,而不是维护其自己的项目列表。

要实现虚拟 ListView,您需要为 RetrieveVirtualItem 事件提供处理程序,将 VirtualListSize 属性 设置为列表的长度,然后设置 VirtualMode 为真。 ListView 将在需要显示项目时调用您的 RetrieveVirtualItem 处理程序。

这是我的 PoC 代码:

// Loaded data rows.
ListViewItem[]? _rows = null;

// Load data and setup ListView for virtual list.
// Uses Task.Run() to (hopefully) get off the UI thread.
private Task LoadData(string filename)
    => Task.Run(() =>
    {
        // Clear current virtual list
        // NB: Invoke() makes this run on the main UI thread
        Invoke((Action)(() =>
        {
            listView1.BeginUpdate();
            listView1.VirtualMode = false;
            listView1.VirtualListSize = 0;
            listView1.EndUpdate();
        }));

        // Read data into '_rows' field.
        using (var stream = File.OpenRead(filename))
        {
            var buffer = new byte[16];
            var rows = new List<ListViewItem>();
            int rc;
            while ((rc = stream.Read(buffer, 0, buffer.Length)) > 0)
            {
                var items = new[]
                {
                    (stream.Position - rc).ToString("X8"),
                    string.Join(" ", buffer.Take(rc).Select(b => $"{b:X2}")),
                    string.Join("", buffer.Take(rc).Select(b => (char)b).Select(b => char.IsControl(b) ? '.' : b)),
                };
                rows.Add(new ListViewItem(items));
            }
            _rows = rows.ToArray();
        }

        // Enable virtual list mode
        Invoke((Action)(() =>
        {
            listView1.BeginUpdate();
            listView1.VirtualListSize = _rows?.Length ?? 0;
            listView1.VirtualMode = _rows?.Length > 0;
            listView1.EndUpdate();
        }));
    });

// Fetch rows from loaded data.
private void ListView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
    if (_rows is not null && e.ItemIndex >= 0 && e.ItemIndex < _rows.Length)
        e.Item = _rows[e.ItemIndex];
}

(插入关于缺少错误处理等的常见免责声明。)

希望这是不言自明的。我在内容生成中采用了一些 LINQ 快捷方式,减少了列数。更多的列意味着滚动期间更慢的刷新时间,无论您是使用虚拟列表还是让控件处理项目集合。


对于非常大的文件,此方法仍然是一个问题。如果您经常加载大小为 10 MB 或更大的文件,那么您需要更有创意地一次加载数据块,并可能创建和缓存 ListViewItems on-the-飞行而不是在初始加载阶段。查看 ListView.VirtualMode 文档。示例代码显示了完整的功能......尽管他们的缓存策略有点简陋。