Про меню, собственную отрисовку и PocketPC
Итак, потребовалось мне сделать меню в MFC с собственной отрисовкой пунктов со значками как в Win2k/XP, да еще чтобы оно выпадало по клику на кнопку в виде картинки в CCeCommandBar-е. Задача казалось бы тривиальная... Ан нет, не все тут так просто.
В первую очередь необходимо сделать наследника от CMenu, реализовав в нем функции отрисовки. Тут все по MSDN-у:
virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct); virtual void MeasureItem(LPMEASUREITEMSTRUCT lpMIS);
переопределили, проверили через CMenu::TrackPopupMenu() и успокоились, обозвав новый класс CMenuEx. Теперь нужно "вставить" его в CCeCommandBar диалога.
Вообще, для вставки туда меню сущевствует лишь один-единственный метод:
CMenu* CCeCommandBar::InsertMenuBar(LPCTSTR lpszMenuName, int nButton = CMDBAR_END);
Этот самый метод грузит из ресурсов HMENU и "запихивает" его, точнее его главные пункты, в комманд-бар внизу экрана. Так-как возвращает он CMenu, то это уже почти хорошо: можем мучать меню, как хотим. В том числе, добавлять и удалять owner-draw пункты. Но, отрисовка-то идет именно в классе CMenuEx, а его еще нужно выставить. В принципе, можно было сделать просто:
// ... CMenu* pMenu = pCeCommandBar->InsertMenuBar(...); HMENU hMenu = pMenu->m_hMenu; pMenu->Detach(); CMenuEx* pMenuEx = new CMenuEx(); pMenuEx->Attach(hMenu); // Здесь уже изменяем CMenuEx как хотим
Таким образом мы бы переаттачили хендл меню к новому классу, но было бы это жуть как криво, я так делать даже не пробовал. Во-первых, потому-что CCeCommandBar::GetMenuBar() нам будет поле этого возвращать указатель на не-валидное меню, во-вторых нам бы пришлось хранить в ресурсах липовое меню, а в-третьих, работать просто так это все-равно не совсем будет =) (почему - обьясню дальше). Но пока это был единственный способ, который делать не хотелось. Да и кроме того, не была решена еще одна проблемма - пункт меню на комманд-баре был текстовым, а мне нужна была картинка.
Я надеюсь, что все знают (наивный =), что все, что есть на комманд-баре ни что иное, как обыкновенные кнопки, которые можно изменять и удалять. И чтобы не мучиться, делаем наследника от CCeCommandBar, где изменяем нужную нам кнопку с текстом меню на новую, с картинкой:
BOOL CCeCommandBarEx::SetButtonMenu(int nButton, int nBitmap) { CToolBarCtrl* pToolBar = &GetToolBarCtrl(); pToolBar->DeleteButton(nButton); // Убиваем старую кнопку // Получаем информацию о бывшей кнопке TBBUTTON btn; memset(&btn, 0, sizeof(btn)); pToolBar->GetButton(nButton, &btn); // Убираем текст и ставим картинку btn.iString = -1; btn.iBitmap = nBitmap; BOOL nResult = pToolBar->InsertButton(nButton, &btn); SetSizes(CSize(23, 21), CSize(16, 15)); return nResult; }
Работать будет стабильно. Но, это хорошо в случае, когда нужно сделать только замену текста в контрол-баре на картинку. У меня-же меню генерилось в CMenuEx динамически, поэтому менять просто нечего было. Ладно, лезем в CCeCommandBar::InsertMenuBar:
CMenu* CCeCommandBar::InsertMenuBar(LPCTSTR lpszMenuName, int nButton /*= CMDBAR_END*/) { // ... HINSTANCE hInst = AfxFindResourceHandle(lpszMenuName, RT_MENU); HMENU hMenu = ::LoadMenu(hInst, lpszMenuName); TBBUTTON tbButton; memset(&tbButton, 0, sizeof(TBBUTTON)); tbButton.iBitmap = I_IMAGENONE; tbButton.fsState = TBSTATE_ENABLED; tbButton.fsStyle = TBSTYLE_DROPDOWN | TBSTYLE_NO_DROPDOWN_ARROW | TBSTYLE_AUTOSIZE; // используется TBSTYLE_NO_DROPDOWN_ARROW, которого нету в доккументации MSDN MENUITEMINFO mii; TCHAR szMenuItem[256]; memset(&mii, 0, sizeof(MENUITEMINFO)); mii.fMask = MIIM_TYPE; mii.fType = MFT_STRING; mii.dwTypeData = szMenuItem; int nIndex = 0; while(TRUE) { tbButton.dwData = (DWORD)::GetSubMenu(hMenu, nIndex); // Хендл попап-меню if(tbButton.dwData == NULL) break; // ... tbButton.iString = (int)szMenuItem; tbButton.idCommand = m_wNextMenuID++; // Интересно... // Вот здесь добавляется кнопка с текстом меню: VERIFY(DefWindowProc(TB_INSERTBUTTON, nButton, (LPARAM)&tbButton)); // ... } m_nCount = GetNumButtons(); CMenu* pMenu = new CMenu; if(pMenu != NULL) { pMenu->Attach(hMenu); m_pMenuArray.Add(pMenu); } // ... return pMenu; }
Первое, что тут борсилось в глаза, это стиль TBSTYLE_NO_DROPDOWN_ARROW, которого нету в доккументации. Ладно, теперь будет: =)
#define TBSTYLE_NO_DROPDOWN_ARROW 0x0080
Но, одно лишь выставление этого стиля + TBSTYLE_DROPDOWN мне _почти_ ничего не дало, а именно, кнопка стала выглядеть нажатой во время выпадения меню (которое делалось в WM_NOTIFY через CMenuEx::TrackPopupMenu(), как у обычной TBSTYLE_DROPDOWN кнопки). Но все-таки не так, как нормальный пункт меню (оставался вверху пробел). Идем дальше.
Следующее, что бросилось мне в глаза - это строка
tbButton.dwData = (DWORD)::GetSubMenu(hMenu, nIndex);
Тут хендл меню выставляется в качестве dwData кнопки. Не долго думая, я прошарил по исходникам MFC в поисках виндовой процедуры, которая будет обрисовывать и отрабатывать такую кнопку, но ничего подобного я не нашел. А значит, показ меню и обработку такой чудо-кнопки выполняет не MFC, а WinCE API! Реализовываем добавление меню к уже сущевствующей кнопке:
BOOL CCeCommandBarEx::SetButtonMenu(int nButton, CMenu* pMenu) { CToolBarCtrl* pToolBar = &GetToolBarCtrl(); TBBUTTON btn; memset(&btn, 0, sizeof(btn)); pToolBar->GetButton(nButton, &btn); pToolBar->DeleteButton(nButton); // Убиваем старую кнопку // Для стиля TBSTYLE_NO_DROPDOWN_ARROW | TBSTYLE_DROPDOWN член dwData выставляется // как хендл нашего pop-up: btn.fsStyle = TBSTYLE_DROPDOWN | TBSTYLE_NO_DROPDOWN_ARROW | TBSTYLE_AUTOSIZE; btn.dwData = (DWORD)pMenu->m_hMenu; btn.idCommand = 0; // Это влом объяснять, почему так. return pToolBar->InsertButton(nButton, &btn); }
Ура, работает! =) Но, не совсем. При добавлении в меню пунктов с собственной отрисовкой, все падает по ASSERT-у. Тут до меня дошло, что кто-то из окон должен принимать WM_MEASUREITEM и WM_DRAWITEM сообщения. В WinAPI это деоает параметр hWnd функции ::TrackPopupMenu(). Но, в MFC это дело как-то обрабатывается в CMenu! Причем, само CMenu даже от CCmdTarget наследником не является, не говоря уж про CWnd... Что-то мне подсказывало, что тут не все так просто. За сим мнением я полез в CMenu::TrackPopupMenu().
И что-же он делает? Он выставляет окно, которое будет получать все сообщения PopUp меню, HMENU отрисовываемого меню в специально отведненные для этого члены класса _AFX_THREAD_STATE. А после показа меню через WinCE API ::TrackPopupMenu(...) сбрасывает это окно на бывшее.
// ... _AFX_THREAD_STATE* pThreadState = AfxGetThreadState(); pThreadState->m_hTrackingWindow = pWnd->m_hWnd; pThreadState->m_hTrackingMenu = m_hMenu; BOOL bOK = ::WCE_FCTN(TrackPopupMenu)(m_hMenu, nFlags, x, y, 0, pThreadState->m_hTrackingWindow, lpRect); // ...
Ладненько, одним CMenu значит тут все не обошлось. Лезем в CWnd.
CWnd в свою очередь имеет собственную реализацию двух методов, которые отвечают за отрисовку: это OnDrawItem и OnMeasureItem. Метод OnDrawItem реализован достаточно просто.
void CWnd::OnDrawItem(int /*nIDCtl*/, LPDRAWITEMSTRUCT lpDrawItemStruct) { if (lpDrawItemStruct->CtlType == ODT_MENU) { CMenu* pMenu = CMenu::FromHandlePermanent( (HMENU)lpDrawItemStruct->hwndItem); // Здесь pMenu уже указывает на наш экземпляр CMenuEx if (pMenu != NULL) { pMenu->DrawItem(lpDrawItemStruct); return; // eat it } } else { // reflect notification to child window control if (ReflectLastMsg(lpDrawItemStruct->hwndItem)) return; // eat it } // not handled - do default Default(); }
Так-как наше меню subclassed на CMenuEx, то CMenu::FromHandlePermanent() вернет из карты указателей экземпляр именно нашего объекта, в котором и будет вызван метод DrawItem(). Тут проблемм возникнуть не должно, благо handle меню передается через структуру информации отрисовки. Зато они возникнут на OnMeasureItem:
void CWnd::OnMeasureItem(int /*nIDCtl*/, LPMEASUREITEMSTRUCT lpMeasureItemStruct) { if (lpMeasureItemStruct->CtlType == ODT_MENU) { ASSERT(lpMeasureItemStruct->CtlID == 0); CMenu* pMenu; _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData(); if (pThreadState->m_hTrackingWindow == m_hWnd) { // Сюда мы не попадаем, т.к. m_hTrackingWindow == NULL во время вызова // из CCeCommandBar pMenu = CMenu::FromHandle(pThreadState->m_hTrackingMenu); } else { // А приаттаченого меню у нашего окна нет, поэтому GetMenu() вернет NULL pMenu = GetMenu(); } if (pMenu != NULL) pMenu->MeasureItem(lpMeasureItemStruct); // ... } // not handled - do default Default(); }
Так-как наше окно не является m_hTrackingWindow окном, которое на практике вообще не выставлено во время вызова из CCeCommandBar (m_hTrackingWindow == NULL) ибо вызов TrackMenuBar() из CCeCommandBar идет не через CMenu, то
указателя на экземпляр класса, в котором вызывать MeasureItem() мы так и не получим. Более того, мы вообще не можем узнать по умолчанию, какое именно меню мы пытаемся нарисовать. Через MEASUREITEMSTRUCT мы можем получить только itemID конкретного пункта меню, и itemData этого пункта. (почему разработчикам MFC было впадлу включить HANDLE вызвавшего сообщение WM_MEASUREITEM контрола, как это сделано у DRAWITEMSTRUCT через hwndItem - для меня остается загадкой...)
Так-как действовать больше неоткуда, будем действовать через itemData. А именно, каждому пункту меню через dwItemData будем передавать структуру, содержащую указатель на экземпляр CMenu и остальные данные, специфичные для конкретной реализации потомка CMenu (у нас ведь может быть много разных типов owner-draw меню).
struct MenuItemData { CMenu* pMenu; // Указатель на наше меню, должен быть первым членом в структуре // .. остальные данные };
После выставления у всех итемов меню UserData в MenuItemData*, остается лишь переопределить обработку OnMeasureItem у класса CCeCommandBar (в Remote Spy++ отловил, что именно его окно получает WM_MEASUREITEM и WM_DRAWITEM сообщения):
void CCeCommandBarEx::OnMeasureItem(int /*nIDCtl*/, LPMEASUREITEMSTRUCT lpMeasureItemStruct) { if (lpMeasureItemStruct->CtlType == ODT_MENU) { ASSERT(lpMeasureItemStruct->CtlID == 0); DWORD* pItemDataStruct = (DWORD*)lpMeasureItemStruct->itemData; CMenu* pMenu = (CMenu*)pItemDataStruct[0]; // Accessing first data member pMenu = _AfxFindPopupMenuFromID(pMenu, lpMeasureItemStruct->itemID); if (pMenu != NULL) pMenu->MeasureItem(lpMeasureItemStruct); return; // ready } else { CWnd* pChild = GetDescendantWindow(lpMeasureItemStruct->CtlID, TRUE); if (pChild != NULL && pChild->SendChildNotifyLastMsg()) return; // eaten by child } // not handled - do default Default(); }
И все. Ура, заработало! =)
--
(с) 2005 JohnCapfull.