首页
/ Electron 原生 Touch Bar 开发实战:基于 macOS 触控栏 API 构建窗口控件布局

Electron 原生 Touch Bar 开发实战:基于 macOS 触控栏 API 构建窗口控件布局

2026-09-06 18:08:41作者:俞予舒Fleming

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:在构造时由配置项决定、之后不可重写的属性(如控件 idtype、点击回调);
    • @LiveProperty:可运行时修改的属性,赋值 setter 会先触发 onMutateemit('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 的子控件类型包括:TouchBarButtonTouchBarColorPickerTouchBarGroupTouchBarLabelTouchBarPopoverTouchBarScrubberTouchBarSegmentedControlTouchBarSliderTouchBarSpacerescapeItem 还可接受 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
  • iconNativeImage(参见 native-image)或图片路径字符串;
  • iconPosition:图标位置,取值 leftrightoverlay,默认 overlay
  • click:点击回调函数;
  • enabled:是否处于启用状态,默认 true(源码中 typeof config.enabled !== 'boolean' ? true : config.enabled)。

可运行时修改的实例属性(修改后即时刷新):accessibilityLabellabelbackgroundColoriconiconPositionenabled

TouchBarLabel:文本标签

对应文档 touch-bar-label.md。构造选项:label(显示文本)、accessibilityLabeltextColor(十六进制文本颜色)。运行时属性 labelaccessibilityLabeltextColor 均可即时更新——上文吃角子老虎机示例中反复重设 reel1.labelresult.textColor 即属此类操作。

TouchBarColorPicker:颜色选择器

对应文档 touch-bar-color-picker.md。构造选项:

  • availableColors:候选颜色的十六进制字符串数组;
  • selectedColor:当前选中的颜色(十六进制);
  • change(color):用户选中颜色时回调,参数 color 为选中色字符串。

运行时属性 availableColorsselectedColor 可即时更新。源码中 change 回调被包装进 onInteraction:原生层交互回调会先通过 setInternalProp 同步内部 selectedColor 状态,再调用用户回调,保证 JS 侧状态与原生一致。

TouchBarGroup:控件编组

对应文档 touch-bar-group.md。构造选项仅一个:items——需要一个 TouchBar 实例,用于把一个 TouchBar 整体嵌入为分组。源码中若传入的是普通对象而非 TouchBar 实例,会自动包装为 new TouchBar(items);该分组在原生层对应 NSTouchBarItemIdentifier 下的 group 实现(见 electron_touch_bar.mmmakeGroupForID:)。

TouchBarPopover:可展开弹出层

对应文档 touch-bar-popover.md。构造选项:labeliconNativeImage)、items(嵌入的 TouchBar)、showCloseButton。运行时 labeliconshowCloseButton 可即时更新。其子项逻辑与 TouchBarGroup 相同(自动包装为 TouchBar,并维护父子关系以完成事件转发)。

TouchBarSlider:滑动条

对应文档 touch-bar-slider.md。构造选项:labelminValuemaxValuevalue,以及 change(value) 回调(value 为用户滑动后的新值)。运行时 labelminValuemaxValuevalue 均可即时更新。

TouchBarSegmentedControl:分段控件

对应文档 touch-bar-segmented-control.md。构造选项:

  • segmentStyle:分段样式;
  • segments:分段数组,每段包含 label / icon 等描述;
  • selectedIndex:当前选中分段下标;
  • mode:单选/多选模式;
  • change(selectedIndex, isSelected):选择变化回调,selectedIndex 为索引,isSelected 标识选中或取消。

运行时 segmentStylesegmentsselectedIndexmode 可即时更新。

TouchBarScrubber:滑动选择器

对应文档 touch-bar-scrubber.md。构造选项:

  • itemsScrubberItem[],每个条目可含 label / icon
  • selectedStyleoverlayStyle:选中态与覆盖层样式;
  • showArrowButtons:是否显示左右箭头按钮;
  • modefreefixed
  • 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'

这两行背后的完整链路为:

  1. 属性 setter 更新 TouchBarLabel 内部的 hiddenProperties,并通过 @LiveProperty 装饰器发出 change 事件(见 lib/browser/api/touch-bar.tsLiveProperty);
  2. 父级 TouchBar(或嵌套的 Group / Popover)捕获事件并转发到已绑定窗口的监听器;
  3. 窗口监听器调用底层 window._refreshTouchBarItem(itemID),仅对单个 item 做局部刷新;
  4. 原生层最终通过 electron_touch_bar.mmrefreshTouchBarItem:...itemForIdentifier: 机制更新对应 NSTouchBarItem

这套“细粒度脏项刷新”机制意味着开发者可以放心地在运行期驱动动画、状态切换乃至游戏计分等高频界面更新,而无需与 AppKit 直接打交道。

完整示例:把 Touch Bar 挂到窗口上

将 TouchBar 实例添加到一个 BrowserWindow 上,使用的是窗口实例的 setTouchBar 方法(BaseWindowBrowserWindow 的文档中都声明了该方法,见 base-window.mdbrowser-window.md)。JS 侧实现位于 base-window.tsBaseWindow.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 控制间距、用带 backgroundColorclickTouchBarButton 触发逻辑、用 TouchBarLabellabel / textColor 属性做高频即时更新,最后通过 window.setTouchBar(touchBar) 完成挂载。滚动节奏通过每次把 setTimeout 间隔放大 1.1 倍实现“逐渐减速”的视觉拟真,点击期间用 spinning 标志做防重入。

运行示例的三种方式

文档推荐按以下步骤在本地运行上述示例(需在保存了 touchbar.js 的目录中进行):

  1. 将上文代码保存为 touchbar.js
  2. 安装 Electron:npm install electron
  3. 启动运行:./node_modules/.bin/electron touchbar.js

运行成功后,会打开一个无边框、隐藏式标题栏(titleBarStyle: 'hiddenInset')的 200×200 黑色小窗口,同时你的 MacBook Pro 触控栏(或 Xcode 提供的 Touch Bar 模拟器)上会出现可交互的吃角子老虎机控件。若在带 Touch Bar 的实体设备上没有触发,可改用 macOS 的「触控栏模拟器」工具进行真机预览。注意示例依赖 app.whenReady() 保证 Electron 完成初始化后再创建窗口与触控栏。

进一步阅读

登录后查看全文
热门项目推荐
相关项目推荐