Yet Another Fully Functional ownerdraw Menu





5.00/5 (12 votes)
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:
Here is a selection of the hits for ownerdraw menu on Code Project:
- Unfortunately MFC: A Powerful Ownerdraw Menu by Neil Yao
⇒ Advantage: Uses icons instead of bitmaps, highly configurable background - Unfortunately MFC: Transparent Menu by Derick Cyril Thomas
⇒ Disadvantage: Uses hooks - Unfortunately MFC: Bitmap Menu by Flaviu
⇒ Advantage: Very little effort
⇒ Disadvantage: Uses bitmaps instead of images - Unfortunately very rudimentary: Add XP Visual Style Support to OWNERDRAW Controls by David Y. Zhao
***
⇒ Advantage: Complete set of theme API method prototypes
***
⇒ Disadvantage: Not specific for menu - Unfortunately MFC: Owner Drawn Menu with Icons, Titles and Shading by Bruno Podetti
⇒ Advantage: Very powerful - Unfortunately very rudimentary: Adding XP Visual Style Support to OWNERDRAW Controls Using HTHEME Wrapper by Pål K Tønder
***
⇒ Advantage: Complete set of theme API method prototypes
***
⇒ Disadvantage: not specific for menu - Unfortunately MFC: CMenuXP The Office XP Style Menu by Jean-Michel LE FOL
- Unfortunately WTL: CMenuXP - The OfficeXP menu (WTL version) by Jean-Michel LE FOL
- Unfortunately MFC: CMenuEx - Ownerdrawn Menu by C. Bügenburg
- Unfortunately MFC: CMenuEX - A Bitmap Menu Class by NormDroid
- Unfortunately MFC: Owner Drawn Menu with Icons, Fade 2D-3D Horizontal/Vertical by Dascalescu Romy
⇒ Advantage: Very powerful - Unfortunately very rudimentary: Native Win32 Theme aware Owner-draw Controls without MFC by Ewan Ward
- Unfortunately MFC: A Revolutionary New Approach to Custom Drawn Menus by .dan.g. optimized via Subclassing menu without hooks by Kancleris
***
⇒ Advantage: Also covers ownerdraw menus from other applications, introduces hidden (undocumented) message0x01e5
- Unfortunately MFC: Cool Owner Drawn Menus with Bitmaps - Version 3.03 by Brent Corkum
- Unfortunately MFC: Owner drawn menus in two lines of code by Roger Allen
- Unfortunately MFC: Simple Menus That Display Icons - Minimalistic Approach by Alex Cohn
- Unfortunately MFC: How to Create Owner Drawn Menus - Step by Step by Kevin Stumpf
***
⇒ Advantage: Handles the fluent rectangle - Unfortunately not ownerdraw: An examination of menus from a beginner's point of view by Roger Allen
***
⇒ Advantage: Very detailed description - Unfortunately MFC: MFC extension library - A plugin to handle owner drawn menus by Roger Allen
... 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 | ![]() | ![]() |
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:
- 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.
- 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
):
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:
#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()
:
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.
/// <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.
/// <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.
/// <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