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

Расширение оболочки для добавления новых страниц в набор свойств файлов

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

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

Обработчик набора свойств
Использование AppWizard
Интерфейс инициализации
Добавление страниц свойств
Неприятная ситуация с периодом жизни объектов
Функции обратного вызова страницы свойств
Обработчики сообщений страницы свойств
Регистрация расширения
Продолжение следует...

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

В пятой части руководства мы осмелимся заглянуть в мир окон свойств. Когда Вы выводите свойства объектов файловой системы проводник показывает их на странице "Общие". Оболочка позволяет нам расширить окно набора свойств, используя тип расширения, который называется обработчиком набора свойств (property sheet handler).

Эта статья подразумевает, что у Вас есть основные знания по расширениям оболочки и Вы знакомы с классами-контейнерами STL. Если Вам необходимо освежить знания по STL, Вы должны прочитать часть II, так как те же методы будут использоваться в этой статье. Код использует несколько функций из shlwapi.dll версии 4.71, так что Вам нужен будет IE4 или выше (Active Desktop устанавливать не обязательно).

Обработчик набора свойств

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

Эта статья представляет расширение, которое позволит Вам изменять время создания, модификации и последнего доступа к файлу прямо из его окна свойств, оперируя прямыми вызовами SDK без использования MFC или ATL/WTL. Я не пробовал использовать MFC или WTL страницы свойств в расширении. Поступая так можно обмануться, потому что оболочка ожидает получения дескриптора к набору свойств (HPROPSHEETPAGE), а MFC скрывает эту деталь в реализации CPropertyPage. (Я не знаю как реализовать это с помощью WTL.)

Если Вы посмотрите свойства для файлов *.URL (ярлычки к internet страницам), Вы можете увидеть обработчик набора свойств в действии. Вкладка "CodeProject" является наглядным примером расширения из этой статьи. Вкладка "Web Document" показывает расширение, инсталлированное IE.


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

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

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

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

Поскольку обработчик набора свойств оперирует всеми выделенными файлами сразу, в качестве интерфейса инициализации используется IShellExtInit. Нам необходимо добавить IShellExtInit к списку интерфейсов, которые реализует CFileTimeShlExt. Инструкция, как это сделать, содержится в части IV. Классу также необходим список строк, в котором будут храниться имена выделенных файлов.

typedef std::list<std::basic_string<TCHAR> > string_list;

protected:
    // IFileTimeShlExt
    string_list m_lsFiles;

Метод Initialize() делает тоже, что и в части II - читает имена выделенных файлов и сохраняет их в списке строк. Вот начало функции:

HRESULT CFileTimeShlExt::Initialize (
    LPCITEMIDLIST pidlFolder,
    LPDATAOBJECT  pDataObj,
    HKEY          hProgID )
{
TCHAR     szFile [MAX_PATH];
UINT      uNumFiles;
HDROP     hdrop;
FORMATETC etc = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM stg;
INITCOMMONCONTROLSEX iccex = { sizeof(INITCOMMONCONTROLSEX), ICC_DATE_CLASSES };

    // Инициализация общих элементов управления
    InitCommonControlsEx ( &iccex );

Мы инициализируем общие элементы управления, потому что наша вкладка будет использовать элемент управления для выбора даты и времени (date/time picker (DTP)). Далее мы выполняем всю черновую работу с помощью интерфейса IDataObject и получаем дескриптор HDROP для перечисления выделенных файлов.

// Читаем список пунктов из объекта данных. Они сохранены в форме HDROP,
// поэтому, просто получаем HDROP и используем drag 'n' drop API
if ( FAILED( pDataObj->GetData ( &etc, &stg )))
        return E_INVALIDARG;

    // Получаем HDROP
    hdrop = (HDROP) GlobalLock ( stg.hGlobal );

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

    // Определяем, сколько файлов участвует в операции
    uNumFiles = DragQueryFile ( hdrop, 0xFFFFFFFF, NULL, 0 );

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

    for ( UINT uFile = 0; uFile < uNumFiles; uFile++ )
        {
        // Получаем следующее имя файла
        if ( 0 == DragQueryFile ( hdrop, uFile, szFile, MAX_PATH ))
            continue;

        // Пропускаем каталоги. Мы могли бы использовать дескрипторы каталогов, т.к. у них       
        // есть время и дата создания, но в этом примере я предпочитаю этого не делать
        if ( PathIsDirectory ( szFile ))
            continue;

        // Добавляем имя файла к нашему списку
        m_lsFiles.push_back ( szFile );
        }   // end for
    // Освобождаем ресурсы
    GlobalUnlock ( stg.hGlobal );
    ReleaseStgMedium ( &stg );

Вот что здесь нового: существует предел числа страниц, которые может иметь набор свойств. Он определен как константа MAXPROPPAGES в prsht.h. Каждый файл получает свою собственную страницу, и, если наш список содержит больше файлов, чем MAXPROPPAGES, остальные придется исключить, так как MAXPROPPAGES это предел. (Хотя, если MAXPROPPAGES=100, то набор свойств все равно не сможет показать все множество вкладок, максимум - 34).

// Проверим, сколько файлов было отмечено, если больше чем
// MAXPROPPAGES, укоротим список
if ( m_lsFiles.size() > MAXPROPPAGES )
        {
        m_lsFiles.resize ( MAXPROPPAGES );
        }

// Если мы нашли какие-нибудь файлы, пригодные для работы, вернем S_OK. 
// В противном случае вернем E_FAIL
return ( m_lsFiles.size() > 0 ) ? S_OK : E_FAIL;
}

Добавление страниц свойств

Если Initialize() возвращает S_OK, проводник запрашивает новый интерфейс - IShellPropSheetExt. IShellPropSheetExt совсем прост, с единственным методом, требующим реализации. Чтобы добавить IShellPropSheetExt к нашему классу, откройте FileTimeShlExt.h и добавьте выделенные строки:

class ATL_NO_VTABLE CFileTimeShlExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CFileTimeShlExt, &CLSID_FileTimeShlExt>,
    public IDispatchImpl<IFileTimeShlExt, &IID_IFileTimeShlExt, &LIBID_FILETIMELib>,
    public IShellExtInit,
    
public IShellPropSheetExt
{
BEGIN_COM_MAP(CFileTimeShlExt)
    COM_INTERFACE_ENTRY(IFileTimeShlExt)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY(IShellExtInit)
    COM_INTERFACE_ENTRY(IShellPropSheetExt)
END_COM_MAP()


public:
    // IShellPropSheetExt
    STDMETHOD(AddPages)(LPFNADDPROPSHEETPAGE, LPARAM);
    STDMETHOD(ReplacePage)(UINT, LPFNADDPROPSHEETPAGE, LPARAM) { return E_NOTIMPL; }

AddPages() - метод, который мы реализуем. Расширения используют ReplacePage() только для того, чтобы заменять вкладки в элементах панели управления. Поэтому нет необходимости здесь его реализовывать. Проводник вызывает нашу функцию AddPages(), чтобы мы могли добавить страницы к набору свойств, установленному проводником.

Параметры функции AddPages() - указатель на функцию и LPARAM используются только оболочкой. lpfnAddPageProc указывает на функцию оболочки, которую мы вызываем, чтобы добавить вкладки. LPARAM - загадочная величина, но она важна только для оболочки. Мы ничего с ней не делаем, просто передаем ее обратно в функцию lpfnAddPageProc.

HRESULT CFileTimeShlExt::AddPages (
    LPFNADDPROPSHEETPAGE lpfnAddPageProc,
    LPARAM lParam )
{
PROPSHEETPAGE  psp;
HPROPSHEETPAGE hPage;
TCHAR          szPageTitle [MAX_PATH];
string_list::const_iterator it, itEnd;
                                  
    for ( it = m_lsFiles.begin(), itEnd = m_lsFiles.end();
          it != itEnd;
          it++ )
        {
        // 'it' - указатель на следующее имя файла.  Создаем копию строки,
        // которой будет владеть страница
        LPCTSTR szFile = _tcsdup ( it->c_str() );

В первую очередь мы должны создать копию имени файла. Причину я объясню ниже.

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

        // Удаляем путь и расширение из имени файла - это будет заголовок страницы.
        // Укорачиваем имя до 24 символов.
        lstrcpy ( szPageTitle, it->c_str() );
        PathStripPath ( szPageTitle );
        PathRemoveExtension ( szPageTitle );
        szPageTitle[24] = '\0';

Т.к. мы используем непосредственно SDK, для создания страницы свойств придется испачкать ручки структурой PROPSHEETPAGE. Вот установки для параметров структуры:

        psp.dwSize      = sizeof(PROPSHEETPAGE);
        psp.dwFlags     = PSP_USEREFPARENT | PSP_USETITLE | PSP_DEFAULT |
                            PSP_USEICONID | PSP_USECALLBACK;
        psp.hInstance   = _Module.GetModuleInstance();
        psp.pszTemplate = MAKEINTRESOURCE(IDD_FILETIME_PROPPAGE);
        psp.pszIcon     = MAKEINTRESOURCE(IDI_ICON);
        psp.pszTitle    = szPageTitle;
        psp.pfnDlgProc  = PropPageDlgProc;
        psp.lParam      = (LPARAM) szFile;
        psp.pfnCallback = PropPageCallbackProc;
        psp.pcRefParent = (UINT*) &_Module.m_nLockCnt;

Теперь несколько деталей, на которые нужно обратить внимание, чтобы расширение работало корректно:

Установив параметры этой структуры, мы обращаемся к API, для создания страницы свойств.

hPage = CreatePropertySheetPage ( &psp );

Если все отлично, мы вызываем callback функцию оболочки, которая добавляет вновь созданную страницу к набору свойств. Функция возвращает BOOL индикатор успеха или неудачи. Если это неудача, мы уничтожаем страницу.

if ( NULL != hPage )
            {
            // Вызываем callback функцию оболочки, чтобы добавить страницу к набору свойств
            if ( !lpfnAddPageProc ( hPage, lParam ))
                {
                DestroyPropertySheetPage ( hPage );
                }
            }
        }   // end for
return S_OK;
}

Неприятная ситуация с периодом жизни объектов

Настало время выполнить мое обещание - объяснить, зачем нужен дубликат строки, содержащей имя файла. Дубликат необходим, потому что после возврата из AddPages() оболочка освобождает свой интерфейс IShellPropSheetExt, который, в свою очередь, разрушает объект CFileTimeShlExt. Это означает, что оконная процедура страницы свойств не сможет получить доступ к элементу m_lsFiles класса CFileTimeShlExt.

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

psp.lParam = (LPARAM) szFile;

очень важна. Она сохраняет указатель в структуре PROPSHEETPAGE и делает его доступным в оконной процедуре страницы свойств.

Функции обратного вызова страницы свойств

Теперь обратимся к странице свойств. Вот как она выглядит:


Держите в уме эту картинку, пока читаете объяснения о том, как она работает.

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

Страница имеет две функции обратного вызова и два обработчика сообщений. Их прототипы идут вначале файла FileTimeShlExt.cpp:

BOOL CALLBACK PropPageDlgProc ( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam );
UINT CALLBACK PropPageCallbackProc ( HWND hwnd, UINT uMsg, LPPROPSHEETPAGE ppsp );
BOOL OnInitDialog ( HWND hwnd, LPARAM lParam );
BOOL OnApply ( HWND hwnd, PSHNOTIFY* phdr );

Оконная процедура приятно проста. Она обрабатывает три сообщения: WM_INITDIALOG, PSN_APPLY и DTN_DATETIMECHANGE. Вот часть WM_INITDIALOG:

BOOL CALLBACK PropPageDlgProc ( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
BOOL bRet = FALSE;

    switch ( uMsg )
        {
        case WM_INITDIALOG:
            bRet = OnInitDialog ( hwnd, lParam );
        break;

OnInitDialog() я объясню позже. Дальше идет PSN_APPLY, которое посылается, если пользователь щелкнет OK или Apply.

case WM_NOTIFY:
            {
            NMHDR* phdr = (NMHDR*) lParam;

            switch ( phdr->code )
                {
                case PSN_APPLY:
                    bRet = OnApply ( hwnd, (PSHNOTIFY*) phdr );
                break;

И, в конце, DTN_DATETIMECHANGE. Это самое простое, мы просто делаем доступной кнопку Apply посылкой сообщения набору свойств (который является родительским окном для нашей страницы).

case DTN_DATETIMECHANGE:
            // Если пользователь изменил содержимое DTP, разрешаем кнопку Apply
            SendMessage ( GetParent(hwnd), PSM_CHANGED, (WPARAM) hwnd, 0 );
                break;
                }
            }
        break;
        }

    return bRet;
}

Пока все хорошо. Другая callback функция вызывается, когда вкладка создается или уничтожается. Нас интересует последний случай, так как именно тогда мы можем освободить дубликат строки, который был создан раньше, в AddPages(). Параметр ppsp указывает на структуру PROPSHEETPAGE, которую мы использовали для создания вкладки. Ее элемент lParam все еще указывает на дубликат строки, который должен быть освобожден.

UINT CALLBACK PropPageCallbackProc ( HWND hwnd, UINT uMsg, LPPROPSHEETPAGE ppsp )
{
    if ( PSPCB_RELEASE == uMsg )
        {
        free ( (void*) ppsp->lParam );
        }

    return
1;
}

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

Обработчики сообщений страницы свойств

Много важного происходит в OnInitDialog(). Параметр lParam опять указывает на структуру PROPSHEETPAGE, использовавшуюся при создании вкладки. Ее член lParam указывает на это вездесущее "имя файла". Поскольку нам необходимо иметь доступ к имени файла в функции OnApply(), мы сохраняем указатель, используя SetWindowLong().

BOOL OnInitDialog ( HWND hwnd, LPARAM lParam )
{        
PROPSHEETPAGE*  ppsp = (PROPSHEETPAGE*) lParam;
LPCTSTR         szFile = (LPCTSTR) ppsp->lParam;
HANDLE          hFind;
WIN32_FIND_DATA rFind;

    // Сохраним имя файла в данных этого окна для дальнейшего использования
    SetWindowLong ( hwnd, GWL_USERDATA, (LONG) szFile );

Затем мы получаем времена создания, модификации и доступа, используя FindFirstFile(). Если это выполнено успешно, DTP инициализируются правильными данными.

hFind = FindFirstFile ( szFile, &rFind );

    if ( INVALID_HANDLE_VALUE != hFind )
        {
        // Инициализируем DTP
        SetCombinedDateTime ( hwnd, IDC_MODIFIED_DATE, IDC_MODIFIED_TIME,
                              &rFind.ftLastWriteTime );

        SetCombinedDateTime ( hwnd, IDC_ACCESSED_DATE, 0,
                              &rFind.ftLastAccessTime );

        SetCombinedDateTime ( hwnd, IDC_CREATED_DATE, IDC_CREATED_TIME,
                              &rFind.ftCreationTime );

        FindClose ( hFind );
        }

SetCombinedDateTime() - прикладная функция, которая устанавливает содержимое DTP. Код можно найти в конце FileTimeShlExt.cpp.

И в дополнение - полный путь к файлу показывается в надписи вверху страницы.

    PathSetDlgItemPath ( hwnd, IDC_FILENAME, szFile );
    
    return FALSE;      // Используем встроенную обработку фокуса
}

Обработчик OnApply() напротив, читает информацию из DTP и устанавливает времена создания, модификации и доступа к файлам. Первый шаг - получить при помощи GetWindowLong() указатель на имя файла и открыть файл для записи.

BOOL OnApply ( HWND hwnd, PSHNOTIFY* phdr )
{
LPCTSTR  szFile = (LPCTSTR) GetWindowLong ( hwnd, GWL_USERDATA );
HANDLE   hFile;
FILETIME ftModified, ftAccessed, ftCreated;

    // Открываем файл
    hFile = CreateFile ( szFile, GENERIC_WRITE, FILE_SHARE_READ, NULL,
                         OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );

Если мы смогли открыть файл, считываем из DTP и пишем времена обратно в файл. GetCombinedDateTime() это пара для SetCombinedDateTime().

if ( INVALID_HANDLE_VALUE != hFile )
        {
        // Извлекаем дату и время из DTP
        GetCombinedDateTime ( hwnd, IDC_MODIFIED_DATE, IDC_MODIFIED_TIME,
                              &ftModified );

        GetCombinedDateTime ( hwnd, IDC_ACCESSED_DATE, 0,
                              &ftAccessed );

        GetCombinedDateTime ( hwnd, IDC_CREATED_DATE, IDC_CREATED_TIME,
                              &ftCreated );

        // Изменяем время создания, доступа и последней модификации файла
        SetFileTime ( hFile, &ftCreated, &ftAccessed, &ftModified );
        CloseHandle ( hFile );
        }
    else
        {
        // <<Обработка ошибок опущена>>
        }

     // Возвращаем PSNRET_NOERROR, чтобы можно было закрыть набор свойств, если пользователь нажал OK.
    SetWindowLong ( hwnd, DWL_MSGRESULT, PSNRET_NOERROR );
    return TRUE;
}

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

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

HKCR
{
    NoRemove *
    {
        NoRemove shellex
        {
            NoRemove PropertySheetHandlers
            {
                {3FCEF010-09A4-11D4-8D3B-D12F9D3D8B02}
            }
        }
    }
}

Вы могли заметить, что GUID расширения сохранен здесь как имя ключа регистрации вместо строкового значения. Документация и книги, которые я просматривал, противоречат друг другу по поводу правильного обозначения, хотя во время моего беглого испытания оба варианта работали. Я решил идти по пути Dino Esposito ("Visual C++ Windows Shell Programming") и поместил GUID как имя ключа.

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

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

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


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