如何正确衡量&显示所有者绘制的上下文菜单项带有复选标记?

时间:2015-04-19 01:25:52

标签: c++ windows winapi mfc contextmenu

我只是想在我的上下文菜单中添加小色样(通过TrackPopupMenu API显示。)这是我想要实现的Photoshop版本:

enter image description here

据我所知,默认菜单不支持它。顺便说一句,上面的样本(没有颜色样本)是通过这样做生成的:

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));

所以我发现我需要使用MFT_OWNERDRAW样式自己绘制菜单项,但这就​​是问题的开始。

我更改了代码以显示我的菜单:

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));

然后我需要覆盖WM_MEASUREITEMWM_DRAWITEM条消息。但是,当我使用下面将要显示的代码执行此操作时,这就是我得到的内容:

enter image description here

所以请耐心等待。关于这个话题我有几个问题:

1)在处理WM_MEASUREITEM时,如果菜单中既没有提供DC也不提供HWND,我应该如何知道文字的大小?换句话说,如果我这样做,菜单的大小是错误的:

#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)然后在处理WM_DRAWITEM时如何知道开始在左侧绘制文本的偏移量?如果我这样做,我的菜单就没有向右偏移(正如你从上面的截图中看到的那样):

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)最后如何在菜单项左侧绘制默认复选框?

2 个答案:

答案 0 :(得分:2)

使用颜色图标的替代方式。

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;

答案 1 :(得分:1)

虽然我不使用颜色样本,并且严格使用MFC,但我会在派生的菜单项上渲染位图。您应该能够根据自己的需要调整以下内容。

在测量项目文本时,我使用桌面直流。

CClientDC dc(CWnd::GetDesktopWindow());
SIZE size;

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

除了通过实验进行的一些微小调整之外,我想出了我需要的尺寸。

为了渲染实际的位图和文本,我检查主题是否处于活动状态,并以两种不同的方式呈现菜单项。作为主题菜单项或标准菜单项。为了渲染文本,我从通过LPDRAWITEMSTRUCT传递的rect开始。然后,我在渲染文本之前进行以下调整。

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

通过反复试验,我发现BITMAP_ADJUSTMENT设置为30对我有效。然后,根据项目的说明(禁用,选择),进一步调整 rectt

//  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);

最后,为了呈现复选标记,我使用预定义的位图创建了一个图像列表(我可能已经从Microsoft dll中提取了它)。同样,根据项目的状态呈现位图。

    //  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));

大多数渲染是通过迭代方法在每次尝试后调整rect对象来完成的。