问题
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 AnsiString
s? 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.
回答1:
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;
来源:https://stackoverflow.com/questions/54674447/wait-for-process-started-by-icontextmenu-invokecommand