Руководство полного идиота по написанию расширений оболочки - Часть II

Расширение оболочки, которое работает сразу для нескольких файлов

Автор: Michael Dunn
Перевод: Инна Кирюшкина
Алексей Кирюшкин

Источник: The Code Project
Опубликовано: 24.04.2001
Версия текста: 1.1

Дополнительные функции расширения, добавляющего пункты к контекстному меню оболочки.
Использование AppWizard
Интерфейс инициализации
Добавление пунктов к меню
Подсказка в строке состояния и "действие"
Выполнение выбора пользователя
Регистрация расширения
Как это выглядит?
Другие способы регистрации расширения
Продолжение следует...

Демонстрационный проект

Часть I настоящего руководства была введением в написание расширений оболочки, а также содержала простой пример расширения, добавляющего команды в контекстное меню. Оно оперировало одним файлом в один момент времени. Во второй части я покажу как оперировать несколькими файлами в одной операции. Пример расширения - утилита, которая может регистрировать и разрегистрировать серверы COM. Кроме того она также демонстрирует применение диалогового класса ATL CDialogImpl. Во второй части я дам объяснение некоторых специальных регистрационных ключей, которые вы можете использовать для загрузки расширения для любых файлов, а не только для файлов заранее выбранного типа.

Я предполагаю, что вы уже читали часть I и, таким образом, имеете понятие о расширениях, добавляющих команды в контекстное меню. Вы также должны иметь представление о COM, ATL и STL.

Дополнительные функции расширения, добавляющего пункты к контекстному меню оболочки.

Это расширение позволяет регистрировать и разрегистрировать серверы СOM в файлах EXE, DLL и OCX. В отличии от расширения, реализованного в первой части, это расширение будет оперировать всеми файлами, выделенными, когда произошел щелчок правой кнопкой мыши.

Использование AppWizard

Запустите AppWizard и создайте новый ATL COM проект. Назовем его DllReg. Сохраните все начальные установки и щелкните Finish. Чтобы добавить COM объект к DLL перейдите в дерево просмотра классов - ClassView, щелкните правой кнопкой мыши на пункте DllReg classes и укажите New ATL Object

В мастере ATL Object на первой панели уже выбран Simple Object, поэтому просто щелкните Next. Во второй панели в поле редактирования Short Name введите CDllRegShlExt и щелкните OK. (Остальные поля редактирования заполняются автоматически.) Мы своими действиями создали класс CDllRegShlExt, в котором содержится базовый код для выполнения COM объекта. Добавим свой код к этому классу.

Интерфейс инициализации

В реализации IShellExtInit::Initialize() будет несколько отличий. Это обусловлено двумя причинами. Во-первых, мы перечислим все выбранные файлы. Во-вторых, мы протестируем их на предмет экспорта функций регистрации и разрегистрации. Мы будем рассматривать только те файлы, которые экспортируют обе функции - DllRegisterServer() и DllUnregisterServer(). Остальные файлы будут проигнорированы.

Поскольку нам придется использовать элемент управления list view, классы строк и списков из STL, необходимо добавить в файл stdafx.h следующие строки:

#include <commctrl.h>
#include <string>
#include <list>
#include <atlwin.h>
typedef std::list<std::basic_string<TCHAR> > string_list;

Наш класс CDllRegShlExt также нуждается в нескольких переменных-членах:

protected:
    HBITMAP     m_hRegBmp;
    HBITMAP     m_hUnregBmp;
    string_list m_lsFiles;
    TCHAR       m_szDir [MAX_PATH];

Конструктор класса CDllRegShlExt загружает из ресурсов две картинки для использования в контекстном меню:

CDLLRegShlExt::CDLLRegShlExt()
{
    m_hRegBmp = LoadBitmap ( _Module.GetModuleInstance(),
                             MAKEINTRESOURCE(IDB_REGISTERBMP) );

    m_hUnregBmp = LoadBitmap ( _Module.GetModuleInstance(),
                               MAKEINTRESOURCE(IDB_UNREGISTERBMP) );
}

Внимательно просмотрите часть I. Она содержит инструкцию, как добавить IShellExtInit к списку интерфейсов, реализуемых CDllRegShlExt. Как только вы это сделаете, вы будете готовы к написанию функции Initialize().

Вот пошаговое содержание функции:

  1. Изменить текущий каталог на каталог, открытый в окне проводника.
  2. Перечислить все выбранные файлы.
  3. Каждый файл попытаться загрузить с LoadLibrary().
  4. Если LoadLibrary() завершилась успешно, убедиться, что файл экспортирует функции DllRegisterServer() и DllUnregisterServer().
  5. Если обе функции обнаружены, добавить имя файла к списку файлов m_lsFiles, пригодных к работе.
HRESULT CDllRegShlExt::Initialize ( 
    LPCITEMIDLIST pidlFolder,
    LPDATAOBJECT pDataObj,
    HKEY hProgID )
{
TCHAR     szFile    [MAX_PATH];
TCHAR     szFolder  [MAX_PATH];
TCHAR     szCurrDir [MAX_PATH];
TCHAR*    pszLastBackslash;
UINT      uNumFiles;
HDROP     hdrop;
FORMATETC etc = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM stg = { TYMED_HGLOBAL };
HINSTANCE hinst;
bool      bChangedDir = false;
HRESULT (STDAPICALLTYPE* pfn)();

Масса рутинных локальных переменных! Сначала нужно получить HDROP из pDataObj. Это делается также, как для расширения в первой части.

    // Read the list of folders from the data object.  They're stored in HDROP
    // format, so just get the HDROP handle and then use the drag 'n' drop APIs
    // on it.
    if ( FAILED( pDO->GetData ( &etc, &stg )))
        return E_INVALIDARG;

    // Get an HDROP handle.
    hdrop = (HDROP) GlobalLock ( stg.hGlobal );

    if ( NULL == hdrop )
        {
        ReleaseStgMedium ( &stg );
        return E_INVALIDARG;
        }

    // Determine how many files are involved in this operation.
    uNumFiles = DragQueryFile ( hdrop, 0xFFFFFFFF, NULL, 0 );

Затем следует цикл for для извлечения следующего имени файла (с помощью DragQueryFile()) и попытка загрузить его с LoadLibrary(). Реальное расширение из демонстрационного проекта заблаговременно меняет текущий каталог. Эту часть я намеренно опустил, так как она слишком пространна.

    for ( UINT uFile = 0; uFile < uNumFiles; uFile++ )
        {
        // Get the next filename.
        if ( 0 == DragQueryFile ( hdrop, uFile, szFile, MAX_PATH ))
            continue;

        // Try & load the DLL.
        hinst = LoadLibrary ( szFile );
        
        if ( NULL == hinst )
            continue;

Далее проверяем, экспортируются ли две необходимые функции:

        // Get the address of DllRegisterServer();
        (FARPROC&) pfn = GetProcAddress ( hinst, "DllRegisterServer" );

        // If it wasn't found, skip the file.
        if ( NULL == pfn )
            {
            FreeLibrary ( hinst );
            continue;
            }

        // Get the address of DllUnregisterServer();
        (FARPROC&) pfn = GetProcAddress ( hinst, "DllUnregisterServer" );

        // If it was found, we can operate on the file, so add it to
        // our list of files (m_lsFiles).
        if ( NULL != pfn )
            {
            m_lsFiles.push_back ( szFile );
            }

        FreeLibrary ( hinst );
    }   // end for

И последний шаг в вышеуказанном блоке шагов - добавить имя файла в m_lsFiles, который является списком из семейства STL-списков, содержащих строки. Этот список будет использован позже, когда мы будем регистрировать и разрегистрировать файлы.

Что еще необходимо сделать в функции Initialize(), так это освободить ресурсы и возвратить проводнику правильное значение.

    // Release resources.
    GlobalUnlock ( stg.hGlobal );
    ReleaseStgMedium ( &stg );

    // If we found any files we can work with, return S_OK.  Otherwise,
    // return E_INVALIDARG so we don't get called again for this right-click
    // operation.

    return ( m_lsFiles.size() > 0 ) ? S_OK : E_INVALIDARG;

Если вы внимательно посмотрите на код демонстрационного проекта, то вы увидите, как я определяю просматриваемый каталог. Вам, должно быть, интересно узнать, почему я не использовал для этой цели параметр pidlFolder, который в документации описан как "список идентификаторов объектов для папки, содержащей объект, для которого отображено контекстное меню". Это хорошо звучит, но во время моего тестирования на Windows 98 этот параметр всегда был NULL, так что он бесполезен.

Добавление пунктов к меню

Следующий этап - методы IContextMenu. Как и раньше, необходимо добавить IContextMenu к списку интерфейсов, реализуемых CDllRegShlExt. Действия, сопровождающие эту операцию описаны в первой части.

Вот как будут выглядеть пункты, которые мы добавим к контекстному меню. Однин для регистрации файлов, другой - для из разрегистрации.


Начало реализации QueryContextMenu() аналогично описанному в первой части. Мы проверяем uFlags и немедленно делаем возврат, если обнаружим флаг CMF_DEFAULTONLY.

HRESULT CDLLRegShlExt::QueryContextMenu (
    HMENU hmenu,
    UINT  uMenuIndex,
    UINT  uidFirstCmd,
    UINT  uidLastCmd,
    UINT  uFlags )
{
UINT uCmdID = uidFirstCmd;

    // If the flags include CMF_DEFAULTONLY then we shouldn't do anything.
    if ( uFlags & CMF_DEFAULTONLY )
        {
        return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );
        }

Затем мы добавляем пункт меню "Register servers". Далее кое-что новенькое. Мы установим картинку для этого пункта. Таким образом, перед нашим пунктом меню будет появляться пиктограмма, также как это делает WinZip.

    // Add our register/unregister items.
    InsertMenu ( hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION, uCmdID++,
                 _T("Register server(s)") );

    // Set the bitmap for the register item.
    if ( NULL != m_hRegBmp )
        {
        SetMenuItemBitmaps ( hmenu, uMenuIndex, MF_BYPOSITION, m_hRegBmp, NULL );
        }

    uMenuIndex++;

Функция API SetMenuItemBitmaps() приводит в действие механизм отображения картинки перед пунктом меню "Register servers". Заметьте, что uCmdID инкрементируется для следующего вызова InsertMenu(). И в конце этого шага также наращивается uMenuIndex, так что второй пункт меню появится вслед за первым.

Далее следует добавление второго пункта меню. Код реализации идентичен коду для первого пункта.

    InsertMenu ( hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION, uCmdID++,
                 _T("Unregister server(s)") );

    // Set the bitmap for the unregister item.
    if ( NULL != m_hUnregBmp )
        {
        SetMenuItemBitmaps ( hmenu, uMenuIndex, MF_BYPOSITION, m_hUnregBmp, NULL );
        }

В конце мы сообщаем проводнику, сколько пунктов меню мы добавили:

return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 2 );

Подсказка в строке состояния и "действие"

Для отображения подсказки и извлечения "действия" из добавленных команд проводнику потребуется вызвать метод GetCommandString(). Создаваемое нами расширение отличается тем, что добавляет два пункта к меню. Поэтому, чтобы определить какой из пунктов меню был выделен, нам потребуется исследовать параметр uCmdID:

#include <atlconv.h>

HRESULT CDLLRegShlExt::GetCommandString ( 
    UINT  uCmdID,
    UINT  uFlags, 
    UINT* puReserved,
    LPSTR szName,
    UINT  cchMax )
{
LPCTSTR szPrompt;

    USES_CONVERSION;

    if ( uFlags & GCS_HELPTEXT )
        {
        switch ( uCmdID )
            {
            case 0:
                szPrompt = _T("Register all selected COM servers");
            break;

            case 1:
                szPrompt = _T("Unregister all selected COM servers");
            break;

            default:
                return E_INVALIDARG;
            break;
            }

Если uCmdID равен 0, то выделен первый пункт (регистрация), если 1, то второй (разрегистрация). После этого мы определяем строку подсказки, копируем ее в установленный буфер, конвертируя в UNICODE, если это необходимо.

        // Copy the help text into the supplied buffer.  If the shell wants
        // a Unicode string, we need to case szName to an LPCWSTR.

        if ( uFlags & GCS_UNICODE )
            {
            lstrcpynW ( (LPWSTR) szName, T2CW(szPrompt), cchMax );
            }
        else
            {
            lstrcpynA ( szName, T2CA(szPrompt), cchMax );
            }
        }

Я также написал для этого расширения код, который извлекает "действие". Однако, при тестировании на Windows 98 проводник никогда не пытался вызвать GetCommandString(), чтобы извлечь "действие". Я даже написал пробную программу, которая вызывала ShellExecute() для DLL и пыталась использовать "действие". Но ничего не работало. Я не знаю, отличается ли чем-то поведение в Windows NT. По этой причине я опустил реализацию кода извлечения "действия", но вы можете попробовать реализовать его самостоятельно, если вам интересно.

Выполнение выбора пользователя

Когда пользователь щелкает на одном из пунктов меню, проводник вызывает метод InvokeCommand(). Он в первую очередь проверяет старшее слово в значении lpVerb. Если оно не нулевое, то это имя "действия", которое можно извлечь. Поскольку мы уже знаем, что "действия" работают некорректно (по крайней мере в Windows 98), не будем обращать на них внимание. Рассмотрим младшее слово. Если оно 0 или 1, можно быть уверенным, что выбран один из наших пунктов меню.

HRESULT CDllRegShlExt::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo )
{
    // If lpVerb really points to a string, ignore this function call and bail out.
    if ( 0 != HIWORD( pInfo->lpVerb ))
        return E_INVALIDARG;

    // Check that lpVerb is one of our commands (0 or 1)
    switch ( LOWORD( pInfo->lpVerb ))
        {
        case 0:
        case 1:
            {
            CProgressDlg dlg ( &m_lsFiles, pInfo );

            dlg.DoModal();
            return S_OK;
            }
        break;

        default:
            return E_INVALIDARG;
        break;
        }
}

Если младшее слово lpVerb принимает значение 0 или 1, мы создаем диалог (производный от класса ATL CDialogImpl) для отображения процесса регистрации/разрегистрации и передаем ему список имен файлов.

Вся реальная работа производится в классе CProgressDlg. Функция этого класса OnInitDialog() инициализирует элемент управления "список" (list control) и затем вызывает CProgressDlg::DoWork(). Эта функция просматривает список, который был построен в CDllRegShlExt::Initialize() и вызывает соответствующую функцию в каждом файле. Основной код приведен ниже, но он не является полным, поскольку я не привел код проверки ошибок и части по заполнению списка. Этого вполне достаточно, чтобы продемонстрировать, как просматривается список имен файлов и показать действия с каждым файлом.

void CProgressDlg::DoWork()
{
HRESULT (STDAPICALLTYPE* pfn)();
string_list::const_iterator it, itEnd;
HINSTANCE hinst;
LPCSTR    pszFnName;
HRESULT   hr;
WORD      wCmd;

    wCmd = LOWORD ( m_pCmdInfo->lpVerb );

    // We only support 2 commands, so check the value passed in lpVerb.
    if ( wCmd > 1 )
        return;

    // Determine which function we'll be calling.  Note that these strings are
    // not enclosed in the _T macro, since GetProcAddress() only takes an
    // ANSI string for the function name.
    pszFnName = wCmd ? "DllUnregisterServer" : "DllRegisterServer";

    for ( it = m_pFileList->begin(), itEnd = m_pFileList->end();
          it != itEnd;
          it++ )
        {
        // Try to load the next file.
        hinst = LoadLibrary ( it->c_str() );

        if ( NULL == hinst )
            continue;

        // Get the address of the register/unregister function.
        (FARPROC&) pfn = GetProcAddress ( hinst, pszFnName );

        // If it wasn't found, go on to the next file.
        if ( NULL == pfn )
            continue;

        // Call the function!
        hr = pfn();

Нужно дать некоторые объяснения относительно цикла "for", поскольку, если вы не в курсе, коллекция классов STL несколько "странновата". m_pFileList - указатель списка m_lsFiles в классе CDllRegShlExt. (Этот указатель был передан конструктору класса CProgressDlg). Коллекция списков STL имеет тип, называемый const_iterator, сущность которого аналогична типу POSITION в MFC. А переменная типа const_iterator действует как указатель на const объект в списке, поэтому этот итератор может применяться с "->" для получения доступа к этому объекту. Он также может быть увеличен с помощью оператора "++" для продвижения вперед по списку.

Итак, в выражении инициализации цикла "for" вызывается list::begin() для получения итератора, "указывающего" на первую строку списка и, затем, вызывается list::end() для получения итератора, который "указывает" на "конец" списка, то есть на следующую позицию после последней строки. (Я поместил термины в кавычки, чтобы подчеркнуть, что понятия указывания, начала и конца списка очень абстрактны для типа const_iterator и могут быть материализованы через методы const_iterator [типа begin()] или операторы [типа "++"]. Итераторы начала и конца обозначены it и itEnd соответственно. Цикл продолжается, пока it не станет равным itEnd, то есть пока it не достигнет "конца" списка. Итератор it, проходя через цикл, каждый раз увеличивается, и таким образом, в одном витке цикла обрабатывается одна строка списка.

Выражение it->c_str() использует оператор "(r)" для итератора. Поскольку it выполняет функцию указателя на строку (помните, m_pFileList является списком строк STL), то it->c_str() вызывает функцию c_str() для строки, на которую в данный момент указывает it. c_str() возвращает указатель на C-style строку символов, в данном случае эквивалент LPCTSTR.

Остальное в DoWork() - это освобождение ресурсов и обработка ошибок. Вы можете найти законченный код в файле ProgressDlg.cpp демонстрационного проекта.

(Я только сейчас осознал, как странно звучит разговор о переменной, названной "it" (это). Sorry.) :-)

Регистрация расширения

Расширение DllReg оперирует исполняемыми файлами, поэтому регистрация расширения позволит загружать его на файлах EXE, DLL, OCX. Как и в первой части мы можем сделать это с помощью RGS скрипта DllRegShlExt.rgs. Вот скрипт, необходимый для регистрации нашей DLL, как обработчика контекстного меню для файлов каждого из вышеуказанных расширений.

HKCR
{
    NoRemove dllfile
    {
        NoRemove shellex
        {
            NoRemove ContextMenuHandlers
            {
                ForceRemove DLLRegSvr = s '{8AB81E72-CB2F-11D3-8D3B-AC2F34F1FA3C}'
            }
        }
    }
    NoRemove exefile
    {
        NoRemove shellex
        {
            NoRemove ContextMenuHandlers
            {
                ForceRemove DLLRegSvr = s '{8AB81E72-CB2F-11D3-8D3B-AC2F34F1FA3C}'
            }
        }
    }
    NoRemove ocxfile
    {
        NoRemove shellex
        {
            NoRemove ContextMenuHandlers
            {
                ForceRemove DLLRegSvr = s '{8AB81E72-CB2F-11D3-8D3B-AC2F34F1FA3C}'
            }
        }
    }
}

Если вы забыли, что имеется ввиду под ключевыми словами NoRemove и ForceRemove, обратитесь к первой части, там же дается описание формата RGS-файла.

Как и для предыдущего расширения, в Windows NT/2000 мы должны внести имя своего расширения в список "одобреных" расширений. Код, реализующий это действие, содержится в функциях DllRegisterServer() и DllUnregisterServer(). Я не буду приводить этот код здесь, так он очень прост. Но вы должны найти его в демонстрационном проекте.

Как это выглядит?

После того, как вы щелкните на одном из пунктов меню, на экране появится окно диалога и покажет результаты операций:


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

Другие способы регистрации расширения

В данный момент расширение может работать только с определенными типами файлов. Однако вполне возможен вызов расширения для любых типов файлов. Для этого необходимо зарегистрировать наше расширение как обработчик контекстного меню под ключом HKCR\*:

HKCR
{
    NoRemove *
    {
        NoRemove shellex
        {
            NoRemove ContextMenuHandlers
            {
                ForceRemove DLLRegSvr = s '{8AB81E72-CB2F-11D3-8D3B-AC2F34F1FA3C}'
            }
        }
    }
}

Ключ HKCR\*, регистрирует расширения оболочки, которые должны загружаться для любых типов файлов. В документации говорится, что расширение может загружаться для любых объектов оболочки (файлы, каталоги, виртуальные папки, объекты панели управления и т.д.). Но во время моих испытаний этот факт не подтвердился. Расширения загружались только для файлов.

В оболочке версии 4.71+ ключ называется HKCR\AllFileSystemObjects. Если регистрироваться под этим ключом, расширение будет загружаться для всех файлов и каталогов, исключая корневой каталог. (Чтобы расширение загружалось для корневого каталога, мы должны зарегистрировать его под ключом HKCR\Drive. Однако я заметил странное поведение после регистрации под этим ключом. Меню SendTo также использует этот ключ, и поэтому пункты меню DllReg и SendTo перемешались:


Вы можете написать расширение, оперирующее каталогами. Пример такого расширения вы можете найти в моей статье "A Utility to Clean Up Compiler Temp Files" ("Утилита для очистки от временных файлов компилятора").

Наконец, в версии 4.71+ расширение, обработчик контекстного меню может загружаться, когда пользователь щелкает правой кнопкой мыши на фоне окна просмотра каталогов, включая рабочий стол. Для этого расширение необходимо зарегистрировать под ключом HKCR\Directory\Background\shellex\ContextMenuHandlers. Используя этот метод вы можете добавить свои собственные пункты к контекстному меню рабочего стола или любой другой директории. Параметры, передаваемые IShellExtInit::Initialize() немного отличаются в этом случае, поэтому я еще вернусь к этой теме в следующих статьях.

Продолжение следует...

В Части III мы изучим новый тип расширения, обработчик всплывающей подсказки, QueryInfo, которая описывает объекты оболочки. Я покажу также, как применять MFC в расширении оболочки.


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.