读取excel并且显示进度条
通过C#实现DataGridView加载EXCEL文件,但加载时不能阻塞UI刷新线程,且向UI显示加载进度条。
用EPPLUS导出Excel时出现了如下问题:
即:在设置属性之前,先引入 System.ComponentModel 命名空间,然后在窗体的构造函数中设置 ExcelPackage.LicenseContext 属性(添加下面一句代码):
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
#region 左上角导入
private async void ToolStripMenuItem_ClickAsync(object sender, EventArgs e)
{
dataGridView1.DataSource = null;
dataGridView1.Rows.Clear();
dataGridView1.Columns.Clear();
progressBar1.Value = 0;
progressBar1.Visible = true;
// 打开文件对话框选择 Excel 文件
OpenFileDialog file = new OpenFileDialog();
file.Filter = "Excel文件|*.xlsx";
if (file.ShowDialog() == DialogResult.OK)
{
string fname = file.FileName;
await LoadExcelDataAsync(fname); // 异步加载数据
}
}
// 异步加载 Excel 数据
private async Task LoadExcelDataAsync(string filePath)
{
var progress = new Progress<int>(value =>
{
progressBar1.Value = value; // 更新进度条
label11.Text = $"{value}%"; // Update label to show percentage
});
// 在后台线程中执行加载数据的任务
System.Data.DataTable dataTable = await Task.Run(() => LoadExcelData(filePath, progress));
// 加载完成后,将数据绑定到 DataGridView
dataGridView1.DataSource = dataTable;
progressBar1.Visible = false; // 隐藏进度条
label11.Visible = false; // 隐藏进度条
}
// 使用 EPPlus 按行读取 Excel 数据并动态更新进度
private System.Data.DataTable LoadExcelData(string filePath, IProgress<int> progress)
{
FileInfo fileInfo = new FileInfo(filePath);
using (var package = new ExcelPackage(fileInfo))
{
ExcelWorksheet worksheet = package.Workbook.Worksheets[0];
int rowCount = worksheet.Dimension.Rows;
int columnCount = worksheet.Dimension.Columns;
// 创建 DataTable 来存储数据
System.Data.DataTable dt = new System.Data.DataTable();
// 为 DataTable 添加列
for (int col = 1; col <= columnCount; col++)
{
dt.Columns.Add(worksheet.Cells[1, col].Text); // 第一行作为列名
}
// 逐行读取数据并添加到 DataTable 中
int progressInterval = rowCount / 100; // 每读取一定行数更新进度
int progressPercentage = 0;
int rowCountProcessed = 0;
for (int row = 2; row <= rowCount; row++) // 跳过第一行作为标题行
{
DataRow newRow = dt.NewRow();
for (int col = 1; col <= columnCount; col++)
{
newRow[col - 1] = worksheet.Cells[row, col].Text;
}
dt.Rows.Add(newRow);
rowCountProcessed++;
if (rowCountProcessed % progressInterval == 0)
{
progressPercentage = (int)((float)rowCountProcessed / rowCount * 100);
progress.Report(progressPercentage); // 更新进度条
}
}
return dt;
}
}
var progress = new Progress<int>(value =>
{
progressBar1.Value = value; // 更新进度条
label11.Text = $"{value}%"; // Update label to show percentage
}); 为什么会出现感觉 value是百分之30或者90 但是进度条只到一半
如果您观察到进度条只显示到一半,即使 value 显示为 30% 或 90%,可能的原因包括:
进度条的最大值设置不正确:
默认情况下,ProgressBar 的最大值是 100。如果您的进度条的最大值被设置为不同的值(比如 50),那么即使 value 达到了 50,进度条也会显示满格,因为它已经达到了其最大值。如果 value 达到 90 而进度条只显示一半,可能是因为进度条的最大值被错误地设置为了 200 或更高。
检查并设置 progressBar1.Maximum 为 100,确保它与 value 的范围相匹配。
数据同步问题:
如果您的进度更新是在非 UI 线程上执行的(比如在后台工作线程中),而 UI 更新(如设置 progressBar1.Value)没有正确地进行线程同步,可能会导致 UI 更新不及时或不一致。
确保所有的 UI 更新都在 UI 线程上执行。如果您在后台线程中更新 UI,可以使用 Invoke 或 BeginInvoke 方法(取决于您使用的 UI 框架,如 WinForms 或 WPF)来确保 UI 更新在 UI 线程上执行。
逻辑错误:
检查您的代码逻辑,确保 value 的计算正确无误,并且确实代表了进度的百分比。
UI 更新频率:
如果进度更新非常频繁,UI 可能无法跟上这些更新,导致显示的进度与实际进度不符。这通常可以通过减少更新频率或优化更新逻辑来解决。
为了解决这个问题,您可以考虑以下几种策略:
减少更新频率:
如果可能,减少后台线程发送进度更新到 UI 线程的频率。例如,您可以实现一个逻辑,只在进度有实质性变化时才发送更新,或者设置一个最小的时间间隔,在该间隔内只发送一次更新。
使用缓冲或合并更新:
在后台线程中,您可以维护一个队列或缓冲区来存储进度更新。然后,您可以定期(而不是每次更新时)将这个队列中的更新发送到 UI 线程。这种方法可以减少 UI 线程的负载,并允许它更平滑地处理更新。
优化 UI 更新逻辑:
确保 UI 更新是高效的。例如,避免在 UI 更新过程中执行耗时的操作,如复杂的计算或数据库查询。此外,如果可能,使用异步方法来更新 UI,以便不会阻塞 UI 线程的其他操作。
使用适当的同步机制:
当从后台线程更新 UI 时,使用适当的同步机制(如 Invoke 或 BeginInvoke 在 WinForms 中,或在 WPF 中使用 Dispatcher.Invoke 或 Dispatcher.BeginInvoke)。这可以确保 UI 更新在 UI 线程上正确执行,并减少因线程竞争而导致的潜在问题。
考虑使用专门的进度报告组件:
在某些框架或库中,可能有专门用于处理进度报告的组件或服务。这些组件通常已经优化了更新频率和同步逻辑,可以提供更好的性能和用户体验。
监控和调试:
使用性能监控工具来观察 UI 线程的负载和响应时间。这可以帮助您识别哪些更新是导致性能问题的根源,并据此调整更新策略。
是的,UI 更新频率过高可能会导致进度条显示不准确,甚至会让进度条看起来没有正常更新。这是因为 UI 控件的更新操作通常是比较耗时的,尤其是在进度更新频繁的情况下,UI 线程可能无法及时处理这些更新,从而造成界面卡顿或者进度显示不准确。
UI 更新频率过高常见的解决方案
减少更新频率: 可以通过减少进度更新的频率来避免这个问题。例如,每次更新进度时,只有在进度发生了足够大的变化时才更新 UI(例如,每隔一定的时间或者当进度值的变化大于某个阈值时)。
示例代码:
csharp复制代码int lastReportedProgress = 0; var progress = new Progress<int>(value => { // 只更新进度条和标签当进度值增加了10%或者更多 if (Math.Abs(value - lastReportedProgress) >= 10) { progressBar1.Value = value; // 更新进度条 label11.Text = $"{value}%"; // 更新标签 lastReportedProgress = value; } });使用
Timer控制更新频率: 你可以使用Timer来控制进度更新的频率,避免每次进度变化都更新 UI。这样可以确保UI控件只在特定的时间间隔内更新,而不是在每次进度变化时都更新。示例代码:
csharp复制代码Timer timer = new Timer(); int lastReportedProgress = 0; timer.Interval = 100; // 每100ms更新一次 timer.Tick += (sender, e) => { if (progressValue != lastReportedProgress) { progressBar1.Value = progressValue; label11.Text = $"{progressValue}%"; lastReportedProgress = progressValue; } }; timer.Start();使用
Task.Delay控制更新间隔: 如果你是在异步任务中更新进度,也可以通过插入适当的延时来降低更新频率。例如,使用Task.Delay来等待一段时间,确保不会过于频繁地更新 UI。示例代码:
csharp复制代码var progress = new Progress<int>(async value => { if (Math.Abs(value - lastReportedProgress) >= 10) { progressBar1.Value = value; label11.Text = $"{value}%"; lastReportedProgress = value; // 延迟100ms再允许下一次更新 await Task.Delay(100); } });避免在
Progress<T>的回调中直接更新 UI:Progress<T>是线程安全的,它会在 UI 线程上更新 UI,但如果更新太频繁,仍然可能出现问题。考虑是否有办法将进度更新逻辑优化为较少的频次,或者在后台线程中积累进度,最后一次性更新 UI。使用
Invoke或BeginInvoke: 如果你在其他线程更新 UI,可以确保在 UI 线程上安全地更新进度。虽然Progress<int>会在 UI 线程上调用回调,但有时可以手动调用Invoke来确保UI更新不会阻塞其他操作。示例代码:
csharp复制代码if (progressBar1.InvokeRequired) { progressBar1.Invoke(new Action(() => { progressBar1.Value = value; label11.Text = $"{value}%"; })); } else { progressBar1.Value = value; label11.Text = $"{value}%"; }
总结
通过减少更新频率、优化进度更新的逻辑、控制更新间隔等方式,可以有效避免进度条更新过于频繁导致 UI 卡顿或进度显示不准的问题。最常用的做法是只在进度变化了较大的时候才更新 UI,或者通过定时器来控制更新频率。