Immersive dark mode + Theme Listener (#18315)

* C++ impl of immersive dark mode

* Stop using the hardcoded value.

* Conjured up theme listener based on registry.

* Update MainWindow.xaml.cpp

* Update expect.txt

* Moved themehelpers to the common themes lib.

* Ported theme helpers back to .NET

* Update expect.txt

* Updated C# Theme Listening logic to mimic the one from Windows Community Toolkit.

* Replaced unmanaged code for RegisterForImmersiveDarkMode with unmanaged ThemeListener class.

* Fix upstream changes

* Update ThemeListener.h

* Update ThemeListener.h

* Proper formatting

* Added handler to Keyboard Manager.

* Update EditKeyboardWindow.cpp

* Added dwmapi.lib to runner, removed condition from additional dependencies.

* Update PowerRenameUI.vcxproj

* Added new deps for ManagedCommon to Product.wxs

* Crude attempts and understanding installer

* Removed Microsoft.Win32.Registry.dll from product.wxs.

* Updated dictionary

* Renamed ThemeListener class file for consistency, removed unused CheckImmersiveDarkMode in theme_helpers.

* Update Themes.vcxproj

* Update theme_listener.cpp

* Removed SupportsImmersiveDarkMode version check

* Removed SupportsImmersiveDarkMode version check

* Whoops

* Update expect.txt
This commit is contained in:
William Bradley
2022-07-01 21:52:48 +12:00
committed by GitHub
parent e637902892
commit b7fccc3211
22 changed files with 393 additions and 78 deletions

View File

@@ -17,6 +17,8 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="System.Management" Version="6.0.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -14,4 +14,10 @@ namespace ManagedCommon
HighContrastBlack,
HighContrastWhite,
}
public enum AppTheme
{
Dark = 0,
Light = 1,
}
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.Win32;
namespace ManagedCommon
{
// Based on https://stackoverflow.com/a/62811758/5001796
public static class ThemeHelpers
{
[DllImport("dwmapi.dll")]
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
internal const string HKeyRoot = "HKEY_CURRENT_USER";
internal const string HkeyWindowsTheme = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Themes";
internal const string HkeyWindowsPersonalizeTheme = $@"{HkeyWindowsTheme}\Personalize";
internal const string HValueAppTheme = "AppsUseLightTheme";
internal const int DWMWAImmersiveDarkMode = 20;
// based on https://stackoverflow.com/questions/51334674/how-to-detect-windows-10-light-dark-mode-in-win32-application
public static AppTheme GetAppTheme()
{
int value = (int)Registry.GetValue($"{HKeyRoot}\\{HkeyWindowsPersonalizeTheme}", HValueAppTheme, 1);
return (AppTheme)value;
}
public static void SetImmersiveDarkMode(IntPtr window, bool enabled)
{
int useImmersiveDarkMode = enabled ? 1 : 0;
_ = DwmSetWindowAttribute(window, DWMWAImmersiveDarkMode, ref useImmersiveDarkMode, sizeof(int));
}
}
}

View File

@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Management;
using System.Security.Principal;
namespace ManagedCommon
{
/// <summary>
/// The Delegate for a ThemeChanged Event.
/// </summary>
/// <param name="sender">Sender ThemeListener</param>
public delegate void ThemeChangedEvent(ThemeListener sender);
public class ThemeListener : IDisposable
{
/// <summary>
/// Gets the App Theme.
/// </summary>
public AppTheme AppTheme { get; private set; }
/// <summary>
/// An event that fires if the Theme changes.
/// </summary>
public event ThemeChangedEvent ThemeChanged;
private readonly ManagementEventWatcher watcher;
public ThemeListener()
{
var currentUser = WindowsIdentity.GetCurrent();
var query = new WqlEventQuery(
$"SELECT * FROM RegistryValueChangeEvent WHERE Hive='HKEY_USERS' AND " +
$"KeyPath='{currentUser.User.Value}\\\\{ThemeHelpers.HkeyWindowsPersonalizeTheme.Replace("\\", "\\\\")}' AND ValueName='{ThemeHelpers.HValueAppTheme}'");
watcher = new ManagementEventWatcher(query);
watcher.EventArrived += Watcher_EventArrived;
watcher.Start();
AppTheme = ThemeHelpers.GetAppTheme();
}
private void Watcher_EventArrived(object sender, EventArrivedEventArgs e)
{
var appTheme = ThemeHelpers.GetAppTheme();
if (appTheme != AppTheme)
{
AppTheme = appTheme;
ThemeChanged?.Invoke(this);
}
}
public void Dispose()
{
watcher.Dispose();
GC.SuppressFinalize(this);
}
}
}

View File

@@ -31,10 +31,14 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="icon_helpers.h" />
<ClInclude Include="theme_listener.h" />
<ClInclude Include="theme_helpers.h" />
<ClInclude Include="windows_colors.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="icon_helpers.cpp" />
<ClCompile Include="theme_listener.cpp" />
<ClCompile Include="theme_helpers.cpp" />
<ClCompile Include="windows_colors.cpp" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,43 @@
// Port Based on https://stackoverflow.com/a/62811758/5001796
#include "theme_helpers.h"
#include "dwmapi.h"
#include <windows.h>
#include <vector>
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#define HKEY_WINDOWS_THEME L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"
// based on https://stackoverflow.com/questions/51334674/how-to-detect-windows-10-light-dark-mode-in-win32-application
AppTheme ThemeHelpers::GetAppTheme()
{
// The value is expected to be a REG_DWORD, which is a signed 32-bit little-endian
auto buffer = std::vector<char>(4);
auto cbData = static_cast<DWORD>(buffer.size() * sizeof(char));
auto res = RegGetValueW(
HKEY_CURRENT_USER,
HKEY_WINDOWS_THEME,
L"AppsUseLightTheme",
RRF_RT_REG_DWORD, // expected value type
nullptr,
buffer.data(),
&cbData);
if (res != ERROR_SUCCESS)
{
return AppTheme::Light;
}
// convert bytes written to our buffer to an int, assuming little-endian
auto i = int(buffer[3] << 24 |
buffer[2] << 16 |
buffer[1] << 8 |
buffer[0]);
return AppTheme(i);
}
void ThemeHelpers::SetImmersiveDarkMode(HWND window, bool enabled)
{
int useImmersiveDarkMode = enabled ? 1 : 0;
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, &useImmersiveDarkMode, sizeof(useImmersiveDarkMode));
}

View File

@@ -0,0 +1,14 @@
#pragma once
#include <windows.h>
enum class AppTheme
{
Dark = 0,
Light = 1
};
struct ThemeHelpers
{
static AppTheme GetAppTheme();
static void ThemeHelpers::SetImmersiveDarkMode(HWND window, bool enabled);
};

View File

@@ -0,0 +1,57 @@
#include "theme_listener.h"
#define HKEY_WINDOWS_THEME L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"
DWORD WINAPI _checkTheme(LPVOID lpParam)
{
auto listener = (ThemeListener*)lpParam;
listener->CheckTheme();
return 0;
}
void ThemeListener::AddChangedHandler(THEME_HANDLE handle)
{
handles.push_back(handle);
}
void ThemeListener::DelChangedHandler(THEME_HANDLE handle)
{
auto it = std::find(handles.begin(), handles.end(), handle);
handles.erase(it);
}
void ThemeListener::CheckTheme()
{
HANDLE hEvent;
HKEY hKey;
// Open the Key to listen
RegOpenKeyEx(HKEY_CURRENT_USER, HKEY_WINDOWS_THEME, 0, KEY_NOTIFY, &hKey);
while (true)
{
// Create an event.
hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (hEvent != 0)
{
// Watch the registry key for a change of value.
RegNotifyChangeKeyValue(hKey,
TRUE,
REG_NOTIFY_CHANGE_LAST_SET,
hEvent,
TRUE);
WaitForSingleObject(hEvent, INFINITE);
auto _theme = ThemeHelpers::GetAppTheme();
if (AppTheme != _theme)
{
AppTheme = _theme;
for (int i = 0; i < handles.size(); i++)
{
handles[i]();
}
}
}
}
}

View File

@@ -0,0 +1,34 @@
#include "theme_helpers.h"
#include "dwmapi.h"
#include <windows.h>
#include <iostream>
#include <vector>
typedef void (*THEME_HANDLE)();
DWORD WINAPI _checkTheme(LPVOID lpParam);
#pragma once
class ThemeListener
{
public:
ThemeListener()
{
AppTheme = ThemeHelpers::GetAppTheme();
dwThreadHandle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)_checkTheme, this, 0, &dwThreadId);
}
~ThemeListener()
{
CloseHandle(dwThreadHandle);
dwThreadId = 0;
}
AppTheme AppTheme;
void ThemeListener::AddChangedHandler(THEME_HANDLE handle);
void ThemeListener::DelChangedHandler(THEME_HANDLE handle);
void CheckTheme();
private:
HANDLE dwThreadHandle;
DWORD dwThreadId;
std::vector<THEME_HANDLE> handles;
};

View File

@@ -1,4 +1,5 @@
#include "windows_colors.h"
#include "theme_helpers.h"
DWORD WindowsColors::rgb_color(DWORD abgr_color)
{
@@ -65,7 +66,7 @@ WindowsColors::Color WindowsColors::get_background_color()
bool WindowsColors::is_dark_mode()
{
return rgb_color(get_background_color()) == 0;
return ThemeHelpers::GetAppTheme() == AppTheme::Dark;
}
bool WindowsColors::update()