У меня есть 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()), но мог пропустить что-то тривиальное.
К вашему сведению, поле CMINVOKECOMMANDINFO.fMask имеет флаг CMIC_MASK_NOASYNC, доступный в Vista+: «Реализация IContextMenu::InvokeCommand должна быть синхронной, не возвращается до завершения.».
Другим подходом может быть мониторинг файлов на наличие изменений. См. этот вопрос и ответ: stackoverflow.com/questions/3418562/….
@RemyLebeau, это было одной из первых вещей, которые я попробовал :) использование флага CMIC_MASK_NOASYNC кажется просто предложением к реализации, мое приложение ни разу не дождалось завершения IContextMenu::InvokeCommand. «Реализация IContextMenu::InvokeCommand должна быть синхронной, а не возвращаться до ее завершения. Поскольку это рекомендуется, вызывающие приложения, которые указывают этот флаг не может гарантировать, что этот запрос будет выполнен, если они не знакомы с реализацией команды, которую они вызывают».
@ Брайан, спасибо за ссылку, прямой мониторинг файла (ов) кажется более надежным и чистым решением на первый взгляд, я попробую это





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