JavaFX实战:打造一个可视化Windows文件夹管理工具

13 阅读9分钟

前言

平时整理电脑文件时,经常需要给文件夹改别名、加备注、换自定义图标,但每次都要手动改 desktop.ini,又要设系统属性又要改编码,非常麻烦。于是用 JavaFX 写了个可视化工具 WindirManager,拖拽文件夹就能一键管理别名、备注、图标,顺便统计文件夹大小和文件数量。

本文从工具用途、核心代码实现到打包发布完整讲一遍,文末附打包脚本,复制就能用。

Image转存失败,建议直接上传图片文件

工具主页面.png

一、工具用途与核心功能

WindirManager 是一个 Windows 平台的文件夹可视化管理工具,基于 JavaFX + JNA 开发,主要解决手动修改 desktop.ini 的痛点。

核心功能

  • 文件夹拖拽导入:支持拖拽或点击选择文件夹,自动读取信息

  • 文件夹属性一览:实时统计文件夹大小、文件数、子文件夹数、修改时间

  • 文件夹别名设置:可视化编辑 LocalizedResourceName,资源管理器直接显示中文名

  • 文件夹备注设置:鼠标悬停显示自定义备注说明

  • 自定义文件夹图标:从系统 DLL 图标库选取图标,一键写入 desktop.ini

  • INI 配置预览:实时预览即将写入的 desktop.ini 内容,确认后再保存

  • 文件列表浏览:展示当前目录下所有文件/文件夹,带图标、类型、大小、修改时间

  • 修改日志查询:记录每次文件夹修改操作,支持历史回溯

文件夹图标选择.png

文件夹图标选择2.png

适用场景

  • 给项目文件夹设置中文别名,英文路径不影响开发

  • 给常用资料文件夹加备注说明,鼠标悬停就能看到

  • 批量美化文件夹图标,让资源管理器更有辨识度

  • 快速查看文件夹大小和文件数量,不用右键属性等半天

修改演示1.png

修改演示2.png

修改演示3.png

修改结果.png

修改日志查询.png


二、技术栈与环境准备

技术版本用途
JDK17+运行环境
JavaFX17.0.6UI 界面框架
JNA5.14.0调用 Windows DLL 获取系统图标
ini4j0.5.4解析与写入 desktop.ini
Maven3.x项目构建与依赖管理
jpackageJDK 自带打包成 exe 安装包/绿色版

特别说明

⚠️ 注意:本工具仅支持 Windows 系统,依赖 Windows API 和 desktop.ini 机制,macOS/Linux 无法使用。

测试环境说明

开发测试环境:Windows 11 + JDK 17 + JavaFX 17.0.6,理论上 Win10 也能用,其他版本没测过。

环境配置

开发前需要配置两个环境变量:

  1. JAVA_HOME:指向 JDK 17+ 安装目录

  2. JAVAFX_HOME:指向 JavaFX SDK 解压目录(下载地址:openjfx.io


三、核心代码实现

3.1 可复用拖拽组件:FileDropPane

封装了一个通用的文件拖拽面板组件,支持单选/多选、仅文件/仅目录、点击选择/仅拖拽等多种模式,项目里直接 new 就能用。

public class FileDropPane extends StackPane {

    private OnFileDropListener listener;
    private boolean onlyDir;      // 仅目录
    private boolean onlyDrag;     // 仅拖拽模式
    private boolean multiple;     // 多选

    // 拖拽进入:高亮样式
    private void setOnDragEntered(DragEvent event) {
        label.setStyle(hoverStyle);
    }

    // 拖拽悬浮:校验是否本地文件
    private void setOnDragOver(DragEvent event) {
        Dragboard dragboard = event.getDragboard();
        if (dragboard.hasFiles()) {
            event.acceptTransferModes(TransferMode.COPY);
        }
        event.consume();
    }

    // 拖拽释放:处理文件列表
    private void setOnDragDropped(DragEvent event) {
        List<File> files = event.getDragboard().getFiles();

        // 单选模式校验
        if (!multiple && files.size() > 1) {
            UIUtil.alert("拖入失败", "一次只能拖入单个文件");
            return;
        }
        // 仅目录模式校验
        if (onlyDir && !files.get(0).isDirectory()) {
            UIUtil.alert("拖入失败", "仅允许拖入文件夹");
            return;
        }

        if (listener != null) {
            listener.onFileDrop(files.get(0));
        }
        event.setDropCompleted(true);
        event.consume();
    }

    // 回调接口
    public interface OnFileDropListener {
        default void onFilesDrop(List<File> files) {}
        default void onFileDrop(File file) {}
    }
}

使用方式

FileDropPane fileDropPane = new FileDropPane();
fileDropPane.setMultiple(false);    // 单选
fileDropPane.setOnlyDir(true);      // 仅文件夹
fileDropPane.setOnlyDrag(false);    // 点击+拖拽双模式
fileDropPane.setListener(new FileDropPane.OnFileDropListener() {
    @Override
    public void onFileDrop(File file) {
        // 处理选中的文件夹
        dir = file;
        readDir();
    }
});

3.2 desktop.ini 读写封装:IniFile

desktop.ini 是 Windows 文件夹配置文件,编码特殊(有的是 UTF-8,有的是 GBK),直接读写容易乱码。封装了 IniFile 类,自动识别编码,支持增删改查。

@Data
public class IniFile {

    private Charset charset = Charset.forName("UTF-8");
    private Map<String, List<IniOption>> selectionMap;
    private String selectionGetKey;  // 当前操作分区

    // 加载INI文件,自动识别编码
    public void loadIniFile(String iniFilePath) {
        Path path = Path.of(iniFilePath);
        Charset iniCharset = MyFileUtil.getIniCharset(path);
        this.setCharset(iniCharset);

        Ini ini = new Ini();
        ini.getConfig().setEscape(false);
        ini.load(path.toFile());

        // 遍历所有分区和配置项
        ini.keySet().forEach(sectionName -> {
            List<IniOption> optList = new ArrayList<>();
            Profile.Section section = ini.get(sectionName);
            for (String optKey : section.keySet()) {
                String val = section.get(optKey);
                optList.add(new IniOption(optKey, val));
            }
            addSelection(sectionName, optList);
        });
    }

    // 设置配置项值,不存在则自动创建
    public void setIniOption(String optionKey, String value) {
        if (this.selectionMap == null) {
            this.selectionMap = new HashMap<>();
        }
        List<IniOption> optList = selectionMap.computeIfAbsent(
            selectionGetKey, k -> new ArrayList<>()
        );
        IniOption option = getIniOption(optionKey);
        if (option == null) {
            optList.add(new IniOption(optionKey, value));
        } else {
            option.setValue(value);
        }
    }

    // 生成标准INI格式文本
    public String viewContent() {
        if (selectionMap == null || selectionMap.isEmpty()) {
            return "";
        }
        StringBuilder builder = new StringBuilder();
        selectionMap.forEach((sectionName, optList) -> {
            builder.append("[").append(sectionName).append("]\n");
            optList.forEach(o -> {
                builder.append(o.getKey()).append("=").append(o.getValue()).append("\n");
            });
            builder.append("\n");
        });
        return builder.toString();
    }

    // 配置项内部类
    @Data
    public static class IniOption {
        private String key;
        private String value;
        private String comment;
    }
}

3.3 JNA 调用 Windows API 获取系统图标

通过 JNA 调用 shell32.dll 的 ExtractIcon 方法,从系统 DLL 文件中提取指定索引的图标。这样就能让用户像系统一样选择 shell32.dll、imageres.dll 里的内置图标。

public interface WinShell32Api extends ShellAPI {
    WinShell32Api INSTANCE = Native.load("shell32", WinShell32Api.class);

    // 从DLL中提取图标
    HICON ExtractIcon(HINSTANCE hInst, String lpszExeFileName, int nIconIndex);
}
@Service
public class WinIconServiceImpl implements IWinIconService {

    @Override
    public WinDllIcon getDllIcon(String dllPath, int iconIndex) {
        // 调用JNA提取图标
        HICON hIcon = WinShell32Api.INSTANCE.ExtractIcon(
            new HINSTANCE(), dllPath, iconIndex
        );

        // 转换为JavaFX Image对象
        BufferedImage bufferedImage = ImageIO.read(...);
        Image fxImage = SwingFXUtils.toFXImage(bufferedImage, null);

        WinDllIcon icon = new WinDllIcon();
        icon.setDllPath(dllPath);
        icon.setIconIndex(iconIndex);
        icon.setImage(fxImage);
        return icon;
    }

    // 写入desktop.ini并设置系统属性
    @Override
    public boolean writeDesktopIniFile(Path folder, String iniContent) {
        Path iniPath = folder.resolve("desktop.ini");
        MyFileUtil.writeFile(iniPath.toString(), iniContent, StandardCharsets.UTF_8);

        // 设置隐藏+系统属性,Windows才会生效
        Files.setAttribute(iniPath, "dos:hidden", true);
        Files.setAttribute(iniPath, "dos:system", true);
        Files.setAttribute(folder, "dos:system", true);
        return true;
    }
}

关键点:desktop.ini 必须设置「隐藏 + 系统」属性,所在文件夹也要设置「系统」属性,否则 Windows 资源管理器不会读取配置。

3.4 主界面搭建

界面用 SplitPane 分成上下两部分,上半部分是拖拽区+属性面板,下半部分是文件列表+INI预览,左右分栏布局。

public void start(Stage primaryStage) {
    SplitPane pane = new SplitPane();
    SplitPane topPan = new SplitPane();
    HBox topLeft = new HBox();    // 拖拽区
    VBox topRight = new VBox();   // 属性面板

    TableView<FileInfo> table = new TableView<>();  // 文件列表
    TextArea iniPreView = new TextArea();          // INI预览

    // 上半部分:拖拽区 + 属性面板
    topPan.getItems().addAll(topLeft, topRight);
    topPan.setDividerPositions(0.1);

    // 下半部分:文件列表 + INI预览
    HBox center = new HBox();
    center.getChildren().addAll(dirChildPane, iniBox);
    HBox.setHgrow(dirChildPane, Priority.ALWAYS);

    // 整体垂直分割
    pane.getItems().addAll(topPan, center);
    pane.setOrientation(Orientation.VERTICAL);
    pane.setDividerPositions(0.2);

    Scene scene = new Scene(pane);
    primaryStage.setScene(scene);
    primaryStage.show();
}

四、打包发布:jpackage 一键脚本

开发完成后,用 JDK 自带的 jpackage 工具打包成绿色免安装文件夹(app-image),用户解压就能用,不需要装 JRE。

打包脚本

把脚本放在项目的 package 目录下,双击运行,自动执行 Maven 打包 → 清理旧文件 → jpackage 构建 → 压缩成 zip。

@echo off
setlocal enabledelayedexpansion
chcp 65001 >nul

:: ========== 项目配置(每个项目改这里) ==========
set "APP_NAME=WindirManager"
set "APP_VERSION=1.0"
set "MAIN_CLASS=com.jcxg.tools.windirmgmt.AppBootApplication"
set "JAR_NAME=windir_mgmt-1.0-SNAPSHOT-jar-with-dependencies.jar"
:: ==============================================

:: ========== 全局配置(一次配置永久生效) ==========
set "JAVAFX_LIB=%JAVAFX_HOME%\lib"
set "GLOBAL_JDK_MODULES=java.logging,java.desktop,java.xml"
set "GLOBAL_FX_MODULES=javafx.controls,javafx.fxml,javafx.graphics"
set "GLOBAL_JVM_OPTS=--enable-native-access=jna,jna.platform --add-opens java.base/java.lang=jna -Dprism.order=sw -Dfile.encoding=UTF-8"
set "OUT_DIR=dist"
set "APP_TYPE=app-image"
:: ==============================================

:: 项目根目录 = 脚本上级目录
set "PROJECT_ROOT=%~dp0..\"
set "TARGET_JAR=!PROJECT_ROOT!target\%JAR_NAME%"

echo ==============================================
echo            JavaFX 打包工具
echo ==============================================
echo 项目名称     :!APP_NAME!
echo 项目根目录   :!PROJECT_ROOT!
echo 打包类型     :!APP_TYPE!
echo ==============================================
echo.

:: 1. Maven 打包
echo [1/5] 执行 Maven 打包
cd /d "!PROJECT_ROOT!"
mvn clean package
if %errorlevel% neq 0 (
    echo ✕ Maven 打包失败,终止脚本!
    pause
    exit /b 1
)
echo ✔ Maven 构建完成
echo.

:: 2. 校验依赖
if not exist "%JAVAFX_LIB%" (
    echo ✕ 错误:JAVAFX_HOME 环境变量未配置
    pause
    exit /b 1
)
echo ✔ JavaFX 依赖校验通过
echo.

:: 3. 清理旧文件
echo [2/5] 清理历史打包缓存
if exist "!PROJECT_ROOT!!OUT_DIR!" (
    rmdir /s /q "!PROJECT_ROOT!!OUT_DIR!"
    echo ✔ 旧目录清理完成
)
echo.

:: 4. jpackage 打包
echo [3/5] 开始 jpackage 构建
set "ALL_MODULES=!GLOBAL_FX_MODULES!,!GLOBAL_JDK_MODULES!"
jpackage ^
--name !APP_NAME! ^
--app-version !APP_VERSION! ^
--input "!PROJECT_ROOT!target" ^
--main-jar !JAR_NAME! ^
--main-class !MAIN_CLASS! ^
--module-path "!JAVAFX_LIB!" ^
--add-modules !ALL_MODULES! ^
--java-options "!GLOBAL_JVM_OPTS!" ^
--win-console ^
--type !APP_TYPE! ^
--dest "!PROJECT_ROOT!!OUT_DIR!" ^
--jlink-options "--strip-debug" ^
--jlink-options "--no-man-pages" ^
--jlink-options "--no-header-files" ^
--jlink-options "--compress=2"

if %errorlevel% neq 0 (
    echo ✕ 打包执行失败
    pause
    exit /b 1
)
echo ✔ 打包构建完成
echo.

:: 5. 压缩成 zip
echo [4/5] 压缩程序包
set "ZIP_PATH=!PROJECT_ROOT!!OUT_DIR!\!APP_NAME!_v!APP_VERSION!.zip"
where 7z >nul 2>&1
if %errorlevel% equ 0 (
    echo ✔ 检测到 7-Zip,使用高压缩
    7z a -tzip -mx9 "!ZIP_PATH!" "!PROJECT_ROOT!!OUT_DIR!\!APP_NAME!" >nul
) else (
    echo ⚠ 未检测到 7-Zip,使用系统 tar 压缩
    tar -a -c -f "!ZIP_PATH!" -C "!PROJECT_ROOT!!OUT_DIR!" !APP_NAME!
)
echo ✅ 压缩包生成:!ZIP_PATH!
echo.

echo [5/5] 全部完成!
echo 输出目录:!PROJECT_ROOT!!OUT_DIR!\!APP_NAME!
echo.
pause

五、踩坑与注意事项

1. JavaFX 打包闪退问题

打包后 exe 双击一闪而过,看不到报错。解决方法:

  • 打包时加 --win-console 参数,保留控制台窗口,崩溃时能看到错误日志

  • 加 JVM 参数 -Dprism.order=sw,强制使用软件渲染,避开显卡驱动兼容性问题

  • -Dfile.encoding=UTF-8,避免中文乱码导致崩溃

2. JNA 模块访问权限

JDK 17+ 模块化后,JNA 访问 sun.misc 会报错。必须加两个 JVM 参数:

--enable-native-access=jna,jna.platform
--add-opens java.base/java.lang=jna

3. desktop.ini 不生效

写完配置但文件夹没变,检查这几点:

  • desktop.ini 文件是否设置了「隐藏 + 系统」属性

  • 文件夹本身是否设置了「系统」属性

  • INI 文件编码是否正确(UTF-8 带 BOM / GBK 都可能)

  • 刷新资源管理器或重启 explorer.exe 再看

4. 打包后删不掉目录

程序闪退之后,Java 后台进程可能残留,锁住了文件夹。解决:

  • 任务管理器杀掉对应 Java 进程再删

  • 代码里加全局异常捕获,崩溃时正常退出,避免进程残留

5. 图标索引从 0 开始

Windows DLL 里的图标索引是从 0 开始计数的,shell32.dll 里文件夹图标通常是索引 3、4 左右,imageres.dll 里图标更多。点击图标按钮可以循环切换预览。


六、总结

这个工具虽然功能不复杂,但涵盖了 JavaFX 界面开发、JNA 原生调用、INI 文件处理、jpackage 打包发布等完整的桌面应用开发流程,适合作为 JavaFX 入门练手项目。

打包脚本已经封装成通用模板,换个项目改改顶部配置就能复用,不用每次都查 jpackage 参数。

后续可以扩展的方向:批量处理多个文件夹、自定义图标上传、文件夹标签分类、右键菜单集成等。

以上就是这个小工具的全部内容了,功能比较简单,代码也还有很多可以优化的地方,欢迎大家提建议和 PR。

源码已上传至码云: https://gitee.com/javafx-proj/windirmgmt.git