Java Swing开发桌面小工具

2023/02/24javaSwing工具

本文将介绍如何用java开发一个桌面程序,并生成安装包以便在无java运行环境的电脑上安装使用。以文件压缩工具为例。

1. 编写代码

工具需求: 界面上可选择压缩文件源目录和压缩文件目标目录,点击开始压缩按钮后将源文件目录中的每一个文件压缩成文件名不变的zip文件(一一对应)。

工具代码:

package cn.ideasphere;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/****
 * @author: linxc
 * @Email: 1757948498@qq.com
 * @ClassName: FileCompressionApp
 * @create_date: 2023/02/25 10:54
 * @description:  界面上可选择压缩文件源目录和压缩文件目标目录,点击开始压缩按钮后将源文件目录中的每一个文件压缩成文件名不变的zip文件(一一对应)
 * @version: V1.0  
 */
public class FileCompressionApp {

    private JTextField sourceDirField;
    private JTextField targetDirField;
    private JProgressBar progressBar;

    public static void main(String[] args) {
        // 启动应用
        SwingUtilities.invokeLater(FileCompressionApp::new);
    }

    public FileCompressionApp() {
        JFrame frame = new JFrame("文件压缩工具");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(600, 300);

        // 设置窗口图标
        ImageIcon icon = new ImageIcon(getClass().getResource("title.png")); // 替换为图标文件的路径
        frame.setIconImage(icon.getImage()); // 设置窗口图标

        frame.setLayout(new BorderLayout());

        JPanel mainPanel = new JPanel();
        mainPanel.setLayout(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.insets = new Insets(5, 5, 5, 5);
        gbc.fill = GridBagConstraints.HORIZONTAL;

        // Source directory selection
        JLabel sourceLabel = new JLabel("压缩文件源目录: ");
        sourceDirField = new JTextField(30);
        JButton sourceButton = new JButton("选择目录");
        sourceButton.addActionListener(e -> chooseDirectory(sourceDirField));

        gbc.gridx = 0;
        gbc.gridy = 0;
        mainPanel.add(sourceLabel, gbc);

        gbc.gridx = 1;
        gbc.gridy = 0;
        mainPanel.add(sourceDirField, gbc);

        gbc.gridx = 2;
        gbc.gridy = 0;
        mainPanel.add(sourceButton, gbc);

        // Target directory selection
        JLabel targetLabel = new JLabel("压缩文件目标目录: ");
        targetDirField = new JTextField(30);
        JButton targetButton = new JButton("选择目录");
        targetButton.addActionListener(e -> chooseDirectory(targetDirField));

        gbc.gridx = 0;
        gbc.gridy = 1;
        mainPanel.add(targetLabel, gbc);

        gbc.gridx = 1;
        gbc.gridy = 1;
        mainPanel.add(targetDirField, gbc);

        gbc.gridx = 2;
        gbc.gridy = 1;
        mainPanel.add(targetButton, gbc);

        // Start compression button
        JButton compressButton = new JButton("开始压缩");
        compressButton.addActionListener(this::startCompression);

        gbc.gridx = 0;
        gbc.gridy = 2;
        gbc.gridwidth = 3;
        mainPanel.add(compressButton, gbc);

        // Progress bar
        progressBar = new JProgressBar();
        progressBar.setStringPainted(true);
        progressBar.setVisible(false);
        gbc.gridx = 0;
        gbc.gridy = 3;
        gbc.gridwidth = 3;
        mainPanel.add(progressBar, gbc);

        frame.add(mainPanel, BorderLayout.CENTER);
        // 设置窗口居中
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }


    private void chooseDirectory(JTextField textField) {
        JFileChooser chooser = new JFileChooser();
        chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
        int result = chooser.showOpenDialog(null);
        if (result == JFileChooser.APPROVE_OPTION) {
            File selectedDir = chooser.getSelectedFile();
            textField.setText(selectedDir.getAbsolutePath());
        }
    }

    private void startCompression(ActionEvent e) {
        String sourceDirPath = sourceDirField.getText();
        String targetDirPath = targetDirField.getText();

        if (sourceDirPath.isEmpty() || targetDirPath.isEmpty()) {
            JOptionPane.showMessageDialog(null, "请填写源目录和目标目录!", "错误", JOptionPane.ERROR_MESSAGE);
            return;
        }

        File sourceDir = new File(sourceDirPath);
        File targetDir = new File(targetDirPath);

        if (!sourceDir.isDirectory()) {
            JOptionPane.showMessageDialog(null, "源目录无效!", "错误", JOptionPane.ERROR_MESSAGE);
            return;
        }

        if (!targetDir.exists() && !targetDir.mkdirs()) {
            JOptionPane.showMessageDialog(null, "无法创建目标目录!", "错误", JOptionPane.ERROR_MESSAGE);
            return;
        }

        File[] files = sourceDir.listFiles();
        if (files == null || files.length == 0) {
            JOptionPane.showMessageDialog(null, "源目录为空!", "提示", JOptionPane.INFORMATION_MESSAGE);
            return;
        }

        // Check for files with the same name but different extensions
        HashSet<String> fileNamesWithoutExtension = new HashSet<>();
        for (File file : files) {
            if (file.isFile()) {
                String baseName = removeFileExtension(file.getName());
                if (fileNamesWithoutExtension.contains(baseName)) {
                    JOptionPane.showMessageDialog(null, "目录中有同名的不同类型文件,请修改后再次压缩,谢谢!", "错误", JOptionPane.ERROR_MESSAGE);
                    return;
                }
                fileNamesWithoutExtension.add(baseName);
            }
        }

        progressBar.setVisible(true);
        progressBar.setMaximum(files.length);
        progressBar.setValue(0);

        // 启动压缩任务
        new CompressionWorker(files, targetDir).execute();
    }

    private class CompressionWorker extends SwingWorker<Void, Integer> {
        private final File[] files;
        private final File targetDir;

        public CompressionWorker(File[] files, File targetDir) {
            this.files = files;
            this.targetDir = targetDir;
        }

        @Override
        protected Void doInBackground() {
            boolean allSuccess = true;
            int completed = 0;

            for (File file : files) {
                if (file.isFile()) {
                    try {
                        File zipFile = getUniqueZipFile(targetDir, removeFileExtension(file.getName()));
                        compressFile(file, zipFile);
                        completed++;
                        publish(completed); // 更新进度条
                    } catch (IOException ex) {
                        System.err.println("压缩文件失败: " + file.getAbsolutePath());
                        ex.printStackTrace();
                        allSuccess = false;
                    }
                }
            }

            String message = allSuccess ? "文件压缩完成!" : "部分文件压缩失败,请检查。";
            JOptionPane.showMessageDialog(null, message, "完成", JOptionPane.INFORMATION_MESSAGE);
            return null;
        }

        @Override
        protected void process(java.util.List<Integer> chunks) {
            // 更新进度条
            if (!chunks.isEmpty()) {
                int progress = chunks.get(chunks.size() - 1);
                progressBar.setValue(progress);
                progressBar.setString("已压缩 " + progress + " 个文件");
            }
        }
    }

    private File getUniqueZipFile(File targetDir, String baseName) {
        File zipFile = new File(targetDir, baseName + ".zip");
        int counter = 1;
        while (zipFile.exists()) {
            zipFile = new File(targetDir, baseName + "_" + counter++ + ".zip");
        }
        return zipFile;
    }

    private void compressFile(File file, File zipFile) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(zipFile);
             ZipOutputStream zos = new ZipOutputStream(fos);
             FileInputStream fis = new FileInputStream(file)) {

            ZipEntry zipEntry = new ZipEntry(file.getName());
            zos.putNextEntry(zipEntry);

            byte[] buffer = new byte[1024];
            int length;
            while ((length = fis.read(buffer)) >= 0) {
                zos.write(buffer, 0, length);
            }

            zos.closeEntry();
        }
    }

    private String removeFileExtension(String fileName) {
        int lastDotIndex = fileName.lastIndexOf('.');
        return (lastDotIndex > 0) ? fileName.substring(0, lastDotIndex) : fileName;
    }
}

在IDEA上执行如下:

image-20241217105755638

2. 将编写的工具打包成jar包

如果是Maven项目,可以在IDEA中使用Maven工具直接打包。也可以使用命令mvn clean package 打包。或使用jar命令完成打包。

注意:

指定主类方法①:

使用命令jar cfe FileCompressionApp.jar cn.ideasphere.FileCompressionApp cn,这条命令会创建一个名为FileCompressionApp.jar的JAR文件,其中cn.ideasphere.FileCompressionApp是主类的全限定名。cfe选项表示创建一个带有指定主类的可执行JAR文件。

或在项目目录下创建MANIFEST.MF 文件,内容如下:

Main-Class: cn.ideasphere.FileCompressionApp

然后使用命令jar cvfm FileCompressionApp.jar ../../MANIFEST.MF cn ,这里的cvfm 选项表示创建一个JAR文件并指定清单文件。

上述两种操作将生成一个含有MANIFEST.MF指定主类的jar包。

指定主类方法②:

pom.xml 文件中加入下面配置代码:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>3.2.0</version>
            <configuration>
                <archive>
                    <manifest>
                        <addClasspath>true</addClasspath>
                        <mainClass>cn.ideasphere.FileCompressionApp</mainClass>
                    </manifest>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

在IDEA中使用Maven工具直接打包。也可以使用命令mvn clean package 打包。将生成一个含有MANIFEST.MF指定主类的jar包。

验证:

执行命令java -jar FileCompressionApp.jar 能正常运行显示窗口即说明打包正确。

3. 将jar包编译成exe可执行文件

可使用Launch4j 软件或exe4j 软件将生成的jar编译成exe文件。

这里讲解exe4j

官方下载地址: https://www.ej-technologies.com/exe4j/download

云盘地址: 这里改成本人的百度云盘下载地址

下载安装完成exe4j软件后,进行打包:

image-20241217144401587

附上exe4j 破解码:

————————————————

A-XVK258563F-1p4lv7mg7sav

A-XVK209982F-1y0i3h4ywx2h1

A-XVK267351F-dpurrhnyarva

A-XVK204432F-1kkoilo1jy2h3r

A-XVK246130F-1l7msieqiwqnq

A-XVK249554F-pllh351kcke50

————————————————

选择“JAR IN EXE”,然后下一步:

image-20241217145151005

填写打包完成后的exe文件输出目录:

image-20241217145603202

填写exe名称和图标:

image-20241217145844485

配置Java invocation:

image-20241217150319399

选择主类:

image-20241217150641569

注意:这里exe4j中选择Main class如果找不到程序入口,大概率是jar包导出有问题。

image-20241217150909301

将Search sequence全部删除:

image-20241217151034886

**为什么要删掉这些?**因为我们的exe文件是需要给没Java环境的人使用的,如果不去更改这里的环境指向,打包成的exe在自己有Java环境的电脑上能用,在没有Java环境的电脑上会报错,所以你需要把你本地的jre复制出来,把它和jar包放在同一个文件夹(不强求,就为了方便),以方便打包。

如何寻找jre包呢?

①可以直接复制本地安装的jdk中的jre文件夹;文件相对较大。

②在jdk9以后的版本提供了一个命令jlink ,可以使用 jlink 打包 JDK : jlink 工具允许你创建一个最小的、包含运行所需的 JDK 模块的自包含运行时。这样生成的包不需要用户单独安装 Java 环境。

a. 找到程序依赖的模块:

jdeps --module-path $JAVA_HOME/jmods --print-module-deps FileCompressionApp.jar

image-20241217153143641

b. 使用jlink 创建一个运行时映像,包含 .jar 文件和相关的依赖:

jlink --module-path $JAVA_HOME/jmods --add-modules java.base,java.desktop --output jre

java.base,java.desktop 是你的程序需要的模块。

custom-runtime 是生成的运行时的文件夹。

c. 验证生成的运行时: 在 custom-runtime/bin 文件夹中运行以下命令,确保运行时正常:

./java --version

继续打包exe。。。

配置运行时环境目录:

image-20241217154050667

image-20241217154342332

到这里,就在前面设置的输出目录中可以找到对应的exe文件了。将运行时环境和exe文件放在同一个目录中发给别人就可以在别人的电脑上运行了。

image-20241217155441478

4. Inno Setup加壳改造成安装包

这样子的exe文件很笨重,并且需要和jre在同一个文件夹下才能运行,可以用Inno Setup 6对其进行再次包装,包装完只要发给这个文件的exe安装包给别人就可以使用。

Inno Setup 是一个免费的 Windows 应用程序安装包制作工具,主要用于创建可执行的安装程序(通常以 .exe 格式分发)。它提供了强大的功能,可以轻松地将一个软件项目打包成一个安装程序,供用户安装和配置软件。

Inno Setup加壳改造成安装包具体步骤:

  1. 根据向导模式生成编译的脚本;

    以下为截图,按顺序:

    image-20241217160206192

    image-20241217160256240

    image-20241217160514431

    image-20241217160613106

    image-20241217160704131

    image-20241217160730881

    image-20241217160844427

    image-20241217160952962

    image-20241217161119470

  2. 修改脚本配置,使其适配java项目的打包

​ 找到下图的这个地方,把{app}改成{app}\jre

image-20241217161500268

​ 添加[Dirs] Name: {app}; Permissions: users-full ,使得打包的程序的安装目录拥有完全控制的权限,因为如果默认安装在c盘,新建和删除文件都需要管理员权限

  1. 运行脚本,生成安装包。

    image-20241217162005662

    双击文件即可安装。

    image-20241217162155377

    image-20241217162244820

最近一次更新 3/20/2025, 7:54:12 AM