Multiple Selection Menu – Part 1

Introduction

In some applications we can find menus which allow multiple selection. One example is the pop-up menu shown by Microsoft Visual Studio for adding or removing toolbar buttons.

VS Add or Remove Buttons Menu

We’ll try to do something similar in our own MFC application.

Finding a solution

By default, a native Windows pop-up menu is dismissed when the user selects an item. I could not find in the documentation any flag, message or notification that allows changing this behavior. So, one solution may be using a hook. On the first site, may be tempted to use a CBT hook, as shown in a previous article “AfxMessageBox with Auto-close” (see the link, below). That’s because a pop-up menu is a window itself (of pre-defined class “#32768”).
However, easier and better for our purpose is using a message filter hook. This type of hook can be installed by passing WH_MSGFILTER value in the first parameter of SetWindowsHookEx function.

Using an application-wide WH_MSGFILTER hook

Next example sets a WH_MSGFILTER hook in application’s main thread class. The hook procedure processes and filters the messages occurred as a result of menu input.

// Demo.h : main header file for the Demo application
//...

struct MSGFILTER_HOOK_INFO
{
   HHOOK hHook;          // hook handle
   HMENU hMenu;          // selected menu handle
   WORD wMenuItemID;     // selected menu item identifier
};
// ...

class CDemoApp : public CWinApp
{
   // ...
// Overrides
public:
   virtual BOOL InitInstance();
   virtual int ExitInstance();

// Implementation
private:
   static MSGFILTER_HOOK_INFO m_hookInfo;
   static LRESULT CALLBACK MessageProc(int nCode, WPARAM wParam, LPARAM lParam);
   static void _HandleMenuMessage(MSG* pMSG);
};
// Demo.cpp : Defines the class behaviors for the application.
//...

MSGFILTER_HOOK_INFO CDemoApp::m_hookInfo;
// ...

BOOL CDemoApp::InitInstance()
{
   // Installs WH_MSGFILTER hook
   m_hookInfo.hHook = SetWindowsHookEx(WH_MSGFILTER, // hook type 
      &CDemoApp::MessageProc,                        // hook procedure
      NULL,                                          // module handle
      GetCurrentThreadId());                         // current thread ID 

   ASSERT(NULL != m_hookInfo.hHook);
   // ...

   return TRUE;
}

// WH_MSGFILTER hook procedure
LRESULT CALLBACK CDemoApp::MessageProc(int nCode, WPARAM wParam, LPARAM lParam)
{
   switch(nCode)
   {
   case MSGF_MENU: // message is for a menu
      {
         MSG* pMSG = (MSG*)lParam;
         _HandleMenuMessage(pMSG);
      }
      break;
   }
   return ::CallNextHookEx(m_hookInfo.hHook, nCode, wParam, lParam);
}

// handles hooked menu messages
void CDemoApp::_HandleMenuMessage(MSG* pMSG)
{
   switch(pMSG->message)
   {
   case WM_MENUSELECT:
      // keep in mind selected menu handle and selected item identifier
      m_hookInfo.wMenuItemID = LOWORD(pMSG->wParam);
      m_hookInfo.hMenu = (HMENU)pMSG->lParam;
      break;
   case WM_LBUTTONUP:
      {
         switch(m_hookInfo.wMenuItemID)
         {
         case ID_VIEW_TOOLBAR:
         case ID_VIEW_STATUS_BAR:
         // cases for other menu command IDs...
            {
               // toggle check item
               MENUITEMINFO menuItemInfo = {0};
               menuItemInfo.cbSize = sizeof(MENUITEMINFO);
               menuItemInfo.fMask = MIIM_STATE;
               ::GetMenuItemInfo(m_hookInfo.hMenu, 
                                 m_hookInfo.wMenuItemID, FALSE, &menuItemInfo);
               if(MFS_CHECKED & menuItemInfo.fState) 
                  menuItemInfo.fState &= ~MFS_CHECKED;
               else
                  menuItemInfo.fState |= MFS_CHECKED;
               ::SetMenuItemInfo(m_hookInfo.hMenu, 
                                 m_hookInfo.wMenuItemID, FALSE, &menuItemInfo);

               // send command message to main window
               CWnd* pWnd = AfxGetMainWnd();
               ASSERT_VALID(pWnd);
               pWnd->SendMessage(WM_COMMAND, MAKEWPARAM( m_hookInfo.wMenuItemID, 0), 0);  

               // change to WM_NULL to prevent closing menu
               pMSG->message = WM_NULL;
            }
         }
      }
      break;
   case WM_LBUTTONDBLCLK:
      // just set WM_NULL to get rid of all default processing 
      pMSG->message = WM_NULL;
      break;
    }
}

int CDemoApp::ExitInstance()
{
   // Uninstalls WH_MSGFILTER hook
   if(NULL != m_hookInfo.hHook)
   {
      ::UnhookWindowsHookEx(m_hookInfo.hHook);
   }
   // ...
   return CWinApp::ExitInstance();
}

Demo project

The demo project attached here is a simple SDI MFC application having the “View” menu with multiple selection.

What’s next?

This article just demonstrates how to use a WH_MSGFILTER hook in order to make a menu with multiple selection. We may notice that using an application-wide hook and some hard-coded menu item IDs is not the best choice. What about if we have tens of menus with tens of menu items?
Well, in the next article we’ll implement an MFC-extension class that can serve our initial purpose in a much more flexible and object-oriented manner.

Resources

See also

Downloads

Leave a Comment