3

我正在编写一些通过 dll 与外部硬件对话的软件(移动一些电机并读回一些值)。对 dll 的调用被阻塞,并且可能不会在 10 秒内返回。该软件通过移动硬件、读取并重复多个点来执行扫描。一次扫描可能需要大约 30 分钟才能完成。在扫描运行时,我显然希望 GUI 能够响应,并且在每个点更新传入数据的实时图表(在 MDI 子项中)。多线程似乎是解决这个问题的明显选择。

我的问题是,在扫描期间线程化并与主 VCL 线程对话以更新图形的最佳方法是什么?

我目前在 ChildForm 的公共 var 部分中有一个执行“扫描逻辑”的 TThread 后代和一个双精度数组。我需要从线程中填写这个数组,但我不知道是否使用 Synchronize 或 CriticalSection 或 PostMessage 或其他方法。每次添加新值时,主 VCL 线程都需要更新图形。我真的应该为作为全局变量的数据提供一个中间对象,并以某种方式分别从 Thread 和 ChildForm 访问它吗?

4

4 回答 4

6

从线程更新 GUI 的最简单方法是与和anonymous methods结合使用。TThread.SynchronizeTThread.Queue

procedure TMyThread.Execute;
begin
  ...
  Synchronize(  // Synchronous example
    procedure
    begin
      // Your code executed in main thread here 
    end
  );
  ...
  Queue( // Asynchronous example
    procedure
    begin
      // Your code executed in main thread here
    end
  );
end;

异步传递值通常需要“捕获”一个值。

procedure TMyThread.PassAValue(anInteger: Integer);
begin
  Queue(
    procedure
    begin
      // Use anInteger in main thread 
    end
  );
end;

procedure TMyThread.Execute;
var
  myInt: Integer;
begin
  ...
  PassAValue(myInt);  // Capture myInt
  ...
end;

当匿名方法使用变量时,会捕获对变量的引用。这意味着如果您在执行匿名方法之前更改变量值,则会使用新值。因此需要捕捉“价值”。

可以在此处找到更详细的示例synchronize-and-queue-with-parameters,作者@UweRaabe

于 2013-10-01T12:05:41.687 回答
1

如果您想多投入一点,而不是一个简单的 Synchronize 调用,顺便阻塞主线程,您可以添加一个简单的 FIFO 队列,上面有消息传递。数据流是这样的:

  1. 线程将数据放入队列。
  2. 该线程将消息发布到主线程窗口。哪个我不在乎:)
  3. 您处理数据可用的消息,并按照您认为合适的方式处理队列中的任何消息。

代码看起来像这样:

队列...

const
  WM_DataAvailable = WM_USER + 1;

var
  ThreadSafeQueue: TThreadSafeQueue;

数据被放入队列...

procedure PutDataIntoQueue;
var
  MyObject: TMyObject;
begin
  MyObject := TMyObject.Create;
  ThreadSafeQueue.Enqueue(MyObject);
  PostMessage(FMainWindowHandle, WM_DataAvailable, 0, 0);
end;

和处理...

procedure ProcessDataInTheQueue(var Msg: TMessage); message WM_DataAvailable;

procedure ProcessDataInTheQueue(var Msg: TMessage);
var
  AnyValue: TAnyValue;
  MyObject: TMyObject;
begin
  while ThreadSafeQueue.Dequeue(AnyValue) do
  begin
    MyObject := TMyObject(AnyValue.AsObject);
    try
      // process the actual object as needed
    finally
      MyObject.Free
    end;
  end;
end;

代码是在没有 Delphi 的情况下编写的,并且会检查它是否包含错误。我使用免费提供的线程安全队列和 TAnyValue 展示了这个示例。你可以在这里找到两者:

http://www.cromis.net/blog/downloads/

另请注意,我没有检查 PostMessage 是否实际发送。您应该在生产代码中检查这一点。

于 2013-10-01T13:19:53.957 回答
1

我发现TThreadList从后台线程填充一个,然后向主线程发布一条消息,表明列表中有一个新项目,然后在主线程中处理列表是简单且易于维护的。

使用这种方法,您可以在列表中存储任意数量的读数,并且每次主线程收到消息时,它都会立即处理列表中的所有项目。

Define a class for the readings, instantiate them, and add them to the list in the background thread. Don't forget to free them in the main thread when you pop them off the list.

于 2013-10-01T13:50:44.980 回答
0

在您的线程中使用 postmessage 并将消息发送到主表单句柄。注册一个(或多个)自定义消息并为它们编写一个处理程序。

const WM_MEASURE_MESSAGE = WM_USER + 1;

创建一个线程类,添加一个 MainFormHandle 属性(Thandle 或 cardinal)。创建挂起的线程,使用主窗体句柄设置 MainFormHandle,然后恢复线程。当您有一个新度量时,为 data1 和 data2 dword 分配一些来自度量的数据,然后

PostMessage(fMainFormHandle,WM_MEASURE_MESSAGE,data1,data2);

在主窗体中,您有消息处理程序:

procedure MeasureMessage(var msg: TMessage); message WM_MEASURE_MESSAGE;
begin
  // update graph here
  // msg.wparam is data1
  // msg.lparam is data2
end;

如果您需要从线程向主窗体发送更多数据,您可以在主上下文中为整个测量数据创建一个适当的结构,传递对线程的引用,让线程写入数据并使用消息来告诉主窗体新数据位置(例如数组索引)。在主上下文中使用 TThread.Waitfor 以避免在线程仍在运行(并写入内存)时释放数据结构。

于 2013-10-01T11:05:15.210 回答