Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Yet Another Fully Functional ownerdraw Menu

5.00/5 (14 votes)
24 Jan 2020CPOL6 min read 21.2K   622  
Another fully functional ownerdraw menu with minimal effort - this time based on Win32, with icons instead of bitmaps, with accelerators and tested for ReactOS and WinNT 4.0 to Windows 10

Introduction

Yes, I know - on Code Project, the topic ownerdraw menu or menus with bitmaps/icons has been covered several times already. Nevertheless, I have still not found, what fits my requirements:

  • C/C++ and Win32 (no MFC, WTL or wxWidgets - because they are too heavy)
  • no hooks or dirty tricks

Here is a selection of the hits for ownerdraw menu on Code Project:

... and so on, and so on. And I have already left out everything in my listing that is VB, C# or .NET! For a deeper dive into ownerdraw menus, I'd recommend to read *** marked articles.

Update 1

In the meantime, I discovered here on Code Project the excellent Win32++ class library. It represents a much thinner layer around the Win32 API than MFC or wxWidgets do. It runs on old Windows versions, ReactOS and comes with a huge amount of sample projects for Code.:Blocks, Dev-C++ and Visual Studio 2003 through 2019. See this tip Win32++ programmatically created menus with Code::Blocks on ReactOS for an impression of what Win32++ menus look like.

How Does It Look

I wanted the OWNERDRAW menus to integrate as seamlessly as possible and ideally be indistinguishable from native menus. For testing purposes, I have compared an OWNERDRAW menu and a native menu for ReactOS 0.4.11 and different Windows versions.

Windows version OWNERDRAW menu native menu

Win2000
⇒ Version 5.0

WinXP
⇒ No theme
⇒ Version 5.1
WinXP
⇒ Ahorn theme
⇒ Version 5.1
ReactOS 0.4.11
⇒ No theme
⇒ Version 5.1
ReactOS
⇒ Modern theme
⇒ Version 5.1
Windows Vista
⇒ Aero theme
⇒ Version 6.0
Windows Vista
⇒ No theme
⇒ Version 6.0
Windows 7
⇒ Aero theme
⇒ Version 6.1
Windows 7
⇒ Contrast black
⇒ Version 6.1
Windows 8
⇒ Aero theme
⇒ Version 6.2
Windows 10
⇒ Aero theme
⇒ Version 6.2

As you can see - there are two drawbacks:

  1. The OWNERDRAW menu item height is not quite correct for Windows Viata and higher Windows versions, if not the Aero theme but a classic theme is used. All I found to work around this problem is to check if a valid theme file has been loaded.
  2. The OWNERDRAW menu item fluent rectangle is not drawn for Windows Viata, Windows 7 and Windows 8/8.1, if the Aero theme is used. It should be easy to fix this by checking the version number (if you have overcome the difficulty to get the correct version number).

Using the Code

Workarounds

These drawbacks can be solved by checking the theme file name (whether there is any) and the version number like this (since the determination of theme file name is quite time-consuming, I save the calculation result on bThemedDimensions):

C++
if (Utils_GetOsVersion() >= 6.0f && OgwwThemes::Init() && OgwwThemes::Themed())
{
    LPWSTR wszThemeFileName = (LPWSTR)::GlobalAlloc(GPTR, MAX_PATH);
    OgwwThemes::GetCurrentThemeName(wszThemeFileName, MAX_PATH - 1, NULL, 0, NULL, 0);
    if (wcslen(wszThemeFileName) > 0)
        bThemedDimensions = true;
    ::GlobalFree((HGLOBAL)wszThemeFileName);
}

...

if (bThemedDimensions)
{
    nVistaOffsetX = 6; // Icon size grows (13px to 15px) and icon spacing grows.
    nVistaOffsetY = 3; // Icon spacing grow.
}

...

if (bThemedDimensions && Utils_GetOsVersion() < 10)
{
    COLORREF crCurrPen = 0;
    crCurrPen = ::GetSysColor(COLOR_3DLIGHT);

    int     nEdgeWidth = ::GetSystemMetrics(SM_CYEDGE);
    int     nImageOffsetX = ::GetSystemMetrics(SM_CXMENUCHECK) + nEdgeWidth + nEdgeWidth;
    HPEN    hNewPen = ::CreatePen(PS_SOLID, 1, crCurrPen);
    HGDIOBJ hOldPen = ::SelectObject(lpDIS->hDC, hNewPen);
    ::MoveToEx(lpDIS->hDC, lpDIS->rcItem.left + nImageOffsetX + 1 + nVistaOffsetX,
               lpDIS->rcItem.top, NULL);
    ::LineTo(lpDIS->hDC, lpDIS->rcItem.left + nImageOffsetX + 1 + nVistaOffsetX,
             lpDIS->rcItem.bottom);
    ::SelectObject(lpDIS->hDC, ::GetStockObject(WHITE_PEN));
    ::MoveToEx(lpDIS->hDC, lpDIS->rcItem.left + nImageOffsetX + 2 + nVistaOffsetX,
               lpDIS->rcItem.top, NULL);
    ::LineTo(lpDIS->hDC, lpDIS->rcItem.left + nImageOffsetX + 2 + nVistaOffsetX,
             lpDIS->rcItem.bottom);
    ::SelectObject(lpDIS->hDC, hOldPen);
}

The Utils_GetOsVersion() is based on ::RtlGetVersion() because ::GetVersionEx() is marked to be deprecated and ::VerifyVersionInfo() returns the manifested version of the application, rather than the runtime version of the OS, starting with Windows 8.1 (version 6.3). This is how it works:

C++
#if defined(__GNUC__) || defined(__MINGW32__)
#else

typedef void (WINAPI * RtlGetVersion_FUNC) (OSVERSIONINFOEXW *);

/// <summary>
/// Gets the current OS version information.
/// </summary>
/// <param name="dwMajorVersion">The buffer for the version information to get.</param>
/// <returns>Returns <c>TRUE</c> on success, or <c>FALSE</c> otherwise.</returns>
BOOL RtlGetVersionEx(OSVERSIONINFOEX * os)
{
    HMODULE hMod;
    RtlGetVersion_FUNC func;
#ifdef UNICODE
    OSVERSIONINFOEXW * osw = os;
#else
    OSVERSIONINFOEXW o;
    OSVERSIONINFOEXW * osw = &o;
#endif

    hMod = LoadLibrary(TEXT("ntdll.dll"));
    if (hMod)
    {
        func = (RtlGetVersion_FUNC)GetProcAddress(hMod, "RtlGetVersion");
        if (func == 0)
        {
            FreeLibrary(hMod);
            return FALSE;
        }
        ZeroMemory(osw, sizeof(*osw));
        osw->dwOSVersionInfoSize = sizeof(*osw);
        func(osw);
#ifndef UNICODE
        os->dwBuildNumber = osw->dwBuildNumber;
        os->dwMajorVersion = osw->dwMajorVersion;
        os->dwMinorVersion = osw->dwMinorVersion;
        os->dwPlatformId = osw->dwPlatformId;
        os->dwOSVersionInfoSize = sizeof(*os);
        DWORD sz = sizeof(os->szCSDVersion);
        WCHAR * src = osw->szCSDVersion;
        unsigned char * dtc = (unsigned char *)os->szCSDVersion;
        while (*src)
            * Dtc++ = (unsigned char)* src++;
        *Dtc = '\ 0';
#endif
    }
    else
        return FALSE;
    FreeLibrary(hMod);
    return TRUE;
}
#endif

...

/// <summary>
/// Gets the OS version as float.
/// </summary>
/// <returns>Returns the OS version as float.</returns>
/// <remarks>
/// Windows 10             : 10.0
/// Windows 8.1            : 6.3
/// Windows 8.0            : 6.2
/// Windows Server 2012    : 6.15
/// Windows 7              : 6.1
/// Windows Server 2008 R2 : 6.05
/// Windows Vista          : 6.0
///</remarks>
__declspec(dllexport) float __cdecl Utils_GetOsVersion()
{
    if (Utils::fOsVersion != 0.0f)
        return Utils::fOsVersion;

    OSVERSIONINFOEX info;
    ::ZeroMemory(&info, sizeof(OSVERSIONINFOEX));
    info.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);

#if defined(__GNUC__) || defined(__MINGW32__)

    ::GetVersionEx((LPOSVERSIONINFOW)&info);

    Utils::fOsVersion = (info.dwMajorVersion * 1.0f) + (info.dwMinorVersion * 0.1f)
        - ((info.wProductType != VER_NT_WORKSTATION) ? 0.05f : 0.0f);

#else

    // Starting with Windows 8.1, '::GetVersionEx()' always returns 6.2 except the
    // application is manifested for a higher version. Deprecated with Windows 8.1.
#pragma warning(suppress : 4996)
    ::GetVersionEx((LPOSVERSIONINFOW)&info);

    Utils::fOsVersion = (info.dwMajorVersion * 1.0f) + (info.dwMinorVersion * 0.1f)
                      - ((info.wProductType != VER_NT_WORKSTATION) ? 0.05f : 0.0f);

    if (Utils::fOsVersion > 6.1f)
    {
        if (RtlGetVersionEx(&info) == TRUE)
        {
            Utils::fOsVersion = (info.dwMajorVersion * 1.0f) + (info.dwMinorVersion * 0.1f)
                              - ((info.wProductType != VER_NT_WORKSTATION) ? 0.05f : 0.0f);
        }
    }

#endif

The result is quite satisfactory:

Windows version OWNERDRAW menu native menu
Windows 7
⇒ Aero theme
⇒ Version 6.1
Windows 7
⇒ Contrast black
⇒ Version 6.1

The Implementation

All my menus are handled by the main frames' WindowProcedure():

C++
case WM_INITMENUPOPUP: // 279
{
    if (window == NULL)
        break;

    window->OnInitMenuPopup((HMENU)wParam);
    break;
}
/* https://www.codeproject.com/articles/16529/simple-menus-that-display-icons-minimalistic-appro */
case WM_MEASUREITEM: // 44
{
    if (window == NULL)
        break;

    // ReactOS calls it every time a menu item hast to be draws.
    // Windows10 calls it only, if menu item hasn't been measured.
    window->OnMeasureItem((LPMEASUREITEMSTRUCT)lParam);
    break;
}
/* https://www.codeproject.com/articles/16529/simple-menus-that-display-icons-minimalistic-appro */
case WM_DRAWITEM: // 43
{
    if (window == NULL)
        break;

    window->OnDrawItem((LPDRAWITEMSTRUCT)lParam);
    break;
}

The challenge when calculating the menu size is the correct handling of the accelerators. The data structures of Win32 do not support this and so I introduced the helper variables _hCurrentlyInitializingMenu, _nCurrentlyInitializingMenuMaxCaptionWidth and _nCurrentlyInitializingMenuMaxAcceleratorWidth.

It can be safely assumed that WM_INITMENUPOPUP is always called before WM_MEASUREITEM and WM_MEASUREITEM always before WM_DRAWITEM. On ReactOS, WM_INITMENUPOPUP is called at every menu startup, on Windows only at the first menu startup.

The calculation result from _nCurrentlyInitializingMenuMaxCaptionWidth and _nCurrentlyInitializingMenuMaxAcceleratorWidth must then be saved for WM_DRAWITEM. The structure MENUITEMINFO is designed to handle this (together with the storage of the icon).

To provide a consistent look & feel, all menu items of a menu must be OWNERDRAW - once at least one menu item of a menu is OWNERDRAW. OnInitMenuPopup() ensures this.

C++
/// <summary>
/// Processes the WM_INITMENUPOPUP message.
/// </summary>
/// <param name="hMenu">The menu to initialize.</param>
void OgwwMainFrame::OnInitMenuPopup(HMENU hMenu)
{
    // We must ensure that NONE or ALL menu items are MF_OWNERDRAW.
    // Otherwise we can't ensure the shortcuts are displayed correctly.
    UINT  nSysDrawn = 0;
    UINT  nOwnerDrawn = 0;

    if (hMenu != NULL)
    {
        int nMaxItem = ::GetMenuItemCount(hMenu);

        // Check whether menu items must upgraded to MF_OWNERDRAW.
        for (int nItemIdx = 0; nItemIdx < nMaxItem; nItemIdx++)
        {
            MENUITEMINFO    mii;
            memset(&mii, 0, sizeof(MENUITEMINFO));
            mii.cbSize = sizeof(MENUITEMINFO);
            mii.fMask = MIIM_STATE | MIIM_TYPE;
            ::GetMenuItemInfo(hMenu, nItemIdx, TRUE, &mii);

            if ((mii.fType & MFT_SEPARATOR) != MFT_SEPARATOR)
            {
                if ((mii.fType & MF_OWNERDRAW) != MF_OWNERDRAW)
                    nSysDrawn++;
                else
                    nOwnerDrawn++;
            }
        }

        // Upgrade menu items to MF_OWNERDRAW.
        for (int nItemIdx = 0;
             nOwnerDrawn != 0 && nSysDrawn != 0 && nItemIdx < nMaxItem; nItemIdx++)
        {
            MENUITEMINFO    mii;
            memset(&mii, 0, sizeof(MENUITEMINFO));
            mii.cbSize = sizeof(MENUITEMINFO);
            mii.fMask = MIIM_STATE | MIIM_TYPE;
            ::GetMenuItemInfo(hMenu, nItemIdx, TRUE, &mii);

            if ((mii.fType & MFT_SEPARATOR) != MFT_SEPARATOR)
            {
                if ((mii.fType & MF_OWNERDRAW) != MF_OWNERDRAW)
                {
                    int nItemID = ::GetMenuItemID(hMenu, nItemIdx);
                    ::ModifyMenu(hMenu, nItemIdx, mii.fState | MF_BYPOSITION | MF_OWNERDRAW,
                                 nItemID, NULL);
                }
            }
        }
        _hCurrentlyInitializingMenu = hMenu;
        _nCurrentlyInitializingMenuMaxCaptionWidth = 0;
        _nCurrentlyInitializingMenuMaxAcceleratorWidth = 0;
    }
}

After OnInitMenuPopup() has reset the helper variables _hCurrentlyInitializingMenu, _nCurrentlyInitializingMenuMaxCaptionWidth and _nCurrentlyInitializingMenuMaxAcceleratorWidth, OnMeasureItem() and OnDrawItem() can assume to be provided with a defined state of these helper variables.

C++
/// <summary>
/// Processes the WM_MEASUREITEM message.
/// </summary>
/// <param name="lpMIS">The menu item measure structure.</param>
void OgwwMainFrame::OnMeasureItem(LPMEASUREITEMSTRUCT lpMIS)
{
    if (lpMIS->CtlType == ODT_MENU)
    {
        int    nEdgeWidth = ::GetSystemMetrics(SM_CYEDGE);
        lpMIS->itemWidth = ::GetSystemMetrics(SM_CXMENUCHECK) + nEdgeWidth + nEdgeWidth;
        lpMIS->itemHeight = 13 + nEdgeWidth + nEdgeWidth;

        LPMENUITEMDATA lpMID = (LPMENUITEMDATA)lpMIS->itemData;
        if (lpMID != NULL && lpMID->hMenu)
        {
            WCHAR wszBuffer[256];
            int   nCharCount = ::GetMenuString(lpMID->hMenu, lpMIS->itemID, wszBuffer,
                                               255, MF_BYCOMMAND);
            if (nCharCount > 0)
            {
                int nAcceleratorDelimiter;
                for (nAcceleratorDelimiter = 0;
                     nAcceleratorDelimiter < nCharCount; nAcceleratorDelimiter++)
                    if (wszBuffer[nAcceleratorDelimiter] == L'\t' ||
                        wszBuffer[nAcceleratorDelimiter] == L'\b')
                        break;

                RECT   rTextMetric = { 0, 0, 0, 0 };
                HDC    hDC = ::GetDC(HWnd());

                if (hDC != NULL)
                {
                    // Obviously, at least ReactOS does not guarantee the selection
                    // of the proper menu font.
                    if (_hMenuFontNormal == NULL)
                    {
                        NONCLIENTMETRICSW nm;
                        nm.cbSize = sizeof(NONCLIENTMETRICS);
                        VERIFY(::SystemParametersInfoW(SPI_GETNONCLIENTMETRICS,
                                                       nm.cbSize, &nm, 0) != FALSE);
                        _hMenuFontNormal = ::CreateFontIndirect(&(nm.lfMenuFont));
                    }
                    VERIFY(_hMenuFontNormal != NULL);

                    HFONT hOldFont = NULL;
                    if (_hMenuFontNormal != NULL)
                        hOldFont = (HFONT)::SelectObject(hDC, _hMenuFontNormal);
                    // DEBUG-BEGIN
                    // TEXTMETRIC tm;
                    // ::GetTextMetrics(hDC, &tm);
                    // DEBUG-END

                    ::DrawTextW(hDC, wszBuffer, nAcceleratorDelimiter, &rTextMetric,
                                DT_CALCRECT);
                    _nCurrentlyInitializingMenuMaxCaptionWidth =
                        Math::Max(_nCurrentlyInitializingMenuMaxCaptionWidth,
                                  rTextMetric.right - rTextMetric.left);

                    if (nAcceleratorDelimiter < nCharCount - 1)
                    {
                        ::DrawTextW(hDC, &(wszBuffer[nAcceleratorDelimiter + 1]),
                                    nCharCount - nAcceleratorDelimiter - 1, &rTextMetric,
                                    DT_CALCRECT);
                        _nCurrentlyInitializingMenuMaxAcceleratorWidth =
                           Math::Max(_nCurrentlyInitializingMenuMaxAcceleratorWidth,
                           rTextMetric.right - rTextMetric.left);
                    }
                    if (hOldFont == NULL)
                        ::SelectObject(hDC, hOldFont);

                    ::ReleaseDC(HWnd(), hDC);

                    lpMIS->itemWidth = _nCurrentlyInitializingMenuMaxCaptionWidth +
                                       _nCurrentlyInitializingMenuMaxAcceleratorWidth +
                                       (_nCurrentlyInitializingMenuMaxAcceleratorWidth > 0 ?
                                        1 + MENU_FONT_AVERAGE_CHAR_WIDTH : 0) + 2;
                    lpMIS->itemWidth += ::GetSystemMetrics(SM_CXMENUCHECK) +
                                        nEdgeWidth + nEdgeWidth;
                }
            }
        }

        if (Utils_GetOsVersion() >= 6.0f && OgwwThemes::Init() && OgwwThemes::Themed())
        {
            LPWSTR wszThemeFileName = (LPWSTR)::GlobalAlloc(GPTR, MAX_PATH);
            OgwwThemes::GetCurrentThemeName(wszThemeFileName, MAX_PATH - 1, NULL, 0, NULL, 0);
            if (wcslen(wszThemeFileName) > 0)
            {
                lpMIS->itemWidth += 14; // Icon size grows (13px to 15px) and icon spacing grows.
                lpMIS->itemHeight += 5; // Icon spacing grows.
            }
            ::GlobalFree((HGLOBAL)wszThemeFileName);
        }
    }
}

After OnMeasureItem() has been called for every menu item, OnDrawItem() can assume that _nCurrentlyInitializingMenuMaxCaptionWidth and _nCurrentlyInitializingMenuMaxAcceleratorWidth have meaningful values.

C++
/// <summary>
/// Processes the WM_DRAWITEM message.
/// </summary>
/// <param name="lpDIS">The menu item draw structure.</param>
void OgwwMainFrame::OnDrawItem(LPDRAWITEMSTRUCT lpDIS)
{
    if (lpDIS->CtlType == ODT_MENU)
    {
        BOOL bDisabled = lpDIS->itemState & ODS_GRAYED;
        BOOL bSelected = lpDIS->itemState & ODS_SELECTED;
        BOOL bChecked = lpDIS->itemState & ODS_CHECKED;

        bool bThemedDimensions = false;
        bool bThemedBg = false;
        LONG nVistaOffsetX = 0;
        LONG nVistaOffsetY = 0;
        LPMENUITEMDATA lpMID = (LPMENUITEMDATA)lpDIS->itemData;

        if (Utils_GetOsVersion() >= 6.0f && OgwwThemes::Init() && OgwwThemes::Themed())
        {
            LPWSTR wszThemeFileName = (LPWSTR)::GlobalAlloc(GPTR, MAX_PATH);
            OgwwThemes::GetCurrentThemeName(wszThemeFileName, MAX_PATH - 1, NULL, 0, NULL, 0);
            if (wcslen(wszThemeFileName) > 0)
                bThemedDimensions = true;
            ::GlobalFree((HGLOBAL)wszThemeFileName);
        }
        if (bThemedDimensions)
        {
            nVistaOffsetX = 6; // Icon size grows (13px to 15px) and icon spacing grows.
            nVistaOffsetY = 3; // Icon spacing grows.
        }

        // Background.
        HBRUSH   hbrBG = ::CreateSolidBrush(::GetSysColor(COLOR_MENU));
        ::FillRect(lpDIS->hDC, &(lpDIS->rcItem), hbrBG);

        // Delimiter.
        if (bThemedDimensions && Utils_GetOsVersion() < 10)
        {
            COLORREF crCurrPen = 0;
            crCurrPen = ::GetSysColor(COLOR_3DLIGHT);

            int     nEdgeWidth = ::GetSystemMetrics(SM_CYEDGE);
            int     nImageOffsetX = ::GetSystemMetrics(SM_CXMENUCHECK) +
                                    nEdgeWidth + nEdgeWidth;
            HPEN    hNewPen = ::CreatePen(PS_SOLID, 1, crCurrPen);
            HGDIOBJ hOldPen = ::SelectObject(lpDIS->hDC, hNewPen);
            ::MoveToEx(lpDIS->hDC, lpDIS->rcItem.left + nImageOffsetX + 1 +
                       nVistaOffsetX, lpDIS->rcItem.top, NULL);
            ::LineTo(lpDIS->hDC, lpDIS->rcItem.left + nImageOffsetX + 1 + nVistaOffsetX,
                     lpDIS->rcItem.bottom);
            ::SelectObject(lpDIS->hDC, ::GetStockObject(WHITE_PEN));
            ::MoveToEx(lpDIS->hDC, lpDIS->rcItem.left + nImageOffsetX + 2 +
                       nVistaOffsetX, lpDIS->rcItem.top, NULL);
            ::LineTo(lpDIS->hDC, lpDIS->rcItem.left + nImageOffsetX + 2 +
                     nVistaOffsetX, lpDIS->rcItem.bottom);
            ::SelectObject(lpDIS->hDC, hOldPen);
        }

        // Highlight.
        if (bSelected)
        {
            bThemedBg = false;
            if (OgwwThemes::Themed() == true)
            {
                HTHEME hTheme = OgwwThemes::OpenThemeData((HWND)_hHandle, L"MENU");
                if (hTheme != NULL)
                {
                    OgwwThemes::DrawThemeBackground(hTheme, lpDIS->hDC,
                        OgwwThemes::PART__MENU_POPUPITEM,
                        OgwwThemes::STATE__MPI_HOT, &(lpDIS->rcItem), NULL);
                    OgwwThemes::CloseThemeData(hTheme);
                    bThemedBg = true;
                }
            }
            if (bThemedBg == false)
            {
                HBRUSH   hbrBG = ::CreateSolidBrush(::GetSysColor(COLOR_HIGHLIGHT));
                ::FillRect(lpDIS->hDC, &(lpDIS->rcItem), hbrBG);
            }
        }

        if (bThemedDimensions)
        {
            nVistaOffsetX += 8;
            nVistaOffsetX += 2;
        }

        // Caption.
        WCHAR wszBuffer[256];
        int   nCharCount = ::GetMenuString((HMENU)lpDIS->hwndItem, lpDIS->itemID, wszBuffer,
                                           255, MF_BYCOMMAND);
        if (nCharCount > 0)
        {
            COLORREF crPrevText = 0;
            COLORREF crCurrText = 0;
            bThemedBg = false;
            if (OgwwThemes::Themed() == true)
            {
                HTHEME hTheme = OgwwThemes::OpenThemeData((HWND)_hHandle, L"MENU");
                if (hTheme != NULL)
                {
                    crCurrText = OgwwThemes::GetThemeSysColor(hTheme,
                                     bDisabled ? COLOR_GRAYTEXT :
                                         bSelected ? COLOR_HIGHLIGHTTEXT : COLOR_MENUTEXT);
                    OgwwThemes::CloseThemeData(hTheme);
                    bThemedBg = true;
                }
                // Fix problem with Vista and above:
                if (Utils_GetOsVersion() >= 6.0f && bSelected)
                    crCurrText = ::GetSysColor(COLOR_MENUTEXT);
            }
            if (bThemedBg == false)
                crCurrText = ::GetSysColor(bDisabled ? COLOR_GRAYTEXT :
                                           bSelected ? COLOR_HIGHLIGHTTEXT : COLOR_MENUTEXT);
            crPrevText = ::SetTextColor(lpDIS->hDC, crCurrText);

            int nAcceleratorDelimiter;
            for (nAcceleratorDelimiter = 0;
                 nAcceleratorDelimiter < nCharCount; nAcceleratorDelimiter++)
                if (wszBuffer[nAcceleratorDelimiter] == L'\t' ||
                    wszBuffer[nAcceleratorDelimiter] == L'\b')
                    break;

            // Obviously, at least ReactOS does not guarantee the selection of
            // the proper menu font.
            if (_hMenuFontNormal == NULL)
            {
                NONCLIENTMETRICSW nm;
                nm.cbSize = sizeof(NONCLIENTMETRICS);
                VERIFY(::SystemParametersInfoW(SPI_GETNONCLIENTMETRICS,
                                               nm.cbSize, &nm, 0) != FALSE);
                _hMenuFontNormal = ::CreateFontIndirect(&(nm.lfMenuFont));
            }
            VERIFY(_hMenuFontNormal != NULL);

            HFONT hOldFont = NULL;
            if (_hMenuFontNormal != NULL)
                hOldFont = (HFONT)::SelectObject(lpDIS->hDC, _hMenuFontNormal);

            int nOldBkMode = ::SetBkMode(lpDIS->hDC, TRANSPARENT);

            int    nEdgeWidth = ::GetSystemMetrics(SM_CYEDGE);
            int    nImageOffsetX = ::GetSystemMetrics(SM_CXMENUCHECK) +
                                   nEdgeWidth + nEdgeWidth;
            lpDIS->rcItem.left += nImageOffsetX + nVistaOffsetX;
            lpDIS->rcItem.top += nVistaOffsetY;
            ::DrawTextW(lpDIS->hDC, wszBuffer, nAcceleratorDelimiter, &(lpDIS->rcItem), 0);

            LONG nMenuMaxCaptionWidth = _nCurrentlyInitializingMenuMaxCaptionWidth;
            if (lpMID != NULL)
            {
                if (lpMID->nMenuMaxCaptionWidth != 0)
                    nMenuMaxCaptionWidth = lpMID->nMenuMaxCaptionWidth;
                else
                    lpMID->nMenuMaxCaptionWidth = _nCurrentlyInitializingMenuMaxCaptionWidth;
            }

            if (nAcceleratorDelimiter < nCharCount - 1)
            {
                lpDIS->rcItem.left += nMenuMaxCaptionWidth + 1 + MENU_FONT_AVERAGE_CHAR_WIDTH;
                if (wszBuffer[nAcceleratorDelimiter] == L'\t')
                    ::DrawTextW(lpDIS->hDC, &(wszBuffer[nAcceleratorDelimiter + 1]),
                                nCharCount - nAcceleratorDelimiter - 1, &(lpDIS->rcItem),
                                DT_LEFT | DT_SINGLELINE);
                else
                    ::DrawTextW(lpDIS->hDC, &(wszBuffer[nAcceleratorDelimiter + 1]),
                                nCharCount - nAcceleratorDelimiter - 1, &(lpDIS->rcItem),
                                DT_RIGHT | DT_SINGLELINE);
                lpDIS->rcItem.left -= nMenuMaxCaptionWidth + 1 + MENU_FONT_AVERAGE_CHAR_WIDTH;
            }
            lpDIS->rcItem.left -= nImageOffsetX + nVistaOffsetX;
            lpDIS->rcItem.top -= nVistaOffsetY;

            ::SetBkMode(lpDIS->hDC, nOldBkMode);
            ::SetTextColor(lpDIS->hDC, crPrevText);

            if (hOldFont == NULL)
                ::SelectObject(lpDIS->hDC, hOldFont);
        }

        MENUITEMINFOW mii;
        ::ZeroMemory(&mii, sizeof(mii));
        mii.cbSize = sizeof(mii);
        mii.fMask = MIIM_CHECKMARKS | MIIM_DATA;

        if (bThemedDimensions)
        {
            nVistaOffsetX = 6; // Icon size grows (13px to 15px) and icon spacing grows.
            nVistaOffsetY = 3; // Icon spacing grows.
        }

        GetMenuItemInfo((HMENU)lpDIS->hwndItem, lpDIS->itemID, FALSE, &mii);
        if ((bChecked && mii.hbmpChecked != NULL) || (mii.hbmpUnchecked != NULL))
        {
            HDC     hdcMem = ::CreateCompatibleDC(lpDIS->hDC);
            HGDIOBJ oldBitmap = ::SelectObject(hdcMem, bChecked ? mii.hbmpChecked :
                                                                  mii.hbmpUnchecked);

            //::GetObject(mii.hbmpItem, sizeof(bitmap), &bitmap);
            ::BitBlt(lpDIS->hDC, lpDIS->rcItem.left + nVistaOffsetX,
                     lpDIS->rcItem.top + nVistaOffsetY, 13, 13, hdcMem, 0, 0, SRCCOPY);

            ::SelectObject(hdcMem, oldBitmap);
            ::DeleteDC(hdcMem);
        }

        if (lpMID != NULL && lpMID->hIcon)
        {
            ::DrawIconEx(lpDIS->hDC, lpDIS->rcItem.left + nVistaOffsetX + 1,
                         lpDIS->rcItem.top + nVistaOffsetY + 2,
                lpMID->hIcon, 13, 13, 0, NULL, DI_NORMAL);
        }
    }
}

Conclusion

The download at the beginning of this article contains the complete source code as the Code::Blocks project for ReactOS and a pre-built compilate. The source code can also be compiled with Visual Studio on Windows without any changes. To follow the progress of the Ogww library, I recommend you take a look at my CodeProject article, A basic icon editor running on ReactOS.

History

  • 14th January, 2020: Initial version
  • 24th January, 2020: Update 1

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)