【发布时间】:2019-02-13 16:00:46
【问题描述】:
我有一个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 事件写回对文件所做的任何更改。
我现在在 JclShell.DisplayContextMenu()(使用 IContextMenu)的帮助下显示 Windows 上下文菜单。
我的目标:等待在上下文菜单中选择的执行操作(例如“属性”、“删除”、..)完成(或以任何方式获得通知),这样我就可以检查临时文件的更改以将其写回,或删除TListItem以防删除。
由于CMINVOKECOMMANDINFO 不像SHELLEXECUTEINFO 那样返回进程句柄,所以我无法以同样的方式进行。
将MakeIntResource(commandId-1) 分配给SHELLEXECUTEINFO.lpVerb 导致对ShellExecuteEx() 的调用以EAccessViolation 崩溃。 SHELLEXECUTEINFO 似乎不支持此方法。
我曾尝试从TrackPopupMenu() 获取带有IContextMenu.GetCommandString() 的命令字符串和命令ID,以便稍后将其传递给SHELLEXECUTEINFO.lpVerb,但GetCommandString() 不会为单击的某些项目返回命令。
工作菜单项:
属性、编辑、复制、剪切、打印、7z:添加到存档(动词是“SevenZipCompress”,不会返回 processHandle),KapserskyScan(动词是“KL_scan”,不会返回 processHandle)
不工作:
“打开方式”或“发送至”中的任何内容
这仅仅是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;
我在 How to host an IContextMenu 上阅读了 Raymond Chen 的博客,并在 MSDN 上进行了研究(例如 CMINVOKECOMMANDINFO、GetCommandString()、SHELLEXECUTEINFO 和 TrackPopupMenu()),但我可能错过了一些琐碎的事情。
【问题讨论】:
-
您尝试做的事情是不可行的,因为并非所有这些活动都会产生一个专用于单个任务的新流程。事实上,您的第一步(使用 ShellExecuteEx 然后等待返回的进程终止)并非在所有情况下都有效。许多应用程序(如 Winword、Notepad++、...)如果它们已经在运行,则不会生成新进程来打开文档,而是会重用现有进程。
-
仅供参考,
CMINVOKECOMMANDINFO.fMask字段在 Vista+ 上有一个CMIC_MASK_NOASYNC标志:“IContextMenu::InvokeCommand的实现应该是同步的,在完成之前不会返回." -
另一种方法是监视文件的更改,请参阅此问题和答案:stackoverflow.com/questions/3418562/…
-
@RemyLebeau 这是我尝试的第一件事:) 使用标志
CMIC_MASK_NOASYNC似乎只是对实施的建议,我的应用程序没有一次等待IContextMenu::InvokeCommand完成。 "IContextMenu::InvokeCommand 的实现应该是同步的,在完成之前不返回。既然建议这样做,调用指定此标志的应用程序 不能保证此请求会被如果他们不熟悉他们正在调用的动词的实现,则很荣幸。” -
@Brian 感谢链接,直接监控文件似乎是一个更可靠和更清洁的解决方案,我会试试这个
标签: windows delphi winapi delphi-10.2-tokyo