我写这文档是为了帮助初学者,特别是从 Web 开发或其他编程语言转向 Sciter.js GUI 开发的开发者,理解和使用 Sciter.js 中的系统托盘图标功能。
什么是系统托盘图标?
系统托盘(System Tray),在 Windows 中也称为通知区域(Notification Area),在 macOS 中称为菜单栏(Menu Bar Extras),是操作系统界面的一部分,通常位于屏幕的角落(如 Windows 的右下角,macOS 的右上角)。应用程序可以将一个小图标放置在这里,用于显示状态、提供快捷访问或在后台运行时保持存在感。
Sciter.js 通过 Window
对象的 trayIcon()
方法提供了与系统托盘交互的能力。
基本用法
核心方法是 Window.this.trayIcon()
,它根据传入的参数执行不同操作。
1. 创建或设置托盘图标
要显示一个托盘图标,你需要提供一个包含 image
和 text
属性的对象:
javascript
// 确保在 async 函数或合适的作用域内使用 await
async function setMyTrayIcon() {
try {
// 加载图标图片 (推荐使用 SVG)
const iconImage = await Graphics.Image.load(__DIR__ + "path/to/your/icon.svg");
// 设置托盘图标和提示文本
const success = Window.this.trayIcon({
image: iconImage,
text: "我的应用正在运行\n点击查看详情"
});
if (success) {
console.log("托盘图标设置成功!");
// 可以禁用设置按钮,防止重复设置
// document.$("button#set").state.disabled = true;
} else {
console.error("设置托盘图标失败。");
}
} catch (error) {
console.error("加载图标或设置时出错:", error);
}
}
// 调用函数
setMyTrayIcon();
image
: 一个Graphics.Image
对象,表示要在托盘中显示的图标。推荐使用矢量图形(如 SVG)以获得最佳显示效果。使用await Graphics.Image.load()
异步加载图片。text
: 一个字符串,当鼠标悬停在托盘图标上时显示的工具提示(Tooltip)文本。Window.this
: 指向当前窗口实例。__DIR__
: 一个常量,表示当前脚本文件所在的目录路径,方便引用相对路径的资源。- 返回值:如果设置成功,返回
true
,否则返回false
。
2. 更新托盘图标或提示文本
如果你只想更新提示文本,可以再次调用 trayIcon
,只传入 text
属性:
javascript
Window.this.trayIcon({ text: "状态已更新: " + new Date() });
如果需要更换图标,则需要同时提供新的 image
对象。
3. 移除托盘图标
当你不再需要托盘图标时(例如,应用程序退出或用户选择隐藏),可以将其移除:
javascript
const removed = Window.this.trayIcon("remove");
if (removed) {
console.log("托盘图标已移除。");
// 可以重新启用设置按钮
// document.$("button#set").state.disabled = false;
}
- 传入字符串
"remove"
作为参数。 - 返回值:如果移除成功,返回
true
。
4. 获取托盘图标的位置
有时你需要知道托盘图标在屏幕上的确切位置,例如为了在图标附近显示弹出窗口。可以使用 "place"
参数:
javascript
// 获取图标位置(物理像素)
const [x, y, w, h] = Window.this.trayIcon("place");
console.log(`图标位置: x=${x}, y=${y}, width=${w}, height=${h}`);
// 获取图标位置(CSS 像素 / DIPs)
const [x_dip, y_dip, w_dip, h_dip] = Window.this.trayIcon("place", false);
console.log(`图标位置 (DIPs): x=${x_dip}, y=${y_dip}, width=${w_dip}, height=${h_dip}`);
- 传入字符串
"place"
。 - 可选的第二个参数
asPPX
(默认为true
):true
表示返回物理像素坐标,false
表示返回设备无关像素(CSS 像素)。 - 返回值:一个包含
[x, y, width, height]
的数组。
处理托盘图标事件
托盘图标可以响应用户的点击事件。最常用的事件是 "trayiconclick"
。
javascript
Window.this.on("trayiconclick", (evt) => {
console.log("托盘图标被点击了!");
// evt.data 包含了点击的详细信息
const clickDetails = evt.data;
console.log(`屏幕坐标: X=${clickDetails.screenX}, Y=${clickDetails.screenY}`);
console.log(`按下的按钮: ${clickDetails.buttons}`); // 1: 左键, 2: 右键, 4: 中键
// 常见用法:右键点击显示菜单
if (clickDetails.buttons === 2) {
showPopupMenu(clickDetails.screenX, clickDetails.screenY);
}
// 常见用法:左键点击显示/隐藏主窗口
else if (clickDetails.buttons === 1) {
toggleMainWindowVisibility();
}
});
function toggleMainWindowVisibility() {
if (Window.this.state === Window.WINDOW_HIDDEN || Window.this.state === Window.WINDOW_MINIMIZED) {
Window.this.state = Window.WINDOW_SHOWN;
Window.this.activate(true); // 激活并带到前台
} else {
Window.this.state = Window.WINDOW_HIDDEN;
}
}
// showPopupMenu 函数将在下一节实现
- 使用
Window.this.on("eventname", handler)
来监听窗口事件。 "trayiconclick"
事件在用户点击托盘图标时触发。- 事件对象
evt
的data
属性包含了点击的详细信息,如屏幕坐标 (screenX
,screenY
) 和按下的鼠标按钮 (buttons
)。
实现弹出菜单 (Popup Menu)
一个常见的需求是在右键点击托盘图标时显示一个上下文菜单。Sciter.js 提供了多种方式来实现这一点。
核心思路是:在 trayiconclick
事件(通常是右键点击)中,创建一个新的 Window.POPUP_WINDOW
类型的窗口,将其定位在托盘图标附近,并在这个弹出窗口中显示菜单项。
方法 1: 使用独立的 HTML 文件
这种方法将菜单的结构和逻辑放在一个单独的 HTML 文件中。
主窗口 (main.htm
) 的脚本部分:
javascript
// ... (接上面的 trayiconclick 事件处理)
function showPopupMenu(screenX, screenY) {
// 创建一个新的弹出窗口
const popup = new Window({
type: Window.POPUP_WINDOW, // 窗口类型为弹出窗口
parent: Window.this, // 设置父窗口,父窗口关闭时子窗口也关闭
url: __DIR__ + "tray-menu.htm", // 加载菜单的 HTML 文件
x: screenX, // X 坐标 (来自点击事件)
y: screenY, // Y 坐标 (来自点击事件)
alignment: 2, // 对齐方式 (2: 窗口顶部中点对齐到 x,y)
state: Window.WINDOW_HIDDEN // 初始状态隐藏,在 ready 后显示
});
}
// 主窗口监听来自菜单窗口的自定义事件
Window.this.on("reveal", evt => {
Window.this.state = Window.WINDOW_SHOWN;
Window.this.activate(true);
});
Window.this.on("exit", evt => {
Window.this.trayIcon("remove"); // 移除图标
Window.this.close(); // 关闭主窗口
});
菜单窗口 (tray-menu.htm
):
html
<html window-frame="extended" <!-- 或 solid-with-shadow -->
window-width="max-content"
window-height="max-content"
window-resizable="false"
window-minimizable="false"
window-maximizable="false"
window-closable="false">
<head>
<style>
/* 基础样式,确保菜单可见且尺寸自适应 */
body { margin:0; font:system; }
menu.popup { visibility:visible; display:block; width:max-content; height:max-content; border:none; }
</style>
<script>
// 文档加载完成后显示并激活窗口
document.on("ready", event => {
Window.this.state = Window.WINDOW_SHOWN;
Window.this.activate(true);
});
// 窗口失去焦点时自动关闭
Window.this.on("activate", event => {
if (!event.reason) // reason 为 0 表示失去焦点
Window.this.close();
return true;
});
// 处理菜单项点击:向父窗口发送事件
document.on("click", "li#reveal", event => {
Window.this.parent.dispatchEvent(new Event("reveal"), true);
Window.this.close(); // 关闭菜单
});
document.on("click", "li#exit", event => {
Window.this.parent.dispatchEvent(new Event("exit"), true);
// 不需要关闭菜单,因为父窗口会处理退出
});
</script>
</head>
<body>
<menu.popup>
<li#reveal>显示窗口</li>
<li#exit>退出应用</li>
</menu>
</body>
</html>
关键点:
- 弹出窗口类型为
Window.POPUP_WINDOW
。 - 设置
parent: Window.this
,使弹出窗口与主窗口关联。 - 使用
alignment
参数(或其他定位逻辑)将弹出窗口放置在托盘图标附近。 - 弹出窗口通常设置为
window-frame="solid-with-shadow"
或"extended"
,并设置window-width/height="max-content"
。 - 在弹出窗口的
ready
事件中显示并激活它 (Window.this.state = Window.WINDOW_SHOWN; Window.this.activate(true);
)。 - 在弹出窗口的
activate
事件中监听失去焦点 (!event.reason
),并在失去焦点时关闭窗口 (Window.this.close()
)。 - 菜单项的点击事件通过
Window.this.parent.dispatchEvent()
向父窗口(主窗口)发送自定义事件,主窗口监听这些事件来执行相应操作。
方法 2: 使用 VDOM / JSX (Reactor/React)
如果你使用 Sciter.js Reactor 或类似的 VDOM 库,可以直接在主窗口的脚本中定义菜单窗口的内容。
javascript
// 在主窗口的脚本中 (例如 samples.reactor/window/trayicon-test.htm)
Window.this.on("trayiconclick", evt => {
if (evt.data.buttons == 2) { // 右键点击
const { screenX, screenY } = evt.data;
const popup = new Window({
type: Window.POPUP_WINDOW,
parent: Window.this,
html: <PopupMenuWindow />, // 直接使用 JSX 组件作为内容
x: screenX,
y: screenY,
alignment: 2,
state: Window.WINDOW_SHOWN // 可以直接显示
});
popup.activate(true); // 激活
}
});
// 定义菜单窗口的 JSX 组件
function PopupMenuWindow(props, kids) {
// 窗口失去焦点时关闭
function onWindowActivate(evt) {
if (!evt.reason)
this.close(); // 'this' 指向窗口本身
}
// 菜单项点击处理函数
function revealWindow(evt) {
evt.target.window.close(); // 关闭弹出窗口
Window.this.state = Window.WINDOW_SHOWN; // 操作主窗口
Window.this.activate(true);
}
function exitApp(evt) {
evt.target.window.close();
Window.this.trayIcon("remove");
Window.this.close(); // 关闭主窗口
}
return (
<html window-frame="solid-with-shadow"
window-width="max-content"
window-height="max-content"
window-onactivate={onWindowActivate}> // 直接绑定窗口事件
<head>
<style>
body { margin:0; font:system; }
menu.popup { visibility:visible; display:block; width:max-content; height:max-content; border:none; }
</style>
</head>
<body>
<menu.popup.visible>
<li onClick={revealWindow}>显示窗口</li>
<li onClick={exitApp}>退出应用</li>
</menu>
</body>
</html>
);
}
关键点:
- 使用
html: <YourJSXComponent />
将 JSX 组件直接作为新窗口的内容。 - 可以在 JSX 中直接定义窗口属性(如
window-frame
)和事件处理器(如window-onactivate
)。 - 菜单项的点击事件可以直接调用主窗口作用域中的函数(因为组件是在主窗口脚本中定义的),或者通过
evt.target.window
获取弹出窗口实例并关闭它。
方法 3: 使用 JS 模块封装逻辑
可以将托盘图标和弹出菜单的创建逻辑封装到一个可复用的 JS 模块中。
模块 (trayicon-popup.js
):
javascript
const hostWindow = Window.this; // 保存主窗口引用
let contentProducer; // 用于生成菜单内容的函数
// 设置图标和点击回调
export function setup(iconUrl, iconText, _contentProducer) {
contentProducer = _contentProducer;
let r = hostWindow.trayIcon({
image: Graphics.Image.load(iconUrl, true),
text: iconText,
});
console.assert(r, "trayIcon setup failed");
// 移除图标的清理逻辑
hostWindow.document.on("beforeunload", evt => { hostWindow.trayIcon("remove") });
// 监听点击事件
hostWindow.on("trayiconclick", evt => {
if (evt.data.buttons === 2 && contentProducer) {
createTrayIconPopupWindow(contentProducer()); // 调用内容生成函数
}
});
}
// 创建弹出菜单窗口
function createTrayIconPopupWindow(content) {
// 定义窗口的 HTML 结构 (可以使用 JSX)
const html = <html window-frame="solid-with-shadow"
window-resizable="false"
window-blurbehind="auto"
window-closable="false" style="overflow:hidden">
<head>
<style>
body { margin:0; font:system; }
menu { visibility:visible; display:block; width:max-content; height:max-content; border:none; }
</style>
</head>
<body>{content}</body>
</html>;
// 获取图标位置并计算弹出窗口位置
let [x, y, w, h] = hostWindow.trayIcon("place", false);
let [sx, sy, sw, sh] = hostWindow.screenBox("workarea", "xywh", false);
let alignment = (y >= sy + sh / 2) ? 2 : 8; // 智能判断在上方还是下方显示
let screenX = x + w / 2;
let screenY = (alignment === 2) ? y : y + h;
// 创建窗口
const window = new Window({
type: Window.POPUP_WINDOW,
parent: hostWindow,
html: html,
state: Window.WINDOW_SHOWN,
x: screenX,
y: screenY,
alignment: alignment
});
// 激活并设置失去焦点关闭
window.document.on("ready", event => { window.activate(true); });
window.on("activate", event => { if (!event.reason) window.close(); return true; });
}
主窗口 (main.htm
) 的脚本部分:
javascript
import * as TrayIconPopup from "./trayicon-popup.js";
// 定义菜单内容生成函数
function createTrayMenuContent() {
// 这些函数会在模块的弹出窗口上下文中被调用
// 因此需要通过 hostWindow 访问主窗口
const host = TrayIconPopup.hostWindow; // 获取模块中保存的主窗口引用
function reveal() {
host.state = Window.WINDOW_SHOWN;
host.activate(true);
// 关闭菜单的操作由模块的 activate 事件处理
}
function exit() {
host.trayIcon("remove");
host.close();
}
return (
<menu.popup>
<li onclick={reveal}>显示窗口</li>
<li onclick={exit}>退出应用</li>
</menu.popup>
);
}
// 初始化托盘图标和弹出菜单
document.on("click", "button#setupTray", function() {
TrayIconPopup.setup(__DIR__ + "icon.svg", "我的应用", createTrayMenuContent);
this.state.disabled = true; // 禁用按钮
});
关键点:
- 模块封装了
setup
和createTrayIconPopupWindow
的逻辑。 setup
函数接收图标信息和一个"内容生成器"函数 (contentProducer
)。- 主窗口调用
setup
,并提供一个函数来动态生成菜单的 VDOM/JSX。 - 模块在需要时调用这个内容生成器函数来获取菜单内容。
- 菜单项的事件处理器需要注意执行上下文,可能需要通过模块暴露的主窗口引用 (
hostWindow
) 来操作主窗口。
最佳实践
-
资源清理 : 最重要的一点是,在应用程序关闭或不再需要托盘图标时,务必调用
Window.this.trayIcon("remove")
来移除它。否则,即使应用程序进程结束,图标可能仍会残留在系统托盘中(尤其是在 Windows 上)。最佳实践是在主窗口的beforeunload
事件或明确的退出逻辑中执行移除操作。javascript// 在主窗口脚本中 document.on("beforeunload", () => { Window.this.trayIcon("remove"); }); // 或者在一个退出函数中 function exitApplication() { Window.this.trayIcon("remove"); Window.this.close(); }
-
异步加载图标 : 使用
await Graphics.Image.load()
异步加载图标文件,避免阻塞 UI 线程。 -
错误处理 : 检查
trayIcon()
方法的返回值,并使用try...catch
处理可能的图片加载错误。 -
弹出窗口行为: 确保弹出菜单窗口在失去焦点时能自动关闭,提供良好的用户体验。
-
图标设计: 使用清晰、简洁的图标,最好是 SVG 格式,以适应不同的屏幕分辨率和主题(亮色/暗色)。
-
提示文本: 提供简洁明了的工具提示文本。
-
跨平台考虑: 虽然 Sciter 的目标是跨平台,但系统托盘的具体行为和外观可能因操作系统而异。测试你的应用在目标平台上的表现。
通过遵循这些指南和示例,你应该能够有效地在你的 Sciter.js 应用程序中集成系统托盘图标功能。