Получить сообщение WM_LBUTTONDOWN, пока ShellExecAndWait?

В Delphi IDE создайте файл VCL Forms Application. Затем добавьте в форму компонент TApplicationEvents и компонент TButton. Затем добавьте эти два обработчика событий:

uses
  JclShell;

procedure TForm3.ApplicationEvents1Message(var Msg: tagMSG; var Handled: Boolean);
begin
  if Msg.Message = WM_LBUTTONDOWN then
  begin
    Self.Caption := 'WM_LBUTTONDOWN';
  end
  else if Msg.Message = WM_LBUTTONUP then
  begin
    Self.Caption := 'WM_LBUTTONUP';
  end
end;

procedure TForm3.Button1Click(Sender: TObject);
begin
  JclShell.ShellExecAndWait('C:\Windows\system32\notepad.exe');
  Self.Caption := 'Notepad closed';
end;

Теперь нажмите на кнопку. Происходит следующее:

  1. "WM_LBUTTONDOWN" появляется в строке заголовка формы.

  2. "WM_LBUTTONUP" появляется в строке заголовка формы.

  3. Блокнот выполнен.

Затем снова нажмите кнопку, которая запускает другой экземпляр Блокнота, НО на этот раз ничего не записывая в строке заголовка формы.

Очевидно, программа застряла в JclShell.ShellExecAndWait, которая возвращается только при закрытии Блокнота. Поэтому, когда Блокнот закрывается, любой щелчок мыши снова записывается в строку заголовка формы.

Итак, мы видим, что пока Блокнот работает в JclShell.ShellExecAndWait, все в программе работает нормально: Вы даже можете производить математические вычисления, пока Блокнот работает в JclShell.ShellExecAndWait. Только ApplicationEvents1Message не срабатывает во время работы Блокнота.

Итак, как я могу получить сообщение WM_LBUTTONDOWN, когда Блокнот работает в JclShell.ShellExecAndWait?


person user1580348    schedule 16.11.2017    source источник
comment
Вам нужно знать, когда блокнот закрылся?   -  person Nat    schedule 17.11.2017
comment
Конечно, мне нужно знать.   -  person user1580348    schedule 17.11.2017


Ответы (1)


Пока JclShell.ShellExecAndWait ожидает выхода порожденного процесса, он использует следующий основной насос сообщений:

while WaitForSingleObject(Sei.hProcess, 10) = WAIT_TIMEOUT do
  repeat
    Msg.hwnd := 0;
    Res := PeekMessage(Msg, Sei.Wnd, 0, 0, PM_REMOVE);
    if Res then
    begin
      TranslateMessage(Msg);
      DispatchMessage(Msg);
    end;
  until not Res;
CloseHandle(Sei.hProcess);

Я говорю «базовый», потому что зацикленная часть потока сообщений VCL выглядит так:

 if PeekMessage(Msg, 0, 0, 0, PM_NOREMOVE) then
  begin
    Unicode := (Msg.hwnd = 0) or IsWindowUnicode(Msg.hwnd);
    if Unicode then
      MsgExists := PeekMessageW(Msg, 0, 0, 0, PM_REMOVE)
    else
      MsgExists := PeekMessageA(Msg, 0, 0, 0, PM_REMOVE);

    if MsgExists then
    begin
      Result := True;
      if Msg.Message <> WM_QUIT then
      begin
        Handled := False;
        if Assigned(FOnMessage) then FOnMessage(Msg, Handled);
        if not IsPreProcessMessage(Msg) and not IsHintMsg(Msg) and
          not Handled and not IsMDIMsg(Msg) and
          not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then
        begin
          TranslateMessage(Msg);
          if Unicode then
            DispatchMessageW(Msg)
          else
            DispatchMessageA(Msg);
        end;
      end
      else
      begin
  {$IF DEFINED(CLR)}
        if Assigned(FOnShutDown) then FOnShutDown(self);
        DoneApplication;
  {$IFEND}
        FTerminate := True;
      end;
    end;

Строка кода

if Assigned(FOnMessage) then FOnMessage(Msg, Handled);

это то, что передаст сообщение вашему TApplicationEvents.OnMessage через TMultiCaster, который TApplicationEvents назначил Application.OnMessage. Это не делается в исходном коде JCL. Дополнительные проблемы могут быть связаны с тем, что на данный момент JCL не поддерживает UniCode в отношении сообщений и не обрабатывает WM_QUIT.

Что с этим делать, тоже зависит от того, чего вы хотите достичь. Почему вы хотите получать эти сообщения в первую очередь?

Я имею в виду, что можно изменить исходный код JCL - если вы хотите это сделать - и добавить VCL.Forms к использованию, а затем вызвать обработчик события, если он назначен, как это делает VCL:

while WaitForSingleObject(Sei.hProcess, 10) = WAIT_TIMEOUT do
  repeat
    Msg.hwnd := 0;
    Res := PeekMessage(Msg, Sei.Wnd, 0, 0, PM_REMOVE);
    if Res then
    begin
      Handled := False;
      if Assigned(Application.OnMessage) then
        Application.OnMessage(Msg, Handled);
      if not Handled then
      begin
        TranslateMessage(Msg);
        DispatchMessage(Msg);
      end;
    end;
  until not Res;
CloseHandle(Sei.hProcess);

Или даже вызовите Application.ProcessMessages, чтобы иметь ту же обработку сообщений, что и VCL:

while WaitForSingleObject(Sei.hProcess, 10) = WAIT_TIMEOUT do
  Application.ProcessMessages;
CloseHandle(Sei.hProcess);

Это работает, я не видел никаких побочных эффектов, как предполагали комментаторы. Но прежде чем изменить исходный код JCL таким образом, я, вероятно, реализовал бы свой собственный ShellExecAndWait. В зависимости от того, чего вы хотите добиться, обычная отправка сообщений все равно должна работать. Итак, если ваш TFrom имеет, например.

procedure WMLButtonDown(var Message: TWMLButtonDown); message WM_LBUTTONDOWN;

реализовано, его следует вызвать. Это, однако, только в том случае, если сообщение было направлено на вашу форму. Если кнопка нажата, результирующие сообщения будут направлены на саму кнопку. Тогда вам нужно будет реализовать свой собственный класс-потомок.

Может быть, совсем другое предложение?

Если ваша цель состоит в том, чтобы определенные элементы управления не нажимались, пока JclShell.ShellExecAndWait ожидает, и использование обработки сообщений является вашим подходом к тому, чтобы сделать это возможным, вы можете попробовать другие вещи.

Почему бы не отключить их? Это также даст пользователю видимую индикацию того, что кликать нельзя.

  Button1.Enabled := False;
  try
    JclShell.ShellExecAndWait('C:\Windows\system32\notepad.exe');
    Self.Caption := 'Notepad closed';
  finally
    Button1.Enabled := True;
  end;

Если вас беспокоит наличие нескольких элементов управления, создайте TAction, назначьте его каждому элементу управления, который хотите отключить, и отключите их все вместе со свойством Enabled элемента TAction.

Или просто скрыть форму, пока открыт notepad.exe?

  Hide;
  try
    JclShell.ShellExecAndWait('C:\Windows\system32\notepad.exe');
    Self.Caption := 'Notepad closed';
  finally
    Show;
  end;

Или вы можете удалить обработчики событий:

var
  ATmpOnClick: TNotifyEvent;
begin
  ATmpOnClick := Button1.OnClick;
  Button1.OnClick := nil;
  try
    JclShell.ShellExecAndWait('C:\Windows\system32\notepad.exe');
    Self.Caption := 'Notepad closed';
  finally
    Button1.OnClick := ATmpOnClick;
  end;
end;
person nil    schedule 16.11.2017
comment
Обратите внимание, что это, вероятно, по-прежнему не будет работать, потому что цикл ShellExecAndWait() фильтрует сообщения по HWND, поэтому PeekMessage() не будет доставлять сообщения для других HWND, таких как кнопка. Лучшее решение — переместить вызов ShellExecuteAndWait() в отдельный рабочий поток, который уведомляет основной поток пользовательского интерфейса, когда блокнот закрывается. - person Remy Lebeau; 16.11.2017
comment
@nil, кажется, ваше решение по замене цикла сообщений в JclShell (я создал заменяющее устройство) работает. Однако не забудьте объявить переменную Handled: Boolean; в реализации ShellExecAndWait. Мне еще нужно сделать несколько тестов, тогда я приму ваш ответ. - person user1580348; 16.11.2017
comment
@user1580348 user1580348 Реми может быть здесь. Я не помню, чтобы Wnd устанавливался в ShellExecuteInfo, нужно изучить это, когда снова будет доступен код. - person nil; 16.11.2017
comment
Мои тесты привели к странным побочным эффектам: После ShellExecAndWait перестала работать нормальная обработка сообщений в программе. - person user1580348; 16.11.2017
comment
@nil Да, procedure WMLButtonDown(var Message: TWMLButtonDown); message WM_LBUTTONDOWN;, кажется, работает после JclShell.ShellExecAndWait. Но как я могу установить Handled := True в этой процедуре? - person user1580348; 16.11.2017
comment
@user1580348 user1580348 Отнеситесь к этому с недоверием: не вызывайте inherited, если вы не хотите, чтобы это сообщение обрабатывалось. Мне нужно будет изучить это подробнее завтра, может быть, я был уверен в некоторых вещах... Я, например, не могу понять, как мой измененный код JCL должен работать во время ожидания - особенно с учетом того, что заметил Реми, - но иметь побочные эффекты после. - person nil; 17.11.2017
comment
Если вы собираетесь добавлять формы в источник JCL, я думаю, вы можете напрямую вызвать Application.ProcessMessage, не удаляя сообщение, и вся обработка приложения будет выполнена. - person Sertac Akyuz; 17.11.2017
comment
@RemyLebeau, JCL не устанавливает поле Wnd для TShellExecuteInfo, поэтому я предполагаю, что PeekMessage эффективно вызывается с 0 Wnd, как в цикле сообщений VCL. - person nil; 17.11.2017
comment
@nil: JCL is not setting the Wnd field - тогда он вообще не должен передавать Wnd в PeekMessage(). Это ошибка, ожидающая появления JCL, когда-либо решившая в будущем обновить ShellExecAndWait(), чтобы заполнить Wnd для обработки пользовательского интерфейса. - person Remy Lebeau; 17.11.2017
comment
@ user1580348: является ли функция JclShell.ShellExecAndWait потокобезопасной? - да. Он не делает ничего, привязанного к конкретному потоку (обратите внимание, что поток, вызывающий ShellExecuteEx(), должен сначала вызвать CoInitialize/Ex()). При этом вызов ShellExecAndWait() в потоке означает, что не будет никаких сообщений пользовательского интерфейса для обработки (если только у потока нет собственного пользовательского интерфейса), поэтому код будет просто выполнять пустой цикл WaitForSingleProcess(), занимая процессорное время. В этой ситуации лучше использовать один вызов WaitForSingleProcess() с тайм-аутом INFINITE. - person Remy Lebeau; 17.11.2017
comment
Я думаю, что нашел ошибку: i.imgur.com/aljFild.png Теперь это работает отлично! - person user1580348; 17.11.2017
comment
@user1580348: Если вы собираетесь вызывать Application.ProcessMessages(), то удалите весь цикл PeekMessage(), так как ProcessMessages() вызывает PeekMessage() вместо вас (Res всегда будет False), например: while WaitForSingleObject(Sei.hProcess, 10) = WAIT_TIMEOUT do Application.ProcessMessages; Это все, что вам нужно. - person Remy Lebeau; 17.11.2017
comment
@ user1580348: Однако вместо этого вы должны использовать MsgWaitForMultipleObjects(). Вызовите ProcessMessages() только тогда, когда действительно есть сообщения, ожидающие обработки, например: var Res: DWORD; ... repeat Res := MsgWaitForMultipleObjects(1, Sei.hProcess, False, INFINITE, QS_ALLINPUT); if Res = WAIT_OBJECT_0+1 Then Application.ProcessMessages; until Res = WAIT_OBJECT_0; - person Remy Lebeau; 17.11.2017
comment
Не могли бы вы написать это в Pastebin, пожалуйста? Спасибо! - person user1580348; 17.11.2017
comment
Нужно ли вообще ждать? Здесь много работы, чтобы решить проблему ожидания дескриптора, когда мы не знаем, почему ждем notepad.exe. - person Nat; 17.11.2017