Electron TouchBarColorPicker 详解:在 macOS Touch Bar 上构建原生色选择器的完整指南
Touch Bar 是 Electron 为原生 macOS 应用提供的一套控件 API,其中 TouchBarColorPicker 负责在 Touch Bar(或 Touch Bar 模拟器)上呈现一个系统级颜色选择器,并能在用户选定颜色时回调主进程。本文以 TouchBarColorPicker 官方文档 为主体,结合 Electron 主进程 JS 层实现 touch-bar.ts 与 macOS 原生层实现 electron_touch_bar.mm,完整讲清构造参数、实例属性的更新机制、事件回调链路,以及如何把它挂到窗口上并在无 Touch Bar 硬件时通过模拟器调试。
需要特别说明两点适用前提:
TouchBarColorPicker不从'electron'模块直接导出,只能通过TouchBar.TouchBarColorPicker静态属性获取,或者作为其他 API 的返回值使用;- 整个 Touch Bar API 目前仍标记为实验性(experimental),未来版本可能变更或移除。
一、类的定位:TouchBar 家族中的颜色选择器
Touch Bar API 的入口是 TouchBar 类,它通过一组静态属性暴露全部子控件类型,包括 TouchBarButton、TouchBarColorPicker、TouchBarGroup、TouchBarLabel、TouchBarPopover、TouchBarScrubber、TouchBarSegmentedControl、TouchBarSlider、TouchBarSpacer、TouchBarOtherItemsProxy 等。这一点可以在源码中得到确认:
// lib/browser/api/touch-bar.ts
static TouchBarButton = TouchBarButton;
static TouchBarColorPicker = TouchBarColorPicker;
static TouchBarGroup = TouchBarGroup;
// ...
见 TouchBar 静态属性定义。这也是为什么文档明确写着该类 "not exported from the 'electron' module"——它是嵌套在 TouchBar 上的二级导出。典型用法是先解构:
const { TouchBar } = require('electron')
const { TouchBarColorPicker } = TouchBar
TouchBar 主文档 给出了完整的调用范式:创建一个 TouchBar 实例,把若干子控件放入 items 数组,最后通过 BrowserWindow.setTouchBar 把整个 touch bar 绑定到窗口。TouchBarColorPicker 就是 items 数组中允许出现的一种元素类型。
二、构造函数与完整参数
TouchBarColorPicker 的构造函数只有一个参数 options 对象:
new TouchBarColorPicker(options)
参数定义(与 官方文档 一致,此处补充源码层面行为说明):
| 参数 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
availableColors |
string[] |
可选 | 一组十六进制颜色字符串(如 '#ABCDEF'),将作为选择器中可挑选的候选颜色 |
selectedColor |
string |
可选 | 选择器当前选中的十六进制颜色,格式如 '#ABCDEF' |
change |
Function |
可选 | 用户选定颜色时的回调,收到 color(string)参数,即用户选择的十六进制颜色 |
一个最小但完整的实例化示例:
const { TouchBar } = require('electron')
const { TouchBarColorPicker } = TouchBar
const picker = new TouchBarColorPicker({
availableColors: ['#FF0000', '#00FF00', '#0000FF', '#888888'],
selectedColor: '#FF0000',
change: (color) => {
console.log('用户在 Touch Bar 上选择了颜色:', color)
}
})
源码视角:参数在 JS 层如何落地
所有 Touch Bar 控件都继承自 TouchBarItem 抽象基类(定义见 touch-bar.ts),构造函数把传入的 options 存到私有字段 _config 中:
// lib/browser/api/touch-bar.ts
constructor(config: ConfigType) {
super()
this._config = this._config || config || ({} as ConfigType)
(this as any)[hiddenProperties] = {}
const hook = (this as any)._hook
if (hook) hook.call(this)
delete (this as any)._hook
}
TouchBarColorPicker 自身只声明了三类属性:
// lib/browser/api/touch-bar.ts
class TouchBarColorPicker
extends TouchBarItem<Electron.TouchBarColorPickerConstructorOptions>
implements Electron.TouchBarColorPicker
{
@ImmutableProperty(() => 'colorpicker')
type!: string;
@LiveProperty<TouchBarColorPicker>((config) => config.availableColors)
availableColors!: string[];
@LiveProperty<TouchBarColorPicker>((config) => config.selectedColor)
selectedColor!: string;
@ImmutableProperty<TouchBarColorPicker>(({ change: onChange }, setInternalProp) =>
typeof onChange === 'function'
? (details: { color: string }) => {
setInternalProp('selectedColor', details.color);
onChange(details.color);
}
: null
)
onInteraction!: Function | null;
}
见 TouchBarColorPicker 类实现。这里有三个值得注意的实现细节:
type被固定为'colorpicker',且标记为@ImmutableProperty——任何后续对type的赋值都会抛出Cannot override property type异常。这个类型字符串是 JS 层与原生层(C++/Objective-C)之间的协议标识,下文会看到原生代码正是靠它分派更新逻辑。availableColors与selectedColor都是@LiveProperty(活属性):构造时从_config中取值初始化,之后每次对外部赋值都会触发onMutate副作用并派发change事件。这正是文档所说 "Changing this value immediately updates the color picker in the touch bar" 的机制来源(见第四节)。change回调被包装成了onInteraction:注意包装逻辑里先执行setInternalProp('selectedColor', details.color),再调用用户的onChange。也就是说,原生端报回一个新颜色时,Electron 会自动把实例的selectedColor同步为新值,无需你在回调里手动再赋值。这一点在文档中并未明说,但源码可以确认。
回调签名与交互链路
change: (color: string) => void 的 color 参数来自原生端上报的 details.color。在 macOS 原生实现中,色选择器控件的动作处理函数把系统控件上的 NSColor 转换为 RGB 十六进制字符串后发回窗口:
// shell/browser/ui/cocoa/electron_touch_bar.mm
- (void)colorPickerAction:(id)sender {
NSString* item_id = [self idFromIdentifier:identifier
withPrefix:ColorPickerIdentifier];
NSColor* color = ((NSColorPickerTouchBarItem*)sender).color;
std::string hex_color =
electron::ToRGBHex(skia::NSDeviceColorToSkColor(color));
base::DictValue details;
details.Set("color", hex_color);
window_->NotifyTouchBarItemInteraction(base::SysNSStringToUTF8(item_id),
std::move(details));
}
见 colorPickerAction 实现。因此 change 回调收到的 color 始终是形如 '#RRGGBB' 的十六进制字符串,即使你在 availableColors 中使用了带透明度或其他格式的颜色,回调返回值也会被归一化为 RGB hex。
JS 侧的完整分派路径是:窗口收到 -touch-bar-interaction 事件后,TouchBar 在 _addToWindow 中注册的监听器按 itemID 找到对应实例并调用其 onInteraction:
// lib/browser/api/touch-bar.ts
const interactionListener = (_: any, itemID: string, details: any) => {
let item = this.items.get(itemID);
// ...
if (item != null && item.onInteraction != null) {
item.onInteraction(details);
}
};
window.on('-touch-bar-interaction', interactionListener);
见 交互事件监听。每个 TouchBarItem 构造时会分配自增 id(nextItemID++),change 回调依赖的就是这套 ID 路由机制。
三、实例属性:availableColors 与 selectedColor
TouchBarColorPicker 实例上暴露两个可读写属性:
touchBarColorPicker.availableColors
string[],表示选择器可供选择的颜色列表。文档明确:修改该值会立即更新 Touch Bar 上的色选择器。
从 JS 层看它是 LiveProperty,赋值即触发 change 事件;从原生层看,刷新逻辑会读取 settings 中的 availableColors 数组,逐个解析为 NSColor 并构建 NSColorList 挂到控件上:
// shell/browser/ui/cocoa/electron_touch_bar.mm
- (void)updateColorPicker:(NSColorPickerTouchBarItem*)item
withSettings:(const gin_helper::PersistentDictionary&)settings {
std::vector<std::string> colors;
if (settings.Get("availableColors", &colors) && !colors.empty()) {
NSColorList* color_list = [[NSColorList alloc] initWithName:@""];
for (size_t i = 0; i < colors.size(); ++i) {
[color_list insertColor:[self colorFromHexColorString:colors[i]]
key:base::SysUTF8ToNSString(colors[i])
atIndex:i];
}
item.colorList = color_list;
}
std::string selectedColor;
if (settings.Get("selectedColor", &selectedColor)) {
item.color = [self colorFromHexColorString:selectedColor];
}
}
见 updateColorPicker 实现。由此可确认两个实操要点:
- 只有当
availableColors非空时才会设置NSColorList;如果留空或不传,控件展示系统默认色板; - 每次刷新都会重建整个色板,因此把
availableColors重新赋一个新数组就是标准的"动态换色板"方式。
touchBarColorPicker.selectedColor
string,十六进制颜色码,表示当前选中的颜色。同样支持随时赋值并立即生效。注意上一节提到的自动同步:当用户在 Touch Bar 上选色触发 change 时,内部已经帮你把 selectedColor 更新为新值,这保证了程序状态与 UI 呈现始终一致,避免手动同步出错。
四、更新机制:为什么赋值"立即生效"
理解 LiveProperty 的设计就能解释文档中反复出现的 "Changing this value immediately updates the color picker in the touch bar":
// lib/browser/api/touch-bar.ts
const LiveProperty = (...) =>
(target: T, propertyKey: keyof T) => {
// ...
Object.defineProperty(target, propertyKey, {
get: function () {
return this[hiddenProperties][propertyKey];
},
set: function (value) {
if (onMutate) onMutate(this as any, value);
this[hiddenProperties][propertyKey] = value;
this.emit('change', this);
},
enumerable: true
});
};
即属性赋值 → 触发实例 change 事件。而 TouchBar 在构造时给每个 item 注册了监听:
const registerItem = (item: TouchBarItem<any>) => {
this.items.set(item.id, item);
item.on('change', this.changeListener);
// ...
};
// ...
private changeListener = (item: TouchBarItem<any>) => {
this.emit('change', item.id, item.type);
};
见 change 事件链。当 touch bar 已绑定窗口时,_addToWindow 里注册的 changeListener 会进一步调用 window._refreshTouchBarItem(itemID),最终到达原生端的 refreshTouchBarItem:withType:withSettings: 分派——对 colorpicker 类型即调用上面的 updateColorPicker:。整条链路为:
picker.selectedColor = '#00FF00'
属性赋值(LiveProperty setter)
→ item.emit('change')
→ TouchBar 转发 'change'(携带 item.id 与 item.type)
→ 窗口 _refreshTouchBarItem(id)
→ 原生 refreshTouchBarItem → updateColorPicker(读取 availableColors / selectedColor 更新 NSColorPickerTouchBarItem)
这套机制对所有 Touch Bar 控件通用,也意味着在 TouchBarGroup、TouchBarPopover 这类容器里嵌套的 color picker 同样会被刷新——原生端专门处理了父容器场景,会沿着 _parents(JS 层由 _addParent/_removeParent 维护)遍历 popover / group 的子 touch bar 递归刷新,见 refreshTouchBarItem 的父容器处理。
五、实战:把色选择器挂到窗口上
TouchBarColorPicker 本身不能独立存在,必须作为 items 的一员放入 TouchBar,再用 window.setTouchBar 绑定到 BrowserWindow。下面给出一个可直接运行的完整示例(主进程):
const { app, BrowserWindow, TouchBar } = require('electron')
const { TouchBarColorPicker, TouchBarLabel } = TouchBar
// 主题色板:模拟一个"窗口背景色"选择器
const palette = ['#000000', '#FFFFFF', '#F00', '#0F0', '#00F',
'#FF0', '#0FF', '#F0F']
let current = '#0000FF'
const status = new TouchBarLabel({
label: `当前颜色: ${current}`,
textColor: current
})
const picker = new TouchBarColorPicker({
availableColors: palette,
selectedColor: current,
change: (color) => {
// 注意:picker.selectedColor 已被框架自动同步,这里只需更新 UI
current = color
status.label = `当前颜色: ${color}`
status.textColor = color
}
})
const touchBar = new TouchBar({
items: [picker, status]
})
app.whenReady().then(() => {
const win = new BrowserWindow({ width: 480, height: 320 })
win.loadFile('index.html')
win.setTouchBar(touchBar)
})
示例要点:
new TouchBar({ items: [...] })的items中不允许重复添加同一个 item 实例,也不允许一个 touch bar 里出现多个OtherItemsProxy;TouchBar构造函数会做校验并抛错(见下文测试证据);setTouchBar(null)可以移除 touch bar;对一个窗口重复调用setTouchBar会用新的 touch bar 替换旧的,源码中TouchBar._setOnWindow会先把旧 touch bar_removeFromWindow,见 替换逻辑;change回调中无需手动回写picker.selectedColor,框架已替你完成(见第二节onInteraction包装逻辑)。
运行与调试:没有 Touch Bar 硬件怎么办
TouchBar 主文档 给出的运行步骤是:
- 将示例保存为
touchbar.js; npm install electron;- 执行
./node_modules/.bin/electron touchbar.js。
对于没有配备 Touch Bar 的 MacBook,可以使用系统自带的 Touch Bar 模拟器:在"系统设置 → 键盘"中开启"在菜单栏中显示 Touch Bar 控制",或运行"控制条"(Control Strip)App,屏幕上就会显示 Touch Bar 的镜像,可以直接点击色选择器验证 change 回调。也可以在 Electron 开发者工具中以 F18(Control Strip)等快捷方式呼出。
测试用例中的标准用法
Electron 自身的集成测试展示了 TouchBarColorPicker 在完整 items 列表中的位置,可作为用法参照:
// spec/api-touch-bar-spec.ts
new TouchBarColorPicker({ selectedColor: '#F00', change: () => {} }),
见 api-touch-bar-spec.ts。同一测试文件还验证了 TouchBar 的构造约束,例如 new TouchBar() 不传参数会抛出 Must specify options object as first argument、items 中混入非法值会抛出 Each item must be an instance of TouchBarItem——编写自定义 items 时应保证每个元素都是 TouchBar 静态属性构造出的实例。
六、关键实现细节速查
| 主题 | 结论 | 依据 |
|---|---|---|
| 导出方式 | 不经 'electron' 顶层导出,用 TouchBar.TouchBarColorPicker |
touch-bar.ts 静态属性 |
| 控件类型标识 | type 固定为 'colorpicker',只读 |
类定义 |
change 参数格式 |
十六进制 RGB 字符串('#RRGGBB'),由原生端 ToRGBHex 归一化 |
colorPickerAction |
| 选中色自动同步 | 原生上报后框架先内部更新 selectedColor 再调用用户回调 |
onInteraction 包装 |
| 色板刷新 | availableColors 非空时重建 NSColorList 并整体替换 |
updateColorPicker |
| 更新触发点 | 任一 LiveProperty 赋值 → item change → 窗口刷新原生控件 |
LiveProperty 与 changeListener |
| 平台 | 仅 macOS;整个 Touch Bar API 属实验性 API | TouchBar 文档说明 |
七、小结
TouchBarColorPicker 的 API 面很小——一个 options 对象加两个实例属性——但把它放进 Electron 的整体 Touch Bar 架构里看,它完整演示了"JS 声明式状态 → 事件驱动刷新 → 原生控件更新 → 交互事件按 ID 路由回 JS"的双向数据流:
- 写侧:
availableColors/selectedColor赋值经LiveProperty→change事件 →window._refreshTouchBarItem→ 原生updateColorPicker:; - 读侧:
NSColorPickerTouchBarItem的colorPickerAction:把用户选色转成 hex 字符串上报,按 item id 路由到onInteraction,自动回写selectedColor后调用你的change回调。
掌握这条链路后,无论是动态更换色板、与 TouchBarLabel 组合展示当前值,还是在 TouchBarGroup / TouchBarPopover 容器中嵌套色选择器,都可以基于同一套机制平滑扩展。需要注意的是 Touch Bar API 仍为实验特性,且仅适用于 macOS 平台,跨平台产品应做好特性检测与降级方案。
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