系統托盤功能讓 Java 桌面應用能常駐後台、最小化不佔用任務欄、提供快捷菜單交互,是桌面端產品級應用的標配。本文基於 AWT SystemTray / TrayIcon API,構建一個完整可運行的「電話薄」Demo,涵蓋圖標載入、右鍵菜單、雙擊/單擊事件、窗口顯隱邏輯、跨平台兼容性處理,並給出生產環境常見坑點與優化建議。
if (SystemTray.isSupported()) {
URL imgUrl = getClass().getResource("/icons/tray-icon.png"); // 建議放 resources 目錄
ImageIcon icon = new ImageIcon(imgUrl);
// ...
}
注意:getResource 路徑以 / 開頭表示 classpath 根目錄,打包成 JAR 後依然生效;避免使用相對路徑或 File 讀取。
PopupMenu popup = new PopupMenu();
MenuItem showItem = new MenuItem("顯示主窗口");
MenuItem exitItem = new MenuItem("退出系統");
showItem.addActionListener(e -> setVisible(true));
exitItem.addActionListener(e -> System.exit(0));
popup.add(showItem);
popup.add(exitItem);
此處必須使用 AWT MenuItem,Swing JMenuItem 無法添加到 PopupMenu。
TrayIcon trayIcon = new TrayIcon(
icon.getImage(),
"電話薄系統 - 雙擊顯示/隱藏",
popup
);
trayIcon.setImageAutoSize(true); // 自動縮適配不同 DPI 托盤區尺寸
SystemTray tray = SystemTray.getSystemTray();
try {
tray.add(trayIcon);
} catch (AWTException e) {
log.error("托盤圖標註冊失敗", e);
}
trayIcon.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) { // 僅左鍵
if (e.getClickCount() == 2) { // 雙擊
toggleWindowVisibility();
}
// 單擊可選:顯示工具提示、彈出氣泡消息
}
}
});
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
int opt = JOptionPane.showConfirmDialog(
MainFrame.this,
"確定要最小化到托盤嗎?選「否」將退出程序。",
"提示", JOptionPane.YES_NO_OPTION
);
if (opt == JOptionPane.YES_OPTION) {
hideToTray();
} else {
System.exit(0);
}
}
@Override
public void windowIconified(WindowEvent e) {
hideToTray(); // 點最小化按鈕直接隱藏
}
});
private void hideToTray() {
setVisible(false);
// 可選:trayIcon.displayMessage("電話薄", "已最小化到托盤", TrayIcon.MessageType.INFO);
}
package com.example.traydemo;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.net.URL;
public class TrayDemo extends JFrame {
public TrayDemo() {
super("TrayDemo - 電話薄");
setSize(500, 400);
setLocationRelativeTo(null);
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
initMenu();
initTray();
addWindowListener(new WindowAdapter() {
@Override public void windowClosing(WindowEvent e) { confirmExitOrHide(); }
@Override public void windowIconified(WindowEvent e) { hideToTray(); }
});
setVisible(true);
}
private void initMenu() {
JMenuBar bar = new JMenuBar();
JMenu menu = new JMenu("系統");
JMenuItem hideItem = new JMenuItem("置於托盤");
JMenuItem exitItem = new JMenuItem("系統退出");
hideItem.addActionListener(e -> hideToTray());
exitItem.addActionListener(e -> System.exit(0));
menu.add(hideItem); menu.add(exitItem);
bar.add(menu);
setJMenuBar(bar);
}
private void initTray() {
if (!SystemTray.isSupported()) return;
URL url = getClass().getResource("/icons/tray.png");
if (url == null) { System.err.println("圖標資源缺失"); return; }
Image image = new ImageIcon(url).getImage();
PopupMenu popup = new PopupMenu();
MenuItem show = new MenuItem("顯示窗口");
MenuItem exit = new MenuItem("退出");
show.addActionListener(e -> setVisible(true));
exit.addActionListener(e -> System.exit(0));
popup.add(show); popup.add(exit);
TrayIcon trayIcon = new TrayIcon(image, "TrayDemo", popup);
trayIcon.setImageAutoSize(true);
trayIcon.addMouseListener(new MouseAdapter() {
@Override public void mouseClicked(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2)
toggleVisibility();
}
});
try { SystemTray.getSystemTray().add(trayIcon); }
catch (AWTException ex) { ex.printStackTrace(); }
}
private void hideToTray() { setVisible(false); }
private void toggleVisibility() { setVisible(!isVisible()); }
private void confirmExitOrHide() {
int opt = JOptionPane.showConfirmDialog(this, "最小化到托盤?", "提示", JOptionPane.YES_NO_OPTION);
if (opt == JOptionPane.YES_OPTION) hideToTray(); else System.exit(0);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); }
catch (Exception ignored) {}
new TrayDemo();
});
}
}
| 問題 | 原因 | 解決方案 |
|---|---|---|
| Linux/GNOME/KDE 托盤不顯示 | 桌面環境不支援 SystemTray 或需啟用 AppIndicator | 檢查 SystemTray.isSupported();Ubuntu 18.04+ 需安裝 libappindicator;或使用 JSystemTray 等第三方庫 |
| 高 DPI 螢幕圖標模糊 | 圖標尺寸固定,未自動縮放 | trayIcon.setImageAutoSize(true) + 提供多尺寸圖標(16/22/24/32/64px) |
| 右鍵菜單中文亂碼 | 系統默認字體不支援中文 | 啟動參數 -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true;或手動設置 MenuItem 字體 |
| 應用退出後托盤圖標殘留 | 未調用 tray.remove(trayIcon) | 在 System.exit 前或 ShutdownHook 中移除:Runtime.getRuntime().addShutdownHook(new Thread(() -> tray.remove(trayIcon))); |
| 多實例運行產生多個托盤圖標 | 未實現單例鎖 | 使用文件鎖 或 java.awt.Desktop 單例模式 |
trayIcon.displayMessage("標題", "內容", TrayIcon.MessageType.INFO),支援 INFO/WARNING/ERROR/NONE 四種圖標。trayIcon.setImage(newImage)。SystemTray 實現原生感托盤。Java 系統托盤開發核心在於 AWT 原生組件與 Swing 窗口的協作。掌握 SystemTray.isSupported() 防禦性檢查、資源路徑正確載入、窗口關閉事件攔截、圖標自適應縮放、殘留圖標清理,即可構建穩健的桌面端常駐應用。建議將托盤邏輯封裝為獨立模組,便於在不同項目中複用與單元測試。