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

Расширение оболочки - обработчик перетаскивания файлов правой кнопкой мыши

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

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

Обработчик перетаскивания
Что делает это расширение?
Интерфейс инициализации
Модификация контекстного меню
Создание связи
Обеспечение подсказки в строке состояния
Создание связи
Регистрация расширения
Если у вас нет Windows 2000
Продолжение следует...

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

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

Часть IV предполагает, что вы понимаете основы расширений оболочки и знакомы с MFC. Это конкретное расширение - реальная утилита, которая создает жесткие связи в Windows 2000. Но даже если у вас не Win 2K, вы все же можете продолжать читать. Код использует несколько функций из shlwapi.dll версии 4.71, так что вам нужен будет IE4 или выше (Active Desktop устанавливать не обязательно).

Обработчик перетаскивания

Каждый продвинутый пользователь знает (да и некоторые нормальные тоже), что объекты в проводнике можно перетаскивать с помощью правой кнопки мыши. Проводник отображает пункты контекстного меню, включающие все доступные действия, которые вы можете предпринять. Обычно это перемещение, копирование и создание ярлыка:


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


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

Что делает это расширение?

Это расширение оболочки будет использовать новую функцию API Windows 2000, CreateHardLink(), для создания жестких связей между файлами на томах NTFS. Мы добавим пункт создания жесткой связи к контекстному меню, так что пользователь сможет создавать жесткие связи тем же путем, что и обычные ярлыки.

Запустите AppWizard и создайте новый ATL COM проект. Назовем его HardLink. Поскольку в этот раз мы собираемся использовать MFC, пометьте переключатель Support MFC, затем щелкните Finish. Чтобы добавить COM объект к DLL, перейдите в дерево просмотра классов, ClassView, щелкните правой кнопкой на пункте HardLink classes и укажите New ATL Object.

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

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

Как и для предыдущих расширений - обработчиков контекстного меню - проводник использует для инициализации интерфейс IShellExtInit. Первое, что нужно сделать - это добавить IShellExtInit к списку интерфейсов, которые реализует CHardLinkShlExt. Откройте HardLinkShlExt.hи добавьте выделенные строки:

#include <comdef.h>
#include <shlobj.h>

class ATL_NO_VTABLE CTxtInfoShlExt : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CTxtInfoShlExt, &CLSID_TxtInfoShlExt>,
    public IDispatchImpl<ITxtInfoShlExt, &IID_ITxtInfoShlExt, &LIBID_TXTINFOLib>,
    public IShellExtInit
{
BEGIN_COM_MAP(CTxtInfoShlExt)
    COM_INTERFACE_ENTRY(ITxtInfoShlExt)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY(IShellExtInit)
END_COM_MAP()

public:
    // IShellExtInit
    STDMETHOD(Initialize)(LPCITEMIDLIST, LPDATAOBJECT, HKEY);

Нам также понадобятся несколько переменных - для картинки к пункту меню и имен перетаскиваемых файлов:

protected:
    // IHardLinkShlExt
    CBitmap     m_bitmap;
    TCHAR       m_szFolderDroppedIn [MAX_PATH];
    CStringList m_lsDroppedFiles;

Также нужно добавить несколько директив #define в stdafx.h чтобы сделать доступными новые функции из shlwapi.dll:

#define WINVER 0x0500
#define _WIN32_WINNT 0x0500
#define _WIN32_IE 0x0400

Определение WINVER как 0x0500 задействует возможности, специфичные для Win98 и 2000, а определение _WIN32_WINNT как 0x0500 делает доступными новые возможности Win2000. Определение _WIN32_IE как 0x0400 задействует возможности предоставляемые IE 4.

Теперь приступим к методу Initialize(). В этот раз я покажу, как использовать MFC, чтобы получить список перетаскиваемых файлов. В MFC есть класс COleDataObject - обертка интерфеса IDataObject. Раньше нам приходилось вызывать методы IDataObject непосредственно. Но, к счастью, MFC немного облегчает нашу работу. Чтобы освежить вашу память, вот прототип Initialize():

HRESULT IShellExtInit::Initialize (
    LPCITEMIDLIST pidlFolder,
    LPDATAOBJECT  pDataObj,
    HKEY          hProgID );

Для расширений - обработчиков перетаскивания pidlFolder - это PIDL папки, куда сбрасываются объекты. (Подробнее об этом PIDL я расскажу позже). pDataObj - это интерфейс IDataObject, с помощью которого мы перечисляем все сброшенные объекты. hProgID - открытый HKEY нашего расширения под ключом HKEY_CLASSES_ROOT.

Первый шаг - загрузка картинки для пункта меню. Затем мы подключаем переменную COleDataObject к интерфейсу IDataObject.

HRESULT CHardLinkShlExt::Initialize (
    LPCITEMIDLIST pidlFolder,
    LPDATAOBJECT  pDataObj,
    HKEY          hProgID )
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState());    // init MFC

    COleDataObject dataobj;
    HGLOBAL        hglobal;
    HDROP          hdrop;
    TCHAR          szRoot [MAX_PATH];
    TCHAR          szFileSystemName [256];
    TCHAR          szFile [MAX_PATH];
    UINT           uFile, uNumFiles;

    m_bitmap.LoadBitmap ( IDB_LINKBITMAP );

    dataobj.Attach ( pDataObj, FALSE );

Передача FALSE как второго параметра в Attach() подразумевает, что не нужно освобождать интерфейс IDataObject, если переменная dataobj уничтожена. Следующий шаг - получить каталог, где были сброшены объекты. У нас есть PIDL этого каталога, но как получить весь путь? Настало время для небольшого отступления.

"PIDL" - сокращение от pointer to an ID list - указатель на список идентификаторов объектов. Т.о. PIDL это способ уникальной идентификации объекта в пределах иерархии, предоставленой проводником. Каждый объект в оболочке, независимо от того, является он частью файловой системы или нет, имеет свой PIDL. Точная структура PIDL зависит от того, где объект находится, но если вы не пишете свое namespace-раширение, вам нечего волноваться по поводу внутренней структуры PIDL.

Мы можем для своих целей использовать API оболочки, чтобы перевести PIDL в стандартный путь. Это сделает функция SHGetPathFromIDList(). Если целевая папка не является каталогом в файловой системе (как, например, панель управления) SHGetPathFromIDList() возвращает ошибку и мы спокойно вываливаемся.

    if ( !SHGetPathFromIDList ( pidlFolder, m_szFolderDroppedIn ))
        {
        return E_FAIL;
        }

Далее, нужно проверить, находится ли целевой каталог на томе NTFS. Мы получим корневой компонент пути (например E:\) и информацию об этом томе. Если файловая система не NTFS мы не сможем установить связи и возвратим E_FAIL.

    lstrcpy ( szRoot, m_szFolderDroppedIn );
    PathStripToRoot ( szRoot );

    if ( !GetVolumeInformation ( szRoot, NULL, 0, NULL, NULL, NULL, 
                                 szFileSystemName, 256 ))
        {
        // не удалось определить тип файловой системы.
        return E_FAIL;
        }

    if ( 0 != lstrcmpi ( szFileSystemName, _T("ntfs") ))
        {
        // файловая система - не NTFS, а значит не поддерживает жесткие связи.
        return E_FAIL;
        }

Далее мы получаем дескриптор HDROP из объекта данных, который мы используем, чтобы перечислить сброшенные файлы. Это похоже на метод из части III, за исключением того, что мы используем класс MFC для получения доступа к данным. COleDataObject оперирует загадочными для нас структурами FORMATETC и STGMEDIUM.

    hglobal = dataobj.GetGlobalData ( CF_HDROP );

    if ( NULL == hglobal )
        return E_INVALIDARG;

    hdrop = (HDROP) GlobalLock ( hglobal );

    if ( NULL == hdrop )
        return E_INVALIDARG;

Затем мы используем дескриптор HDROP, чтобы перечислить сброшенные файлы. Для каждого из них делаем проверку, не является ли он каталогом. Каталоги не могут быть связаны, поэтому, если найден каталог - возвращаем E_FAIL.

    uNumFiles = DragQueryFile ( hdrop, 0xFFFFFFFF, NULL, 0 );

    for ( uFile = 0; uFile < uNumFiles; uFile++ )
        {
        if ( DragQueryFile ( hdrop, uFile, szFile, MAX_PATH ))
            {
            if ( PathIsDirectory ( szFile ))
                {
                // Мы нашли каталог! Прерываем перебор.
                m_lsDroppedFiles.RemoveAll();
                break;
                }

Мы также проверяем, находились ли сброшенные файлы на том же самом томе, что и целевой каталог. Для этого сравниваем корневые компоненты для каждого файла и целевого каталога. Если они различаются - возвращаем E_FAIL. Вообще-то это не полное решение, поскольку в Win2000 вы можете установить том в середине другого тома. Например, вы можете иметь том C:\ и смонтировать как другой том каталог C:\DEV. Этот код не исключает попытку установить связь из C:\DEV куда-нибудь еще на C:.

Вот сравнение корневых компонент:


            if ( !PathIsSameRoot ( szFile, m_szFolderDroppedIn ))
                {
                // Файлы переносятся между разными томами. Прерываем перебор.
                m_lsDroppedFiles.RemoveAll();
                break;
                }

Если файл успешно прошел обе проверки, мы добавляем его к m_lsDroppedFiles, который является объектом класса CStringList (связанный список строк CString).

            // Добавляем файл к списку сброшенных файлов
            m_lsDroppedFiles.AddTail ( szFile );
            }
        }   // end for

После цикла for мы освобождаем ресурсы и возвращаем управление проводнику. Если список содержит какие-либо имена файлов, мы возвращаем S_OK, показывая, что нам необходимо модифицировать контекстное меню. В противном случае мы возвращаем E_FAIL, чтобы наше расширение больше не вызывалось для данного события перетаскивания.

    GlobalUnlock ( hglobal );

    return ( m_lsDroppedFiles.GetCount() > 0 ) ? S_OK : E_FAIL;
}

Модификация контекстного меню

Подобно другим расширениям - обработчикам контекстных меню, обработчик перетаскивания реализует интерфейс IContextMenu. Чтобы добавить его к нашему расширению, откроем HardLinkShlExt.h и добавим выделенные строки:

class ATL_NO_VTABLE CHardLinkShlExt : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CHardLinkShlExt, &CLSID_HardLinkShlExt>,
    public IDispatchImpl<IHardLinkShlExt, &IID_IHardLinkShlExt, &LIBID_HARDLINKLib>,
    public IShellExtInit,
    public IContextMenu
{
BEGIN_COM_MAP(CHardLinkShlExt)
    COM_INTERFACE_ENTRY(IHardLinkShlExt)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY(IShellExtInit)
    COM_INTERFACE_ENTRY(IContextMenu)
END_COM_MAP()

public:
    // IContextMenu
    STDMETHOD(GetCommandString)(UINT, UINT, UINT*, LPSTR, UINT);
    STDMETHOD(InvokeCommand)(LPCMINVOKECOMMANDINFO);
    STDMETHOD(QueryContextMenu)(HMENU, UINT, UINT, UINT, UINT);

Проводник вызывает нашу функцию QueryContextMenu(), чтобы позволить нам модифицировать контекстное меню. Все это вам уже знакомо. Мы просто добавляем один пункт меню и устанавливаем для него картинку.

HRESULT CHardLinkShlExt::QueryContextMenu (
    HMENU hmenu,
    UINT  uMenuIndex,
    UINT  uidFirstCmd,
    UINT  uidLastCmd,
    UINT  uFlags )
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState());    // init MFC

    // если выставлен флаг CMF_DEFAULTONLY мы не должны что-либо делать
    if ( uFlags & CMF_DEFAULTONLY )
        {
        return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );
        }

    // добавляем пункт в меню.
    InsertMenu ( hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION, uidFirstCmd,
                 _T("Create hard link(s) here") );

    if ( NULL != m_bitmap.GetSafeHandle() )
        {
        SetMenuItemBitmaps ( hmenu, uMenuIndex, MF_BYPOSITION,
                             (HBITMAP) m_bitmap.GetSafeHandle(), NULL );
        }

    // возвращаем 1, чтобы сообщить оболочке, что мы добавили в меню 1 пункт верхнего уровня.
    return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 1 );
}

Вот как выглядит новый пункт меню:


Создание связи

Если пользователь щелкнет на нашем пункте меню, проводник вызовет нашу InvokeCo... Что это? Я упустил еще одну функцию? Простите.

Обеспечение подсказки в строке состояния

HRESULT CHardLinkShlExt::GetCommandString ( 
    UINT  idCmd,
    UINT  uFlags,
    UINT* pwReserved,
    LPSTR pszName,
    UINT  cchMax )
{
    return E_NOTIMPL;
}

Я серьезно. :) Для обработчика перетаскивания проводник не вызывает GetCommandString(). А теперь вернемся к ...

Создание связи

Как я уже говорил, проводник вызывает InvokeCommand(), когда пользователь щелкнет на нашем пункте меню. Мы установим связи для всех сброшенных файлов. Имена будут такими "Hard link to <filename>", или, если это имя уже использовалось, "Hard link (2) to <filename>". Номер может быть произвольным. Мы установим предел равным 99.

Сначала - локальные переменные и проверка параметра lpVerb (он должен равняться нулю, поскольку у нас всего один пункт меню).

HRESULT CHardLinkShlExt::InvokeCommand ( LPCMINVOKECOMMANDINFO pInfo )
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState());    // init MFC

    TCHAR    szNewFilename [MAX_PATH+32];
    CString  sSrcFile;
    TCHAR    szSrcFileTitle [MAX_PATH];
    CString  sMessage;
    UINT     uLinkNum;
    POSITION pos;

    // перепроверяем, что вызван наш пункт меню - lpVerb должен быть 0.
    if ( 0 != pInfo->lpVerb )
        {
        return E_INVALIDARG;
        }

Далее, мы получаем значение типа POSITION , указывающее на начало списка строк. POSITION - непрозрачный тип данных, который не используется непосредственно, но мы передаем его другим методам класса CStringList. В этом отличие от класса STL итераторов, у которого есть операторы для получения доступа к списку данных. Чтобы получить POSITION заголовка списка, мы вызываем GetHeadPosition():

    pos = m_lsDroppedFiles.GetHeadPosition();
    ASSERT ( NULL != pos );

pos принимает значение NULL, когда список пуст, но список не должен быть пустым, поэтому я добавил ASSERT, чтобы проверить этот случай. Далее идет начало цикла, который проходит по именам файлов и устанавливает связи для каждого из них.

    while ( NULL != pos )
        {
        // получаем следующее имя файла
        sSrcFile = m_lsDroppedFiles.GetNext ( pos );

        // удаляем путь - преобразуем "C:\xyz\foo\stuff.exe" к "stuff.exe"
        lstrcpy ( szSrcFileTitle, sSrcFile );
        PathStripPath ( szSrcFileTitle );

        // создаем имя для жесткой связи - сначала пробуем 
        // "Hard link to stuff.exe"
        wsprintf ( szNewFilename, _T("%sHard link to %s"), m_szFolderDroppedIn,
                   szSrcFileTitle );

GetNext() возвращает строку в позиции pos и наращивает pos, чтобы указать на следующую строку. Если мы достигаем конца списка, то pos станет =NULL (вот как закончится цикл while).

В этом месте szNewFilename содержит полное имя жесткой связи. Мы проверяем, существует ли файл с таким именем, и если существует, пытаемся добавить номера от 0 до 99, в зависимости от того, используется ли это имя уже, или нет. Мы также должны быть уверены, что длина имени связи (включая завершающий 0) не превышает MAX_PATH.

        for ( uLinkNum = 2;
              PathFileExists ( szNewFilename )  &&  uLinkNum < 100; 
              uLinkNum++ )
            {
            // составляем другое имя для связи
            wsprintf ( szNewFilename, _T("%sHard link (%u) to %s"),
                       m_szFolderDroppedIn, uLinkNum, szSrcFileTitle );

            // если длина имени превышает  MAX_PATH, выводим сообщение об ошибке
            if ( lstrlen ( szNewFilename ) >= MAX_PATH )
                {
                sMessage.Format ( _T("Failed to make a link to %s. The resulting filename would be too long.\n\nDo you want to continue making links?"),
                                  (LPCTSTR) sSrcFile );

                if ( IDNO == MessageBox ( pInfo->hwnd, sMessage, _T("Create Hard Links"),
                                          MB_ICONQUESTION | MB_YESNO ))
                    break;
                else
                    continue;
                }
            }

Окно сообщения позволяет отменить всю операцию, если вы захотите. Далее, проверяем, не достигли ли мы предела в 99 связей. Снова мы позволяем пользователю отменить всю операцию.

        if ( 100 == uLinkNum )
            {
            sMessage.Format ( _T("Failed to make a link to %s. Reached limit of 99 links in a single directory.\n\nDo you want to continue making links?"),
                              (LPCTSTR) sSrcFile );

            if ( IDNO == MessageBox ( pInfo->hwnd, sMessage, _T("Create Hard Links"),
                                      MB_ICONQUESTION | MB_YESNO ))
                break;
            else
                continue;
            }

Все что осталось - установить жесткую связь. Я не привожу обработку ошибок для большей ясности кода.

        CreateHardLink ( szNewFilename, sSrcFile, NULL );
        }   // end while loop

    return S_OK;
}

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


Подведем итоги по использованию класса CStringList:

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

Регистрация обработчика перетаскивания проще, чем других расширений - обработчиков контекстных меню. Все обработчики перетаскивания регистрируются под ключом HKCR\Directory, так как перетаскивание заканчивается в каком-либо каталоге. Однако, о чем не говорится в документации, так это о том, что регистрации под HKCR\Directory недостаточно, чтобы обработать все случаи. Вы должны зарегистрировать ваше расширение также под ключами HKCR\Folder, чтобы обрабатывать сбрасывания на рабочий стол (desktop) и HKCR\Drive, чтобы обрабатывать сбрасывания в корневой каталог.

Вот RGS скрипт, учитывающий все три вышеуказанных случая:

HKCR
{
    NoRemove Directory
    {
        NoRemove shellex
        {
            NoRemove DragDropHandlers
            {
                ForceRemove HardLinkShlExt = s '{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}'
            }
        }
    }
    NoRemove Folder
    {
        NoRemove shellex
        {
            NoRemove DragDropHandlers
            {
                ForceRemove HardLinkShlExt = s '{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}'
            }
        }
    }
    NoRemove Drive
    {
        NoRemove shellex
        {
            NoRemove DragDropHandlers
            {
                ForceRemove HardLinkShlExt = s '{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}'
            }
        }
    }
}

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

Если у вас нет Windows 2000

Вы все же можете собрать демонстрационный проект в ранних версиях Windows. Откройте файл stdafx.h и раскоментируйте строку:

//#define NOT_ON_WIN2K

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

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

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


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