Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Программирование для начинающих и не только


Информационный Канал Subscribe.Ru


Shell Extensions и как с ними бороться, или история написания одного Delphi компонента.

Эта статья посвящена использованию Shell Extensions из Delphi и предназначена для программистов среднего уровня, желающих задействовать Delphi для внедрения в оболочку Windows Explorer.

Определения

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

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

  • работа с системными контекстными меню;
  • работа с папками и объектами из пространства имён оболочки Windows. (Мои Документы, Принтеры, Панель управления...);
  • использование механизма Drag&Drop;
  • cоздание и использование ярлыков.

С чего начать?

В качестве первого примера создадим контекстное меню (это чего открывается правой кнопкой мыши) для заданного элемента системы. Создаём новый компонент Delphi (File -> New -> Other -> New -> Component).

В появившемся диалоговом окне заполняем поля:

  • Ancestor Type:
    TComponent;
  • Class Name:
    TShellPopupMenu (или по вкусу);
  • Palette Page и остальные пункты:
    кто хочет, пусть меняет, но тут нас вполне устроит предлагаемый вариант. Жмем Ok.
После этих манипуляций в окне редактора появляется шаблон нашего компонента:

type
  TShellPopupMenu = class(TComponet)
  private
    { Private declarations }
  protected
    { Protected declarations }
  public
    { Public declarations }
  published
    { Published declarations }
  end;

Чем это наполнить?

Для реализации задуманного нам понадобилятся интерфейсы IContextMenu и IShellFolder. Первый можно получить из второго ну как бы не совсем из второго... путем вызова функции IShellFolder.GetUIObjectOf(). Указатель на главный интерфейс IShellFolder соответствующий "Рабочему столу" оболочки можно получить, используя функцию SHGetDesktopFolder, объявление которой звучит скорее выглядит следующим образом:
function SHGetDesktopFolder(var ppshf: IShellFolder): HResult; stdcall;

Вот эта функция возвращает нам указатель на интерфейс IShellFolder, который возвращается в переменной ppshf. Результат этой функции - значение типа HResult информирует нас о результате выполнения функции и, учитывая "исключительные" наклонности Delphi, мы сразу же передаем этот результат как параметр процедуры OleCheck() из модуля ComObj.pas. Далее, допустим, что у нас в компоненте имеется поле под названием ShellObject типа String, в котором хранится путь к необходимому объекту, к примеру "C:\Windows\NotePad.exe", и нам нужно получить его контекстное меню (рис.2). Для этого используем метод:
function GetUIObjectOf(hwndOwner: HWND; cidl: UINT; var apidl: PItemIDList;const riid: TIID; prgfInOut: Pointer; out ppvOut): HResult; stdcall;
из интерфейса IShellFolder. Параметры этой функции соответственно представляют собой:

  • hwndOwner
    дескриптор родительского окна, которому посылаются сообщения при возникновении ошибок (если вы уверены, что ошибок быть не может, то спокойно можете ставить значение 0, иначе воспользуйтесь свойством компонента Handle);
  • cidl
    количество элементов на которое указывает значение apidl (на первых порах должен быть 1).
  • apidl
    параметр, который представляет собой уникальный идентификатор объекта. Его тип PItemIDList определён в том же модуле ShlObj.pas, тут вы можете смело использовать операторы присваивания, не боясь никаких New() и Dispose() (GetMem и FreeMem); их следует использовать лишь в случае крайней необходимости. Подробнее об использовании PItemIDList смотрим ниже.
  • riid
    глобальный уникальный идентификатор системы Windows. Унифицирует всех и вся: контекстные меню, интерфейсы, библиотеки типов, сопряженные классы...(смотрим RegEdit значения ключей CLSID). В даном примере должен ровняться константе IID_IContextMenu из файла ShlObj.pas, что означает то, что мы делаем "заказ" на интерфейс IContextMenu.
  • prgfInOut
    зарезервировано. Должно быть nil;
  • ppvOut
    переменная которая получит указатель на "заказанный" интерфейс в случае положительного результата выполнения данного метода, иначе будет ровняться nil.
  • Ну и естественно возвращаемое функцией значение, типа HResult, которое, как и в предыдущем этапе, передаем в OleCheck.
После использования этого оператора нам понадобится обратиться к функциям 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;
Синтаксис первой и последней функции, я думаю, понятен и без разъяснений. Код выглядить примерно таким образом:

Var Menu:HMenu;
 ...
begin
 ... 
Menu:=CreatePopupMenu;
 ... 
DestroyMenu(Menu);
 ...
end;

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

  • hMenu
    дескриптор контекстного меню. это тот самый Menu, что мы сделали ему CreatePopupMenu
  • uFlags
    выравнивание относительно координат. Возможные значения: TPM_CENTERALIGN, TPM_LEFTALIGN, TPM_RIGHTALIGN (использовать не все сразу а по одному!), TPM_LEFTBUTTON, TPM_RIGHTBUTTON (применяются в паре с предыдущими флагами для того чтобы определить на какую кнопку мышки реагировать), TPM_RETURNCMD используется для возврата команды как будет показано ниже.
  • x, y
    координаты, по которым будем "впрыгивать" наше меню.
  • nReserved
    соответственно приравниваем к 0.
  • hWnd
    как и в большинстве приложений - дескриптор родительского окна (может ровняться тому же Handl'у что и hwndOwner из GetUIObjectOf, или 0).
  • prcRect
    указатель на структуру TRect, которая задает "окно" в экранных координатах в пределах которого пользователь может щелкать без каких либо исчезновений контекстного меню, если = nil, то при нажатии мышкой за пределами контекстного меню оное исчезнет.
  • Возвращаемое значение показывает наличие команды или её отсутствие. Если True - пользователь выбрал пункт; False - соответственно не выбрал пункт.

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

Ну что ж, сделали мы Menu, остается наполнить его содержимым соответствующим нашему ShellObject, но для этого сначала нам понадобится узнать его идентификатор (PItemIDList). Сделать это можно при помощи метода из все того же интерфейса IShellFolder под названием ParseDisplayName который объявлен следующим образом:
function ParseDisplayName(hwndOwner: HWND;pbcReserved: Pointer; lpszDisplayName: POLESTR; out pchEaten: ULONG;out ppidl: PItemIDList; var dwAttributes: ULONG): HResult; stdcall;

    Расклад такой:
  • hwndOwner
    как и в предыдущих методах должен быть Handle или 0;
  • pbcReserved
    на то он и Reserved чтоб был nil;
  • lpszDisplayName
    имя объекта для которого надо найти PItemIDList;
  • pchEaten
    переменная, которая используется для возврата значения количества символов которые были правильно разобраны;
  • ppidl
    как раз то что нам надо. Теперь надо бы сохранить его в каком то поле (например FItemIDList).
  • dwAttributes
    атрибуты для только что найденного FItemIDList;
  • Значение функции - в OleCheck().

Но здесь надо быть осторожным. Как вы помните нам надо вывести контекстное меню для C:\Windows\NotePad.exe. Но прямо этого сделать нельзя (ну в принципе можно, но это будет не совсем та информация, точнее, совсем не та информация, которую мы ищем). Для этого мы сначала найдём PItemIDList для папки C:\Windows папки (в смысле родительского объекта) файла NotePad.exe. для этого пишем:
OleCheck(ShellFolder.ParseDisplayName(Handle,nil,StringToOleStr(ExtractFileDir(ShellObject)), FEaten,FItemIDList,FAtt));

    Где:
  • ShellFolder
    значение которое мы получили из SHGetDesktopFolder.
  • Handle
    о нем много уже было сказано. Это может быть или 0, или Self.Handle, если вы просто хотите посмотреть что из этого получиться и пишете это дело на простой форме, или что-то на подобии TWinControl(GetOwner).Handle если вы уже пишете полноценный компонент.
  • StringToOleStr
    если вы не забыли поле ShellObject у нас имеет тип String, а lpszDisplayName - PWideChar и поскольку сама Delphi преобразование не сделает мы должны коректно преобразовать ShellObject в PWideChar.
  • ExtractFileDir
    как видно из названия возвращает строку с путем к заданому файлу.
  • FEaten,FAtt
    как я уже говорил мне они не пригодились но чем черт не шутит, лучше их придержать.
  • FItemIDList
    сохранаем, и запоминаем где сохранили, потому что он нам ещё понадобиться.

После удачного завершения (то есть OleCheck не вернул EOleSysError) нам надо бы перейти к классу родителя нашего NotePad.exe, так как информация о нём целиком и полностью содержится у его родителя C:\Windows, для этого воспользуемся функцией из состава IShellFolder под названием BindToObject, которая объявлена следующим образом:
Function BindToObject(pidl: PItemIDList; pbcReserved: Pointer;const riid: TIID; out ppvOut): HResult; stdcall;

    Тут:
  • pidl - наш FItemIDList;
  • pbcReserved - nil;
  • riid - каким он должен быть. В нашем случае IID_IShellFolder
  • ppvOut - куда нам его запихнут(скажем ShellFolder1).

После очередной строчки кода:
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);

{Обработка результатов}
 .............................................
Finally
  ICM:=nil;
End;
Обработку результатов можно сделать следующим образом:

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;

Что тут написано:
Во первых - вызов интерфейса IContextMenu сопряженного с объектом FItemIDList (т.е. "Notepad.exe") папки ShellFolder0 (т.е. C:\Windows). Во вторых создание дескриптора контекстного меню, который идентифицирует пустое контекстное меню. В-третьих, использование метода QueryContextMenu для заполнения контекстного меню, после этого использование команды TrackPopupMenu для вывода контекстного меню в точку (100,100).

Небольшая характеристика кода обработки разультата комманды TrackPopupMenu:

  • Переменная Command типа LongBool преобразуется в тип Longint;
  • CommandStr:Array[0..255] of Char - переменная в которую заносится название команды, которую пользователь желает исполнить;
  • DoCommandEvent - процедура обработки события о ней будет рассказано дальше;
  • Структура ICI типа _CMINVOKECOMMANDINFO задает параметры, необходимые для запуска на исполнение кода приписанного выбранному пункту меню по умолчанию.
  • InvokeCommand(ICI) - запуск кода по умолчанию.
  • Внутри оператора Finally обнуляем ICM с тем, чтобы интерфейс не засорял память.

А как же быть с компонентом?

Да, действительно, если это все вписать в простую кнопку на форме, и если оно с первого раза заработает. То этот подход будет, мягко выражаясь, не в стиле Delphi. Для того чтобы его "причесать" мы и создадим некий компонент (ShellPopupMenu рис.1) который будет выполнять всю рутинную работу, а нам останется только корректировать его поведение в зависимости от ситуации. Но как говорится не "Context'ом единым...", ведь большинство из используемых переменных, полей(ShellFolder,SpecialRoot,ShellObject...) используется не только для вывода контекстных меню, но и для других мирных целей. И чтобы не переписывать одни и те же строки кода в компонентах, мы организуем абстрактный класс (TAbstractShellObject), в котором реализуем все часто используемые процедуры (основное его задание - использование исходных данных из полей ShellObect и SpecialRoot для заполнения свойства ShellFolder и поля FItemIDList) [см. листинг 1]. Там также имеется и определение класса исключения для использования в этом конкретном случае при возникновении несогласования системы со значением свойства ShellObject. Как видно из листинга все "ненужные" параметры, возвращаемые методами, помещены в раздел protected с задней мыслью: "Авось пригодятся". Там же расположился и FItemIDList так как об его существовании нет необходимости знать тому, кто использует этот компонент для построения приложений, но он все же остаётся доступным для использования при создании компонентов - потомков TAbstractShellObject.

Не будем останавливаться на деталях реализации этого класса, только скажем, что его условно можно поделить на 2 ветки реализации при UseSpecialRoot = True, и при UseSpecialRoot = False.

Первая ветка обеспечивает заполнение вышеназванных полей, используя функцию SHGetSpecialFolderLocation с параметром SpecialRoot для заполнения свойства ShellFolder, а для заполнения FItemIDList - используется значение ShellObject и метод ParseDisplayName из интерфейса IShellFolder.

Вторая ветка предназначена для исполнения этой же функции, но в рамках файловой системы, и качестве исходных данных для заполнения ShellFolder принимает результат функции ExtractFilePath, а для заполнения FItemIDList - ExtractFileName с приминением свойства ShellObject.

Класс TShellPopupMenu использует данные найденные его предком для вывода контекстного меню [см. листинг 1]. И кроме этого реализует интерфейс для обработки событий посылаемых контекстным меню. Итак, при выводе щелчке пользователя на одном из пунктов контекстного меню сперва вызывается событие OnCommand, которое в качестве входных данных принимает (в лучших традициях Delphi) идентификатор вызвавшего объекта (Sender) и идентификатор команды контекстного меню (Command). А также параметр Handled который используется для разрешения (запрещения) дальнейшей реакции системы на команду контекстного меню.

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

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

Рис.3Рис.4

http://subscribe.ru/
E-mail: ask@subscribe.ru
Отписаться
Убрать рекламу

В избранное