package com.hwtd.mes.collect.config;
|
|
import com.hwtd.mes.collect.handler.SerialPortListener;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.boot.ApplicationArguments;
|
import org.springframework.boot.ApplicationRunner;
|
import org.springframework.boot.SpringApplication;
|
import org.springframework.context.ApplicationContext;
|
import org.springframework.stereotype.Component;
|
import org.springframework.util.StringUtils;
|
|
import javax.annotation.PreDestroy;
|
import javax.imageio.ImageIO;
|
import javax.swing.AbstractAction;
|
import javax.swing.BorderFactory;
|
import javax.swing.BoxLayout;
|
import javax.swing.JButton;
|
import javax.swing.JComponent;
|
import javax.swing.JDialog;
|
import javax.swing.JLabel;
|
import javax.swing.JOptionPane;
|
import javax.swing.JPanel;
|
import javax.swing.JSeparator;
|
import javax.swing.JWindow;
|
import javax.swing.KeyStroke;
|
import javax.swing.SwingConstants;
|
import javax.swing.Timer;
|
import javax.swing.WindowConstants;
|
import java.awt.AWTException;
|
import java.awt.BorderLayout;
|
import java.awt.Color;
|
import java.awt.Cursor;
|
import java.awt.Desktop;
|
import java.awt.Dimension;
|
import java.awt.EventQueue;
|
import java.awt.FlowLayout;
|
import java.awt.GridLayout;
|
import java.awt.Font;
|
import java.awt.Graphics2D;
|
import java.awt.GraphicsConfiguration;
|
import java.awt.GraphicsDevice;
|
import java.awt.GraphicsEnvironment;
|
import java.awt.Image;
|
import java.awt.MouseInfo;
|
import java.awt.Point;
|
import java.awt.Rectangle;
|
import java.awt.RenderingHints;
|
import java.awt.SystemTray;
|
import java.awt.TrayIcon;
|
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionListener;
|
import java.awt.event.KeyEvent;
|
import java.awt.event.MouseAdapter;
|
import java.awt.event.MouseEvent;
|
import java.awt.event.WindowAdapter;
|
import java.awt.event.WindowEvent;
|
import java.awt.image.BufferedImage;
|
import java.io.File;
|
import java.io.InputStream;
|
import java.util.Arrays;
|
import java.util.HashSet;
|
import java.util.Set;
|
|
/**
|
* 系统托盘图标:exe(或 jar)启动后在任务栏右下角托盘常驻一个小图标,右键菜单可以快速退出。
|
* <p>
|
* 说明:
|
* <ul>
|
* <li>托盘依赖 AWT,而 Spring Boot 默认 {@code java.awt.headless=true}(此时 {@link SystemTray#isSupported()} 为 false),
|
* 所以启动类里已经用 {@code setHeadless(false)} 关掉;</li>
|
* <li>右键菜单用 Swing 自己画一个无边框窗口({@link JWindow}),不用 AWT 的 PopupMenu:
|
* AWT 菜单是 Windows 原生菜单,文字用系统菜单字体绘制,部分机器上不含中文字形会显示成方块,而且 {@code setFont} 不生效;</li>
|
* <li>菜单弹出位置和系统托盘菜单一致:显示在光标上方、右边缘与光标对齐,并保证不超出屏幕(多显示器时按光标所在屏幕计算);</li>
|
* <li>菜单收起方式:点击菜单外面(窗口失焦)、按 ESC、再右键一次、点了某一项之后,
|
* 另外还有一层兜底——鼠标离开菜单附近也会自动收起(避免个别环境窗口抢不到焦点导致菜单关不掉);</li>
|
* <li>菜单里的“查看串口状态”(或双击托盘图标)会弹出一个状态窗口:串口名称、监听状态、串口连接、缓存条数,每秒自动刷新;</li>
|
* <li>菜单里的“关闭串口”只关闭串口监听(不退出程序),方便现场把串口释放给别的程序用,结果用托盘气泡提示;</li>
|
* <li>图标优先读取 classpath 下的 {@code tray.png}(放一张 32x32 的 png 即可换成自己的图标),没有就用代码画的默认图标;</li>
|
* <li>点“退出”会先关闭 Spring 容器(触发 {@code @PreDestroy},例如关闭串口监听、停止补帧线程),再结束进程;
|
* 用任务管理器结束进程、或直接关掉 exe 的控制台窗口,则不会走这套清理流程;</li>
|
* <li>以 Windows 服务(Session 0,无桌面)方式运行时托盘不会显示,代码会自动跳过,不影响接口使用。</li>
|
* </ul>
|
*/
|
@Slf4j
|
@Component
|
public class SystemTrayInitializer implements ApplicationRunner {
|
|
/** 默认图标尺寸 */
|
private static final int DEFAULT_ICON_SIZE = 32;
|
/** 菜单背景色 */
|
private static final Color MENU_BACKGROUND = Color.WHITE;
|
/** 菜单项鼠标悬停背景色 */
|
private static final Color MENU_HOVER_BACKGROUND = new Color(0xE8F0FE);
|
/** 菜单边框颜色 */
|
private static final Color MENU_BORDER = new Color(0xC8C8C8);
|
|
@Value("${server.port:9527}")
|
private int serverPort;
|
|
@Value("${logging.file-location:logs}")
|
private String logLocation;
|
|
private final ApplicationContext applicationContext;
|
|
/** 托盘图标,退出时先移除 */
|
private TrayIcon trayIcon;
|
|
/** 右键菜单窗口,第一次右键时创建 */
|
private JWindow popupWindow;
|
|
/** 状态窗口,第一次点“查看采集状态”时创建 */
|
private JDialog statusDialog;
|
|
/** 状态窗口的自动刷新定时器(每秒刷新一次) */
|
private Timer statusRefreshTimer;
|
|
/** 状态窗口里的值 */
|
private JLabel serialPortNameValue;
|
private JLabel listeningValue;
|
private JLabel portOpenValue;
|
private JLabel dataSizeValue;
|
|
/** 兜底收起检查:万一菜单窗口没抢到焦点,鼠标离开菜单后也能自动收起 */
|
private Timer popupWatchdog;
|
|
/** 弹出菜单时的光标位置(托盘图标位置) */
|
private Point popupAnchor;
|
|
/** 光标连续几次不在菜单附近 */
|
private int outsideCount;
|
|
public SystemTrayInitializer(ApplicationContext applicationContext) {
|
this.applicationContext = applicationContext;
|
}
|
|
@Override
|
public void run(ApplicationArguments args) {
|
if (!SystemTray.isSupported()) {
|
log.warn("当前环境不支持系统托盘(headless 模式、无桌面会话或缺少 java.desktop 模块),跳过托盘图标");
|
return;
|
}
|
try {
|
// AWT/Swing 组件统一在事件分发线程(EDT)里创建
|
EventQueue.invokeAndWait(this::createTrayIcon);
|
} catch (Exception e) {
|
log.error("创建系统托盘图标失败:{}", e.getMessage(), e);
|
}
|
}
|
|
/**
|
* 创建托盘图标,并注册鼠标事件:右键弹菜单、左键双击打开采集状态
|
*/
|
private void createTrayIcon() {
|
trayIcon = new TrayIcon(loadTrayImage(), "亨旺特导MES数据采集器");
|
trayIcon.setImageAutoSize(true);
|
trayIcon.addMouseListener(new MouseAdapter() {
|
@Override
|
public void mouseReleased(MouseEvent event) {
|
if (event.isPopupTrigger() || event.getButton() == MouseEvent.BUTTON3) {
|
showPopupWindow();
|
}
|
}
|
|
@Override
|
public void mouseClicked(MouseEvent event) {
|
if (event.getButton() == MouseEvent.BUTTON1 && event.getClickCount() >= 2) {
|
showStatusDialog();
|
}
|
}
|
});
|
|
try {
|
SystemTray.getSystemTray().add(trayIcon);
|
log.info("系统托盘图标已启动,右键托盘图标可以退出程序(端口 {})", serverPort);
|
} catch (AWTException e) {
|
log.error("添加系统托盘图标失败:{}", e.getMessage(), e);
|
}
|
}
|
|
/**
|
* 在光标位置弹出右键菜单;已经弹着就收起来
|
*/
|
private void showPopupWindow() {
|
Point mouse = MouseInfo.getPointerInfo() == null ? null : MouseInfo.getPointerInfo().getLocation();
|
if (mouse == null) {
|
return;
|
}
|
JWindow window = ensurePopupWindow(mouse);
|
if (window.isVisible()) {
|
hidePopupWindow();
|
return;
|
}
|
Rectangle screen = screenConfigurationOf(mouse).getBounds();
|
Dimension size = window.getSize();
|
// 和系统托盘菜单一样:显示在光标上方,右边缘与光标对齐
|
int x = mouse.x - size.width;
|
int y = mouse.y - size.height - 2;
|
if (y < screen.y) {
|
// 上方放不下(例如任务栏在顶部)就放到光标下方
|
y = mouse.y + 12;
|
}
|
// 保证菜单完整显示在屏幕内
|
x = Math.max(screen.x, Math.min(x, screen.x + screen.width - size.width));
|
y = Math.max(screen.y, Math.min(y, screen.y + screen.height - size.height));
|
|
window.setLocation(x, y);
|
window.setVisible(true);
|
window.toFront();
|
window.requestFocus();
|
startPopupWatchdog(mouse);
|
}
|
|
/**
|
* 收起右键菜单
|
*/
|
private void hidePopupWindow() {
|
stopPopupWatchdog();
|
JWindow window = popupWindow;
|
if (window != null && window.isVisible()) {
|
window.setVisible(false);
|
}
|
}
|
|
/**
|
* 启动兜底收起检查:正常情况下点击别处窗口失焦就会收起,
|
* 万一窗口没抢到焦点(部分环境会这样),靠鼠标离开菜单附近也能收起。
|
*/
|
private void startPopupWatchdog(Point anchor) {
|
stopPopupWatchdog();
|
popupAnchor = anchor;
|
outsideCount = 0;
|
popupWatchdog = new Timer(250, event -> checkPopupDismiss());
|
// 留一点时间给用户从托盘图标移动到菜单上
|
popupWatchdog.setInitialDelay(600);
|
popupWatchdog.start();
|
}
|
|
/**
|
* 兜底检查:光标既不在菜单上、也不在托盘图标附近时,连续两次就收起菜单
|
*/
|
private void checkPopupDismiss() {
|
JWindow window = popupWindow;
|
if (window == null || !window.isVisible()) {
|
stopPopupWatchdog();
|
return;
|
}
|
Point mouse = MouseInfo.getPointerInfo() == null ? null : MouseInfo.getPointerInfo().getLocation();
|
if (mouse == null) {
|
return;
|
}
|
boolean nearMenu = window.getBounds().contains(mouse);
|
boolean nearAnchor = popupAnchor != null && popupAnchor.distance(mouse) < 80;
|
if (nearMenu || nearAnchor) {
|
outsideCount = 0;
|
return;
|
}
|
if (++outsideCount >= 2) {
|
hidePopupWindow();
|
}
|
}
|
|
private void stopPopupWatchdog() {
|
if (popupWatchdog != null) {
|
popupWatchdog.stop();
|
popupWatchdog = null;
|
}
|
outsideCount = 0;
|
}
|
|
/**
|
* 菜单窗口按屏幕创建:多显示器时创建在光标所在的那块屏幕上
|
*/
|
private JWindow ensurePopupWindow(Point mouse) {
|
GraphicsConfiguration configuration = screenConfigurationOf(mouse);
|
if (popupWindow == null || popupWindow.getGraphicsConfiguration() != configuration) {
|
if (popupWindow != null) {
|
popupWindow.dispose();
|
}
|
popupWindow = createPopupWindow(configuration);
|
}
|
return popupWindow;
|
}
|
|
/**
|
* 创建菜单窗口:无边框、置顶、可获取焦点(点别处失焦就能自动收起)
|
*/
|
private JWindow createPopupWindow(GraphicsConfiguration configuration) {
|
Font menuFont = resolveMenuFont();
|
|
JPanel panel = new JPanel();
|
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
|
panel.setBackground(MENU_BACKGROUND);
|
panel.setBorder(BorderFactory.createCompoundBorder(
|
BorderFactory.createLineBorder(MENU_BORDER),
|
BorderFactory.createEmptyBorder(4, 0, 4, 0)));
|
panel.add(createMenuItem("查看串口状态", menuFont, e -> showStatusDialog()));
|
panel.add(createMenuItem("关闭串口监听", menuFont, e -> closeSerialPort()));
|
panel.add(createMenuItem("打开日志目录", menuFont, e -> openLogDirectory()));
|
panel.add(createSeparator());
|
panel.add(createMenuItem("退出", menuFont, e -> exit()));
|
|
// 按 ESC 收起菜单
|
panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
|
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "hidePopupWindow");
|
panel.getActionMap().put("hidePopupWindow", new AbstractAction() {
|
private static final long serialVersionUID = 1L;
|
|
@Override
|
public void actionPerformed(ActionEvent event) {
|
hidePopupWindow();
|
}
|
});
|
|
JWindow window = new JWindow(configuration);
|
window.setContentPane(panel);
|
window.setAlwaysOnTop(true);
|
window.setFocusable(true);
|
// 关键:窗口必须能获得焦点,点菜单外面时才会触发失焦、菜单才会自动收起
|
window.setFocusableWindowState(true);
|
window.addWindowFocusListener(new WindowAdapter() {
|
@Override
|
public void windowLostFocus(WindowEvent event) {
|
hidePopupWindow();
|
}
|
});
|
window.addWindowListener(new WindowAdapter() {
|
@Override
|
public void windowDeactivated(WindowEvent event) {
|
hidePopupWindow();
|
}
|
});
|
window.pack();
|
return window;
|
}
|
|
/**
|
* 菜单项:扁平按钮 + 悬停高亮
|
*/
|
private JButton createMenuItem(String text, Font font, ActionListener actionListener) {
|
JButton item = new JButton(text);
|
item.setFont(font);
|
item.setHorizontalAlignment(SwingConstants.LEFT);
|
item.setBorder(BorderFactory.createEmptyBorder(6, 18, 6, 24));
|
item.setBorderPainted(false);
|
item.setFocusPainted(false);
|
item.setContentAreaFilled(false);
|
item.setOpaque(true);
|
item.setBackground(MENU_BACKGROUND);
|
item.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
item.setMaximumSize(new Dimension(Integer.MAX_VALUE, item.getPreferredSize().height));
|
item.addMouseListener(new MouseAdapter() {
|
@Override
|
public void mouseEntered(MouseEvent event) {
|
item.setBackground(MENU_HOVER_BACKGROUND);
|
}
|
|
@Override
|
public void mouseExited(MouseEvent event) {
|
item.setBackground(MENU_BACKGROUND);
|
}
|
});
|
item.addActionListener(event -> {
|
// 先收起菜单,再执行动作
|
hidePopupWindow();
|
actionListener.actionPerformed(event);
|
});
|
return item;
|
}
|
|
/**
|
* 菜单分隔线
|
*/
|
private JSeparator createSeparator() {
|
JSeparator separator = new JSeparator();
|
separator.setForeground(MENU_BORDER);
|
separator.setMaximumSize(new Dimension(Integer.MAX_VALUE, 1));
|
return separator;
|
}
|
|
/**
|
* @return 光标所在屏幕的显示配置,找不到就用主屏幕
|
*/
|
private GraphicsConfiguration screenConfigurationOf(Point point) {
|
GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment();
|
for (GraphicsDevice device : environment.getScreenDevices()) {
|
GraphicsConfiguration configuration = device.getDefaultConfiguration();
|
if (configuration.getBounds().contains(point)) {
|
return configuration;
|
}
|
}
|
return environment.getDefaultScreenDevice().getDefaultConfiguration();
|
}
|
|
/**
|
* 退出程序:先关闭 Spring 容器(触发 @PreDestroy,关闭串口、停止补帧线程),再结束进程
|
*/
|
private void exit() {
|
log.info("从系统托盘退出程序");
|
removeTrayIcon();
|
int exitCode = SpringApplication.exit(applicationContext, () -> 0);
|
System.exit(exitCode);
|
}
|
|
/**
|
* 项目停止时移除托盘图标、销毁菜单窗口
|
*/
|
@PreDestroy
|
public void destroy() {
|
removeTrayIcon();
|
stopPopupWatchdog();
|
stopStatusRefreshTimer();
|
if (statusDialog != null) {
|
statusDialog.dispose();
|
statusDialog = null;
|
}
|
if (popupWindow != null) {
|
popupWindow.dispose();
|
popupWindow = null;
|
}
|
}
|
|
private void removeTrayIcon() {
|
TrayIcon icon = trayIcon;
|
trayIcon = null;
|
if (icon != null) {
|
SystemTray.getSystemTray().remove(icon);
|
}
|
}
|
|
/**
|
* 弹出采集状态窗口(不是打开浏览器),窗口里的状态每秒自动刷新
|
*/
|
private void showStatusDialog() {
|
if (statusDialog == null) {
|
statusDialog = createStatusDialog();
|
}
|
refreshStatus();
|
statusDialog.setLocationRelativeTo(null);
|
statusDialog.setVisible(true);
|
statusDialog.toFront();
|
}
|
|
/**
|
* 创建串口状态窗口:串口名称 / 监听状态 / 串口连接 / 缓存条数 + 刷新、关闭按钮
|
*/
|
private JDialog createStatusDialog() {
|
Font font = resolveMenuFont();
|
|
JPanel fields = new JPanel(new GridLayout(4, 2, 12, 10));
|
fields.setBackground(MENU_BACKGROUND);
|
serialPortNameValue = addStatusRow(fields, "串口名称", font);
|
listeningValue = addStatusRow(fields, "监听状态", font);
|
portOpenValue = addStatusRow(fields, "串口连接", font);
|
dataSizeValue = addStatusRow(fields, "缓存数据", font);
|
|
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0));
|
buttons.setBackground(MENU_BACKGROUND);
|
JButton refreshButton = new JButton("刷新");
|
refreshButton.setFont(font);
|
refreshButton.addActionListener(event -> refreshStatus());
|
JButton closeButton = new JButton("关闭");
|
closeButton.setFont(font);
|
closeButton.addActionListener(event -> statusDialog.setVisible(false));
|
buttons.add(refreshButton);
|
buttons.add(closeButton);
|
|
JPanel content = new JPanel(new BorderLayout(0, 14));
|
content.setBackground(MENU_BACKGROUND);
|
content.setBorder(BorderFactory.createEmptyBorder(18, 22, 14, 22));
|
content.add(fields, BorderLayout.CENTER);
|
content.add(buttons, BorderLayout.SOUTH);
|
|
JDialog dialog = new JDialog();
|
dialog.setTitle("串口状态");
|
dialog.setModal(false);
|
dialog.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
|
dialog.setContentPane(content);
|
dialog.addWindowListener(new WindowAdapter() {
|
@Override
|
public void windowOpened(WindowEvent event) {
|
refreshStatus();
|
startStatusRefreshTimer();
|
}
|
|
@Override
|
public void windowClosing(WindowEvent event) {
|
// 关闭(其实是隐藏)时停掉自动刷新
|
stopStatusRefreshTimer();
|
}
|
});
|
dialog.pack();
|
dialog.setLocationRelativeTo(null);
|
return dialog;
|
}
|
|
/**
|
* 状态窗口里的一行:左边是标题,右边是值
|
*
|
* @return 值那一列的标签,刷新时直接 setText
|
*/
|
private JLabel addStatusRow(JPanel parent, String title, Font font) {
|
JLabel titleLabel = new JLabel(title + ":", SwingConstants.RIGHT);
|
titleLabel.setFont(font);
|
titleLabel.setForeground(new Color(0x666666));
|
JLabel valueLabel = new JLabel("-");
|
valueLabel.setFont(font);
|
valueLabel.setForeground(new Color(0x222222));
|
parent.add(titleLabel);
|
parent.add(valueLabel);
|
return valueLabel;
|
}
|
|
/**
|
* 刷新状态窗口里的内容
|
*/
|
private void refreshStatus() {
|
if (serialPortNameValue == null) {
|
return;
|
}
|
SerialPortListener listener = serialPortListener();
|
if (listener == null) {
|
serialPortNameValue.setText("-");
|
listeningValue.setText("获取串口监听器失败");
|
portOpenValue.setText("-");
|
dataSizeValue.setText("-");
|
return;
|
}
|
String serialPortName = listener.getListenName();
|
serialPortNameValue.setText(StringUtils.hasText(serialPortName) ? serialPortName : "-");
|
listeningValue.setText(listener.isListening() ? "监听中" : "未监听");
|
portOpenValue.setText(listener.isPortOpen() ? "已打开" : "未打开");
|
dataSizeValue.setText(listener.dataSize() + " 条");
|
}
|
|
/**
|
* @return 串口监听器,取不到时返回 null
|
*/
|
private SerialPortListener serialPortListener() {
|
if (applicationContext == null) {
|
return null;
|
}
|
try {
|
return applicationContext.getBean(SerialPortListener.class);
|
} catch (Exception e) {
|
log.warn("获取串口监听器失败:{}", e.getMessage());
|
return null;
|
}
|
}
|
|
private void startStatusRefreshTimer() {
|
if (statusRefreshTimer == null) {
|
statusRefreshTimer = new Timer(1000, event -> refreshStatus());
|
}
|
statusRefreshTimer.start();
|
}
|
|
private void stopStatusRefreshTimer() {
|
if (statusRefreshTimer != null) {
|
statusRefreshTimer.stop();
|
}
|
}
|
|
/**
|
* 关闭串口监听(只关串口,不退出程序),方便现场直接把串口释放给别的程序用
|
*/
|
private void closeSerialPort() {
|
SerialPortListener listener = serialPortListener();
|
if (listener == null) {
|
showError("获取串口监听器失败,无法关闭串口");
|
return;
|
}
|
String serialPortName = listener.getListenName();
|
serialPortName = StringUtils.hasText(serialPortName) ? serialPortName : "-";
|
if (!listener.isListening() && !listener.isPortOpen()) {
|
notifyMessage("串口 " + serialPortName + " 当前未开启监听");
|
return;
|
}
|
if (listener.closeListening()) {
|
log.info("从系统托盘关闭串口 {} 监听", serialPortName);
|
notifyMessage("串口 " + serialPortName + " 监听已关闭");
|
} else {
|
showError("串口 " + serialPortName + " 关闭失败,请检查串口状态");
|
}
|
}
|
|
/**
|
* 托盘气泡提示,不打断当前操作
|
*/
|
private void notifyMessage(String message) {
|
if (trayIcon == null) {
|
log.info(message);
|
return;
|
}
|
trayIcon.displayMessage("亨旺特导MES数据采集器", message, TrayIcon.MessageType.INFO);
|
}
|
|
/**
|
* 出错时弹窗提示(气泡提示可能被系统设置屏蔽,失败必须让人看见)
|
*/
|
private void showError(String message) {
|
log.warn(message);
|
try {
|
JOptionPane.showMessageDialog(null, message, "数据采集器", JOptionPane.ERROR_MESSAGE);
|
} catch (Exception e) {
|
log.warn("弹出错误提示失败:{}", e.getMessage());
|
}
|
}
|
|
/**
|
* 打开日志目录,方便现场排查问题
|
*/
|
private void openLogDirectory() {
|
try {
|
File directory = new File(StringUtils.hasText(logLocation) ? logLocation : "logs");
|
if (!directory.exists() && !directory.mkdirs()) {
|
log.warn("日志目录不存在且创建失败:{}", directory.getAbsolutePath());
|
}
|
Desktop.getDesktop().open(directory);
|
} catch (Exception e) {
|
log.warn("打开日志目录失败:{}", e.getMessage());
|
}
|
}
|
|
/**
|
* 菜单字体:优先挑一个系统里存在的中文字体
|
*
|
* @return 菜单字体
|
*/
|
private Font resolveMenuFont() {
|
Set<String> fontFamilies = new HashSet<>(Arrays.asList(
|
GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()));
|
for (String name : new String[]{"Microsoft YaHei", "微软雅黑", "SimSun", "宋体"}) {
|
if (fontFamilies.contains(name)) {
|
return new Font(name, Font.PLAIN, 12);
|
}
|
}
|
return new Font(Font.DIALOG, Font.PLAIN, 12);
|
}
|
|
/**
|
* 托盘图标:优先用 classpath 下的 tray.png,没有就现画一个,避免少一个图片文件就跑不起来
|
*/
|
private Image loadTrayImage() {
|
try (InputStream in = getClass().getClassLoader().getResourceAsStream("tray.png")) {
|
if (in != null) {
|
BufferedImage image = ImageIO.read(in);
|
if (image != null) {
|
return image;
|
}
|
}
|
} catch (Exception e) {
|
log.warn("读取托盘图标 tray.png 失败,改用默认图标:{}", e.getMessage());
|
}
|
return createDefaultTrayImage();
|
}
|
|
/**
|
* 默认图标:蓝底白字 S
|
*/
|
private Image createDefaultTrayImage() {
|
BufferedImage image = new BufferedImage(DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE, BufferedImage.TYPE_INT_ARGB);
|
Graphics2D graphics = image.createGraphics();
|
try {
|
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
graphics.setColor(new Color(0x2D8CF0));
|
graphics.fillRoundRect(0, 0, DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE, 8, 8);
|
graphics.setColor(Color.WHITE);
|
graphics.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 20));
|
graphics.drawString("S", 10, 24);
|
} finally {
|
graphics.dispose();
|
}
|
return image;
|
}
|
}
|