Часто можно увидеть в разных программах красивые менюшки, которые нельзя создать с помощью мастеров. Такие меню есть и в 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); }
| Вот и всё! |