系统通知与剪贴板

深入理解 Electron 中的系统通知(Notification)实现、剪贴板(Clipboard)操作、以及通知与 IPC 的结合使用。


系统通知(Notification)

基本用法

const { Notification } = require('electron');

// 检查是否支持通知
if (Notification.isSupported()) {
  const notification = new Notification({
    title: '文件保存成功',
    body: '您的文档已保存到 /Documents/report.pdf',
    icon: path.join(__dirname, 'icon.png'),
    silent: false  // 是否静音
  });

  notification.show();

  // 监听点击事件
  notification.on('click', () => {
    console.log('Notification clicked');
    // 聚焦主窗口
    const mainWindow = BrowserWindow.getAllWindows()[0];
    if (mainWindow) {
      mainWindow.show();
      mainWindow.focus();
    }
  });

  // 监听关闭事件
  notification.on('close', () => {
    console.log('Notification closed');
  });
}

macOS 通知设置

macOS 的通知需要请求权限:

// macOS 上需要请求通知权限
if (process.platform === 'darwin') {
  const { Notification } = require('electron');
  // macOS 会自动请求权限,或者用户首次调用 Notification 时系统会弹出授权框
}

通知按钮(macOS)

const notification = new Notification({
  title: '新消息',
  body: '来自张三的消息:今晚开会吗?',
  actions: [
    { type: 'button', text: '回复' },
    { type: 'button', text: '忽略' }
  ]
});

notification.on('action', (event, index) => {
  if (index === 0) {
    // 回复
    openReplyWindow();
  } else {
    // 忽略
    console.log('Ignored');
  }
});

通知与 IPC

主进程管理通知

// main.js
const { Notification, ipcMain, BrowserWindow } = require('electron');

// 处理来自渲染进程的通知请求
ipcMain.handle('notification:show', async (event, options) => {
  if (!Notification.isSupported()) {
    return { success: false, error: 'Notifications not supported' };
  }

  const notification = new Notification({
    title: options.title,
    body: options.body,
    icon: options.icon,
    silent: options.silent
  });

  return new Promise((resolve) => {
    notification.on('click', () => {
      // 通知被点击
      resolve({ clicked: true });
    });

    notification.on('close', () => {
      resolve({ clicked: false });
    });

    notification.show();
  });
});

// 显示带操作的通知
ipcMain.handle('notification:show-with-action', async (event, options) => {
  const notification = new Notification({
    title: options.title,
    body: options.body,
    actions: options.actions || []
  });

  return new Promise((resolve) => {
    notification.on('action', (e, index) => {
      resolve({ action: index });
    });

    notification.on('click', () => {
      resolve({ action: 'click' });
    });

    notification.show();
  });
});

渲染进程调用通知

// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
  showNotification: (options) => {
    return ipcRenderer.invoke('notification:show', options);
  }
});
// renderer.js
async function notifyUser(message) {
  const result = await window.electronAPI.showNotification({
    title: 'My App',
    body: message,
    icon: '/icon.png'
  });

  if (result.clicked) {
    console.log('User clicked notification');
  }
}

进度通知

// 带有进度条的通知(Windows/macOS)
const notification = new Notification({
  title: '下载中',
  body: '正在下载 document.pdf',
  silent: true
});

notification.progress = 0;

// 模拟进度更新
let progress = 0;
const interval = setInterval(() => {
  progress += 10;
  notification.progress = progress / 100;

  if (progress >= 100) {
    clearInterval(interval);
    notification.body = '下载完成';
    notification.progress = -1;  // 隐藏进度条
  }
}, 1000);

剪贴板(Clipboard)

基本操作

const { clipboard } = require('electron');

// 读取文本
const text = clipboard.readText();

// 写入文本
clipboard.writeText('Hello, clipboard!');

// 清除
clipboard.clear();

读取不同格式

const { clipboard } = require('electron');

// 读取纯文本
const plainText = clipboard.readText();

// 读取 HTML
const html = clipboard.readHTML();

// 读取 RTF(富文本)
const rtf = clipboard.readRTF();

// 读取图片
const image = clipboard.readImage();

// 检查剪贴板内容
const formats = clipboard.availableFormats();
console.log(formworks);
// ['text/plain', 'text/html', 'text/rtf', 'image/png']

写入不同格式

const { clipboard, nativeImage } = require('electron');

// 写入纯文本
clipboard.writeText('Plain text content');

// 写入 HTML
clipboard.writeHTML('<strong>Bold</strong> and <em>italic</em>');

// 写入 RTF
clipboard.writeRTF('{\\rtf1\\ansi Rich Text Content}');

// 写入图片
const image = nativeImage.createFromPath('/path/to/image.png');
clipboard.writeImage(image);

复制图片

const { clipboard, nativeImage } = require('electron');

// 从文件复制图片
const image = nativeImage.createFromPath('/path/to/screenshot.png');
clipboard.writeImage(image);

// 从 URL 复制图片
async function copyImageFromURL(url) {
  const response = await fetch(url);
  const buffer = await response.arrayBuffer();
  const image = nativeImage.createFromBuffer(Buffer.from(buffer));
  clipboard.writeImage(image);
}

// 获取图片的 Base64
function getImageAsDataURL(image) {
  return image.toDataURL();
}

// 从剪贴板获取图片并保存
function saveImageFromClipboard() {
  const image = clipboard.readImage();
  if (!image.isEmpty()) {
    const buffer = image.toPNG();
    require('fs').writeFileSync('clipboard-image.png', buffer);
  }
}

剪贴板与 IPC

主进程处理剪贴板

// main.js
ipcMain.handle('clipboard:read', (event, format) => {
  switch (format) {
    case 'text':
      return clipboard.readText();
    case 'html':
      return clipboard.readHTML();
    case 'image':
      return clipboard.readImage().toPNG().toString('base64');
    default:
      return null;
  }
});

ipcMain.handle('clipboard:write', (event, format, content) => {
  switch (format) {
    case 'text':
      clipboard.writeText(content);
      break;
    case 'html':
      clipboard.writeHTML(content);
      break;
    case 'image':
      const image = nativeImage.createFromDataURL(content);
      clipboard.writeImage(image);
      break;
  }
  return true;
});

渲染进程使用剪贴板

// renderer.js
async function copyToClipboard(text) {
  await window.electronAPI.clipboardWrite('text', text);
}

async function pasteFromClipboard() {
  return await window.electronAPI.clipboardRead('text');
}

高级用法

监听剪贴板变化

// 使用定时器检查剪贴板变化(轮询方式)
let lastClipboardContent = clipboard.readText();

setInterval(() => {
  const currentContent = clipboard.readText();
  if (currentContent !== lastClipboardContent) {
    console.log('Clipboard changed!');
    lastClipboardContent = currentContent;

    // 触发事件
    mainWindow.webContents.send('clipboard:changed', currentContent);
  }
}, 1000);

复制格式化文本

function copyFormattedText(htmlContent, plainContent) {
  const { clipboard } = require('electron');

  // 同时写入 HTML 和纯文本
  // 粘贴到富文本编辑器会使用 HTML,粘贴到纯文本编辑器会使用 plainContent
  clipboard.write({
    text: plainContent,
    html: htmlContent
  });
}

// 使用示例
copyFormattedText(
  '<p><strong>粗体文字</strong> 和 <em>斜体文字</em></p>',
  '粗体文字 和 斜体文字'
);

复制代码片段

function copyCodeSnippet(code, language = 'javascript') {
  const { clipboard } = require('electron');

  // 写入带语言的纯文本
  // VS Code 等编辑器会根据语言识别
  const formattedCode = `\`\`\`${language}\n${code}\n\`\`\``;
  clipboard.writeText(formattedCode);
}

// 使用示例
copyCodeSnippet(`
function hello() {
  console.log('Hello, World!');
}
`, 'javascript');

实际应用场景

1. 截图并复制

const { desktopCapturer, clipboard, nativeImage } = require('electron');

async function captureAndCopy() {
  const sources = await desktopCapturer.getSources({
    types: ['screen'],
    thumbnailSize: { width: 1920, height: 1080 }
  });

  if (sources.length > 0) {
    const screenshot = sources[0].thumbnail;
    clipboard.writeImage(screenshot);
    return true;
  }

  return false;
}

2. 复制文件路径

const { clipboard, shell } = require('electron');

function copyFilePath(filePath) {
  // Windows 需要使用反斜杠
  const formattedPath = process.platform === 'win32'
    ? filePath.replace(/\//g, '\\')
    : filePath;

  clipboard.writeText(formattedPath);
}

// 复制文件链接(macOS)
function copyFileLink(filePath) {
  shell.writeShortcutLink;  // 需要创建快捷方式
}

3. 实现粘贴板历史

class ClipboardHistory {
  constructor(maxItems = 50) {
    this.history = [];
    this.maxItems = maxItems;
    this.pollInterval = null;
  }

  start() {
    let lastContent = clipboard.readText();

    this.pollInterval = setInterval(() => {
      const currentContent = clipboard.readText();
      if (currentContent && currentContent !== lastContent) {
        this.add(currentContent);
        lastContent = currentContent;
      }
    }, 500);
  }

  stop() {
    if (this.pollInterval) {
      clearInterval(this.pollInterval);
    }
  }

  add(content) {
    // 去重
    this.history = this.history.filter(item => item !== content);

    // 添加到开头
    this.history.unshift(content);

    // 限制数量
    if (this.history.length > this.maxItems) {
      this.history.pop();
    }
  }

  get(index) {
    return this.history[index];
  }

  clear() {
    this.history = [];
  }
}

最佳实践

1. 通知使用注意

  • 不要频繁发送通知,会打扰用户
  • 通知内容应该简洁明了
  • 点击通知后应该聚焦到相关窗口

2. 剪贴板使用注意

  • 剪贴板可能包含敏感信息,不要随意读取
  • 大图片会占用大量内存,谨慎处理
  • 使用完毕及时清理敏感数据

3. 结合使用示例

// 当用户复制内容时,发送通知确认
clipboard.on('change', (content) => {
  if (content.length > 1000) {
    new Notification({
      title: '已复制',
      body: `已复制 ${content.length} 个字符`
    }).show();
  }
});

这一章想说的

系统通知和剪贴板是桌面应用的重要交互方式:

  1. 通知:使用 Notification API,支持点击回调和操作按钮
  2. 剪贴板:支持文本、HTML、图片等多种格式
  3. IPC 结合:渲染进程通过 IPC 调用主进程的原生能力
  4. 实际场景:截图复制、代码片段、粘贴板历史

合理使用这些功能可以显著提升用户体验。


延展阅读