Цикл сообщений темы для темы со скрытым окном?

У меня есть приложение Delphi 6, в котором есть поток, предназначенный для связи с внешним приложением, которое использует сообщения SendMessage () и WM_COPYDATA для взаимодействия с внешними программами. Поэтому я создаю скрытое окно с помощью AllocateHWND () для обслуживания, которое необходимо, поскольку очередь сообщений потока не будет работать из-за того, что функция SendMessage () принимает только дескрипторы окон, а не идентификаторы потоков. Я не уверен в том, что добавить в метод потока Execute ().

Я предполагаю, что если я использую цикл GetMessage () или создаю цикл с вызовом функции WaitFor * () в нем, поток будет заблокирован, и поэтому поток WndProc () никогда не будет обрабатывать сообщения SendMessage () из сторонней программы. правильно? Если да, то каков правильный код для включения цикла Execute (), который не будет излишне потреблять циклы ЦП, но завершится после получения сообщения WM_QUIT? При необходимости я всегда могу выполнить цикл с помощью Sleep (), но мне интересно, есть ли способ лучше.


person Robert Oschler    schedule 08.10.2011    source источник
comment
SendMessage не должен работать с потоком MQ, PostMessage есть.   -  person Premature Optimization    schedule 09.10.2011
comment
SendMessage () по-прежнему требует, чтобы принимающий поток выполнял извлечение сообщения (т. Е. Цикл сообщений), если HWND принадлежит другому процессу.   -  person Remy Lebeau    schedule 09.10.2011


Ответы (2)


AllocateHWnd() (точнее, MakeObjectInstance()) не является потокобезопасным, поэтому вы должны быть осторожны с ним. Лучше использовать CreatWindow/Ex() напрямую вместо (или поточно-ориентированную версию AllocateHWnd(), например _ 5_.

В любом случае HWND привязан к контексту потока, который его создает, поэтому вы должны создавать и уничтожать HWND внутри вашего Execute() метода, а не в конструкторе / деструкторе потока. Кроме того, даже несмотря на то, что SendMessage() используется для отправки вам сообщений, они поступают из другого процесса, поэтому они не будут обрабатываться вашим HWND, пока его собственный поток не выполнит операции извлечения сообщений, поэтому потоку нужен собственный цикл сообщений.

Ваш Execute() метод должен выглядеть примерно так:

procedure TMyThread.Execute;
var
  Message: TMsg;
begin
  FWnd := ...; // create the HWND and tie it to WndProc()...
  try
    while not Terminated do
    begin
      if MsgWaitForMultipleObjects(0, nil^, False, 1000, QS_ALLINPUT) = WAIT_OBJECT_0 then
      begin
        while PeekMessage(Message, 0, 0, 0, PM_REMOVE) do
        begin
          TranslateMessage(Message);
          DispatchMessage(Message);
        end;
      end;
    end;
  finally
    // destroy FWnd...
  end;
end;

procedure TMyThread.WndProc(var Message: TMessage);
begin
  if Message.Msg = WM_COPYDATA then
  begin
    ...
    Message.Result := ...;
  end else
    Message.Result := DefWindowProc(FWnd, Message.Msg, Message.WParam, Message.LParam);
end;

Альтернативно:

// In Delphi XE2, a virtual TerminatedSet() method was added to TThread,
// which is called when TThread.Terminate() is called.  In earlier versions,
// use a custom method instead...

type
  TMyThread = class(TThread)
  private
    procedure Execute; override;
    {$IF RTLVersion >= 23}
    procedure TerminatedSet; override;
    {$IFEND}
  public
    {$IF RTLVersion < 23}
    procedure Terminate; reintroduce;
    {$IFEND}
  end;

procedure TMyThread.Execute;
var
  Message: TMsg;
begin
  FWnd := ...; // create the HWND and tie it to WndProc()...
  try
    while not Terminated do
    begin
      if WaitMessage then
      begin
        while PeekMessage(Message, 0, 0, 0, PM_REMOVE) do
        begin
          if Message.Msg = WM_QUIT then Break;
          TranslateMessage(Message);
          DispatchMessage(Message);
        end;
      end;
    end;
  finally
    // destroy FWnd...
  end;
end;

{$IF RTLVersion < 23}
procedure TMyThread.Terminate;
begin
  inherited Terminate;
  PostThreadMessage(ThreadID, WM_QUIT, 0, 0);
end;
{$ELSE}
procedure TMyThread.TerminatedSet;
begin
  PostThreadMessage(ThreadID, WM_QUIT, 0, 0);
end;
{$IFEND}
person Remy Lebeau    schedule 09.10.2011
comment
Спасибо @Remy Lebeau. MsgWaitForMultipleObjects () был ключевым ингредиентом, которого мне не хватало. - person Robert Oschler; 09.10.2011
comment
Вы должны использовать DSiAllocateHwnd вместо AllocateHwnd. thedelphigeek.com/2007/06/allocatehwnd-is- not-thread-safe.html - person gabr; 09.10.2011
comment
@David: WaitMessage() не возвращается, пока не придет новое сообщение, блокируя вызывающий поток. MsgWaitForMultipleObjects() имеет тайм-аут, поэтому поток может проснуться для выполнения других дел, пока очередь сообщений простаивает, например, для проверки свойства Terminated. Вы можете сделать это WaitMessage(), если только сами не отправите сообщение. - person Remy Lebeau; 09.10.2011
comment
Но я думаю, что лучше отправить сообщение. Не хочу ждать тайм-аута. И если вы сделаете это таким образом, вы можете нормально бездействовать. Ваш код будет просыпаться каждую секунду, несмотря ни на что. - person David Heffernan; 09.10.2011
comment
@RemyLebeau - лучше использовать MsgWaitForMultipleObjects с таймаутом INFINITE, а не 1000, и использовать событие на случай, если нам нужно быстро завершить поток. Вместо события мы можем отправить сообщение в поток после вызова Terminate, чтобы он проверил свое свойство Terminated и завершился. Использование INFINITE значительно уменьшит столбец «Ошибки страниц» в диспетчере задач и сэкономит ресурсы ;-)) - person Maxim Masiutin; 10.05.2017
comment
@MaximMasiutin: да, я все это в курсе. Мои предыдущие комментарии были написаны много лет назад. В современных версиях Delphi TThread имеет виртуальный TerminatedSet() метод, который можно переопределить для выполнения такого сообщения / сигнала при вызове Terminate(). Но исходный вопрос касался Delphi 6, в котором нет TerminatedSet(), поэтому вместо этого потребуется специальный метод. Я обновил свой ответ. - person Remy Lebeau; 10.05.2017
comment
@RemyLebeau - Спасибо за оценку и за обновление ответа. Я также опубликовал код - это аналогичное решение, но не зависит от какой-либо версии Delphi, поскольку оно в основном работает на чистом Win32. - person Maxim Masiutin; 10.05.2017
comment
@RemyLebeau Если бы я использовал AllocateHWnd() и DeallocateHWnd() в контексте основного потока (конструктор и диструктор основной формы) и использовал бы его для отправки сообщений в основной поток, было бы это безопасно? - person Nasreddine Galfout; 26.11.2017
comment
@NasreddineAbdelillahGalfout да - person Remy Lebeau; 26.11.2017
comment
@RemyLebeau Между прочим, ссылка в вашем ответе на DSiAllocateHwnd мертва, пожалуйста, замените ее на ссылку из комментария gabr. - person Nasreddine Galfout; 27.11.2017

Вот цикл, который не требует Classes.pas и полагается исключительно на System.pas для некоторых вспомогательных функций, Windows.pas для функций Win32 API и Messages.pas для констант WM_.

Обратите внимание, что дескриптор окна здесь создается и уничтожается из рабочего потока, но основной поток ждет, пока рабочий поток завершит инициализацию. Вы можете отложить это ожидание до более позднего момента, когда вам действительно понадобится дескриптор окна, чтобы основной поток тем временем мог выполнять некоторую работу, пока рабочий поток сам настраивается.

unit WorkerThread;

interface

implementation

uses
  Messages,
  Windows;

var
  ExitEvent, ThreadReadyEvent: THandle;
  ThreadId: TThreadID;
  ThreadHandle: THandle;
  WindowHandle: HWND;

function HandleCopyData(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT;
begin
  Result := 0; // handle it
end;

function HandleWmUser(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT;
// you may handle other messages as well - just an example of the WM_USER handling
begin
  Result := 0; // handle it
end;

function MyWindowProc(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
begin
  if Msg = WM_COPYDATA then
  begin
    Result := HandleCopyData(hWnd, Msg, wParam, lParam);
  end else
  if Msg = WM_USER then
  begin
    // you may handle other messages as well - just an example of the WM_USER handling
    // if you have more than 2 differnt messag types, use the "case" switch
    Result := HandleWmUser(hWnd, Msg, wParam, lParam);
  end else
  begin
    Result := DefWindowProc(hWnd, Msg, wParam, lParam);
  end;
end;

const
  WindowClassName = 'MsgHelperWndClass';
  WindowClass: TWndClass = (
    style: 0;
    lpfnWndProc: @MyWindowProc;
    cbClsExtra: 0;
    cbWndExtra: 0;
    hInstance: 0;
    hIcon: 0;
    hCursor: 0;
    hbrBackground: 0;
    lpszMenuName: nil;
    lpszClassName: WindowClassName);

procedure CreateWindowFromThread;
var
  A: ATOM;
begin
  A := RegisterClass(WindowClass);
  WindowHandle := CreateWindowEx(WS_EX_TOOLWINDOW, WindowClassName, 'Message Helper Window', WS_POPUP, 0, 0, 0, 0, 0, 0, hInstance, nil);
end;

procedure FreeWindowFromThread;
var
  H: HWND;
begin
  H := WindowHandle;
  WindowHandle := 0;
  DestroyWindow(H);
  UnregisterClass(WindowClassName, hInstance);
end;

function ThreadFunc(P: Pointer): Integer;  //The worker thread main loop, windows handle initialization and finalization
const
  EventCount = 1;
var
  EventArray: array[0..EventCount-1] of THandle;
  R: Cardinal;
  M: TMsg;
begin
  Result := 0;
  CreateWindowFromThread;
  try
    EventArray[0] := ExitEvent; // you may add other events if you need - just enlarge the Events array
    SetEvent(ThreadReadyEvent);
    repeat
      R := MsgWaitForMultipleObjects(EventCount, EventArray, False, INFINITE, QS_ALLINPUT);
      if R = WAIT_OBJECT_0 + EventCount then
      begin
        while PeekMessage(M, WindowHandle, 0, 0, PM_REMOVE) do
        begin
          case M.Message of
             WM_QUIT:
               Break;
             else
                begin
                  TranslateMessage(M);
                  DispatchMessage(M);
                end;
          end;
        end;
        if M.Message = WM_QUIT then
          Break;
      end else
      if R = WAIT_OBJECT_0 then
      begin
        // we have the ExitEvent signaled - so the thread have to quit
        Break;
      end else
      if R = WAIT_TIMEOUT then
      begin
        // do nothing, the timeout should not have happened since we have the INFINITE timeout
      end else
      begin
        // some errror happened, or the wait was abandoned with WAIT_ABANDONED_0 to (WAIT_ABANDONED_0 + nCount– 1)
        // just exit the thread
        Break;
      end;
    until False;
  finally
    FreeWindowFromThread;
  end;
end;

procedure InitializeFromMainThread;
begin
  ExitEvent := CreateEvent(nil, False, False, nil);
  ThreadReadyEvent := CreateEvent(nil, False, False, nil);
  ThreadHandle := BeginThread(nil, 0, @ThreadFunc, nil, 0, ThreadId);
end;

procedure WaitUntilHelperThreadIsReady;
begin
  WaitForSingleObject(ThreadReadyEvent, INFINITE); // wait until the worker thread start running and initialize the main window
  CloseHandle(ThreadReadyEvent); // we won't need it any more
  ThreadReadyEvent := 0;
end;

procedure FinalizeFromMainThread;
begin
  SetEvent(ExitEvent); // we should call it AFTER terminate for the Terminated property would already be True when the tread exits from MsgWaitForMultipleObjects
  WaitForSingleObject(ThreadHandle, INFINITE);
  CloseHandle(ThreadHandle); ThreadHandle := 0;
  CloseHandle(ExitEvent); ExitEvent := 0;
end;

initialization
  InitializeFromMainThread;

  WaitUntilHelperThreadIsReady; // we can call it later, just before we need the window handle
finalization
  FinalizeFromMainThread;
end.
person Maxim Masiutin    schedule 10.05.2017
comment
если я использовал Halt в своей программе, раздел финализации не будет выполнен. это нормально. - person Nasreddine Galfout; 26.11.2017
comment
@NasreddineAbdelillahGalfout не используйте Halt. Редко есть веская причина использовать его, кроме как в экстремальных условиях. - person Remy Lebeau; 26.11.2017
comment
@RemyLebeau спасибо за оба ответа. Я читал документацию о AllocateHWnd() и других альтернативах. появился раздел финализации, и когда я прочитал об этом, я узнал о Halt. Я им не пользуюсь, но это полезно знать. Еще раз спасибо. - person Nasreddine Galfout; 26.11.2017