Swing介绍及如何打包exe:https://blog.csdn.net/YXWik/article/details/162202223
整体方案说明
主程序:1 个 Swing 打包的 exe(Launcher 启动器)
功能:
可视化配置每个外部 exe 路径、启动参数、工作目录、备注分类
保存配置到本地 json/ini 文件,下次打开自动加载
点击按钮直接调用系统进程启动对应 exe
互不干扰:外部 exe 是独立进程,主程序只是调用,不会嵌入窗口(Swing 无法把别的 exe 窗口内嵌,只能外部唤起)
第一版

package org.swing;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
// 单个外部程序配置实体
class AppConfig implements Serializable {
private String name; // 程序名称
private String exePath; // exe完整路径
private String args; // 启动参数
private String workDir; // 工作目录
// getter setter
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getExePath() { return exePath; }
public void setExePath(String exePath) { this.exePath = exePath; }
public String getArgs() { return args; }
public void setArgs(String args) { this.args = args; }
public String getWorkDir() { return workDir; }
public void setWorkDir(String workDir) { this.workDir = workDir; }
}
// 主启动器窗口
public class ExeLauncher extends JFrame {
private List<AppConfig> configList = new ArrayList<>();
private JPanel btnPanel;
private final File configFile = new File("app_config.dat");
public ExeLauncher() {
setTitle("多程序统一启动管理工具");
setSize(600, 450);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLocationRelativeTo(null);
// 主容器
JPanel root = new JPanel(new BorderLayout(10,10));
root.setBorder(new EmptyBorder(15,15,15,15));
// 顶部操作栏:新增配置、保存配置按钮
JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.LEFT,10,0));
JButton addBtn = new JButton("添加新程序配置");
JButton saveBtn = new JButton("保存所有配置");
topPanel.add(addBtn);
topPanel.add(saveBtn);
// 中间:程序快捷启动按钮区域
btnPanel = new JPanel();
btnPanel.setLayout(new GridLayout(0,3,10,10));
JScrollPane scroll = new JScrollPane(btnPanel);
root.add(topPanel, BorderLayout.NORTH);
root.add(scroll, BorderLayout.CENTER);
add(root);
// 加载本地配置
loadConfig();
refreshButtonList();
// 添加配置弹窗
addBtn.addActionListener(e -> openEditDialog(null));
// 保存配置
saveBtn.addActionListener(e -> saveConfig());
}
// 弹窗:新增/编辑程序配置
private void openEditDialog(AppConfig oldCfg) {
JDialog dialog = new JDialog(this, "配置外部EXE", true);
dialog.setSize(420, 300);
dialog.setLayout(new GridLayout(5,2,8,8));
dialog.setLocationRelativeTo(this);
JTextField nameField = new JTextField();
JTextField pathField = new JTextField();
JTextField argField = new JTextField();
JTextField dirField = new JTextField();
// 编辑回填旧数据
if(oldCfg != null) {
nameField.setText(oldCfg.getName());
pathField.setText(oldCfg.getExePath());
argField.setText(oldCfg.getArgs());
dirField.setText(oldCfg.getWorkDir());
}
dialog.add(new JLabel("程序名称:"));
dialog.add(nameField);
dialog.add(new JLabel("EXE路径:"));
JPanel pathBox = new JPanel(new BorderLayout());
pathBox.add(pathField, BorderLayout.CENTER);
JButton selectExe = new JButton("选择");
pathBox.add(selectExe, BorderLayout.EAST);
dialog.add(pathBox);
dialog.add(new JLabel("启动参数(空格分隔):"));
dialog.add(argField);
dialog.add(new JLabel("工作目录:"));
JPanel dirBox = new JPanel(new BorderLayout());
dirBox.add(dirField, BorderLayout.CENTER);
JButton selectDir = new JButton("选择");
dirBox.add(selectDir, BorderLayout.EAST);
dialog.add(dirBox);
// 选择exe文件
selectExe.addActionListener(ev -> {
JFileChooser chooser = new JFileChooser();
chooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("可执行程序exe", "exe"));
int res = chooser.showOpenDialog(dialog);
if(res == JFileChooser.APPROVE_OPTION) {
pathField.setText(chooser.getSelectedFile().getAbsolutePath());
// 自动填充工作目录
dirField.setText(chooser.getSelectedFile().getParent());
}
});
// 选择文件夹
selectDir.addActionListener(ev -> {
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
int res = chooser.showOpenDialog(dialog);
if(res == JFileChooser.APPROVE_OPTION) {
dirField.setText(chooser.getSelectedFile().getAbsolutePath());
}
});
// 确认按钮
JButton confirm = new JButton("确认保存");
dialog.add(new JLabel());
dialog.add(confirm);
confirm.addActionListener(ev -> {
String name = nameField.getText().trim();
String exePath = pathField.getText().trim();
if(name.isEmpty() || exePath.isEmpty()) {
JOptionPane.showMessageDialog(dialog, "名称和exe路径不能为空");
return;
}
AppConfig cfg = oldCfg == null ? new AppConfig() : oldCfg;
cfg.setName(name);
cfg.setExePath(exePath);
cfg.setArgs(argField.getText().trim());
cfg.setWorkDir(dirField.getText().trim());
if(oldCfg == null) configList.add(cfg);
refreshButtonList();
dialog.dispose();
});
dialog.setVisible(true);
}
// 刷新所有启动按钮
private void refreshButtonList() {
btnPanel.removeAll();
for(AppConfig cfg : configList) {
JPanel itemPanel = new JPanel(new BorderLayout(5,5));
JButton runBtn = new JButton("启动:" + cfg.getName());
JButton editBtn = new JButton("编辑");
JButton delBtn = new JButton("删除");
// 启动程序
runBtn.addActionListener(e -> startExeAsync(cfg));
// 编辑配置
editBtn.addActionListener(e -> openEditDialog(cfg));
// 删除配置
delBtn.addActionListener(e -> {
configList.remove(cfg);
refreshButtonList();
});
JPanel opPanel = new JPanel();
opPanel.add(editBtn);
opPanel.add(delBtn);
itemPanel.add(runBtn, BorderLayout.CENTER);
itemPanel.add(opPanel, BorderLayout.SOUTH);
btnPanel.add(itemPanel);
}
btnPanel.updateUI();
}
// 异步启动exe,不阻塞界面
private void startExeAsync(AppConfig cfg) {
new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
List<String> cmd = new ArrayList<>();
cmd.add(cfg.getExePath());
// 分割启动参数
String argsStr = cfg.getArgs();
if(!argsStr.isEmpty()) {
String[] argsArr = argsStr.split(" ");
for(String arg : argsArr) cmd.add(arg);
}
ProcessBuilder pb = new ProcessBuilder(cmd);
// 设置工作目录
if(!cfg.getWorkDir().isEmpty()) {
pb.directory(new File(cfg.getWorkDir()));
}
// 启动外部exe
pb.start();
return null;
}
@Override
protected void done() {
try {
get();
JOptionPane.showMessageDialog(null, cfg.getName() + " 已启动");
} catch (Exception ex) {
JOptionPane.showMessageDialog(null, "启动失败:" + ex.getMessage());
ex.printStackTrace();
}
}
}.execute();
}
// 保存配置到本地文件
private void saveConfig() {
try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(configFile))) {
oos.writeObject(configList);
JOptionPane.showMessageDialog(this, "配置保存成功!下次打开自动加载");
} catch (Exception e) {
JOptionPane.showMessageDialog(this, "保存失败:" + e.getMessage());
}
}
// 读取本地配置
private void loadConfig() {
if(!configFile.exists()) return;
try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(configFile))) {
Object obj = ois.readObject();
if(obj instanceof List<?>) {
configList = (List<AppConfig>) obj;
}
} catch (Exception ignored) {}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new ExeLauncher().setVisible(true));
}
}
第二版
优化了卡片大小,并且携带了原exe的图标,增加了一件启动的功能

package org.swing;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import sun.awt.shell.ShellFolder;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
// 单个程序配置实体
class AppConfig implements Serializable {
private String name;
private String exePath;
private String args;
private String workDir;
private String iconCachePath;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getExePath() { return exePath; }
public void setExePath(String exePath) { this.exePath = exePath; }
public String getArgs() { return args; }
public void setArgs(String args) { this.args = args; }
public String getWorkDir() { return workDir; }
public void setWorkDir(String workDir) { this.workDir = workDir; }
public String getIconCachePath() { return iconCachePath; }
public void setIconCachePath(String iconCachePath) { this.iconCachePath = iconCachePath; }
}
class AppCard extends JPanel {
private final AppConfig cfg;
private final Runnable runCb, editCb, delCb;
private JLabel iconLabel;
// 固定原生提取尺寸32px,不放大,零模糊
private static final int RAW_ICON_SIZE = 32;
public AppCard(AppConfig cfg, Runnable runCb, Runnable editCb, Runnable delCb) {
this.cfg = cfg;
this.runCb = runCb;
this.editCb = editCb;
this.delCb = delCb;
// 卡片整体尺寸:宽度增加,高度增加,空间更舒服
setPreferredSize(new Dimension(160, 195));
setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
setLayout(new BorderLayout(8, 10));
setBorder(new EmptyBorder(12, 12, 12, 12));
// 顶部图标区域:32px 原生图标,不放大
iconLabel = new JLabel();
iconLabel.setHorizontalAlignment(SwingConstants.CENTER);
iconLabel.setPreferredSize(new Dimension(32, 32));
loadExeIcon();
// 图标外层容器,让图标上下居中、不贴边
JPanel iconPanel = new JPanel(new BorderLayout());
iconPanel.setOpaque(false);
iconPanel.setPreferredSize(new Dimension(32, 46));
iconPanel.add(iconLabel, BorderLayout.CENTER);
add(iconPanel, BorderLayout.NORTH);
// 中间程序名称
JLabel nameLabel = new JLabel(cfg.getName(), SwingConstants.CENTER);
nameLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12));
nameLabel.setForeground(Color.BLACK);
JPanel namePanel = new JPanel(new BorderLayout());
namePanel.setOpaque(false);
namePanel.setPreferredSize(new Dimension(160, 36));
namePanel.add(nameLabel, BorderLayout.CENTER);
add(namePanel, BorderLayout.CENTER);
// 底部三个按钮
JPanel btnPanel = new JPanel(new GridLayout(1, 3, 4, 0));
btnPanel.setPreferredSize(new Dimension(160, 32));
JButton runBtn = new JButton("启动");
JButton editBtn = new JButton("编辑");
JButton delBtn = new JButton("删除");
Font btnFont = new Font("微软雅黑", Font.PLAIN, 10);
runBtn.setFont(btnFont);
editBtn.setFont(btnFont);
delBtn.setFont(btnFont);
Insets zeroMargin = new Insets(0, 0, 0, 0);
runBtn.setMargin(zeroMargin);
editBtn.setMargin(zeroMargin);
delBtn.setMargin(zeroMargin);
runBtn.addActionListener(e -> runCb.run());
editBtn.addActionListener(e -> editCb.run());
delBtn.addActionListener(e -> delCb.run());
btnPanel.add(runBtn);
btnPanel.add(editBtn);
btnPanel.add(delBtn);
add(btnPanel, BorderLayout.SOUTH);
}
// 加载图标:缓存存在直接显示原图,不做拉伸
private void loadExeIcon() {
String cachePath = cfg.getIconCachePath();
File cacheFile = null;
if (cachePath != null && !cachePath.isBlank()) {
cacheFile = new File(cachePath);
}
// 读取缓存原图,完全不缩放,原生清晰度
if (cacheFile != null && cacheFile.exists()) {
ImageIcon rawIcon = new ImageIcon(cacheFile.getAbsolutePath());
iconLabel.setIcon(rawIcon);
return;
}
// 后台执行提取
new SwingWorker<ImageIcon, Void>() {
@Override
protected ImageIcon doInBackground() throws Exception {
return extractIconOnlyPowershell(cfg.getExePath());
}
@Override
protected void done() {
try {
ImageIcon rawIcon = get();
iconLabel.setIcon(rawIcon);
} catch (Exception e) {
e.printStackTrace();
iconLabel.setIcon(getDefaultIcon());
}
}
}.execute();
}
// 纯PowerShell提取,无任何第三方依赖,稳定兼容所有Windows
private ImageIcon extractIconOnlyPowershell(String exePath) throws Exception {
File exeFile = new File(exePath);
if (!exeFile.exists()) {
throw new IOException("EXE文件不存在");
}
File cacheDir = new File(".icon_cache");
if (!cacheDir.exists()) cacheDir.mkdirs();
String cacheFileName = Math.abs(exePath.hashCode()) + ".png";
File outPng = new File(cacheDir, cacheFileName);
cfg.setIconCachePath(outPng.getAbsolutePath());
// 静默执行PowerShell提取
String psCmd = String.format(
"Add-Type -AssemblyName System.Drawing;" +
"$file = '%s';" +
"$icon = [System.Drawing.Icon]::ExtractAssociatedIcon($file);" +
"$bmp = $icon.ToBitmap();" +
"$bmp.Save('%s', [System.Drawing.Imaging.ImageFormat]::Png)",
exePath.replace("'", "''"),
outPng.getAbsolutePath()
);
ProcessBuilder pb = new ProcessBuilder("powershell", "-WindowStyle", "Hidden", "-Command", psCmd);
pb.redirectErrorStream(true);
Process proc = pb.start();
proc.waitFor();
// 判断图标文件是否生成成功
if (!outPng.exists() || outPng.length() < 100) {
// 提取失败兜底系统默认文件图标
return (ImageIcon) UIManager.getIcon("FileView.fileIcon");
}
// 直接返回32px原图,不做任何放大
return new ImageIcon(outPng.getAbsolutePath());
}
// 默认占位图标,同步32px尺寸
private ImageIcon getDefaultIcon() {
BufferedImage img = new BufferedImage(RAW_ICON_SIZE, RAW_ICON_SIZE, BufferedImage.TYPE_4BYTE_ABGR);
Graphics2D g = img.createGraphics();
g.setColor(Color.LIGHT_GRAY);
g.fillRect(0, 0, RAW_ICON_SIZE, RAW_ICON_SIZE);
g.setColor(Color.DARK_GRAY);
g.setFont(new Font("微软雅黑", Font.BOLD, 12));
g.drawString("EXE", 6, 22);
g.dispose();
return new ImageIcon(img);
}
}
// 主窗口(保留一键启动全部功能)
public class ExeLauncher extends JFrame {
private List<AppConfig> configList = new ArrayList<>();
private JPanel cardPanel;
private final File configFile = new File("app_config.dat");
public ExeLauncher() {
setTitle("多程序统一启动管理工具");
setSize(900, 600);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLocationRelativeTo(null);
JPanel root = new JPanel(new BorderLayout(10,10));
root.setBorder(new EmptyBorder(15,15,15,15));
// 顶部按钮栏
JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.LEFT,10,0));
JButton addBtn = new JButton("添加新程序配置");
JButton saveBtn = new JButton("保存所有配置");
JButton startAllBtn = new JButton("一键启动全部程序");
startAllBtn.setBackground(new Color(150, 220, 150));
topPanel.add(addBtn);
topPanel.add(saveBtn);
topPanel.add(startAllBtn);
// 卡片流式布局
cardPanel = new JPanel();
cardPanel.setLayout(new FlowLayout(FlowLayout.LEFT,12,12));
JScrollPane scroll = new JScrollPane(cardPanel);
scroll.getVerticalScrollBar().setUnitIncrement(15);
root.add(topPanel, BorderLayout.NORTH);
root.add(scroll, BorderLayout.CENTER);
add(root);
loadConfig();
refreshCardList();
addBtn.addActionListener(e -> openEditDialog(null));
saveBtn.addActionListener(e -> saveConfig());
startAllBtn.addActionListener(e -> startAllExeAsync());
}
// 一键批量启动所有程序
private void startAllExeAsync() {
if (configList.isEmpty()) {
JOptionPane.showMessageDialog(this, "暂无配置的程序,请先添加!");
return;
}
new SwingWorker<Integer, String>() {
int successCount = 0;
int failCount = 0;
StringBuilder failMsg = new StringBuilder();
@Override
protected Integer doInBackground() throws Exception {
for (AppConfig cfg : configList) {
try {
List<String> cmd = new ArrayList<>();
cmd.add(cfg.getExePath());
String argsStr = cfg.getArgs();
if(!argsStr.isBlank()) {
cmd.addAll(List.of(argsStr.split(" ")));
}
ProcessBuilder pb = new ProcessBuilder(cmd);
if(!cfg.getWorkDir().isBlank()) {
pb.directory(new File(cfg.getWorkDir()));
}
pb.start();
successCount++;
publish(cfg.getName() + " 启动成功");
Thread.sleep(300);
} catch (Exception ex) {
failCount++;
failMsg.append(cfg.getName()).append(":").append(ex.getMessage()).append("\n");
publish(cfg.getName() + " 启动失败");
}
}
return successCount;
}
@Override
protected void done() {
String tip = String.format("批量启动完成!\n成功:%d 个\n失败:%d 个", successCount, failCount);
if (failCount > 0) {
tip += "\n失败列表:\n" + failMsg;
JOptionPane.showMessageDialog(null, tip, "批量启动结果", JOptionPane.WARNING_MESSAGE);
} else {
JOptionPane.showMessageDialog(null, tip);
}
}
}.execute();
}
// 新增/编辑弹窗
private void openEditDialog(AppConfig oldCfg) {
JDialog dialog = new JDialog(this, "配置外部EXE", true);
dialog.setSize(440, 320);
dialog.setLayout(new GridLayout(5,2,8,8));
dialog.setLocationRelativeTo(this);
JTextField nameField = new JTextField();
JTextField pathField = new JTextField();
JTextField argField = new JTextField();
JTextField dirField = new JTextField();
if(oldCfg != null) {
nameField.setText(oldCfg.getName());
pathField.setText(oldCfg.getExePath());
argField.setText(oldCfg.getArgs());
dirField.setText(oldCfg.getWorkDir());
}
dialog.add(new JLabel("程序名称:"));
dialog.add(nameField);
dialog.add(new JLabel("EXE文件路径:"));
JPanel pathBox = new JPanel(new BorderLayout());
pathBox.add(pathField);
JButton selectExe = new JButton("选择EXE");
pathBox.add(selectExe, BorderLayout.EAST);
dialog.add(pathBox);
dialog.add(new JLabel("启动参数:"));
dialog.add(argField);
dialog.add(new JLabel("运行工作目录:"));
JPanel dirBox = new JPanel(new BorderLayout());
dirBox.add(dirField);
JButton selectDir = new JButton("选择文件夹");
dirBox.add(selectDir, BorderLayout.EAST);
dialog.add(dirBox);
selectExe.addActionListener(ev -> {
JFileChooser chooser = new JFileChooser();
chooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("可执行程序(*.exe)", "exe"));
int res = chooser.showOpenDialog(dialog);
if(res == JFileChooser.APPROVE_OPTION) {
File exe = chooser.getSelectedFile();
pathField.setText(exe.getAbsolutePath());
dirField.setText(exe.getParent());
}
});
selectDir.addActionListener(ev -> {
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
int res = chooser.showOpenDialog(dialog);
if(res == JFileChooser.APPROVE_OPTION) {
dirField.setText(chooser.getSelectedFile().getAbsolutePath());
}
});
JButton confirm = new JButton("确认保存");
dialog.add(new JLabel());
dialog.add(confirm);
confirm.addActionListener(ev -> {
String name = nameField.getText().trim();
String exePath = pathField.getText().trim();
if(name.isEmpty() || exePath.isEmpty()) {
JOptionPane.showMessageDialog(dialog, "程序名称和EXE路径不能为空!");
return;
}
AppConfig cfg = oldCfg == null ? new AppConfig() : oldCfg;
cfg.setName(name);
cfg.setExePath(exePath);
cfg.setArgs(argField.getText().trim());
cfg.setWorkDir(dirField.getText().trim());
// 修改exe路径清空旧图标缓存,重新提取高清图标
if(oldCfg != null && !exePath.equals(oldCfg.getExePath())) {
cfg.setIconCachePath(null);
}
if(oldCfg == null) configList.add(cfg);
refreshCardList();
dialog.dispose();
});
dialog.setVisible(true);
}
// 刷新卡片列表
private void refreshCardList() {
cardPanel.removeAll();
for(AppConfig cfg : configList) {
Runnable run = () -> startExeAsync(cfg);
Runnable edit = () -> openEditDialog(cfg);
Runnable del = () -> {
configList.remove(cfg);
refreshCardList();
};
AppCard card = new AppCard(cfg, run, edit, del);
cardPanel.add(card);
}
cardPanel.revalidate();
cardPanel.repaint();
}
// 单个程序异步启动
private void startExeAsync(AppConfig cfg) {
new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
List<String> cmd = new ArrayList<>();
cmd.add(cfg.getExePath());
String argsStr = cfg.getArgs();
if(!argsStr.isBlank()) {
cmd.addAll(List.of(argsStr.split(" ")));
}
ProcessBuilder pb = new ProcessBuilder(cmd);
if(!cfg.getWorkDir().isBlank()) {
pb.directory(new File(cfg.getWorkDir()));
}
pb.start();
return null;
}
@Override
protected void done() {
try {
get();
JOptionPane.showMessageDialog(null, cfg.getName() + " 已启动");
} catch (Exception ex) {
JOptionPane.showMessageDialog(null, "启动失败:" + ex.getMessage());
}
}
}.execute();
}
// 保存配置
private void saveConfig() {
try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(configFile))) {
oos.writeObject(configList);
JOptionPane.showMessageDialog(this, "配置保存成功,图标已缓存!");
} catch (Exception e) {
JOptionPane.showMessageDialog(this, "保存失败:" + e.getMessage());
}
}
// 加载配置
private void loadConfig() {
if(!configFile.exists()) return;
try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(configFile))) {
Object obj = ois.readObject();
if(obj instanceof List<?>) {
configList = (List<AppConfig>) obj;
}
} catch (Exception ignored) {}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new ExeLauncher().setVisible(true));
}
}
第三版
增加程序运行状态检测展示(是否正在运行)
增加自定义修改配置文件位置功能

package org.swing;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
class AppConfig implements Serializable {
private String name;
private String exePath;
private String iconCachePath;
public AppConfig(String name, String exePath) {
this.name = name;
this.exePath = exePath;
this.iconCachePath = "";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getExePath() {
return exePath;
}
public void setExePath(String exePath) {
this.exePath = exePath;
}
public String getIconCachePath() {
return iconCachePath;
}
public void setIconCachePath(String iconCachePath) {
this.iconCachePath = iconCachePath;
}
}
class AppCard extends JPanel {
private final AppConfig cfg;
private final Runnable runCb, editCb, delCb;
private JLabel iconLabel;
private JLabel nameLabel;
private JLabel statusLabel;
private static final int RAW_ICON_SIZE = 32;
private static final int POWERSHELL_TIMEOUT = 3000;
private static final int NAME_MAX_WIDTH = 120;
private boolean isRunning = false;
private final String fullAppName;
private static final Color BTN_NORMAL_BG = new Color(245, 245, 245);
private static final Color BTN_HOVER_BG = new Color(220, 235, 255);
private static final Color BTN_PRESS_BG = new Color(190, 215, 245);
private static final Color BTN_BORDER = new Color(200, 200, 200);
private static final int BTN_RADIUS = 6;
public AppCard(AppConfig cfg, Runnable runCb, Runnable editCb, Runnable delCb) {
this.cfg = cfg;
this.runCb = runCb;
this.editCb = editCb;
this.delCb = delCb;
this.fullAppName = cfg.getName();
setPreferredSize(new Dimension(160, 220));
setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
setBorder(new EmptyBorder(8, 8, 8, 8));
BoxLayout verticalLayout = new BoxLayout(this, BoxLayout.Y_AXIS);
setLayout(verticalLayout);
int rowGap = 2;
// 1 名称行
JPanel nameRow = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
nameRow.setOpaque(false);
nameLabel = new JLabel();
nameLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12));
nameLabel.setForeground(Color.BLACK);
nameLabel.setPreferredSize(new Dimension(NAME_MAX_WIDTH, 18));
nameLabel.setText(getTruncatedText(fullAppName, NAME_MAX_WIDTH));
nameLabel.setToolTipText(fullAppName);
nameRow.add(nameLabel);
add(nameRow);
add(Box.createVerticalStrut(rowGap));
// 2 图标行
JPanel iconRow = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
iconRow.setOpaque(false);
iconLabel = new JLabel();
iconLabel.setHorizontalAlignment(SwingConstants.CENTER);
iconLabel.setPreferredSize(new Dimension(32, 32));
loadExeIcon();
iconRow.add(iconLabel);
add(iconRow);
add(Box.createVerticalStrut(rowGap));
// 3 状态行
JPanel statusRow = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
statusRow.setOpaque(false);
statusLabel = new JLabel("未运行");
statusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 11));
statusLabel.setForeground(Color.GRAY);
statusRow.add(statusLabel);
add(statusRow);
add(Box.createVerticalStrut(rowGap));
// 4 按钮行
JPanel btnRow = new JPanel(new GridLayout(1, 3, 6, 0));
btnRow.setPreferredSize(new Dimension(160, 34));
JButton runBtn = createStyleBtn("启动");
JButton editBtn = createStyleBtn("编辑");
JButton delBtn = createStyleBtn("删除");
runBtn.addActionListener(e -> runCb.run());
editBtn.addActionListener(e -> editCb.run());
delBtn.addActionListener(e -> {
int opt = JOptionPane.showConfirmDialog(this, "确认删除该程序配置?", "提示", JOptionPane.YES_NO_OPTION);
if (opt == JOptionPane.YES_OPTION) delCb.run();
});
btnRow.add(runBtn);
btnRow.add(editBtn);
btnRow.add(delBtn);
add(btnRow);
}
private JButton createStyleBtn(String text) {
JButton btn = new JButton(text) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int w = getWidth();
int h = getHeight();
g2.setColor(getBackground());
g2.fillRoundRect(0, 0, w, h, BTN_RADIUS, BTN_RADIUS);
g2.setColor(BTN_BORDER);
g2.drawRoundRect(0, 0, w - 1, h - 1, BTN_RADIUS, BTN_RADIUS);
FontMetrics fm = g2.getFontMetrics();
int textX = (w - fm.stringWidth(getText())) / 2;
int textY = (h - fm.getHeight()) / 2 + fm.getAscent();
g2.setColor(getForeground());
g2.drawString(getText(), textX, textY);
g2.dispose();
}
};
btn.setFont(new Font("微软雅黑", Font.PLAIN, 10));
btn.setFocusPainted(false);
btn.setBorderPainted(false);
btn.setBackground(BTN_NORMAL_BG);
btn.setMargin(new Insets(4, 2, 4, 2));
btn.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
btn.setBackground(BTN_HOVER_BG);
}
@Override
public void mouseExited(MouseEvent e) {
btn.setBackground(BTN_NORMAL_BG);
}
@Override
public void mousePressed(MouseEvent e) {
btn.setBackground(BTN_PRESS_BG);
}
@Override
public void mouseReleased(MouseEvent e) {
btn.setBackground(BTN_HOVER_BG);
}
});
return btn;
}
private String getTruncatedText(String text, int limitWidth) {
if (text == null || text.isBlank()) return text;
FontMetrics fm = nameLabel.getFontMetrics(nameLabel.getFont());
if (fm.stringWidth(text) <= limitWidth) return text;
StringBuilder sb = new StringBuilder(text);
while (sb.length() > 0 && fm.stringWidth(sb + "...") > limitWidth) {
sb.deleteCharAt(sb.length() - 1);
}
return sb + "...";
}
public void setRunningStatus(boolean running) {
this.isRunning = running;
if (running) {
statusLabel.setText("● 运行中");
statusLabel.setForeground(new Color(0, 160, 0));
} else {
statusLabel.setText("未运行");
statusLabel.setForeground(Color.GRAY);
}
repaint();
}
private void loadExeIcon() {
String cachePath = cfg.getIconCachePath();
File cacheFile = null;
if (cachePath != null && !cachePath.isBlank()) {
cacheFile = new File(cachePath);
}
if (cacheFile != null && cacheFile.exists() && cacheFile.length() > 100) {
ImageIcon rawIcon = new ImageIcon(cacheFile.getAbsolutePath());
iconLabel.setIcon(rawIcon);
return;
}
new SwingWorker<ImageIcon, Void>() {
@Override
protected ImageIcon doInBackground() throws Exception {
return extractIconOnlyPowershell(cfg.getExePath());
}
@Override
protected void done() {
try {
ImageIcon rawIcon = get();
iconLabel.setIcon(rawIcon);
} catch (Exception e) {
iconLabel.setIcon(getDefaultIcon());
}
}
}.execute();
}
private ImageIcon extractIconOnlyPowershell(String exePath) throws Exception {
File exeFile = new File(exePath);
if (!exeFile.exists()) throw new IOException("EXE文件不存在");
File cacheDir = new File(".icon_cache");
if (!cacheDir.exists()) cacheDir.mkdirs();
String cacheFileName = Math.abs(exePath.hashCode()) + ".png";
File outPng = new File(cacheDir, cacheFileName);
cfg.setIconCachePath(outPng.getAbsolutePath());
String safeExePath = exePath.replace("'", "''");
String safePngPath = outPng.getAbsolutePath().replace("'", "''");
String psCmd = String.format(
"Add-Type -AssemblyName System.Drawing;" +
"$file = '%s';" +
"$icon = [System.Drawing.Icon]::ExtractAssociatedIcon($file);" +
"if ($icon -eq $null) { exit 1 }" +
"$bmp = $icon.ToBitmap();" +
"$bmp.Save('%s', [System.Drawing.Imaging.ImageFormat]::Png)",
safeExePath, safePngPath
);
ProcessBuilder pb = new ProcessBuilder("powershell", "-WindowStyle", "Hidden", "-Command", psCmd);
pb.redirectErrorStream(true);
Process proc = pb.start();
boolean exit = proc.waitFor(POWERSHELL_TIMEOUT, TimeUnit.SECONDS);
if (!exit) {
proc.destroy();
throw new Exception("图标提取超时");
}
try (BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()))) {
br.lines().forEach(l -> {});
}
if (!outPng.exists() || outPng.length() < 100) {
return getDefaultIcon();
}
return new ImageIcon(outPng.getAbsolutePath());
}
private ImageIcon getDefaultIcon() {
BufferedImage img = new BufferedImage(RAW_ICON_SIZE, RAW_ICON_SIZE, BufferedImage.TYPE_4BYTE_ABGR);
Graphics2D g = img.createGraphics();
try {
g.setColor(Color.LIGHT_GRAY);
g.fillRect(0, 0, RAW_ICON_SIZE, RAW_ICON_SIZE);
g.setColor(Color.DARK_GRAY);
g.setFont(new Font("微软雅黑", Font.BOLD, 12));
g.drawString("EXE", 6, 22);
} finally {
g.dispose();
}
return new ImageIcon(img);
}
public AppConfig getConfig() {
return cfg;
}
public boolean isRunning() {
return this.isRunning;
}
}
// 主窗口
public class ExeLauncher extends JFrame {
private List<AppConfig> configList = new ArrayList<>();
private List<AppCard> cardList = new ArrayList<>();
private JPanel cardPanel;
private File configFile;
// 默认配置文件名
private static final String DEFAULT_CONFIG_NAME = "app_config.dat";
// 路径记录文件:记住上次使用的配置文件路径
private static final String PATH_RECORD_FILE = "config_path.lock";
private ScheduledExecutorService statusChecker;
public ExeLauncher() {
// 启动时读取上次保存的配置文件路径
loadLastUsedConfigPath();
setTitle("多程序统一启动管理工具");
setSize(1400, 800);
setDefaultCloseOperation(EXIT_ON_CLOSE);
// 窗口关闭前,保存当前配置文件路径
addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent e) {
saveCurrentConfigPathToRecord();
}
});
setLocationRelativeTo(null);
JPanel rootPanel = new JPanel(new BorderLayout(10, 10));
rootPanel.setBorder(new EmptyBorder(12, 12, 12, 12));
JPanel topBtnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0));
JButton btnAdd = new JButton("添加新程序配置");
JButton btnSaveCurrent = new JButton("保存到当前配置文件");
JButton btnChangeConfigFile = new JButton("修改配置文件");
JButton btnBatchStart = new JButton("一键启动全部程序");
JButton btnRefreshStatus = new JButton("刷新运行状态");
JButton btnClearCache = new JButton("清理图标缓存");
btnBatchStart.setBackground(new Color(180, 240, 180));
btnClearCache.setBackground(new Color(255, 200, 200));
btnRefreshStatus.setBackground(new Color(200, 225, 255));
topBtnPanel.add(btnAdd);
topBtnPanel.add(btnSaveCurrent);
topBtnPanel.add(btnChangeConfigFile);
topBtnPanel.add(btnBatchStart);
topBtnPanel.add(btnRefreshStatus);
topBtnPanel.add(btnClearCache);
cardPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 15));
JScrollPane scrollPane = new JScrollPane(cardPanel);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
rootPanel.add(topBtnPanel, BorderLayout.NORTH);
rootPanel.add(scrollPane, BorderLayout.CENTER);
getContentPane().add(rootPanel);
// 全部按钮事件绑定完整
btnAdd.addActionListener(e -> openEditDialog(null));
btnSaveCurrent.addActionListener(e -> saveConfigCurrent());
btnChangeConfigFile.addActionListener(e -> openSetConfigPathDialog());
btnBatchStart.addActionListener(e -> batchStartAll());
btnRefreshStatus.addActionListener(e -> refreshAllStatus());
btnClearCache.addActionListener(e -> clearIconCache());
loadConfig();
refreshCardList();
startStatusCheckTask();
}
// 读取上次程序关闭时使用的配置文件路径
private void loadLastUsedConfigPath() {
File recordFile = new File(PATH_RECORD_FILE);
// 记录文件不存在,使用默认文件
if (!recordFile.exists()) {
configFile = new File(DEFAULT_CONFIG_NAME);
return;
}
// 读取存储的路径
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(recordFile), StandardCharsets.UTF_8))) {
String savedPath = br.readLine();
if (savedPath != null && !savedPath.isBlank()) {
File targetFile = new File(savedPath);
// 路径文件存在,直接使用;不存在则回退默认
if (targetFile.getParentFile() != null && targetFile.getParentFile().exists()) {
configFile = targetFile;
} else {
configFile = new File(DEFAULT_CONFIG_NAME);
}
} else {
configFile = new File(DEFAULT_CONFIG_NAME);
}
} catch (Exception e) {
// 读取失败, fallback 默认
configFile = new File(DEFAULT_CONFIG_NAME);
}
}
// 将当前正在使用的配置文件路径写入记录文件(关闭窗口/切换文件时调用)
private void saveCurrentConfigPathToRecord() {
try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(PATH_RECORD_FILE), StandardCharsets.UTF_8))) {
bw.write(configFile.getAbsolutePath());
} catch (Exception ignored) {
}
}
// 修改配置文件弹窗:切换完成后自动更新路径记录
private void openSetConfigPathDialog() {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("修改配置文件(切换存储路径)");
chooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("配置文件(*.dat)", "dat"));
chooser.setApproveButtonText("切换并迁移配置");
File currentFile = configFile;
File parentDir = currentFile.getParentFile();
if (parentDir != null && parentDir.exists()) {
chooser.setCurrentDirectory(parentDir);
} else {
chooser.setCurrentDirectory(new File("."));
}
chooser.setSelectedFile(currentFile);
int selectCode = chooser.showSaveDialog(this);
if (selectCode != JFileChooser.APPROVE_OPTION) return;
File newTargetFile = chooser.getSelectedFile();
if (!newTargetFile.getName().endsWith(".dat")) {
newTargetFile = new File(newTargetFile.getAbsolutePath() + ".dat");
}
if (newTargetFile.getAbsolutePath().equals(configFile.getAbsolutePath())) {
JOptionPane.showMessageDialog(this, "当前已是正在使用的配置文件,无需切换");
return;
}
// 迁移全部数据到新文件
saveConfigToFile(newTargetFile);
// 切换全局文件对象
this.configFile = newTargetFile;
// 立刻更新路径记录文件,下次开机自动读取
saveCurrentConfigPathToRecord();
// 重新加载、刷新UI
loadConfig();
refreshCardList();
JOptionPane.showMessageDialog(this, "配置文件切换完成!\n新路径:" + configFile.getAbsolutePath());
}
private void saveConfigCurrent() {
saveConfigToFile(configFile);
JOptionPane.showMessageDialog(this, "保存成功!\n当前配置文件:" + configFile.getAbsolutePath());
}
private void saveConfigToFile(File target) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(target))) {
oos.writeObject(configList);
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "写入配置失败:" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
}
private void loadConfig() {
if (!configFile.exists()) {
configList = new ArrayList<>();
return;
}
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(configFile))) {
Object obj = ois.readObject();
if (obj instanceof List<?>) {
configList = (List<AppConfig>) obj;
}
} catch (Exception e) {
configList = new ArrayList<>();
}
}
private void refreshCardList() {
cardPanel.removeAll();
cardList.clear();
for (AppConfig cfg : configList) {
AppCard card = new AppCard(cfg,
() -> launchExe(cfg),
() -> openEditDialog(cfg),
() -> deleteConfig(cfg)
);
cardList.add(card);
cardPanel.add(card);
}
cardPanel.revalidate();
cardPanel.repaint();
refreshAllStatus();
}
private void launchExe(AppConfig cfg) {
File exe = new File(cfg.getExePath());
if (!exe.exists()) {
JOptionPane.showMessageDialog(this, "程序路径不存在:" + cfg.getExePath());
return;
}
try {
new ProcessBuilder(exe.getAbsolutePath()).start();
} catch (Exception e) {
JOptionPane.showMessageDialog(this, "启动失败:" + e.getMessage());
}
}
// 编辑弹窗
private void openEditDialog(AppConfig editTarget) {
JDialog dialog = new JDialog(this, "编辑程序配置", true);
dialog.setSize(400, 220);
dialog.setLayout(new BorderLayout(10, 10));
dialog.setLocationRelativeTo(this);
JPanel inputPanel = new JPanel(new GridLayout(2, 2, 8, 8));
JTextField txtName = new JTextField();
JTextField txtPath = new JTextField();
JButton btnSelect = new JButton("选择EXE");
if (editTarget != null) {
txtName.setText(editTarget.getName());
txtPath.setText(editTarget.getExePath());
}
inputPanel.add(new JLabel("程序名称:"));
inputPanel.add(txtName);
inputPanel.add(new JLabel("EXE路径:"));
JPanel pathPanel = new JPanel(new BorderLayout(4, 0));
pathPanel.add(txtPath, BorderLayout.CENTER);
pathPanel.add(btnSelect, BorderLayout.EAST);
inputPanel.add(pathPanel);
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton btnOk = new JButton("确定");
JButton btnCancel = new JButton("取消");
btnPanel.add(btnOk);
btnPanel.add(btnCancel);
dialog.add(inputPanel, BorderLayout.CENTER);
dialog.add(btnPanel, BorderLayout.SOUTH);
btnSelect.addActionListener(e -> {
JFileChooser chooser = new JFileChooser();
chooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("可执行程序(*.exe)", "exe"));
if (chooser.showOpenDialog(dialog) == JFileChooser.APPROVE_OPTION) {
txtPath.setText(chooser.getSelectedFile().getAbsolutePath());
}
});
btnCancel.addActionListener(e -> dialog.dispose());
btnOk.addActionListener(e -> {
String name = txtName.getText().trim();
String path = txtPath.getText().trim();
if (name.isEmpty() || path.isEmpty()) {
JOptionPane.showMessageDialog(dialog, "名称和路径不能为空");
return;
}
if (editTarget == null) {
configList.add(new AppConfig(name, path));
} else {
editTarget.setName(name);
editTarget.setExePath(path);
}
dialog.dispose();
refreshCardList();
});
dialog.setVisible(true);
}
private void deleteConfig(AppConfig target) {
configList.remove(target);
refreshCardList();
}
private void batchStartAll() {
for (AppConfig cfg : configList) {
launchExe(cfg);
}
}
private void refreshAllStatus() {
for (AppCard card : cardList) {
String exePath = card.getConfig().getExePath();
boolean running = checkProcessRunning(exePath);
card.setRunningStatus(running);
}
}
private void startStatusCheckTask() {
statusChecker = Executors.newSingleThreadScheduledExecutor();
statusChecker.scheduleAtFixedRate(() -> SwingUtilities.invokeLater(this::refreshAllStatus), 0, 3, TimeUnit.SECONDS);
}
// 判断EXE是否正在运行
private boolean checkProcessRunning(String targetExeFullPath) {
File targetFile = new File(targetExeFullPath);
String targetLowerPath = targetFile.getAbsolutePath().toLowerCase();
String targetName = targetFile.getName().toLowerCase();
try {
ProcessBuilder pb = new ProcessBuilder("wmic", "process", "get", "Name,ExecutablePath", "/value");
pb.redirectErrorStream(true);
Process proc = pb.start();
BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream(), "GBK"));
String line;
String procName = "";
String procPath = "";
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.startsWith("Name=")) {
procName = line.substring("Name=".length()).trim().toLowerCase();
} else if (line.startsWith("ExecutablePath=")) {
procPath = line.substring("ExecutablePath=".length()).trim().toLowerCase();
}
if (line.isEmpty()) {
// 1. 优先完整路径匹配(普通exe,杜绝cc同名误判)
if (!procPath.isBlank() && procPath.equals(targetLowerPath)) {
br.close();
proc.destroy();
return true;
}
// 2. 只有当前进程路径为空(商店程序),才走进程名匹配
if (procPath.isBlank() && procName.equals(targetName)) {
br.close();
proc.destroy();
return true;
}
procName = "";
procPath = "";
}
}
br.close();
proc.destroy();
} catch (Exception ignored) {}
return false;
}
private void clearIconCache() {
File cacheDir = new File(".icon_cache");
if (!cacheDir.exists()) return;
File[] files = cacheDir.listFiles();
if (files == null) return;
int delCount = 0;
for (File f : files) {
if (f.delete()) delCount++;
}
JOptionPane.showMessageDialog(this, "缓存清理完成,共删除" + delCount + "张图标");
refreshCardList();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new ExeLauncher().setVisible(true));
}
}
第四版
增加拖拽添加 exe 创建卡片功能(支持快捷方式)
增加卡片之间拖拽,自由调整展示排序


package org.swing;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
// 程序配置序列化实体
class AppConfig implements Serializable {
private String name;
private String exePath;
private String iconCachePath;
public AppConfig(String name, String exePath) {
this.name = name;
this.exePath = exePath;
this.iconCachePath = "";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getExePath() {
return exePath;
}
public void setExePath(String exePath) {
this.exePath = exePath;
}
public String getIconCachePath() {
return iconCachePath;
}
public void setIconCachePath(String iconCachePath) {
this.iconCachePath = iconCachePath;
}
}
// 单程序卡片UI组件
class AppCard extends JPanel {
private final AppConfig cfg;
private final Runnable runCb, editCb, delCb;
private JLabel iconLabel;
private JLabel nameLabel;
private JLabel statusLabel;
private static final int RAW_ICON_SIZE = 32;
private static final int POWERSHELL_TIMEOUT = 3000;
private static final int NAME_MAX_WIDTH = 120;
private boolean isRunning = false;
private final String fullAppName;
// 圆角按钮配色常量
private static final Color BTN_NORMAL_BG = new Color(245, 245, 245);
private static final Color BTN_HOVER_BG = new Color(220, 235, 255);
private static final Color BTN_PRESS_BG = new Color(190, 215, 245);
private static final Color BTN_BORDER = new Color(200, 200, 200);
private static final int BTN_RADIUS = 6;
public AppCard(AppConfig cfg, Runnable runCb, Runnable editCb, Runnable delCb) {
this.cfg = cfg;
this.runCb = runCb;
this.editCb = editCb;
this.delCb = delCb;
this.fullAppName = cfg.getName();
setPreferredSize(new Dimension(160, 220));
setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
setBorder(new EmptyBorder(8, 8, 8, 8));
BoxLayout verticalLayout = new BoxLayout(this, BoxLayout.Y_AXIS);
setLayout(verticalLayout);
int rowGap = 2;
// 1. 程序名称行
JPanel nameRow = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
nameRow.setOpaque(false);
nameLabel = new JLabel();
nameLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12));
nameLabel.setForeground(Color.BLACK);
nameLabel.setPreferredSize(new Dimension(NAME_MAX_WIDTH, 18));
nameLabel.setText(getTruncatedText(fullAppName, NAME_MAX_WIDTH));
nameLabel.setToolTipText(fullAppName);
nameRow.add(nameLabel);
add(nameRow);
add(Box.createVerticalStrut(rowGap));
// 2. EXE图标行
JPanel iconRow = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
iconRow.setOpaque(false);
iconLabel = new JLabel();
iconLabel.setHorizontalAlignment(SwingConstants.CENTER);
iconLabel.setPreferredSize(new Dimension(32, 32));
loadExeIcon();
iconRow.add(iconLabel);
add(iconRow);
add(Box.createVerticalStrut(rowGap));
// 3. 运行状态行
JPanel statusRow = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
statusRow.setOpaque(false);
statusLabel = new JLabel("未运行");
statusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 11));
statusLabel.setForeground(Color.GRAY);
statusRow.add(statusLabel);
add(statusRow);
add(Box.createVerticalStrut(rowGap));
// 4. 操作按钮行
JPanel btnRow = new JPanel(new GridLayout(1, 3, 6, 0));
btnRow.setPreferredSize(new Dimension(160, 34));
JButton runBtn = createStyleBtn("启动");
JButton editBtn = createStyleBtn("编辑");
JButton delBtn = createStyleBtn("删除");
runBtn.addActionListener(e -> runCb.run());
editBtn.addActionListener(e -> editCb.run());
delBtn.addActionListener(e -> {
int opt = JOptionPane.showConfirmDialog(this, "确认删除该程序配置?", "提示", JOptionPane.YES_NO_OPTION);
if (opt == JOptionPane.YES_OPTION) delCb.run();
});
btnRow.add(runBtn);
btnRow.add(editBtn);
btnRow.add(delBtn);
add(btnRow);
}
// 创建圆角交互按钮
private JButton createStyleBtn(String text) {
JButton btn = new JButton(text) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON);
int w = getWidth();
int h = getHeight();
g2.setColor(getBackground());
g2.fillRoundRect(0, 0, w, h, BTN_RADIUS, BTN_RADIUS);
g2.setColor(BTN_BORDER);
g2.drawRoundRect(0, 0, w - 1, h - 1, BTN_RADIUS, BTN_RADIUS);
FontMetrics fm = g2.getFontMetrics();
int textX = (w - fm.stringWidth(getText())) / 2;
int textY = (h - fm.getHeight()) / 2 + fm.getAscent();
g2.setColor(getForeground());
g2.drawString(getText(), textX, textY);
g2.dispose();
}
};
btn.setFont(new Font("微软雅黑", Font.PLAIN, 10));
btn.setFocusPainted(false);
btn.setBorderPainted(false);
btn.setBackground(BTN_NORMAL_BG);
btn.setMargin(new Insets(4, 2, 4, 2));
btn.addMouseListener(new java.awt.event.MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
btn.setBackground(BTN_HOVER_BG);
}
@Override
public void mouseExited(MouseEvent e) {
btn.setBackground(BTN_NORMAL_BG);
}
@Override
public void mousePressed(MouseEvent e) {
btn.setBackground(BTN_PRESS_BG);
}
@Override
public void mouseReleased(MouseEvent e) {
btn.setBackground(BTN_HOVER_BG);
}
});
return btn;
}
private String getTruncatedText(String text, int limitWidth) {
if (text == null || text.isBlank()) return text;
FontMetrics fm = nameLabel.getFontMetrics(nameLabel.getFont());
if (fm.stringWidth(text) <= limitWidth) return text;
StringBuilder sb = new StringBuilder(text);
while (sb.length() > 0 && fm.stringWidth(sb + "...") > limitWidth) {
sb.deleteCharAt(sb.length() - 1);
}
return sb + "...";
}
public void setRunningStatus(boolean running) {
this.isRunning = running;
if (running) {
statusLabel.setText("● 运行中");
statusLabel.setForeground(new Color(0, 160, 0));
} else {
statusLabel.setText("未运行");
statusLabel.setForeground(Color.GRAY);
}
repaint();
}
// 设置拖动高亮边框
public void setDragHighlight(boolean highlight) {
if (highlight) {
setBorder(BorderFactory.createLineBorder(new Color(255, 100, 100), 3));
} else {
setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
}
repaint();
}
private void loadExeIcon() {
String cachePath = cfg.getIconCachePath();
File cacheFile = null;
if (cachePath != null && !cachePath.isBlank()) {
cacheFile = new File(cachePath);
}
if (cacheFile != null && cacheFile.exists() && cacheFile.length() > 100) {
ImageIcon rawIcon = new ImageIcon(cacheFile.getAbsolutePath());
iconLabel.setIcon(rawIcon);
return;
}
new SwingWorker<ImageIcon, Void>() {
@Override
protected ImageIcon doInBackground() throws Exception {
return extractIconOnlyPowershell(cfg.getExePath());
}
@Override
protected void done() {
try {
ImageIcon rawIcon = get();
iconLabel.setIcon(rawIcon);
} catch (Exception e) {
iconLabel.setIcon(getDefaultIcon());
}
}
}.execute();
}
private ImageIcon extractIconOnlyPowershell(String exePath) throws Exception {
File exeFile = new File(exePath);
if (!exeFile.exists()) throw new IOException("EXE文件不存在");
File cacheDir = new File(".icon_cache");
if (!cacheDir.exists()) cacheDir.mkdirs();
String cacheFileName = Math.abs(exePath.hashCode()) + ".png";
File outPng = new File(cacheDir, cacheFileName);
cfg.setIconCachePath(outPng.getAbsolutePath());
String safeExePath = exePath.replace("'", "''");
String safePngPath = outPng.getAbsolutePath().replace("'", "''");
String psCmd = String.format(
"Add-Type -AssemblyName System.Drawing;" +
"$file = '%s';" +
"$icon = [System.Drawing.Icon]::ExtractAssociatedIcon($file);" +
"if ($icon -eq $null) { exit 1 }" +
"$bmp = $icon.ToBitmap();" +
"$bmp.Save('%s', [System.Drawing.Imaging.ImageFormat]::Png)",
safeExePath, safePngPath
);
ProcessBuilder pb = new ProcessBuilder("powershell", "-WindowStyle", "Hidden", "-Command", psCmd);
pb.redirectErrorStream(true);
Process proc = pb.start();
boolean exit = proc.waitFor(POWERSHELL_TIMEOUT, TimeUnit.SECONDS);
if (!exit) {
proc.destroy();
throw new Exception("图标提取超时");
}
try (BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()))) {
br.lines().forEach(l -> {});
}
if (!outPng.exists() || outPng.length() < 100) {
return getDefaultIcon();
}
return new ImageIcon(outPng.getAbsolutePath());
}
private ImageIcon getDefaultIcon() {
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(RAW_ICON_SIZE, RAW_ICON_SIZE, java.awt.image.BufferedImage.TYPE_4BYTE_ABGR);
Graphics2D g = img.createGraphics();
try {
g.setColor(Color.LIGHT_GRAY);
g.fillRect(0, 0, RAW_ICON_SIZE, RAW_ICON_SIZE);
g.setColor(Color.DARK_GRAY);
g.setFont(new Font("微软雅黑", Font.BOLD, 12));
g.drawString("EXE", 6, 22);
} finally {
g.dispose();
}
return new ImageIcon(img);
}
public AppConfig getConfig() {
return cfg;
}
public boolean isRunning() {
return this.isRunning;
}
}
// 主窗口
public class ExeLauncher extends JFrame {
private List<AppConfig> configList = new ArrayList<>();
private List<AppCard> cardList = new ArrayList<>();
private JPanel cardPanel;
private File configFile;
// 拖拽标记
private AppCard dragSourceCard = null;
private Point dragStartPoint = null;
private AppCard hoverTargetCard = null;
private static final String DEFAULT_CONFIG_NAME = "app_config.dat";
private static final String PATH_RECORD_FILE = "config_path.lock";
private ScheduledExecutorService statusChecker;
public ExeLauncher() {
loadLastUsedConfigPath();
setTitle("多程序统一启动管理工具");
setSize(1400, 800);
setDefaultCloseOperation(EXIT_ON_CLOSE);
addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent e) {
saveCurrentConfigPathToRecord();
}
});
setLocationRelativeTo(null);
JPanel rootPanel = new JPanel(new BorderLayout(10, 10));
rootPanel.setBorder(new EmptyBorder(12, 12, 12, 12));
JPanel topBtnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0));
JButton btnAdd = new JButton("添加新程序配置");
JButton btnSaveCurrent = new JButton("保存到当前配置文件");
JButton btnChangeConfigFile = new JButton("修改配置文件");
JButton btnBatchStart = new JButton("一键启动全部程序");
JButton btnRefreshStatus = new JButton("刷新运行状态");
JButton btnClearCache = new JButton("清理图标缓存");
btnBatchStart.setBackground(new Color(180, 240, 180));
btnClearCache.setBackground(new Color(255, 200, 200));
btnRefreshStatus.setBackground(new Color(200, 225, 255));
topBtnPanel.add(btnAdd);
topBtnPanel.add(btnSaveCurrent);
topBtnPanel.add(btnChangeConfigFile);
topBtnPanel.add(btnBatchStart);
topBtnPanel.add(btnRefreshStatus);
topBtnPanel.add(btnClearCache);
cardPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 15));
cardPanel.setBackground(Color.WHITE);
JScrollPane scrollPane = new JScrollPane(cardPanel);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
// 统一拖拽监听器
DropTargetListener fileDropListener = new DropTargetAdapter() {
// 拖拽进入窗口必须接受所有动作,否则直接拦截
@Override
public void dragEnter(DropTargetDragEvent dtde) {
dtde.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
}
@Override
public void dragOver(DropTargetDragEvent dtde) {
dtde.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
}
@Override
public void drop(DropTargetDropEvent dtde) {
try {
dtde.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
Transferable trans = dtde.getTransferable();
if (!trans.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
dtde.dropComplete(false);
System.out.println("拖拽内容不是文件");
return;
}
List<File> fileList = (List<File>) trans.getTransferData(DataFlavor.javaFileListFlavor);
boolean hit = false;
for (File f : fileList) {
String fullPath = f.getAbsolutePath();
System.out.println("拖拽文件完整路径:" + fullPath);
File checkFile = f;
// 处理快捷方式 lnk
if (fullPath.toLowerCase().endsWith(".lnk")) {
String realExePath = resolveLnkTarget(f);
if (realExePath != null && new File(realExePath).exists()) {
checkFile = new File(realExePath);
System.out.println("解析快捷方式真实程序路径:" + realExePath);
} else {
System.out.println("快捷方式解析失败或目标不存在");
}
}
// 判断真实文件是否为exe
String checkPathLow = checkFile.getAbsolutePath().toLowerCase();
if (checkPathLow.endsWith(".exe")) {
openEditDialogWithPath(checkFile.getAbsolutePath());
hit = true;
System.out.println("成功识别EXE:" + checkFile.getAbsolutePath());
}
}
dtde.dropComplete(hit);
System.out.println("拖拽成功,处理exe数量:" + (hit ? 1 : 0));
} catch (Exception e) {
e.printStackTrace();
dtde.dropComplete(false);
}
}
// 解析lnk快捷方式真实目标路径
private String resolveLnkTarget(File lnkFile) throws IOException, InterruptedException {
String lnkAbsPath = lnkFile.getAbsolutePath();
// 使用powershell稳定解析lnk,兼容带空格/中文路径
String psCommand = "$ws = New-Object -ComObject WScript.Shell; $sc = $ws.CreateShortcut('" + lnkAbsPath + "'); Write-Output $sc.TargetPath";
ProcessBuilder pb = new ProcessBuilder("powershell", "-Command", psCommand);
pb.redirectErrorStream(true);
Process proc = pb.start();
BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream(), StandardCharsets.UTF_8));
String line;
StringBuilder sb = new StringBuilder();
while ((line = br.readLine()) != null) {
sb.append(line.trim());
}
br.close();
int exitCode = proc.waitFor();
proc.destroy();
if (exitCode == 0 && sb.length() > 0) {
return sb.toString();
}
return null;
}
};
// 三层全部绑定:滚动面板 + 视口 + 卡片面板,彻底穿透遮挡
DropTarget dtScroll = new DropTarget(scrollPane, DnDConstants.ACTION_COPY_OR_MOVE, fileDropListener);
DropTarget dtViewport = new DropTarget(scrollPane.getViewport(), DnDConstants.ACTION_COPY_OR_MOVE, fileDropListener);
DropTarget dtCardPanel = new DropTarget(cardPanel, DnDConstants.ACTION_COPY_OR_MOVE, fileDropListener);
scrollPane.setDropTarget(dtScroll);
scrollPane.getViewport().setDropTarget(dtViewport);
cardPanel.setDropTarget(dtCardPanel);
rootPanel.add(topBtnPanel, BorderLayout.NORTH);
rootPanel.add(scrollPane, BorderLayout.CENTER);
getContentPane().add(rootPanel);
btnAdd.addActionListener(e -> openEditDialog(null));
btnSaveCurrent.addActionListener(e -> saveConfigCurrent());
btnChangeConfigFile.addActionListener(e -> openSetConfigPathDialog());
btnBatchStart.addActionListener(e -> batchStartAll());
btnRefreshStatus.addActionListener(e -> refreshAllStatus());
btnClearCache.addActionListener(e -> clearIconCache());
loadConfig();
refreshCardList();
startStatusCheckTask();
}
private void loadLastUsedConfigPath() {
File recordFile = new File(PATH_RECORD_FILE);
if (!recordFile.exists()) {
configFile = new File(DEFAULT_CONFIG_NAME);
return;
}
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(recordFile), StandardCharsets.UTF_8))) {
String savedPath = br.readLine();
if (savedPath != null && !savedPath.isBlank()) {
File targetFile = new File(savedPath);
if (targetFile.getParentFile() != null && targetFile.getParentFile().exists()) {
configFile = targetFile;
} else {
configFile = new File(DEFAULT_CONFIG_NAME);
}
} else {
configFile = new File(DEFAULT_CONFIG_NAME);
}
} catch (Exception e) {
configFile = new File(DEFAULT_CONFIG_NAME);
}
}
private void saveCurrentConfigPathToRecord() {
try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(PATH_RECORD_FILE), StandardCharsets.UTF_8))) {
bw.write(configFile.getAbsolutePath());
} catch (Exception ignored) {
}
}
private void openSetConfigPathDialog() {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("修改配置文件(切换存储路径)");
chooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("配置文件(*.dat)", "dat"));
chooser.setApproveButtonText("切换并迁移配置");
File currentFile = configFile;
File parentDir = currentFile.getParentFile();
if (parentDir != null && parentDir.exists()) {
chooser.setCurrentDirectory(parentDir);
} else {
chooser.setCurrentDirectory(new File("."));
}
chooser.setSelectedFile(currentFile);
int selectCode = chooser.showSaveDialog(this);
if (selectCode != JFileChooser.APPROVE_OPTION) return;
File newTargetFile = chooser.getSelectedFile();
if (!newTargetFile.getName().endsWith(".dat")) {
newTargetFile = new File(newTargetFile.getAbsolutePath() + ".dat");
}
if (newTargetFile.getAbsolutePath().equals(configFile.getAbsolutePath())) {
JOptionPane.showMessageDialog(this, "当前已是正在使用的配置文件,无需切换");
return;
}
saveConfigToFile(newTargetFile);
this.configFile = newTargetFile;
saveCurrentConfigPathToRecord();
loadConfig();
refreshCardList();
JOptionPane.showMessageDialog(this, "配置文件切换完成!\n新路径:" + configFile.getAbsolutePath());
}
private void saveConfigCurrent() {
saveConfigToFile(configFile);
JOptionPane.showMessageDialog(this, "保存成功!\n当前配置文件:" + configFile.getAbsolutePath());
}
private void saveConfigToFile(File target) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(target))) {
oos.writeObject(configList);
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "写入配置失败:" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
}
private void loadConfig() {
if (!configFile.exists()) {
configList = new ArrayList<>();
return;
}
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(configFile))) {
Object obj = ois.readObject();
if (obj instanceof List<?>) {
configList = (List<AppConfig>) obj;
}
} catch (Exception e) {
configList = new ArrayList<>();
}
}
// 刷新卡片 + 拖动高亮预览
private void refreshCardList() {
cardPanel.removeAll();
cardList.clear();
dragSourceCard = null;
hoverTargetCard = null;
for (AppConfig cfg : configList) {
AppCard card = new AppCard(
cfg,
() -> launchExe(cfg),
() -> openEditDialog(cfg),
() -> deleteConfig(cfg)
);
// 拖动鼠标监听,增加hover高亮反馈
card.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
dragSourceCard = card;
dragStartPoint = e.getPoint();
card.setDragHighlight(true);
}
@Override
public void mouseReleased(MouseEvent e) {
if (dragSourceCard == null || dragSourceCard != card) return;
// 清除所有高亮
for(AppCard c : cardList) c.setDragHighlight(false);
Point releasePoint = SwingUtilities.convertPoint(card, e.getPoint(), cardPanel);
Component targetComp = cardPanel.getComponentAt(releasePoint);
if (targetComp instanceof AppCard targetCard && targetCard != dragSourceCard) {
int dragIdx = cardList.indexOf(dragSourceCard);
int targetIdx = cardList.indexOf(targetCard);
AppConfig temp = configList.get(dragIdx);
configList.set(dragIdx, configList.get(targetIdx));
configList.set(targetIdx, temp);
refreshCardList();
}
dragSourceCard = null;
dragStartPoint = null;
hoverTargetCard = null;
}
});
// 鼠标拖动实时检测hover卡片,高亮提示
card.addMouseMotionListener(new MouseAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
if(dragSourceCard == null) return;
Point dragPoint = SwingUtilities.convertPoint(card, e.getPoint(), cardPanel);
Component targetComp = cardPanel.getComponentAt(dragPoint);
// 清除上一个hover高亮
if(hoverTargetCard != null && hoverTargetCard != targetComp) {
hoverTargetCard.setDragHighlight(false);
}
if(targetComp instanceof AppCard targetCard && targetCard != dragSourceCard) {
targetCard.setDragHighlight(true);
hoverTargetCard = targetCard;
} else {
hoverTargetCard = null;
}
}
});
cardList.add(card);
cardPanel.add(card);
}
cardPanel.revalidate();
cardPanel.repaint();
refreshAllStatus();
}
private void launchExe(AppConfig cfg) {
File exe = new File(cfg.getExePath());
if (!exe.exists()) {
JOptionPane.showMessageDialog(this, "程序路径不存在:" + cfg.getExePath());
return;
}
try {
new ProcessBuilder(exe.getAbsolutePath()).start();
} catch (Exception e) {
JOptionPane.showMessageDialog(this, "启动失败:" + e.getMessage());
}
}
private void openEditDialogWithPath(String exePath) {
File f = new File(exePath);
String name = f.getName().replace(".exe", "");
openEditDialog(new AppConfig(name, exePath));
}
private void openEditDialog(AppConfig editTarget) {
JDialog dialog = new JDialog(this, "编辑程序配置", true);
dialog.setSize(400, 220);
dialog.setLayout(new BorderLayout(10, 10));
dialog.setLocationRelativeTo(this);
JPanel inputPanel = new JPanel(new GridLayout(2, 2, 8, 8));
JTextField txtName = new JTextField();
JTextField txtPath = new JTextField();
JButton btnSelect = new JButton("选择EXE");
if (editTarget != null) {
txtName.setText(editTarget.getName());
txtPath.setText(editTarget.getExePath());
}
inputPanel.add(new JLabel("程序名称:"));
inputPanel.add(txtName);
inputPanel.add(new JLabel("EXE路径:"));
JPanel pathPanel = new JPanel(new BorderLayout(4, 0));
pathPanel.add(txtPath, BorderLayout.CENTER);
pathPanel.add(btnSelect, BorderLayout.EAST);
inputPanel.add(pathPanel);
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton btnOk = new JButton("确定");
JButton btnCancel = new JButton("取消");
btnPanel.add(btnOk);
btnPanel.add(btnCancel);
dialog.add(inputPanel, BorderLayout.CENTER);
dialog.add(btnPanel, BorderLayout.SOUTH);
btnSelect.addActionListener(e -> {
JFileChooser chooser = new JFileChooser();
chooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("可执行程序(*.exe)", "exe"));
if (chooser.showOpenDialog(dialog) == JFileChooser.APPROVE_OPTION) {
txtPath.setText(chooser.getSelectedFile().getAbsolutePath());
}
});
btnCancel.addActionListener(e -> dialog.dispose());
btnOk.addActionListener(e -> {
String name = txtName.getText().trim();
String path = txtPath.getText().trim();
if (name.isEmpty() || path.isEmpty()) {
JOptionPane.showMessageDialog(dialog, "名称和路径不能为空");
return;
}
if (editTarget == null || !configList.contains(editTarget)) {
configList.add(new AppConfig(name, path));
} else {
editTarget.setName(name);
editTarget.setExePath(path);
}
dialog.dispose();
refreshCardList();
});
dialog.setVisible(true);
}
private void deleteConfig(AppConfig target) {
configList.remove(target);
refreshCardList();
}
private void batchStartAll() {
for (AppConfig cfg : configList) {
launchExe(cfg);
}
}
private void refreshAllStatus() {
for (AppCard card : cardList) {
String exePath = card.getConfig().getExePath();
boolean running = checkProcessRunning(exePath);
card.setRunningStatus(running);
}
}
private void startStatusCheckTask() {
statusChecker = Executors.newSingleThreadScheduledExecutor();
statusChecker.scheduleAtFixedRate(() -> SwingUtilities.invokeLater(this::refreshAllStatus), 0, 3, TimeUnit.SECONDS);
}
private boolean checkProcessRunning(String targetExeFullPath) {
File targetFile = new File(targetExeFullPath);
String targetLowerPath = targetFile.getAbsolutePath().toLowerCase();
String targetName = targetFile.getName().toLowerCase();
try {
ProcessBuilder pb = new ProcessBuilder("wmic", "process", "get", "Name,ExecutablePath", "/value");
pb.redirectErrorStream(true);
Process proc = pb.start();
BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream(), "GBK"));
String line;
String procName = "";
String procPath = "";
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.startsWith("Name=")) {
procName = line.substring("Name=".length()).trim().toLowerCase();
} else if (line.startsWith("ExecutablePath=")) {
procPath = line.substring("ExecutablePath=".length()).trim().toLowerCase();
}
if (line.isEmpty()) {
if (!procPath.isBlank() && procPath.equals(targetLowerPath)) {
br.close();
proc.destroy();
return true;
}
if (procPath.isBlank() && procName.equals(targetName)) {
br.close();
proc.destroy();
return true;
}
procName = "";
procPath = "";
}
}
br.close();
proc.destroy();
} catch (Exception ignored) {
}
return false;
}
private void clearIconCache() {
File cacheDir = new File(".icon_cache");
if (!cacheDir.exists()) return;
File[] files = cacheDir.listFiles();
if (files == null) return;
int delCount = 0;
for (File f : files) {
if (f.delete()) delCount++;
}
JOptionPane.showMessageDialog(this, "缓存清理完成,共删除" + delCount + "张图标");
refreshCardList();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new ExeLauncher().setVisible(true));
}
}


83

被折叠的 条评论
为什么被折叠?



