Красивые менюшки Owner-Drawn Menus

Часто можно увидеть в разных программах красивые менюшки, которые
нельзя создать с помощью мастеров. Такие меню есть и в WordXP ExсelXP. Эта статья научит вас
создавать такие меню.

Такие меню не создаются автоматически - им надо рисовать себя самим. Итак:
ШАГ 1. Чтобы пункт меню был саморисующийся ему надо установить стиль MF_OWNERDRAW. Поскольку оно само себя рисует - то надо создать обработчик DrawItem сообщения WM_DRAWITEM. Также мы должны сами определить размеры меню: надо создать обработчик MeasureItem сообщения WM_MEASUREITEM. И MF_OWNERDRAW и WM_DRAWITEM вызывается для _каждого_ пункта меню.
Сделаем наше меню на основе MFC класса CMenu и назовём его CMenuEx. Оно будет простенькое, но при желании можно усложнить до требуемого состояния самому. Главное понять принципы, по которым оно работает.
Значит у нас есть:
class CMenuEx : public CMenu  
{
public:
    virtual void MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct);
    virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
    CMenuEx() {};
    virtual ~CMenuEx() {};
};
void CMenuEx::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{}
void CMenuEx::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{}
ШАГ 2. Допустим к нашему класу уже присоединён описатель стандартного меню. Нам надо для каждого пункта установить стиль MF_OWNERDRAW и ещё некоторые атрибуты (которые будут использоваться при отрисовке, или заданиии размеров). Для этого обьявим структуру:
struct MYOWNMENUITEM : public CObject
    {
        bool bIsTop;    // признак является ли пункт верхним в menu bar
        CString sCaption;   // надпись меню

        MYOWNMENUITEM()
        {
            this->bIsTop = false;
            this->sCaption = "";
        }
    };
Также, для усложнения, в ней можно хранить значки, картинки либо признак какой-то особенности.
Создадим метод, в котором пройдёмся по всем пунктам изменяя их стиль и заполняя их структуры.
Пункты и субменю будем хранить в переменных класса
    CPtrArray m_MenuArray;
    CPtrArray m_ItemArray;
А вот наш метод:
void CMenuEx::Prepare(bool bTopLevel /*= false*/)
{
    // bTopLevel - признак, что пункт меню есть верхним в menu bar
    for (UINT i=0; i < GetMenuItemCount(); i++)
    {
        MYOWNMENUITEM* pItem = new MYOWNMENUITEM;

        pItem->bIsTop = bTopLevel;
        GetMenuString(i, pItem->sCaption, MF_BYPOSITION);
        ModifyMenu(i, MF_BYPOSITION|MF_OWNERDRAW, GetMenuItemID(i), (TCHAR*) pItem);
        m_ItemArray.Add(pItem);

        if(GetSubMenu(i))
        {
            CMenuEx* pMenu = new CMenuEx;
            pMenu->m_pWnd = this->m_pWnd;
            pMenu->Attach((this->GetSubMenu(i))->GetSafeHmenu());
            m_MenuArray.Add(pMenu);
            pMenu->Prepare();
        }
    };
}

Также для удобства я создал метод
void CMenuEx::MakeMenuEx(CWnd* pWnd, bool bToolBar/* = false*/)
{
    m_pWnd = pWnd;
    if(bToolBar)
    {
        for(UINT i=0; i < GetMenuItemCount(); i++)
            Prepare(true);
    }
    else
        Prepare();
}
Где переменная класса CWnd* m_pWnd - это окно, в котором показывается меню, а bToolBar - признак, является ли меню popup или toolbar.
Соответственно, выделенную память нужно освободить в деструкторе:
CMenuEx::~CMenuEx()
{
    for(INT32 a=0; a<m_MenuArray.GetSize(); a++)
    {
        delete (CMenuEx*) m_MenuArray[a];
    };
    for(INT32 b=0; b<m_ItemArray.GetSize(); b++)
    {
        delete (MYOWNMENUITEM*) m_ItemArray[b];
    };
}

ШАГ 3. ::Примечание:: id для сепаратора всегда =0, а для субменю =-1 !!!
Надо определить размеры меню. Мы получаем LPMEASUREITEMSTRUCT - это указатель на
структуру MEASUREITEMSTRUCT:
typedef struct tagMEASUREITEMSTRUCT { 
    UINT CtlType;   // для меню всегда равно ODT_MENU
    UINT CtlID;     // не используется в меню
    UINT itemID;    // содержит ID пукта
    UINT itemWidth;     // ширина меню - нам надо установить желаемую ширину
    UINT itemHeight;    // высота меню - нам надо установить желаемую высоту
    DWORD itemData  // данные, которые добавлены к пункту с помощью методов CMenu::AppendMenu,
            // CMenu::InsertMenu, CMenu::ModifyMenu
            // Тут содержится наша структура MYOWNMENUITEM , которую мы добавляли в Prepare()
} MEASUREITEMSTRUCT;
Размер, для простоты, можно просто вбить:
void CMenuEx::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{
    lpMeasureItemStruct->itemHeight = 20;
    lpMeasureItemStruct->itemWidth = 50;
}

(в присоединённом проекте сделано чуть сложнее - там ширина зависит от длины надписи, от.....)

Теперь рисуем пунк меню. Мы получаем LPDRAWITEMSTRUCT - это указатель на структуру DRAWITEMSTRUCT:
typedef struct tagDRAWITEMSTRUCT { 
    UINT CtlType;   // для меню всегда равно ODT_MENU
    UINT CtlID;     // не используется в меню
    UINT itemID;    // содержит ID пукта
    UINT itemAction;    // Сообщает какое действие требуется отрисовать. Может содержать такие биты:
            // ODA_DRAWENTIRE - этот бит установлен, когда пункт надо отрисовать
            // ODA_FOCUS - этот бит установлен, когда пункт получает или теряет фокус.
            // ODA_SELECT - этот бит установлен, когда пункт получает или теряет выделение
            // [COLOR=blue]!!!Совет: Надо проверить itemState чтобы определить, когда пункт выделен.[/COLOR]
    UINT itemState;     // состояние пункта. Для меню может быть:
            // ODS_CHECKED - установлен, когда пункт в состоянии checked
            // ODS_DISABLED - установлен, когда пункт отключён
            // ODS_FOCUS - установлен, когда пункт получает фокус
            // ODS_GRAYED - установлен, когда пункт недоступный (dimmed, серый)
            // ODS_SELECTED - установлен, когда пункт выбран
            // ODS_DEFAULT - установлен, если пункт есть пунктом по умолчанию
    HWND hwndItem;  // определяет дескриптор меню (HMENU) которое содержит пункт меню
    HDC hDC;        // определяет контекст устройства, который используется для рисования пункта
    RECT rcItem;    // прямоугольник, который ограничивает наш пункт (его мы задавали в MeasureItem)
    DWORD itemData;     // данные, которые добавлены к пункту с помощью методов CMenu::AppendMenu,
            // CMenu::InsertMenu, CMenu::ModifyMenu
            // Тут содержится наша структура MYOWNMENUITEM , которую мы добавляли в Prepare()
} DRAWITEMSTRUCT;
Для простоты отрисовку можно сделать такую:
void CMenuEx::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
    // Получаем нашу структуру
    MYOWNMENUITEM* pItem = (MYOWNMENUITEM*)lpDrawItemStruct->itemData;

    CRect RFull(lpDrawItemStruct->rcItem); // Ограничивающий пункт прямоугольник
    // Зона значка, или в нашем случае - градиентной заливки
    CRect RIcon(RFull.left,RFull.top,RFull.left+m_szIconPadding.cx,RFull.top+RFull.bottom);
    // зона текста
    CRect RText(RIcon.right,RFull.top,RFull.right,RFull.bottom);

    COLORREF ColorIconRL = COLORREF(RGB(246,245,244)); // Цвет левой части заливки
    COLORREF ColorIconRR = COLORREF(RGB(0,209,201)); // Цвет правой части заливки
    COLORREF TextColor = COLORREF(RGB(249, 248, 247)); // Цвет фона текста

    if(pItem->bIsTop) // признак, что пункт меню есть верхним в menu bar
    {
        ZeroMemory(&RIcon, sizeof(CRect));
        RText = RFull;
        TextColor = GetSysColor(COLOR_BTNFACE);// COLORREF(RGB(192,192,192));
    }

    // получаем контекст, на котором будем рисовать
    CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);

    // Функция градиентной заливки
    FillFluentRect(pDC->GetSafeHdc(), RIcon, 246,245,244,213,209,201);

    pDC->FillSolidRect(&RText, TextColor); // Рисуем фон текста

    pDC->SetBkColor(TextColor);
    
    // Рисуем текст пункта
    pDC->DrawText(pItem->sCaption, &RText, DT_EXPANDTABS|DT_LEFT|DT_VCENTER|DT_EDITCONTROL );
}

Но в нашем случае (во вложении) всё немного сложнее:
void CMenuEx::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
    //TRACE("CMenuEx::DrawItem\n");

    // Получаем нашу структуру
    MYOWNMENUITEM* pItem = (MYOWNMENUITEM*)lpDrawItemStruct->itemData;

        CRect RFull(lpDrawItemStruct->rcItem); // Ограничивающий пункт прямоугольник
    // Зона значка, или в нашем случае - градиентной заливки
    CRect RIcon(RFull.left,RFull.top,RFull.left+m_szIconPadding.cx,RFull.top+RFull.bottom);
    // зона текста
    CRect RText(RIcon.right,RFull.top,RFull.right,RFull.bottom);

    COLORREF ColorIconRL = COLORREF(RGB(246,245,244)); // Цвет левой части заливки
    COLORREF ColorIconRR = COLORREF(RGB(0,209,201)); // Цвет правой части заливки
    COLORREF TextColor = COLORREF(RGB(249, 248, 247)); // Цвет фона текста

    if(pItem->bIsTop) // признак, что пункт меню есть верхним в menu bar
    {
        ZeroMemory(&RIcon, sizeof(CRect));
        RText = RFull;
        TextColor = GetSysColor(COLOR_BTNFACE);// COLORREF(RGB(192,192,192));
    }

    // получаем контекст, на котором будем рисовать
    CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);

    // Функция градиентной заливки
    FillFluentRect(pDC->GetSafeHdc(), RIcon, 246,245,244,213,209,201);
    // если есть значёк, то его можно отрисовать 
    // поверх заливки с помощью функции BitBlt

    pDC->FillSolidRect(&RText, TextColor); // Рисуем фон текста

    if(lpDrawItemStruct->itemID == 0) // если этот пункт - это Separator
    {
        // Функция градиентной заливки
        FillFluentRect(pDC->GetSafeHdc(), RIcon, 246,245,244,213,209,201);

        pDC->FillSolidRect(&RText, TextColor); // рисуем фон
        CPen pen;
        pen.CreatePen(PS_SOLID, 1, GetSysColor(25));
        CPen* pOldPen = pDC->SelectObject(&pen);

        // рисуем сепаратор
        pDC->MoveTo(RText.left+5,  RText.top+(RText.bottom-RText.top)/2);
        pDC->LineTo(RText.right, RText.top+(RText.bottom-RText.top)/2);

        pDC->SelectObject(pOldPen);
        DeleteObject(pen);
    }

    else if ((lpDrawItemStruct->itemState & ODS_SELECTED) &&
             (lpDrawItemStruct->itemAction & (ODA_SELECT | ODA_DRAWENTIRE)) )
    {
        // Если пункт выделен - то рисуем выделение
        if (!(lpDrawItemStruct->itemState & ODS_GRAYED)) // проверка доступен ли пункт
        {
            TextColor = COLORREF(RGB(182, 189, 210));
            pDC->FillSolidRect(&RFull, TextColor); // фон
            CBrush* br = new CBrush;
            br->CreateSolidBrush(COLORREF(RGB(10, 36, 106)));
            pDC->FrameRect(&RFull, br); // рамка
            delete br;
        };
    };

    if(lpDrawItemStruct->itemState & ODS_CHECKED) // если пункт в состоянии checked
    {
        // Checked Item
        HBITMAP     hBmp;
        CBitmap*    pBmp;
        BITMAP      bmp;
        CSize       szBmp;
        CPoint      ptBmp;
        ZeroMemory(&bmp, sizeof(BITMAP));
        
        // Загружаем значёк checked
        hBmp = ::LoadBitmap(NULL, MAKEINTRESOURCE(32760));
        pBmp = CBitmap::FromHandle(hBmp);
        pBmp->GetBitmap(&bmp);
        szBmp = CSize(bmp.bmWidth, bmp.bmHeight);
        ptBmp = CPoint(RIcon.left+(m_szIconPadding.cx-szBmp.cx)/2+1, 
                        RIcon.top+(m_szIconPadding.cy-szBmp.cy)/2);
        // рисуем состояние
        pDC->DrawState(ptBmp, szBmp, hBmp, DSS_NORMAL|DSS_UNION);
        DeleteObject(hBmp);
    };

    // Устанавливаем цвет фона и границу надписи
    pDC->SetBkColor(TextColor);
    RText.left += m_szTextPadding.cx;
    RText.top += m_szTextPadding.cy;
    RText.bottom -= m_szTextPadding.cy;

    // если пункт недоступен - устанавливаем соответствующий цвет текста
    if (lpDrawItemStruct->itemState & ODS_GRAYED)
        pDC->SetTextColor(GetSysColor(COLOR_GRAYTEXT));
    // Рисуем текст пункта
    if(pItem->bIsTop) 
        // если пункт меню есть верхним в menu bar - то выравниваем по центру
        pDC->DrawText(pItem->sCaption, &RText, DT_EXPANDTABS|DT_CENTER|DT_VCENTER);
    else
        // иначе - по левому краю
        pDC->DrawText(pItem->sCaption, &RText, DT_EXPANDTABS|DT_LEFT|DT_VCENTER|
                      DT_EDITCONTROL );
}

ШАГ 4. Использование:
1) Если надо отобразить popup, то надо обьявить указатель CMenuEx* m_menu;
В конструкторе окна создать обьект и инициализировать его:
CmenuView::CmenuView()
{
    m_menu = new CMenuEx;
    m_menu->LoadMenuEx(IDR_MEMU, this);
}
соответственно в деструкторе - удалить обьект delete m_menu;
Создать обработчик OnMeasureItem и вызвать из него MeasureItem нашего класса
void CmenuView::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{
    m_menu->MeasureItem(lpMeasureItemStruct);

    CView::OnMeasureItem(nIDCtl, lpMeasureItemStruct);
}

Запустить popup при клике правой кнопкой мышки:
void CmenuView::OnRButtonDown(UINT nFlags, CPoint point)
{
    ClientToScreen(&point);
    m_menu->TrackPopupMenu(TPM_LEFTALIGN|TPM_RIGHTBUTTON, point.x, point.y, this);

    CView::OnRButtonDown(nFlags, point);
}

2) Если надо отобразить как menu bar, то тоже надо сначала обьявить указатель.
Потом создать обьект в конструкторе и соответственно удаление в деструкторе.
В OnCreate окна инициализировать и установить меню:
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   ...........
    m_myme->LoadMenuEx(IDR_MAINFRAME, this, true);
    ::DestroyMenu(m_hMenuDefault);
    SetMenu(m_myme);
    m_hMenuDefault = m_myme->GetSafeHmenu();
   ............
}
Создать обработчики OnMeasureItem и OnDrawItem окна и вызвать из них соответствующие
методы нашего меню:
void CMainFrame::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{
    m_myme->MeasureItem(lpMeasureItemStruct);

    CFrameWnd::OnMeasureItem(nIDCtl, lpMeasureItemStruct);
}

void CMainFrame::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct)
{
    m_myme->DrawItem(lpDrawItemStruct);

    CFrameWnd::OnDrawItem(nIDCtl, lpDrawItemStruct);
}

Вот и всё!
 
« Предыдущая статья