Setting the menu icon background colour when using Dark Mode

ChuckieAJ 261 Reputation points
2025-02-26T15:33:22.3066667+00:00

I have this function that I use to change the background of an image to match the menu background colour. This has worked up until now because I only supported the default theme. So GetSysColor(COLOR_MENU) was correct.

void CMeetingScheduleAssistantApp::UpdateBitmapBackground(HBITMAP hbmp, bool enabled, COLORREF crBackground /* GetSysColor(COLOR_MENU) */)
{
	if (!hbmp)
		return;
	HDC memdc = CreateCompatibleDC(nullptr);

	BITMAP bm{};
	::GetObject(hbmp, sizeof(bm), &bm);
	const int w = bm.bmWidth;
	const int h = bm.bmHeight;
	BITMAPINFO bi = { sizeof(BITMAPINFOHEADER), w, h, 1, 32, BI_RGB };

	std::vector<uint32_t> pixels(w * h);
	GetDIBits(memdc, hbmp, 0, h, &gsl::at(pixels, 0), &bi, DIB_RGB_COLORS);

	//assume that the color at (0,0) is the background color
	const uint32_t old_color = gsl::at(pixels, 0);

	//this is the new background color
	const uint32_t bk = crBackground;

	//swap RGB with BGR
	uint32_t new_color = RGB(GetBValue(bk), GetGValue(bk), GetRValue(bk));

	//define lambda functions to swap between BGR and RGB
	const auto bgr_r = [](uint32_t color) { return GetBValue(color); };
	const auto bgr_g = [](uint32_t color) { return GetGValue(color); };
	const auto bgr_b = [](uint32_t color) { return GetRValue(color); };

	const BYTE new_red = bgr_r(new_color);
	const BYTE new_grn = bgr_g(new_color);
	const BYTE new_blu = bgr_b(new_color);

	//change background and modify disabled bitmap
	for (auto &p : pixels)
	{
		if (p == old_color)
		{
			p = new_color;
		}
		else if (!enabled)
		{
			//blend color with background, similar to 50% alpha
			BYTE red = (bgr_r(p) + new_red) / 2;
			BYTE grn = (bgr_g(p) + new_grn) / 2;
			BYTE blu = (bgr_b(p) + new_blu) / 2;
			p = RGB(blu, grn, red); //<= BGR/RGB swap
		}
	}

	//fix corner edges
	for (int row = h - 2; row >= 1; row--)
	{
		for (int col = 1; col < w - 1; col++)
		{
			const int i = row * w + col;
			if (gsl::at(pixels, i) != new_color)
			{
				//check the color of neighboring pixels:
				//if that pixel has background color,
				//then that pixel is the background 

				const bool l = gsl::at(pixels, i - 1) == new_color; //left pixel is background
				const bool r = gsl::at(pixels, i + 1) == new_color; //right  ...
				const bool t = gsl::at(pixels, i - w) == new_color; //top    ...
				const bool b = gsl::at(pixels, i + w) == new_color; //bottom ...

				//we are on a corner pixel if:
				//both left-pixel and top-pixel are background or
				//both left-pixel and bottom-pixel are background or
				//both right-pixel and bottom-pixel are background or
				//both right-pixel and bottom-pixel are background
				if (l && t || l && b || r && t || r && b)
				{
					//blend corner pixel with background
					BYTE red = (bgr_r(gsl::at(pixels, i)) + new_red) / 2;
					BYTE grn = (bgr_g(gsl::at(pixels, i)) + new_grn) / 2;
					BYTE blu = (bgr_b(gsl::at(pixels, i)) + new_blu) / 2;
					gsl::at(pixels, i) = RGB(blu, grn, red);//<= BGR/RGB swap
				}
			}
		}
	}

	SetDIBits(memdc, hbmp, 0, h, &gsl::at(pixels, 0), &bi, DIB_RGB_COLORS);
	DeleteDC(memdc);
}

It makes the menu icons look transparent. I have previously tried to use transparent images (eg. PNG) for menu items and I could not get it to work. Perhaps someone can clarify? Because that would do away with the need to change the icon background.

Anyway, when my app is set to use the dark Windows Explorer theme (SetWindowTheme(pDialog->GetSafeHwnd(), L"DarkMode_Explorer", nullptr);) it means that my icons don't have the right background colour any more:

enter image description here

So:

  1. Should we be able to use transparent PNG menu icons in MFC, and if so, how?
  2. How do I get the menu background colour for the DarkMode_Explorer theme so that I can pass it to my function to get the correct background colour?

Update

I tried:

	static COLORREF GetDarkModeMenuBackground()
	{
		static std::optional<COLORREF> cachedColor;
		if (cachedColor.has_value())
			return cachedColor.value();

		// COLORREF color = RGB(32, 32, 32); // Default dark mode background
		COLORREF color = GetSysColor(COLOR_MENU); // Default dark mode background
		HMODULE hUxTheme = LoadLibrary(L"uxtheme.dll");

		if (hUxTheme)
		{
			using OpenThemeData_t = HTHEME(WINAPI*)(HWND, LPCWSTR);
			using GetThemeColor_t = HRESULT(WINAPI*)(HTHEME, int, int, int, COLORREF*);
			using CloseThemeData_t = HRESULT(WINAPI*)(HTHEME);

			auto pOpenThemeData = (OpenThemeData_t)GetProcAddress(hUxTheme, "OpenThemeData");
			auto pGetThemeColor = (GetThemeColor_t)GetProcAddress(hUxTheme, "GetThemeColor");
			auto pCloseThemeData = (CloseThemeData_t)GetProcAddress(hUxTheme, "CloseThemeData");

			if (pOpenThemeData && pGetThemeColor && pCloseThemeData)
			{
				HWND hWnd = AfxGetMainWnd() ? AfxGetMainWnd()->GetSafeHwnd() : nullptr;
				HTHEME hTheme = pOpenThemeData(hWnd, L"Menu"); // Try "Menu" instead of "DarkMode_Explorer"
				if (hTheme)
				{
					pGetThemeColor(hTheme, MENU_POPUPBACKGROUND, 0, TMT_FILLCOLOR, &color);
					pCloseThemeData(hTheme);
				}
			}
			FreeLibrary(hUxTheme);
		}

		cachedColor = color;
		return color;
	}

Sadly, it detects the same colour as GetSysColor(COLOR_MENU).

C++
C++
A high-level, general-purpose programming language, created as an extension of the C programming language, that has object-oriented, generic, and functional features in addition to facilities for low-level memory manipulation.
3,867 questions
{count} votes

1 answer

Sort by: Most helpful
  1. Castorix31 87,076 Reputation points
    2025-02-27T11:38:49.1633333+00:00

    You can use ImageList_Draw and ILD_TRANSPARENT to draw icons with transparency.

    Test with a .png for the ImageList, in C++/Win32 with an OD context menu :

    User's image

    #include <windows.h>
    #include <tchar.h>
    
    #include <commctrl.h>
    #pragma comment (lib, "comctl32")
    
    #include <Vsstyle.h>
    #include <vssym32.h>
    #include <uxtheme.h> 
    #pragma comment(lib, "UxTheme")
    
    #include <gdiplus.h>
    using namespace Gdiplus;
    #pragma comment(lib, "gdiplus.lib")
    
    #pragma comment(linker,"\"/manifestdependency:type='win32' \
    name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
    processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
    
    HINSTANCE hInst;
    LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
    int nWidth = 600, nHeight = 400;
    #define IDC_STATIC 10
    #define IDC_BUTTON 11
    
    struct ODM_DATA
    {
    	int nImageListIndex;
    	TCHAR szItemText[128];
    };
    
    int m_nSizeBitmap = 48;
    HIMAGELIST m_hImageList = NULL;
    HBITMAP m_hBitmapImageList = NULL;
    
    void MeasureItem(HWND hWnd, LPMEASUREITEMSTRUCT lpMeasure);
    void DrawItem(HWND hWnd, LPDRAWITEMSTRUCT lpDraw);
    
    enum PreferredAppMode { Default, AllowDark, ForceDark, ForceLight, Max };
    typedef PreferredAppMode(WINAPI* fnSetPreferredAppMode)(PreferredAppMode appMode);
    fnSetPreferredAppMode pSetPreferredAppMode;
    
    int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow)
    {
    	 GdiplusStartupInput gdiplusStartupInput;
    	 ULONG_PTR gdiplusToken;
    	 GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
    
    	hInst = hInstance;
    	WNDCLASSEX wcex =
    	{
    		sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW, WndProc, 0, 0, hInst, LoadIcon(NULL, IDI_APPLICATION),
    		LoadCursor(NULL, IDC_ARROW), (HBRUSH)(COLOR_WINDOW + 1), NULL, TEXT("WindowClass"), NULL,
    	};
    	if (!RegisterClassEx(&wcex))
    		return MessageBox(NULL, TEXT("Cannot register class !"), TEXT("Error"), MB_ICONERROR | MB_OK);
    	int nX = (GetSystemMetrics(SM_CXSCREEN) - nWidth) / 2, nY = (GetSystemMetrics(SM_CYSCREEN) - nHeight) / 2;
    	HWND hWnd = CreateWindowEx(0, wcex.lpszClassName, TEXT("Test"), WS_OVERLAPPEDWINDOW, nX, nY, nWidth, nHeight, NULL, NULL, hInst, NULL);
    	if (!hWnd)
    		return MessageBox(NULL, TEXT("Cannot create window !"), TEXT("Error"), MB_ICONERROR | MB_OK);
    	ShowWindow(hWnd, SW_SHOWNORMAL);
    	UpdateWindow(hWnd);
    	MSG msg;
    	while (GetMessage(&msg, NULL, 0, 0))
    	{
    		TranslateMessage(&msg);
    		DispatchMessage(&msg);
    	}
    	GdiplusShutdown(gdiplusToken);
    	return (int)msg.wParam;
    }
    
    LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
    	static HWND hWndButton = NULL, hWndStatic = NULL;
    	int wmId, wmEvent;
    	switch (message)
    	{
    	case WM_CREATE:
    	{
    		// hWndStatic = CreateWindowEx(0, TEXT("Static"), TEXT(""), WS_CHILD | WS_VISIBLE | SS_BITMAP, 10, 10, 200, 200, hWnd, (HMENU)IDC_STATIC, hInst, NULL);
    		// hWndButton = CreateWindowEx(0, L"Button", L"Click", WS_CHILD | WS_VISIBLE | BS_PUSHLIKE, 100, 60, 60, 32, hWnd, (HMENU)IDC_BUTTON, hInst, NULL);
    	
    		// copied at https://i.ibb.co/0RmQM2dp/Balls-Image-List.png
    		Bitmap* bitmap = new Bitmap(TEXT("Balls_ImageList.png"));
    		Gdiplus:Status nStatus = bitmap->GetHBITMAP(Gdiplus::Color(255, 255, 255), &m_hBitmapImageList);
    		m_hImageList = ImageList_Create(m_nSizeBitmap, m_nSizeBitmap, ILC_COLOR32 | ILC_MASK, 1, 0);
    		int nIndex = ImageList_AddMasked(m_hImageList, m_hBitmapImageList, RGB(255, 255, 255));
    		HMODULE hDll = LoadLibrary(TEXT("UXTheme.dll"));
    		pSetPreferredAppMode = (fnSetPreferredAppMode)GetProcAddress(hDll, MAKEINTRESOURCEA(135));
    		if (pSetPreferredAppMode)
    			pSetPreferredAppMode(AllowDark);
    		return 0;
    	}
    	break;
    	case WM_RBUTTONDOWN:
    	{
    		HMENU hMenu = CreatePopupMenu();
    		ODM_DATA data;		
    		for (int i = 1; i <= 10; i++)
    		{
    			ODM_DATA* pData = (ODM_DATA*)malloc(sizeof(ODM_DATA));
    			if (pData)
    			{
    				data.nImageListIndex = i - 1;
    				swprintf_s(data.szItemText, TEXT("Menu Item %d"), i);
    				memcpy(pData, &data, sizeof(ODM_DATA));
    				AppendMenu(hMenu, MF_BYCOMMAND | MF_OWNERDRAW, i, (LPCWSTR)pData);				
    			}
    		}			
    		POINT pt;
    		GetCursorPos(&pt);
    		int nChoice = TrackPopupMenu(hMenu, TPM_LEFTALIGN | TPM_RETURNCMD, pt.x, pt.y, 0, hWnd, NULL);
    		if (nChoice != 0)
    		{
    			MENUITEMINFO mii = { sizeof(MENUITEMINFO) };
    			mii.fMask = MIIM_DATA;
    			if (GetMenuItemInfo(hMenu, nChoice, FALSE, &mii))
    			{
    				free((ODM_DATA*)mii.dwItemData);
    				TCHAR wsMessage[MAX_PATH] = _T("");
    				wsprintf(wsMessage, TEXT("Selected item : %d"), nChoice);
    				MessageBox(NULL, wsMessage, TEXT("Information"), MB_OK | MB_ICONINFORMATION);
    			}
    		}
    		DestroyMenu(hMenu);
    	}
    	break;
    	case WM_MEASUREITEM:
    	{
    		MeasureItem(hWnd, (LPMEASUREITEMSTRUCT)lParam);
    		return	TRUE;
    	}
    	break;
    	case WM_DRAWITEM:
    	{
    		DrawItem(hWnd, (LPDRAWITEMSTRUCT)lParam);
    		return	TRUE;
    	}
    	break;
    	case WM_COMMAND:
    	{
    		wmId = LOWORD(wParam);
    		wmEvent = HIWORD(wParam);
    		switch (wmId)
    		{
    		case IDC_BUTTON:
    		{
    			if (wmEvent == BN_CLICKED)
    			{
    				Beep(1000, 10);
    			}
    		}
    		break;
    		default:
    			return DefWindowProc(hWnd, message, wParam, lParam);
    		}
    	}
    	break;
    	case WM_PAINT:
    	{
    		PAINTSTRUCT ps;
    		HDC hDC = BeginPaint(hWnd, &ps);
    
    		EndPaint(hWnd, &ps);
    	}
    	break;
    	case WM_DESTROY:
    	{
    		PostQuitMessage(0);
    		return 0;
    	}
    	break;
    	default:
    		return DefWindowProc(hWnd, message, wParam, lParam);
    	}
    	return 0;
    }
    
    void MeasureItem(HWND hWnd, LPMEASUREITEMSTRUCT pMIS)
    {
    	ODM_DATA* data = (ODM_DATA*)pMIS->itemData;
    	pMIS->itemHeight = m_nSizeBitmap;
    	pMIS->itemWidth = 260;
    }
    
    void DrawItem(HWND hWnd, LPDRAWITEMSTRUCT pDIS)
    {
    	if (!pDIS || !pDIS->itemData) return;
    
    	ODM_DATA* data = (ODM_DATA*)pDIS->itemData;
    	HTHEME hMenuTheme = OpenThemeData(NULL, TEXT("MENU"));
    
    	HDC hdc = pDIS->hDC;
    	RECT rcItem = pDIS->rcItem;
    
    	if (hMenuTheme)
    	{
    		DrawThemeBackground(hMenuTheme, hdc, MENU_POPUPBACKGROUND, MPI_NORMAL, &rcItem, NULL);
    		DrawThemeBackground(hMenuTheme, hdc, MENU_POPUPITEM, (pDIS->itemState & ODS_SELECTED) ? MPI_HOT : MPI_NORMAL,&rcItem, NULL);
    	}
    	else
    	{		
    		HBRUSH hBrush = CreateSolidBrush(GetSysColor((pDIS->itemState & ODS_SELECTED) ? COLOR_HIGHLIGHT : COLOR_MENU));
    		FillRect(hdc, &rcItem, hBrush);
    		DeleteObject(hBrush);
    	}
    
    	ImageList_Draw(m_hImageList, data->nImageListIndex, hdc, rcItem.left, rcItem.top, ILD_TRANSPARENT);
    
    	RECT rcText = rcItem;
    	rcText.left += m_nSizeBitmap + 8;
    
    	if (hMenuTheme)
    	{
    		DrawThemeText(hMenuTheme, hdc, MENU_POPUPITEM, (pDIS->itemState & ODS_SELECTED) ? MPI_HOT : MPI_NORMAL,
    			data->szItemText, -1, DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX, 0,	&rcText);
    		CloseThemeData(hMenuTheme);
    	}
    	else
    	{		
    		SetBkMode(hdc, TRANSPARENT);
    		SetTextColor(hdc, GetSysColor((pDIS->itemState & ODS_SELECTED) ? COLOR_HIGHLIGHTTEXT : COLOR_MENUTEXT));
    		DrawText(hdc, data->szItemText, -1, &rcText, DT_SINGLELINE | DT_VCENTER);
    	}	
    }
    
    
    

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.