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

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

Руководство по написанию расширения, добавляющего колонки в окно детального просмотра Проводника.

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

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

Введение
Детальный просмотр в Windows 2000
Использование AppWizard
Интерфейс расширения
Инициализация
Перечисление новых столбцов
Отображение данных в столбцах
Небольшое отступление - обработка тэгов ID3
Как это все выглядит?
Регистрация расширения оболочки
Еще одна полезная штучка - InfoTips

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

Введение

Часть Руководства Идиота, составленная по просьбам читателей продолжается! В этой части я возьмусь за тему добавления столбцов в окно детального просмотра списка файлов в Проводнике Windows 2000. Этот тип расширений не существует на NT4 или Win 9x, поэтому у вас должна быть установлена Win 2K, чтобы запустить демонстрационный проект.

Детальный просмотр в Windows 2000

Windows 2000 добавляет много настроечных опций к детальному просмотру в Проводнике. Возможны 37 различных столбцов. Вы можете включать и исключать столбцы двумя путями. Во-первых, в контекстном меню выводятся 8 столбцов, если вы щелкаете правой кнопкой мыши на заголовке столбца:


Если вы выбираете More..., Проводник отображает диалог, где вы можете выбирать уже из всех доступных столбцов:


Проводник позволяет нам добавить столбцы к списку в этом диалоге с помощью специального расширения. Однако расширения не могут добавлять столбцы к списку в контекстном меню.

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

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

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

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

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

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

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

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

struct __declspec(uuid("E8025004-1C42-11d2-BE2C-00A0C9A83DA1")) IColumnProvider;

/////////////////////////////////////////////////////////////////////////////
// CMP3ColExt

class ATL_NO_VTABLE CMP3ColExt : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CMP3ColExt, &CLSID_MP3ColExt>,
    public IMP3ColExt,
    public IColumnProvider
{
BEGIN_COM_MAP(CMP3ColExt)
    COM_INTERFACE_ENTRY(IMP3ColExt)
    COM_INTERFACE_ENTRY(IColumnProvider)
END_COM_MAP()

public:
    // IColumnProvider 
    STDMETHOD (Initialize)(LPCSHCOLUMNINIT psci) { return S_OK; }
    STDMETHOD (GetColumnInfo)(DWORD dwIndex, SHCOLUMNINFO* psci);
    STDMETHOD (GetItemData)(LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd, VARIANT* pvarData);
};

Обратите внимание, что объявление IColumnProvider находится перед объявлением класса. Этот порядок необходим для работы макроса COM_INTERFACE_ENTRY. Кто-то в Microsoft забыл определить UUID для IColumnProvider в comdef.h, поэтому нам необходимо сделать это объявление самим. В ATL есть макрос COM_INTERFACE_ENTRY_IID для использования в такой ситуации, когда для символа нет UUID, назначенного посредством __declspec(uuid()), однако, когда я использовал этот макрос, все заканчивалось передачей проводником недействительного указателя в IDispatch::GetTypeInfo(), и, в результате, аварией расширения.

Нам также необходимо сделать некоторые изменения в stdafx.h. Поскольку мы используем новые возможности Win 2000, мы должны определить несколько макросов, чтобы получить доступ к декларациям и прототипам, связанным с этими возможностями:

#define WINVER       0x0500     // W2K/98
#define _WIN32_WINNT 0x0500     // W2K
#define _WIN32_IE    0x0500     // IE 5+

Инициализация

У интерфейса IColumnProvider три метода. Первый из них - Initialize(), имеющий следующий прототип:

HRESULT IColumnProvider::Initialize ( LPCSHCOLUMNINIT psci );

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

Перечисление новых столбцов

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

HRESULT IColumnProvider::GetColumnInfo ( DWORD dwIndex, SHCOLUMNINFO* psci );

dwIndex - это начинающийся с 0 счетчик, который показывает, каким столбцом интересуется Проводник. Другой параметр - это указатель на структуру SHCOLUMNINFO, которую наше расширение заполняет параметрами столбца.

Первый член структуры SHCOLUMNINFO - другая структура - SHCOLUMNID. SHCOLUMNID - это пара GUID/DWORD, в которой GUID это "format ID", а DWORD - "property ID". Эта пара чисел однозначно идентифицирует любой столбец в системе. Расширение может повторно использовать уже имеющийся столбец (например, Автор), в этом случае format ID и property ID принимают предопределенные значения. Расширение, которое добавляет новые столбцы, может использовать свой собственный GUID для format ID (т.к. CLSID гарантированно уникален) и простой счетчик для property ID.

Наше расширение будет использовать оба метода. Мы будем повторно использовать столбцы Author (Автор), Title (Название), и Comments (Комментарии), и добавим еще три: MP3 Album (Альбом), MP3 Year (Год), и MP3 Genre (Жанр).

Вот начало нашего метода GetColumnInfo():

STDMETHODIMP CMP3ColExt::GetColumnInfo ( DWORD dwIndex, SHCOLUMNINFO* psci )
{
    // У нас 6 столбцов, поэтому, если dwIndex = 6 или больше, возвращаем S_FALSE
    // чтобы показать,  что все столбцы перечислены.
    if ( dwIndex > 5 )
        return S_FALSE;

Если dwIndex больше или равен 6, мы возвращаем S_FALSE, чтобы остановить перечисление, в противном случае мы заполняем структуру SHCOLUMNINFO. Для dwIndex от 0 до 2 мы возвращаем данные одного из наших новых столбцов. Для значений от 3 до 5 мы возвращаем данные одного из повторно используемых встроенных столбцов. Вот как мы задаем наш первый столбец, который показывает название альбома из соответствующего поля тэга ID3:

switch ( dwIndex )
        {
        case 0:    // MP3 Album - дополнительный столбец
            {
            psci->scid.fmtid = *_Module.pguidVer;   // Удобно использовать этот GUID
            psci->scid.pid   = 0;                   // Можно сделать любой ID, но самое простое - использовать номер столбца
            psci->vt         = VT_LPSTR;            // Возвращаем данные в виде строки
            psci->fmt        = LVCFMT_LEFT;         // Текст выровнен по левой границе
            psci->csFlags    = SHCOLSTATE_TYPE_STR; // Данные сортируются как строки
            psci->cChars     = 32;                  // Начальная ширина колонки в символах

            lstrcpynW ( psci->wszTitle, L"MP3 Album", MAX_COLUMN_NAME_LEN );
            lstrcpynW ( psci->wszDescription, L"Album name of an MP3", MAX_COLUMN_DESC_LEN );
            }
        break;

Мы используем GUID модуля нашего расширения как format ID, и номер столбца как property ID. Член структуры SHCOLUMNINIT vt индицирует, какой тип данных мы возвратим Проводнику. Значение VT_LPSTR указывает, что это будет строка в C-стиле. fmt может быть одной из LVCFMT_* констант, и указывает на тип выравнивания текста столбца. В данном случае текст будет выровнен по левой границе.

csFlags содержит несколько флажков для столбца. Однако, не все флажки, кажется, поддерживаются оболочкой. Ниже приведены существующие флажки и объяснение их эффектов:

cChars содержит заданную по умолчанию ширину для столбца в символах. Установите его равным максимуму из длины наименования столбца и самой длинной строки, которая может появится в столбце. Вы должны также добавить 2 или 3 к этому числу, чтобы гарантировать, к что ширина столбца достаточна, чтобы отобразить весь текст. (Если Вы не сделаете этого небольшого дополнения, заданная по умолчанию ширина столбца может быть недостаточно широка, и текст получится сокращенным.)

Заключительные два члена - строки в Unicode, которые содержат название столбца (текст, который показывается в заголовке) и описание столбца. В настоящее время описание не используется оболочкой, и пользователь никогда не видит его.

Столбцы #1 и #2 довольно похожи, однако столбец #1 иллюстрирует момент относительно типа данных и метода сортировки. Этот столбец показывает год. Вот код, который его определяет:

case 1:     // MP3 year - дополнительный столбец
            {
            psci->scid.fmtid = *_Module.pguidVer;   // Удобно использовать этот GUID
            psci->scid.pid   = 1;                   // Можно сделать любой ID, но самое простое - использовать номер столбца
            psci->vt         = VT_LPSTR;            // Возвращаем данные в виде строки
            psci->fmt        = LVCFMT_RIGHT;        // Текст выровнен по правой границе
            psci->csFlags    = SHCOLSTATE_TYPE_INT; // Данные сортируются как числа
            psci->cChars     = 6;                   // Начальная ширина колонки в символах

            lstrcpynW ( psci->wszTitle, L"MP3 Year", MAX_COLUMN_NAME_LEN );
            lstrcpynW ( psci->wszDescription, L"Year of an MP3", MAX_COLUMN_DESC_LEN );
            }
        break;

Заметьте, что vt = VT_LPSTR, т.е. мы передаем Проводнику строку, но csFlags = SHCOLSTATE_TYPE_INT, а это означает, что когда данные сортируются, это должно происходить в цифровой форме. Поскольку есть такая возможность, возвратить номер вместо строки, а тэг ID3 хранит год как строку, такое определение столбца избавляет нас от неприятностей, связанных с преобразованием года в цифровую форму.

Когда dwIndex имеет значение от 3 до 5, мы возвращаем информацию о встроенных столбцах повторного использования. Столбец 3 показывает поле Artist тэга ID3 в столбце Author:

case 3:     // MP3 artist - повторное использование встроенного столбца Author
            {
            psci->scid.fmtid = FMTID_SummaryInformation;  // Предопределенный FMTID
            psci->scid.pid   = 4;                   // Предопределенный - Author
            psci->vt         = VT_LPSTR;            // Возвращаем данные в виде строки
            psci->fmt        = LVCFMT_LEFT;         // Текст выровнен по левой границе
            psci->csFlags    = SHCOLSTATE_TYPE_STR; // Данные сортируются как строки
            psci->cChars     = 32;                  // Начальная ширина колонки в символах
            }
        break;

FMTID_SummaryInformation - предопределенный символ, а ID поля Author (4) приведено в MSDN. См. страницу "The Summary Information Property Set". При повторном использовании столбца мы не возвращаем заголовок или описание, так как оболочка сама позаботится об этом.

Наконец, после блока оператора switch мы возвращаем S_OK, чтобы показать, что мы заполнили структуру SHCOLUMNINFO.

Отображение данных в столбцах

Последний метод интерфейса IColumnProvider, GetItemData(), который Проводник вызывает, чтобы показать в столбцах данные о файле, имеет следующий прототип:

HRESULT IColumnProvider::GetItemData (
    LPCSHCOLUMNID   pscid,
    LPCSHCOLUMNDATA pscd,
    VARIANT*        pvarData );

Структура SHCOLUMNID показывает, данные какого столбца нужны Проводнику. Она содержит ту же самую информацию, которую мы дали Проводнику в GetColumnInfo(). Структура SHCOLUMNDATA содержит подробности о файле или каталоге, включая его путь. Мы можем использовать эту информацию, чтобы решить, хотим ли мы предоставить какие-либо данные для этого файла или каталога. pvarData - это указатель на VARIANT, в котором мы сохраним данные для показа их Проводником. VARIANT - это воплощение в C "свободного" типа переменных, существующего в VB и других скриптовых языках. Переменная VARIANT состоит фактически из двух частей - типа и данных. В ATL есть удобный класс CComVariant, делающий все черную работу по инициализации и установке переменных типа VARIANT. Я продемонстрирую его использование ниже.

Небольшое отступление - обработка тэгов ID3

Сейчас самое время показать, как наше расширение будет читать и сохранять информацию из тэгов ID3. Тэг ID3v1 - это структура фиксированной длины, добавленная в конец файла MP3, которая выглядит примерно так:

struct CID3v1Tag
{
    char szTag[3];      // Всегда 'T','A','G'
    char szTitle[30];
    char szArtist[30];
    char szAlbum[30];
    char szYear[4];
    char szComment[30];
    char byGenre;
};

Все поля - простые символьные, строки не обязательно заканчиваются нулем и требуют немного специальной обработки. Первое поле, szTag, содержит символы "TAG", идентифицирующие тэг ID3. byGenre - это номер, который идентифицирует жанр песни. (Существует предопределенный список жанров и их числовых идентификаторов, доступный на ID3.org.)

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

#include <string>
#include <list>
typedef std::basic_string<TCHAR> tstring;   // a TCHAR string

struct CID3CacheEntry
{
    tstring   sFilename;
    CID3v1Tag rTag;
};

typedef std::list<CID3CacheEntry> list_ID3Cache;

Объект CID3CacheEntry содержит имя файла и тэг ID3, сохраненный в этом файле. list_ID3Cache - это связанный список структур CID3CacheEntry.

OK, вернемся к нашему расширению. Вот начало нашей функции GetItemData(). Сначала мы проверяем структуру SHCOLUMNID, чтобы удостовериться, что нас вызывают для одного из наших собственных столбцов.

#include <atlconv.h>

STDMETHODIMP CMP3ColExt::GetItemData (
    LPCSHCOLUMNID   pscid,
    LPCSHCOLUMNDATA pscd,
    VARIANT*        pvarData )
{
USES_CONVERSION;
LPCTSTR   szFilename = OLE2CT(pscd->wszFile);
char      szField[31];
TCHAR     szDisplayStr[31];
bool      bUsingBuiltinCol = false;
CID3v1Tag rTag;
bool      bCacheHit = false;

    // Проверить, что format id и номер столбца - те, что мы ожидаем.
    if ( pscid->fmtid == *_Module.pguidVer )
        {
        if ( pscid->pid > 2 )
            return S_FALSE;
        }

Если format ID - это наш собственый GUID, property ID должен быть 0, 1, или 2, как и те ID, что мы мы использовали в GetColumnInfo(). Если по каким либо причинам ID выходит за эти рамки мы возвращаем S_FALSE, чтобы сообщить оболочке, что мы не имеем никаких данных, и столбец должен быть пустым.

Далее, мы сравниваем format ID с FMTID_SummaryInformation и проверяем идентификатор свойства property ID, чтобы посмотреть, является ли оно свойством, которое мы предоставляем.

else if ( pscid->fmtid == FMTID_SummaryInformation )
        {
        bUsingBuiltinCol = true;

        if ( pscid->pid != 2  &&  pscid->pid != 4  &&  pscid->pid != 6 )
            return S_FALSE;
        }
    else
        {
        return S_FALSE;
        }

Далее, мы проверяем атрубуты файла, имя которого нам было передано. Если это каталог или файл "в оффлайне" (т.е. был перемещен на другой носитель, например ленту), мы спокойно вываливаемся. Также мы проверяем расширение файла и возвращаем S_FALSE, если оно не MP3.

    // Если нас вызывают с каталогом (вместо файла) мы можем
    // выйти немедленно.
    // Также выходим,  если файл "в оффлайне" (например сохранен на ленте,  или 
    // другом носителе).  
    if ( pscd->dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_OFFLINE) )
        return S_FALSE;

    // Проверяем расширение файла. Если не MP3,  мы можем выйти.
    if ( 0 != lstrcmpiW ( pscd->pwszExt, L".mp3" ))
        return S_FALSE;

В этом месте мы решили, что хотим работать с файлом. Начинаем использовать наш кэш тэгов ID3. Документация MSDN говорит, что оболочка группирует запросы к GetItemData() по имени файла, это означает, что вызывы GetItemData() с одним и тем же именем файла будут идти подряд. Мы можем воспользоваться этим поведением и кэшировать тэг ID3 для отдельного файла, так что нам не придется читать тэг из этого файла снова при следующих запросах.

Сначала мы просматриваем наш кэш (сохраняемый как переменная-член m_ID3Cache), сравнивая имя файла в кэше с именем, переданным в функцию. Если мы находим имя в нашем кэше, мы используем связанный с ним тэг ID3.

    
// Искать имя файла в нашем кэше.
list_ID3Cache::const_iterator it, itEnd;

    for ( it = m_ID3Cache.begin(), itEnd = m_ID3Cache.end();
          !bCacheHit && it != itEnd; it++ )
        {
        if ( 0 == lstrcmpi ( szFilename, it->sFilename.c_str() ))
            {
            CopyMemory ( &rTag, &it->rTag, sizeof(CID3v1Tag) );
            bCacheHit = true;
            }
        }

Если bCacheHit = false после этого цикла, мы должны читать файл и смотреть, имеет ли он тэг ID3. Вспомогательная функция ReadTagFromFile() делает грязную работу по чтению последних 128 байтов файла и возвращает TRUE в случае успеха или FALSE если произошла ошибка чтения файла. Заметьте, что ReadTagFromFile() возвращает последние 128 байт, независимо от того, являются ли они действительно тэгом ID3.

// Если тэга файла нет в нашем кэше, читаем тэг из файла.
    if ( !bCacheHit )
        {
        if ( !ReadTagFromFile ( szFilename, &rTag ))
            return S_FALSE;

Итак, теперь у нас есть тэг ID3. Мы проверяем размер нашего кэша, и если он содержит 5 вхождений, удаляем самое старое, чтобы освободить место для нового. (5 - это произвольный маленький номер.) Мы создаем новый объект CID3CacheEntry и добавляем его в список.

        // Мы храним тэги только для последних 5 кэшируемых файлов - удаляем самое старое
        // вхождение, если кэш имеет больший чем 4 вхождения.
        while ( m_ID3Cache.size() > 4 )
            {
            m_ID3Cache.pop_back();
            }

        // Добавим новый тэг ID3 к нашему кэшу.
        CID3CacheEntry entry;

        entry.sFilename = szFilename;
        CopyMemory ( &entry.rTag, &rTag, sizeof(CID3v1Tag) );
        
        m_ID3Cache.push_front ( entry );
        }   // end if(!bCacheHit)

Наш следующий шаг - проверить первые три байта сигнатуры, чтобы определить, ID3-тэг это или нет. Если нет, мы можем вернуть S_FALSE немедленно.

// Проверяем, имеем ли мы действительно тэг ID3, ища сигнатуру.
    if ( 0 != StrCmpNA ( rTag.szTag, "TAG", 3 ))
        return S_FALSE;

Затем мы читаем из тэга ID3 поле, которое соответствует свойству, затребованному оболочкой. Для этого нужно только протестировать идентификаторы свойств. Например, для поля Title:

    // Форматировать строку подробностей.
    if ( bUsingBuiltinCol )
        {
        switch ( pscid->pid )
            {
            case 2:                     // заголовок песни
                CopyMemory ( szField, rTag.szTitle, countof(rTag.szTitle) );
                szField[30] = '\0';
            break;
        ...
        }

Обратите внимание, что наш буфер szField - длиной 31 символ, что на 1 символ длинее, чем соответствующее поле ID3v1. Таким образом мы гарантируем, что строка всегда будет должным образом закончена нулевым символом в конце. Флажок bUsingBuiltinCol был установлен ранее, когда мы проверили пару FMTID/PID. Этот флажок нам нужен, т.к. одного PID не достаточно, чтобы идентифицировать столбец - столбцы Title и MP3 Genre оба имеют PID=2.

В этом месте szField содержит строку, которую мы считали из тэга ID3. Редактор ID3 тэгов программы WinAmp дополняет строки пробелами вместо нулевых символов, поэтому мы правим строки, удаляя конечные пробелы:

    // WinAmp дополняет строки с пробелами вместо нулей, так что удаляем любые 
    // завершающие строку пробелы.
    StrTrimA ( szField, " " );

И наконец мы создаем объект CComVariant и сохраняем в нем строку szDisplayStr. Мы вызываем CComVariant::Detach(), чтобы скопировать данные из CComVariant в VARIANT, предоставленный Проводником.

// Создать VARIANT со строкой подробностей, и возвратить его назад оболочке.
CComVariant vData ( szField );

    vData.Detach ( pvarData );

    return S_OK;
}

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

Наши новые столбцы появляются в конце списка в диалоге "Настройка cтолбцов":


Вот как выглядят столбцы. Файлы сортируются по заполненному нами столбцу Author.


Регистрация расширения оболочки

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

HKCR
{
    NoRemove Folder
    {
        NoRemove Shellex
        {
            NoRemove ColumnHandlers
            {
                ForceRemove {AC146E80-3679-4BCA-9BE4-E36512573E6C} = s 'MP3 ID3v1 viewer column ext'
            }
        }
    }
}

Еще одна полезная штучка - InfoTips

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

HKCR
{
    NoRemove .mp3
    {
        val InfoTip = s 'prop:Type;Author;Title;Comment;{AC146E80-3679-4BCA-9BE4-E36512573E6C},0;{AC146E80-3679-4BCA-9BE4-E36512573E6C},1;{AC146E80-3679-4BCA-9BE4-E36512573E6C},2;Size'
    }
}

Обратите внимание, что поля Author, Title, и Comment указаны в строке prop:. Когда Вы задерживаете курсор мыши над MP3 файлом, Проводник вызывает наше расширение, чтобы получить и показать строки из этих полей. Документация в MSDN утверждает, что наши заказные поля также могут появляться в InfoTips (именно поэтому наш GUID и идентификаторы свойств появились в вышеприведенной строке), однако, в Win2K это не работает. Только встроенные свойства появляются в InfoTips. Вот как выглядит InfoTip:



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