Версия для печати
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум на Исходниках.RU > C/C++ FAQ > Owner-Drawn Menus step-by-step


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

Такие меню не создаются автоматически - им надо рисовать себя самим. Итак:
ШАГ 1. Чтобы пункт меню был саморисующийся ему надо установить стиль MF_OWNERDRAW. Поскольку оно само себя рисует - то надо создать обработчик DrawItem сообщения WM_DRAWITEM. Также мы должны сами определить размеры меню: надо создать обработчик MeasureItem сообщения WM_MEASUREITEM. И MF_OWNERDRAW и WM_DRAWITEM вызывается для _каждого_ пункта меню.
Сделаем наше меню на основе MFC класса CMenu и назовём его CMenuEx. Оно будет простенькое, но при желании можно усложнить до требуемого состояния самому. Главное понять принципы, по которым оно работает.
Значит у нас есть:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    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 и ещё некоторые атрибуты (которые будут использоваться при отрисовке, или заданиии размеров). Для этого обьявим структуру:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    struct MYOWNMENUITEM : public CObject
        {
            bool bIsTop;    // признак является ли пункт верхним в menu bar
            CString sCaption;   // надпись меню
     
            MYOWNMENUITEM()
            {
                this->bIsTop = false;
                this->sCaption = "";
            }
        };
Также, для усложнения, в ней можно хранить значки, картинки либо признак какой-то особенности.
Создадим метод, в котором пройдёмся по всем пунктам изменяя их стиль и заполняя их структуры.
Пункты и субменю будем хранить в переменных класса
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
        CPtrArray m_MenuArray;
        CPtrArray m_ItemArray;
А вот наш метод:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    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();
            }
        };
    }

Также для удобства я создал метод
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    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()
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    {
        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:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    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;
Размер, для простоты, можно просто вбить:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    void CMenuEx::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct)
    {
        lpMeasureItemStruct->itemHeight = 20;
        lpMeasureItemStruct->itemWidth = 50;
    }

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

Теперь рисуем пунк меню. Мы получаем LPDRAWITEMSTRUCT - это указатель на структуру DRAWITEMSTRUCT:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    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;
Для простоты отрисовку можно сделать такую:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    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 );
    }

Но в нашем случае (во вложении) всё немного сложнее:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    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;
В конструкторе окна создать обьект и инициализировать его:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    CmenuView::CmenuView()
    {
        m_menu = new CMenuEx;
        m_menu->LoadMenuEx(IDR_MEMU, this);
    }
соответственно в деструкторе - удалить обьект delete m_menu;
Создать обработчик OnMeasureItem и вызвать из него MeasureItem нашего класса
void CmenuView::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct)
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    {
        m_menu->MeasureItem(lpMeasureItemStruct);
     
        CView::OnMeasureItem(nIDCtl, lpMeasureItemStruct);
    }

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

2) Если надо отобразить как menu bar, то тоже надо сначала обьявить указатель.
Потом создать обьект в конструкторе и соответственно удаление в деструкторе.
В OnCreate окна инициализировать и установить меню:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    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 окна и вызвать из них соответствующие
методы нашего меню:
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    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);
    }

Вот и всё! :yes:

Демонстрационное приложение.В прикрепленном архиве содержится проект, который демонстрируют работу с owner-drawn menu.

Ссылки:Owner-Drawn Menu, не отрисовывается один пункт :(
3 вопроса про меню
http://www.codeproject.com/menu/
http://www.codeproject.com/menu/owndraw.asp

Список ключевых слов:
CMenu, owner-drawn, MeasureItem, LPMEASUREITEMSTRUCT, MEASUREITEMSTRUCT, LPDRAWITEMSTRUCT, DRAWITEMSTRUCT, DrawItem, OnDrawItem, OnMeasureItem

Powered by Invision Power Board (https://www.invisionboard.com)
© Invision Power Services (https://www.invisionpower.com)