本文在上一篇基礎托盤教程基礎上,構建一個具備桌面截圖核心功能的「EasyCapture」工具,展示托盤應用的完整工程架構:主窗口生命週期管理、托盤控制器解耦、截圖邏輯佔位、系統外觀適配、資源加載、關閉/最小化策略、單擊/雙擊語義區分。代碼採用 MVC 分層,便於功能擴展。
com.b510.tray
├── DesktopCapture.java // 主窗口控制器 (View + Controller)
├── MyTray.java // 托盤組件封裝 (Model)
└── resources/
└── image/xiaomai.png // 托盤圖標
package com.b510.tray;
import javax.swing.*;
import java.awt.event.*;
public class DesktopCapture extends JFrame {
private static final long serialVersionUID = 1L;
private MyTray tray;
private boolean iconed = false; // 標記是否主動最小化到托盤
public DesktopCapture() {
super("EasyCapture");
initUI();
initTray();
initWindowListeners();
pack();
setSize(350, 230);
setLocationRelativeTo(null);
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); // 交由監聽器處理
setResizable(false);
setVisible(true);
}
private void initUI() {
// 實際項目:工具欄、設置面板、歷史記錄列表等
JLabel label = new JLabel("易捕獲截圖工具", SwingConstants.CENTER);
label.setFont(label.getFont().deriveFont(18f));
add(label);
}
private void initTray() {
tray = new MyTray(this);
}
private void initWindowListeners() {
addWindowListener(new WindowAdapter() {
@Override
public void windowIconified(WindowEvent e) { // 點最小化按鈕
hideToTray();
}
@Override
public void windowClosing(WindowEvent e) { // 點關閉按鈕
int opt = JOptionPane.showConfirmDialog(
DesktopCapture.this,
"確定最小化到托盤?選「否」退出程序。",
"提示", JOptionPane.YES_NO_OPTION
);
if (opt == JOptionPane.YES_OPTION) {
hideToTray();
} else {
shutdown();
}
}
});
}
// 業務邏輯:截圖佔位
public void capture() {
// TODO: 實現全屏截圖、區域選取、編輯標註、保存/複製/上傳
System.out.println(">> 觸發截圖邏輯");
// 示例:Robot.createScreenCapture + Swing 選區組件
}
private void hideToTray() {
if (!iconed) {
iconed = true;
setVisible(false);
// tray.getTrayIcon().displayMessage("EasyCapture", "已最小化到托盤", TrayIcon.MessageType.INFO);
}
}
public void restoreFromTray() {
iconed = false;
setVisible(true);
setExtendedState(JFrame.NORMAL); // 確保非最大化/圖標化狀態
toFront(); // 置頂
}
private void shutdown() {
if (tray != null) tray.dispose();
System.exit(0);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ignored) {}
new DesktopCapture();
});
}
}
package com.b510.tray;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class MyTray implements ActionListener, MouseListener {
private final DesktopCapture frame;
private final SystemTray systemTray;
private final TrayIcon trayIcon;
private final PopupMenu popup = new PopupMenu();
private final MenuItem miCapture = new MenuItem("截圖");
private final MenuItem miShow = new MenuItem("顯示主窗口");
private final MenuItem miExit = new MenuItem("退出");
public MyTray(DesktopCapture frame) {
this.frame = frame;
// 1. 載入圖標(classpath 方式,支援 JAR 打包)
Image icon = loadIcon("/image/xiaomai.png");
if (icon == null) {
System.err.println("托盤圖標加載失敗,功能不可用");
return;
}
// 2. 檢查系統托盤支援
if (!SystemTray.isSupported()) {
System.err.println("當前系統不支援 SystemTray");
return;
}
systemTray = SystemTray.getSystemTray();
// 3. 構建彈出菜單
buildPopupMenu();
// 4. 創建並註冊 TrayIcon
trayIcon = new TrayIcon(icon, "EasyCapture - 單擊截圖", popup);
trayIcon.setImageAutoSize(true); // 高 DPI 自適應
trayIcon.addMouseListener(this); // 單擊/雙擊事件
try {
systemTray.add(trayIcon);
} catch (AWTException e) {
e.printStackTrace();
}
}
private Image loadIcon(String path) {
java.net.URL url = getClass().getResource(path);
if (url == null) return null;
return new ImageIcon(url).getImage();
}
private void buildPopupMenu() {
miCapture.addActionListener(this);
miShow.addActionListener(this);
miExit.addActionListener(this);
popup.add(miCapture);
popup.add(miShow);
popup.add(miExit);
}
@Override
public void actionPerformed(ActionEvent e) {
Object src = e.getSource();
if (src == miShow) {
frame.restoreFromTray();
} else if (src == miCapture) {
frame.capture();
} else if (src == miExit) {
frame.shutdown();
}
}
// === MouseListener: 單擊托盤圖標觸發截圖 ===
@Override
public void mouseClicked(MouseEvent e) {
// 僅左鍵單擊(非右鍵彈出菜單)觸發
if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 1) {
frame.capture();
}
}
@Override public void mousePressed(MouseEvent e) {}
@Override public void mouseReleased(MouseEvent e) {}
@Override public void mouseEntered(MouseEvent e) {}
@Override public void mouseExited(MouseEvent e) {}
public void dispose() {
if (systemTray != null && trayIcon != null) {
systemTray.remove(trayIcon);
}
}
}
| 決策點 | 方案 | 理由 |
|---|---|---|
| 圖標加載 | getClass().getResource() | 支援 IDE 運行、JAR 打包、模塊化運行時,避免相對路徑失效 |
| 主窗口關閉策略 | DO_NOTHING_ON_CLOSE + 確認對話框 | 防止用戶誤關閉,符合托盤應用「關閉=隱藏」直覺 |
| 最小化行為 | 直接隱藏到托盤 | 減少干擾,任務欄不佔位 |
| 托盤點擊語義 | 左鍵單擊=截圖,右鍵=菜單 | 高頻操作零點擊成本,參考 Snipaste/QQ 截圖交互 |
| 高 DPI 適配 | setImageAutoSize(true) | Windows 縮放 125%/150%、Mac Retina 下圖標不模糊 |
| 資源清理 | dispose() 移除托盤圖標 | 避免進程退出後圖標殘留(Windows 常見) |
| 系統外觀 | UIManager.getSystemLookAndFeelClassName() | 原生風格,用戶無違和感 |
public void capture() {
// 1. 隱藏主窗口(如可見)
boolean wasVisible = isVisible();
if (wasVisible) setVisible(false);
// 2. 獲取全屏圖像
BufferedImage screen = new Robot().createScreenCapture(
new Rectangle(Toolkit.getDefaultToolkit().getScreenSize())
);
// 3. 彈出選區框架(全屏透明窗口 + MouseListener 拖拽選區)
// 參考開源項目:ScreenShot (GitHub), ShareX 邏輯
// 選區完成後 -> 截取子圖 -> 進入編輯模式 (標註、文字、馬賽克)
// 4. 編輯完成後:保存文件 / 複製到剪貼板 / 上傳圖床
// Toolkit.getDefaultToolkit().getSystemClipboard().setContents(...)
// 5. 恢復主窗口狀態
if (wasVisible) setVisible(true);
}
libappindicator3-1,部分桌面環境需啟用 AppIndicator 支援;若 SystemTray.isSupported() 返回 false,降級為狀態欄圖標庫。SystemTray 映射到菜單欄右側,圖標需為模板圖像(僅 alpha 通道),建議提供 .png 並設置 trayIcon.setImageAutoSize(true)。dpiAware=true 或啟動參數 -Dsun.java2d.dpiaware=true。本例展示了生產級托盤應用的骨架:主窗口與托盤解耦、生命週期精細管理、資源加載規範化、交互語義符合用戶習慣、跨平台降級策略。在此基礎上接入 Robot 截圖、Swing/JavaFX 編輯器、剪貼板、圖床上傳,即可構建商業級截圖工具。