执行长时间运行的导出任务时如何拥有响应式 UI(表单)?
Posted
技术标签:
【中文标题】执行长时间运行的导出任务时如何拥有响应式 UI(表单)?【英文标题】:How to have responsive UI (Form) when performing long-running export task? 【发布时间】:2013-11-29 22:13:28 【问题描述】:大家好。首先,我不是以英语为母语的人,我可能有一些语法错误等。
我需要那些做过某事的人或类似我的应用程序的人的建议,嗯,问题是我在我的 delphi 表单中使用了一个 TProgressBar,另一个名为“TExcelApplication”的组件和一个 TDBGrid。
当我导出 DBGrid 的内容时,应用程序“冻结”,所以我基本上把那个 ProgressBar 放给用户看这个过程完成了多少。我意识到当 TDBGrid 检索每一行并将其导出到新的 Excel 工作簿时,您无法移动实际的表单,因此您必须等到该过程完成才能移动该表单。
那么,是否有可能做一些事情(我考虑过线程,但我不确定它们是否可以提供帮助)以便用户可以根据需要移动窗口?
非常感谢您花时间阅读并给我建议。我正在使用 Delphi XE。
这是我用来导出行的代码:
with ZQDetalles do
begin
First;
while not EOF do
begin
i := i + 1;
workSheet.Cells.Item[i,2] := DBGridDetalles.Fields[0].AsString;
workSheet.Cells.Item[i,3] := DBGridDetalles.Fields[1].AsString;
workSheet.Cells.Item[i,4] := DBGridDetalles.Fields[2].AsString;
workSheet.Cells.Item[i,5] := DBGridDetalles.Fields[3].AsString;
workSheet.Cells.Item[i,6] := DBGridDetalles.Fields[4].AsString;
workSheet.Cells.Item[i,7] := DBGridDetalles.Fields[5].AsString;
workSheet.Cells.Item[i,8] := DBGridDetalles.Fields[6].AsString;
workSheet.Cells.Item[i,9] := DBGridDetalles.Fields[7].AsString;
Next;
barraProgreso.StepIt;
end;
end;
如果您想查看“导出”按钮的完整代码,请随时查看此链接:http://pastebin.com/FFWAPdey
【问题讨论】:
您要导出的数据集中有多少行和多少列?如果数据量大小合理,您可以使用变体数组进行批量传输,从而更快地导出到 Excel;它比逐行逐列更新要快得多。您还需要包含当前用于导出的代码;可能有一些方法可以加快它的速度,所以它不会花很长时间,但是没有代码我们无法判断。 另外,请注意您在此处选择的标签的描述。您使用的advice
标签与您在主题和问题文本中要求的advice
的含义不同。请不要只是在不知道它们在这里指的是什么的情况下抓取标签。 *** 不是一个建议站点。 :-)
@Cycascovar:问题不在于如何将数据从 DBGrid 获取到 ExcelApplication,而在于如何从将 DBGrid 填充到其中的数据集中获取数据。 a) 什么类型的数据集连接到连接到 DBGrid 的 TDataSource? b) 数据集中大约有多少条记录?
@Martyn:我在第一条评论中不是已经说过了吗? (“您要导出的数据集中有多少行和列?”):-) 数据集的 type 并不重要;不过,数据量是有意义的。
最快速和肮脏的解决方案是每 N 次循环迭代 (N > 0) 调用 Application.Process
。不要忘记禁用控件。
【参考方案1】:
每当您在带有 GUI 的应用程序中执行需要大量时间的操作时,您都希望将其放在单独的线程中,以便用户仍然可以操作表单。你可以这样声明一个简单的线程:
TWorkingThread = class(TThread)
protected
procedure Execute; override;
procedure UpdateGui;
procedure TerminateNotify(Sender: TObject);
end;
procedure TWorkingThread.Execute;
begin
// do whatever you want to do
// make sure to use synchronize whenever you want to update gui:
Synchronize(UpdateGui);
end;
procedure TWorkingThread.UpdateGui;
begin
// e.g. updating the progress bar
end;
procedure TWorkingThread.TerminateNotify(Sender: TObject);
begin
// this gets executed when the work is done
// usually you want to give some kind of feedback to the user
end;
// ...
// calling the thread:
procedure TSettingsForm.Button1Click(Sender: TObject);
var WorkingThread: TWorkingThread;
begin
WorkingThread := TWorkingThread.Create(true);
WorkingThread.OnTerminate := TerminateNotify;
WorkingThread.FreeOnTerminate := true;
WorkingThread.Start;
end;
这很简单,当您想从线程更新视觉元素时,请记住始终使用同步。通常,您还需要注意用户不能在线程仍在工作时再次调用线程,因为他现在可以使用 GUI。
【讨论】:
对于发帖者的问题,使用它只有一个问题,那就是需要为线程设置 COM(使用CoInitializeEx
),因为它使用的是 COM 自动化。您还需要一个新的数据库连接和数据集用于单独的线程,这意味着连接到数据库并在线程的上下文中再次获取正确的行和列。【参考方案2】:
如果行数很少(并且您知道将有多少行),您可以使用变体的变体数组更快地(并且一次全部)传输数据,如下所示:
var
xls, wb, Range: OLEVariant;
arrData: Variant;
RowCount, ColCount, i, j: Integer;
Bookmark: TBookmark;
begin
// Create variant array where we'll copy our data
// Note that getting RowCount can be slow on large datasets; if
// that's the case, it's better to do a separate query first to
// ask for COUNT(*) of rows matching your WHERE clause, and use
// that instead; then run the query that returns the actual rows,
// and use them in the loop itself
RowCount := DataSet1.RecordCount;
ColCount := DataSet1.FieldCount;
arrData := VarArrayCreate([1, RowCount, 1, ColCount], varVariant);
// Disconnect from visual controls
DataSet1.DisableControls;
try
// Save starting row so we can come back to it after
Bookmark := DataSet1.GetBookmark;
try
fill array
i := 1;
while not DataSet1.Eof do
begin
for j := 1 to ColCount do
arrData[i, j] := DataSet1.Fields[j-1, i-1].Value;
DataSet1.Next;
Inc(i);
// If we have a lot of rows, we can allow the UI to
// refresh every so often (here every 100 rows)
if (i mod 100) = 0 then
Application.ProcessMessages;
end;
finally
// Reset record pointer to start, and clean up
DataSet1.GotoBookmark;
DataSet1.FreeBookmark;
finally
// Reconnect GUI controls
DataSet1.EnableControls;
end;
// Initialize an instance of Excel - if you have one
// already, of course the next couple of lines aren't
// needed
xls := CreateOLEObject('Excel.Application');
// Create workbook - again, not needed if you have it.
// Just use ActiveWorkbook instead
wb := xls.Workbooks.Add;
// Retrieve the range where data must be placed. Again, your
// own WorkSheet and start of range instead of using 1,1 when
// needed.
Range := wb.WorkSheets[1].Range[wb.WorkSheets[1].Cells[1, 1],
wb.WorkSheets[1].Cells[RowCount, ColCount]];
// Copy data from allocated variant array to Excel in single shot
Range.Value := arrData;
// Show Excel with our data
xls.Visible := True;
end;
循环遍历数据的行和列仍需要相同的时间,但将数据实际传输到 Excel 所需的时间大大减少,尤其是在数据量很大的情况下。
【讨论】:
以上是关于执行长时间运行的导出任务时如何拥有响应式 UI(表单)?的主要内容,如果未能解决你的问题,请参考以下文章