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

Руководство по использованию owner-drawn меню в расширениях контекстных меню и по созданию расширения контекстного меню, которое отзывается на правый щелчок на фоне окна каталога.

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

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

Введение
Расширение 1 - Пункты меню owner-drawn.
Использование AppWizard
Интерфейс инициализации.
Взаимодействие с контекстным меню
Модифицирование контекстного меню.
Отображение всплывающей подсказки в строке состояния.
Выполнение выбора пользователя.
Рисование пункта меню.
Обработка WM_MEASUREITEM
Обработка WM_DRAWITEM
Регистрация расширения оболочки
Расширение 2 - Обработка щелчка правой кнопкой мыши на фоне окна каталога.
Отличия в IShellExtInit::Initialize()
Отличия в регистрации.
Продолжение следует...?

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

Введение

Руководство Идиота возвращается с новой главой! В этой части я отвечу на некоторые вопросы читателей и затрону две темы: использование owner-drawn (собственноручно-нарисованного) меню в расширениях контекстных меню и создание расширений контекстных меню, которые загружаются, когда пользователь щелкает правой кнопкой мыши на фоне окна каталога. Вы должны были уже прочесть Части I и II, поэтому знаете основные понятия о расширениях контекстных меню.

Расширение 1 - Пункты меню owner-drawn.

В этой части я рассмотрю дополнительную работу, необходимую для реализации owner-drawn меню.

Поскольку это расширение реализует owner-drawn меню, нам придется иметь дело с графикой. Я решил скопировать что-нибудь из того, что делает программа PicaView: Показать эскиз графического файла в контекстном меню. Вот как выглядят пункты меню PicaView:


Это расширение создаст эскиз файла ВМР. Чтобы упростить код я не буду беспокоиться о пропорциях и сочетании цветов. Исправление этих недочетов я оставляю в качестве домашнего задания для читателя. ;)

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

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

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

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

Как и для предыдущих расширений контекстных меню, это расширение реализует интерфейс IShellExtInit. Чтобы добавить IShellExtInit к нашему COM объекту, откройте BmpCtxMenuExt.h и добавьте выделенные строки. Несколько переменных, которые здесь указаны, мы будем использовать позже, в процессе рисования пункта меню.

#include <comdef.h>

/////////////////////////////////////////////////////////////////////////////
// CBmpCtxMenuExt

class ATL_NO_VTABLE CBmpCtxMenuExt : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CBmpCtxMenuExt, &CLSID_BmpCtxMenuExt>,
    public IDispatchImpl<IBmpCtxMenuExt, &IID_IBmpCtxMenuExt, &LIBID_BMPVIEWEREXTLib>,
    public IShellExtInit
{
BEGIN_COM_MAP(CBmpCtxMenuExt)
    COM_INTERFACE_ENTRY(IBmpCtxMenuExt)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY(IShellExtInit)
END_COM_MAP()

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

protected:
    TCHAR   m_szFile[MAX_PATH];
    CBitmap m_bmp;
    UINT    m_uOurItemID;

    LONG m_lItemWidth, m_lItemHeight;
    LONG m_lBmpWidth, m_lBmpHeight;

    static const LONG m_lMaxThumbnailSize;
    static const LONG m_l3DBorderWidth;
    static const LONG m_lMenuItemSpacing;
    static const LONG m_lTotalBorderSpace;

    // Helper functions for handling the menu-related messages.
    STDMETHOD(MenuMessageHandler)(UINT, WPARAM, LPARAM, LRESULT*);
    STDMETHOD(OnMeasureItem)(MEASUREITEMSTRUCT*, LRESULT*);
    STDMETHOD(OnDrawItem)(DRAWITEMSTRUCT*, LRESULT*);
};

Что нам предстоит сделать в IShellExtInit::Initialize(), так это получить имя выбранного файла, и если это BMP-файл, создать его эскиз.

В файл BmpCtxMenuExt.cpp добавим объявления для следующих статических переменных. Они управляют визуальными аспектами эскиза и его границами. Не стесняясь измените эти значения, и проследите, как это отразится на конечном результате в меню.

const LONG CBmpCtxMenuExt::m_lMaxThumbnailSize = 64;
const LONG CBmpCtxMenuExt::m_l3DBorderWidth    = 2;
const LONG CBmpCtxMenuExt::m_lMenuItemSpacing  = 4;
const LONG CBmpCtxMenuExt::m_lTotalBorderSpace = 2*(m_lMenuItemSpacing+m_l3DBorderWidth);

Эти константы означают следующее:

Также добавим определение функции IShellExtInit::Initialize():

STDMETHODIMP CBmpCtxMenuExt::Initialize (
    LPCITEMIDLIST pidlFolder,
    LPDATAOBJECT  pDO,
    HKEY          hkeyProgID )
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState());

    COleDataObject dataobj;
    HGLOBAL        hglobal;
    HDROP          hdrop;
    bool           bOK = false;

    dataobj.Attach ( pDO, FALSE );      // FALSE = don't release IDataObject interface when destroyed

    // Get the first selected file name.  I'll keep this simple and just check
    // the first name to see if it's a .BMP.
    hglobal = dataobj.GetGlobalData ( CF_HDROP );

    if ( NULL == hglobal )
        return E_INVALIDARG;

    hdrop = (HDROP) GlobalLock ( hglobal );

    if ( NULL == hdrop )
        return E_INVALIDARG;

    // Get the name of the first selected file.
    if ( DragQueryFile ( hdrop, 0, m_szFile, MAX_PATH ))
        {
        // Is its extension .BMP?
        if ( PathMatchSpec ( m_szFile, _T("*.bmp") ))
            {
            // Load the bitmap and attach our CBitmap object to it.
            HBITMAP hbm = (HBITMAP) LoadImage ( NULL, m_szFile, IMAGE_BITMAP, 0, 0, 
                                                LR_LOADFROMFILE );

            if ( NULL != hbm )
                {
                // We loaded the bitmap, so attach the CBitmap to it.
                m_bmp.Attach ( hbm );
                bOK = true;
                }
            }
        }

    GlobalUnlock ( hglobal );

    return bOK ? S_OK : E_FAIL;
}

Код здесь вполне прямолинейный. Мы загружаем картинку из файла и подключаем к ней объект CBitmap для дальнейшей работы.

Взаимодействие с контекстным меню

Как и раньше, если IShellExtInit::Initialize() возвращает S_OK, проводник запрашивает у нашего расширения интерфейс IContextMenu. Чтобы позволить нашему расширению создавать пункты owner-drawn меню он также запрашивает IContextMenu3. IContextMenu3 добавляет метод к IContextMenu, который мы будем использовать для нашего рисования.

Существует интерфейс IContextMenu2, который, как заявляет Microsoft, поддерживается оболочкой версии 4.0. Однако, на Windows95 оболочка никогда не вызывает IContextMenu2. В результате вы просто не сможете сделать пункты меню owner-drawn в оболочке версии 4.0 (на NT4 может быть по-другому. У меня не было возможности проверить на NT4). Вы можете сделать так, чтобы пункт меню отображал битмап, но это приведет к тому, что когда этот пункт меню выбран, он выглядит просто безобразно.(Это то, что делает Pica View).

IContextMenu3 унаследован от IContextMenu2 и добавляет метод HandleMenuMsg2() к IContextMenu. Этот метод дает нам возможность отреагировать на два сообщения меню: WM_MEASUREITERM и WM_DROWITEM. Документация говорит, что метод HandleMenuMsg2() будет вызван, так что он вполне может обработать WM_INITMENUPOPUP и WM_MENUCHAR, однако во время моего тестирования в оболочке 4.72(Win95) и 5.0(Win2K) HandleMenuMsg2() никогда не получал ни одного из этих сообщений.

Поскольку IContextMenu3 наследуется от IContextMenu2 (который сам наследуется от IContextMenu) нам надо, чтобы наш класс С++ просто наследоввался от IContextMenu3. Откройте BmpCtxMenuExt.h и добавьте выделенные строки:

class ATL_NO_VTABLE CBmpCtxMenuExt : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CBmpCtxMenuExt, &CLSID_BmpCtxMenuExt>,
    public IDispatchImpl<IBmpCtxMenuExt, &IID_IBmpCtxMenuExt, &LIBID_BMPVIEWEREXTLib>,
    public IShellExtInit,
    public IContextMenu3
{
BEGIN_COM_MAP(CSimpleShlExt)
    COM_INTERFACE_ENTRY(ISimpleShlExt)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY(IShellExtInit)
    COM_INTERFACE_ENTRY(IContextMenu)
    COM_INTERFACE_ENTRY(IContextMenu2)
    COM_INTERFACE_ENTRY(IContextMenu3)
END_COM_MAP()

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

    // IContextMenu2
    STDMETHOD(HandleMenuMsg)(UINT, WPARAM, LPARAM);

    // IContextMenu3
    STDMETHOD(HandleMenuMsg2)(UINT, WPARAM, LPARAM, LRESULT*);

Модифицирование контекстного меню.

Как и раньше мы выполняем работу в трех методах IContextMenu. Мы добавляем свой пункт меню в QueryContextMenu(). Сначала мы проверяем версию оболочки. Если это 4.71 или выше, то мы можем добавить пункт меню owner-drawn. В других случаях мы добавляем пункт, который показывает битмап. В последнем случае это все, что мы должны сделать. Меню само позаботится об отображении битмапа.

Итак, во-первых, код для проверки версии оболочки. Он вызывает экспортируемую функцию DllGetVersion() для извлечения версии. Если эта функция отсутствует, то DLL версии 4.0, так как DllGetVersion() не было в shell32.dll версии 4.0.

STDMETHODIMP CBmpCtxMenuExt::QueryContextMenu (
    HMENU hmenu,
    UINT uIndex,
    UINT uidCmdFirst,
    UINT uidCmdLast,
    UINT uFlags )
{
    // If the flags include CMF_DEFAULTONLY then we shouldn't do anything.
    if ( uFlags & CMF_DEFAULTONLY )
        {
        return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );
        }

    bool bUseOwnerDraw = false;
    HINSTANCE hinstShell;

    hinstShell = GetModuleHandle ( _T("shell32") );

    if ( NULL != hinstShell )
        {
        DLLGETVERSIONPROC pProc;

        pProc = (DLLGETVERSIONPROC) GetProcAddress(hinstShell, "DllGetVersion");

        if ( NULL != pProc )
            {
            DLLVERSIONINFO rInfo = { sizeof(DLLVERSIONINFO) };

            if ( SUCCEEDED( pProc ( &rInfo ) ))
                {
                if ( rInfo.dwMajorVersion > 4 ||
                     rInfo.dwMinorVersion >= 71 )
                    {
                    bUseOwnerDraw = true;
                    }
                }
            }
        }

В этой точке значение bUseOwnerDrow дает добро на использование owner-drawn пунктов меню. Если TRUE, то мы вставляем пункт owner-drawn (смотри строку, которая устанавливает mii.fType). Если FALSE, мы добавляем пункт - битмап, и затем сообщаем меню хэндл битмапа для отображения. Для добавления пункта код использует InsertMenuItem(); использование старой функции InsertMenu() требует, чтобы вы вернулись и вызвали ModifyMenu() для изменения стиля пункта меню на owner-drawn.

    MENUITEMINFO mii;

    mii.cbSize = sizeof(MENUITEMINFO);
    mii.fMask  = MIIM_ID | MIIM_TYPE;
    mii.fType  = bUseOwnerDraw ? MFT_OWNERDRAW : MFT_BITMAP;
    mii.wID    = uidCmdFirst;

    if ( !bUseOwnerDraw )
        {
        // NOTE: This will put the full-sized bitmap in the menu, which is
        // obviously a bit less than optimal.  Compressing the bitmap down
        // to a thumbnail is left as an exercise.
        mii.dwTypeData = (LPTSTR) m_bmp.GetSafeHandle();
        }

    InsertMenuItem ( hmenu, uIndex, TRUE, &mii );

    // Store the menu item's ID so we can check against it later when
    // WM_MEASUREITEM/WM_DRAWITEM are sent.
    m_uOurItemID = uidCmdFirst;

    // Tell the shell we added 1 top-level menu item.
    return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 1 );
}

Мы сохраняем ID пункта меню в m_UOurItemID, так мы опознаем ID позже, когда прибудут другие сообщения. Это не строго необходимо, поскольку у нас только один пункт меню, но, тем не менее, является хорошей практикой, и абсолютно обязательно, если у вас много пунктов.

Отображение всплывающей подсказки в строке состояния.

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

#include <atlconv.h>  // for ATL string conversion macros

STDMETHODIMP CBmpCtxMenuExt::GetCommandString (
                               UINT uCmd, UINT uFlags, UINT* puReserved,
                               LPSTR pszName, UINT cchMax )
{
    static LPCTSTR szHelpString = _T("Select this thumbnail to view the entire picture.");

    USES_CONVERSION;

    // Check idCmd, it must be 0 since we have only one menu item.
    if ( 0 != uCmd )
        return E_INVALIDARG;

    // If Explorer is asking for a help string, copy our string into the
    // supplied buffer.
    if ( uFlags & GCS_HELPTEXT )
        {
        if ( uFlags & GCS_UNICODE )
            {
            // We need to cast pszName to a Unicode string, and then use the
            // Unicode string copy API.
            lstrcpynW ( (LPWSTR) pszName, T2CW(szHelpString), cchMax );
            }
        else
            {
            // Use the ANSI string copy API to return the help string.
            lstrcpynA ( pszName, T2CA(szHelpString), cchMax );
            }
        }

    return S_OK;
}

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

Последний метод в IContextMenu - это InvokeCommand(). Этот метод будет вызван, если пользователь щелкнет на пункте меню, добавленном нами. Расширение вызовет ShellExecute(), чтобы открыть картинку в Paint (или другой программе, ассоциированной с BMP-файлами).

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

    // The command ID must be 0 since we only have one menu item.
    if ( 0 != LOWORD( pInfo->lpVerb ))
        return E_INVALIDARG;

    // Open the bitmap in the default paint program.
    ShellExecute ( pInfo->hwnd, _T("open"), m_szFile, NULL, NULL, SW_SHOWNORMAL );

    return S_OK;
}

Рисование пункта меню.

Итак, держу пари, вы заскучали к этому времени со всеми этими кодами, которые вы видели и раньше. Пора приступить к новым и интересным штучкам! Два дополнительных метода IContextMenu2 и IContextMenu3 приведены ниже. Они просто вызывают единственную вспомогательную функцию, которая, в свою очередь, вызывает обработчик сообщений. Я разработал этот метод чтобы избежать двух разных версий обработчиков сообщений (по одному для IContextMenu2 и IContextMenu3). В HandleMenuMsg2() есть одна странность, связанная с параметром LRESULT, которую я объясню в комментариях.

STDMETHODIMP CBmpCtxMenuExt::HandleMenuMsg ( UINT uMsg, WPARAM wParam, LPARAM lParam )
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState());

    // res - подставная переменная типа LRESULT. Она не используется  
    // (IContextMenu2::HandleMenuMsg() не предоставляет способа 
    // возвращать результат), но она здесь для того, чтобы
    // код в MenuMessageHandler() мог быть одинаковым   
    // независимо от того, через какой интерфейс
    // был получен вызов (IContextMenu2 или 3).

    LRESULT res;

    return MenuMessageHandler ( uMsg, wParam, lParam, &res );
}

STDMETHODIMP CBmpCtxMenuExt::HandleMenuMsg2 ( UINT uMsg, WPARAM wParam, LPARAM lParam,
                                              LRESULT* pResult )
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState());

    // Для сообщений, которые не имеют возвращаемых значений,     
    // pResult есть NULL. Это очень плохо со стороны MS, поскольку это     
    // вынуждает нас проверять корректность pResult перед использованием.  
    // Вы могли бы подумать, что указатель 
    // к возвращаемому значению всегда должен бы быть действительным, 
    // но нет. Если он NULL, я создаю подставную переменную,   
    // поэтому код в MenuMessageHandler() всегда будет иметь     
    // действительный указатель pResult .                       

    if ( NULL == pResult )
        {
        LRESULT res;
        return MenuMessageHandler ( uMsg, wParam, lParam, &res );
        }
    else
        {
        return MenuMessageHandler ( uMsg, wParam, lParam, pResult );
        }
}

MenuMessageHandler() просто пересылает WM_MEASUREITEM и WM_DROWITEM отдельным функциям-обработчикам сообщений.

STDMETHODIMP CBmpCtxMenuExt::MenuMessageHandler ( UINT uMsg, WPARAM wParam,
                                                  LPARAM lParam, LRESULT* pResult )
{
    switch ( uMsg )
        {
        case WM_MEASUREITEM:
            return OnMeasureItem ( (MEASUREITEMSTRUCT*) lParam, pResult );
        break;

        case WM_DRAWITEM:
            return OnDrawItem ( (DRAWITEMSTRUCT*) lParam, pResult );
        break;
        }

    return S_OK;
}

Как я отмечал раньше, документация говорит, что оболочка должна позволять нашему расширению оперировать с WM_INITMENUPOPUP и WM_MENUCHAR. Но за все время моих опытов я никогда не видел, чтобы эти сообщения приходили.

Обработка WM_MEASUREITEM

Когда оболочка посылает нашему расширению сообщение WM_MEASUREITEM для запроса размеров нашего пункта меню, мы начинаем с проверки, затребованы ли размеры нашего собственного пункта меню. Если тест пройден, мы получаем размеры битмапа, затем вычисляем размер всего пункта меню.

Сначала размер битмапа:

STDMETHODIMP CBmpCtxMenuExt::OnMeasureItem ( MEASUREITEMSTRUCT* pmis, LRESULT* pResult )
{
BITMAP bm;
LONG   lThumbWidth;
LONG   lThumbHeight;

    // Check that we're getting called for our own menu item.
    if ( m_uOurItemID != pmis->itemID )
        return S_OK;

    m_bmp.GetBitmap ( &bm );

    m_lBmpWidth = bm.bmWidth;
    m_lBmpHeight = bm.bmHeight;

Далее, мы вычисляем размер эскиза, и, исходя из этого, размер всего пункта меню. Если картинка меньше, чем максимальный размер эскиза (который в демонстрационном проекте 64х64), то она будет нарисована как есть. Иначе, картинка будет нарисована размером 64х64. Это может исказить ее вид, но опять же, создание приятного вида эскиза оставлено для упражнений.

    // Calculate the bitmap thumbnail size.
    lThumbWidth = (m_lBmpWidth <= m_lMaxThumbnailSize) ? m_lBmpWidth : 
                    m_lMaxThumbnailSize;

    lThumbHeight = (m_lBmpHeight <= m_lMaxThumbnailSize) ? m_lBmpHeight :
                     m_lMaxThumbnailSize;

    // Calculate the size of the menu item, which is the size of the thumbnail +
    // the border and padding (which provides some space at the edges of the item).
    m_lItemWidth = lThumbWidth + m_lTotalBorderSpace;
    m_lItemHeight = lThumbHeight + m_lTotalBorderSpace;

Теперь, когда у нас есть размер пункта меню, мы сохраняем размеры обратно в MENUITEMSTRUCT, которую мы получили с сообщением. Проводник зарезервирует достаточно места в меню для нашего пункта.

    // Store the size of the item in the MEASUREITEMSTRUCT.
    pmis->itemWidth = m_lItemWidth;
    pmis->itemHeight = m_lItemHeight;

    *pResult = TRUE;            // we handled the message

    return S_OK;
}

Обработка WM_DRAWITEM

Когда мы получаем сообщение WM_DRAWITEM, проводник требует, чтобы мы действительно вывели наш пункт меню. Мы начинаем с вычисления прямоугольника, в котором мы нарисуем объемную границу вокруг эскиза. Это не обязательно тот же прямоугольник, которые занимает пункт меню, потому что меню может оказаться шире, чем размер, который мы установили в обработчике WM_MEASUREITEM.

STDMETHODIMP CBmpCtxMenuExt::OnDrawItem ( DRAWITEMSTRUCT* pdis, LRESULT* pResult )
{
CDC   dcBmpSrc;
CDC*  pdcMenu = CDC::FromHandle ( pdis->hDC );
CRect rcItem ( pdis->rcItem );  // RECT of our menu item
CRect rcDraw;                   // RECT in which we'll be drawing

    // Check that we're getting called for our own menu item.
    if ( m_uOurItemID != pdis->itemID )
        return S_OK;
            
    // rcDraw will first be set to the RECT that we set in WM_MEASUREITEM.
    // It will get deflated as we go along.
    rcDraw.left = (rcItem.right + rcItem.left - m_lItemWidth) / 2;
    rcDraw.top = (rcItem.top + rcItem.bottom - m_lItemHeight) / 2;
    rcDraw.right = rcDraw.left + m_lItemWidth;
    rcDraw.bottom = rcDraw.top + m_lItemHeight;

    // Shrink rcDraw to account for the padding space around
    // the thumbnail.

    rcDraw.DeflateRect ( m_lMenuItemSpacing, m_lMenuItemSpacing );

Первый шаг рисования - окрашивание фона пункта меню. Элемент itemState структуры DRAWITEMSTRUCT показывает, отмечен наш пункт меню, или нет. Это определяет, какой цвет мы используем для фона.

    // Fill in the background of the menu item.
    if ( pdis->itemState & ODS_SELECTED )
        pdcMenu->FillSolidRect ( rcItem, GetSysColor ( COLOR_HIGHLIGHT ));
    else
        pdcMenu->FillSolidRect ( rcItem, GetSysColor ( COLOR_3DFACE ));

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

    
    // Draw the sunken 3D border.
    for ( int i = 1; i <= m_l3DBorderWidth; i++ )
        {
        pdcMenu->Draw3dRect ( rcDraw, GetSysColor ( COLOR_3DDKSHADOW ),
                              GetSysColor ( COLOR_3DHILIGHT ));

        rcDraw.DeflateRect ( 1, 1 );
        }

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

    
    // Create a new DC and select the original bitmap into it.
    dcBmpSrc.CreateCompatibleDC ( &dc );
    dcBmpSrc.SelectObject ( &m_bmp );

    // Blit the bitmap to the menu DC.
    pdcMenu->StretchBlt ( rcDraw.left, rcDraw.top, rcDraw.Width(), rcDraw.Height(),
                          &dcBmpSrc, 0, 0, m_lBmpWidth, m_lBmpHeight, SRCCOPY );

    *pResult = TRUE;            // we handled the message

    return S_OK;
}

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

Вот несколько экранных кадров меню! Сначала - это меню в невыбраном и в выбранном состоянии.



А здесь - меню в оболочке версии 4.00. Заметьте, как в выбранном состоянии инвертируются все цвета, делая его безобразным.



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

Регистрация нашего "вьювера битмапов" делается так же, как и других расширений контекстных меню. Вот RGS сценарий для этой работы:

HKCR
{
    NoRemove Paint.Picture
    {
        NoRemove ShellEx
        {
            NoRemove ContextMenuHandlers
            {
                BitmapPreview = s '{D6F469CD-3DC7-408F-BB5F-74A1CA2647C9}'
            }
        }
    }
}

Заметьте, что тип файлов "Paint.Picture" здесь жестко закодирован. Если вы не используете Paint, как вьювер по умолчанию для BMP-файлов, то вам необходимо изменить "Paint.Picture" на то, что указано в значении по умолчанию в ключе HKCR\.bmp. Разумеется в коммерческом коде вы должны делать эту регистрацию в DllRegisterServer(), поэтому вы можете проверить, является ли ключ "Paint.Picture" подходящем местом для записи. Я более подробно говорил об этом в Части I.

Расширение 2 - Обработка щелчка правой кнопкой мыши на фоне окна каталога.

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

Я не пойду снова по всем шагам, необходимым для расширения. Посмотрите демонстрационный проект, если вы хотите увидеть все целиком.

Отличия в IShellExtInit::Initialize()

Метод Initialize() имеет параметр pidlFolder, который до этого времени мы игнорировали, потому что он был NULL. Сейчас, наконец-то, этот параметр пригодится! Это PIDL каталога, где произошел щелчок правой кнопкой мыши. Второй параметр (IDataObject*) - NULL, потому что не было выбрано ни одного файла.

Вот реализация Initialize() из демонстрационного проекта:

STDMETHODIMP CBkgndCtxMenuExt::Initialize (
    LPCITEMIDLIST pidlFolder,
    LPDATAOBJECT  pDO,
    HKEY          hkeyProgID )
{
    // pidlFolder - PIDL каталога,  на котором был щелчок.  pDO -
    // NULL, т.к. не было указано с чем работать.
    // Мы получаем обычный путь к каталогу из его PIDL с помощью
    // SHGetPathFromIDList() API.

    return SHGetPathFromIDList ( pidlFolder, m_szDirClickedIn ) ? S_OK : E_INVALIDARG;
}

Функция SHGetPathFromIDList() получает полный путь папки, который мы сохраняем для дальнейшего использования. Возвращаемое этой функцией BOOL-значение используется для индикации успеха или неудачи операции.

Отличия в регистрации.

Этот тип расширений регистрируется под другим ключом - а именно HKCR\Directory\Background\ShellEx\ContextMenuHandlers. Вот RGS сценарий для регистрации:

HKCR
{
    NoRemove Directory
    {
        NoRemove Background
        {
            NoRemove ShellEx
            {
                NoRemove ContextMenuHandlers
                {
                    ForceRemove SimpleBkgndExtension = s '{9E5E1445-6CEA-4761-8E45-AA19F654571E}'
                }
            }
        }
    }
}

За исключением этих двух отличий, расширение работает также, как и другие расширения контекстного меню. Хотя все же есть одна ловушка в IContextMenu::QueryContextMenu(). Похоже параметр uIndex всегда равен -1 (0xFFFFFFFF). Передача -1 в качестве индекса в InsertMenu() означает, что новый пункт будет расположен в самом низу меню. Однако, если вы инкрементируете uIndex, он переполнится и станет равным нулю, означая, что если вы снова передадите uIndex в InsertMenu(), то второй пункт появится в самом верху меню. Посмотрите в демонстрационном проекте код для QueryContextMenu() чтобы видеть, как правильно добавить пункты меню на нужное место.

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


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

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

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


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