在 Windows 下检测深色模式并监听变化
✨小透明・宸✨
2023-03-29 21:50:12

最近几年很多应用都加入了深色模式,或者叫暗黑模式,英文的写法是 Dark Theme 或 Dark Mode。一般的说法是,深色模式对于 OLED 屏幕可以达到省电的效果,在夜晚等较暗的环境下使用深色模式可以保护视力。保护视力这一点其实还存在争议,有些理论表示人类的视觉本来就偏向于前暗后亮,因此使用深色模式反而会影响阅读效率并且更容易导致疲劳。不过我自己的感受是在夜晚使用深色模式不至于被屏幕上很亮的一大片亮瞎眼睛,从这个体验来说使用深色模式还是有着一定的意义的。

之前出于自己的需求用 Python 和 Tkinter 给图片放大工具 Real-ESRGAN 做了一个 GUI,选择了 Sun-Valley-ttk-theme 这个非常符合 Windows 11 的设计风格的主题,正好发现它同时提供了浅色和深色模式的主题,就打算借用这个在自己做的 GUI 里也加入深色模式的支持了。

首先需要解决的问题是:如何检测当前是否正在使用深色模式?

如果用的是 Qt 之类的大而全的 GUI 框架,这种功能说不定都已经封装好了,不过 Tkinter 是没有这种功能的。还好,有人写了一个叫 Darkdetect 的包,可以跨平台(Windows、Linux 和 macOS)检测系统是否启用了深色模式,使用 darkdetect.isDark() 就可以了。

不过如果只是推荐一个包的话我就不会特地写这个了(笑),那么 Darkdetect 又是怎么实现检测的呢?在 Windows 下,设置里“个性化→颜色→选择模式→选择默认应用模式”就是是否使用深色模式的选项,底层的记录则是注册表里 HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\PersonalizeDWORD 类型的键 AppsUseLightTheme。从名字就可以看出来,值为 0 表示使用深色模式,值为 1 表示不使用深色模式。

通过读取注册表就可以实现检测了。Python 自带的 winreg 模块可以用来读写注册表,其他语言的话也总有读取注册表的办法,实在没有的话直接用 Windows API 也不是不可以(

以前我写过一个简单的 PowerShell 脚本,功能就是读取和写入 AppsUseLightTheme 来快速切换深色模式,借助计划任务就可以实现每天早上和晚上的定时切换了,后来发现了 Auto Dark Mode 这个小工具就转而使用它了。

检测深色模式的问题解决了,现在可以实现在系统使用深色模式的情况下打开那个 GUI 就选择使用深色模式的主题了,不过还有一个问题:在切换深色模式的设置时,GUI 还不能根据跟着自动切换主题。

Darkdetect(当时)还不支持监听深色模式的设置变化,有人提议添加这个功能,但是对于怎么实现大家也没什么头绪。如果在 Google 上搜索“检测深色模式”的话,搜索结果基本上都是前端开发相关的,教你用 CSS 的 @media (prefers-color-scheme: dark) 检测深色模式以及用 JS 的 matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => e.matches) 来监听变化的各种教程;如果搜索“windows detect dark mode change”的话,也基本上只能搜索到怎么“detect”,没什么和“change”有关的东西。

如果 GUI 用的是 Electron 之类的使用 Webview 的方案自然就可以把前端开发的那一套直接拿来用了,但是不使用浏览器的话要怎么办?总不能每秒轮询一次吧?浏览器已经能实现深色模式的监听了,它是怎么实现的呢?

Chromium 是开源的浏览器内核,于是我就试着去翻了 Chromium 的源代码,用前面提过的 AppsUseLightTheme 作为关键词搜索,还真的翻到了几个相关的函数:

翻了 RegKey::Watcher::StartWatching 的源代码才知道,原来 Windows API 里提供了 RegNotifyChangeKeyValue 这个函数可以在注册表更改时进行通知。如果用它检测 AppsUseLightTheme 的更改,就可以实现深色模式的监听了。

用 C 写了一个简单的示例:

#pragma comment(lib, "Advapi32.lib")

#include <stdio.h>
#include <windows.h>

int main(int argc, const char *argv[]) {
    HKEY hKey;
    RegOpenKeyExA(HKEY_CURRENT_USER, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", 0, KEY_NOTIFY | KEY_READ, &hKey);
    DWORD dwSize = sizeof(DWORD);
    DWORD queryValueLast;
    DWORD queryValue;
    RegQueryValueExA(hKey, "AppsUseLightTheme", NULL, NULL, (LPBYTE)&queryValueLast, &dwSize);
    while (TRUE) {
        RegNotifyChangeKeyValue(hKey, TRUE, REG_NOTIFY_CHANGE_LAST_SET, NULL, FALSE);
        RegQueryValueExA(hKey, "AppsUseLightTheme", NULL, NULL, (LPBYTE)&queryValue, &dwSize);
        if (queryValueLast != queryValue) {
            queryValueLast = queryValue;
            puts(queryValue ? "Light" : "Dark");
        }
    }
    return 0;
}

在运行到 RegNotifyChangeKeyValue 这里的时候会一直阻塞,直到注册表的值改变为止,然后读取 AppsUseLightTheme 的值检查深色模式的设置是否有变化,如果有则输出当前的主题。这个监听是死循环,实际使用的时候单独开一个线程就可以了。效果如下:

发现这个之后顺便就给 Darkdetect 提交了一个 Pull request,因为原作者希望使用纯 Python 代码,后来使用 ctypes 进行了改写

与此同时还发现了 Linux 下的监听方法,于是也写进了 Pull request,具体做法可以参见当时提交的代码,这里就不详细介绍了。我没有使用 macOS 的设备,所以当时没有添加 macOS 下的实现,不过这个后来也有人补充了。

“很惭愧,做了一点微小的工作,谢谢大家!”

封面图:Pixiv ID: 96300365 「温かいコーヒーどうぞ!」 by チノマロン

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。不允许内容农场类网站、CSDN 用户和微信公众号转载。
本文作者:✨小透明・宸✨
本文链接:https://akarin.dev/2023/03/29/detect-dark-theme-on-windows/
chevron_left 上一篇 下一篇 chevron_right