Дождитесь запуска процесса IContextMenu.InvokeCommand

У меня есть TListView, элементы которого представляют собой файлы, которые пользователь может открыть, дважды щелкнув по ним.

Для этого я сохраняю файл во временной папке Windows, запускаю поток, который открывает сохраненный файл с помощью ShellExecuteEx() и ждет ShellExecuteInfo.hProcess, например:

TNotifyThread = class(TThread)
private
  FFileName: string;
  FFileAge: TDateTime;
public
  constructor Create(const FileName: string; OnClosed: TNotifyEvent); overload;
  procedure Execute; override;

  property FileName: String read FFileName;
  property FileAge: TDateTime read FFileAge;
end;

{...}

constructor TNotifyThread.Create(const FileName: string; OnClosed: TNotifyEvent);
begin
  inherited Create(True);
  if FileExists(FileName) then
    FileAge(FileName, FFileAge);

  FreeOnTerminate := True;
  OnTerminate := OnClosed;
  FFileName := FileName;

  Resume;
end;

procedure TNotifyThread.Execute;
var
  se: SHELLEXECUTEINFO;
  ok: boolean;
begin
  with se do
  begin
    cbSize := SizeOf(SHELLEXECUTEINFO);
    fMask := SEE_MASK_INVOKEIDLIST or SEE_MASK_NOCLOSEPROCESS or SEE_MASK_NOASYNC;
    lpVerb := PChar('open');
    lpFile := PChar(FFileName);
    lpParameters := nil;
    lpDirectory := PChar(ExtractFilePath(ParamStr(0)));
    nShow := SW_SHOW;
  end;

  if ShellExecuteEx(@se) then
  begin
    WaitForSingleObject(se.hProcess, INFINITE);
    if se.hProcess <> 0 then
      CloseHandle(se.hProcess);
  end;
end;

Таким образом, я могу использовать событие TThread.OnTerminate для записи любых изменений, внесенных в файл после того, как пользователь его закроет.

Теперь я показываю контекстное меню Windows с помощью JclShell.DisplayContextMenu() (который использует IContextMenu).

МОЯ ЦЕЛЬ: дождаться завершения выполненного действия (например, «свойства», «удалить», ..), выбранного в контекстном меню (или получить уведомление любым способом), чтобы я мог проверить временный файл на наличие изменений написать их обратно или удалить TListItem в случае удаления.

Поскольку CMINVOKECOMMANDINFO не возвращает дескриптор процесса, как это делает SHELLEXECUTEINFO, я не могу сделать это таким же образом.

Назначение MakeIntResource(commandId-1) на SHELLEXECUTEINFO.lpVerb привело к сбою вызова ShellExecuteEx() с EAccessViolation. Этот метод не поддерживается для SHELLEXECUTEINFO.

Я попытался получить командную строку с IContextMenu.GetCommandString() и идентификатором команды из TrackPopupMenu(), чтобы позже передать его SHELLEXECUTEINFO.lpVerb, но GetCommandString() не возвращал команды для некоторых нажатых элементов.

рабочие пункты меню:

properties, edit, copy, cut, print, 7z: add to archive (verb is 'SevenZipCompress', wont return processHandle), KapserskyScan (verb is 'KL_scan', wont return processHandle)

не работает:

anything within "open with" or "send to"

Это просто вина реализации IContextMenu?

Может быть, это как-то связано с тем, что я использую AnsiStrings? Однако я не мог заставить GCS_VERBW работать. Есть ли лучшие способы надежно получить CommandString, чем этот?

function CustomDisplayContextMenuPidlWithoutExecute(const Handle: THandle; 
const Folder: IShellFolder;
  Item: PItemIdList; Pos: TPoint): String;
var
  ContextMenu: IContextMenu;
  ContextMenu2: IContextMenu2;
  Menu: HMENU;
  CallbackWindow: THandle;
  LResult: AnsiString;
  Cmd: Cardinal;
begin
  Result := '';
  if (Item = nil) or (Folder = nil) then
    Exit;
  Folder.GetUIObjectOf(Handle, 1, Item, IID_IContextMenu, nil,
    Pointer(ContextMenu));
  if ContextMenu <> nil then
  begin
    Menu := CreatePopupMenu;
    if Menu <> 0 then
    begin
      if Succeeded(ContextMenu.QueryContextMenu(Menu, 0, 1, $7FFF, CMF_EXPLORE)) then
      begin
        CallbackWindow := 0;
        if Succeeded(ContextMenu.QueryInterface(IContextMenu2, ContextMenu2)) then
        begin
          CallbackWindow := CreateMenuCallbackWnd(ContextMenu2);
        end;
        ClientToScreen(Handle, Pos);
        cmd := Cardinal(TrackPopupMenu(Menu, TPM_LEFTALIGN or TPM_LEFTBUTTON or
          TPM_RIGHTBUTTON or TPM_RETURNCMD, Pos.X, Pos.Y, 0, CallbackWindow, nil));
        if Cmd <> 0 then
        begin
          SetLength(LResult, MAX_PATH);
          cmd := ContextMenu.GetCommandString(Cmd-1, GCS_VERBA, nil, LPSTR(LResult), MAX_PATH);
          Result := String(LResult);
        end;
        if CallbackWindow <> 0 then
          DestroyWindow(CallbackWindow);
      end;
      DestroyMenu(Menu);
    end;
  end;
end;

Я читал блог Рэймонда Чена на Как разместить IContextMenu, а также исследовал в MSDN (например, CMINVOKECOMMANDINFO, GetCommandString(), SHELLEXECUTEINFO и TrackPopupMenu()), но мог пропустить что-то тривиальное.

То, что вы пытаетесь сделать, невозможно, потому что не все эти действия порождают новый процесс, посвященный одной задаче. Фактически, ваш первый шаг (использование ShellExecuteEx и последующее ожидание завершения возвращенного процесса) не будет работать во всех случаях. Многие приложения (например, Winword, Notepad++, ...) не создают новый процесс для открытия документа, если они уже запущены, а повторно используют существующий процесс.

NineBerry 13.02.2019 19:30

К вашему сведению, поле CMINVOKECOMMANDINFO.fMask имеет флаг CMIC_MASK_NOASYNC, доступный в Vista+: «Реализация IContextMenu::InvokeCommand должна быть синхронной, не возвращается до завершения.».

Remy Lebeau 13.02.2019 20:17

Другим подходом может быть мониторинг файлов на наличие изменений. См. этот вопрос и ответ: stackoverflow.com/questions/3418562/….

Brian 13.02.2019 20:43

@RemyLebeau, это было одной из первых вещей, которые я попробовал :) использование флага CMIC_MASK_NOASYNC кажется просто предложением к реализации, мое приложение ни разу не дождалось завершения IContextMenu::InvokeCommand. «Реализация IContextMenu::InvokeCommand должна быть синхронной, а не возвращаться до ее завершения. Поскольку это рекомендуется, вызывающие приложения, которые указывают этот флаг не может гарантировать, что этот запрос будет выполнен, если они не знакомы с реализацией команды, которую они вызывают».

yberk 14.02.2019 07:52

@ Брайан, спасибо за ссылку, прямой мониторинг файла (ов) кажется более надежным и чистым решением на первый взгляд, я попробую это

yberk 14.02.2019 08:03
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
5
212
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В итоге я использовал TJvChangeNotify для мониторинга временной папки Windows, сохраняя отслеживаемые файлы в папке TDictionary<FileName:String, LastWrite: TDateTime>.

Поэтому всякий раз, когда TJvChangeNotify запускает событие OnChangeNotify, я могу проверить, какие из моих отслеживаемых файлов были удалены (путем проверки существования) или изменены (путем сравнения времени последней записи).

Пример ChangeNotifyEvent:

procedure TFileChangeMonitor.ChangeNotifyEvent(Sender: TObject; Dir: string;
  Actions: TJvChangeActions);
var
  LFile: TPair<String, TDateTime>;
  LSearchRec: TSearchRec;
  LFoundErrorCode: Integer;
begin
  for LFile in FMonitoredFiles do
  begin
    LFoundErrorCode := FindFirst(LFile.Key, faAnyFile, LSearchRec);
    try
      if LFoundErrorCode = NOERROR then
      begin
        if LSearchRec.TimeStamp > LFile.Value then
        begin
          // do something with the changed file
          {...}

          // update last write time
          FMonitoredFiles.AddOrSetValue(LFile.Key, LSearchRec.TimeStamp);
        end;
      end // 
      else if (LFoundErrorCode = ERROR_FILE_NOT_FOUND) then
      begin
        // do something with the deleted file
        {...}

        // stop monitoring the deleted file
        FMonitoredFiles.Remove(LFile.Key);
      end;
    finally
      System.SysUtils.FindClose(LSearchRec);
    end;
  end;
end;

Другие вопросы по теме