Search code examples
windowsdelphiwinapidelphi-10.2-tokyo

Wait for process started by IContextMenu.InvokeCommand


I have a TListView whose items are files, which the user can open via double clicking on them.

To do this, I save the file in the windows temp folder, start a thread that opens the saved file with ShellExecuteEx(), and let it wait for ShellExecuteInfo.hProcess, like this:

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;

This way, I can use the TThread.OnTerminate event to write back any changes made to the file after the user closes it.

I now show the windows context menu with the help of JclShell.DisplayContextMenu() (which uses IContextMenu).

MY GOAL: To wait for the performed action (e.g. 'properties' , 'delete', ..) chosen in the context menu to finish (or get notified in any kind of fashion), so that I can check the temporary file for changes to write those back, or remove the TListItem in case of deletion.

Since CMINVOKECOMMANDINFO does not return a process handle like SHELLEXECUTEINFO does, I am unable to do it in the same way.

Assigning MakeIntResource(commandId-1) to SHELLEXECUTEINFO.lpVerb made the call to ShellExecuteEx() crash with an EAccessViolation. This method seems unsupported for SHELLEXECUTEINFO.

I have tried to get the command string with IContextMenu.GetCommandString() and the command ID from TrackPopupMenu() to later pass it to SHELLEXECUTEINFO.lpVerb, but GetCommandString() wouldn't return commands for some items clicked.

working menu items:

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

not working:

anything within "open with" or "send to"

Is this simply the fault of the IContextMenu implementation?

Maybe it has something to do with my use of AnsiStrings? I couldn't get GCS_VERBW to work, though. Are there better ways to reliably get the CommandString than this?

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;

I have read Raymond Chen's blog on How to host an IContextMenu, as well as researched on MSDN (for example CMINVOKECOMMANDINFO, GetCommandString(), SHELLEXECUTEINFO and TrackPopupMenu()), but I might have missed something trivial.


Solution

  • I ended up using TJvChangeNotify to monitor the windows temp folder, while keeping the monitored-files in a TDictionary<FileName:String, LastWrite: TDateTime>.

    So whenever TJvChangeNotify fires the OnChangeNotify event, i can check which of my monitored-files have been deleted (by checking existence) or have changed (by comparing the last write time).

    Example 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;