Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to properly measure & display owner drawn context menu item with a checkmark?

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?

like image 964
c00000fd Avatar asked Apr 19 '15 01:04

c00000fd


1 Answers

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.

like image 99
rrirower Avatar answered Oct 11 '22 13:10

rrirower