摆动中的 JavaFX FileChooser

JavaFX FileChooser in swing

我想每个人都会同意 JFileChooser 真的很糟糕。所以我在寻找替代方案,发现 JavaFX 有一个很棒的 FileChooser class。那么现在很明显的问题是:我如何才能将简洁的 FileChooser 嵌入到我的 Swing 应用程序中?

不用说,我在发布这篇文章之前做了一些研究,这是我目前所发现的:link to a Reddit post

那个JavaFXFileDialogclass的代码非常有趣,但是当我退出我的应用程序时它并没有关闭(JavaFX似乎在后台继续运行)。此外,我还缺少一些可以传递给 FileChooser 的字段,例如设置默认目录的路径。而且我不喜欢它是静态的。

非常感谢任何意见。

除了您提到的问题之外,该对话框的代码还有多个问题。例如,它不处理 JavaFX 平台在调用 isJavaFXStillUsable() 之后立即关闭但在调用 Platform.runLater() 之前关闭的情况,这仍然会使它永远挂起。我也不喜欢那个巨大的 synchronized 块,尽管这似乎没有任何实际问题。我也不明白为什么 "the stupid synchronization object had to be a field" - chooseFileWithJavaFXDialog() 的每次调用都是相互独立的,所以它也可以使用本地最终锁(即使那个数组也可以)。

使 JVM 正确退出的正确方法是在关闭应用程序时调用 Platform.exit()(可能在主 window 的 windowClosed() 中)。您需要手动执行此操作,因为选择器 class 不知道您是否需要 JavaFX,并且一旦关闭就无法重新启动它。

该代码启发我开发了一个实用程序 class,用于调用 JavaFX 事件线程中的几乎所有代码,并将结果返回给调用线程,从而很好地处理各种异常和 JavaFX 状态:

/**
 * A utility class to execute a Callable synchronously
 * on the JavaFX event thread.
 * 
 * @param <T> the return type of the callable
 */
public class SynchronousJFXCaller<T> {
    private final Callable<T> callable;

    /**
     * Constructs a new caller that will execute the provided callable.
     * 
     * The callable is accessed from the JavaFX event thread, so it should either
     * be immutable or at least its state shouldn't be changed randomly while
     * the call() method is in progress.
     * 
     * @param callable the action to execute on the JFX event thread
     */
    public SynchronousJFXCaller(Callable<T> callable) {
        this.callable = callable;
    }

    /**
     * Executes the Callable.
     * <p>
     * A specialized task is run using Platform.runLater(). The calling thread
     * then waits first for the task to start, then for it to return a result.
     * Any exception thrown by the Callable will be rethrown in the calling
     * thread.
     * </p>
     * @param startTimeout time to wait for Platform.runLater() to <em>start</em>
     * the dialog-showing task
     * @param startTimeoutUnit the time unit of the startTimeout argument
     * @return whatever the Callable returns
     * @throws IllegalStateException if Platform.runLater() fails to start
     * the task within the given timeout
     * @throws InterruptedException if the calling (this) thread is interrupted
     * while waiting for the task to start or to get its result (note that the
     * task will still run anyway and its result will be ignored)
     */
    public T call(long startTimeout, TimeUnit startTimeoutUnit)
            throws Exception {
        final CountDownLatch taskStarted = new CountDownLatch(1);
        // Can't use volatile boolean here because only finals can be accessed
        // from closures like the lambda expression below.
        final AtomicBoolean taskCancelled = new AtomicBoolean(false);
        // a trick to emulate modality:
        final JDialog modalBlocker = new JDialog();
        modalBlocker.setModal(true);
        modalBlocker.setUndecorated(true);
        modalBlocker.setOpacity(0.0f);
        modalBlocker.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        final CountDownLatch modalityLatch = new CountDownLatch(1);
        final FutureTask<T> task = new FutureTask<T>(() -> {
            synchronized (taskStarted) {
                if (taskCancelled.get()) {
                    return null;
                } else {
                    taskStarted.countDown();
                }
            }
            try {
                return callable.call();
            } finally {
                // Wait until the Swing thread is blocked in setVisible():
                modalityLatch.await();
                // and unblock it:
                SwingUtilities.invokeLater(() ->
                        modalBlocker.setVisible(false));
            }
        });
        Platform.runLater(task);
        if (!taskStarted.await(startTimeout, startTimeoutUnit)) {
            synchronized (taskStarted) {
                // the last chance, it could have been started just now
                if (!taskStarted.await(0, TimeUnit.MILLISECONDS)) {
                    // Can't use task.cancel() here because it would
                    // interrupt the JavaFX thread, which we don't own.
                    taskCancelled.set(true);
                    throw new IllegalStateException("JavaFX was shut down"
                            + " or is unresponsive");
                }
            }
        }
        // a trick to notify the task AFTER we have been blocked
        // in setVisible()
        SwingUtilities.invokeLater(() -> {
            // notify that we are ready to get the result:
            modalityLatch.countDown();
        });
        modalBlocker.setVisible(true); // blocks
        modalBlocker.dispose(); // release resources
        try {
            return task.get();
        } catch (ExecutionException ex) {
            Throwable ec = ex.getCause();
            if (ec instanceof Exception) {
                throw (Exception) ec;
            } else if (ec instanceof Error) {
                throw (Error) ec;
            } else {
                throw new AssertionError("Unexpected exception type", ec);
            }
        }
    }

}

唯一让我担心的是模态技巧。它可以很好地工作 没有它(只需删除任何引用 modalBlockermodalityHatch 的代码),但应用程序的 Swing 部分不仅会停止响应用户输入(这是我们需要的),而且还会会冻结, 停止更新、进度条等等,这不是很好。这个特殊技巧让我担心的是,不可见的对话框在某些 L&F 中可能不那么不可见,或者导致其他不需要的故障。

我故意没有包含任何初始化或关闭代码,因为我相信它不属于那里。我会在 main()Platform.exit() 中执行 new JFXPanel() 执行其他关闭任务。

使用这个 class,调用 FileChooser 很容易:

/**
 * A utility class that summons JavaFX FileChooser from the Swing EDT.
 * (Or anywhere else for that matter.) JavaFX should be initialized prior to
 * using this class (e. g. by creating a JFXPanel instance). It is also
 * recommended to call Platform.setImplicitExit(false) after initialization
 * to ensure that JavaFX platform keeps running. Don't forget to call
 * Platform.exit() when shutting down the application, to ensure that
 * the JavaFX threads don't prevent JVM exit.
 */
public class SynchronousJFXFileChooser {
    private final Supplier<FileChooser> fileChooserFactory;

    /**
     * Constructs a new file chooser that will use the provided factory.
     * 
     * The factory is accessed from the JavaFX event thread, so it should either
     * be immutable or at least its state shouldn't be changed randomly while
     * one of the dialog-showing method calls is in progress.
     * 
     * The factory should create and set up the chooser, for example,
     * by setting extension filters. If there is no need to perform custom
     * initialization of the chooser, FileChooser::new could be passed as
     * a factory.
     * 
     * Alternatively, the method parameter supplied to the showDialog()
     * function can be used to provide custom initialization.
     * 
     * @param fileChooserFactory the function used to construct new choosers
     */
    public SynchronousJFXFileChooser(Supplier<FileChooser> fileChooserFactory) {
        this.fileChooserFactory = fileChooserFactory;
    }

    /**
     * Shows the FileChooser dialog by calling the provided method.
     * 
     * Waits for one second for the dialog-showing task to start in the JavaFX
     * event thread, then throws an IllegalStateException if it didn't start.
     * 
     * @see #showDialog(java.util.function.Function, long, java.util.concurrent.TimeUnit) 
     * @param <T> the return type of the method, usually File or List&lt;File&gt;
     * @param method a function calling one of the dialog-showing methods
     * @return whatever the method returns
     */
    public <T> T showDialog(Function<FileChooser, T> method) {
        return showDialog(method, 1, TimeUnit.SECONDS);
    }

    /**
     * Shows the FileChooser dialog by calling the provided method. The dialog 
     * is created by the factory supplied to the constructor, then it is shown
     * by calling the provided method on it, then the result is returned.
     * <p>
     * Everything happens in the right threads thanks to
     * {@link SynchronousJFXCaller}. The task performed in the JavaFX thread
     * consists of two steps: construct a chooser using the provided factory
     * and invoke the provided method on it. Any exception thrown during these
     * steps will be rethrown in the calling thread, which shouldn't
     * normally happen unless the factory throws an unchecked exception.
     * </p>
     * <p>
     * If the calling thread is interrupted during either the wait for
     * the task to start or for its result, then null is returned and
     * the Thread interrupted status is set.
     * </p>
     * @param <T> return type (usually File or List&lt;File&gt;)
     * @param method a function that calls the desired FileChooser method
     * @param timeout time to wait for Platform.runLater() to <em>start</em>
     * the dialog-showing task (once started, it is allowed to run as long
     * as needed)
     * @param unit the time unit of the timeout argument
     * @return whatever the method returns
     * @throws IllegalStateException if Platform.runLater() fails to start
     * the dialog-showing task within the given timeout
     */
    public <T> T showDialog(Function<FileChooser, T> method,
            long timeout, TimeUnit unit) {
        Callable<T> task = () -> {
            FileChooser chooser = fileChooserFactory.get();
            return method.apply(chooser);
        };
        SynchronousJFXCaller<T> caller = new SynchronousJFXCaller<>(task);
        try {
            return caller.call(timeout, unit);
        } catch (RuntimeException | Error ex) {
            throw ex;
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
            return null;
        } catch (Exception ex) {
            throw new AssertionError("Got unexpected checked exception from"
                    + " SynchronousJFXCaller.call()", ex);
        }
    }

    /**
     * Shows a FileChooser using FileChooser.showOpenDialog().
     * 
     * @see #showDialog(java.util.function.Function, long, java.util.concurrent.TimeUnit) 
     * @return the return value of FileChooser.showOpenDialog()
     */
    public File showOpenDialog() {
        return showDialog(chooser -> chooser.showOpenDialog(null));
    }

    /**
     * Shows a FileChooser using FileChooser.showSaveDialog().
     * 
     * @see #showDialog(java.util.function.Function, long, java.util.concurrent.TimeUnit) 
     * @return the return value of FileChooser.showSaveDialog()
     */
    public File showSaveDialog() {
        return showDialog(chooser -> chooser.showSaveDialog(null));
    }

    /**
     * Shows a FileChooser using FileChooser.showOpenMultipleDialog().
     * 
     * @see #showDialog(java.util.function.Function, long, java.util.concurrent.TimeUnit) 
     * @return the return value of FileChooser.showOpenMultipleDialog()
     */
    public List<File> showOpenMultipleDialog() {
        return showDialog(chooser -> chooser.showOpenMultipleDialog(null));
    }

    public static void main(String[] args) {
        javafx.embed.swing.JFXPanel dummy = new javafx.embed.swing.JFXPanel();
        Platform.setImplicitExit(false);
        try {
            SynchronousJFXFileChooser chooser = new SynchronousJFXFileChooser(() -> {
                FileChooser ch = new FileChooser();
                ch.setTitle("Open any file you wish");
                return ch;
            });
            File file = chooser.showOpenDialog();
            System.out.println(file);
            // this will throw an exception:
            chooser.showDialog(ch -> ch.showOpenDialog(null), 1, TimeUnit.NANOSECONDS);
        } finally {
            Platform.exit();
        }
    }

}

使用这个 class,您可以在工厂方法中初始化您的选择器,或者,如果您需要为每次调用执行不同的初始化,您可以将自定义方法传递给 showDialog() :

    System.out.println(chooser.showDialog(ch -> {
        ch.setInitialDirectory(new File(System.getProperty("user.home")));
        return ch.showOpenDialog(null);
    }));