Sources
Delphi Russian Knowledge Base
DRKB - база знаний по Дельфи в рунете, составленная Виталием Невзоровым

Shell Extensions и как с ними бороться (статья)

07.05.2004
Михаил Продан

Думаю, вы замечали, что некоторые программы добавляют собственные пункты в системное контекстное меню. Например, WinRAR добавляет "Сжать" и "Распаковать в...", ICQ - "Переслать пользователю" и пр. Механизм, с помощью которого ваш код "внедряется" в оболочку Windows, называется Shell Extensions - именно о нем пойдет речь...

Определения

В дальнейшем, говоря "программист среднего уровня", мы будем подразумевать человека, который уже написал свой первый текстовый редактор и знаком с практикой создания собственных компонент.

clip0244

Shell Extensions - набор сервисных функций Windows API, призванный обеспечить расширение базовых функций оболочки Windows Explorer за счет наших надстроек. В числе основных функций Shell Extensions:

Итак...

Для реализации задуманного нам понадобятся интерфейсы IContextMenu и IShellFolder. Указатель на главный интерфейс IshellFolder, соответствующий "Рабочему столу" оболочки, можно получить, используя функцию SHGetDesktopFolder, объявление которой выглядит так:

function SHGetDesktopFolder (var ppshf: IShellFolder): HResult; stdcall; 

Эта функция возвращает нам указатель на интерфейс IShellFolder, который возвращается в переменной ppshf. Далее допустим, что у нас в компоненте имеется поле под названием ShellObject типа String, в котором хранится путь к необходимому объекту (к примеру - C:\Windows\NotePad.exe), и что нам нужно получить его контекстное меню (рис. 2).

clip0245

Для этого сначала используем метод из интерфейса IShellFolder:

function GetUIObjectOf (hwndOwner: HWND; cidl: UINT;
                        var apidl: PItemIDList;
                        const riid: TIID;
                        prgfInOut: Pointer; out ppvOut): HResult; stdcall; 

Параметры:

После использования этого оператора нам понадобится обратиться к функциям WinAPI - для работы с контекстными меню. Это, в первую очередь:

Function CreatePopupMenu: HMENU; stdcall; 
Function TrackPopupMenu (hMenu: HMENU; uFlags: UINT; x, y, nReserved: Integer;
                         hWnd: HWND; prcRect: PRect): BOOL; stdcall; 
Function DestroyMenu (Menu:HMENU):LogBool; stdcall; 

Синтаксис первой и последней функции, я думаю, понятен и без разъяснений. Функция TrackPopupMenu, собственно, и выводит на экран контекстное меню. Параметры этой функции принимают значения:

Возвращаемое значение показывает наличие команды или ее отсутствие. Если True - пользователь выбрал пункт; False - соответственно, не выбрал.

А теперь - самое главное

Ну что ж, сделали мы Menu - остается наполнить его содержимым, соответствующим нашему ShellObject. Для этого узнаем сначала его идентификатор (PItemIDList) - сделаем это при помощи метода ParseDisplayName из интерфейса IshellFolder. Этот метод объявлен следующим образом:

function ParseDisplayName (hwndOwner: HWND;
                           pbcReserved: Pointer; 
                           lpszDisplayName: POLESTR; 
                       out pchEaten: ULONG;
                       out ppidl: PItemIDList; 
                       var dwAttributes: ULONG): HResult; stdcall; 

Расклад такой:

Но здесь следует проявлять осторожность. Как вы помните, нам нужно вывести контекстное меню для C:\Windows\NotePad.exe. Но сделать это прямо нельзя. Поэтому найдем сначала PItemIDList для папки C:\Windows - контейнера нашего NotePad.exe. Пишем:

OleCheck (ShellFolder.ParseDisplayName (Handle,nil,
                                        StringToOleStr (ExtractFileDir (ShellObject)),
                                        FEaten,FItemIDList,FAtt)); 

где:

После удачного завершения надо бы перейти к классу родителя нашего NotePad.exe. Воспользуемся для этого функцией IShellFolder.BindToObject, объявленной следующим образом:

Function BindToObject (pidl: PItemIDList; pbcReserved: Pointer;
                       const riid: TIID; out ppvOut): HResult; stdcall; 

Тут:

И после очередной строчки кода:

OleCheck (ShellFolder.BindToObject (FItemIDList,nil,IID_IShellFolder,ShellFolder0));

мы получим в переменной ShellFolder0 указатель на интерфейс IShellFolder, соответствующий папке C:\Windows. Теперь мы можем узнать PItemIDList нашего NotePad:

OleCheck (ShellFolder0.ParseDisplayName (
                 Handle,nil,
                 StringToOleStr (ExtractFileName (ShellObject)),
                 FEaten,FItemIDList,FAtt)); 

Для чего все это было написано?

Теперь мы без зазрений совести можем приступать к выводу нашего контекстного меню:

OleCheck (ShellFolder0.GetUIObjectOf (Handle,1,FItemIDList,IID_IContextMenu,nil,ICM));
Menu:=CreatePopupMenu;
Try
ICM.QueryContextMenu (Menu,1,$7FFF,CMF_EXPLORE or CMF_CANRENAME);
Command:=TrackPopupMenu (Menu, TPM_LEFTALIGN or TPM_LEFTBUTTON or TPM_RETURNCMD,100,100,0,Handle,nil);
If Command then
Begin
  ICmd:=Longint (Command)-1;
  OleCheck (ICM.GetCommandString (ICmd,GCS_VERBA,nil,CommandStr,SizeOf (CommandStr)));
  CHandled:=False;
  DoCommandEvent (StrPas (CommandStr),CHandled);
  if not CHandled then
  begin
    FillChar (ICI,SizeOf (ICI),#0);
    ICI.cbSize:=SizeOf (ICI);
    ICI.hwnd:=Handle;
    ICI.lpVerb:=MakeIntResource (ICmd);
    ICI.nShow:=SW_SHOWNORMAL;
    OleCheck (ICM.InvokeCommand (ICI));
  end;
End;
Finally
  ICM:=nil;
End;

Что тут написано. Во-первых - вызов интерфейса IcontextMenu, сопряженного с объектом FItemIDList папки ShellFolder0. Во-вторых, создание дескриптора пустого контекстного меню; заполнение контекстного QueryContextMenu; использование команды TrackPopupMenu для вывода контекстного меню в точку (100, 100).

Обработка результата команды TrackPopupMenu:

Недоработки...

...а где их нет? То есть, конечно, этот компонент работает, я его использую, но в нем (пока) отсутствуют некоторые полезные функции. К примеру, если вы заглянете в файл ShlObj.pas, то обнаружите, что там, помимо использованного нами интерфейса IcontextMenu, объявлены также интерфейсы IContextMenu2 и IContextMenu3, которые используются для расширения базовых функций интерфейса (к примеру, IContextMenu2 используется для работы с элементами подменю). Кроме того, небольшая доработка компонента даст возможность включать в него свои собственные пункты меню (сравните рис. 3 и рис. 4).

clip0247

clip0246

Так что не стоит рассматривать эту статью как исчерпывающее руководство по Shell Extensions - она призвана всего лишь пробудить в вас аппетит к дальнейшим исследованиям.

Previous page:
Пример приложения, использующего Shell API
Top:
DRKB
Next page:
Системные папки