3

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.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
yberk
  • 78
  • 1
  • 7
  • 3
    What you are trying to do is not feasible because not all these activities will spawn a new process dedicated to a single task. In fact, your first step (using ShellExecuteEx and then waiting for the returned process to terminate) will not work in all cases. Many applications (like Winword, Notepad++, ...) do not spawn a new process to open a document if they are already running but will reuse the existing process. – NineBerry Feb 13 '19 at 18:30
  • FYI, the `CMINVOKECOMMANDINFO.fMask` field has a `CMIC_MASK_NOASYNC` flag available on Vista+: "*The implementation of `IContextMenu::InvokeCommand` should be synchronous, **not returning before it is complete**.*" – Remy Lebeau Feb 13 '19 at 19:17
  • 3
    Another approach would be to monitor the files for changes, See this question and answer: https://stackoverflow.com/questions/3418562/delphi-notification-when-a-file-gets-updated – Brian Feb 13 '19 at 19:43
  • @RemyLebeau that was one of the first things i tried :) usage of flag `CMIC_MASK_NOASYNC` seems to be merely a suggestion to the implementation, my application did not once wait for `IContextMenu::InvokeCommand` to finish. _"The implementation of IContextMenu::InvokeCommand should be synchronous, not returning before it is complete. Since this is recommended, **calling applications** that specify this flag **cannot guarantee that this request will be honored** if they are not familiar with the implementation of the verb that they are invoking."_ – yberk Feb 14 '19 at 06:52
  • @Brian thanks for the link, monitoring the file(s) directly seems a more reliable and cleaner solution on first look, i will try this – yberk Feb 14 '19 at 07:03

1 Answers1

0

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;
yberk
  • 78
  • 1
  • 7