除了您提到的问题之外,该对话框的代码还有多个问题。例如,它不会处理 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);
}
}
}
}
唯一让我担心的是那个模态技巧。它可以很好地工作
没有它(只需删除任何引用 modalBlocker 和 modalityHatch 的代码),但应用程序的 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<File>
* @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<File>)
* @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);
}));