Win10 dark theme - how to use in WINAPI? - windows

Starting with October 2018 Update (version 1809) Win10 has support for Dark theme in Windows Explorer.
It can be configured here:
UI: Desktop | Context Menu | Personalize | Colors | Choose your default app mode = Dark
Registry : HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize\AppsUseLightTheme = DWORD:0
While this setting exists for a while now, it only affected UWP applications. However, with this Windows 10 release, it also affects Windows Explorer, which is a Desktop application. This means that Windows now have internal support for it. Still, Desktop applications other then Windows Explorer are not affected at the moment.
I'd like to use it in my application. How is it implemented under the hood? Is there some way (manifest, WINAPI, etc) to subscribe for new dark theme?
Update 1:
I noticed that Windows Explorer Control Panel is partially light and partially dark, so it should be a per-window setting, rather then per-process setting.
One other example: Open File dialogs become dark in all Desktop applications, while the application itself remains in old light theme.
Update 2:
I tried SetWindowTheme(hwnd, L"Explorer", NULL); for TreeView and ListView. This visibly changes TreeView style (+ expand button becomes V), but the window remains white.

See https://github.com/ysc3839/win32-darkmode
This guy has it all laid out in some nice reusable code (MIT license).
It seems that Dark Mode is still an area of development in Windows 10, but I believe that Microsoft will eventually properly document and expose it to desktop apps.
Until then, we are stuck with undocumented ordinal-only imports, then custom draw and WM_CTLCOLOR* messages to dictate how controls that don't yet have native Dark Mode support are painted.
The most fundamental of the new Windows APIs are SetPreferredAppMode (uxtheme#135), to be called prior to any window creation, and AllowDarkModeForWindow (uxtheme#133), to be called on any Window that intends to use native Windows 10 dark mode support.
Here is the full list ordinal-only imports from that project:
using fnRtlGetNtVersionNumbers = void (WINAPI *)(LPDWORD major, LPDWORD minor, LPDWORD build);
// 1809 17763
using fnShouldAppsUseDarkMode = bool (WINAPI *)(); // ordinal 132
using fnAllowDarkModeForWindow = bool (WINAPI *)(HWND hWnd, bool allow); // ordinal 133
using fnAllowDarkModeForApp = bool (WINAPI *)(bool allow); // ordinal 135, removed since 18334
using fnFlushMenuThemes = void (WINAPI *)(); // ordinal 136
using fnRefreshImmersiveColorPolicyState = void (WINAPI *)(); // ordinal 104
using fnIsDarkModeAllowedForWindow = bool (WINAPI *)(HWND hWnd); // ordinal 137
using fnGetIsImmersiveColorUsingHighContrast = bool (WINAPI *)(IMMERSIVE_HC_CACHE_MODE mode); // ordinal 106
using fnOpenNcThemeData = HTHEME(WINAPI *)(HWND hWnd, LPCWSTR pszClassList); // ordinal 49
// Insider 18290
using fnShouldSystemUseDarkMode = bool (WINAPI *)(); // ordinal 138
// Insider 18334
using fnSetPreferredAppMode = PreferredAppMode (WINAPI *)(PreferredAppMode appMode); // ordinal 135, since 18334
using fnIsDarkModeAllowedForApp = bool (WINAPI *)(); // ordinal 139
InitDarkMode imports and initializes dark mode, in a safe manner, carefully checking for min and max supported Windows 10 builds:
void InitDarkMode()
{
fnRtlGetNtVersionNumbers RtlGetNtVersionNumbers = reinterpret_cast<fnRtlGetNtVersionNumbers>(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "RtlGetNtVersionNumbers"));
if (RtlGetNtVersionNumbers)
{
DWORD major, minor;
RtlGetNtVersionNumbers(&major, &minor, &g_buildNumber);
g_buildNumber &= ~0xF0000000;
if (major == 10 && minor == 0 && 17763 <= g_buildNumber && g_buildNumber <= 18363) // Windows 10 1809 10.0.17763 - 1909 10.0.18363
{
HMODULE hUxtheme = LoadLibraryExW(L"uxtheme.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
if (hUxtheme)
{
_OpenNcThemeData = reinterpret_cast<fnOpenNcThemeData>(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(49)));
_RefreshImmersiveColorPolicyState = reinterpret_cast<fnRefreshImmersiveColorPolicyState>(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(104)));
_GetIsImmersiveColorUsingHighContrast = reinterpret_cast<fnGetIsImmersiveColorUsingHighContrast>(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(106)));
_ShouldAppsUseDarkMode = reinterpret_cast<fnShouldAppsUseDarkMode>(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(132)));
_AllowDarkModeForWindow = reinterpret_cast<fnAllowDarkModeForWindow>(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(133)));
auto ord135 = GetProcAddress(hUxtheme, MAKEINTRESOURCEA(135));
if (g_buildNumber < 18334)
_AllowDarkModeForApp = reinterpret_cast<fnAllowDarkModeForApp>(ord135);
else
_SetPreferredAppMode = reinterpret_cast<fnSetPreferredAppMode>(ord135);
//_FlushMenuThemes = reinterpret_cast<fnFlushMenuThemes>(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(136)));
_IsDarkModeAllowedForWindow = reinterpret_cast<fnIsDarkModeAllowedForWindow>(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(137)));
if (_OpenNcThemeData &&
_RefreshImmersiveColorPolicyState &&
_ShouldAppsUseDarkMode &&
_AllowDarkModeForWindow &&
(_AllowDarkModeForApp || _SetPreferredAppMode) &&
//_FlushMenuThemes &&
_IsDarkModeAllowedForWindow)
{
g_darkModeSupported = true;
AllowDarkModeForApp(true);
_RefreshImmersiveColorPolicyState();
g_darkModeEnabled = _ShouldAppsUseDarkMode() && !IsHighContrast();
FixDarkScrollBar();
}
}
}
}
}
Elsewhere, he takes advantage of WM_CTLCOLOR* messages and custom draw notifications to paint dark where Windows doesn't (yet) do it for us.
Note the FixDarkScrollBar. That is an IAT hook on OpenNcThemeData to over-ride the scrollbar theme selection by the listview class in comctl32. That is the part that bothers me most and I'm looking to axe it. I'm sure he is too.
I've adapted this code to my own application and it works well. I am, however, uncomfortable using these undocumented ordinal-only APIs (even as safely as possible), and fully expect Microsoft to eventually announce and document dark mode for Win32 apps and make this work redundant.

After some digging, I was able to find these two approaches. Both are undocumented and may change without notice.
1
SetWindowTheme(hwnd, L"DarkMode_Explorer", NULL);
2
using TYPE_AllowDarkModeForWindow = bool (WINAPI *)(HWND a_HWND, bool a_Allow);
static const TYPE_AllowDarkModeForWindow AllowDarkModeForWindow = (TYPE_AllowDarkModeForWindow)GetProcAddress(hUxtheme, MAKEINTRESOURCEA(133));
AllowDarkModeForWindow(a_HWND, true);
SetWindowTheme(hwnd, L"Explorer", NULL);
WARNING: Ordinal 133 may have completely different API behind it on other versions of Windows, including newer/older Win10 builds.
Both approaches apply some effects, but not everything.
For example, TreeView gets dark scrollbars and dark background for selected item, but the rest of background stays default.
Unfortunately, so far it's not like "call a function and that's it". It seems that even with correct theme applied, some background colors need to be handled manually.

Related

How to enable 'Microsoft.Windows.Common-Controls' for specificate control?

I have a old MFC application that I can't to enable 'Microsoft.Windows.Common-Controls' to all controls in this application because new behavior of some controls. But I need it for CEdit that support to EM_SETCUEBANNER.
I try to do that in OnInitDialog:
m_edt = (CEdit *)GetDlgItem(edit_id);
int i= SetWindowTheme(m_edt->m_hWnd, L"Explorer", NULL);
SetWindowTheme returns 0 but I still cannot use the EM_SETCUEBANNER message.
How can I enable Microsoft.Windows.Common-Controls only for CEdit?
You need to create an Activatation Context that uses a ComCtrl32 v6 manifest. Then you can activate the context before creating the CEdit, and deactivate the context afterwards.
See How can you use both versions 5 and 6 of the common controls within the same module? on Raymond Chen's blog on MSDN.
For example, I did a quick test:
// setup main UI as needed, then...
// I borrowed code from https://stackoverflow.com/a/10444161/65863
// for testing purposes, but you can set lpSource to your own manifest
// file, if needed...
ACTCTX ctx = {};
ctx.cbSize = sizeof(actCtx);
ctx.dwFlags = ACTCTX_FLAG_RESOURCE_NAME_VALID
| ACTCTX_FLAG_SET_PROCESS_DEFAULT
| ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID;
ctx.lpSource = TEXT("shell32.dll");
ctx.lpAssemblyDirectory = TEXT("C:\\Windows\\System32\\"); // <-- don't hard-code this in production code!
ctx.lpResourceName = MAKEINTRESOURCE(124);
HANDLE hActCtx = CreateActCtx(&ctx);
if (hActCtx == INVALID_HANDLE_VALUE) {
// handle error ...
return;
}
ULONG_PTR ulCookie = 0;
if (!ActivateActCtx(hActCtx, &ulCookie)) {
// handle error ...
ReleaseActCtx(hActCtx);
return;
}
// make single Edit control as needed ...
DeactivateActCtx(0, ulCookie);
ReleaseActCtx(hActCtx);
And this was the result:
The app was compiled without any manifest at all, so ComCtrl32 v5 would be the default. The top Edit control was created using the process's default Activation Context, and the bottom Edit control was created with an explicit Activation Context using a ComCtrl32 v6 manifest, and then EM_SETCUEBANNER applied to it (if you don't want to create your own manifest, you can use resource #124 from shell32.dll, per this answer to How to enable visual styles without a manifest).

How can I load the same icon as used by MessageBox on Windows 10?

On Windows 10 calling LoadIcon asking for the standard icon IDI_INFORMATION yields this icon:
On the other hand, calling MessageBox passing IDI_INFORMATION produces a dialog that uses this icon:
How can I obtain the second icon, if the obvious call to LoadIcon does not do so?
This feels like a bug in user32.dll but Windows 8 has the same issue so I guess Microsoft doesn't care.
You can get the flat icon used by MessageBox by calling SHGetStockIconInfo:
SHSTOCKICONINFO sii;
sii.cbSize = sizeof(sii);
if (SUCCEEDED(SHGetStockIconInfo(SIID_INFO, SHGSI_ICON|SHGSI_LARGEICON, &sii)))
{
// Use sii.hIcon here...
DestroyIcon(sii.hIcon);
}
SHGetStockIconInfo is the documented way to get icons used in the Windows UI on Vista and later. Most of the icons come from imageres.dll but you should not assume that this is the case...
we can try next code for test/demo
MSGBOXPARAMSW mbi = {
sizeof(mbi),
HWND_DESKTOP,
NULL,
L"lpszText",
L"lpszCaption",
MB_USERICON,
IDI_INFORMATION
};
MessageBoxIndirectW(&mbi);
if (HMODULE hmodImageRes = LoadLibraryEx(L"imageres", 0, LOAD_LIBRARY_AS_DATAFILE))
{
mbi.hInstance = hmodImageRes;
mbi.lpszIcon = MAKEINTRESOURCE(81);
MessageBoxIndirectW(&mbi);
FreeLibrary(hmodImageRes);
}
first message box use standard IDI_INFORMATION icon
when second the same icon on windows 7, and on windows 8.1 and windows 10.
are MAKEINTRESOURCE(81) from imageres.dll somehow documented and be stable - i doubt
so obtain the second icon you can by LoadIcon(hmodImageRes, MAKEINTRESOURCE(81)) where HMODULE hmodImageRes = LoadLibraryEx(L"imageres", 0, LOAD_LIBRARY_AS_DATAFILE) or simply LoadLibrary(L"imageres")

Using DEFAULT_GUI_FONT in high DPI Windows application

I have a Windows application which I want to look good at high DPI monitors. The application is using DEFAULT_GUI_FONT in lots of places, and the font created this way doesn't scale correctly.
Is there any simple way to fix this problem with not too much pain?
you need get NONCLIENTMETRICS by SystemParametersInfo(SPI_GETNONCLIENTMETRICS,) and then use it LOGFONT data, for create self font. or you can query for SystemParametersInfo(SPI_GETICONTITLELOGFONT) and use it
The recommended fonts for different purposes can be obtained from the NONCLIENTMETRICS structure.
For automatically DPI-scaled fonts (Windows 10 1607+, must be per-monitor DPI-aware):
// Your window's handle
HWND window;
// Get the DPI for which your window should scale to
UINT dpi = GetDpiForWindow(window);
// Obtain the recommended fonts, which are already correctly scaled for the current DPI
NONCLIENTMETRICSW non_client_metrics;
if (!SystemParametersInfoForDpi(SPI_GETNONCLIENTMETRICS, sizeof(non_client_metrics), &non_client_metrics, 0, dpi)
{
// Error handling
}
// Create an appropriate font(s)
HFONT message_font = CreateFontIndirectW(&non_client_metrics.lfMessageFont);
if (!message_font)
{
// Error handling
}
For older Windows versions you can use the system-wide DPI and scale the font manually (Windows 7+, must be system DPI-aware):
// Your window's handle
HWND window;
// Obtain the recommended fonts, which are already correctly scaled for the current DPI
NONCLIENTMETRICSW non_client_metrics;
if (!SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof(non_client_metrics), &non_client_metrics, 0)
{
// Error handling
}
// Get the system-wide DPI
HDC hdc = GetDC(nullptr);
if (!hdc)
{
// Error handling
}
UINT dpi = GetDeviceCaps(hdc, LOGPIXELSY);
ReleaseDC(nullptr, hdc);
// Scale the font(s)
constexpr UINT font_size = 12;
non_client_metrics.lfMessageFont.lfHeight = -((font_size * dpi) / 72);
// Create the appropriate font(s)
HFONT message_font = CreateFontIndirectW(&non_client_metrics.lfMessageFont);
if (!message_font)
{
// Error handling
}
NONCLIENTMETRICS has also many other fonts in it. Make sure to choose the right one for your purpose.
You should set the DPI-awareness level in your application manifest as described here for best compatibility.
WinForms in the .NET framework internally converts the DEFAULT_GUI_FONT (which is in fact used to get the default font for WinForms Forms and Controls in most situations) by scaling its height from pixels (which is the unit GDI fonts use natively) to Points (which is preferred by GDI+). Drawing text using points implies that the physical size of the rendered text depends on the monitor DPI setting.
System.Drawing.Font.SizeInPoints:
float emHeightInPoints;
IntPtr screenDC = UnsafeNativeMethods.GetDC(NativeMethods.NullHandleRef);
try {
using( Graphics graphics = Graphics.FromHdcInternal(screenDC)){
float pixelsPerPoint = (float) (graphics.DpiY / 72.0);
float lineSpacingInPixels = this.GetHeight(graphics);
float emHeightInPixels = lineSpacingInPixels * FontFamily.GetEmHeight(Style) / FontFamily.GetLineSpacing(Style);
emHeightInPoints = emHeightInPixels / pixelsPerPoint;
}
}
finally {
UnsafeNativeMethods.ReleaseDC(NativeMethods.NullHandleRef, new HandleRef(null, screenDC));
}
return emHeightInPoints;
Obviously you cannot use this directly as it's C#. But besides that, this article suggests that you should scale pixel dimensions assuming a 96 dpi design, and use GetDpiForWindow to determine the actual DPI. Note that the "72" in the formula above has nothing to do with the monitor DPI setting, it comes from the fact that .NET likes to use fonts specified in points rather than pixels (otherwise just scale the LOGFONT's height by DPIy/96).
This site suggests something similar, but with GetDpiForMonitor.
I cannot say for sure whether the general approach of manually scaling the font size according to some DPI-dependent factor is a robust and future-proof for scaling fonts (it seems to be the way to go about scaling non-font GUI elements though). However, since .NET basically also just calculates some magic factor based on some sort of DPI value, it's probably a pretty good guess.
Also, you'll want to cache that HFONT. HFONT - LOGFONT conversions are not negligible.
See also (references):
WinForms gets its default using GetStockObject(DEFAULT_GUI_FONT) (there are a few exceptions though, mostly obsolete):
IntPtr handle = UnsafeNativeMethods.GetStockObject(NativeMethods.DEFAULT_GUI_FONT);
try {
Font fontInWorldUnits = null;
// SECREVIEW : We know that we got the handle from the stock object,
// : so this is always safe.
//
IntSecurity.ObjectFromWin32Handle.Assert();
try {
fontInWorldUnits = Font.FromHfont(handle);
}
finally {
CodeAccessPermission.RevertAssert();
}
try{
defaultFont = FontInPoints(fontInWorldUnits);
}
finally{
fontInWorldUnits.Dispose();
}
}
catch (ArgumentException) {
}
https://referencesource.microsoft.com/#System.Drawing/commonui/System/Drawing/SystemFonts.cs,355
The HFONT is converted to GDI+, and then the GDI+ font retrieved this way is transformed using FontInPoints:
private static Font FontInPoints(Font font) {
return new Font(font.FontFamily, font.SizeInPoints, font.Style, GraphicsUnit.Point, font.GdiCharSet, font.GdiVerticalFont);
}
https://referencesource.microsoft.com/#System.Drawing/commonui/System/Drawing/SystemFonts.cs,452
The content of the SizeInPoints getter is already listed above.
https://referencesource.microsoft.com/#System.Drawing/commonui/System/Drawing/Advanced/Font.cs,992

ExtCreatePen and Windows 7 GDI

I created DIBPATTERN pens with ExtCreatePen API for custom pattern pens.
It sucessfully draws desired lines on Windows XP,
But on Windows 7 (x64 for my case), it does not draw any lines; no changes on screen.
(Other simply created pens, for example CreatePen(PS_DOT,1,0), are working.)
I found that calling SetROP2(hdc, R2_XORPEN) makes the following line-drawing API calls draw something but with XOR operation. I don't want XOR drawing.
Here is my code to create the pen. It has no problem on Windows XP:
LOGBRUSH lb;
lb.lbStyle = BS_DIBPATTERN;
lb.lbColor = DIB_RGB_COLORS;
int cb = sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * 2 + 8*4;
HGLOBAL hg = GlobalAlloc(GMEM_MOVEABLE, cb);
BITMAPINFO* pbmi = (BITMAPINFO*) GlobalLock(hg);
ZeroMemory(pbmi, cb);
pbmi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
pbmi->bmiHeader.biWidth = 8;
pbmi->bmiHeader.biHeight = 8;
pbmi->bmiHeader.biPlanes = 1;
pbmi->bmiHeader.biBitCount = 1;
pbmi->bmiHeader.biCompression = BI_RGB;
pbmi->bmiHeader.biSizeImage = 8;
pbmi->bmiHeader.biClrUsed = 2;
pbmi->bmiHeader.biClrImportant = 2;
pbmi->bmiColors[1].rgbBlue =
pbmi->bmiColors[1].rgbGreen =
pbmi->bmiColors[1].rgbRed = 0xFF;
DWORD* p = (DWORD*) &pbmi->bmiColors[2];
for(int k=0; k<8; k++) *p++ = patterns[k];
GlobalUnlock(hg);
lb.lbHatch = (LONG) hg;
s_aSelectionPens[i] = ExtCreatePen(PS_GEOMETRIC, 1, &lb, 0, NULL);
ASSERT(s_aSelectionPens[i]); // success on both XP and Win7
GlobalFree(hg);
Is it bug only on my PC? Please check this problem.
Thank you.
This is a known bug with the Windows 7 GDI, though good luck getting Microsoft to acknowledge it.
http://social.technet.microsoft.com/Forums/en-US/w7itproappcompat/thread/a70ab0d5-e404-4e5e-b510-892b0094caa3
-Noel
I will admit, I was dubious as first, but I compiled and ran your program, and it does indeed fail to draw the second line on Windows 7, buy only in aero mode
By switching to Windows basic or classic mode, all four lines are drawn, as expected.
I can only assume that this is some kind of bad interaction with your custom pen and the new way aero mode implements GDI calls. This seems like it might be a Microsoft bug, perhaps you can post this question on one of their message boards?
So you are creating an 8x8 black/white (monochrome) bitmap as a DIB, and then using that to create a pen. I see nothing wrong with this code. this definitely looks like a windows bug, but there may be a workaround.
Try setting
pbmi->bmiHeader.biClrUsed = 0;
pbmi->bmiHeader.biClrImportant = 0;
In this context, setting the values to 0 should mean the same thing as setting them to 2, but 0 is more standard behavior for situations where you have are using the full palette. You still need two entries in your palette, 0 just means "full size based on biBitCount".
Also, each palette entrie is a RGBQUAD, which means there is room for alpha, and your alpha is set to 0, which should be ignored, but maybe it isn't. so try setting the high byte of your two palette entries to 0xFF or 0x80.
Finally, it's possible that your palette is being ignored entirely, and Windows is using the BkMode, BkColor and TextColor of the destination DC for everything, so you need to make sure that they are set to values that you can see.
My guess is that this has something to do with alpha transparency, since GDI ignores alpha entirely, but Aero doesn't.

Is it necessary to use InitCommonControlsEx() and InitCommonControls()?

I'm completely new to win32. I have been working on it the last 48 hours.
I'm trying to build a "grid", and I got examples of a List-View control and a Header control on msdn.microsoft.com .
The first one calls the InitCommonControls() function (besides I read this function is obsolete).
HWND DoCreateHeader(HWND hwndParent, HINSTANCE hInst)
{
HWND hwndHeader;
RECT rcParent;
HDLAYOUT hdl;
WINDOWPOS wp;
// Ensure that the common control DLL is loaded, and then create
// the header control.
InitCommonControls();
// ...
// hwndHeader = CreateWindowEx(0, WC_HEADER, ...
}
The second one calls the InitCommonControlsEx() function.
HWND CreateListView (HWND hwndParent, HINSTANCE hInst)
{
RECT rcl;
INITCOMMONCONTROLSEX icex;
// Ensure that the common control DLL is loaded.
icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
icex.dwICC = ICC_LISTVIEW_CLASSES;
InitCommonControlsEx(&icex);
// ...
// HWND hWndListView = CreateWindow(WC_LISTVIEW ...
}
Seems these functions need comctl32.lib library, but download it is a mess.
Furthermore I have noticed that if I remove these functions, everything keeps working well. Then, are they necessary?
Thanks!
Yes it is necessary. They are required to get the window classes for those custom controls registered. Odds are, some other component in your code is loading them. I'm not sure, but I think if you have support for comctl v6 (XP and up visual styles) in your manifest, you get commctl32.dll automatically.
More info on what InitCommonControlsEx does is here.
Not sure what you mean by downloading comctl32.lib, it is present on every Windows platform since NT 4 and Windows 95 so you don't need to redistribute it.

Resources