像十六进制查看器一样使用 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 或更大的文件,那么您需要更有创意地一次加载数据块,并可能创建和缓存 ListViewItem
s on-the-飞行而不是在初始加载阶段。查看 ListView.VirtualMode
文档。示例代码显示了完整的功能......尽管他们的缓存策略有点简陋。
我正在开发一个应用程序,该应用程序具有用作十六进制查看器的列表视图,但存在重要的性能问题。我不是一个非常有经验的程序员,所以我不知道如何优化代码。
这个想法是为了避免在将项目添加到列表视图时应用程序冻结。我正在考虑将项目添加到或多或少的 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 或更大的文件,那么您需要更有创意地一次加载数据块,并可能创建和缓存 ListViewItem
s on-the-飞行而不是在初始加载阶段。查看 ListView.VirtualMode
文档。示例代码显示了完整的功能......尽管他们的缓存策略有点简陋。