执行长时间运行的导出任务时如何拥有响应式 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(表单)?的主要内容,如果未能解决你的问题,请参考以下文章

响应式网页设计需注意9点

响应式网页设计中的单个样式表与多个样式表

windows计划任务执行mysqldump大数据表不成功小数据表没问题

高性能JavaScript(DOM编程)快速响应的用户界面

性能优化之快速响应的用户界面

当一个线程完成任务而另一个线程正在运行时,有没有办法更新 Web 表单 UI