Figure 3   CCoolMenuManager

MainFrm.h


////////////////////////////////////////////////////////////////
// ROPView 1997 Microsoft Systems Journal
// If this code works, it was written by Paul DiLascia.
// If not, I don't know who wrote it.
//
#include "CoolBar.h"
#include "CoolMenu.h"

/////////////////
// Main frame window has menu manager, cool bar, status bar
//
class CMainFrame : public CFrameWnd {
public:
   CMainFrame();

protected:
   CCoolMenuManager     m_menuManager;  // manages button menus
•
•
•
// message handlers
   DECLARE_MESSAGE_MAP()
   afx_msg int  OnCreate(LPCREATESTRUCT lpCreateStruct);

   // command handlers
   afx_msg void OnViewMenuButtons();
   afx_msg void OnUpdateViewMenuButtons(CCmdUI* pCmdUI);
•
•
•
};
MainFrm.cpp
////////////////////////////////////////////////////////////////// 
RopView 1997 Microsoft Systems Journal
// If this code works, it was written by Paul DiLascia.
// If not, I don't know who wrote it.
//
#include "StdAfx.h"
#include "MainFrm.h"
#include "View.h"
#include "resource.h"
•
•
•
IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd)

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
   ON_WM_CREATE()
   ON_COMMAND(ID_VIEW_MENU_BUTTONS,           OnViewMenuButtons)
   ON_UPDATE_COMMAND_UI(ID_VIEW_MENU_BUTTONS, OnUpdateViewMenuButtons)
•
•
•
END_MESSAGE_MAP()

•
•
•

//////////////////
// Create handler creates control bars
//
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   // create toolbars, etc.
•
•
•
   // install/load cool menus
   m_menuManager.Install(this);
   m_menuManager.LoadToolbar(IDR_MAINFRAME);

   return 0;
}

//////////////////
// Command handlers for View | Hide/Show Menu Buttons
//
void CMainFrame::OnViewMenuButtons()
{
   m_menuManager.m_bShowButtons = !m_menuManager.m_bShowButtons;
}

void CMainFrame::OnUpdateViewMenuButtons(CCmdUI* pCmdUI)
{
   pCmdUI->SetCheck(m_menuManager.m_bShowButtons);
   pCmdUI->SetText(m_menuManager.m_bShowButtons ?
      _T("Hide &Menu Buttons") : _T("Show &Menu Buttons"));
}
CoolMenu.h
////////////////////////////////////////////////////////////////
// Microsoft Systems Journal. 
// If this code works, it was written by Paul DiLascia.
// If not, I don't know who wrote it.
//
#include "SubClass.h"

namespace PxLib {

extern void FillRect(CDC& dc, const CRect& rc, COLORREF color);
extern void DrawEmbossed(CDC& dc, CImageList& il, int i, CPoint p, 
                         BOOL bColor=FALSE);

extern HBITMAP LoadSysColorBitmap(LPCTSTR lpResName, BOOL bMono=FALSE);
inline HBITMAP LoadSysColorBitmap(UINT nResID, BOOL bMono=FALSE)
{
   return LoadSysColorBitmap(MAKEINTRESOURCE(nResID), bMono);
}

} // end namespace

//////////////////
// CCoolMenuManager implements "cool" menus with buttons in the M. To use:
//
//  * Instantiate in your CMainFrame.
//  * Call Install to install it
//  * Call LoadToolbars or LoadToolbar to load toolbars
//
//  Don't forget to link with CoolMenu.cpp, Subclass.cpp and DrawTool.cpp!
//
class CCoolMenuManager : private CSubclassWnd {
public:
   DECLARE_DYNAMIC(CCoolMenuManager)
   CCoolMenuManager();
   ~CCoolMenuManager();

   // You can set the Se any time
   BOOL m_bShowButtons;       // use to control whether buttons are shown
   BOOL m_bAutoAccel;         // generate auto accelerators
   BOOL m_bUseDrawState;      // use ::DrawState for disabled buttons
   BOOL m_bDrawDisabledButtonsInColor; // draw disabled buttons in color
                                       // (only if m_bUseDrawState = FALSE)

   // public functions to use
   void Install(CFrameWnd* pFrame);             // connect to main frame
   BOOL LoadToolbars(const UINT* arIDs, int n); // load multiple toolbars
   BOOL LoadToolbar(UINT nID);                  // load one toolbar

   // should never need to call:
   virtual void Destroy(); // destroys everything--to re-load new toolbars?
   virtual void Refresh(); // called when system colors, etc change
   static  void FixMFCDotBitmap();

   static BOOL bTRACE;  // Set TRUE to see extra diagnostics in DEBUG code

protected:
   CFrameWnd*     m_pFrame;      // frame window I belong to
   CUIntArray     m_arToolbarID; // array of toolbar IDs loaded
   CImageList     m_ilButtons;   // image list for all buttons
   CMapWordToPtr  m_mapIDtoImage;// maps command ID -> image list index
   CMapWordToPtr  m_mapIDtoAccel;// maps command ID -> ACCEL*
   HACCEL         m_hAccel;      // current accelerators, if any
   ACCEL*         m_pAccel;      // ..and table in memory
   CPtrList       m_menuList;    // list of HMENU's initialized
   CSize          m_szBitmap;    // size of button bitmap
   CSize          m_szButton;    // size of button (including shadow)
   CFont          m_fontMenu;    // menu font

   // helpers
   void DestroyAccel();
   void DrawMenuText(CDC& dc, CRect rc, CString text, COLORREF color);
   BOOL Draw3DCheckmark(CDC& dc, const CRect& rc, BOOL bSelected,
                        HBITMAP hbmCheck=NULL);
   void ConvertMenu(CMenu* pMenu,UINT nIndex,BOOL bSysMenu,BOOL bShowButtons);
   void LoadAccel(HACCEL hAccel);
   BOOL AppendAccelName(CString& sItemName, UINT nID);
   CFont* GetMenuFont();

   // Get button index for given command ID, or -1 if not found
   int  GetButtonIndex(WORD nID) {
      void* val;
      return m_mapIDtoImage.Lookup(nID, val) ? (int)val : -1;
   }

   // Get ACCEL structure associated with a given command ID
   ACCEL* GetAccel(WORD nID) {
      void* val;
      return m_mapIDtoAccel.Lookup(nID, val) ? (ACCEL*)val : NULL;
   }

   // window proc to hook frame using CSubclassWnd implementation
   virtual LRESULT WindowProc(UINT msg, WPARAM wp, LPARAM lp);

   // CSubclassWnd message handlers 
   virtual void OnInitMenuPopup(CMenu* pMenu, UINT nIndex, BOOL bSysMenu);
   virtual BOOL OnMeasureItem(LPMEASUREITEMSTRUCT lpms);
   virtual BOOL OnDrawItem(LPDRAWITEMSTRUCT lpds);
   virtual LONG OnMenuChar(UINT nChar, UINT nFlags, CMenu* pMenu);
   virtual void OnMenuSelect(UINT nItemID, UINT nFlags, HMENU hSysMenu);
};

//////////////////
// Friendly version of MENUITEMINFO initializes itself
//
struct CMenuItemInfo : public MENUITEMINFO {
   CMenuItemInfo()
   { memset(this, 0, sizeof(MENUITEMINFO));
     cbSize = sizeof(MENUITEMINFO);
   }
};
CoolMenu.cpp
////////////////////////////////////////////////////////////////
// 1997 Microsoft Systems Journal. 
// If this code works, it was written by Paul DiLascia.
// If not, I don't know who wrote it.
//
#include "StdAfx.h"
#include "CoolMenu.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

// if you want to see extra TRACE diagnostics, set below to TRUE
BOOL CCoolMenuManager::bTRACE = FALSE;

#ifdef _DEBUG
#define CMTRACEFN       \
   CTraceFn __fooble;   \
   if (bTRACE)          \
      TRACE
#define CMTRACE         \
   if (bTRACE)          \
      TRACE
#else
#define CMTRACEFN TRACE
#define CMTRACE   TRACE
#endif

// constants used for drawing
const CXGAP = 1;           // num pixels between button and text
const CXTEXTMARGIN = 2;    // num pixels after hilite to start text
const CXBUTTONMARGIN = 2;  // num pixels wider button is than bitmap
const CYBUTTONMARGIN = 2;  // ditto for height

// DrawText flags
const DT_MYSTANDARD = DT_SINGLELINE|DT_LEFT|DT_VCENTER;

// identifies owner-draw data as mine
const LONG MYITEMID = MAKELONG(MAKEWORD('m','i'),MAKEWORD('d','0'));

// private struct: one of the Se for each owner-draw menu item
struct CMyItemData {
   long     magicNum;      // magic number identifying me
   CString  text;          // item text
   UINT     fType;         // original item type flags
   int      iButton;       // index of button image in image list
   CMyItemData()           { magicNum = MYITEMID; }
   BOOL IsMyItemData()     { return magicNum == MYITEMID; }
};

IMPLEMENT_DYNAMIC(CCoolMenuManager, CSubclassWnd)

CCoolMenuManager::CCoolMenuManager()
{
   m_szBitmap = m_szButton = CSize(0,0);  // will compute later
   m_bShowButtons = TRUE;                 // show buttons by default
   m_bAutoAccel = TRUE;                   // auto accelerators by default
   m_hAccel = NULL;                       // no accelerators loaded yet
   m_pAccel = NULL;                       // no accelerators loaded yet
   m_bUseDrawState = FALSE;               // use DrawEmbossed by default
   m_bDrawDisabledButtonsInColor = FALSE; // use color for disabled buttons
   FixMFCDotBitmap();
}

CCoolMenuManager::~CCoolMenuManager()
{
   Destroy();
}

//////////////////
// Destroy everything. Called from destructor and Refresh.
//
void CCoolMenuManager::Destroy()
{
   m_ilButtons.DeleteImageList();
   m_mapIDtoImage.RemoveAll();
   m_szBitmap = m_szButton = CSize(0,0);
   m_arToolbarID.RemoveAll();
   m_fontMenu.DeleteObject();
   DestroyAccel();
}

/////////////////
// Destroy accelerators
//
void CCoolMenuManager::DestroyAccel()
{
   m_mapIDtoAccel.RemoveAll();      // delete ACCEL entries in map
   delete m_pAccel;                 // delete current accelerators
   m_pAccel = NULL;                 // ...
}

//////////////////
// Call this to install the menu manager. Install(NULL) to un-install.
//
void CCoolMenuManager::Install(CFrameWnd* pFrame)
{
   ASSERT_VALID(pFrame);
   m_pFrame = pFrame;
   HookWindow(pFrame);   // install message hook
}

//////////////////
// Load array of toolbar IDs.
//
BOOL CCoolMenuManager::LoadToolbars(const UINT* arID, int n)
{
   ASSERT(arID);
   BOOL bRet = TRUE;
   for (int i=0; i<n; i++)
      bRet |= LoadToolbar(arID[i]);
   return bRet;
}

// structure of RT_TOOLBAR resource
struct TOOLBARDATA {
   WORD wVersion;    // version # should be 1
   WORD wWidth;      // width of one bitmap
   WORD wHeight;     // height of one bitmap
   WORD wItemCount;  // number of items
   WORD items[1];    // array of command IDs, actual size is wItemCount
};

//////////////////
// Load one toolbar. Assumes bg color is gray.
// 
//  * add toolbar bitmap to image list
//  * add each button ID to button map
//
BOOL CCoolMenuManager::LoadToolbar(UINT nIDToolbar)
{
   // load bitmap
   HBITMAP hbmToolbar = PxLib::LoadSysColorBitmap(nIDToolbar);
   if (!hbmToolbar) {
      TRACE(_T("*** Can't load bitmap for toolbar %d!\n"), nIDToolbar);
      return FALSE;
   }
   CBitmap bmToolbar;
   bmToolbar.Attach(hbmToolbar); // destructor will detach & destroy

   // load toolbar
   LPTSTR lpResName = MAKEINTRESOURCE(nIDToolbar);
   HINSTANCE hInst;
   HRSRC hRsrc;
   TOOLBARDATA* ptbd;
   if ((hInst= AfxFindResourceHandle(lpResName, RT_TOOLBAR)) == NULL ||
       (hRsrc= FindResource(hInst, lpResName, RT_TOOLBAR))   == NULL ||
       (ptbd = (TOOLBARDATA*)LoadResource(hInst, hRsrc))     == NULL) {

      TRACE(_T("*** Can't load toolbar %d!\n"), nIDToolbar);
      return FALSE;
   }
   ASSERT(ptbd->wVersion==1);
      
   // OK, I have the bitmap and toolbar. 

   CSize sz(ptbd->wWidth, ptbd->wHeight);
   if (m_szBitmap.cx==0) {
      // First toolbar: initialized bitmap/button sizes and create image list.
      m_szBitmap = sz;
      m_szButton = sz + CSize(CXBUTTONMARGIN<<1, CYBUTTONMARGIN<<1);
      VERIFY(m_ilButtons.Create(sz.cx, sz.cy, ILC_MASK, 0, 10));

   } else if (m_szBitmap != sz) {
      // button sizes different -- oops
      TRACE(_T("*** Toolbar %d button size differs!\n"), nIDToolbar);
      return FALSE;
   }

   // I have a good toolbar: now add bitmap to the image list, and each
   // command ID to m_mapIDtoImage array. Note that LoadSysColorBitmap will
   // change gray -> COLOR_3DFACE, so use that for image list background.
   //
   int iNextImage = m_ilButtons.GetImageCount();
   m_ilButtons.Add(&bmToolbar, GetSysColor(COLOR_3DFACE));
   for (int i = 0; i < ptbd->wItemCount; i++) {
      UINT nID = ptbd->items[i];
      if (nID > 0) {
         if (GetButtonIndex(nID) >= 0) {
            TRACE(_T("*** Duplicate button ID %d ignored\n"), nID);
         } else
            m_mapIDtoImage.SetAt(nID, (void*)iNextImage++);
      }
   }
   m_arToolbarID.Add(nIDToolbar);  // remember toolbar ID for Refresh
   bmToolbar.Detach();
   return TRUE; // success!
}

//////////////////
// Virtual CSubclassWnd window proc. All messages come here before frame
// window. Isn't it cool? Just like in the Old days!
//
LRESULT CCoolMenuManager::WindowProc(UINT msg, WPARAM wp, LPARAM lp)
{
   switch(msg) {
   case WM_SYSCOLORCHANGE:
   case WM_SETTINGCHANGE:
      Refresh();
      break;

   case WM_MEASUREITEM:
      if (OnMeasureItem((MEASUREITEMSTRUCT*)lp))
         return TRUE; // handled
      break;

   case WM_DRAWITEM:
      if (OnDrawItem((DRAWITEMSTRUCT*)lp))
         return TRUE; // handled
      break;

   case WM_INITMENUPOPUP:
      // Very important: must let frame window handle it first!
      // Because if someone calls CCmdUI::SetText, MFC will change item to
      // MFT_STRING, so I must change back to MFT_OWNERDRAW.
      //
      CSubclassWnd::WindowProc(msg, wp, lp);
      OnInitMenuPopup(CMenu::FromHandle((HMENU)wp), (UINT)LOWORD(lp), 
                      (BOOL)HIWORD(lp));
      return 0;

   case WM_MENUSELECT:
      OnMenuSelect((UINT)LOWORD(wp), (UINT)HIWORD(wp), (HMENU)lp);
      break;

   case WM_MENUCHAR:
      LRESULT lr = OnMenuChar((TCHAR)LOWORD(wp), (UINT)HIWORD(wp),
                              CMenu::FromHandle((HMENU)lp));
      if (lr!=0)
         return lr;
      break;
   }
   return CSubclassWnd::WindowProc(msg, wp, lp);
}

//////////////////
// Refresh all colors, fonts, etc. For WM_SETTINGCHANGE, WM_SYSCOLORCHANGE.
//
void CCoolMenuManager::Refresh()
{
   // first copy list (array) of toolbar IDs now loaded.
   CUIntArray arToolbarID;
   arToolbarID.Copy(m_arToolbarID);

   // destroy everything
   Destroy();

   // re-load toolbars.
   int nToolbars = arToolbarID.GetSize();
   for (int i = 0; i < nToolbars; i++)
      LoadToolbar(arToolbarID[i]);
}

//////////////////
// Get menu font, creating if needed
//
CFont* CCoolMenuManager::GetMenuFont()
{
   if (!(HFONT)m_fontMenu) {
      NONCLIENTMETRICS info;
      info.cbSize = sizeof(info);
      SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(info), &info, 0);
      VERIFY(m_fontMenu.CreateFontIndirect(&info.lfMenuFont));
   }
   return &m_fontMenu;
}

//////////////////
// Handle WM_MEASUREITEM on behalf of frame: compute menu item size.
//
BOOL CCoolMenuManager::OnMeasureItem(LPMEASUREITEMSTRUCT lpms)
{
   ASSERT(lpms);
   CMyItemData* pmd = (CMyItemData*)lpms->itemData;
   ASSERT(pmd);
   if (lpms->CtlType != ODT_MENU || !pmd->IsMyItemData())
      return FALSE; // not handled by me

   if (pmd->fType & MFT_SEPARATOR) {
      // separator: use half system height and zero width
      lpms->itemHeight = GetSystemMetrics(SM_CYMENU)>>1;
      lpms->itemWidth  = 0;

   } else {

      // compute size of text: use DrawText with DT_CALCRECT

      CWindowDC dc(NULL);  // screen DC--I won't actually draw on it
      CRect rcText(0,0,0,0);
      CFont* pOldFont = dc.SelectObject(GetMenuFont());
      dc.DrawText(pmd->text, rcText, DT_MYSTANDARD|DT_CALCRECT);
      dc.SelectObject(pOldFont);

      // height of item is just height of a standard menu item
      lpms->itemHeight= max(GetSystemMetrics(SM_CYMENU), rcText.Height());

      // width is width of text plus a bunch of stuff
      int cx = rcText.Width();   // text width 
      cx += CXTEXTMARGIN<<1;     // L/R margin for readability
      cx += CXGAP;               // space between button and menu text
      cx += m_szButton.cx<<1;    // button width (L=button; R=empty margin)

      // whatever value I return in lpms->itemWidth, Windows will add the
      // width of a menu checkmark, so I must subtract to defeat Windows. Argh.
      //
      cx -= GetSystemMetrics(SM_CXMENUCHECK)-1;
      lpms->itemWidth = cx;      // done deal

      CMTRACE(_T("OnMeasureItem for '%s':\tw=%d h=%d\n"), (LPCTSTR)pmd->text,
              lpms->itemWidth, lpms->itemHeight);
   }
   return TRUE; // handled
}

/////////////////
// Handle WM_DRAWITEM on behalf of frame. Note: in all that goes
// below, can't assume rcItem.left=0 because of multi-column menus!
//
BOOL CCoolMenuManager::OnDrawItem(LPDRAWITEMSTRUCT lpds)
{
   ASSERT(lpds);
   CMyItemData* pmd = (CMyItemData*)lpds->itemData;
   ASSERT(pmd);
   if (lpds->CtlType != ODT_MENU || !pmd->IsMyItemData())
      return FALSE; // not handled by me

   ASSERT(lpds->itemAction != ODA_FOCUS);
   ASSERT(lpds->hDC);
   CDC dc;
   dc.Attach(lpds->hDC);

   const CRect& rcItem = lpds->rcItem;
   if (pmd->fType & MFT_SEPARATOR) {
      // draw separator
      CRect rc = rcItem;                        // copy rect
      rc.top += rc.Height()>>1;                 // vertical center
      dc.DrawEdge(&rc, EDGE_ETCHED, BF_TOP);    // draw separator line

   } else {                                     // not a separator

      CMTRACE(_T("OnDrawItem for '%s':\tw=%d h=%d\n"), (LPCTSTR)pmd->text,
              rcItem.Width(), rcItem.Height());

      BOOL bDisabled = lpds->itemState & ODS_GRAYED;
      BOOL bSelected = lpds->itemState & ODS_SELECTED;
      BOOL bChecked  = lpds->itemState & ODS_CHECKED;
      BOOL bHaveButn=FALSE;

      // Paint button, or blank if none
      CRect rcButn(rcItem.TopLeft(), m_szButton);  // button rect
      rcButn += CPoint(0,                          // center vertically
                       (rcItem.Height() - rcButn.Height())>>1 );

      int iButton = pmd->iButton;
      if (iButton >= 0) {

         // this item has a button!
         bHaveButn = TRUE;

         // compute point to start drawing
         CSize sz = rcButn.Size() - m_szBitmap;
         sz.cx >>= 1;
         sz.cy >>= 1;
         CPoint p(rcButn.TopLeft() + sz);

         // draw disabled or normal
         if (!bDisabled) {
            // normal: fill BG depending on state
            PxLib::FillRect(dc, rcButn, GetSysColor(
               (bChecked && !bSelected) ? COLOR_3DLIGHT : COLOR_MENU));

            // draw pushed-in or popped-out edge
            if (bSelected || bChecked) {
               CRect rc2 = rcButn;
               dc.DrawEdge(rc2, bChecked ? BDR_SUNKENOUTER : BDR_RAISEDINNER,
                           BF_RECT);
            }
            // draw the button!
            m_ilButtons.Draw(&dc, iButton, p, ILD_TRANSPARENT);

         } else if (m_bUseDrawState) {
            // use DrawState to draw disabled button: must convert to icon
            HICON hIcon=m_ilButtons.ExtractIcon(iButton);
            ASSERT(hIcon);
            dc.DrawState(p, CSize(0,0), hIcon, DSS_DISABLED, (HBRUSH)NULL);
            DestroyIcon(hIcon);

         } else
            // use DrawEmbossed to draw disabeld button, w/color flag
            PxLib::DrawEmbossed(dc, m_ilButtons, iButton, p,
                                m_bDrawDisabledButtonsInColor);

      } else {
         // no button: look for custom checked/unchecked bitmaps
         CMenuItemInfo info;
         info.fMask = MIIM_CHECKMARKS;
         GetMenuItemInfo((HMENU)lpds->hwndItem, lpds->itemID, 
                         MF_BYCOMMAND, &info);
         if (bChecked || info.hbmpUnchecked) {
            bHaveButn = Draw3DCheckmark(dc, rcButn, bSelected,
               bChecked ? info.hbmpChecked : info.hbmpUnchecked);
         }
      }

      // Done with button, now paint text. First do background if needed.
      int cxButn = m_szButton.cx;            // width of button
      COLORREF colorBG = GetSysColor(bSelected ? COLOR_HIGHLIGHT : COLOR_MENU);
      if (bSelected || lpds->itemAction==ODA_SELECT) {
         // selected or selection state changed: paint text background
         CRect rcBG = rcItem;                // whole rectangle
         if (bHaveButn)                      // if there's a button:
            rcBG.left += cxButn + CXGAP;     //  don't paint over it!
         PxLib::FillRect(dc, rcBG, colorBG); // paint it!
      }

      // compute text rectangle and colors
      CRect rcText = rcItem;                 // start w/whole item
      rcText.left += cxButn + CXGAP + CXTEXTMARGIN; // left margin
      rcText.right -= cxButn;                // right margin
      dc.SetBkMode(TRANSPARENT);             // paint transparent text
      COLORREF colorText = GetSysColor(bDisabled ?  COLOR_GRAYTEXT :
         bSelected ? COLOR_HIGHLIGHTTEXT : COLOR_MENUTEXT);

      // Now paint menu item text.  No need to select font,
      // because Windows sets it up before sending WM_DRAWITEM
      //
      if (bDisabled && (!bSelected || colorText == colorBG)) {
         // disabled: draw hilite text shifted southe Ast 1 pixel for embossed
         // look. Don't do it if item is selected, tho--unless text color same
         // as menu highlight color. Got it?
         //
         DrawMenuText(dc, rcText + CPoint(1,1), pmd->text,
                      GetSysColor(COLOR_3DHILIGHT));
      }
      DrawMenuText(dc, rcText, pmd->text, colorText); // finally!
   }
   dc.Detach();

   return TRUE; // handled
}

/////////////////
// Helper function to draw justified menu text. If the text contains a TAB,
// draw everything after the tab right-aligned
//
void CCoolMenuManager::DrawMenuText(CDC& dc, CRect rc, CString text,
                                    COLORREF color)
{
   CString left = text;
   CString right;
   int iTabPos = left.Find('\t');
   if (iTabPos >= 0) {
      right = left.Right(left.GetLength() - iTabPos - 1);
      left  = left.Left(iTabPos);
   }
   dc.SetTextColor(color);
   dc.DrawText(left, &rc, DT_MYSTANDARD);
   if (iTabPos > 0)
      dc.DrawText(right, &rc, DT_MYSTANDARD|DT_RIGHT);
}

#ifndef OBM_CHECK
#define OBM_CHECK 32760 // from winuser.h
#endif

//////////////////
// Draw 3D checkmark
//
//    dc          device context to draw in
//    rc          rectangle to center bitmap in
//    bSelected   TRUE if button is also selected
//    hbmCheck    Checkmark bitmap to use, or NULL for default
//
BOOL CCoolMenuManager::Draw3DCheckmark(CDC& dc, const CRect& rc,
                                       BOOL bSelected, HBITMAP hbmCheck)
{
   // get checkmark bitmap if none, use Windows standard
   if (!hbmCheck) {
      CBitmap bm;
      VERIFY(bm.LoadOEMBitmap(OBM_CHECK));
      hbmCheck = (HBITMAP)bm.Detach();
      ASSERT(hbmCheck);
   }
   
   // center bitmap in caller's rectangle
   BITMAP bm;
   ::GetObject(hbmCheck, sizeof(bm), &bm);
   int cx = bm.bmWidth;
   int cy = bm.bmHeight;
   CRect rcDest = rc;
   CPoint p(0,0);
   CSize delta(CPoint((rc.Width() - cx)/2, (rc.Height() - cy)/2));
   if (rc.Width() > cx)
      rcDest = CRect(rc.TopLeft() + delta, CSize(cx, cy));
   else
      p -= delta;

   // select checkmark into memory DC
   CDC memdc;
   memdc.CreateCompatibleDC(&dc);
   HBITMAP hOldBM = (HBITMAP)::SelectObject(memdc, hbmCheck);

   // set BG color based on selected state
   COLORREF colorOld =
      dc.SetBkColor(GetSysColor(bSelected ? COLOR_MENU : COLOR_3DLIGHT));
   dc.BitBlt(rcDest.left, rcDest.top, rcDest.Width(), rcDest.Height(),
             &memdc, p.x, p.y, SRCCOPY);
   dc.SetBkColor(colorOld);

   ::SelectObject(memdc, hOldBM); // restore

   // draw pushed-in highlight.
   if (rc.Width() > cx)          // if room:
      rcDest.InflateRect(1,1);   // inflate checkmark by one pixel all around
   dc.DrawEdge(&rcDest, BDR_SUNKENOUTER, BF_RECT);

   return TRUE;
}

//////////////////
// Handle WM_INITMENUPOPUP on behalf of frame.
//
void CCoolMenuManager::OnInitMenuPopup(CMenu* pMenu, UINT nIndex, BOOL bSysMenu)
{
   if (m_bAutoAccel) {
      // check for new accels. If ASSERT bombs, you forgot to call Install.
      ASSERT_VALID(m_pFrame);
      HACCEL hAccel = m_pFrame->GetDefaultAccelerator();
      if (hAccel != m_hAccel)
         LoadAccel(hAccel);
   }
   ConvertMenu(pMenu, nIndex, bSysMenu, m_bShowButtons);
}

//////////////////
// Set the accelerator table used to generate automatic key
// names in menus. Delete previous table if any.
//
void CCoolMenuManager::LoadAccel(HACCEL hAccel)
{
   DestroyAccel();
   int nAccel;
   if (hAccel && (nAccel = CopyAcceleratorTable(hAccel, NULL, 0)) > 0) {
      m_pAccel = new ACCEL [nAccel];
      ASSERT(m_pAccel);
      CopyAcceleratorTable(hAccel, m_pAccel, nAccel);

      // Now I have the accelerators. Look over list, linking each command
      // ID with its ACCEL structure--i.e., m_mapIDtoAccel[nID] = ACCEL for
      // that ID. If more than one ACCEL for a given command (command has more
      // than one shortcut), fix up so ACCEL.cmd is offset of prev ACCEL
      // 
      for (int i=0; i<nAccel; i++) {
         ACCEL& ac = m_pAccel[i];
         ACCEL* pAccel = GetAccel(ac.cmd);
         m_mapIDtoAccel.SetAt(ac.cmd, &ac);
         ac.cmd = pAccel ? &ac - pAccel : 0; // ac.cmd = offset of prev, or 0
      }
   }
}

//////////////////
// This rather gnarly function is used both to convert the menu from strings to
// owner-draw and vice versa. In either case, it also appends automagic
// accelerator key names to the menu items, if m_bAutoAccel is TRUE.
//
void CCoolMenuManager::ConvertMenu(CMenu* pMenu, UINT nIndex, BOOL bSysMenu, 
                                   BOOL bShowButtons)
{
   ASSERT_VALID(pMenu);

   CString sItemName;

   UINT nItem = pMenu->GetMenuItemCount();
   for (UINT i = 0; i < nItem; i++) {  // loop over each item in menu

      // get menu item info
      char itemname[256];
      CMenuItemInfo info;
      info.fMask = MIIM_SUBMENU | MIIM_DATA | MIIM_ID | MIIM_TYPE;
      info.dwTypeData = itemname;
      info.cch = sizeof(itemname);
      ::GetMenuItemInfo(*pMenu, i, TRUE, &info);
      CMyItemData* pmd = (CMyItemData*)info.dwItemData;

      if (pmd && !pmd->IsMyItemData()) {
         CMTRACE(_T("CCoolMenuManager: ignoring foreign owner-draw item\n"));
         continue; // owner-draw menu item isn't mine--leave it alone
      }

      if (bSysMenu && info.wID >= 0xF000) {
         CMTRACE(_T("CCoolMenuManager: ignoring sys menu item\n"));
         continue; // don't do for system menu commands
      }
   
      // now that I have the info, I will modify it
      info.fMask = 0;   // assume nothing to change

      if (bShowButtons) {

         // I'm showing buttons: convert to owner-draw

         if (!(info.fType & MFT_OWNERDRAW)) {
            // If not already owner-draw, make it so. NOTE: If app calls
            // pCmdUI->SetText to change the text of a menu item, MFC will
            // turn the item to MFT_STRING. So I must set it back to
            // MFT_OWNERDRAW again. In this case, the menu item data (pmd)
            // will still be there.
            // 
            info.fType |= MFT_OWNERDRAW;
            info.fMask |= MIIM_TYPE;
            if (!pmd) {                         // if no item data:
               pmd = new CMyItemData;           //   create one
               ASSERT(pmd);                     //   (I hope)
               pmd->fType = info.fType;         //   handy when drawing
               pmd->iButton = GetButtonIndex(info.wID);
               info.dwItemData = (DWORD)pmd;    //   set in menu item data
               info.fMask |= MIIM_DATA;         //   set item data
            }
            pmd->text = info.dwTypeData;        // copy menu item string
         }

         // now add the menu to list of "converted" menus
         HMENU hmenu = pMenu->GetSafeHmenu();
         ASSERT(hmenu);
         if (!m_menuList.Find(hmenu))
            m_menuList.AddHead(hmenu);

         // append accelerators to menu item name
         if (m_pAccel && m_bAutoAccel)
            AppendAccelName(pmd->text, info.wID);

      } else {

         // no buttons -- I'm converting to strings
         
         if (info.fType & MFT_OWNERDRAW) {   // if ownerdraw:
            info.fType &= ~MFT_OWNERDRAW;    //   turn it off
            info.fMask |= MIIM_TYPE;         //   change item type
            ASSERT(pmd);                     //   sanity check
            sItemName = pmd->text;           //   save name before deleting pmd
         } else                              // otherwise:
            sItemName = info.dwTypeData;     //   use name from MENUITEMINFO

         if (pmd) {
            // NOTE: pmd (item data) could still be left hanging around even
            // if MFT_OWNERDRAW is not set, in case mentioned above where app
            // calls pCmdUI->SetText to set text of item and MFC sets the type
            // to MFT_STRING.
            //
            info.dwItemData = NULL;          // item data is NULL
            info.fMask |= MIIM_DATA;         // change it
            delete pmd;                      // and item data too
         }

         // possibly add accelerator name
         if (m_pAccel  && m_bAutoAccel && AppendAccelName(sItemName, info.wID))
            info.fMask |= MIIM_TYPE;         //  change item type (string)
            
         if (info.fMask & MIIM_TYPE) {
            // if setting name, copy name from CString to buffer and set cch
            strncpy(itemname, sItemName, sizeof(itemname));
            info.dwTypeData = itemname;
            info.cch = sItemName.GetLength();
         }
      }

      // if after all the above, there is anything to change, change it
      if (info.fMask) {
         CMTRACE(_T("Converting '%s' to %s\n"), itemname,
            (info.fType & MFT_OWNERDRAW) ? _T("OWNERDRAW") : _T("STRING"));
         SetMenuItemInfo(*pMenu, i, TRUE, &info);
      }
   }
}

//////////////////
// User typed a char into menu. Look for item with & preceeding the char typed.
//
LRESULT CCoolMenuManager::OnMenuChar(UINT nChar, UINT nFlags, CMenu* pMenu)
{
   ASSERT_VALID(pMenu);

   UINT iCurrentItem = (UINT)-1; // guaranteed higher than any command ID
   CUIntArray arItemsMatched;    // items that match the character typed

   UINT nItem = pMenu->GetMenuItemCount();
   for (UINT i=0; i< nItem; i++) {
      // get menu info
      CMenuItemInfo info;
      info.fMask = MIIM_DATA | MIIM_TYPE | MIIM_STATE;
      ::GetMenuItemInfo(*pMenu, i, TRUE, &info);

      CMyItemData* pmd = (CMyItemData*)info.dwItemData;
      if ((info.fType & MFT_OWNERDRAW) && pmd && pmd->IsMyItemData()) {
         CString& text = pmd->text;
         int iAmpersand = text.Find('&');
         if (iAmpersand >=0 && toupper(nChar)==toupper(text[iAmpersand+1]))
            arItemsMatched.Add(i);
      }
      if (info.fState & MFS_HILITE)
         iCurrentItem = i; // note index of current item
   }

   // arItemsMatched now contains indexes of items that match the char typed.
   //
   //   * if none: beep
   //   * if one:  execute it
   //   * if more than one: hilite next
   //
   UINT nFound = arItemsMatched.GetSize();
   if (nFound == 0)
      return 0;

   else if (nFound==1)
      return MAKELONG(arItemsMatched[0], MNC_EXECUTE);

   // more than one found--return 1st one past current selected item;
   UINT iSelect = 0;
   for (i=0; i < nFound; i++) {
      if (arItemsMatched[i] > iCurrentItem) {
         iSelect = i;
         break;
      }
   }
   return MAKELONG(arItemsMatched[iSelect], MNC_SELECT);
}

//////////////////
// Handle WM_MENUSELECT: check for menu closed
//
void CCoolMenuManager::OnMenuSelect(UINT nItemID, UINT nFlags, HMENU hSysMenu)
{
   if (hSysMenu==NULL && nFlags==0xFFFF) {
      // Windows has closed the menu: restore all menus to original state
      while (!m_menuList.IsEmpty()) {
         ConvertMenu(CMenu::FromHandle((HMENU)m_menuList.RemoveHead()),
                     0, FALSE, FALSE);
      }
   }
}

//////////////////
// Append the name of accelerator for given command ID to menu string.
// sItemName is menu item name, which will have the accelerator appended.
// For example, might call with sItemName = "File &Open" and return with
// sItemName = "File &Open\tCtrl-O". Returns BOOL = whether string changed.
//
BOOL CCoolMenuManager::AppendAccelName(CString& sItemName, UINT nID)
{
   int iTabPos = sItemName.Find('\t');
   if (iTabPos > 0)
      sItemName = sItemName.Left(iTabPos);

   BOOL bFound = FALSE;
   for (ACCEL* pa = GetAccel(nID); pa; pa -= pa->cmd) {
      sItemName += bFound ? _T(", ") : _T("\t");
      if (pa->fVirt & FALT)         sItemName += _T("Alt+");
      if (pa->fVirt & FCONTROL)     sItemName += _T("Ctrl+");
      if (pa->fVirt & FSHIFT)       sItemName += _T("Shift+");
      if (pa->fVirt & FVIRTKEY) {
         TCHAR keyname[64];
         UINT vkey = MapVirtualKey(pa->key, 0)<<16;
         GetKeyNameText(vkey, keyname, sizeof(keyname));
         sItemName += keyname;
      } else
         sItemName += (char)pa->key;

      bFound = TRUE;
      if (pa->cmd == 0)
         break;
   }
   return bFound;
}

//////////////////
// This function fixes MFC's diseased dot bitmap used for
// "radio-style" menu items (CCmdUI->SetRadio), which is completely
// wrong if the menu font is large. 
//
void CCoolMenuManager::FixMFCDotBitmap()
{
   // First, get MFC's dot bitmap. This is stored in afxData.hbmMenuDot, but
   // afxData is MFC-private, so the Only way to get it is create a menu,
   // set the radio check, and then see what bitmap MFC set in the menu item.
   CMenu menu;
   VERIFY(menu.CreateMenu());
   VERIFY(menu.AppendMenu(MFT_STRING, 0, (LPCTSTR)NULL));
   CCmdUI cui;
   cui.m_pMenu = &menu;
   cui.m_nIndex = 0;
   cui.m_nIndexMax = 1;
   cui.SetRadio(TRUE);
   CMenuItemInfo info;
   info.fMask = MIIM_CHECKMARKS;
   GetMenuItemInfo(menu, 0, MF_BYPOSITION, &info);
   HBITMAP hbmDot = info.hbmpChecked;

   if (hbmDot) {
      // OK, now I have the bitmap: draw a centered dot of appropriate size
      BITMAP bm;
      ::GetObject(hbmDot, sizeof(bm), &bm);
      CRect rcDot(0,0, bm.bmWidth, bm.bmHeight);
      rcDot.DeflateRect((bm.bmWidth>>1)-2, (bm.bmHeight>>1)-2);

      CWindowDC dcScreen(NULL);
      CDC memdc;
      memdc.CreateCompatibleDC(&dcScreen);
      int nSave = memdc.SaveDC();
      memdc.SelectStockObject(BLACK_PEN);
      memdc.SelectStockObject(BLACK_BRUSH);
      memdc.SelectObject((HGDIOBJ)hbmDot);
      memdc.PatBlt(0, 0, bm.bmWidth, bm.bmHeight, WHITENESS);
      memdc.Ellipse(&rcDot);
      memdc.RestoreDC(nSave);
   }
   menu.DestroyMenu();
}

////////////////////////////////////////////////////////////////
// Helper functions

//////////////////
// Load a bitmap, converting the standard colors.
// Calls AfxLoadSysColorBitmap to do the work.
//
// RGB(0x00, 0x00, 0x00) (black)      --> COLOR_BTNTEXT
// RGB(0x80, 0x80, 0x80) (dark gray)  --> COLOR_3DSHADOW
// RGB(0xC0, 0xC0, 0xC0) (gray)       --> COLOR_3DFACE
// RGB(0xFF, 0xFF, 0xFF) (white)      --> COLOR_3DHILIGHT
// 
HBITMAP PxLib::LoadSysColorBitmap(LPCTSTR lpResName, BOOL bMono)
{
   HINSTANCE hInst = AfxFindResourceHandle(lpResName, RT_BITMAP);
   HRSRC hRsrc = ::FindResource(hInst, lpResName, RT_BITMAP);
   if (hRsrc == NULL)
      return NULL;
   return AfxLoadSysColorBitmap(hInst, hRsrc, bMono);
}

//////////////////
// Shorthand to fill a rectangle with a solid color.
//
void PxLib::FillRect(CDC& dc, const CRect& rc, COLORREF color)
{
   CBrush brush(color);
   CBrush* pOldBrush = dc.SelectObject(&brush);
   dc.PatBlt(rc.left, rc.top, rc.Width(), rc.Height(), PATCOPY);
   dc.SelectObject(pOldBrush);
}

Figure 15   CCoolMenuManager Colors and Metrics

GetSysColor codes used for drawing menus
Code Description
COLOR_3DFACE Normal color for face of 3D objects.
COLOR_3DSHADOW 3D shadow color (edge away from light source).
COLOR_3DHILIGHT 3D highlight color (edge facing light source).
COLOR_3DLIGHT Light color for 3D objects, used for the face of pushed-in buttons.
COLOR_3DKSHADOW Dark shadow (not used by CCoolMenuManager).
COLOR_MENU Menu color. Normally, this is the same as COLOR_3DFACE, but the user can change it.
COLOR_MENUTEXT Color of text in menus.
COLOR_HIGHLIGHT Background color for highlighted (selected) menu item.
COLOR_HIGHLIGHTTEXT Color of text in highlighted (selected) menu item.
COLOR_GRAYTEXT Color of grayed (disabled) text.
GetSystemMetrics codes used in CcoolMenuManager
Code Description
SM_CYMENU Height of menu bar/item.
SM_CXMENUCHECK Width of menu checkmark.