I am developing a software which gets an image from a camera and displays it live in a JavaFX ImageView
. I have a thread which gets the last image (in this case a BufferedImage
), and an AnimationTimer
that assigns it to the ImageView
. The reason I went ahead with an AnimationTimer
was it seemed better than to fill the Platform with Runnable each time a new image is obtained. The refresh works fine, and FPS are decent.
However, I noticed that when the AnimationTimer
was running, the menu bar in my software was not displayed properly. Some menu items went missing when I moused over others. This picture explains it:
On the left side, you have what the menu normally looks like, and on the right side, what it looks like when the AnimationTimer
is running. As you can see, the "Save" menu item is missing, and the background of my live image is displayed instead. Moreover, when I was opening a new window (on a new Scene
), when I moused over any kind of Node
(a button, a checkbox...), the background turned black. I was able to fix this problem by setting the depth-buffer boolean to true when initializing the Scene
. I have however no clue how to fix this menu bar bug, and I think these bugs show what I am doing is probably not right.
I was thinking maybe the JavaFX application thread was saturated with new images to display, and it was basically taking too much time for other elements (such as a menu item) to be painted.
Questions:
AnimationTimer
for instance?Here's a code snippet that reproduces the bug. Change the two strings in the start function to path to images. The images should be relatively large (several MB).
Click on the "Start" button to start the animation timer. Then try to open the "File" menu, and mouse over the menu items. The bug does not appear systematically, try repeating moving your mouse up and down over, it should appear at some point.
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javax.imageio.ImageIO;
public class ImageRefresher extends Application {
@Override
public void start(Stage primaryStage) {
//Here change the 2 Strings to a path to an image on your HDD
//The bug appears more easily with large images (>3-4MB)
String pathToImage1 = "/path/to/your/first/image";
String pathToImage2 = "/path/to/your/second/image";
try {
//Image content (contains buffered image, see below)
ImageContent image = new ImageContent(pathToImage1);
//If this line is commented, the bug does not appear
image.setImage(ImageIO.read(new File(pathToImage2)));
//JavaFX class containing nodes (see below)
MainWindow window = new MainWindow(image);
Scene scene = new Scene(window.getPane(), 300, 250);
primaryStage.setTitle("Menu refresh");
primaryStage.setScene(scene);
primaryStage.show();
} catch (IOException ex) {
Logger.getLogger(ImageRefresher.class.getName()).log(Level.SEVERE, null, ex);
}
}
public static void main(String[] args) {
launch(args);
}
public class MainWindow {
private BorderPane pane;
private MenuBar menuBar;
private ImageView displayImage;
private Button startRefreshingButton;
private ImageContent imageContent;
private AnimationTimer animationTimer;
public MainWindow(ImageContent imageContent) {
this.imageContent = imageContent;
//Builds the window's components
buildGraphic();
//The image is reset at each frame
animationTimer = new AnimationTimer() {
@Override
public void handle(long now) {
displayImage.setImage(imageContent.getDisplayableImage());
}
};
}
private void buildGraphic() {
pane = new BorderPane();
menuBar = new MenuBar();
Menu menu = new Menu("File");
menu.getItems().addAll(new MenuItem("Save"),
new MenuItem("Open"),
new MenuItem("Close"));
menuBar.getMenus().add(menu);
displayImage = new ImageView();
startRefreshingButton = new Button("Start");
startRefreshingButton.setOnAction((event) -> {
animationTimer.start();
});
pane.setTop(menuBar);
pane.setCenter(displayImage);
pane.setBottom(startRefreshingButton);
}
public Pane getPane() {
return pane;
}
}
public class ImageContent {
private BufferedImage imageContent;
//Initializes bufferedimage with the path specified
public ImageContent(String pathToImage) throws IOException {
imageContent = ImageIO.read(new File(pathToImage));
}
public void setImage(BufferedImage newImage) {
imageContent = newImage;
}
//Function called by the animation timer to
//get a JavaFX image from a bufferedimage
public Image getDisplayableImage() {
return SwingFXUtils.toFXImage(imageContent, null);
}
}
}
I guess the issue is that since you're repainting the image every frame, you're overlaying the menu popup with the image. That seems like a bug, but you're also requesting way more work from the FX Application Thread than you need.
Ideally, you should find a way to check if there's really a new image, and only update the image if there's genuinely a new file. (Consider using java.nio.file.Path
to represent the file and calling Files.getLastModifiedTime(path)
.)
For another way to avoid flooding the FX Application Thread with too many Platform.runLater(...)
calls, see Throttling javafx gui updates