【问题标题】:JavaFX FileChooser in swingJavaFX FileChooser 在摇摆
【发布时间】:2015-05-09 08:31:14
【问题描述】:

我想每个人都同意 JFileChooser 真的是大便。所以我一直在寻找替代方案,发现 JavaFX 有一个很棒的 FileChooser 类。那么现在显而易见的问题是:如何将简洁的 FileChooser 嵌入到我的 Swing 应用程序中?

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

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

感谢您的任何意见。

【问题讨论】:

    标签: swing javafx


    【解决方案1】:

    除了您提到的问题之外,该对话框的代码还有多个问题。例如,它不会处理 JavaFX 平台在调用 isJavaFXStillUsable() 之后立即关闭但在调用 Platform.runLater() 之前关闭的情况,这仍然会使其永远挂起。我也不喜欢那个巨大的synchronized 块,尽管这似乎没有任何真正的问题。我也不明白为什么“愚蠢的同步对象必须是一个字段”——chooseFileWithJavaFXDialog() 的每次调用都是相互独立的,所以它也可以使用本地最终锁(即使那个数组也可以) .

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

    该代码启发我开发了一个实用程序类,用于调用 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()

    使用这个类,调用 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();
            }
        }
    
    }
    

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

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

    【讨论】:

    • 首先,非常感谢您的努力!目前我对 Java 知识一无所知,因为我们不得不在大学转学 C,而且我很快就要期末考试了。所以如果你能给我一个月的时间,我可以熟悉我的代码并尝试实施你的建议。我会尽快报告并标记您的答案!
    • 我不得不承认,我正在努力弄清楚如何正确实施这一点。你能不能给我一个详细的例子,说明你将如何在一个非常简单的摇摆应用程序中使用它(可能是一个带有两个按钮的 JFrame)?如果还能够指定当前目录、当前文件名和扩展过滤器,那就太好了。再次感谢您并为您需要这么多帮助表示歉意:|
    • @Haeri,我不明白。我在上一个 sn-p 中给出了一个示例,它可以从任何 Swing 应用程序安全地调用。只需将其插入您可以想象的任何代码中,而不是像我那样插入 println() 调用。在我的示例中,我已经指定了一个当前目录,您实际上可以在该 lambda 表达式中调用 ch 上的任何方法,指定过滤器或您需要的任何其他内容。
    猜你喜欢
    • 1970-01-01
    • 2019-02-27
    • 2019-11-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多