Contents

electron 新手做Tray通知(以electron-chat-app-demo為例)

Electron 讓開發者用 Web 技術(HTML、CSS、JavaScript)開發桌面應用程式,系統 Tray(系統匣)是桌面應用的常見功能,讓程式在背景執行時仍可從工具列圖示快速操作。

Electron 基本架構

Electron 分為兩個程序:

  • Main Process(主程序):控制應用生命週期、建立視窗、存取系統 API(包含 Tray)
  • Renderer Process(渲染程序):負責 UI 顯示,類似瀏覽器環境

Tray 只能在 Main Process 中建立。

建立系統 Tray

基本設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// main.js(Main Process)
const { app, Menu, Tray, nativeImage } = require('electron');
const path = require('path');

let tray = null;

app.whenReady().then(() => {
    // 建立 Tray 圖示(建議使用 16x16 或 32x32 的 PNG)
    const trayIcon = path.join(__dirname, 'icons', 'icon-16x16.png');
    tray = new Tray(trayIcon);

    // 設定 Tray 提示文字
    tray.setToolTip('我的應用程式');

    // 設定右鍵選單
    const contextMenu = Menu.buildFromTemplate([
        {
            label: '顯示視窗',
            click: () => {
                mainWindow.show();
            }
        },
        {
            label: '設定',
            click: () => {
                // 開啟設定視窗
            }
        },
        { type: 'separator' },
        {
            label: '離開',
            click: () => {
                app.quit();
            }
        }
    ]);

    tray.setContextMenu(contextMenu);
});

點擊事件處理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 點擊 Tray 圖示時顯示/隱藏主視窗
tray.on('click', () => {
    if (mainWindow.isVisible()) {
        mainWindow.hide();
    } else {
        mainWindow.show();
        mainWindow.focus();
    }
});

// 雙擊事件(Windows 常見行為)
tray.on('double-click', () => {
    mainWindow.show();
    mainWindow.focus();
});

氣泡通知(系統通知)

Electron 使用 Web Notification API 發送系統通知:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Renderer Process 中使用(或透過 ipcMain 從 Main Process 發送)
function showNotification(title, body) {
    const notification = new Notification(title, {
        body: body,
        icon: path.join(__dirname, 'icons', 'icon-128x128.png')
    });

    notification.onclick = () => {
        mainWindow.show();
        mainWindow.focus();
    };

    notification.show();
}

// 呼叫範例
showNotification('新訊息', '你有一則新訊息!');

最小化到系統匣(而非關閉)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
mainWindow.on('close', (event) => {
    if (!app.isQuiting) {
        event.preventDefault();
        mainWindow.hide();  // 最小化到 Tray,而非真正關閉
    }
});

// 在 Tray 選單的「離開」中
{
    label: '離開',
    click: () => {
        app.isQuiting = true;
        app.quit();
    }
}

動態更新 Tray 圖示

可以透過更換圖示來表示不同狀態(例如有新通知時):

1
2
3
4
5
// 切換圖示
function setTrayIcon(hasNotification) {
    const iconName = hasNotification ? 'icon-notification.png' : 'icon.png';
    tray.setImage(path.join(__dirname, 'icons', iconName));
}

electron-builder 打包注意事項

使用 electron-builder 打包時,需要確保 Tray 圖示被正確包含:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// package.json
{
  "build": {
    "appId": "com.example.myapp",
    "win": {
      "icon": "build/icon.ico"
    },
    "mac": {
      "icon": "build/icon.icns"
    },
    "linux": {
      "icon": "build/icons"
    },
    "extraResources": [
      {
        "from": "src/icons",
        "to": "icons",
        "filter": ["**/*"]
      }
    ]
  }
}

圖示格式注意事項:

  • Windows:需要 .ico 格式,建議包含多尺寸(16x16 到 256x256)
  • macOS:Tray 圖示建議使用模板圖示(純黑白),會自動適應深色/淺色模式
  • Linux:使用 PNG 格式

完整 main.js 範例結構

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const { app, BrowserWindow, Menu, Tray } = require('electron');
const path = require('path');

let mainWindow;
let tray;

function createWindow() {
    mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true,
            contextIsolation: false
        }
    });

    mainWindow.loadFile('index.html');

    mainWindow.on('close', (event) => {
        if (!app.isQuiting) {
            event.preventDefault();
            mainWindow.hide();
        }
    });
}

function createTray() {
    const trayIcon = path.join(__dirname, 'icons', 'icon-16x16.png');
    tray = new Tray(trayIcon);
    tray.setToolTip('My App');

    const menu = Menu.buildFromTemplate([
        { label: '顯示', click: () => mainWindow.show() },
        { type: 'separator' },
        { label: '離開', click: () => { app.isQuiting = true; app.quit(); } }
    ]);

    tray.setContextMenu(menu);
    tray.on('click', () => mainWindow.show());
}

app.whenReady().then(() => {
    createWindow();
    createTray();
});

app.on('window-all-closed', (event) => {
    event.preventDefault(); // 關閉所有視窗後不退出,繼續在 Tray 運行
});

參考資料