Electron Windows Toast通知: 添加按钮并响应交互
2025-04-01 17:57:43
在 Electron 应用中实现 Windows Toast 通知交互按钮
咱们在用 Electron 开发 Windows 应用时,常常需要用到系统原生的 Toast 通知。单纯显示个消息挺简单,用 Electron 自带的 Notification
API 就能搞定。要是想用 toastXml
来自定义通知样式,比如不让通知在操作中心堆积,也能做到。
就像下面这样:
const { Notification, app } = require('electron');
const path = require('path');
const toastXmlString = `
<toast>
<audio silent="true" />
<visual>
<binding template="ToastImageAndText01">
<image id="1" src="${path.join(__dirname, 'icon.png')}" alt="img"/>
<text id="1">${app.getName()} \n你好,世界!</text>
<text placement="attribution">这条小字在底部</text>
</binding>
</visual>
</toast>
`;
function showBasicToast() {
let ENotification = new Notification({ toastXml: toastXmlString });
ENotification.show();
// 演示用,2.5秒后自动关闭,实际应用中看需求
setTimeout(() => { ENotification.close(); }, 2500);
}
// 在 app ready 后调用
// app.whenReady().then(showBasicToast);
这段代码能显示一个带图标、标题、内容和署名的基础 Toast 通知,而且用 toastXml
的好处是,默认情况下新通知会替换掉同一个应用发出的旧通知(如果 ID 没特殊处理),看起来清爽些。
但是,问题来了:如果我想在通知上加几个按钮,比如“同意”、“拒绝”或者“稍后处理”,用户点了之后应用能知道点的是哪个,并且执行相应的操作,这该怎么弄呢?单纯的 toastXml
似乎只管显示,怎么把用户的点击行为传回我们的 Electron 主进程呢?
为什么会这样?
主要是因为 Electron 的 Notification
API 设计目标是跨平台,它提供的是各平台通知功能的交集 。像 Windows Toast 这种带有复杂交互(按钮、输入框等)的功能,属于 Windows 平台的特性 。
虽然 Electron 允许通过 toastXml
参数直接使用 Windows 的原生 Toast XML 结构,但如何把 XML 里定义的 actions
(操作按钮)跟 Electron 的事件机制联系起来,官方文档示例确实不多,需要咱们自己摸索一下 Windows 的 Toast 规范和 Electron 的事件处理。
简单说,你需要两步:
- 在
toastXml
里正确定义这些按钮。 - 在 Electron 代码里监听特定事件来接收按钮点击的信号。
搞定它:添加并响应 Toast 操作
好消息是,Electron 确实提供了处理这种交互的能力,主要是通过 Notification
实例触发的 action
事件。
第一步:改造你的 toastXml
要在 Toast 通知上添加按钮,你需要在 <toast>
根节点下,紧随 <visual>
之后(或者有时在 <audio>
之后,位置需符合 Toast Schema),添加一个 <actions>
节点。
<actions>
节点里面可以包含一个或多个 <action>
子节点,每个 <action>
代表一个按钮。关键属性有:
content
: 按钮上显示的文字。arguments
: 非常重要 ,这是你自定义的一个字符串,当用户点击这个按钮时,这个字符串会作为信息传递给你的应用程序。你可以用它来区分点的是哪个按钮,或者传递一些上下文数据。activationType
: 定义点击按钮后如何激活你的应用。常见的值有:foreground
: 激活应用并把窗口带到前台。background
: 在后台激活应用(应用进程启动或唤醒),但不打扰用户当前窗口。适用于后台任务。protocol
: 通过注册的协议(例如myapp://
)启动应用。需要额外配置,用起来要小心安全问题。
我们来修改之前的 toastXmlString
,加入两个按钮,“确认”和“取消”:
const { Notification, app } = require('electron');
const path = require('path');
// 假设你的应用名叫 'MyApp'
const appName = app.getName() || '我的应用';
const iconPath = path.join(__dirname, 'icon.png'); // 确保路径正确且图片存在
// 注意:XML 中的 src 路径需要是绝对路径或者 Windows 能理解的格式
// file:/// 协议通常是可靠的
const iconSrc = `file:///${iconPath.replace(/\\/g, '/')}`;
const toastXmlWithActions = `
<toast launch="app-defined-string">
<visual>
<binding template="ToastGeneric">
<image placement="appLogoOverride" hint-crop="circle" src="${iconSrc}" alt="logo"/>
<text>${appName}</text>
<text>这是一个带有操作按钮的通知示例。请选择:</text>
</binding>
</visual>
<actions>
<action
content="确认"
arguments="confirmAction"
activationType="foreground"/>
<action
content="取消"
arguments="cancelAction"
activationType="background"/>
</actions>
<audio silent="true" />
</toast>
`;
// 注意:
// 1. `launch` 属性在 `<toast>` 根节点上,定义了点击通知主体(非按钮部分)时的行为参数。
// 2. 模板改用了更通用的 `ToastGeneric`,可以更好地容纳按钮。
// 3. `image` 的 `placement="appLogoOverride"` 让它显示在标题旁边。
// 4. `arguments` 分别设为 "confirmAction" 和 "cancelAction" 用于区分。
// 5. “确认”按钮使用 foreground 激活,“取消”使用 background。
第二步:监听 action
事件
定义好了带按钮的 XML,接下来就是在 Electron 里显示这个通知,并且监听按钮点击 。
Notification
对象在用户点击了你在 <actions>
里定义的某个按钮时,会触发一个名为 action
的事件。
这个事件的回调函数会收到两个参数:
event
: 标准的事件对象。index
: 用户点击的按钮在<actions>
节点下的索引 (从 0 开始)。
所以,如果用户点了第一个按钮(“确认”),index
就是 0
;点了第二个按钮(“取消”),index
就是 1
。
我们可以根据这个 index
来判断用户意图,并执行对应逻辑。如果你需要获取当时在 XML 里定义的 arguments
字符串,理论上你需要自己解析 toastXmlWithActions
,找到对应 index
的 <action>
节点的 arguments
属性值。不过,在多数情况下,仅仅知道点击的是第几个按钮(即 index
)就足够执行相应的分支逻辑了。
看代码:
// ...接上面的代码...
function showInteractiveToast() {
// 确保 app ID 正确设置,这对于 Toast 通知很重要
if (process.platform === 'win32') {
app.setAppUserModelId(app.name); // 或者你的自定义 AppUserModelId
}
// 解析 XML 中的 actions 以便之后根据 index 获取 arguments
// 注意:这是一个简化的解析示例,实际应用可能需要更健壮的 XML 解析库
const actionsInXml = [];
const actionMatches = toastXmlWithActions.matchAll(/<action\s+.*?arguments="([^"]*?)"/gs);
for (const match of actionMatches) {
actionsInXml.push({ arguments: match[1] });
}
let ENotification = new Notification({ toastXml: toastXmlWithActions });
ENotification.on('show', () => {
console.log('通知已显示');
});
ENotification.on('click', (event) => {
console.log('通知主体被点击');
// 这里可以处理点击通知本体(非按钮区域)的逻辑
// event 里可能没有额外信息表明是哪个部分被点击
// 通常是激活应用窗口
});
// 这就是关键:监听按钮点击
ENotification.on('action', (event, index) => {
console.log(`按钮被点击了!索引:${index}`);
const clickedActionInfo = actionsInXml[index]; // 获取对应按钮的信息
if (clickedActionInfo) {
const actionArgument = clickedActionInfo.arguments;
console.log(`对应的 argument 是: ${actionArgument}`);
// 根据 argument 执行不同操作
if (actionArgument === 'confirmAction') {
console.log('执行确认操作...');
// 比如:打开某个窗口、发送网络请求等
// mainWindow?.show(); // 如果希望确认后显示主窗口
} else if (actionArgument === 'cancelAction') {
console.log('执行取消操作...');
// 比如:记录日志、默默完成某个清理任务
} else {
console.log('未知的 action argument:', actionArgument);
}
} else {
console.error('无法根据索引找到对应的 action 信息: ', index);
}
// 点击按钮后,通知通常会自动消失,不需要手动 close
// ENotification.close();
});
ENotification.on('close', (event) => {
console.log('通知已关闭');
});
ENotification.on('failed', (event, error) => {
console.error('显示通知失败:', error);
});
ENotification.show();
}
// 同样,在 app ready 后调用
// app.whenReady().then(showInteractiveToast);
一个更完整的例子
把上面的代码整合一下,放在主进程 (main.js
或类似文件) 里:
const { app, BrowserWindow, Notification } = require('electron');
const path = require('path');
let mainWindow;
function createWindow () {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js') // 示例性的
}
});
mainWindow.loadFile('index.html'); // 你的 UI 页面
mainWindow.on('closed', () => mainWindow = null);
}
// 设置 AppUserModelId (仅 Windows)
if (process.platform === 'win32') {
// 非常重要:确保这里的 ID 和你的快捷方式或安装程序设置的一致
// 通常用 'com.squirrel.YourAppName.YourAppName' 或类似格式
app.setAppUserModelId(`com.electron.${app.getName().toLowerCase()}`);
}
app.whenReady().then(() => {
createWindow();
// 应用启动后,延迟几秒显示一个带按钮的通知作为演示
setTimeout(showInteractiveToast, 5000);
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// ---- 通知相关代码 ----
function showInteractiveToast() {
// 防止在非 Windows 平台执行 ToastXML 特定逻辑
if (process.platform !== 'win32') {
console.log('ToastXML with actions is only supported on Windows.');
// 可以考虑显示一个普通的 Electron Notification 作为回退
new Notification({ title: '通知', body: '这是一个操作通知' }).show();
return;
}
const appName = app.getName() || '我的应用';
const iconPath = path.join(__dirname, 'assets', 'icon.png'); // 推荐把资源放进 assets 目录
const iconSrc = `file:///${iconPath.replace(/\\/g, '/')}`;
const toastXmlWithActions = `
<toast launch="app-defined-string">
<visual>
<binding template="ToastGeneric">
<image placement="appLogoOverride" hint-crop="circle" src="${iconSrc}" alt="logo"/>
<text>${appName}</text>
<text>这是一个带有操作按钮的通知示例。请选择:</text>
</binding>
</visual>
<actions>
<action
content="确认"
arguments="confirmAction"
activationType="foreground"/>
<action
content="取消"
arguments="cancelAction"
activationType="background"/>
</actions>
<audio silent="true" />
</toast>
`;
// 解析 XML 中的 actions (基础版解析)
const actionsInXml = [];
const actionMatches = toastXmlWithActions.matchAll(/<action\s+.*?arguments="([^"]*?)"/gs);
for (const match of actionMatches) {
actionsInXml.push({ arguments: match[1] });
}
let ENotification = new Notification({ toastXml: toastXmlWithActions });
ENotification.on('action', (event, index) => {
console.log(`按钮被点击了!索引:${index}`);
const clickedActionInfo = actionsInXml[index];
if (clickedActionInfo) {
const actionArgument = clickedActionInfo.arguments;
console.log(`对应的 argument 是: ${actionArgument}`);
// 在这里根据 actionArgument 执行你的应用逻辑
handleToastAction(actionArgument);
} else {
console.error('无法根据索引找到对应的 action 信息: ', index);
}
});
ENotification.on('click', () => {
console.log('通知主体被点击,通常是激活窗口。');
// 例如,确保主窗口可见并获得焦点
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
ENotification.on('failed', (event, error) => {
console.error('显示通知失败:', error);
// 可能的原因:XML 格式错误、AppUserModelId 未设置或错误、资源路径问题等
});
ENotification.show();
}
function handleToastAction(argument) {
// 这是处理按钮点击逻辑的函数
console.log(`处理 Action: ${argument}`);
if (argument === 'confirmAction') {
// 可能需要通知渲染进程更新 UI,或者直接在主进程执行操作
if (mainWindow) {
mainWindow.webContents.send('toast-action', { action: 'confirm' });
// 同时也可以让主窗口获得焦点
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
console.log('确认操作逻辑已触发');
} else if (argument === 'cancelAction') {
// 执行后台任务,比如记录日志
console.log('取消操作逻辑已触发(后台)');
// 可能不需要打扰用户界面
}
}
进阶技巧与注意事项
-
区分
click
和action
事件 :要搞清楚,用户点击通知的主体部分(非按钮区域)会触发click
事件,而点击<actions>
里定义的按钮则触发action
事件。它们的用途和传递的信息不同。通常click
用来激活应用窗口,action
用来执行具体操作。 -
activationType
的选择 :foreground
:最常用,适合需要用户立即看到结果或进行后续交互的操作。background
:适合不需要界面反馈、可以在后台默默完成的任务,比如“标记为已读”、“静音”等。这能减少对用户的打扰。protocol
:使用场景相对特殊,比如点击按钮打开一个特定格式的链接(myapp://dosomething?id=123
)。需要你的应用注册并能处理这个协议。用这个要特别注意安全,恶意构造的协议链接可能有风险。
-
动态生成
arguments
:arguments
字符串不一定是固定的。你可以根据通知相关的上下文动态生成它。例如,如果通知是关于某个特定项目的,你可以把项目 ID 包含进去:arguments="viewProject-${projectId}"
。这样,在action
事件处理器里,解析这个字符串就能知道具体要操作哪个项目了。// 动态生成 arguments 示例 const itemId = 'item-abc-123'; const dynamicArgs = `processItem-${itemId}`; const dynamicAction = `<action content="处理它" arguments="${dynamicArgs}" activationType="foreground"/>`; // 然后把 dynamicAction 插入到你的 toastXml 模板中
-
错误处理 :监听
failed
事件很重要。它可以帮你诊断为什么通知没显示出来,常见原因包括 XML 格式错误、图片路径不正确、或者 Windows 上的通知设置被禁用了等等。 -
AppUserModelId (AUMID) :在 Windows 上,Toast 通知的许多行为(包括能否正确响应点击、是否堆叠等)都和应用程序的 AppUserModelId 紧密相关。务必使用
app.setAppUserModelId()
设置一个唯一的 ID,并且最好与你的安装包或快捷方式设置的 ID 保持一致。开发时用com.electron.yourappname
常常可行,但发布时建议用更规范的 ID。
安全第一
- 谨慎使用
activationType="protocol"
:如果你的应用注册了自定义协议,要确保对传入的 URL 参数做了严格的校验和清理,防止潜在的命令注入或其他攻击。 - 验证
arguments
内容 :即使是foreground
或background
类型,如果你在arguments
里传递了数据(比如 ID),在处理action
事件时,也应该验证这些数据的有效性,不要盲目信任。比如,检查 ID 格式是否正确,查询数据库确认该 ID 对应的资源确实存在且用户有权限操作等。
通过这种方式,你就能在 Electron 应用中充分利用 Windows Toast 通知的交互能力,为用户提供更原生、更便捷的操作体验了。记住关键:构造好 toastXml
的 <actions>
部分,并正确监听 Notification
实例的 action
事件获取用户点击的按钮索引。