Сообщений 0    Оценка 90        Оценить  
Система Orphus

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

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

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

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

Введение
Файловые иконки в Проводнике
Использование AppWizard
Интерфейс расширения
Интерфейс инициализации
Интерфейс IExtractIcon
Извлечение методом 1
Извлечение методом 2
Регистрация расширения
Продолжение следует...?

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

Введение

Итак, мы дошли до 9-й части! Эта статья также написана по просьбам читателей. Мы обсудим как показать пользовательскую иконку для всех файлов некоторого типа (в нашем случае - для текстовых файлов). Пример расширения будет работать на любой версии Windows 9x и NT/2000. (Я еще не использовал Me и не имел возможности протестировать в ней мой код, но думаю, там он тоже будет хорошо работать.)

Файловые иконки в Проводнике

Все знают, что каждый тип файла представлен в Проводнике конкретной иконкой. BMP файлы показываются со значком "кисти в краске", HTML страницы - значком "страничка с логотипом IE" и т.д. Проводник определяет какую иконку использовать просматривая реестр и читая ключ под HKEY_CLASSES_ROOT, связанный с данным типом файлов. В результате использования этого метода одна иконка ассоциируется со всеми файлами конкретного типа.

Однако это не единственный путь для задания вида иконки. Проводник позволяет настраивать вид иконки от файла к файлу при помощи соответствующего расширения. Фактически такой пример уже встроен в Windows. Откройте в Проводнике каталог Windows (или любой другой каталог, содержащий много EXE файлов) и вы увидите, что каждый EXE файл имеет собственную иконку. (За исключением EXE файлов, ресурсы которых не содержат иконок. Для них генерируется одна общая иконка.)

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


- 8K или больше


- от 4K до 8K


- от 1 байта до 4K


- 0 байт

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

Запустите AppWizard и создайте новый ATL COM проект. Назовем его TxtFileIcons. Щелкните ОК, чтобы перейти к первому (и единственному) диалогу мастера. Сохраните все установки по умолчанию и щелкните Finish. Теперь у нас есть пустой ATL проект, который построит DLL, но нам необходимо еще добавить наш COM объект-расширение. В дереве ClassView щелкните правой кнопкой мыши на пункте TxtFileIcons classes и укажите New ATL Object.

В мастере ATL Object на первой панели уже выбран Simple Object, поэтому просто щелкните Next. На второй панели в поле редактирования Short Name введите TxtIconShlExt и щелкните ОК (остальные поля заполнятся автоматически). Эти действия создадут класс CTxtIconShlExt, который содержит основной код для реализации COM объектов. Мы добавим наш код в этот класс.

Интерфейс расширения

Хэндлер иконок реализует два интерфейса, IPersistFile и IExtractIcon. Вспомните, что IPersistFile используется, чтобы инициализировать расширения, которые оперируют одиночными файлами, в отличии от IShellExtInit, который предназначен для расширений, действующих на всех отмеченных файлах сразу. IExtractIcon имеет два метода, оба они используются для информирования Проводника о том, какую иконку использовать для данного файла.

Имейте ввиду, что Проводник создает COM объект, для каждого показываемого файла. Это означает, что для каждого файла создается экземпляр класса C++. Попытайтесь избежать трудоемких операций, требующих больших затрат времени в вашем расширении, чтобы не замедлять появление интерфейса Проводника.

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

Чтобы добавить поддержку IPersistFile к нашему COM объекту, откройте TxtIconShlExt.h и добавьте выделенные строки:

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

/////////////////////////////////////////////////////////////////////////////
// CTxtIconShlExt

class ATL_NO_VTABLE CTxtIconShlExt : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CTxtIconShlExt, &CLSID_TxtIconShlExt>,
    public IDispatchImpl<ITxtIconShlExt, &IID_ITxtIconShlExt, &LIBID_TXTFILEICONSLib>,
    public IPersistFile
{
BEGIN_COM_MAP(CTxtIconShlExt)
    COM_INTERFACE_ENTRY(ITxtIconShlExt)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY(IPersistFile)
END_COM_MAP()

public:
    // IPersistFile
    STDMETHOD(GetClassID)( CLSID* )       { return E_NOTIMPL; }
    STDMETHOD(IsDirty)()                  { return E_NOTIMPL; }
    STDMETHOD(Save)( LPCOLESTR, BOOL )    { return E_NOTIMPL; }
    STDMETHOD(SaveCompleted)( LPCOLESTR ) { return E_NOTIMPL; }
    STDMETHOD(GetCurFile)( LPOLESTR* )    { return E_NOTIMPL; }
    STDMETHOD(Load)( LPCOLESTR wszFile, DWORD /*dwMode*/ )
        { 
        USES_CONVERSION;
        lstrcpyn ( m_szFilename, OLE2CT(wszFile), MAX_PATH );
        return S_OK;
        }

protected:
    TCHAR     m_szFilename [MAX_PATH];  // Полный путь к рассматриваемому файлу 
    DWORDLONG m_ldwFileSize;            // Размер файла; Используется при извлечении методом 2.
   
};

Как и в других расширениях, которые используют IPersistFile, только один метод нуждается в реализации - Load(), поскольку с помощью него Проводник сообщает нам, с каким файлом мы работаем. Реализация Load() сделана inline, это просто копирование имени файла в переменную m_szFilename для дальнейшего использования.

Интерфейс IExtractIcon

Хэндлер иконок также реализует интерфейс IExtractIcon, который Проводник вызывает, когда ему нужна иконка для файла. Поскольку наше расширение предназначено для текстовых файлов, Проводник вызывает IExtractIcon как только текстовый файл появится в его окне или меню "Start" ("Пуск"). Чтобы добавить IExtractIcon к нашему COM объекту откройте TxtIconShlExt.h и добавьте выделенные строки:

/////////////////////////////////////////////////////////////////////////////
// CTxtIconShlExt

class ATL_NO_VTABLE CTxtIconShlExt : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CTxtIconShlExt, &CLSID_TxtIconShlExt>,
    public IDispatchImpl<ITxtIconShlExt, &IID_ITxtIconShlExt, &LIBID_TXTFILEICONSLib>,
    public IPersistFile,
    public IExtractIcon
{
BEGIN_COM_MAP(CTxtIconShlExt)
    COM_INTERFACE_ENTRY(ITxtIconShlExt)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY(IPersistFile)
    COM_INTERFACE_ENTRY(IExtractIcon)
END_COM_MAP()

public:
    // IExtractIcon
    STDMETHOD(GetIconLocation)( UINT uFlags, LPTSTR szIconFile, UINT cchMax,
                                int* piIndex, UINT* pwFlags );
    STDMETHOD(Extract)( LPCTSTR pszFile, UINT nIconIndex, HICON* phiconLarge,
                        HICON* phiconSmall, UINT nIconSize );
};

Есть два способа передать иконку Проводнику. Во-первых, GetIconLocation() может вернуть пару имя_файла/индекс, которая состоит из имени файла содержащего иконку и начинающегося с 0 индекса иконки в этом файле. Например, C:\windows\system\shell32.dll/9 одно из возможных значений, которое говорит Проводнику использовать иконку #9 (считая с 0) из shell32.dll. Это не означает использовать иконку чей идентификатор ресурса равен 9, это означает просмотреть идентификаторы ресурсов иконок по порядку (от меньшего к большему) и использовать девятый. Метод Extract() при этом ничего не делает, кроме возврата S_FALSE, чтобы сообщить Проводнику, что ему самому нужно извлечь иконку.

Особенностью этого метода является то, что Проводник может вызвать, а может и не вызывать Extract() после вызова GetIconLocation(). У Проводника есть кэш иконок, в котором хранятся недавно использованные исконки. Если GetIconLocation() передает пару - имя файла/индекс, которая недавно использовалась и иконка все еще в кэше, Проводник извлечет иконку от туда и не будет вызывать Extract() поскольку в этом нет необходимости.

Второй метод заключается в том, чтобы вернуть из GetIconLocation() флаг "не смотреть в кэш", заставляющий Проводник всегда вызывать Extract(). Далее уже Extract() отвечает за загрузку иконки и возврат дескрипторов (HICON) большой (32х32) и малой (16х16) иконок для использования Проводником.

Извлечение методом 1

Первый вызываемый метод IExtractIcon - GetIconLocation(). Эта функция просматривает файл (чье имя было сохранено в IPersistFile::Load()) и возвращает пару имя файла/индекс, как это обсуждалось выше. Вот прототип GetIconLocation():

HRESULT IExtractIcon::GetIconLocation (
    UINT   uFlags,
    LPTSTR szIconFile,
    UINT   cchMax,
    int*   piIndex,
    UINT*  pwFlags );

Параметры:

GetIconLocation() заполняет параметры szIconFile и piIndex и возвращает S_OK. Можно также вернуть S_FALSE, если мы все-же решим, что не хотим обеспечивать указанную иконку. В этом случае Проводник установит иконку "неизвестный файл":


В параметре pwFlags Проводнику могут быть переданы следующие флаги:

В методе 1 GetIconLocation() получает размер файла и основываясь на нем возвращает индекс от 0 до 3 включительно. У этого метода есть один недостаток - нам нужно отслеживать ID ресурсов и быть уверенным, что они в правильном порядке. Наше расширение имеет только 4 иконки, так что эта бухгалтерия не сложна, но если иконок будет больше, или вы будете удалять и добавлять иконки в вашем проекте, вы должны будете внимательно следить за идентификаторами ваших ресурсов.

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

STDMETHODIMP CTxtIconShlExt::GetIconLocation (
    UINT   uFlags,
    LPTSTR szIconFile,
    UINT   cchMax,
    int*   piIndex,
    UINT*  pwFlags )
{
DWORD     dwFileSizeLo, dwFileSizeHi;
DWORDLONG ldwSize;
HANDLE    hFile;

    hFile = CreateFile ( m_szFilename, GENERIC_READ, FILE_SHARE_READ, NULL,
                         OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );

    if ( INVALID_HANDLE_VALUE == hFile )
        return S_FALSE;   // Сообщаем оболочке, что нужно использовать иконку по умолчанию
                                        
    dwFileSizeLo = GetFileSize ( hFile, &dwFileSizeHi );

    CloseHandle ( hFile );

    if ( (DWORD) -1 == dwFileSizeLo  &&  GetLastError() != NO_ERROR )
        return S_FALSE;   // Сообщаем оболочке, что нужно использовать иконку по умолчанию
                                        

    ldwSize = ((DWORDLONG) dwFileSizeHi)<<32 | dwFileSizeLo;

Далее мы получаем путь к нашей DLL, поскольку иконки находятся в ней и копируем его в буффер szIconFile.

TCHAR szModulePath[MAX_PATH];

    GetModuleFileName ( _Module.GetModuleInstance(), szModulePath, MAX_PATH );
    lstrcpyn ( szIconFile, szModulePath, cchMax );

На следующем шаге мы проверяем размер файла и записываем в piIndex индекс иконки, соответствующей этому размеру.

    if ( 0 == ldwSize )
        *piIndex = 0;
    else if ( ldwSize < 4096 )
        *piIndex = 1;
    else if ( ldwSize < 8192 )
        *piIndex = 2;
    else 
        *piIndex = 3;

В конце мы устанавливаем pwFlags в 0, чтобы получить от Проводника поведение по умолчанию. Это означает, что он проверит кэш иконок, чтобы определить, есть ли иконка szIconFile/piIndex в кэше, и, если да, то IExtractIcon::Extract() вызываться не будет. Мы возвращаем S_OK чтобы показать, что GetIconLocation() завершилась успешно.

    *pwFlags = 0;
    return S_OK;
}

Поскольку мы сообщили Проводнику, где найти иконку, наша реализация Extract() возвращает S_FALSE, чтобы он самостоятельно извлек иконку. Параметры Extract() я буду обсуждать в следующем разделе.

STDMETHODIMP CTxtIconShlExt::Extract (
    LPCTSTR pszFile, 
    UINT    nIconIndex, 
    HICON*  phiconLarge,
    HICON*  phiconSmall,
    UINT    nIconSize )
{
    return S_FALSE;   // Сообщаем оболочке что извлечение ей нужно сделать самостоятельно
}

И вот как выглядят наши иконки в действии:



Если изменить GetIconLocation() так чтобы pwFlags был установлен в GIL_SIMULATEDOC, иконки будут выглядеть так:



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

Извлечение методом 2

Метод 2 заключается в том, что наше расширение само извлекает иконки, игнорируя кэш иконок Проводника. При использовании этого метода функция IExtractIcon::Extract() всегда вызывается и именно она отвечает за загрузку иконок и возврат двух HICON Проводнику - для большой и для маленькой иконок. Достоинства этого метода заключаются в том, что вам не нужно волноваться об упорядочивании идентификаторов ресурсов ваших иконок. Оборотная сторона заключается в том, что не используется кэш иконок Проводника, что предположительно должно несколько замедлять загрузку списка файлов в окно Проводника, если вы зайдете в каталог со множеством текстовых файлов.

GetIconLocation() подобна реализованой в методе 1, но делает меньше работы, т.к. нужно только получить размер файла.

STDMETHODIMP CTxtIconShlExt::GetIconLocation (
    UINT   uFlags,
    LPTSTR szIconFile,
    UINT   cchMax,
    int*   piIndex,
    UINT*  pwFlags )
{
DWORD  dwFileSizeLo, dwFileSizeHi;
HANDLE hFile;

    hFile = CreateFile ( m_szFilename, GENERIC_READ, FILE_SHARE_READ, NULL,
                         OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );

    if ( INVALID_HANDLE_VALUE == hFile )
        return S_FALSE;   // Сообщаем оболочке, что нужно использовать иконку по умолчанию

    dwFileSizeLo = GetFileSize ( hFile, &dwFileSizeHi );

    CloseHandle ( hFile );

    if ( (DWORD) -1 == dwFileSizeLo  &&  GetLastError() != NO_ERROR )
        return S_FALSE;   // Сообщаем оболочке, что нужно использовать иконку по умолчанию

    m_ldwFileSize = ((DWORDLONG) dwFileSizeHi)<<32 | dwFileSizeLo;

Как только мы сохраним размер файла, устанавливаем pwFlags в GIL_DONTCACHE, чтобы сообщить Проводнику, что ему не нужно проверять кэш иконок. Этот флаг необходим, т.к. мы не заполнили szIconFile/piIndex и нам нужно, чтобы Проводник их проигнорировал.

Флаг GIL_NOTFILENAME также устанавливается, хотя в текущих версиях оболочки он не имеет эффекта. Согласно документации его цель - сообщить Проводнику, что мы не заполнили пару szIconFile/piIndex, но т.к. использование этого флага является бессмысленным (мы не дали Проводнику других данных для извлечения), то похоже, что он не проверяется Проводником. Но, так или иначе, это хорошая идея - выставить этот флаг на случай, если будущие версии оболочки будут проверять его наличие.

    *pwFlags = GIL_NOTFILENAME | GIL_DONTCACHE;
    return S_OK;
}

Теперь посмотрим внимательнее на Extract(). Вот ее прототип:

HRESULT IExtractIcon::Extract (
    LPCTSTR pszFile, 
    UINT    nIconIndex, 
    HICON*  phiconLarge,
    HICON*  phiconSmall,
    UINT    nIconSize );

Параметры:

В нашем расширении мы не заполняли пару имя/индекс в GetIconLocation(), так что можно проигнорировать pszFile и nIconIndex. Мы просто загружаем две иконки (какие - зависит от размера txt файла) и передаем их Проводнику.

STDMETHODIMP CTxtIconShlExt::Extract ( 
    LPCTSTR pszFile,
    UINT    nIconIndex, 
    HICON*  phiconLarge,
    HICON*  phiconSmall,
    UINT    nIconSize )
{
UINT uIconID;

    // Определить используемую иконку, исходя из размера файла
    if ( 0 == m_ldwFileSize )
        uIconID = IDI_ZERO_BYTES;
    else if ( m_ldwFileSize < 4096 )
        uIconID = IDI_UNDER_4K;
    else if ( m_ldwFileSize < 8192 )
        uIconID = IDI_UNDER_8K;
    else 
        uIconID = IDI_OVER_8K;

    // Загружаем иконки
    *phiconLarge = (HICON) LoadImage ( _Module.GetResourceInstance(), 
                                       MAKEINTRESOURCE(uIconID), IMAGE_ICON,
                                       32, 32, LR_DEFAULTCOLOR );

    *phiconSmall = (HICON) LoadImage ( _Module.GetResourceInstance(), 
                                       MAKEINTRESOURCE(uIconID), IMAGE_ICON,
                                       16, 16, LR_DEFAULTCOLOR );

    return S_OK;
}

И вот оно! Проводник показывает иконки, которые мы ему передали.

Еще одну вещь надо отметить при использовании метода 2 - установка флага GIL_SIMULATEDOC в GetIconLocation() не имеет никакого эффекта.

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

Хэндлер иконок регистрируется под ключом типа файлов, для которого он предназначен, в нашем случае это HKCR\txtfile. Как и для других расширений используется ключ ShellEx под txtfile. Затем следует ключ IconHandler. Значение умолчанию для этого ключа - GUID расширения. Заметьте, что может быть только один хэндлер иконок для каждого типа файлов. Мы также должны изменить значение ключа DefaultIcon на "%1", чтобы наш обработчик был задействован.

Вот RGS сценарий регистрации расширения:

HKCR
{
    NoRemove txtfile
    {
        NoRemove DefaultIcon = s '%%1'
        NoRemove ShellEx
        {
            ForceRemove IconHandler = s '{DF4F5AE4-E795-4C12-BC26-7726C27F71AE}'
        }
    }
}

Отметим, что для того, чтобы определить строку "%1", мы должны писать в RGS файле "%%1", т.к. % это спецсимвол, используемый для указания заменяемых параметров (например "%MODULE%").

Тот факт, что мы перезаписываем существующее значение DefaultIcon имеет большое значение. Как нам правильно деинсталировать свое расширение, если мы уничтожили старое значение DefaultIcon? Выход в том, что мы сохраняем значение DefaultIcon в DllRegisterServer() и восстанавливаем его в DllUnregisterServer(). Мы должны сделать это в процессе деинсталляции и оставить иконки текстовых файлов такими, какими они были до нашего вмешательства.

Посмотрите код функций регистрации/разрегистрации чтобы увидеть как они работают. Отметим, что мы делали сохранение перед вызовом ATL для выполнения RGS скрипта, поскольку если мы сделаем по другому, значение DefaultIcon будет перезаписано прежде, чем мы получить возможность сохранить его.

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

Если у вас есть идеи для будущих статей по расширениям оболочки, сообщите мне на email (и на CodeProject объявите тоже, чтобы другие могли обсудить ваше предложение).


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