Electron 原生 Touch Bar 开发实战:基于 macOS 触控栏 API 构建窗口控件布局
Touch Bar(触控栏)是 2016 年之后部分 MacBook Pro 机型在键盘顶部提供的多点触控 OLED 功能区。Electron 在主进程内提供了一套完整的 TouchBar 类与配套组件类,让开发者可以用 JavaScript 直接为原生 macOS 应用构建 Touch Bar 布局——按钮、标签、颜色选择器、滑动条、分段控件、弹出层与 scrubber 等一应俱全。本文以 docs/api/touch-bar.md 为核心,结合仓库内的 TypeScript 绑定源码 lib/browser/api/touch-bar.ts 与 Objective-C++ 原生实现 shell/browser/ui/cocoa/electron_touch_bar.mm,完整讲解 Touch Bar 的构造方式、可用控件、实时更新机制与可运行的完整示例,读完即可在自有 Electron 应用中快速接入。
适用范围与使用前提
Electron 的 Touch Bar 能力具有几个明确的使用边界,先厘清再动手:
- 平台限定 macOS:Touch Bar 底层依赖 macOS AppKit 的
NSTouchBar框架,因此整套 API 仅对 macOS 生效;在 Windows / Linux 上调用setTouchBar不会产生任何效果。 - 进程限定:
TouchBar及相关组件类只能在 主进程(Main Process) 中使用,渲染进程无法直接创建或挂载 Touch Bar,参见术语表 主进程定义。 - API 仍处于实验阶段:Touch Bar 文档与源码中的类定义当前标记为 experimental,接口在未来 Electron 版本中可能发生变化甚至被移除,接入时应留意版本升级带来的破坏性变更。
- 不可继承系统内置类:
TouchBar及其组件类均由 Electron 内置实现,用户代码不能对其进行子类化(子类化 Electron 内置模块会导致继承失效),原因详见 FAQ:Electron 内置模块的类继承问题。
从源码角度看,TouchBar 组件在 lib/browser/api/module-list.ts 中被注册为主进程模块 { name: 'TouchBar', loader: () => require('./touch-bar') },因此可直接通过 require('electron').TouchBar 解构使用。
核心概念:TouchBar 容器与子控件
TouchBar 采用“一个容器 + 若干子控件”的树状结构。容器负责承载与排列子项,子项则各司其职完成展示与交互。容器与全部子项在 lib/browser/api/touch-bar.ts 中实现为 TypeScript 类:
- 每个子控件继承抽象基类
TouchBarItem,其内部维护一个自增的id、一个表示控件类型的type字符串,以及一个onInteraction回调入口(用于把原生侧的用户操作回传 JS)。 - 控件属性按可变性分为两类,由装饰器实现:
@ImmutableProperty:在构造时由配置项决定、之后不可重写的属性(如控件id、type、点击回调);@LiveProperty:可运行时修改的属性,赋值 setter 会先触发onMutate再emit('change', this),从而通知 Electron 刷新原生 Touch Bar 中对应控件——这就是“改属性即时生效”的底层机制。
- 容器
TouchBar继承EventEmitter,对子项的变化统一监听:子项发生change时,容器向外发出携带(item.id, item.type)的事件;绑定到窗口后,再由_addToWindow里注册的changeListener调用底层window._refreshTouchBarItem(itemID)完成原生刷新。
所有子控件在构造时会校验类型与唯一性(参考 lib/browser/api/touch-bar.ts 的构造函数),例如:同一个 TouchBar 中只能有一个 TouchBarOtherItemsProxy,同一个控件实例不能重复放入多个位置,items 中每一项都必须是 TouchBarItem 实例。
创建 TouchBar:构造选项详解
TouchBar 的构造方式见 docs/api/touch-bar.md,语法如下:
const { TouchBar } = require('electron')
const touchBar = new TouchBar(options)
其中 options 包含两个字段:
| 配置项 | 类型 | 必填 | 说明 |
|---|---|---|---|
items |
TouchBarItem[] |
可选 | 依次排列在触控栏上的子控件数组,元素类型可以是下述任一子控件类的实例;省略时为空布局 |
escapeItem |
TouchBarItem | null |
可选 | 用于替换触控栏上“esc”键的自定义子控件;设为 null 恢复系统默认的 esc 键 |
可用作 items / escapeItem 的子控件类型包括:TouchBarButton、TouchBarColorPicker、TouchBarGroup、TouchBarLabel、TouchBarPopover、TouchBarScrubber、TouchBarSegmentedControl、TouchBarSlider、TouchBarSpacer(escapeItem 还可接受 null)。
从源码看,构造函数会把 items 中每个实例登记到内部 Map,并把子项配置中内嵌的 TouchBar(如 Popover / Group 的 items)递归展开,最终在挂载窗口时把 orderedItems 一次交给原生层渲染。若 options 整体为空或 null,构造会直接抛出 Must specify options object as first argument;传入的 items 若非数组也会被安全归一为空数组。
TouchBar 的静态属性(子控件类入口)
为方便使用,所有子控件类都以静态属性形式挂在 TouchBar 上,避免开发者单独导入:
| 静态属性 | 对应的类 |
|---|---|
TouchBar.TouchBarButton |
TouchBarButton 类 |
TouchBar.TouchBarColorPicker |
TouchBarColorPicker 类 |
TouchBar.TouchBarGroup |
TouchBarGroup 类 |
TouchBar.TouchBarLabel |
TouchBarLabel 类 |
TouchBar.TouchBarPopover |
TouchBarPopover 类 |
TouchBar.TouchBarScrubber |
TouchBarScrubber 类 |
TouchBar.TouchBarSegmentedControl |
TouchBarSegmentedControl 类 |
TouchBar.TouchBarSlider |
TouchBarSlider 类 |
TouchBar.TouchBarSpacer |
TouchBarSpacer 类 |
TouchBar.TouchBarOtherItemsProxy |
TouchBarOtherItemsProxy 类 |
例如典型的解构写法 const { TouchBarLabel, TouchBarButton, TouchBarSpacer } = TouchBar,正是读取这些静态属性。对应的原生实现中每种控件都注册了一个固定的 identifier 前缀(如 Button、Label、ColorPicker、Slider 等),见 shell/browser/ui/cocoa/electron_touch_bar.mm,JS 控件 type 与 Objective-C 侧标识一一映射。
实例属性:escapeItem 的动态替换
TouchBar 实例上暴露唯一一个实例属性:
touchBar.escapeItem:类型为TouchBarItem | null。赋值后会立即用指定控件替换触控栏最左侧的 esc 键;赋值为null则恢复默认 esc 键。修改该属性会立刻在原生触控栏上生效(不再需要重建 TouchBar 或重新 setTouchBar)。
在 lib/browser/api/touch-bar.ts 中,该属性由 escapeItemSymbol 私有字段承载:setter 会校验值必须是 TouchBarItem 实例或 null,移除旧 escape 项的监听、为新项注册 change 监听,并向窗口发出 escape-item-change 事件,最终驱动原生层调用 window._setEscapeTouchBarItem(...)。
可用的子控件体系
以下子控件文档均位于 docs/api 目录下,这里汇总各类的构造选项、即时可变属性与交互回调,方便按需查阅对应文档。
TouchBarButton:触控栏按钮
对应文档 touch-bar-button.md。构造选项:
label:按钮文字;accessibilityLabel:屏幕阅读器(如 VoiceOver)使用的简短描述;设置时需符合 macOS 的无障碍规范(仅当未设置label时屏幕阅读器才会读取它);backgroundColor:按钮背景色,十六进制格式如#ABCDEF;icon:NativeImage(参见 native-image)或图片路径字符串;iconPosition:图标位置,取值left、right或overlay,默认overlay;click:点击回调函数;enabled:是否处于启用状态,默认true(源码中typeof config.enabled !== 'boolean' ? true : config.enabled)。
可运行时修改的实例属性(修改后即时刷新):accessibilityLabel、label、backgroundColor、icon、iconPosition、enabled。
TouchBarLabel:文本标签
对应文档 touch-bar-label.md。构造选项:label(显示文本)、accessibilityLabel、textColor(十六进制文本颜色)。运行时属性 label、accessibilityLabel、textColor 均可即时更新——上文吃角子老虎机示例中反复重设 reel1.label、result.textColor 即属此类操作。
TouchBarColorPicker:颜色选择器
对应文档 touch-bar-color-picker.md。构造选项:
availableColors:候选颜色的十六进制字符串数组;selectedColor:当前选中的颜色(十六进制);change(color):用户选中颜色时回调,参数color为选中色字符串。
运行时属性 availableColors、selectedColor 可即时更新。源码中 change 回调被包装进 onInteraction:原生层交互回调会先通过 setInternalProp 同步内部 selectedColor 状态,再调用用户回调,保证 JS 侧状态与原生一致。
TouchBarGroup:控件编组
对应文档 touch-bar-group.md。构造选项仅一个:items——需要一个 TouchBar 实例,用于把一个 TouchBar 整体嵌入为分组。源码中若传入的是普通对象而非 TouchBar 实例,会自动包装为 new TouchBar(items);该分组在原生层对应 NSTouchBarItemIdentifier 下的 group 实现(见 electron_touch_bar.mm 的 makeGroupForID:)。
TouchBarPopover:可展开弹出层
对应文档 touch-bar-popover.md。构造选项:label、icon(NativeImage)、items(嵌入的 TouchBar)、showCloseButton。运行时 label、icon、showCloseButton 可即时更新。其子项逻辑与 TouchBarGroup 相同(自动包装为 TouchBar,并维护父子关系以完成事件转发)。
TouchBarSlider:滑动条
对应文档 touch-bar-slider.md。构造选项:label、minValue、maxValue、value,以及 change(value) 回调(value 为用户滑动后的新值)。运行时 label、minValue、maxValue、value 均可即时更新。
TouchBarSegmentedControl:分段控件
对应文档 touch-bar-segmented-control.md。构造选项:
segmentStyle:分段样式;segments:分段数组,每段包含label/icon等描述;selectedIndex:当前选中分段下标;mode:单选/多选模式;change(selectedIndex, isSelected):选择变化回调,selectedIndex为索引,isSelected标识选中或取消。
运行时 segmentStyle、segments、selectedIndex、mode 可即时更新。
TouchBarScrubber:滑动选择器
对应文档 touch-bar-scrubber.md。构造选项:
items:ScrubberItem[],每个条目可含label/icon;selectedStyle、overlayStyle:选中态与覆盖层样式;showArrowButtons:是否显示左右箭头按钮;mode:free或fixed;continuous:是否连续触发回调,默认true(源码中未显式传入时取true);select(selectedIndex)与highlight(highlightedIndex):分别在选择与高亮(预览)时回调。
源码中当只提供 select 或只提供 highlight 时,二者会被合并包装到同一个原生交互入口内按事件类型分发。
TouchBarSpacer:间隔控件
对应文档 touch-bar-spacer.md。构造选项仅 size,取值与原生 macOS 常量对应关系如下:
size |
语义 | 对应原生常量 |
|---|---|---|
small |
项之间的小间隔(默认值) | NSTouchBarItemIdentifierFixedSpaceSmall |
large |
项之间的大间隔 | NSTouchBarItemIdentifierFixedSpaceLarge |
flexible |
占据所有可用空间,实现左右推挤对齐 | NSTouchBarItemIdentifierFlexibleSpace |
该映射关系在 electron_touch_bar.mm 的标识符转换逻辑中可以看到(针对不同 spacer 类型分别返回对应的 NSTouchBarItemIdentifier)。
TouchBarOtherItemsProxy:系统其余项代理
对应文档 touch-bar-other-items-proxy.md。放入该代理后,系统会把它后面渲染的所有项收拢进触控栏右侧的展开按钮中,避免布局过载;一个 TouchBar 中只允许放置一个(源码会校验并抛错)。
实例属性即时更新与原生联动
Touch Bar 一个显著且实用的设计是:修改实例属性会即时刷新原生触控栏,无需重建对象或重新挂载。以 touch-bar.md 中示例为例:
result.label = '💰 Jackpot!'
result.textColor = '#FDFF00'
这两行背后的完整链路为:
- 属性 setter 更新
TouchBarLabel内部的hiddenProperties,并通过@LiveProperty装饰器发出change事件(见 lib/browser/api/touch-bar.ts 的LiveProperty); - 父级
TouchBar(或嵌套的 Group / Popover)捕获事件并转发到已绑定窗口的监听器; - 窗口监听器调用底层
window._refreshTouchBarItem(itemID),仅对单个 item 做局部刷新; - 原生层最终通过 electron_touch_bar.mm 的
refreshTouchBarItem:...与itemForIdentifier:机制更新对应NSTouchBarItem。
这套“细粒度脏项刷新”机制意味着开发者可以放心地在运行期驱动动画、状态切换乃至游戏计分等高频界面更新,而无需与 AppKit 直接打交道。
完整示例:把 Touch Bar 挂到窗口上
将 TouchBar 实例添加到一个 BrowserWindow 上,使用的是窗口实例的 setTouchBar 方法(BaseWindow 与 BrowserWindow 的文档中都声明了该方法,见 base-window.md 与 browser-window.md)。JS 侧实现位于 base-window.ts:BaseWindow.prototype.setTouchBar = function (touchBar) { TouchBar._setOnWindow(touchBar, this) }。它内部会先解绑该窗口旧有的 TouchBar(若有),并在传入空值时清空当前触控栏。
源码层面,一次完整的绑定涉及以下事件线(lib/browser/api/touch-bar.ts 的 _addToWindow):
change事件驱动window._refreshTouchBarItem(itemID)局部刷新;escape-item-change事件驱动window._setEscapeTouchBarItem(...)更新 esc 位;- 原生侧的用户交互通过
-touch-bar-interaction内部事件回传,查表定位到 JS 控件实例后调用其onInteraction(details); - 窗口
closed时自动移除全部监听并解绑,防止内存泄漏(多个窗口可各自绑定不同的 TouchBar,windowListeners按窗口id管理)。
以文档自带的“吃角子老虎机”为例,保存为 touchbar.js:
const { app, BrowserWindow, TouchBar } = require('electron')
const { TouchBarLabel, TouchBarButton, TouchBarSpacer } = TouchBar
let spinning = false
// Reel labels
const reel1 = new TouchBarLabel({ label: '' })
const reel2 = new TouchBarLabel({ label: '' })
const reel3 = new TouchBarLabel({ label: '' })
// Spin result label
const result = new TouchBarLabel({ label: '' })
// Spin button
const spin = new TouchBarButton({
label: '🎰 Spin',
backgroundColor: '#7851A9',
click: () => {
// Ignore clicks if already spinning
if (spinning) {
return
}
spinning = true
result.label = ''
let timeout = 10
const spinLength = 4 * 1000 // 4 seconds
const startTime = Date.now()
const spinReels = () => {
updateReels()
if ((Date.now() - startTime) >= spinLength) {
finishSpin()
} else {
// Slow down a bit on each spin
timeout *= 1.1
setTimeout(spinReels, timeout)
}
}
spinReels()
}
})
const getRandomValue = () => {
const values = ['🍒', '💎', '7️⃣', '🍊', '🔔', '⭐', '🍇', '🍀']
return values[Math.floor(Math.random() * values.length)]
}
const updateReels = () => {
reel1.label = getRandomValue()
reel2.label = getRandomValue()
reel3.label = getRandomValue()
}
const finishSpin = () => {
const uniqueValues = new Set([reel1.label, reel2.label, reel3.label]).size
if (uniqueValues === 1) {
// All 3 values are the same
result.label = '💰 Jackpot!'
result.textColor = '#FDFF00'
} else if (uniqueValues === 2) {
// 2 values are the same
result.label = '😍 Winner!'
result.textColor = '#FDFF00'
} else {
// No values are the same
result.label = '🙁 Spin Again'
result.textColor = null
}
spinning = false
}
const touchBar = new TouchBar({
items: [
spin,
new TouchBarSpacer({ size: 'large' }),
reel1,
new TouchBarSpacer({ size: 'small' }),
reel2,
new TouchBarSpacer({ size: 'small' }),
reel3,
new TouchBarSpacer({ size: 'large' }),
result
]
})
let window
app.whenReady().then(() => {
window = new BrowserWindow({
frame: false,
titleBarStyle: 'hiddenInset',
width: 200,
height: 200,
backgroundColor: '#000'
})
window.loadURL('about:blank')
window.setTouchBar(touchBar)
})
该示例覆盖了本文几乎所有关键点:用 TouchBarSpacer 控制间距、用带 backgroundColor 与 click 的 TouchBarButton 触发逻辑、用 TouchBarLabel 的 label / textColor 属性做高频即时更新,最后通过 window.setTouchBar(touchBar) 完成挂载。滚动节奏通过每次把 setTimeout 间隔放大 1.1 倍实现“逐渐减速”的视觉拟真,点击期间用 spinning 标志做防重入。
运行示例的三种方式
文档推荐按以下步骤在本地运行上述示例(需在保存了 touchbar.js 的目录中进行):
- 将上文代码保存为
touchbar.js; - 安装 Electron:
npm install electron; - 启动运行:
./node_modules/.bin/electron touchbar.js。
运行成功后,会打开一个无边框、隐藏式标题栏(titleBarStyle: 'hiddenInset')的 200×200 黑色小窗口,同时你的 MacBook Pro 触控栏(或 Xcode 提供的 Touch Bar 模拟器)上会出现可交互的吃角子老虎机控件。若在带 Touch Bar 的实体设备上没有触发,可改用 macOS 的「触控栏模拟器」工具进行真机预览。注意示例依赖 app.whenReady() 保证 Electron 完成初始化后再创建窗口与触控栏。
进一步阅读
- 更多 Touch Bar 子控件细节可逐个查阅 touch-bar-button.md、touch-bar-label.md、touch-bar-color-picker.md、touch-bar-group.md、touch-bar-popover.md、touch-bar-slider.md、touch-bar-segmented-control.md、touch-bar-scrubber.md、touch-bar-spacer.md、touch-bar-other-items-proxy.md;
- 从源码层面理解控件属性装饰器、事件转发与窗口绑定逻辑,可阅读 lib/browser/api/touch-bar.ts 与 lib/browser/api/base-window.ts;
- 想深入了解每个控件如何翻译成原生
NSTouchBarItem,可查看 shell/browser/ui/cocoa/electron_touch_bar.mm 中对应makeButtonForID:、makeSliderForID:、makeScrubberForID:等方法; - 若需在图标或图片类控件中使用自定义图像资源,请先阅读 native-image;
- 完整 API 上下文请对照 BaseWindow 与 BrowserWindow 中声明的
setTouchBar方法。
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 StartedRust0625
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