Electron nativeTheme 模块详解:监听与控制系统原生暗色主题的完整指南
【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron
本篇以 Electron 仓库的 nativeTheme API 文档 为主体,完整覆盖该模块全部属性与事件,并结合 C++ 实现层、Chromium 补丁 与 测试用例 讲清其底层原理。读完后你将能够:正确地在主进程中监听系统主题变化、用themeSource实现“跟随系统 / 深色 / 浅色”三态切换,并理解该属性如何同步影响 Electron 原生 UI、macOS 系统外观与prefers-color-schemeCSS 查询。
模块概览与进程归属
nativeTheme模块用于读取并响应 Chromium 原生颜色主题的变化(Read and respond to changes in Chromium's native color theme)。
该模块运行在 Main 进程 中,通过require('electron')解构获得:
const { nativeTheme } = require('electron') console.log(nativeTheme.shouldUseDarkColors) // true / false从源码结构看,模块在 C++ 侧通过 gin 绑定注册。Initialize 函数 将NativeTheme::Create(isolate)挂到 Node 导出对象上,而绑定符号electron_browser_native_theme也与 typings/internal-ambient.d.ts 中声明的_linkedBinding一一对应。
一个关键实现细节:构造时 Electron 同时持有了两个ui::NativeTheme实例——
// shell/browser/api/electron_api_native_theme.cc // NativeTheme::Create 中: ui::NativeTheme::GetInstanceForNativeUi(), // ui_theme_:控制原生 UI(菜单、DevTools) ui::NativeTheme::GetInstanceForWeb() // web_theme_:控制 Web 内容(prefers-color-scheme)这解释了为什么设置themeSource时需要同时修改两者,下文会详细展开。
事件:'updated'
ThenativeThememodule emits the following events:
Event: 'updated'
当底层 NativeTheme 发生任何变化时触发。这通常意味着shouldUseDarkColors、shouldUseHighContrastColors或shouldUseInvertedColorScheme三者之一的值发生了变化。你必须在回调中重新读取这三个属性,才能确定具体是哪一项改变了——事件本身不携带变更信息。
const { nativeTheme } = require('electron') nativeTheme.on('updated', () => { // 事件不指明变化来源,需自行检查 console.log({ dark: nativeTheme.shouldUseDarkColors, highContrast: nativeTheme.shouldUseHighContrastColors, inverted: nativeTheme.shouldUseInvertedColorScheme }) })底层触发链路在 NativeTheme::OnNativeThemeUpdated 中:ui::NativeThemeObserver回调可能在任意线程被调用,实现将其PostTask回 UI 线程后再执行Emit("updated"),保证事件始终在主进程事件循环中派发:
void NativeTheme::OnNativeThemeUpdated(ui::NativeTheme* theme) { content::GetUIThreadTaskRunner({})->PostTask( FROM_HERE, base::BindOnce(&NativeTheme::OnNativeThemeUpdatedOnUI, ...)); }Windows 平台还有一个附加行为:OnNativeThemeUpdatedOnUI 在派发事件前会读取注册表键HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize的SystemUsesLightTheme值,据此刷新“系统整合 UI 的深浅色”状态(见下文shouldUseDarkColorsForSystemIntegratedUI)。
测试用例 api-native-theme-spec.ts 精确验证了该事件的语义边界:
- 当设置
themeSource导致shouldUseDarkColors结果改变时,必须触发updated('light' → 'dark'与'dark' → 'light'各触发一次); - 当新设置的值与当前状态相同时(如已是
'dark'再设'dark'),不触发updated。
属性:shouldUseDarkColors(只读)
nativeTheme.shouldUseDarkColorsReadonly
boolean,表示当前 OS / Chromium 是否启用了暗色模式,或正被指示显示暗色风格 UI。如果需要修改该值,应使用下文介绍的themeSource属性,而非直接改动此属性。
C++ 侧的实现 ShouldUseDarkColors 揭示了判定优先级:强制覆盖 > 系统偏好:
bool NativeTheme::ShouldUseDarkColors() { auto theme_source = GetThemeSource(); if (theme_source == ui::NativeTheme::ThemeSource::kForcedLight) return false; // 强制浅色:无论系统如何,恒为 false if (theme_source == ui::NativeTheme::ThemeSource::kForcedDark) return true; // 强制深色:恒为 true return ui_theme_->preferred_color_scheme() == ui::NativeTheme::PreferredColorScheme::kDark; }即:themeSource为light时该属性恒为false,为dark时恒为true,为system时取决于系统实际偏好。测试 直接断言了这一覆盖行为。
核心:themeSource 三态属性
nativeTheme.themeSource
string,取值为system、light或dark。它用于覆盖并取代(override and supersede)Chromium 内部所选定的主题值。默认值为system。
- 设为
system:移除覆盖,一切恢复为 OS 默认; - 设为
dark/light:强制对应主题。
设置该属性为dark会产生以下效果:
- 访问
nativeTheme.shouldUseDarkColors时返回true; - Electron 在 Linux 和 Windows 上渲染的所有 UI(包括右键上下文菜单、DevTools 等)使用暗色 UI;
- macOS 上由 OS 渲染的 UI(菜单、窗口边框等)使用暗色 UI;
- CSS 媒体查询
prefers-color-scheme匹配dark模式; - 触发
updated事件。
设置为light的效果与之镜像对称:shouldUseDarkColors返回false,Electron 在 Linux/Windows 上渲染的 UI(上下文菜单、DevTools 等)与 macOS 上 OS 渲染的 UI(菜单、窗口边框等)均使用浅色,prefers-color-scheme匹配light,并触发updated事件。
推荐的“暗色模式”状态机用法
文档明确建议:该属性的使用应与应用程序中经典的“dark mode”状态机保持一致,用户拥有三个选项:
| 用户选项 | 代码写法 |
|---|---|
| 跟随系统(Follow OS) | themeSource = 'system' |
| 深色模式(Dark Mode) | themeSource = 'dark' |
| 浅色模式(Light Mode) | themeSource = 'light' |
并且:应用此后应始终使用shouldUseDarkColors来决定应用哪套 CSS,而不是直接判断themeSource——因为system模式下真正的深浅色取决于 OS。
const { nativeTheme } = require('electron') // 三态切换 function setThemeChoice(choice) { // 'system' | 'dark' | 'light' nativeTheme.themeSource = choice } // CSS 侧:始终依据 shouldUseDarkColors 的实时结果 console.log(nativeTheme.shouldUseDarkColors)底层实现:一次赋值如何联动原生 UI 与 Web 内容
SetThemeSource 展示了完整的副作用链:
void NativeTheme::SetThemeSource(ui::NativeTheme::ThemeSource override) { ui_theme_->set_theme_source(override); // ① 原生 UI(菜单、DevTools) web_theme_->set_theme_source(override); // ② Web 内容(prefers-color-scheme) #if BUILDFLAG(IS_MAC) UpdateMacOSAppearanceForOverrideValue(override); // ③ macOS 系统外观 #endif }三个环节对应文档中列出的三类效果:
ui_theme_:Chromium 侧主题源由 Electron 对上游的补丁 feat: add set_theme_source... 提供。该补丁在ui/native_theme/native_theme.h中新增了ThemeSource { kSystem, kForcedDark, kForcedLight }枚举与set_theme_source()方法,并让preferred_color_scheme()在强制状态下直接返回kLight/kDark;只有当强制导致的明暗状态真正翻转时才调用NotifyOnNativeThemeUpdated()——这正是“值不变则不发updated事件”这一测试语义的根源。web_theme_:使渲染进程中的prefers-color-scheme媒体查询随之切换。测试 通过executeJavaScript在页面内读取matchMedia("(prefers-color-scheme: dark)").matches并监听其change事件,验证了切换themeSource后页面内的媒体查询结果确实被同步覆盖。- macOS 外观:UpdateMacOSAppearanceForOverrideValue 将
dark映射为NSAppearanceNameDarkAqua、light映射为NSAppearanceNameAqua、system映射为nil(交还系统),并调用[[NSApplication sharedApplication] setAppearance:...]。因为 macOS 的菜单栏、窗口边框等由 OS 自身绘制,Electron 必须通过NSApplication的外观属性才能让这些 OS 渲染的 UI 跟随应用内选择——这正是文档中“Any UI the OS renders on macOS including menus, window frames, etc.”效果的实现来源。
此外,字符串与枚举之间的映射由 gin 转换器完成:Converter<ui::NativeTheme::ThemeSource> 的FromV8仅接受"dark"、"light"、"system"三种字符串,传入其他值会返回false(即赋值无效)。
辅助只读属性:无障碍与高对比度
nativeTheme.shouldUseHighContrastColorsmacOSWindowsReadonly
boolean,表示当前 OS / Chromium 是否启用了高对比度模式,或正被指示显示高对比度 UI。
实现上 ShouldUseHighContrastColors 直接比较ui_theme_->preferred_contrast()是否等于PreferredContrast::kMore。注意平台标注:仅 macOS 与 Windows 有该属性(Linux 无此概念,故源码中未做平台条件编译,但文档标注其适用平台)。
nativeTheme.shouldUseDarkColorsForSystemIntegratedUImacOSWindowsReadonly
boolean,表示系统主题是否被设置为深色或浅色。
在 Windows 上,该属性用于区分“系统主题”与“应用主题”:返回true表示系统主题设为深色,否则返回false(Windows 允许系统与应用使用不同的明暗主题)。在 macOS 上,返回值与nativeTheme.shouldUseDarkColors相同。
从源码看,ShouldUseDarkColorsForSystemIntegratedUI 优先返回缓存的should_use_dark_colors_for_system_integrated_ui_(std::optional<bool>,默认nullopt),该缓存由 Windows 注册表读取逻辑(见上文updated事件一节)在主题更新时刷新;无缓存时回退到ShouldUseDarkColors()。
nativeTheme.shouldUseInvertedColorSchememacOSWindowsReadonly
boolean,表示 OS / Chromium 是否启用了反色(inverted color scheme),或正被指示使用反色方案。
实现 因平台而异:
- macOS:读取
com.apple.universalaccess偏好域的whiteOnBlack布尔值(“白底变黑底”辅助功能开关); - 其他平台:
forced_colors非kNone且偏好色板为kDark时返回true。
nativeTheme.inForcedColorsModeWindowsReadonly
boolean,表示 Chromium 是否处于强制颜色模式(forced colors mode),该模式由系统无障碍设置控制。目前,Windows 高对比度是唯一能触发强制颜色模式的系统设置。
实现为 InForcedColorsMode:判断ui_theme_->forced_colors()是否不等于ColorProviderKey::ForcedColors::kNone。
nativeTheme.prefersReducedTransparencyReadonly
boolean,表示用户是否通过系统无障碍设置在 OS 层级选择了减少透明度(reduce transparency)。
实现 直接透传ui_theme_->prefers_reduced_transparency()。典型用途:当该值为true时,为使用vibrancy/ 毛玻璃效果的窗口提供不透明回退样式。
nativeTheme.shouldDifferentiateWithoutColormacOSReadonly
boolean,表示用户是否偏好用颜色以外的方式(如形状或标签)区分 UI 元素。该属性直接映射到 macOS 的NSWorkspace.accessibilityDisplayShouldDifferentiateWithoutColor。
实现 确认了这一点:
bool NativeTheme::ShouldDifferentiateWithoutColor() { return [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldDifferentiateWithoutColor]; }注意该属性在 对象模板注册 处被#if BUILDFLAG(IS_MAC)包裹,仅在 macOS 构建中存在;测试 也仅在process.platform === 'darwin'时运行。
实战:完整的暗色模式应用
仓库 docs/tutorial/dark-mode.md 提供了一个完整示例(fiddle 位于 docs/fiddles/features/dark-mode),演示了一个从nativeTheme派生主题色、并通过 IPC 提供“切换 / 重置为系统”控件的应用。核心结构如下:
main.js(主进程)—— 实际的nativeTheme操作只发生在主进程:
const { app, BrowserWindow, ipcMain, nativeTheme } = require('electron') const path = require('node:path') const createWindow = () => { const win = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, 'preload.js') } }) win.loadFile('index.html') ipcMain.handle('dark-mode:toggle', () => { if (nativeTheme.shouldUseDarkColors) { nativeTheme.themeSource = 'light' } else { nativeTheme.themeSource = 'dark' } return nativeTheme.shouldUseDarkColors }) ipcMain.handle('dark-mode:system', () => { nativeTheme.themeSource = 'system' }) } app.whenReady().then(() => { createWindow(); /* ... */ })preload.js—— 通过contextBridge安全地暴露两个 IPC 通道给渲染进程:
const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('darkMode', { toggle: () => ipcRenderer.invoke('dark-mode:toggle'), system: () => ipcRenderer.invoke('dark-mode:system') })styles.css—— 页面侧只需声明prefers-color-scheme媒体查询,themeSource的变化会被自动传播到渲染进程,相关 CSS 规则随之更新:
@media (prefers-color-scheme: dark) { body { background: #333; color: white; } } @media (prefers-color-scheme: light) { body { background: #ddd; color: black; } }renderer.js—— 按钮点击经window.darkMode调用 IPC,主进程返回的shouldUseDarkColors用于更新页面显示。
注意示例中一个细节:切换逻辑判断的是shouldUseDarkColors(实时结果)而非themeSource(用户意图)——当themeSource为system且系统为深色时,shouldUseDarkColors为true,点击“Toggle”会进入light分支并强制浅色,这符合“三态状态机”的语义。
验证:测试套件如何约束该模块的行为
spec/api-native-theme-spec.ts 对该模块建立了系统化的行为契约,可作为 API 语义的权威参考:
| 用例 | 验证的语义 |
|---|---|
themeSource is system by default | 默认值为system |
should override the value of shouldUseDarkColors | dark/light对shouldUseDarkColors的强制覆盖 |
should emit the "updated" event when ... value changes | 明暗状态翻转时必发事件 |
should not emit ... when ... value is the same | 状态未变化时不发事件 |
should override the result of prefers-color-scheme CSS media query | 通过页面内matchMedia+ IPCchange事件验证 Web 侧同步 |
各只读属性returns a boolean | shouldUseInvertedColorScheme、shouldUseHighContrastColors、shouldUseDarkColorsForSystemIntegratedUI、inForcedColorsMode、prefersReducedTransparency、shouldDifferentiateWithoutColor(仅 darwin)均返回布尔值 |
小结
nativeTheme模块的完整 API 面由一个事件(updated)、一个可写属性(themeSource)和七个只读属性(shouldUseDarkColors、shouldUseHighContrastColors、shouldUseDarkColorsForSystemIntegratedUI、shouldUseInvertedColorScheme、inForcedColorsMode、prefersReducedTransparency、shouldDifferentiateWithoutColor)组成。实践要点可归纳为三条:
- 监听:
updated事件不携带变化字段,回调中需重新读取相关只读属性; - 控制:遵循“system / dark / light”三态状态机写
themeSource,随后始终用shouldUseDarkColors决定 CSS; - 无障碍:高对比度、反色、减少透明度、无颜色区分等属性应作为无障碍样式回退的判定依据,其中
shouldDifferentiateWithoutColor仅存在于 macOS(源码中由BUILDFLAG(IS_MAC)条件编译),inForcedColorsMode仅标注于 Windows。
进一步阅读可参考 shell/browser/api/electron_api_native_theme.cc(跨平台实现)、electron_api_native_theme_mac.mm(macOS 外观与辅助功能)、set_theme_source 补丁(Chromium 上游能力注入)与 dark mode 教程。
【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考