I have an application where I want to show a FlowPane with PDF pages thumbnails drawn into canvases. I'm using PDFBox and FXGraphics2D to render pages.
My current implementation creates a number of canvases as the number of pages, adds them to the FlowPane and then spins a number or async tasks to draw the pages content into the canvas.
I'm not sure if the async drawing is the recommended way but the idea is to not use the JavaFX thread to do the PDF parsing to avoid freezing the application.
Now, my issue is this, I can see from the logs that all the rendering tasks have finished and the document is closed. The UI shows some rendered pages but it stays unresponsive for ~10 seconds. After that the application revives, all pages are rendered and everything works nicely.
I tried to profile and I think this is the relevant part:
But I have limited knowledge of what is going on under the hood and I couldn't figure out what I'm doing wrong. Do you have an idea or hint on what is wrong with my approach/code and how it can be improved? Ideally I'd like to have the application fast and responsive while the pages thumbnails are filled.
I'm on Linux with a pretty decent machine but I also tested on Windows and got the same behavior. I also tried to replace FlowPane with HBox or VBox but still the same happened.
Here is some ugly code to reproduce the behavior:
public class TestApp extends Application {
public static void main(String[] args) {
Application.launch(TestApp.class, args);
}
#Override
public void start(Stage primaryStage) {
try {
BorderPane root = new BorderPane();
Scene scene = new Scene(root, 400, 400);
var flow = new FlowPane();
flow.setOnDragOver(e -> {
if (e.getDragboard().hasFiles()) {
e.acceptTransferModes(TransferMode.COPY);
}
e.consume();
});
flow.setOnDragDropped(e -> {
var tasks = new ArrayList<TestApp.RenderTask>();
try {
var document = PDDocument.load(e.getDragboard()
.getFiles().get(0));
var renderer = new PDFRenderer(document);
for (int i = 1; i <= document.getNumberOfPages(); i++) {
var page = document.getPage(i - 1);
var cropbox = page.getCropBox();
var thumbnailDimension = new Dimension2D(400,
400 * (cropbox.getHeight() / cropbox.getWidth()));
var thumbnail = new Canvas(thumbnailDimension.getWidth(), thumbnailDimension.getHeight());
var gs = thumbnail.getGraphicsContext2D();
var pop = gs.getFill();
gs.setFill(Color.WHITE);
gs.fillRect(0, 0, thumbnailDimension.getWidth(), thumbnailDimension.getHeight());
gs.setFill(pop);
tasks.add(new TestApp.RenderTask(renderer, thumbnail, i));
flow.getChildren().add(new Group(thumbnail));
}
var exec = Executors.newSingleThreadExecutor();
tasks.forEach(exec::submit);
exec.submit(()-> {
try {
document.close();
System.out.println("close");
} catch (IOException ioException) {
ioException.printStackTrace();
}
});
} catch (Exception ioException) {
ioException.printStackTrace();
}
e.setDropCompleted(true);
e.consume();
});
var scroll = new ScrollPane(flow);
scroll.setFitToHeight(true);
scroll.setFitToWidth(true);
root.setCenter(scroll);
primaryStage.setScene(scene);
primaryStage.show();
} catch (Exception e) {
e.printStackTrace();
}
}
private static record RenderTask(PDFRenderer renderer, Canvas canvas, int page) implements Runnable {
#Override
public void run() {
var gs = new FXGraphics2D(canvas.getGraphicsContext2D());
gs.setBackground(WHITE);
try {
renderer.renderPageToGraphics(page - 1, gs);
} catch (IOException e) {
e.printStackTrace();
}
gs.dispose();
}
}
}
And this is the 310 pages PDF file I'm using to test it.
Thanks
I finally managed to get what I wanted, the application responds, the thumbnails are populated as they are ready and the memory usage is limited.
The issue
I thought I was drawing the canvases off of the FX thread but how Canvas works is that you fill a buffer of drawing instructions and they are executed when the canvas becomes part of the scene (I think). What was happening is that I was quickly filling the canvases with a lot of drawing instructions, adding all the canvases to the FlowPane and the the application was spending a lot of time actually executing the drawing instructions for 310 canvas and becoming unresponsive for ~10 seconds.
After some reading and suggestions from the JavaFX community here and on Twitter, I tried to switch to an ImageView implementation, telling PDFBox to create a BufferedImage of the page and use it to create an ImageView. It worked nicely but memory usage was 10 times compared to the Canvas impl:
The best of both worlds
In my final solution I add white canvases to the FlowPane, create a BufferedImage of the page and convert it to an Image off of the FX thread, draw the image to the Canvas and discard the Image.
This is memory usage for the same 310 pages PDF file:
And this is the application responsiveness:
EDIT (added code to reproduce)
Here the code I used (with SAMBox instead of PDFBox)
public class TestAppCanvasImageMixed extends Application {
public static void main(String[] args) {
Application.launch(TestAppCanvas.class, args);
}
#Override
public void start(Stage primaryStage) {
try {
BorderPane root = new BorderPane();
Scene scene = new Scene(root, 400, 400);
var button = new Button("render");
button.setDisable(true);
root.setTop(new HBox(button));
var flow = new FlowPane();
flow.setOnDragOver(e -> {
if (e.getDragboard().hasFiles()) {
e.acceptTransferModes(TransferMode.COPY);
}
e.consume();
});
flow.setOnDragDropped(e -> {
ArrayList<TestAppCanvasImageMixed.RenderTaskToImage> tasks = new ArrayList<>();
try {
var document = PDDocument.load(e.getDragboard()
.getFiles().get(0));
var renderer = new PDFRenderer(document);
for (int i = 1; i <= document.getNumberOfPages(); i++) {
var page = document.getPage(i - 1);
var cropbox = page.getCropBox();
var thumbnailDimension = new Dimension2D(400,
400 * (cropbox.getHeight() / cropbox.getWidth()));
var thumbnail = new Canvas(thumbnailDimension.getWidth(), thumbnailDimension.getHeight());
var gs = thumbnail.getGraphicsContext2D();
var clip = new Rectangle(thumbnailDimension.getWidth(), thumbnailDimension.getHeight());
clip.setArcHeight(15);
clip.setArcWidth(15);
thumbnail.setClip(clip);
var pop = gs.getFill();
gs.setFill(WHITE);
gs.fillRect(0, 0, thumbnailDimension.getWidth(), thumbnailDimension.getHeight());
gs.setFill(pop);
var g = new Group();
tasks.add(new TestAppCanvasImageMixed.RenderTaskToImage(renderer, 400 / cropbox.getWidth(), i, thumbnail,
() -> g.getChildren().setAll(thumbnail)));
flow.getChildren().add(new Group());
}
button.setOnAction(a -> {
var exec = Executors.newFixedThreadPool(1);
tasks.forEach(exec::submit);
exec.submit(() -> {
try {
document.close();
System.out.println("close");
} catch (IOException ioException) {
ioException.printStackTrace();
}
});
});
button.setDisable(false);
} catch (Exception ioException) {
ioException.printStackTrace();
}
e.setDropCompleted(true);
e.consume();
});
var scroll = new ScrollPane(new StackPane(flow));
scroll.setFitToHeight(true);
scroll.setFitToWidth(true);
root.setCenter(scroll);
primaryStage.setScene(scene);
primaryStage.show();
} catch (Exception e) {
e.printStackTrace();
}
}
private static record RenderTaskToImage(PDFRenderer renderer, float scale, int page, Canvas canvas, Runnable r) implements Runnable {
#Override
public void run() {
try {
var bufferedImage = renderer.renderImage(page - 1, scale, ImageType.ARGB, RenderDestination.VIEW);
var image = SwingFXUtils.toFXImage(bufferedImage, null);
var gc = canvas.getGraphicsContext2D();
gc.drawImage(image, 0, 0);
Platform.runLater(() -> r.run());
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Here is the tableview controller :
Trying to send the data associated with the selected row into InstructorProfileContoller, when the button is pressed. The code works and prints you "You selected an Instructor" but then doesn't send any data over
Instructor instructor = (Instructor) InsTable.getSelectionModel().getSelectedItem();
if(instructor != null){
System.out.println("You select an instructor");
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("InstructorProfile.fxml"));
Parent root = fxmlLoader.load();
Stage stage = new Stage();
stage.setTitle("update instructor");
stage.setScene(new Scene(root));
stage.initModality(Modality.APPLICATION_MODAL);
stage.showAndWait();
InstructorProfileController instructorProfileController = fxmlLoader.getController();
instructorProfileController.countrychoicebox.setValue(instructor.getCountryID());
instructorProfileController.insstatuschoicebox.setValue(instructor.getIns_statusCode());
instructorProfileController.firstnametextfield.setText(instructor.getIns_firstName());
instructorProfileController.lastnametextfield.setText(instructor.getIns_lastName());
instructorProfileController.addresstextfield.setText(instructor.getIns_address());
instructorProfileController.citytextfield.setText(instructor.getIns_city());
// instructorProfileController.dateofBirthpicker.setValue(instructor.getIns_dateOfBirth());
instructorProfileController.gendertextfield.setText(instructor.getIns_sex());
instructorProfileController.zipcodetextfield.setText(String.valueOf(instructor.getIns_zipcode()));
}
initialize();
}
The method call stage.showAndWait() shows the stage, and then waits until it is closed. So you don't set the values until after the user has closed the window. Just move the calls that set the values to before the call to showAndWait().
Instructor instructor = (Instructor) InsTable.getSelectionModel().getSelectedItem();
if(instructor != null){
System.out.println("You select an instructor");
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("InstructorProfile.fxml"));
Parent root = fxmlLoader.load();
InstructorProfileController instructorProfileController = fxmlLoader.getController();
instructorProfileController.countrychoicebox.setValue(instructor.getCountryID());
instructorProfileController.insstatuschoicebox.setValue(instructor.getIns_statusCode());
instructorProfileController.firstnametextfield.setText(instructor.getIns_firstName());
instructorProfileController.lastnametextfield.setText(instructor.getIns_lastName());
instructorProfileController.addresstextfield.setText(instructor.getIns_address());
instructorProfileController.citytextfield.setText(instructor.getIns_city());
// instructorProfileController.dateofBirthpicker.setValue(instructor.getIns_dateOfBirth());
instructorProfileController.gendertextfield.setText(instructor.getIns_sex());
instructorProfileController.zipcodetextfield.setText(String.valueOf(instructor.getIns_zipcode()));
Stage stage = new Stage();
stage.setTitle("update instructor");
stage.setScene(new Scene(root));
stage.initModality(Modality.APPLICATION_MODAL);
stage.showAndWait();
}
initialize();
As an aside, it would be a much better style to just define a method setInstructor(...) in your InstructorProfileController:
public class InstructorProfileController {
// ...
public void setInstructor(Instructor instructor) {
countrychoicebox.setValue(instructor.getCountryID());
insstatuschoicebox.setValue(instructor.getIns_statusCode());
firstnametextfield.setText(instructor.getIns_firstName());
lastnametextfield.setText(instructor.getIns_lastName());
addresstextfield.setText(instructor.getIns_address());
citytextfield.setText(instructor.getIns_city());
// dateofBirthpicker.setValue(instructor.getIns_dateOfBirth());
gendertextfield.setText(instructor.getIns_sex());
zipcodetextfield.setText(String.valueOf(instructor.getIns_zipcode()));
}
// ...
}
and then, of course, just do
Instructor instructor = (Instructor) InsTable.getSelectionModel().getSelectedItem();
if(instructor != null){
System.out.println("You select an instructor");
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("InstructorProfile.fxml"));
Parent root = fxmlLoader.load();
InstructorProfileController instructorProfileController = fxmlLoader.getController();
instructorProfileController.setInstructor(instructor);
// ...
}
That way you don't expose the UI controls outside the relevant controller, which will make your code far more maintainable.
I'm a newbie in JavaFX. I made a fx application it has a home and another jfxml file.
This is HomeContoler.java file for open another jfxmlfile
#FXML
public void actionIngredencesReg(ActionEvent event) {
try {
mainHome.setOpacity(0.2);
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("Ingrdences.fxml"));
Parent root1 = (Parent) fxmlLoader.load();
Stage stage = new Stage();
stage.initModality(Modality.APPLICATION_MODAL);
stage.initStyle(StageStyle.UNDECORATED);
Scene scene = new Scene(root1);
stage.setScene(scene);
stage.show();
// mainHome.setOpacity(1);
} catch (Exception e) {
e.printStackTrace();
}
}
when I click on the menu items it will works likes
this the close code of Ingredients.fxml file
#FXML
Label close_label;
this is a label, here in action
#Override
public void initialize(URL url, ResourceBundle rb) {
close_label.setOnMouseClicked(e -> {
//this is code for close only science
Stage stage = (Stage) close_label.getScene().getWindow();
stage.close();
});
}
but after close Ingredients.fxml file home.fxml file be like this
[this is not needed for me][4]
I want to convert home.fxml file like this
as like this after closing the ingredient.fxml
i want to setOpcaity of home.fxml file into 1 after close the ingredient.fxml file
anyone can help me to fix it...
Just revert the opacity when the stage is hidden:
stage.setOnHidden(e -> mainHome.setOpacity(1));
I am creating a ListView with the ability to double click each item and have a window popup with inputs structured by an FXML file. The FXML file itemStep contains fx:controller="controller.ItemStep"
listViewVariable.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
if (mouseEvent.getButton().equals(MouseButton.PRIMARY)) {
if (mouseEvent.getClickCount() == 2) {
ItemStep item = listViewVariable.getSelectionModel()
.getSelectedItem();
if (item != null) {
try {
FXMLLoader isLoader = new FXMLLoader(Main.class.getResource("/view/itemStep.fxml"));
AnchorPane pane = isLoader.load();
Scene scene = new Scene(pane);
Stage stage = new Stage();
stage.setScene(scene);
item.setUrl(item.urlField.getText());
stage.show();
stage.setOnCloseRequest(new EventHandler<WindowEvent>() {
public void handle(WindowEvent we) {
item.setUrl(item.urlField.getText());
}
});
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
});
I continue to get the following error using the above. I need to be able use the FXML file within this Stage.
Caused by: java.lang.InstantiationException: controller.ItemStep
at java.lang.Class.newInstance(Unknown Source)
at sun.reflect.misc.ReflectUtil.newInstance(Unknown Source)
... 44 more
Caused by: java.lang.NoSuchMethodException: controller.ItemStep.<init>()
at java.lang.Class.getConstructor0(Unknown Source)
... 46 more
Your ItemStep class does not have a no argument constructor.
Does a new instance of ItemStep get recreated after each double[click]?
Yes, that is what you have wrote your code to do.
If you didn't wish to do that, you should invoke the load method on your FXMLLoader outside of your event handler, store the reference to the loaded pane in a final variable, and use that reference inside the event handler, rather than loading a new pane time every time the event handler is invoked.
when I invoke both the load FXMLLoader and final AnchorPane outside the event handler I get: AnchorPane#ed39805[styleClass=root]is already set as root of another scene
You can't add a node to more than one scene or to a single scene more than once, you either have to remove it from the original scene, or just re-use the original scene with a single instance of the node. I don't know what kind of behavior you desire, but probably, you want to just have a single window pop-up and make it modal, rather than creating a new window every time somebody clicks on something.
Basically, you do something like this (though this is just an outline that I have never compiled or executed as I don't completely know your complete requirements, nor what your ItemStep and urlField really are):
final FXMLLoader isLoader = new FXMLLoader(Main.class.getResource("/view/itemStep.fxml"));
final AnchorPane pane = isLoader.load();
final Scene scene = new Scene(pane);
final Stage stage = new Stage();
stage.initModality(Modality.APPLICATION_MODAL);
stage.setScene(scene);
listViewVariable.setOnMouseClicked(event -> {
if (mouseEvent.getButton().equals(MouseButton.PRIMARY) && (mouseEvent.getClickCount() == 2)) {
ItemStep item = listViewVariable.getSelectionModel().getSelectedItem();
if (item != null) {
stage.showAndWait();
item.setUrl(item.urlField.getText());
}
}
});
I am looking for a way to display an html file in a different stage once the help button is clicked.
public void handleButtonAction(ActionEvent event) throws IOException {
if (event.getSource() == help) {
stage = (Stage) help.getScene().getWindow();
root = FXMLLoader.load(getClass().getResource("help.fxml"));
WebView browser = new WebView();
Scene helpScene = new Scene(root);
Stage helpStage = new Stage();
helpStage.setTitle("Help Menu");
helpStage.setScene(helpScene);
URL url = getClass().getResource("readme.html");
browser.getEngine().load(url.toExternalForm());
helpStage.show();
}
}
Your code is fine except that you forgot to add the webview to the scene, do
((Pane) helpScene.getRoot()).getChildren().add(browser);