понедельник, 3 августа 2009 г.

Как управлять IContextMenu, часть 1 - вступительное слово

Это перевод How to host an IContextMenu, part 1 - Initial foray. Автор: Реймонд Чен.

Большая часть документации описывает, как встроиться в структуру контекстного меню оболочки (shell) и быть context menu provider. Если вы будете читать документацию с другой стороны, то вы также узнаете, как управлять (host) контекстным меню. Это первая часть из запланированных 11 частей. Да, одинадцать частей - прошу прощения у тех парней, кто заходит сюда послушать посты о истории. Я попробую разбавлять серию случайными постами.

IContextMenu используется следующим образом:
  • Создание.
  • IContextMenu.QueryContextMenu. Это инициализирует контекстное меню. Во время этого вызова, контекстное меню решает, какие элементы нужно отображать, основываясь на переданных вами флагах.
  • Показать меню или выбрать его команду иным способом, используя IContextMenu.GetCommandString, IContextMenu2.HandleMenuMsg и IContextMenu3.HandleMenuMsg2 для эмуляции взаимодействия пользователя.
  • IContextMenu.InvokeCommand. Этот вызов выполняет команду.
Подробности этих действий объясняются в Creating Context Menu Handlers с точки зрения реализатора (implementor) IContextMenu.
Оболочка сначала вызывает IContextMenu.QueryContextMenu. Она передаёт дескриптор HMENU, который метод может использовать для добавления элементов в контекстное меню. Если пользователь выбирает одну из команд, вызывается IContextMenu.GetCommandString для получения строки-подсказки, которая будет показана в статусной строке Microsoft Windows Explorer. Если пользователь щёлкает по одному из пунктов меню, оболочка вызывает IContextMenu.InvokeCommand. Тогда обработчик может выполнить действия команды.
Прочитайте это с другой стороны и вы увидите, что вам нужно сделать, чтобы самому использовать (host) IContextMenu:
Управляющий IContextMenu сначала вызывает IContextMenu.QueryContextMenu. Он передаёт дескриптор HMENU, который метод может использовать для добавления элементов в контекстное меню. Если пользователь выбирает одну из команд, вызывается IContextMenu.GetCommandString для получения строки-подсказки, которая будет показана в строке статуса вызывающего. Если пользователь щёлкает по одному из пунктов меню, управляющий IContextMenu вызывает IContextMenu.InvokeCommand. Тогда обработчик может выполнить действия команды.
Изучение последствий этой новой интерпретации документации контекстного меню будет нашим вниманием в течение следующих нескольких недель.

Окей, давайте начнем. Начнем, как всегда, с пустого VCL-приложения. Я буду предполагать, что вы уже знакомы с пространством имён оболочки (shell namespace) и pidl, так что я сфокусируюсь на проблеме контекстного меню.

uses
ActiveX, JwaShlObj;

function SHBindToParent(pidl: PItemIDList; const riid: TIID; out ppv; out ppidlLast: PItemIDList): HResult; stdcall; external 'shell32.dll' name 'SHBindToParent';

function GetUIObjectOfFile(wnd: HWND; const pszPath: WideString; const riid: TGUID; out ppv): HRESULT;
var
pidl: PItemIDList;
sfgao: SFGAOF;
psf: IShellFolder;
pidlChild: PItemIDList;
begin
DWord(ppv) := 0;
Result := SHParseDisplayName(PWideChar(pszPath), nil, pidl, 0, sfgao);
if SUCCEEDED(Result) then
try
Result := SHBindToParent(pidl, IID_IShellFolder, psf, pidlChild);
if SUCCEEDED(Result) then
try
Result := psf.GetUIObjectOf(wnd, 1, pidlChild, riid, nil, ppv);
finally
psf := nil;
end;
finally
CoTaskMemFree(pidl);
end;
end;

Эта простая функция берёт путь и получает UI-объект оболочки (shell) для него. Мы конвертируем путь в PIDL с помощью SHParseDisplayName, затем привязываемся (bind) к родителю PIDL с помощью SHBindToParent, после чего запрашиваем у родителя UI-объект с помощью IShellFolder.GetUIObjectOf. Я предполагаю, что у вас достаточно опыта работы с пространством имён, чтобы этот код не вызывал проблем.

(Вспомогательные функции типа SHParseDisplayName и SHBindToParent не делают ничего, чего бы вы не смогли сделать сами. Они просто экономят вам время на печать кода. Когда вы начнёте использовать пространство имён оболочки постоянно, вы соберёте собственную библиотеку небольших вспомогательных функций, типа этих).

Для нашего первого раза, всё, что мы сделаем - просто вызовем действие (verb) "Play" на файле, когда пользователь щёлкнет правой кнопкой (почему правой? Потому что в следующей версии мы покажем контекстное меню).

procedure TForm1.FormContextPopup(Sender: TObject; MousePos: TPoint; var Handled: Boolean);
const
SCRATCH_QCM_FIRST = 1;
SCRATCH_QCM_LAST = $7FFF;
var
pcm: IContextMenu;
Menu: HMENU;
Info: TCMInvokeCommandInfo;
begin
Handled := True;

if SUCCEEDED(GetUIObjectOfFile(Handle, 'C:\Windows\clock.avi', IID_IContextMenu, pcm)) then
try
Menu := CreatePopupMenu;
if Menu <> 0 then
try
if SUCCEEDED(pcm.QueryContextMenu(Menu, 0, SCRATCH_QCM_FIRST, SCRATCH_QCM_LAST, CMF_NORMAL)) then
begin
FillChar(Info, SizeOf(Info), 0);
Info.cbSize := SizeOf(info);
Info.hwnd := Handle;
Info.lpVerb := 'play';
SetLastError(pcm.InvokeCommand(info));
if GetLastError <> 0 then
RaiseLastOSError;
end;
finally
DestroyMenu(Menu);
end;
finally
pcm := nil;
end;
end;

Как указано в списке выше, сначала мы создаём IContextMenu, затем инициализируем его вызовом IContextMenu.QueryContextMenu. Заметьте, что даже хотя мы не собираемся показывать контекстное меню, нам всё ещё нужно создать popup-меню, потому что этого требует IContextMenu.QueryContextMenu. Однако, мы вообще не будем показывать получившееся меню; вместо того, чтобы просить пользователя сделать выбор из меню, мы сами делаем его за пользователя, выбирая действие "Play", заполняя запись TCMInvokeCommandInfo и вызывая его.

Но как мы узнаем, что действие (verb) будет именно "Play"? В нашем случае мы это знаем потому что мы жёстко зашили в код путь к файлу "clock.avi" и знаем, что AVI файлы имеют действие "Play". Но, конечно же, это не сработает в общем случае (прим. пер.: если вы запускаете пример, то убедитесь, что у вас есть этот файл или введите туда любое другое имя и посмотрите, какие действия есть для этого типа файлов. Кроме того, даже для AVI-файлов действия могут быть переназначены, если какой-то проигрыватель менял ассоциации). Прежде чем вызывать действие по-умолчанию (default verb), давайте сначала сделаем щаг попроще и попросим пользователя выбрать действие для вызова. Вообще-то это упражнение отвлечёт от нашего пути, но в конце-концов мы вернёмся к выбору действия по-умолчанию.

Если код выше - это всё, что вам было нужно (вызвать фиксированное действие для файла), то тогда вам не нужно проходить через остальные действия для контекстного меню. Код выше эквивалентен вызову функции ShellExecuteEx, передавая ей флаг SEE_MASK_INVOKEIDLIST для указания того, что вы хотите, чтобы вызов (invoke) шёл через IContextMenu.

6 комментариев:

  1. "Прежде чем вызвать умалчиваемое действие..."
    звучит как-то не по-русски... обыяно это назвается "действие по умолчанию"

    ОтветитьУдалить
  2. Окей, поправил.
    Спасибо за замечание.

    ОтветитьУдалить
  3. Спасибо огромное, сильно помогло

    ОтветитьУдалить
  4. Анонимный16 мая 2010 г., 19:37

    а как насчет перевести все доп. функции типа GetUIObjectOfFile ?

    ОтветитьУдалить
  5. Не понял вопроса. В тексте поста есть исходный код GetUIObjectOfFile.

    ОтветитьУдалить
  6. А как же вызвать QueryContextMenu в результате drag-drop операции (когда перетаскивание было правой кнопкой мыши, например, на ярлык rar-файла в listview в моем окне)? Что-то прочтение с другой стороны никак не помогает понять, чем этот случай отличается от вызова контекстного меню одиночным правым кликом. Надо же как-то передать контекстному меню и shell-объект, соответствующий rar-архиву, и список бросаемых на него файлов. Закрадывается мысль, что достаточно унаследовать свой класс от IContextMenu и IDropTarget одновременно. Но что-то нигде ничего подобного не нашлось. Не там искал?

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.