Figure 2 MenuTipper

MenuTipper.h
///////////////////////////////////////////////////////////////
// MSDN Magazine November 2003
// If this code works, it was written by Paul DiLascia.
// If not, I don't know who wrote it.
// Compiles with Visual Studio .NET on Windows XP. Tab size=3.
//
#pragma once
#include "PupText.h"
#include "subclass.h"

//////////////////
// Implement menu tips for any MFC main window. To use:
//
// - instantiate CMenuTipManager in your CMainFrm
// - call Install
// - implement prompt strings the normal way: as resource 
//   strings w/ID=command ID.
//
class CMenuTipManager : public CSubclassWnd {
protected:
   CPopupText m_wndTip;    // home-grown "tooltip"
   BOOL m_bMouseSelect;    // whether menu invoked by mouse
   BOOL m_bSticky;         // after first tip appears, show 
                           // rest immediately

public:
   int m_iDelay;           // tooltip delay: you can change

   CMenuTipManager() : m_iDelay(1000), m_bSticky(FALSE) { }
   ~CMenuTipManager() { }

   // call this to install tips
   void Install(CWnd* pWnd) { HookWindow(pWnd); }

   // Useful helpers to get window/rect of current active menu
   static CWnd* GetRunningMenuWnd();
   static void  GetRunningMenuRect(CRect& rcMenu);
   CRect GetMenuTipRect(HMENU hmenu, UINT nID);

   // Useful helper to get the prompt string for a command ID.
   // Like CFrameWnd::GetMessageString, but you don't need a
   // frame wnd.
   static CString GetResCommandPrompt(UINT nID);

   // Get the prompt for given command ID
   virtual CString OnGetCommandPrompt(UINT nID)
   {
      return GetResCommandPrompt(nID);
   }

   // hook fn to trap main window's messages
   virtual LRESULT WindowProc(UINT msg, WPARAM wp, LPARAM lp);

   // Call these handlers from your main window
   void OnMenuSelect(UINT nItemID, UINT nFlags, HMENU hMenu);
   void OnEnterIdle(UINT nWhy, HWND hwndWho);
};
MenuTipper.cpp
///////////////////////////////////////////////////////////////
// MSDN Magazine November 2003
// If this code works, it was written by Paul DiLascia.
// If not, I don't know who wrote it.
// Compiles with Visual Studio .NET on Windows XP. Tab size=3.
//
#include "stdafx.h"
#include "MenuTipper.h"
#include <afxpriv.h> // for AfxLoadString

//////////////////
// This is more or less copied from 
// CFrameWnd::GetMessageString, but I want a static function 
// that doesn't require a frame window.
//
CString CMenuTipManager::GetResCommandPrompt(UINT nID)
{
   // load appropriate string
   CString s;
   if (s.LoadString(nID)) {
      LPTSTR lpsz = s.GetBuffer(255);
      // first newline terminates prompt
      lpsz = _tcschr(lpsz, '\n');
      if (lpsz != NULL)
         *lpsz = '\0';
      s.ReleaseBuffer();
   }
   return s;
}

//////////////////
// Override CSubclassWnd::WindowProc to hook messages on behalf 
// of main window.
//
LRESULT CMenuTipManager::WindowProc(UINT msg, WPARAM wp, LPARAM lp)
{
   if (msg==WM_MENUSELECT) {
      OnMenuSelect(LOWORD(wp), HIWORD(wp), (HMENU)lp);
   } else if (msg==WM_ENTERIDLE) {
      OnEnterIdle(wp, (HWND)lp);
   }
   return CSubclassWnd::WindowProc(msg, wp, lp);
}

//////////////////
// Got WM_MENUSELECT: show tip.
//
void CMenuTipManager::OnMenuSelect(UINT nItemID, UINT nFlags, 
    HMENU hMenu)
{
   if (!m_wndTip.m_hWnd) {
      m_wndTip.Create(CPoint(0,0), CWnd::FromHandle(m_hWnd));
      m_wndTip.m_szMargins = CSize(4,0);
   }

   if ((nFlags & 0xFFFF)==0xFFFF) {
      m_wndTip.Cancel();   // cancel/hide tip
      m_bMouseSelect = FALSE;
      m_bSticky = FALSE;

   } else if (nFlags & MF_POPUP) {
      m_wndTip.Cancel();   // new popup: cancel
      m_bSticky = FALSE;

   } else if (nFlags & MF_SEPARATOR) {
      // separator: hide tip but remember sticky state
      m_bSticky = m_wndTip.IsWindowVisible();
      m_wndTip.Cancel();

   } else if (nItemID && hMenu) {
      // if tips already displayed, keep displayed
      m_bSticky = m_wndTip.IsWindowVisible() || m_bSticky;

      // remember if mouse used to invoke menu
      m_bMouseSelect = (nFlags & MF_MOUSESELECT)!=0;

      // get prompt and display tip (with or without timeout)
      CString prompt = OnGetCommandPrompt(nItemID);
      if (prompt.IsEmpty())
         m_wndTip.Cancel(); // no prompt: cancel tip

      else {
         CRect rc = GetMenuTipRect(hMenu, nItemID);
         m_wndTip.SetWindowPos(&CWnd::wndTopMost, rc.left, 
            rc.top, rc.Width(), rc.Height(), SWP_NOACTIVATE);
         m_wndTip.SetWindowText(prompt);
         m_wndTip.ShowDelayed(m_bSticky ? 0 : m_iDelay);
      }
   }
}

//////////////////
// Calculate position of tip: next to menu item.
//
CRect CMenuTipManager::GetMenuTipRect(HMENU hmenu, UINT nID)
{
   CWnd* pWndMenu = GetRunningMenuWnd(); //CWnd::WindowFromPoint(pt);
   ASSERT(pWndMenu);
   CRect rcMenu;
   pWndMenu->GetWindowRect(rcMenu); // whole menu rect

   // add heights of menu items until i reach nID
   int count = ::GetMenuItemCount(hmenu);
   int cy = rcMenu.top + GetSystemMetrics(SM_CYEDGE)+1;
   for (int i=0; i<count; i++) {
      CRect rc;
      ::GetMenuItemRect(m_hWnd, hmenu, i, &rc);
      if (::GetMenuItemID(hmenu,i)==nID) {
         // found menu item: adjust rectangle to right and down
         rc += CPoint(rcMenu.right - rc.left, cy - rc.top);
         return rc;
      }
      cy += rc.Height(); // add height
   }
   return CRect(0,0,0,0);
}

//////////////////
// Note that windows are enumerated in top-down Z-order, so the 
// menu window should always be the first one found.
//
static BOOL CALLBACK MyEnumProc(HWND hwnd, LPARAM lParam)
{
   char buf[16];
   GetClassName(hwnd,buf,sizeof(buf));
   if (strcmp(buf,"#32768")==0) { // special classname for 
                                  // menus
      *((HWND*)lParam) = hwnd;    // found it
      return FALSE;
   }
   return TRUE;
}

//////////////////
// Get running menu window.
//
CWnd* CMenuTipManager::GetRunningMenuWnd()
{
   HWND hwnd = NULL;
   EnumWindows(MyEnumProc,(LPARAM)&hwnd);
   return CWnd::FromHandle(hwnd);
}

//////////////////
// Need to handle WM_ENTERIDLE to cancel the tip if the user 
// moved the mouse off the popup menu. For main menus, Windows 
// will send a WM_MENUSELECT message for the parent menu when 
// this happens, but for context menus there's no other way to 
// know the user moved the mouse off the menu.
//
void CMenuTipManager::OnEnterIdle(WPARAM nWhy, HWND hwndWho)
{
   if (m_bMouseSelect && nWhy==MSGF_MENU) {
      CPoint pt;
      GetCursorPos(&pt);
      if (hwndWho != ::WindowFromPoint(pt)) {
         m_wndTip.Cancel();
      }
   }
}