skip to Main Content

I’m simply trying to add small color swatches to my context menu (displayed via the TrackPopupMenu API.) Here’s a Photoshopped version of what I’m trying to achieve:

enter image description here

As far as I understand the default menu does not support it. Btw, the sample above (without color swatches) was generated by doing this:

MENUITEMINFO mii = {0};
mii.cbSize = sizeof(mii);
mii.fMask = MIIM_FTYPE | MIIM_ID | MIIM_STATE | MIIM_STRING;
mii.fType = MFT_STRING;
mii.wID = ID_1_MARKER_01 + m;
mii.dwTypeData = L"Marker";
mii.cch = TSIZEOF(L"Marker");
mii.fState = m == 1 ? MFS_CHECKED : MFS_ENABLED;
if(m == 2)
    mii.fState |= MFS_GRAYED;

VERIFY(::InsertMenuItem(hMenu, ID_1_BEFORE, FALSE, &mii));

So I discovered that I need to use MFT_OWNERDRAW style to draw menu items myself, but that’s where the problems begin.

I changed my code to display my menu as such:

MENUITEMINFO mii = {0};
mii.cbSize = sizeof(mii);
mii.fMask = MIIM_FTYPE | MIIM_ID | MIIM_STATE;
mii.fType = MFT_OWNERDRAW;
mii.wID = ID_1_MARKER_01 + m;
mii.dwItemData = MARKER_ID_01 + m;
mii.fState = m == 1 ? MFS_CHECKED : MFS_ENABLED;
if(m == 2)
    mii.fState |= MFS_GRAYED;

VERIFY(::InsertMenuItem(hMenu, ID_1_BEFORE, FALSE, &mii));

then I needed to override WM_MEASUREITEM and WM_DRAWITEM messages. But when I do it with the code that I’ll show below, here’s what I get:

enter image description here

So please bear with me. I have several questions on this topic:

1) While processing WM_MEASUREITEM how am I supposed to know the size of text if they do not supply neither DC nor HWND for the menu? In other words, if I do this, the size of the menus is wrong:

#define TSIZEOF(f) ((sizeof(f) - sizeof(TCHAR)) / sizeof(TCHAR))

//hwnd = HWND supplied in WM_MEASUREITEM notification
HDC hDC = ::GetDC(hwnd);
HGDIOBJ hOldFont = ::SelectObject(hDC, ::SendMessage(hwnd, WM_GETFONT, 0, 0));

SIZE szTxt = {0};
::GetTextExtentPoint32(hDC, 
    L"Marker", 
    TSIZEOF(L"Marker"), 
    &szTxt);

//lpmis = MEASUREITEMSTRUCT*
lpmis->itemWidth = szTxt.cx;
lpmis->itemHeight = szTxt.cy;

::SelectObject(hDC, hOldFont);
::ReleaseDC(hwnd, hDC);

2) Then while processing WM_DRAWITEM how do I know the offset to begin drawing text on the left? If I do this, my menus aren’t offset enough to the right (as you can see from the screenshot above):

int nCheckW = ::GetSystemMetrics(SM_CXMENUCHECK);

//lpdis = DRAWITEMSTRUCT*
::ExtTextOut(lpdis->hDC, 
    lpdis->rcItem.left + nCheckW, 
    lpdis->rcItem.top, 
    ETO_OPAQUE, 
            &lpdis->rcItem, 
    L"Marker", 
    TSIZEOF(L"Marker"), 
    NULL);

3) And lastly how do I draw that default checkbox on the left of the menu item?

2

Answers


  1. While I don’t use color swatches, and, use strictly MFC, I do render bitmaps on my derived menu items. You should be able to adapt the following for your needs.

    In measuring the item text, I use the desktop dc.

    CClientDC dc(CWnd::GetDesktopWindow());
    SIZE size;
    
    GetTextExtentPoint32(dc.m_hDC, buff, buff.GetLength(), &size );
    lpMeasureItemStruct->itemWidth = size.cx+12;
    lpMeasureItemStruct->itemHeight = size.cy+8;
    

    Along with some tiny adjustments through experimentation, I came up with what I needed for the size.

    To render the actual bitmap and text, I check to see if a theme is active and render the menu item in one of two different ways. Either as a themed menu item or a standard menu item. To render the text, I start with the rect that was passed via the LPDRAWITEMSTRUCT. I then make the following adjustment before rendering the text.

    //  adjust if non-themed.
    if (!IsThemeActive())
        rectt.left+= m_bmWidth+4;
    else
        rectt.left+= BITMAP_ADJUSTMENT;
    

    Through trial and error, I found that BITMAP_ADJUSTMENT set to 30 worked for me. Then, depending on the stated of the item (disabled, selected), the rectt is adjusted further.

    //  draw disabled text.
    if (disabled)
        {
        //  draw selected text.
        if (selected)
            {
            pDC->SetTextColor(::GetSysColor(COLOR_GRAYTEXT));
            pDC->DrawText(text, &rectt, format);
            }
        else
            {
            offset = rectt;
            offset.left+=   1;
            offset.right+=1;
            offset.top+=    1;
            offset.bottom+= 1;
            pDC->SetTextColor(::GetSysColor(COLOR_BTNHILIGHT));
            pDC->DrawText(text, &offset, format);
            pDC->SetTextColor(::GetSysColor(COLOR_GRAYTEXT));
            pDC->DrawText(text, &rectt, format);
            }
        }
    else
        //  draw normal text.
        pDC->DrawText(text, &rectt, format);
    

    Lastly, to render the checkmark, I create an image list using a pre-defined bitmap (I may have extracted it from a Microsoft dll). Again, the bitmap is rendered according to the state of the item.

        //  draw non-disabled bitmap.
        if (!disabled)
            {
            bmp.GetBitmap(&bm);
            m_bmWidth = bm.bmWidth;
            imgList.Create(bm.bmWidth, bm.bmWidth, ILC_COLOR24|ILC_MASK, 1, 1);
            imgList.Add(&bmp, COLOR_BITMAP_BACKGROUND);
    
            if (checked)
                {
                if (!selected)
                    imgList.DrawEx(pDC, 0, CPoint(4,rect.top+4), CSize(bm.bmWidth, bm.bmWidth), COLOR_NOT_SELECTED, 0, ILD_NORMAL);
                else
                    imgList.DrawEx(pDC, 0, CPoint(4,rect.top+4), CSize(bm.bmWidth, bm.bmWidth), 0, COLOR_SELECTED, ILD_SELECTED);
                }
            else
                imgList.DrawEx(pDC, 0, CPoint(4,rect.top+4), CSize(bm.bmWidth, bm.bmWidth), 0, 0, ILD_TRANSPARENT);
            }
        else
            //  draw a disabled bitmap.
            AfxDrawGrayBitmap(pDC, 4, rect.top+4, bmp, ::GetSysColor(COLOR_3DFACE));
    

    Most of the rendering was accomplished through an iterative approach adjusting the rect objects after each try.

    Login or Signup to reply.
  2. Alternative way with color icons.

    Windows Vista+:

    enter image description here

    Windows XP:

    enter image description here

    function GetOptimalCheckColor(AColor: COLORREF): COLORREF;
    var
      Gray: Integer;
    begin
      Gray := Round((0.30 * GetRValue(AColor)) +
                    (0.59 * GetGValue(AColor)) +
                    (0.11 * GetBValue(AColor)));
      if Gray > 127 then Result := $000000
                    else Result := $FFFFFF;
    end;
    
    type
      TBitmapInfo1 = packed record
        bmiHeader: TBitmapInfoHeader;
        Color0: DWORD {TRGBQuad};
        Color1: DWORD {TRGBQuad};
      end;
    
    function CreateMonochromeBitmap(ADC: HDC; AWidth, AHeight: Integer): HBITMAP;
    const
      Alignment = 31;
    var
      BitmapInfo: TBitmapInfo1;
      Data: Pointer;
    begin
      ZeroMemory(@BitmapInfo, SizeOf(BitmapInfo));
      with BitmapInfo, bmiHeader do
        begin
          biSize := SizeOf(bmiHeader);
          biWidth := AWidth;
          biHeight := -AHeight;
          biPlanes := 1;
          biBitCount := 1;
          biCompression := BI_RGB;
          biSizeImage := ((AWidth + Alignment) and not Alignment) div 8;
          biClrUsed := 2;
          biClrImportant := biClrUsed;
          Color0 := $000000;
          Color1 := $FFFFFF;
        end;
      Result := GDICheck(CreateDIBSection(ADC, PBitmapInfo(@BitmapInfo)^, DIB_RGB_COLORS, Data, 0, 0));
    end;
    
    
    function CreateColorBitmap(AWidth, AHeight: Integer; AColor: COLORREF; ACheckBitmap: HBITMAP): HBITMAP;
    var
      Bitmap: Windows.TBitmap;
      BaseDC: HDC;
      Brush: HBRUSH;
      ACheckDC: HDC;
      ACheckOldDCBitmap: HBITMAP;
      CheckBitmap: HBITMAP;
      CheckDC: HDC;
      CheckOldDCBitmap: HBITMAP;
      ResultDC: HDC;
      ResultOldBitmap: HBITMAP;
      TempBitmap: HBITMAP;
      TempDC: HDC;
      TempOldDCBitmap: HBITMAP;
      Theme: HTHEME;
    begin
      BaseDC := GDICheck(GetDC(0));
      try
        if ACheckBitmap <> 0 then
          GDICheck(GetObject(ACheckBitmap, SizeOf(Bitmap), @Bitmap));
        if (ACheckBitmap <> 0) and (Bitmap.bmWidth < AWidth) or (Bitmap.bmHeight < AHeight) then
          begin
            CheckBitmap := GDICheck(CreateMonochromeBitmap(BaseDC, AWidth, AHeight));
            try
              CheckDC := GDICheck(CreateCompatibleDC(BaseDC));
              try
                CheckOldDCBitmap := SelectObject(CheckDC, CheckBitmap);
                try
                  Brush := GDICheck(CreateSolidBrush($FFFFFF));
                  try
                    GDICheck(FillRect(CheckDC, Rect(0, 0, AWidth, AHeight), Brush));
                    ACheckDC := GDICheck(CreateCompatibleDC(BaseDC));
                    try
                      ACheckOldDCBitmap := SelectObject(ACheckDC, ACheckBitmap);
                      try
                        GDICheck(BitBlt(CheckDC, (AWidth - Bitmap.bmWidth) div 2 + 1, (AHeight - Bitmap.bmHeight) div 2,
                          Bitmap.bmWidth, Bitmap.bmHeight, ACheckDC, 0, 0, SRCCOPY));
                      finally
                        SelectObject(ACheckDC, ACheckOldDCBitmap);
                      end;
                    finally
                      DeleteDC(ACheckDC);
                    end;
                  finally
                    DeleteObject(Brush);
                  end;
                finally
                  SelectObject(CheckDC, CheckOldDCBitmap);
                end;
              finally
                DeleteDC(CheckDC);
              end;
            except
              DeleteObject(CheckBitmap);
              raise;
            end;
          end
        else
          CheckBitmap := ACheckBitmap;
        try
          Result := GDICheck(CreateCompatibleBitmap(BaseDC, AWidth, AHeight));
          ResultDC := GDICheck(CreateCompatibleDC(BaseDC));
          try
            ResultOldBitmap := SelectObject(ResultDC, Result);
            try
              Brush := GDICheck(CreateSolidBrush(AColor));
              try
                GDICheck(FillRect(ResultDC, Rect(0, 0, AWidth, AHeight), Brush));
              finally
                DeleteObject(Brush);
              end;
    
              if CheckBitmap <> 0 then
                {if IsWindowsVistaOrLater and ThemeServices.Available and ThemeServices.Enabled then
                  begin
                    Theme := OpenThemeData(0, VSCLASS_MENU);
                    try
                      DrawThemeBackground(Theme, ResultDC, MENU_POPUPCHECK, MC_CHECKMARKNORMAL, Rect(0, 0, AWidth, AHeight), nil);
                    finally
                      CloseThemeData(Theme);
                    end;
                  end
                else}
                  begin
                    TempBitmap := GDICheck(CreateCompatibleBitmap(BaseDC, AWidth, AHeight));
                    try
                      TempDC := GDICheck(CreateCompatibleDC(BaseDC));
                      try
                        TempOldDCBitmap := SelectObject(TempDC, TempBitmap);
                        try
                          Brush := GDICheck(CreateSolidBrush(GetOptimalCheckColor(AColor)));
                          try
                            GDICheck(FillRect(TempDC, Rect(0, 0, AWidth, AHeight), Brush));
                          finally
                            DeleteObject(Brush);
                          end;
                          GDICheck(MaskBlt(ResultDC, 0, 0, AWidth, AHeight, TempDC, 0, 0, CheckBitmap, 0, 0, MAKEROP4($00AA0029, SRCCOPY)));
                        finally
                          SelectObject(TempDC, TempOldDCBitmap);
                        end;
                      finally
                        DeleteDC(TempDC);
                      end;
                    finally
                      DeleteObject(TempBitmap);
                    end;
                  end;
            finally
              SelectObject(ResultDC, ResultOldBitmap);
            end;
          finally
            DeleteDC(ResultDC);
          end;
        finally
          if (CheckBitmap <> 0) and (CheckBitmap <> ACheckBitmap) then
            DeleteObject(CheckBitmap)
        end;
      finally
        ReleaseDC(0, BaseDC)
      end;
    end;
    
    procedure AddMarkerMenuItem(AMenu: HMENU; AColor: COLORREF; AID: UINT; AChecked: Boolean; AEnabled: Boolean = True);
    var
      MI: MENUITEMINFO;
      Bitmap: Windows.TBitmap;
      W, H: Integer;
      Theme: HTHEME;
      CheckBitmap: HBITMAP;
      CheckDC: HDC;
      CheckOldDCBitmap: HBITMAP;
    begin
      ZeroMemory(@MI, SizeOf(MI));
      MI.cbSize := SizeOf(MI);
      MI.fMask := MIIM_FTYPE or MIIM_ID or MIIM_STATE or MIIM_STRING;
      MI.fType := MFT_STRING;
      MI.wID := AID;
      MI.dwTypeData := 'Marker';
      MI.cch := Length(MI.dwTypeData);
      if AEnabled then MI.fState := MFS_ENABLED
                  else MI.fState := MFS_DISABLED;
      if AChecked then
        MI.fState := MI.fState or MFS_CHECKED;
    
      CheckBitmap := GDICheck(LoadBitmap(0, PChar(OBM_CHECK)));
      try
        GDICheck(GetObject(CheckBitmap, SizeOf(Bitmap), @Bitmap));
    
        if IsWindowsVistaOrLater then
          begin
            MI.fMask := MI.fMask or MIIM_BITMAP;
            W := GetSystemMetrics(SM_CXSMICON);
            H := GetSystemMetrics(SM_CYSMICON);
            if AChecked then
              begin
                CheckBitmap := GDICheck(LoadBitmap(0, PChar(OBM_CHECK)));
                try
                  MI.hbmpItem := CreateColorBitmap(W, H, AColor, CheckBitmap);
                finally
                  DeleteObject(CheckBitmap)
                end;
              end
            else
              MI.hbmpItem := CreateColorBitmap(W, H, AColor, 0);
          end
        else
          begin
            MI.fMask := MI.fMask or MIIM_CHECKMARKS;
            MI.hbmpChecked := CreateColorBitmap(Bitmap.bmWidth - 2, Bitmap.bmHeight, AColor, CheckBitmap);
            MI.hbmpUnchecked := CreateColorBitmap(Bitmap.bmWidth - 2, Bitmap.bmHeight, AColor, 0);
          end;
      finally
        DeleteObject(CheckBitmap)
      end;
    
      InsertMenuItem(AMenu, GetMenuItemCount(AMenu), True, MI);
    end;
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search