JavaFX: interaction between AnimationTimer and MenuBar - java

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:
Is that really where this bug comes from?
Is there a way to improve my code, using something different from an 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

In the end I didn't file any issue on jira since I was able to solve my problem. The issue came from the way I called SwingFXUtils.toFXImage(imageContent, null). I returned this function's result on every frame, and I am not sure about the details but this probably created a new object every time. A simple way to avoid that is passing a WritableImage as parameter, and binding the ImageProperty value of the ImageView to it.
If I take the MCVE posted above this could something like this (not tested, probably cleaner solutions existing):
public class MainWindow {
private BorderPane pane;
private MenuBar menuBar;
private ImageView displayImage;
private Button startRefreshingButton;
private ImageContent imageContent;
private AnimationTimer animationTimer;
// Here's the value to bind
private ObservableValue<WritableImage> imageProperty;
public MainWindow(ImageContent imageContent) {
//initialization stuff
this.imageProperty = new ObservableValue<>(imageContent.getWritableImage());
displayImage.imageProperty().bind(imageProperty);
//The image is reset at each frame
animationTimer = new AnimationTimer() {
#Override
public void handle(long now) {
SwingFXUtils.toFXImage(imageContent.getBufferedImage(), image.getWritableImage());
}
};
}
}
public class ImageContent {
private BufferedImage imageContent;
private WritableImage writableImage;
//Initializes bufferedimage with the path specified
public ImageContent(String pathToImage) throws IOException {
imageContent = ImageIO.read(new File(pathToImage));
//Get the width and height values from your image
int width = imageContent.getWidth();
int height = imageContent.getHeight();
writableImage = new WritableImage(width, height);
}
public void setImage(BufferedImage newImage) {
imageContent = newImage;
}
public WritableImage getWritableImage() {
return writableImage;
}
public BufferedImage getBufferedImage() {
return imageContent;
}
}
However, this seems to be quite memory intensive now, I'll look into it.

Related

Node.snapshot(null, null) changes size of Scene

I have Scene which is set to the Scene of my primaryStage that - amongst other nodes - contains a VBox with a TableView and some buttons. When I take a snapshot on a row in the table using TableRow.snapshot(null, null), the size of the Scene is changed. The width is changed by about 10 pixels while the height is changed by about 40 - sometimes more than 600 (!) - pixels.
This happens because Node.snapshot(null, null) invokes Scene.doCSSLayoutSyncForSnapshot(Node node) which seems to get the preferred size of all nodes in the size and recalculate the size using that. This somehow returns the wrong values since my nodes only has preferred sizes specified and looks great before this method is invoked. Is there any way to prevent this?
The size change is a problem, but it is also a problem that the primary stage doesn't change size with the Scene that it contains.
I have tried to create an MCVE reproducing the issue, but after a few days of trying to do this, I am still unable to reproduce the problem. The original program contains around 2000 lines of code that I don't want to post here.
Why would Scene.doCSSLayoutSyncForSnapshot(Node node) compromise my layout when it is properly laid out in the first place? Can I somehow make sure that the layout is properly synced before this method is invoked to make sure that it doesn't change anything?
Solved the issue. Had to copy my whole project and then remove parts of the code until the issue disappeared.
Anyway. I basically had three components in my application. A navigation component, a table compontent, and a status bar compontent. It looked like this:
The problem I had was that the width of the status bar and the width and height of the table component was increased whenever I took a snapshot of a row in the table.
Apparently, this was due to the padding of the status bar compontent. It had a right and left padding of 5 pixels, and once I removed the padding, the problem disappeared.
The added 10 pixels in width made the BorderPane that contained all of this expand with the same amount of pixels, and since the table width was bound to the BorderPane width, it increased by the same amount. What I still don't understand though, is why the Stage that contains the BorderPane doesn't adjust to the new width.
The component was properly padded before Scene.doCSSLayoutSyncForSnapshot(Node node) was invoked, so I don't understand why the extra width of ten pixels is added.
Anyhow: Removing the padding from the status bar component and instead padding the components inside the status bar fixed the issue. If someone has a good explanation for this, I'm all ears.
Here's a MCVE where you can reproduce the issue by dragging a row in the table:
import java.io.File;
import java.sql.SQLException;
import java.util.ArrayList;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class MCVE extends Application {
private Stage primaryStage;
private BorderPane rootLayout;
private VBox detailsView;
private StatusBar statusBar;
public void start(Stage primaryStage) throws SQLException {
this.primaryStage = primaryStage;
this.primaryStage.setTitle("MCVE");
initRootLayout();
showStatusBar();
showDetailsView();
detailsView.prefWidthProperty().bind(rootLayout.widthProperty());
detailsView.prefHeightProperty().bind(rootLayout.heightProperty());
}
#Override
public void init() throws Exception {
super.init();
}
public void initRootLayout() {
rootLayout = new BorderPane();
primaryStage.setWidth(1000);
primaryStage.setHeight(600);
Scene scene = new Scene(rootLayout);
primaryStage.setScene(scene);
primaryStage.show();
}
public void showStatusBar() {
statusBar = new StatusBar();
rootLayout.setBottom(statusBar);
}
public void showDetailsView() {
detailsView = new VBox();
rootLayout.setCenter(detailsView);
setDetailsView(new Table(this));
detailsView.prefHeightProperty().bind(primaryStage.heightProperty());
detailsView.setMaxHeight(Region.USE_PREF_SIZE);
}
public static void main(String[] args) {
launch(args);
}
public VBox getDetailsView() {
return detailsView;
}
public void setDetailsView(Node content) {
detailsView.getChildren().add(0, content);
}
public StatusBar getStatusBar() {
return statusBar;
}
class StatusBar extends HBox {
public StatusBar() {
setPadding(new Insets(0, 5, 0, 5));
HBox leftBox = new HBox(10);
getChildren().addAll(leftBox);
/**
* CONTROL SIZES
*/
setPrefHeight(28);
setMinHeight(28);
setMaxHeight(28);
// Leftbox takes all the space not occupied by the helpbox.
leftBox.prefWidthProperty().bind(widthProperty());
setStyle("-fx-border-color: black;");
}
}
class Table extends TableView<ObservableList<String>> {
private ObservableList<ObservableList<String>> data;
public Table(MCVE app) {
prefWidthProperty().bind(app.getDetailsView().widthProperty());
prefHeightProperty()
.bind(app.getDetailsView().heightProperty());
widthProperty().addListener((obs, oldValue, newValue) -> {
System.out.println("Table width: " + newValue);
});
setRowFactory(r -> {
TableRow<ObservableList<String>> row = new TableRow<ObservableList<String>>();
row.setOnDragDetected(e -> {
Dragboard db = row.startDragAndDrop(TransferMode.ANY);
db.setDragView(row.snapshot(null, null));
ArrayList<File> files = new ArrayList<File>();
// We create a clipboard and put all of the files that
// was selected into the clipboard.
ClipboardContent filesToCopyClipboard = new ClipboardContent();
filesToCopyClipboard.putFiles(files);
db.setContent(filesToCopyClipboard);
});
row.setOnDragDone(e -> {
e.consume();
});
return row;
});
ObservableList<String> columnNames = FXCollections.observableArrayList("Col1", "col2", "Col3", "Col4");
data = FXCollections.observableArrayList();
for (int i = 0; i < columnNames.size(); i++) {
final int colIndex = i;
TableColumn<ObservableList<String>, String> column = new TableColumn<ObservableList<String>, String>(
columnNames.get(i));
column.setCellValueFactory((param) -> new SimpleStringProperty(param.getValue().get(colIndex).toString()));
getColumns().add(column);
}
// Adds all of the data from the rows the data list.
for (int i = 0; i < 100; i++) {
// Each column from the row is a String in the list.
ObservableList<String> row = FXCollections.observableArrayList();
row.add("Column 1");
row.add("Column 2");
row.add("Column 3");
row.add("Column 4");
// Adds the row to data.
data.add(row);
}
// Adds all of the rows in data to the table.
setItems(data);
}
}
}
This answer talks about it a little bit
Set scene width and height
but after diving into the source code I found that the resizing in snapshot is conditional on the scene never having a size set by one of its constructors.
You can only set a scene's size in its constructors and never again. That makes a little bit of sense, since its otherwise only used to size the window that contains it. It is unfortunate that the snapshot code is not smart enough to use the window's dimensions when set by the user in addition to the scene's possible user settings.
None of this prevents resizing later, so if you depend on taking snapshots, you may want to make a best practice out of using the Scene constructors which take a width and height and sending them something above 0

Easily print image on the screen for debug purposes in a blocking manner

I am working on a computer vision project and somewhere in a process an endless loop happens. It seems that my image data is being corrupted.
In past, I used to save debug results on the disk using this method:
public static boolean saveToPath(String path, BufferedImage image) {
File img = new File(path);
try {
ImageIO.write(image, "png", new File(path));
} catch (IOException ex) {
System.err.println("Failed to save image as '"+path+"'. Error:"+ex);
return false;
}
return true;
}
The problem is that once loops are used and the error is somewhere inbetween, I need to see many images. So basically, I'd like a method that would be defined like this:
/** Displays image on the screen and stops the execution until the window with image is closed.
*
* #param image image to be displayed
*/
public static void printImage(BufferedImage image) {
???
}
And could be called in a loop or any function to show be the actual image, effectively behaving like a break point. Because while multithreading is very good in production code, blocking functions are much better for debugging.
You can code something like this. In this example, the image file has to be in the same directory as the source code.
Here's the image displayed in a dialog. You left click the OK button to continue processing.
If the image is bigger than your screen, scroll bars will appear to let you see the whole image.
In your code, since you already have the Image, you can just copy and paste the displayImage method.
package com.ggl.testing;
import java.awt.Image;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
public class DisplayImage {
public DisplayImage() {
displayImage(getImage());
}
private Image getImage() {
try {
return ImageIO.read(getClass().getResourceAsStream(
"StockMarket.png"));
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public void displayImage(Image image) {
JLabel label = new JLabel(new ImageIcon(image));
JPanel panel = new JPanel();
panel.add(label);
JScrollPane scrollPane = new JScrollPane(panel);
JOptionPane.showMessageDialog(null, scrollPane);
}
public static void main(String[] args) {
new DisplayImage();
}
}

Render GUI to Image in Memory

Is it somehow possible to render a GUI to a BufferedImage or another kind of memory image without displaying it on a screen ?
I know this will loose all kinds of hardware acceleration, but for a simple GUI that is refreshed only once or twice a second this should not be an issue.
Already tried to get JavaFX to output an image, but i can't find a way to leave out rendering on a screen first. Does anyone know a way to do this with JavaFX or Swing ?
It is no problem to draw a simple GUI myself using simple image manipulations, but then i would have to do it all by hand and using Swing or FX would make it much easier.
Edit:
To make it a bit more clear, i don't have an active display, but i can save an image which then gets displayed through other means. To be exact its a raspberry pi, but without a primary display device with a connected tft display using the GPIO port. So i can't render the UI directly to a display device, but need to create an image that i can save at a specific location. All methods i have tried so far need a primary display device.
Yes, it is possible to render a GUI to an image offscreen.
Here is a sample using JavaFX, with example image output as below:
The example works by rendering the chart to an scene which is not added to any window and no window (Stage in JavaFX terminology) is ever shown. The snapshot method is used to take a snapshot of the node and then ImageIO utilities are used to save the snapshot to disk.
Rendering of the offscreen scene will be hardware accelerated if the underlying hardware/software platform supports it.
import javafx.application.*;
import javafx.collections.*;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.*;
import javafx.scene.chart.PieChart;
import javafx.scene.image.Image;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.*;
public class OffscreenImageRecorder extends Application {
private static final Logger logger = Logger.getLogger(OffscreenImageRecorder.class.getName());
private static final String IMAGE_TYPE = "png";
private static final String IMAGE_FILENAME = "image." + IMAGE_TYPE;
private static final String WORKING_DIR = System.getProperty("user.dir");
private static final String IMAGE_PATH = new File(WORKING_DIR, IMAGE_FILENAME).getPath();
private final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss.SSS");
private final Random random = new Random();
private final int CHART_SIZE = 400;
#Override
public void start(Stage stage) throws IOException {
Parent chart = createChart();
Image image = snapshot(chart);
exportPng(SwingFXUtils.fromFXImage(image, null), IMAGE_PATH);
Platform.exit();
}
private Parent createChart() {
// create a chart.
final PieChart chart = new PieChart();
ObservableList<PieChart.Data> pieChartData =
FXCollections.observableArrayList(
new PieChart.Data("Grapefruit", random.nextInt(30)),
new PieChart.Data("Oranges", random.nextInt(30)),
new PieChart.Data("Plums", random.nextInt(30)),
new PieChart.Data("Pears", random.nextInt(30)),
new PieChart.Data("Apples", random.nextInt(30))
);
chart.setData(pieChartData);
chart.setTitle("Imported Fruits - " + dateFormat.format(new Date()));
// It is important for snapshots that the chart is not animated
// otherwise we could get a snapshot of the chart before the
// data display has been animated in.
chart.setAnimated(false);
chart.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
chart.setPrefSize(CHART_SIZE, CHART_SIZE);
chart.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
return chart;
}
private Image snapshot(final Parent sourceNode) {
// Note: if the source node is not in a scene, css styles will not
// be applied during a snapshot which may result in incorrect rendering.
final Scene snapshotScene = new Scene(sourceNode);
return sourceNode.snapshot(
new SnapshotParameters(),
null
);
}
private void exportPng(BufferedImage image, String filename) {
try {
ImageIO.write(image, IMAGE_TYPE, new File(filename));
logger.log(Level.INFO, "Wrote image to: " + filename);
} catch (IOException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
public static void main(String[] args) {
launch(args);
}
}
It's a bit of a hack, but you could create a frame and position it on a invisible location (using Swing in this example):
frame = new JFrame("Invisible frame");
frame.setBounds(-1000, 100, 640, 480);

How to resize an image when resizing the window in JavaFX

I want an image to be resized automatically when the user drags the main window. Is that possible?
I have the following code that sets a window of a certain size. It also loads the image from an external URL.
#Override
public void start(Stage primaryStage) {
MenuBar menuBar=new MenuBar();
Menu menuGame = new Menu("Game");
MenuItem newGame = new MenuItem("New Game F1");
MenuItem exit = new MenuItem("Exit F2");
exit.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
primaryStage.close();
}
});
menuGame.getItems().addAll(newGame,new SeparatorMenuItem(),exit);
menuBar.getMenus().addAll(menuGame);
Image image = new Image("http://docs.oracle.com/javafx/"
+ "javafx/images/javafx-documentation.png");
ImageView imageView = new ImageView();
imageView.setImage(image);
VBox vbox=new VBox();
StackPane root=new StackPane();
root.getChildren().addAll(imageView);
vbox.getChildren().addAll(menuBar,root);
Scene scene= new Scene(vbox,400,400);
primaryStage.setScene(scene);
primaryStage.setMaxHeight(800);
primaryStage.setMinHeight(400);
primaryStage.setMaxWidth(1000);
primaryStage.setMinWidth(800);
primaryStage.setTitle("Minesweeper");
primaryStage.show();
}
Applying the solution to JavaFx image resizing to your sample code and resizing the window results in different image sizes for me.
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.beans.value.*;
import javafx.event.*;
import javafx.geometry.HPos;
import javafx.geometry.VPos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
public class ImageResizer extends Application {
#Override
public void start(Stage primaryStage) {
MenuBar menuBar=new MenuBar();
Menu menuGame = new Menu("Game");
MenuItem newGame = new MenuItem("New Game F1");
MenuItem exit = new MenuItem("Exit F2");
exit.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
primaryStage.close();
}
});
menuGame.getItems().addAll(newGame,new SeparatorMenuItem(),exit);
menuBar.getMenus().addAll(menuGame);
Image image = new Image("http://docs.oracle.com/javafx/"
+ "javafx/images/javafx-documentation.png");
ImageView imageView = new ImageView();
imageView.setImage(image);
ImageViewPane viewPane = new ImageViewPane(imageView);
VBox vbox=new VBox();
StackPane root=new StackPane();
root.getChildren().addAll(viewPane);
vbox.getChildren().addAll(menuBar,root);
VBox.setVgrow(root, Priority.ALWAYS);
Scene scene= new Scene(vbox,200,200);
primaryStage.setScene(scene);
primaryStage.setMaxHeight(400);
primaryStage.setMinHeight(200);
primaryStage.setMaxWidth(500);
primaryStage.setMinWidth(400);
primaryStage.setTitle("Minesweeper");
primaryStage.show();
}
public static void main(String[] args) { launch(); }
}
/*
* Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*/
/**
*
* #author akouznet
*/
class ImageViewPane extends Region {
private ObjectProperty<ImageView> imageViewProperty = new SimpleObjectProperty<ImageView>();
public ObjectProperty<ImageView> imageViewProperty() {
return imageViewProperty;
}
public ImageView getImageView() {
return imageViewProperty.get();
}
public void setImageView(ImageView imageView) {
this.imageViewProperty.set(imageView);
}
public ImageViewPane() {
this(new ImageView());
}
#Override
protected void layoutChildren() {
ImageView imageView = imageViewProperty.get();
if (imageView != null) {
imageView.setFitWidth(getWidth());
imageView.setFitHeight(getHeight());
layoutInArea(imageView, 0, 0, getWidth(), getHeight(), 0, HPos.CENTER, VPos.CENTER);
}
super.layoutChildren();
}
public ImageViewPane(ImageView imageView) {
imageViewProperty.addListener(new ChangeListener<ImageView>() {
#Override
public void changed(ObservableValue<? extends ImageView> arg0, ImageView oldIV, ImageView newIV) {
if (oldIV != null) {
getChildren().remove(oldIV);
}
if (newIV != null) {
getChildren().add(newIV);
}
}
});
this.imageViewProperty.set(imageView);
}
}
Alternate approaches and extra information based upon comments
If you have to do all that, then it's a weakness in he JavaFX platform. Ideally I would expect there to have been a scale property on the image that one could set so that it uses the SceneGraph to determine it's size.
The solution presented above is just one answer, there are others possible. The scene graph can be used by binding various properties to the width and height of the parent node or by overriding layoutChildren in the parent node.
Related properties:
There is a scale property which can be applied to any node.
A node also has a transform list to which a Scale may be applied.
ImageView has a fitWidth and fitHeight properties (use of this is demonstrated in other answers).
I prefer the region subclass approach to a scale property or transform based approach because then the image is automatically sized based upon the layout manager. The ImageViewPane defined above is just a one-off definition class which can be reused as much as you want, the actual application code to use an ImageView or ImageViewPane is pretty much equivalent.
Another possible approach is to use a Region subclass (such as a Pane), which has a defined CSS style class or id and then to define the image in CSS as a background. The nice thing about a CSS defined image is that you can then use additional CSS based attributes to define things, like sizing, scaling, positioning and repeats for the image. This background can also be set programmatically if desired rather than via CSS.
Related question:
Resizing images to fit the parent node
To maintain proportional height and width of the image
The following code can be used in the ImageViewPane class provided above:
if (imageView.isPreserveRatio()) {
if (getHeight() > getWidth()) {
imageView.setFitWidth(getWidth());
imageView.setFitHeight(0);
} else {
imageView.setFitWidth(0);
imageView.setFitHeight(getHeight());
}
} else {
imageView.setFitWidth(getWidth());
imageView.setFitHeight(getHeight());
}
I know this is old but someone like me may stumble upon this so here is my solution. I am assuming that root is the root node of your scene. However, this should work if your parent node of the ImageView is any child of Pane.
imv = new ImageView();
root.getChildren().add(imv);
Image image = new Image(Main.class.getResourceAsStream("image.png"))
imv.setImage(image);
imv.fitWidthProperty().bind(center.widthProperty());
imv.fitHeightProperty().bind(center.heightProperty());
Just use a normal pane with a background image, here the css needed.
#titleImage{
-fx-background-image: url("../img/logo.png");
-fx-background-repeat: stretch;
-fx-background-position: center center;
}
Are you looking to resize an actual image, or just to center it?
You can try this: VBox.setVgrow(root, Priority.ALWAYS); and see if that's what you want.
Also, be careful with you image, because it's a png image. I'm a bit skeptic regarding what's resized there (the actual image or the transparent background).

making jfilechooser show image thumbnails

I wanted to create a JFileChooser with thumbnail view of image files.So I subclassed FileView and in the method which creates ImageIcon did some scaling sothat thumbnail images are shown.
However,the overall effect is that, the filechooser widget takes some time before opening a directory and showing thumbnails..In createImageIcon() below,I need to call new ImageIcon() twice-once with the image filepath and next with the resized image as constructor argument.I think this is what slows the widget .
Is there a more efficient alternative?Any suggestions/pointers most welcome.
thanks,
mark
public static void main(String[] args) {
JFileChooser chooser=new JFileChooser();
ThumbNailView thumbView=new ThumbNailView();
chooser.setFileView(thumbView);
}
class ThumbNailView extends FileView{
public Icon getIcon(File f){
Icon icon=null;
if(isImageFile(f.getPath())){
icon=createImageIcon(f.getPath(),null);
}
return icon;
}
private ImageIcon createImageIcon(String path,String description) {
if (path != null) {
ImageIcon icon=new ImageIcon(path);
Image img = icon.getImage() ;
Image newimg = img.getScaledInstance( 16, 16, java.awt.Image.SCALE_SMOOTH ) ;
return new ImageIcon(newimg);
} else {
System.err.println("Couldn't find file: " + path);
return null;
}
}
private boolean isImageFile(String filename){
//return true if this is image
}
I was actually surprised to see that, despite using the native look & feel in Windows, the file chooser indeed doesn't have a thumbnail view. I tried your example and you're going along the right lines, but I see how slow it was for folders with a lot of large images. The overhead is, of course, due to I/O when reading the file contents and then interpreting the image, which is unavoidable.
What's even worse, is that I found out that FileView.getIcon(File) is called a lot - before the file list is shown, when you mouse over an icon, and when the selection changes. If we don't cache the images after loading them, we'll be pointlessly reloading images all the time.
The obvious solution is to push all the image loading off onto another thread or a thread pool, and once we have our scaled-down result, put it into a temporary cache so it can be retrieved again.
I played around with Image and ImageIcon a lot and I discovered that an ImageIcon's image can be changed at any time by calling setImage(Image). What this means for us is, within getIcon(File), we can immediately return a blank or default icon, but keep a reference to it, passing it along to a worker thread that will load the image in the background and set the icon's image later when it's done (The only catch is that we must call repaint() to see the change).
For this example, I'm using an ExecutorService cached thread pool (this is the fastest way to get all images, but uses a lot of I/O) to process the image loading tasks. I'm also using a WeakHashMap as the cache, to ensure that we only hold onto the cached icons for as long as we need them. You could use another kind of Map, but you would have to manage the number of icons you hold onto, to avoid running out of memory.
package guitest;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JFileChooser;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.filechooser.FileView;
public class ThumbnailFileChooser extends JFileChooser {
/** All preview icons will be this width and height */
private static final int ICON_SIZE = 16;
/** This blank icon will be used while previews are loading */
private static final Image LOADING_IMAGE = new BufferedImage(ICON_SIZE, ICON_SIZE, BufferedImage.TYPE_INT_ARGB);
/** Edit this to determine what file types will be previewed. */
private final Pattern imageFilePattern = Pattern.compile(".+?\\.(png|jpe?g|gif|tiff?)$", Pattern.CASE_INSENSITIVE);
/** Use a weak hash map to cache images until the next garbage collection (saves memory) */
private final Map imageCache = new WeakHashMap();
public static void main(String[] args) throws Exception {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
JFileChooser chooser = new ThumbnailFileChooser();
chooser.showOpenDialog(null);
System.exit(1);
}
public ThumbnailFileChooser() {
super();
}
// --- Override the other constructors as needed ---
{
// This initializer block is always executed after any constructor call.
setFileView(new ThumbnailView());
}
private class ThumbnailView extends FileView {
/** This thread pool is where the thumnnail icon loaders run */
private final ExecutorService executor = Executors.newCachedThreadPool();
public Icon getIcon(File file) {
if (!imageFilePattern.matcher(file.getName()).matches()) {
return null;
}
// Our cache makes browsing back and forth lightning-fast! :D
synchronized (imageCache) {
ImageIcon icon = imageCache.get(file);
if (icon == null) {
// Create a new icon with the default image
icon = new ImageIcon(LOADING_IMAGE);
// Add to the cache
imageCache.put(file, icon);
// Submit a new task to load the image and update the icon
executor.submit(new ThumbnailIconLoader(icon, file));
}
return icon;
}
}
}
private class ThumbnailIconLoader implements Runnable {
private final ImageIcon icon;
private final File file;
public ThumbnailIconLoader(ImageIcon i, File f) {
icon = i;
file = f;
}
public void run() {
System.out.println("Loading image: " + file);
// Load and scale the image down, then replace the icon's old image with the new one.
ImageIcon newIcon = new ImageIcon(file.getAbsolutePath());
Image img = newIcon.getImage().getScaledInstance(ICON_SIZE, ICON_SIZE, Image.SCALE_SMOOTH);
icon.setImage(img);
// Repaint the dialog so we see the new icon.
SwingUtilities.invokeLater(new Runnable() {public void run() {repaint();}});
}
}
}
Known issues:
1) We don't maintain the image's aspect ratio when scaling. Doing so could result in icons with strange dimensions that will break the alignment of the list view. The solution is probably to create a new BufferedImage that is 16x16 and render the scaled image on top of it, centered. You can implement that if you wish!
2) If a file is not an image, or is corrupted, no icon will be shown at all. It looks like the program only detects this error while rendering the image, not when we load or scale it, so we can't detect this in advance. However, we might detect it if we fix issue 1.
Use fileDialog instead of JfileChooser for choising the image:
FileDialog fd = new FileDialog(frame, "Test", FileDialog.LOAD);
String Image_path
fd.setVisible(true);
name = fd.getDirectory() + fd.getFile();
image_path=name;
ImageIcon icon= new ImageIcon(name);
icon.setImage(icon.getImage().getScaledInstance(jLabel2.getWidth(),jLabel2.getHeight() , Image.SCALE_DEFAULT));
jLabel2.setIcon(icon);
You could use a default icon for each fileand load the actual icons in another thread (perhaps using a SwingWorker?). As the icons are loaded the SwingWorker could call back and update the FileView.
Not sure if a single SwingWorker would do the trick, or whether it would be better to use one for each icon being loaded.

Categories

Resources