java Swing 实现程序启动管理台.exe

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));
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值