JavaFX 并发 - 使用在线程中运行但挂起 UI 的任务
JavaFX Concurrency - Using a task, which runs in a thread, but hangs the UI
关于并发有很多问题和答案,我的可能与其他人类似,但对我来说它不是重复的,因为出于某种原因我一定遗漏了一些东西并希望得到一些建议...
我的问题更多,我需要第二双眼睛来指出我做错了什么,以便在后台线程中启用我的代码 运行,但也更新了 GUI,没有冻结它。
最初,使用线程中的任务将 PDF 文件上传到应用程序。
这很好用。
显示一个进度条,动画没有问题:
上传文件()
public void uploadFile(File fileToProcess) {
fileBeingProcessed = fileToProcess;
Task<Parent> uploadingFileTask = new Task<Parent>() {
@Override
public Parent call() {
try {
progressBarStackPane.setVisible(true);
pdfPath = loadPDF(fileBeingProcessed.getAbsolutePath());
createPDFViewer();
openDocument();
} catch (IOException ex) {
java.util.logging.Logger.getLogger(MainSceneController.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
};
uploadingFileTask.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
@Override
public void handle(WorkerStateEvent event) {
fileHasBeenUploaded = true;
progressBarStackPane.setVisible(false);
uploadFilePane.setVisible(false);
tabPane.setVisible(true);
/* This is where I am getting issue, more so in createThumbnailPanels() */
setupThumbnailFlowPane();
createThumbnailPanels();
/****** ^^^^^^^^^^^^ ******/
}
});
uploadingFileTask.setOnFailed(evt -> {
uploadingFileTask.getException().printStackTrace(System.err);
System.err.println(Arrays.toString(uploadingFileTask.getException().getSuppressed()));
});
Thread uploadingFileThread = new Thread(uploadingFileTask);
uploadingFileThread.start();
}
文档上传后,将显示在允许用户查看文档的选项卡中。
有一个辅助选项卡,在上传后禁用,直到完成另一个名为 createThumbnailPanelsTask
;
的任务
但是,在此任务 运行 之前,已创建缩略图面板的 FlowPane
。这似乎没有问题,并且似乎不是 GUI 挂起的原因(这显然是 createThumbnailPanelsTask
中的一个循环,但为了清楚起见,我将显示 setupThumbnailFlowPane()
):
setupThumbnailFlowPane()
public void setupThumbnailFlowPane() {
stage = model.getStage();
root = model.getRoot();
secondaryTabScrollPane.setFitToWidth(true);
secondaryTabScrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
/**
This will be removed from here when refactored but for now it is here,
I don't think this is anything to do with my issue
**/
Set<Node> nodes = secondaryTabScrollPane.lookupAll(".scroll-bar");
for (final Node node : nodes) {
if (node instanceof ScrollBar) {
ScrollBar sb = (ScrollBar) node;
if (sb.getOrientation() == Orientation.VERTICAL) {
sb.setUnitIncrement(30.0);
}
if (sb.getOrientation() == Orientation.HORIZONTAL) {
sb.setVisible(false);
}
}
}
secondaryTab = new FlowPane();
secondaryTab.setId("secondaryTab");
secondaryTab.setBackground(new Background(new BackgroundFill(Color.LIGHTSLATEGRAY, new CornerRadii(0), new Insets(0))));
secondaryTab.prefWidthProperty().bind(stage.widthProperty());
secondaryTab.prefHeightProperty().bind(stage.heightProperty());
secondaryTab.setPrefWrapLength(stage.widthProperty().intValue() - 150);
secondaryTab.setHgap(5);
secondaryTab.setVgap(30);
secondaryTab.setBorder(new Border(new BorderStroke(Color.TRANSPARENT, BorderStrokeStyle.NONE, CornerRadii.EMPTY, new BorderWidths(8, 10, 20, 10))));
secondaryTab.setAlignment(Pos.CENTER);
}
最后,createThumbnailPanels()
被调用,我认为这是我遇到问题的地方。
假设发生的情况是,在文件上传后,上传文件窗格被隐藏,显示查看器选项卡,还有辅助选项卡。
辅助选项卡此时已禁用,左侧还有一个加载图像(gif
)。
预期的行为是 createThumbnailPanels()
任务将在后台 运行,并且在完成之前,该选项卡将保持禁用状态,但是在此期间,gif
图片会旋转,给人的印象是正在加载。
加载完成后,gif
将被删除,并启用选项卡,允许用户导航到它,并查看生成的缩略图面板。
这一切都有效,但是,如前所述,任务是挂起 GUI:
createThumbnailPanels()
public void createThumbnailPanels() {
Task<Void> createThumbnailPanelsTask = new Task<Void>() {
@Override
public Void call() {
if (model.getIcePdfDoc() != null) {
numberOfPagesInDocument = model.getIcePdfDoc().getNumberOfPages();
for (int thumbIndex = 0; thumbIndex < numberOfPagesInDocument; thumbIndex++) {
ThumbnailPanel tb = new ThumbnailPanel(thumbIndex, main, model);
Thumbnail tn = new Thumbnail(tb);
model.setThumbnailAt(tn, thumbIndex);
eventHandlers.setMouseEventsForThumbnails(tb);
/*
I have added this in as I am under the impression that a task runs in a background thread,
and then to update the GUI, I need to call this:
*/
Platform.runLater(() -> {
secondaryTab.getChildren().add(tb);
});
}
}
return null;
}
};
createThumbnailPanelsTask.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
@Override
public void handle(WorkerStateEvent event) {
/*
Further GUI modification run in setOnSucceeded so it runs on main GUI thread(?)
*/
secondaryTabScrollPane.setContent(secondaryTab);
secondaryTab.setDisable(false);
secondaryTab.setGraphic(null);
}
});
createThumbnailPanelsTask.setOnFailed(evt -> {
createThumbnailPanelsTask.getException().printStackTrace(System.err);
System.err.println(Arrays.toString(createThumbnailPanelsTask.getException().getSuppressed()));
});
Thread createThumbnailPanelsThread = new Thread(createThumbnailPanelsTask);
createThumbnailPanelsThread.start();
}
除 GUI 在创建面板时挂起外,一切正常。
创建后,可以再次控制 GUI,移除加载 gif
,启用选项卡,用户可以导航到它并查看面板。
很明显,这里关于并发我缺少一些东西。
如前所述,我的印象是后台线程中有一个任务 运行,所以我有点困惑为什么它似乎没有这样做。再一次,显然我错过了什么。
关于并发,我读了又读又读,但似乎无法弄清楚我的方法哪里出了问题。我很想尝试使用服务,但是,我觉得考虑到这一点我只是把事情复杂化了,而且显然有一种简单的方法可以实现我想要实现的目标。
任何帮助将不胜感激...推动正确的方向,或对我理解错误的地方进行一些澄清。
提前致谢,毫无疑问,一旦排序就可以帮助我避免以后出现这个问题!
更新代码
createThumbnailPanels()
public void createThumbnailPanels() {
Task<Void> createThumbnailPanelsTask = new Task<Void>() {
//TODO: Need to check that it's a PDF
@Override
public Void call() {
if (model.getIcePdfDoc() != null) {
numberOfPagesInDocument = model.getIcePdfDoc().getNumberOfPages();
for (int thumbIndex= 0; thumbIndex< numberOfPagesInDocument; thumbIndex++) {
ThumbnailPanel tb = new ThumbnailPanel(thumbIndex, main, model);
Thumbnail tn = new Thumbnail(tb);
eventHandlers.setMouseEventsForThumbnails(tb);
model.setThumbnailAt(tn, thumbIndex);
model.setThumbnailPanels(tb);
}
setThumbnailPanelsToScrollPane();
}
return null;
}
};
createThumbnailPanelsTask.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
@Override
public void handle(WorkerStateEvent event) {
// setThumbnailPanelsToScrollPane();
}
});
createThumbnailPanelsTask.setOnFailed(evt -> {
createThumbnailPanelsTask.getException().printStackTrace(System.err);
System.err.println(Arrays.toString(createThumbnailPanelsTask.getException().getSuppressed()));
});
Thread createThumbnailPanelsThread = new Thread(createThumbnailPanelsTask);
createThumbnailPanelsThread.start();
}
setThumbnailPanelsToScrollPane()
public void setThumbnailPanelsToScrollPane() {
Task<Void> setThumbnailPanelsToScrollPaneTask = new Task<Void>() {
//TODO: Need to check that it's a PDF
@Override
public Void call() {
Platform.runLater(() -> {
secondaryTab.getChildren().addAll(model.getThumbnailPanels());
secondaryTabScrollPane.setContent(main.informationExtractionPanel);
secondaryTab.setDisable(false);
secondaryTab.setGraphic(null);
});
return null;
}
};
setThumbnailPanelsToScrollPaneTask.setOnFailed(evt -> {
setThumbnailPanelsToScrollPaneTask.getException().printStackTrace(System.err);
System.err.println(Arrays.toString(setThumbnailPanelsToScrollPaneTask.getException().getSuppressed()));
});
Thread setThumbnailPanelsToScrollPaneThread = new Thread(setThumbnailPanelsToScrollPaneTask);
setThumbnailPanelsToScrollPaneThread.start();
}
仅供参考:如果我在 setOnSucceeded 中调用 setThumbnailPanelsToScrollPane();
,它似乎不起作用。
getChildren().add
在 JavaFX GUI 线程上 运行ning(这就是 Platform.runLater
所做的),但它只需要 运行 它在 Platform.runLater
如果你添加子项的父项连接到显示的 gui 的根,这意味着你应该能够将子项添加到未连接到任何根的父项,并且在子添加过程结束时将整个父添加到根,如果您在任何异步代码中执行 Platform.runLater
,它将在 gui 线程上 运行 在您的情况下,它在您的异步 for
循环添加 ThumbnailPanels
如果它们的数量很大,gui 将挂起。
关于并发有很多问题和答案,我的可能与其他人类似,但对我来说它不是重复的,因为出于某种原因我一定遗漏了一些东西并希望得到一些建议...
我的问题更多,我需要第二双眼睛来指出我做错了什么,以便在后台线程中启用我的代码 运行,但也更新了 GUI,没有冻结它。
最初,使用线程中的任务将 PDF 文件上传到应用程序。
这很好用。
显示一个进度条,动画没有问题:
上传文件()
public void uploadFile(File fileToProcess) {
fileBeingProcessed = fileToProcess;
Task<Parent> uploadingFileTask = new Task<Parent>() {
@Override
public Parent call() {
try {
progressBarStackPane.setVisible(true);
pdfPath = loadPDF(fileBeingProcessed.getAbsolutePath());
createPDFViewer();
openDocument();
} catch (IOException ex) {
java.util.logging.Logger.getLogger(MainSceneController.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
};
uploadingFileTask.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
@Override
public void handle(WorkerStateEvent event) {
fileHasBeenUploaded = true;
progressBarStackPane.setVisible(false);
uploadFilePane.setVisible(false);
tabPane.setVisible(true);
/* This is where I am getting issue, more so in createThumbnailPanels() */
setupThumbnailFlowPane();
createThumbnailPanels();
/****** ^^^^^^^^^^^^ ******/
}
});
uploadingFileTask.setOnFailed(evt -> {
uploadingFileTask.getException().printStackTrace(System.err);
System.err.println(Arrays.toString(uploadingFileTask.getException().getSuppressed()));
});
Thread uploadingFileThread = new Thread(uploadingFileTask);
uploadingFileThread.start();
}
文档上传后,将显示在允许用户查看文档的选项卡中。
有一个辅助选项卡,在上传后禁用,直到完成另一个名为 createThumbnailPanelsTask
;
但是,在此任务 运行 之前,已创建缩略图面板的 FlowPane
。这似乎没有问题,并且似乎不是 GUI 挂起的原因(这显然是 createThumbnailPanelsTask
中的一个循环,但为了清楚起见,我将显示 setupThumbnailFlowPane()
):
setupThumbnailFlowPane()
public void setupThumbnailFlowPane() {
stage = model.getStage();
root = model.getRoot();
secondaryTabScrollPane.setFitToWidth(true);
secondaryTabScrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
/**
This will be removed from here when refactored but for now it is here,
I don't think this is anything to do with my issue
**/
Set<Node> nodes = secondaryTabScrollPane.lookupAll(".scroll-bar");
for (final Node node : nodes) {
if (node instanceof ScrollBar) {
ScrollBar sb = (ScrollBar) node;
if (sb.getOrientation() == Orientation.VERTICAL) {
sb.setUnitIncrement(30.0);
}
if (sb.getOrientation() == Orientation.HORIZONTAL) {
sb.setVisible(false);
}
}
}
secondaryTab = new FlowPane();
secondaryTab.setId("secondaryTab");
secondaryTab.setBackground(new Background(new BackgroundFill(Color.LIGHTSLATEGRAY, new CornerRadii(0), new Insets(0))));
secondaryTab.prefWidthProperty().bind(stage.widthProperty());
secondaryTab.prefHeightProperty().bind(stage.heightProperty());
secondaryTab.setPrefWrapLength(stage.widthProperty().intValue() - 150);
secondaryTab.setHgap(5);
secondaryTab.setVgap(30);
secondaryTab.setBorder(new Border(new BorderStroke(Color.TRANSPARENT, BorderStrokeStyle.NONE, CornerRadii.EMPTY, new BorderWidths(8, 10, 20, 10))));
secondaryTab.setAlignment(Pos.CENTER);
}
最后,createThumbnailPanels()
被调用,我认为这是我遇到问题的地方。
假设发生的情况是,在文件上传后,上传文件窗格被隐藏,显示查看器选项卡,还有辅助选项卡。
辅助选项卡此时已禁用,左侧还有一个加载图像(gif
)。
预期的行为是 createThumbnailPanels()
任务将在后台 运行,并且在完成之前,该选项卡将保持禁用状态,但是在此期间,gif
图片会旋转,给人的印象是正在加载。
加载完成后,gif
将被删除,并启用选项卡,允许用户导航到它,并查看生成的缩略图面板。
这一切都有效,但是,如前所述,任务是挂起 GUI:
createThumbnailPanels()
public void createThumbnailPanels() {
Task<Void> createThumbnailPanelsTask = new Task<Void>() {
@Override
public Void call() {
if (model.getIcePdfDoc() != null) {
numberOfPagesInDocument = model.getIcePdfDoc().getNumberOfPages();
for (int thumbIndex = 0; thumbIndex < numberOfPagesInDocument; thumbIndex++) {
ThumbnailPanel tb = new ThumbnailPanel(thumbIndex, main, model);
Thumbnail tn = new Thumbnail(tb);
model.setThumbnailAt(tn, thumbIndex);
eventHandlers.setMouseEventsForThumbnails(tb);
/*
I have added this in as I am under the impression that a task runs in a background thread,
and then to update the GUI, I need to call this:
*/
Platform.runLater(() -> {
secondaryTab.getChildren().add(tb);
});
}
}
return null;
}
};
createThumbnailPanelsTask.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
@Override
public void handle(WorkerStateEvent event) {
/*
Further GUI modification run in setOnSucceeded so it runs on main GUI thread(?)
*/
secondaryTabScrollPane.setContent(secondaryTab);
secondaryTab.setDisable(false);
secondaryTab.setGraphic(null);
}
});
createThumbnailPanelsTask.setOnFailed(evt -> {
createThumbnailPanelsTask.getException().printStackTrace(System.err);
System.err.println(Arrays.toString(createThumbnailPanelsTask.getException().getSuppressed()));
});
Thread createThumbnailPanelsThread = new Thread(createThumbnailPanelsTask);
createThumbnailPanelsThread.start();
}
除 GUI 在创建面板时挂起外,一切正常。
创建后,可以再次控制 GUI,移除加载 gif
,启用选项卡,用户可以导航到它并查看面板。
很明显,这里关于并发我缺少一些东西。
如前所述,我的印象是后台线程中有一个任务 运行,所以我有点困惑为什么它似乎没有这样做。再一次,显然我错过了什么。
关于并发,我读了又读又读,但似乎无法弄清楚我的方法哪里出了问题。我很想尝试使用服务,但是,我觉得考虑到这一点我只是把事情复杂化了,而且显然有一种简单的方法可以实现我想要实现的目标。
任何帮助将不胜感激...推动正确的方向,或对我理解错误的地方进行一些澄清。
提前致谢,毫无疑问,一旦排序就可以帮助我避免以后出现这个问题!
更新代码
createThumbnailPanels()
public void createThumbnailPanels() {
Task<Void> createThumbnailPanelsTask = new Task<Void>() {
//TODO: Need to check that it's a PDF
@Override
public Void call() {
if (model.getIcePdfDoc() != null) {
numberOfPagesInDocument = model.getIcePdfDoc().getNumberOfPages();
for (int thumbIndex= 0; thumbIndex< numberOfPagesInDocument; thumbIndex++) {
ThumbnailPanel tb = new ThumbnailPanel(thumbIndex, main, model);
Thumbnail tn = new Thumbnail(tb);
eventHandlers.setMouseEventsForThumbnails(tb);
model.setThumbnailAt(tn, thumbIndex);
model.setThumbnailPanels(tb);
}
setThumbnailPanelsToScrollPane();
}
return null;
}
};
createThumbnailPanelsTask.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
@Override
public void handle(WorkerStateEvent event) {
// setThumbnailPanelsToScrollPane();
}
});
createThumbnailPanelsTask.setOnFailed(evt -> {
createThumbnailPanelsTask.getException().printStackTrace(System.err);
System.err.println(Arrays.toString(createThumbnailPanelsTask.getException().getSuppressed()));
});
Thread createThumbnailPanelsThread = new Thread(createThumbnailPanelsTask);
createThumbnailPanelsThread.start();
}
setThumbnailPanelsToScrollPane()
public void setThumbnailPanelsToScrollPane() {
Task<Void> setThumbnailPanelsToScrollPaneTask = new Task<Void>() {
//TODO: Need to check that it's a PDF
@Override
public Void call() {
Platform.runLater(() -> {
secondaryTab.getChildren().addAll(model.getThumbnailPanels());
secondaryTabScrollPane.setContent(main.informationExtractionPanel);
secondaryTab.setDisable(false);
secondaryTab.setGraphic(null);
});
return null;
}
};
setThumbnailPanelsToScrollPaneTask.setOnFailed(evt -> {
setThumbnailPanelsToScrollPaneTask.getException().printStackTrace(System.err);
System.err.println(Arrays.toString(setThumbnailPanelsToScrollPaneTask.getException().getSuppressed()));
});
Thread setThumbnailPanelsToScrollPaneThread = new Thread(setThumbnailPanelsToScrollPaneTask);
setThumbnailPanelsToScrollPaneThread.start();
}
仅供参考:如果我在 setOnSucceeded 中调用 setThumbnailPanelsToScrollPane();
,它似乎不起作用。
getChildren().add
在 JavaFX GUI 线程上 运行ning(这就是 Platform.runLater
所做的),但它只需要 运行 它在 Platform.runLater
如果你添加子项的父项连接到显示的 gui 的根,这意味着你应该能够将子项添加到未连接到任何根的父项,并且在子添加过程结束时将整个父添加到根,如果您在任何异步代码中执行 Platform.runLater
,它将在 gui 线程上 运行 在您的情况下,它在您的异步 for
循环添加 ThumbnailPanels
如果它们的数量很大,gui 将挂起。