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.
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; nVistaOffsetY = 3; }
...
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 *);
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
...
__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
#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: {
if (window == NULL)
break;
window->OnInitMenuPopup((HMENU)wParam);
break;
}
case WM_MEASUREITEM: {
if (window == NULL)
break;
window->OnMeasureItem((LPMEASUREITEMSTRUCT)lParam);
break;
}
case WM_DRAWITEM: {
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.
void OgwwMainFrame::OnInitMenuPopup(HMENU hMenu)
{
UINT nSysDrawn = 0;
UINT nOwnerDrawn = 0;
if (hMenu != NULL)
{
int nMaxItem = ::GetMenuItemCount(hMenu);
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++;
}
}
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.
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)
{
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);
::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; lpMIS->itemHeight += 5; }
::GlobalFree((HGLOBAL)wszThemeFileName);
}
}
}
After OnMeasureItem()
has been called for every menu item, OnDrawItem()
can assume that _nCurrentlyInitializingMenuMaxCaptionWidth
and _nCurrentlyInitializingMenuMaxAcceleratorWidth
have meaningful values.
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; nVistaOffsetY = 3; }
HBRUSH hbrBG = ::CreateSolidBrush(::GetSysColor(COLOR_MENU));
::FillRect(lpDIS->hDC, &(lpDIS->rcItem), hbrBG);
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);
}
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;
}
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;
}
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;
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; nVistaOffsetY = 3; }
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);
::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