1 ////////////////////////////////////////////////////////////////
2 // Copyright 1998 Paul DiLascia
3 // If this code works, it was written by Paul DiLascia.
4 // If not, I don't know who wrote it.
6 // CMenuBar implements menu bar for MFC. See MenuBar.h for how
7 // to use, and also the MBTest sample application.
10 #include "UIMenuBar.h"
12 const UINT MB_SET_MENU_NULL = WM_USER + 1100;
17 static char THIS_FILE[] = __FILE__;
20 // if you want to see extra TRACE diagnostics, set CMenuBar::bTRACE = TRUE
21 BOOL CMenuBar::bTRACE = FALSE;
26 if (CMenuBar::bTRACE)\
29 if (CMenuBar::bTRACE)\
32 #define MBTRACEFN TRACE
36 IMPLEMENT_DYNAMIC(CMenuBar, CFlatToolBar)
38 BEGIN_MESSAGE_MAP(CMenuBar, CFlatToolBar)
43 ON_UPDATE_COMMAND_UI_RANGE(0, 256, OnUpdateMenuButton)
44 ON_MESSAGE(MB_SET_MENU_NULL, OnSetMenuNull)
49 if (iVerComCtl32 <= 470)
50 AfxMessageBox(_T("Warning: This program requires comctl32.dll version 4.71 or greater."));
52 m_iTrackingState = TRACK_NONE; // initial state: not tracking
53 m_iPopupTracking = m_iNewPopup = -1; // invalid
55 m_hMenuTracking = NULL;
56 m_bAutoRemoveFrameMenu = TRUE; // set frame's menu to NULL
64 // Menu bar was created: install hook into owner window
66 int CMenuBar::OnCreate(LPCREATESTRUCT lpCreateStruct)
68 if (CFlatToolBar::OnCreate(lpCreateStruct)==-1)
71 CWnd* pFrame = GetOwner();
73 m_frameHook.Install(this, *pFrame);
78 // Set menu bar font from current system menu font
80 void CMenuBar::UpdateFont()
83 NONCLIENTMETRICS info;
84 info.cbSize = sizeof(info);
85 SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(info), &info, 0);
88 VERIFY(font.CreateFontIndirect(&info.lfMenuFont));
93 // The reason for having this is so MFC won't automatically disable
94 // the menu buttons. Assumes < 256 top-level menu items. The ID of
95 // the ith menu button is i. IOW, the index and ID are the same.
97 void CMenuBar::OnUpdateMenuButton(CCmdUI* pCmdUI)
100 if (IsValidButton(pCmdUI->m_nID))
101 pCmdUI->Enable(TRUE);
105 // Recompute layout of menu bar
107 void CMenuBar::RecomputeMenuLayout()
109 SetWindowPos(NULL, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOACTIVATE |
110 SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER);
114 // Make frame recalculate control bar sizes after menu change
116 void CMenuBar::RecomputeToolbarSize()
118 // Force toolbar to recompute size
119 CFrameWnd* pFrame = (CFrameWnd*)GetOwner();
120 ASSERT_VALID(pFrame);
121 ASSERT(pFrame->IsFrameWnd());
122 pFrame->RecalcLayout();
125 pFrame = GetParentFrame();
126 if (pFrame->IsKindOf(RUNTIME_CLASS(CMiniFrameWnd)))
127 pFrame->RecalcLayout();
131 // Set tracking state: none, button, or popup
133 void CMenuBar::SetTrackingState(TRACKINGSTATE iState, int iButton)
136 if (iState != m_iTrackingState) {
137 if (iState == TRACK_NONE)
141 static LPCTSTR StateName[] = { _T("NONE"), _T("BUTTON"), _T("POPUP") };
142 MBTRACE(_T("CMenuBar::SetTrackingState to %s, button=%d\n"),
143 StateName[iState], iButton);
146 SetHotItem(iButton); // could be none (-1)
148 if (iState==TRACK_POPUP) {
149 // set related state stuff
150 m_bEscapeWasPressed = FALSE; // assume Esc key not pressed
151 m_bProcessRightArrow = // assume left/right arrow..
152 m_bProcessLeftArrow = TRUE; // ..will move to prev/next popup
153 m_iPopupTracking = iButton; // which popup I'm tracking
155 m_iTrackingState = iState;
160 // Toggle state from home state to button-tracking and back
162 void CMenuBar::ToggleTrackButtonMode()
165 if (m_iTrackingState == TRACK_NONE || m_iTrackingState == TRACK_BUTTON) {
166 SetTrackingState(m_iTrackingState == TRACK_NONE ?
167 TRACK_BUTTON : TRACK_NONE, 0);
172 // Get button index before/after a given button
174 int CMenuBar::GetNextOrPrevButton(int iButton, BOOL bPrev)
180 iButton = GetButtonCount() - 1;
183 if (iButton >= GetButtonCount())
190 // This is to correct a bug in the system toolbar control: TB_HITTEST only
191 // looks at the buttons, not the size of the window. So it returns a button
192 // hit even if that button is totally outside the size of the window!
194 int CMenuBar::HitTest(CPoint p) const
196 int iHit = CFlatToolBar::HitTest(p);
200 if (!rc.PtInRect(p)) // if point is outside window
201 iHit = -1; // can't be a hit!
207 // Load a different menu. The HMENU must not belong to any CMenu,
208 // and you must free it when you're done. Returns old menu.
210 HMENU CMenuBar::LoadMenu(HMENU hmenu)
212 UINT iPrevID=(UINT)-1;
213 ASSERT(::IsMenu(hmenu));
216 if (m_bAutoRemoveFrameMenu) {
217 CFrameWnd* pFrame = GetParentFrame();
218 if (::GetMenu(*pFrame)!=NULL) {
219 // I would like to set the frame's menu to NULL now, but if I do, MFC
220 // gets all upset: it calls GetMenu and expects to have a real menu.
221 // So Instead, I post a message to myself. Because the message is
222 // posted, not sent, I won't process it until MFC is done with all its
223 // initialization stuff. (MFC needs to set CFrameWnd::m_hMenuDefault
224 // to the menu, which it gets by calling GetMenu.)
226 PostMessage(MB_SET_MENU_NULL, (WPARAM)pFrame->GetSafeHwnd());
229 HMENU hOldMenu = m_hmenu;
232 // delete existing buttons
233 int nCount = GetButtonCount();
235 VERIFY(DeleteButton(0));
239 // SetButtonSize(CSize(0,0)); // This barfs in VC 6.0
241 DWORD dwStyle = GetStyle();
242 BOOL bModifyStyle = ModifyStyle(0, TBSTYLE_FLAT|TBSTYLE_TRANSPARENT);
245 UINT nMenuItems = hmenu ? ::GetMenuItemCount(hmenu) : 0;
247 for (UINT i=0; i < nMenuItems; i++) {
249 memset(name, 0, sizeof(name)); // guarantees double-0 at end
250 if (::GetMenuString(hmenu, i, name, countof(name)-1, MF_BYPOSITION)) {
252 memset(&tbb, 0, sizeof(tbb));
253 tbb.idCommand = ::GetMenuItemID(hmenu, i);
255 // Because the toolbar is too brain-damaged to know if it already has
256 // a string, and is also too brain-dead to even let you delete strings,
257 // I have to determine if each string has been added already. Otherwise
258 // in a MDI app, as the menus are repeatedly switched between doc and
259 // no-doc menus, I will keep adding strings until somebody runs out of
263 for (int j=0; j<m_arStrings.GetSize(); j++) {
264 if (m_arStrings[j] == name) {
265 iString = j; // found it
270 // string not found: add it
271 iString = AddStrings(name);
272 m_arStrings.SetAtGrow(iString, name);
275 tbb.iString = iString;
276 tbb.fsState = TBSTATE_ENABLED;
277 tbb.fsStyle = TBSTYLE_AUTOSIZE;
280 VERIFY(AddButtons(1, &tbb));
285 SetWindowLong(m_hWnd, GWL_STYLE, dwStyle);
288 AutoSize(); // size buttons
289 RecomputeToolbarSize(); // and menubar itself
295 // Load menu from resource
297 HMENU CMenuBar::LoadMenu(LPCTSTR lpszMenuName)
299 return LoadMenu(::LoadMenu(AfxFindResourceHandle(lpszMenuName,RT_MENU),lpszMenuName));
303 // Set the frame's menu to NULL. WPARAM is HWND of frame.
305 LRESULT CMenuBar::OnSetMenuNull(WPARAM wp, LPARAM lp)
307 HWND hwnd = (HWND)wp;
308 ASSERT(::IsWindow(hwnd));
309 ::SetMenu(hwnd, NULL);
314 // Handle mouse click: if clicked on button, press it
315 // and go into main menu loop.
317 void CMenuBar::OnLButtonDown(UINT nFlags, CPoint pt)
320 int iButton = HitTest(pt);
321 if (iButton >= 0 && iButton<GetButtonCount()) // if mouse is over a button:
322 TrackPopup(iButton); // track it
324 CFlatToolBar::OnLButtonDown(nFlags, pt); // pass it on...
328 // Handle mouse movement
330 void CMenuBar::OnMouseMove(UINT nFlags, CPoint pt)
334 if (m_iTrackingState==TRACK_BUTTON) {
336 // In button-tracking state, ignore mouse-over to non-button area.
337 // Normally, the toolbar would de-select the hot item in this case.
339 // Only change the hot item if the mouse has actually moved.
340 // This is necessary to avoid a bug where the user moves to a different
341 // button from the one the mouse is over, and presses arrow-down to get
342 // the menu, then Esc to cancel it. Without this code, the button will
343 // jump to wherever the mouse is--not right.
345 int iHot = HitTest(pt);
346 if (IsValidButton(iHot) && pt != m_ptMouse)
348 return; // don't let toolbar get it
350 m_ptMouse = pt; // remember point
351 CFlatToolBar::OnMouseMove(nFlags, pt);
355 // Window was resized: need to recompute layout
357 void CMenuBar::OnSize(UINT nType, int cx, int cy)
359 CFlatToolBar::OnSize(nType, cx, cy);
360 RecomputeMenuLayout();
364 // Bar style changed: eg, moved from left to right dock or floating
366 void CMenuBar::OnBarStyleChange(DWORD dwOldStyle, DWORD dwNewStyle)
368 CFlatToolBar::OnBarStyleChange(dwOldStyle, dwNewStyle);
369 RecomputeMenuLayout();
373 // When user selects a new menu item, note whether it has a submenu
374 // and/or parent menu, so I know whether right/left arrow should
375 // move to the next popup.
377 void CMenuBar::OnMenuSelect(HMENU hmenu, UINT iItem)
379 if (m_iTrackingState > 0) {
380 // process right-arrow iff item is NOT a submenu
381 m_bProcessRightArrow = (::GetSubMenu(hmenu, iItem) == NULL);
382 // process left-arrow iff curent menu is one I'm tracking
383 m_bProcessLeftArrow = hmenu==m_hMenuTracking;
387 // globals--yuk! But no other way using windows hooks.
389 static CMenuBar* g_pMenuBar = NULL;
390 static HHOOK g_hMsgHook = NULL;
393 // Menu filter hook just passes to virtual CMenuBar function
396 CMenuBar::MenuInputFilter(int code, WPARAM wp, LPARAM lp)
398 return (code==MSGF_MENU && g_pMenuBar &&
399 g_pMenuBar->OnMenuInput(*((MSG*)lp))) ? TRUE
400 : CallNextHookEx(g_hMsgHook, code, wp, lp);
404 // Handle menu input event: Look for left/right to change popup menu,
405 // mouse movement over over a different menu button for "hot" popup effect.
406 // Returns TRUE if message handled (to eat it).
408 BOOL CMenuBar::OnMenuInput(MSG& m)
411 ASSERT(m_iTrackingState == TRACK_POPUP); // sanity check
414 if (msg==WM_KEYDOWN) {
415 // handle left/right-arow.
416 TCHAR vkey = m.wParam;
418 MBTRACE(_T("CMenuBar::OnMenuInput: handle VK_LEFT - m_bProcessLeftArrow=%d\n"),m_bProcessLeftArrow);
419 else if (vkey == VK_RIGHT)
420 MBTRACE(_T("CMenuBar::OnMenuInput: handle RIGHT - m_bProcessRightArrow=%d\n"),m_bProcessRightArrow);
421 if ((vkey == VK_LEFT && m_bProcessLeftArrow) ||
422 (vkey == VK_RIGHT && m_bProcessRightArrow)) {
423 CancelMenuAndTrackNewOne(
424 GetNextOrPrevButton(m_iPopupTracking, vkey==VK_LEFT));
425 return TRUE; // eat it
427 } else if (vkey == VK_ESCAPE) {
428 m_bEscapeWasPressed = TRUE; // (menu will abort itself)
431 } else if (msg==WM_MOUSEMOVE || msg==WM_LBUTTONDOWN) {
432 // handle mouse move or click
433 CPoint pt = m.lParam;
436 if (msg == WM_MOUSEMOVE) {
437 if (pt != m_ptMouse) {
438 int iButton = HitTest(pt);
439 if (IsValidButton(iButton) && iButton != m_iPopupTracking) {
440 // user moved mouse over a different button: track its popup
441 CancelMenuAndTrackNewOne(iButton);
446 } else if (msg == WM_LBUTTONDOWN) {
447 if (HitTest(pt) == m_iPopupTracking) {
448 // user clicked on same button I am tracking: cancel menu
449 MBTRACE(_T("CMenuBar:OnMenuInput: handle mouse click to exit popup\n"));
450 CancelMenuAndTrackNewOne(-1);
451 return TRUE; // eat it
455 return FALSE; // not handled
459 // Cancel the current popup menu by posting WM_CANCELMODE, and track a new
460 // menu. iNewPopup is which new popup to track (-1 to quit).
462 void CMenuBar::CancelMenuAndTrackNewOne(int iNewPopup)
464 MBTRACE(_T("CMenuBar::CancelMenuAndTrackNewOne: %d\n"), iNewPopup);
466 if (iNewPopup != m_iPopupTracking) {
467 GetOwner()->PostMessage(WM_CANCELMODE); // quit menu loop
468 m_iNewPopup = iNewPopup; // go to this popup (-1 = quit)
473 // Track the popup submenu associated with the i'th button in the menu bar.
474 // This fn actually goes into a loop, tracking different menus until the user
475 // selects a command or exits the menu.
477 void CMenuBar::TrackPopup(int iButton)
479 MBTRACE(_T("CMenuBar::TrackPopup %d\n"), iButton);
484 menu.Attach(m_hmenu);
485 int nMenuItems = menu.GetMenuItemCount();
487 while (iButton >= 0) { // while user selects another menu
489 m_iNewPopup = -1; // assume quit after this
490 PressButton(iButton, TRUE); // press the button
491 UpdateWindow(); // and force repaint now
493 // post a simulated arrow-down into the message stream
494 // so TrackPopupMenu will read it and move to the first item
495 GetOwner()->PostMessage(WM_KEYDOWN, VK_DOWN, 1);
496 GetOwner()->PostMessage(WM_KEYUP, VK_DOWN, 1);
498 SetTrackingState(TRACK_POPUP, iButton); // enter tracking state
500 // Need to install a hook to trap menu input in order to make
501 // left/right-arrow keys and "hot" mouse tracking work.
503 ASSERT(g_pMenuBar == NULL);
505 ASSERT(g_hMsgHook == NULL);
506 g_hMsgHook = SetWindowsHookEx(WH_MSGFILTER,
507 MenuInputFilter, NULL, ::GetCurrentThreadId());
509 // get submenu and display it beneath button
512 GetRect(iButton, rcButton);
513 ClientToScreen(&rcButton);
514 CPoint pt = ComputeMenuTrackPoint(rcButton, tpm);
515 HMENU hMenuPopup = ::GetSubMenu(m_hmenu, iButton);
517 m_hMenuTracking = hMenuPopup;
518 BOOL bRet = TrackPopupMenuEx(hMenuPopup,
519 TPM_LEFTALIGN|TPM_LEFTBUTTON|TPM_VERTICAL,
520 pt.x, pt.y, GetOwner()->GetSafeHwnd(), &tpm);
523 ::UnhookWindowsHookEx(g_hMsgHook);
527 PressButton(iButton, FALSE); // un-press button
528 UpdateWindow(); // and force repaint now
530 // If the user exited the menu loop by pressing Escape,
531 // return to track-button state; otherwise normal non-tracking state.
532 SetTrackingState(m_bEscapeWasPressed ?
533 TRACK_BUTTON : TRACK_NONE, iButton);
535 // If the user moved mouse to a new top-level popup (eg from File to
536 // Edit button), I will have posted a WM_CANCELMODE to quit
537 // the first popup, and set m_iNewPopup to the new menu to show.
538 // Otherwise, m_iNewPopup will be -1 as set above.
539 // So just set iButton to the next popup menu and keep looping...
540 iButton = m_iNewPopup;
543 m_hMenuTracking = m_hmenu;
547 // Given button rectangle, compute point and "exclude rect" for
548 // TrackPopupMenu, based on current docking style, so that the menu will
549 // appear always inside the window.
551 CPoint CMenuBar::ComputeMenuTrackPoint(const CRect& rcButn, TPMPARAMS& tpm)
553 tpm.cbSize = sizeof(tpm);
554 DWORD dwStyle = m_dwStyle;
556 CRect& rcExclude = (CRect&)tpm.rcExclude;
558 ::GetWindowRect(::GetDesktopWindow(), &rcExclude);
560 switch (dwStyle & CBRS_ALIGN_ANY) {
561 case CBRS_ALIGN_BOTTOM:
562 pt = CPoint(rcButn.left, rcButn.top);
563 rcExclude.top = rcButn.top;
566 case CBRS_ALIGN_LEFT:
567 pt = CPoint(rcButn.right, rcButn.top);
568 rcExclude.right = rcButn.right;
571 case CBRS_ALIGN_RIGHT:
572 pt = CPoint(rcButn.left, rcButn.top);
573 rcExclude.left = rcButn.left;
576 default: // case CBRS_ALIGN_TOP:
577 pt = CPoint(rcButn.left, rcButn.bottom);
584 // This function translates special menu keys and mouse actions.
585 // You must call it from your frame's PreTranslateMessage.
587 BOOL CMenuBar::TranslateFrameMessage(MSG* pMsg)
591 UINT msg = pMsg->message;
592 if (WM_LBUTTONDOWN <= msg && msg <= WM_MOUSELAST) {
593 if (pMsg->hwnd != m_hWnd && m_iTrackingState > 0) {
594 // user clicked outside menu bar: exit tracking mode
595 MBTRACE(_T("CMenuBar::TranslateFrameMessage: user clicked outside menu bar: end tracking\n"));
596 SetTrackingState(TRACK_NONE);
599 } else if (msg==WM_SYSKEYDOWN || msg==WM_SYSKEYUP || msg==WM_KEYDOWN) {
601 BOOL bAlt = HIWORD(pMsg->lParam) & KF_ALTDOWN; // Alt key down
602 TCHAR vkey = pMsg->wParam; // get virt key
604 (vkey==VK_F10 && !((GetKeyState(VK_SHIFT) & 0x80000000) ||
605 (GetKeyState(VK_CONTROL) & 0x80000000) || bAlt))) {
607 // key is VK_MENU or F10 with no alt/ctrl/shift: toggle menu mode
608 if (msg==WM_SYSKEYUP) {
609 MBTRACE(_T("CMenuBar::TranslateFrameMessage: handle menu key\n"));
610 ToggleTrackButtonMode();
614 } else if ((msg==WM_SYSKEYDOWN || msg==WM_KEYDOWN)) {
615 if (m_iTrackingState == TRACK_BUTTON) {
616 // I am tracking: handle left/right/up/down/space/Esc
620 // left or right-arrow: change hot button if tracking buttons
621 MBTRACE(_T("CMenuBar::TranslateFrameMessage: VK_LEFT/RIGHT\n"));
622 SetHotItem(GetNextOrPrevButton(GetHotItem(), vkey==VK_LEFT));
625 case VK_SPACE: // (personally, I like SPACE to enter menu too)
628 // up or down-arrow: move into current menu, if any
629 MBTRACE(_T("CMenuBar::TranslateFrameMessage: VK_UP/DOWN/SPACE\n"));
630 TrackPopup(GetHotItem());
634 // escape key: exit tracking mode
635 MBTRACE(_T("CMenuBar::TranslateFrameMessage: VK_ESCAPE\n"));
636 SetTrackingState(TRACK_NONE);
641 // Handle alphanumeric key: invoke menu. Note that Alt-X
642 // chars come through as WM_SYSKEYDOWN, plain X as WM_KEYDOWN.
643 if ((bAlt || m_iTrackingState == TRACK_BUTTON) && isalnum(vkey)) {
644 // Alt-X, or else X while in tracking mode
646 if (MapAccelerator(vkey, nID)) {
647 MBTRACE(_T("CMenuBar::TranslateFrameMessage: map acclerator\n"));
648 TrackPopup(nID); // found menu mnemonic: track it
649 return TRUE; // handled
650 } else if (m_iTrackingState==TRACK_BUTTON && !bAlt) {
656 // Default for any key not handled so far: return to no-menu state
657 if (m_iTrackingState > 0) {
658 MBTRACE(_T("CMenuBar::TranslateFrameMessage: unknown key, stop tracking\n"));
659 SetTrackingState(TRACK_NONE);
663 return FALSE; // not handled, pass along
667 void CMenuBar::AssertValid() const
669 CFlatToolBar::AssertValid();
670 ASSERT(m_hmenu==NULL || ::IsMenu(m_hmenu));
671 ASSERT(TRACK_NONE<=m_iTrackingState && m_iTrackingState<=TRACK_POPUP);
672 m_frameHook.AssertValid();
675 void CMenuBar::Dump(CDumpContext& dc) const
677 CFlatToolBar::Dump(dc);
681 //////////////////////////////////////////////////////////////////
682 // CMenuBarFrameHook is used to trap menu-related messages sent to the owning
683 // frame. The same class is also used to trap messages sent to the MDI client
684 // window in an MDI app. I should really use two classes for this,
685 // but it uses less code to chare the same class. Note however: there
686 // are two different INSTANCES of CMenuBarFrameHook in CMenuBar: one for
687 // the frame and one for the MDI client window.
689 CMenuBarFrameHook::CMenuBarFrameHook()
693 CMenuBarFrameHook::~CMenuBarFrameHook()
695 HookWindow((HWND)NULL); // (unhook)
699 // Install hook to trap window messages sent to frame or MDI client.
701 BOOL CMenuBarFrameHook::Install(CMenuBar* pMenuBar, HWND hWndToHook)
703 ASSERT_VALID(pMenuBar);
704 m_pMenuBar = pMenuBar;
705 return HookWindow(hWndToHook);
708 //////////////////////////////////////////////////////////////////
709 // Trap frame/MDI client messages specific to menubar.
711 LRESULT CMenuBarFrameHook::WindowProc(UINT msg, WPARAM wp, LPARAM lp)
713 CMenuBar& mb = *m_pMenuBar;
716 // The following messages are trapped for the frame window
717 case WM_SYSCOLORCHANGE:
722 mb.OnMenuSelect((HMENU)lp, (UINT)LOWORD(wp));
725 return CSubclassWnd::WindowProc(msg, wp, lp);