Electron systemPreferences 模块深度解析:读取系统偏好、主题色、媒体权限与 macOS 原生通知桥接
本篇以 Electron 仓库的 systemPreferences API 文档为主体,完整覆盖其全部方法、事件与属性的用途、参数、返回值和平台限制,并结合仓库中 shell/browser/api/electron_api_system_preferences.cc 等 C++ 源码与 spec/api-system-preferences-spec.ts 测试用例,解释每个 API 在底层如何映射到 Windows 系统消息、macOS 的 NSNotificationCenter 族、Linux 的 ui::NativeTheme,帮助你在主进程(以及 Utility 进程)中可靠地读取系统外观、主题色、媒体访问状态与系统通知。
一、模块定位与总体架构
systemPreferences 是 Electron 提供在 Main 进程和 Utility 进程 中的系统偏好读取模块,典型用法如下:
const { systemPreferences } = require('electron')
console.log(systemPreferences.getEffectiveAppearance())
从源码结构看,该模块的实现分为三层:
- 公共层 shell/browser/api/electron_api_system_preferences.cc:负责用 gin 的
ObjectTemplateBuilder注册 JS 方法(getColor、getAccentColor、getAnimationSettings等),并按BUILDFLAG编译条件决定哪些方法在哪个平台暴露。例如getColor和getMediaAccessStatus仅在IS_WIN || IS_MAC时注册,而整个 macOS 通知订阅族(postNotification、subscribeNotification等)仅在IS_MAC时注册。 - 平台层:
- Windows 实现在 shell/browser/api/electron_api_system_preferences_win.cc,通过 WinRT
DeviceAccessInformation查询设备权限,通过隐藏的WS_POPUP消息窗口监听WM_DWMCOLORIZATIONCOLORCHANGED广播消息来发出accent-color-changed事件; - macOS 实现在 shell/browser/api/electron_api_system_preferences_mac.mm,桥接 AppKit 的
NSColor、NSUserDefaults、LAContext(Touch ID)、AVCaptureDevice(媒体授权)与三个通知中心; - Linux 的构造函数会订阅
ui::NativeTheme(见 electron_api_system_preferences.cc#L36-L40),主题更新时把新主题色转成 RGBA 十六进制并Emit("accent-color-changed", ...)(见 electron_api_system_preferences.cc#L85-L91)。
- Windows 实现在 shell/browser/api/electron_api_system_preferences_win.cc,通过 WinRT
- 注入层:
Initialize函数把单例SystemPreferences挂到exports.systemPreferences上(见 electron_api_system_preferences.cc#L186-L193),该实例通过cppgc::Persistent全局驻留,是进程级单例。
此外还有一个值得注意的细节:GetAnimationSettings 是所有平台都注册的方法(见 electron_api_system_preferences.cc#L161-L162),因此它是少数可以跨平台调用的接口之一。
二、Events:系统外观变更的两个信号
Event: accent-color-changed Windows Linux
返回 event 与 newColor(string,用户设置的新系统主题色 RGBA 十六进制字符串)。
从源码看三端的实现路径:
- Windows:
SystemPreferences::InitializeWindow()注册了一个窗口类Electron_SystemPreferencesHostWindow并创建一个隐藏弹出窗口(electron_api_system_preferences_win.cc#L156-L186)。注释明确解释了原因:HWND_MESSAGE窗口收不到广播消息,只有顶层窗口(如WS_POPUP)才能接收WM_DWMCOLORIZATIONCOLORCHANGED。WndProc收到该消息后,把wparam中的颜色 DWORD 转成 RGBA 十六进制并Emit("accent-color-changed", ...),且仅在颜色实际变化时触发(electron_api_system_preferences_win.cc#L200-L213)。 - Linux:
OnNativeThemeUpdated通过 weak cell 把回调投递到 UI 线程,比对current_accent_color_后发出事件,避免重复派发(electron_api_system_preferences.cc#L85-L100)。 - macOS:此事件当前未实现(源码中无对应
Emit分支)。
Event: color-changed Windows
仅 Windows 实现:OnWndProc 在收到 WM_SYSCOLORCHANGE 或 WM_SETTINGCHANGE(且 wparam == SPI_SETHIGHCONTRAST,即高对比度模式切换)时发出(electron_api_system_preferences_win.cc#L215-L224)。适合用来在系统高对比度或系统色变化时重新取用 getColor() 的缓存值。
三、颜色与外观类 API
systemPreferences.getAccentColor()
返回用户当前系统级主题色的 RGBA 十六进制字符串(不带 # 前缀,源码中 ToRGBAHex(..., false) 传入了 include_hash = false):
const color = systemPreferences.getAccentColor() // "aabbccdd"
const red = color.substr(0, 2) // "aa"
const green = color.substr(2, 2) // "bb"
const blue = color.substr(4, 2) // "cc"
const alpha = color.substr(6, 2) // "dd"
文档标注此 API 需要 macOS 10.14 Mojave 或更新版本(底层读取 NSColor.controlAccentColor)。但当前仓库源码已把它扩展到 Windows(GetSystemAccentColor(),见 electron_api_system_preferences_win.cc#L87-L94)与 Linux(读取 ui_theme_->user_color()),且测试用例 spec/api-system-preferences-spec.ts#L8-L19 正是在 win32/linux 平台上验证它返回字符串。因此实际可用性为:Windows、macOS 10.14+、Linux;在取不到主题色时返回空字符串。
systemPreferences.getColor(color) Windows macOS
返回系统颜色设置,格式为带 # 的 RGBA 十六进制(#RRGGBBAA)。测试用例验证其输出匹配 /^#[0-9A-F]{8}$/i(见 spec/api-system-preferences-spec.ts#L28-L30)。
Windows 可取值(对应 Win32 COLOR_* 常量,映射表见 electron_api_system_preferences_win.cc#L98-L129):
| 名称 | 含义 |
|---|---|
3d-dark-shadow / 3d-face / 3d-highlight / 3d-light / 3d-shadow |
三维显示元素的阴影/面/高亮等 |
active-border |
活动窗口边框 |
active-caption / active-caption-gradient |
活动窗口标题栏(渐变左/右侧) |
app-workspace |
MDI 应用背景 |
button-text / caption-text |
按钮文字 / 标题栏、滚动条箭头框文字 |
desktop / disabled-text |
桌面背景 / 灰色禁用文字 |
highlight / highlight-text / hotlight |
控件选中项及其文字 / 超链接色 |
inactive-border / inactive-caption / inactive-caption-gradient / inactive-caption-text |
非活动窗口边框、标题栏(渐变左/右)与文字 |
info-background / info-text |
tooltip 背景与文字 |
menu / menu-highlight / menubar / menu-text |
菜单背景、平铺菜单高亮、菜单条背景与文字 |
scrollbar / window / window-frame / window-text |
滚动条灰区、窗口背景、边框与文字 |
macOS 可取值(映射到 NSColor 动态系统色,实现见 electron_api_system_preferences_mac.mm#L533-L607):
control-background、control、control-text、disabled-control-text、find-highlight、grid、header-text、highlight、keyboard-focus-indicator、label、link、placeholder-text、quaternary-label、scrubber-textured-background、secondary-label、selected-content-background、selected-control、selected-control-text、selected-menu-item-text、selected-text-background、selected-text、separator、shadow、tertiary-label、text-background、text、under-page-background、unemphasized-selected-content-background、unemphasized-selected-text-background、unemphasized-selected-text、window-background、window-frame-text。
其中 find-highlight、selected-content-background、separator、unemphasized-selected-content-background、unemphasized-selected-text-background、unemphasized-selected-text 仅 macOS 10.14 起可用。
两个平台对未知颜色名都会抛出 Unknown color: <name> 错误(测试见 spec/api-system-preferences-spec.ts#L21-L31 与 L177-L183)。
systemPreferences.getSystemColor(color) macOS
返回九个标准系统色之一,格式为 #RRGGBBAA:blue、brown、gray、green、orange、pink、purple、red、yellow。
这些颜色会自动适配 vibrancy 以及"增强对比度"、"降低透明度"等辅助功能设置。实现见 electron_api_system_preferences_mac.mm#L433-L460:字符串名映射到 systemBlueColor 等 NSColor 类属性,再经 NSSystemColorToSkColor 转成 SkColor 后输出十六进制;非法名称抛出 Unknown system color: <name>(测试见 spec/api-system-preferences-spec.ts#L159-L175)。
systemPreferences.getEffectiveAppearance() macOS
返回 dark、light 或 unknown 三种字符串之一,映射到 NSApplication.effectiveAppearance。源码中通过一个 NSAppearance* 的 gin Converter 完成双向转换:NSAppearanceNameAqua → "light"、NSAppearanceNameDarkAqua → "dark"、其它 → "unknown"(见 electron_api_system_preferences_mac.mm#L33-L75 与 L657-L661)。
对应的只读属性 systemPreferences.effectiveAppearance 返回同样的字符串,二者等价(测试同时覆盖属性与函数两种访问方式,见 spec/api-system-preferences-spec.ts#L228-L243)。
systemPreferences.getAnimationSettings()
跨平台方法,返回包含三个布尔字段的对象:
shouldRenderRichAnimation:是否应渲染丰富动画,综合会话类型(如远程桌面)与辅助功能设置给出建议;scrollAnimationsEnabledBySystem:按平台决定滚动动画(如 home/end 键产生的动画)是否启用;prefersReducedMotion:基于平台 API 判断用户是否希望减少动态效果。
实现非常直接,三个字段分别取自 Chromium 的 gfx::Animation::ShouldRenderRichAnimation()、ScrollAnimationsEnabledBySystem()、PrefersReducedMotion()(见 electron_api_system_preferences.cc#L65-L75)。测试确认三个属性都存在且为 boolean(见 spec/api-system-preferences-spec.ts#L305-L313)。
四、macOS 原生通知桥接
Electron 把 macOS 三套通知中心完整暴露给 JS,三者结构一致,只是"总线"不同:
| API 族 | 底层通知中心 | 典型事件 |
|---|---|---|
subscribeNotification / postNotification / unsubscribeNotification |
NSDistributedNotificationCenter |
AppleInterfaceThemeChangedNotification、AppleAquaColorVariantChanged、AppleColorPreferencesChangedNotification、AppleShowScrollBarsSettingChanged |
subscribeLocalNotification / postLocalNotification / unsubscribeLocalNotification |
NSNotificationCenter(本地) |
NSUserDefaultsDidChangeNotification |
subscribeWorkspaceNotification / postWorkspaceNotification / unsubscribeWorkspaceNotification |
NSWorkspace.sharedWorkspace.notificationCenter |
NSWorkspaceDidActivateApplicationNotification |
订阅:subscribeNotification(event, callback) macOS
event:string 或 null。传 null 时,通知中心不把名称作为投递条件(即接收所有通知);callback(event, userInfo, object):userInfo是随通知发送的用户信息字典,object是发送者,目前仅支持NSString值(源码中只有当notification.object是NSString类时才转换,否则为空字符串,见 electron_api_system_preferences_mac.mm#L261-L281);- 返回 number 订阅 ID,用于对应的
unsubscribeXxx(id)移除。
实现要点:每路订阅分配一个自增 request_id,通过 addObserverForName:object:queue:usingBlock: 注册 block observer,并把 request_id → observer token 存入全局 GetIdMap();queue 传 nil 表示在发通知所在线程同步回调。若 event 既不是 string 也不是 null,会抛出 Must pass null or a string(测试见 spec/api-system-preferences-spec.ts#L126-L157)。SystemPreferences 销毁时会通过 ClearNotificationSubscriptions() 清理全部订阅,防止悬挂回调(见 electron_api_system_preferences_mac.mm#L297-L304)。
发布:postNotification(event, userInfo[, deliverImmediately]) macOS
userInfo 为 Record<string, any>;可选的 deliverImmediately 为 true 时,即使订阅方应用处于非活动状态也会立即投递。postLocalNotification 与 postWorkspaceNotification 签名相同但没有立即投递参数。实现分别是向三个中心调用 postNotificationName:object:userInfo:deliverImmediately: 系列方法(见 electron_api_system_preferences_mac.mm#L171-L241)。
五、macOS NSUserDefaults 访问
这一组 API 让 Electron 应用与 macOS 系统的用户默认值(即 defaults 命令所管理的键值)直接交互。
registerDefaults(defaults) macOS
把一批 key: value 默认值注册进应用的 NSUserDefaults(值类型限定为 string | boolean | number)。实现会把字典中所有值取出,若发现 null/NSNull 会抛错,否则调用 [NSUserDefaults registerDefaults:](见 electron_api_system_preferences_mac.mm#L338-L358)。测试验证了 string/integer/array 的注册与回读,以及非法输入抛错(见 spec/api-system-preferences-spec.ts#L33-L64)。
getUserDefault<Type>(key, type) macOS
type 可取 string、boolean、integer、float、double、url、array、dictionary,返回类型即 UserDefaultTypes 的对应成员。常见键值示例:
AppleInterfaceStyle:stringAppleAquaColorVariant:integerAppleHighlightColor:stringAppleShowScrollBars:stringNSNavRecentPlaces:arrayNSPreferredWebServices:dictionaryNSUserDictionaryReplacementItems:array
从实现看,未知键的返回遵循 Cocoa 的"零值"约定:boolean → false、integer/float/double → 0、string/url → ''、array → []、dictionary → {};而完全非法的 type 返回 undefined——测试用例 spec/api-system-preferences-spec.ts#L75-L87 逐条验证了这些行为。
setUserDefault<Type>(key, type, value) macOS
把 key 写入 NSUserDefaults。type 必须与 value 的真实类型一致,否则抛出 Unable to convert value to: <type>;type 非法时抛出 Invalid type: <type>(实现见 electron_api_system_preferences_mac.mm#L360-L426,测试见 spec/api-system-preferences-spec.ts#L90-L124)。文档给出的示例键是 ApplePressAndHoldEnabled(boolean)。
removeUserDefault(key) macOS
移除 key,可用来恢复之前用 setUserDefault 设置的默认/全局值;键不存在时不会抛错(测试见 spec/api-system-preferences-spec.ts#L245-L256)。
六、Touch ID 与辅助功能(macOS)
canPromptTouchID()
返回 boolean,表示设备是否具备 Touch ID 能力。实现基于 LAContext:先用 LAPolicyDeviceOwnerAuthenticationWithBiometricsOrWatch 策略判断可评估性,再确认 biometryType == LABiometryTypeTouchID(见 electron_api_system_preferences_mac.mm#L462-L468)。
promptTouchID(reason)
reason:向用户说明请求 Touch ID 认证的原因;- 返回
Promise<void>,认证成功则 resolve。
const { systemPreferences } = require('electron')
systemPreferences.promptTouchID('To get consent for a Security-Gated Thing').then(() => {
console.log('You have successfully authenticated with Touch ID!')
}).catch(err => {
console.log(err)
})
注意:该 API 本身不提供数据保护,它只是生物识别同意机制。真正保护数据需要你在 Keychain 条目上设置 kSecAccessControlUserPresence 等访问控制常量,例如用 node-keytar 存储加密密钥、仅在 promptTouchID() resolve 后再取用。从源码看,reason 为空会 reject reason must be non-empty,非法 UTF-8(如孤立代理对)会 reject reason must be valid UTF-8(实现见 electron_api_system_preferences_mac.mm#L470-L524,测试见 spec/api-system-preferences-spec.ts#L264-L274)。底层实际调用了 evaluateAccessControl:operation:LAAccessControlOperationUseKeySign,即模拟了一次带用户存在性要求的私钥签名操作。
isTrustedAccessibilityClient(prompt)
promptboolean:若当前进程未获得信任,是否向用户弹出提示;- 返回当前进程是否是受信任的辅助功能客户端。
底层是单行调用 AXIsProcessTrustedWithOptions,把 prompt 放入 kAXTrustedCheckOptionPrompt 选项字典(见 electron_api_system_preferences_mac.mm#L526-L531)。典型用途:需要全局快捷键/自动化能力的应用启动时检测并引导用户到"系统设置 → 隐私 → 辅助功能"中勾选授权。
七、媒体访问权限查询
getMediaAccessStatus(mediaType) Windows macOS
mediaType:microphone、camera或screen;- 返回
not-determined、granted、denied、restricted或unknown之一。
各平台行为差异(务必按系统版本理解):
- macOS 10.13 High Sierra 及更早:无需用户同意,
microphone/camera总是返回granted;10.14 Mojave 起要求同意,10.15 Catalina 起screen也需要同意; - Windows 10:存在针对所有 win32 应用的全局麦克风/摄像头设置;
screen与旧版 Windows 的所有类型总是返回granted。
实现上,macOS 调用 system_permission_settings::CheckSystemVideoCapturePermission() 等(见 electron_api_system_preferences_mac.mm#L609-L625);Windows 通过 WinRT DeviceAccessInformation.CreateFromDeviceClass 查询 DeviceClass_VideoCapture/DeviceClass_AudioCapture 的 CurrentStatus,并把 DeviceAccessStatus 映射成上述字符串(见 electron_api_system_preferences_win.cc#L37-L74、L138-L154)。
askForMediaAccess(mediaType) macOS
mediaType:microphone或camera(无效值会 reject);- 返回
Promise<boolean>:同意 resolvetrue,拒绝 resolvefalse。
const granted = await systemPreferences.askForMediaAccess('microphone')
两个关键使用约束:
- 必须在
Info.plist中设置用途说明:NSMicrophoneUsageDescription与NSCameraUsageDescription,这些值会填充到系统权限对话框中,向用户说明请求目的。在 Electron 打包语境下,参考 Application Distribution 的 Rebranding 一节 了解如何把这些键写进已下载的 Electron 二进制的 plist。 - 授权状态的变更规则:一旦用户拒绝过,之后不会再次弹窗,Promise 会按既有状态 resolve;如果用户后来在"系统设置"中改回允许,需要重启应用才能生效。macOS 10.13 及更早无需同意,此方法总是 resolve
true。
实现是把 microphone/camera 解析为 AVMediaTypeAudio/AVMediaTypeVideo 后调用 [AVCaptureDevice requestAccessForMediaType:completionHandler:](见 electron_api_system_preferences_mac.mm#L627-L646)。
八、其他方法
systemPreferences.isSwipeTrackingFromScrollEventsEnabled() macOS
返回 boolean,即系统"Swipe between pages"(在页面间轻扫切换)设置是否开启,直接映射 [NSEvent isSwipeTrackingFromScrollEventsEnabled](见 electron_api_system_preferences_mac.mm#L653-L655)。适合做多页文档类应用的手势适配判断。
Properties
systemPreferences.accessibilityDisplayShouldReduceTransparencymacOS Deprecated:boolean,对应NSWorkspace.accessibilityDisplayShouldReduceTransparency。已弃用,请改用 nativeTheme.prefersReducedTransparency;systemPreferences.effectiveAppearancemacOS Readonly:dark/light/unknown,与getEffectiveAppearance()等价。
九、测试用例与平台可用性速查
仓库的完整测试位于 spec/api-system-preferences-spec.ts,覆盖了:accent color(win32/linux)、getColor(win32/darwin,含非法值抛错与 #RRGGBBAA 格式断言)、registerDefaults/getUserDefault/setUserDefault/removeUserDefault(darwin,含类型冲突与非法类型的异常断言)、三个订阅 API 的非法参数断言、getSystemColor/getColor(darwin 全量颜色名遍历)、effectiveAppearance、canPromptTouchID/promptTouchID(darwin)、isTrustedAccessibilityClient、getMediaAccessStatus(win32/darwin)与 getAnimationSettings(全平台)。
按平台归纳的可用性矩阵:
| API | Windows | macOS | Linux |
|---|---|---|---|
getAccentColor() |
✔ | ✔(10.14+) | ✔ |
getColor() / getMediaAccessStatus() |
✔ | ✔ | ✘ |
getSystemColor() |
✘ | ✔ | ✘ |
getEffectiveAppearance() / effectiveAppearance |
✘ | ✔ | ✘ |
post/subscribe/unsubscribe(Notification/Local/Workspace) |
✘ | ✔ | ✘ |
registerDefaults / get/set/removeUserDefault |
✘ | ✔ | ✘ |
isSwipeTrackingFromScrollEventsEnabled() |
✘ | ✔ | ✘ |
canPromptTouchID() / promptTouchID() |
✘ | ✔ | ✘ |
isTrustedAccessibilityClient() |
✘ | ✔ | ✘ |
askForMediaAccess() |
✘ | ✔ | ✘ |
事件 accent-color-changed |
✔ | ✘ | ✔ |
事件 color-changed |
✔ | ✘ | ✘ |
getAnimationSettings() |
✔ | ✔ | ✔ |
在跨平台应用中使用 systemPreferences 时的实用建议:用 typeof systemPreferences.getColor === 'function' 之类的运行时探测替代纯 process.platform 判断(因为方法注册受编译条件控制,探测更贴近真实能力);把 accent-color-changed 事件与 getAccentColor() 组合,实现主题色的"读一次 + 变更时刷新"缓存模式;在需要摄像头/麦克风的 macOS 应用启动早期就调用 getMediaAccessStatus 决定 UI 引导,并保证 Info.plist 中的用途说明完整。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00