The scenario: the top-level container is a Swing JDialog which has some fx content, including a fx button that triggers a dispose of the button. Disposing works a expected (dialog is hidden) when the button is created and configured with the appropriate eventHandler manually. The dialog is not disposed when the button is created/configure via fxml. The example below contains both a manually configured and a fxml loaded/bound button to see the different behaviour.
Questions:
anything wrong with the example?
is there any difference in swing/fx interaction to be expected (manual vs. fxml)?
how to make it work from fxml?
The code:
package fxml;
import java.io.IOException;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.FlowPane;
import javax.swing.JDialog;
import javax.swing.SwingUtilities;
public class DisposeExample {
#FXML
Button closeButton;
Button fxButton;
private JDialog dialog;
/**
* The action handler method used by fx buttons.
*/
public void onAction(final ActionEvent ac) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
System.out.println("onAction: " +ac);
dialog.dispose();
}
});
}
protected Button createFxButton() {
Button fxButton = new Button("close from fx");
fxButton.setOnAction(new javafx.event.EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
onAction(event);
}
});
return fxButton;
}
public void initFX() {
final JFXPanel fxPanel = new JFXPanel();
dialog.add(fxPanel);
Platform.runLater(new Runnable() {
#Override
public void run() {
FlowPane parent = null;
try {
parent = FXMLLoader.load(DisposeExample.class.getResource(
"DisposeController.fxml"));
} catch (IOException e) {
e.printStackTrace();
}
fxButton = createFxButton();
parent.getChildren().add(fxButton);
Scene scene = new Scene(parent);
fxPanel.setScene(scene);
}
});
}
public DisposeExample() {
dialog = new JDialog();
dialog.setTitle("Simple Swing Dialog");
initFX();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
JDialog example = new DisposeExample().dialog;
example.setSize(400, 400);
example.setVisible(true);
}
});
}
}
The fxml content:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<FlowPane id="Content" fx:id="windowPanel"
xmlns:fx="http://javafx.com/fxml" fx:controller="fxml.DisposeExample">
<children>
<Button fx:id="closeButton" onAction="#onAction"
prefHeight="35.0" prefWidth="300.0" text="Close (fxml controller)">
</Button>
</children>
</FlowPane>
BTW: there's a similar question from the beginning of this year, unanswered.
Edit
something weird going on: after running for a couple of minutes, it throws a OutOfMemoryError - something deeper down doesn't stop creating .. what?
java.lang.OutOfMemoryError: Java heap space
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java:2521)
at java.lang.Class.privateGetPublicMethods(Class.java:2641)
at java.lang.Class.privateGetPublicMethods(Class.java:2657)
at java.lang.Class.privateGetPublicMethods(Class.java:2657)
at java.lang.Class.privateGetPublicMethods(Class.java:2657)
at java.lang.Class.privateGetPublicMethods(Class.java:2657)
at java.lang.Class.privateGetPublicMethods(Class.java:2657)
at java.lang.Class.getMethods(Class.java:1457)
at sun.reflect.misc.MethodUtil.getMethods(MethodUtil.java:99)
at com.sun.javafx.fxml.BeanAdapter.updateMethodCache(BeanAdapter.java:265)
at com.sun.javafx.fxml.BeanAdapter.setBean(BeanAdapter.java:250)
at com.sun.javafx.fxml.BeanAdapter.<init>(BeanAdapter.java:213)
at javafx.fxml.FXMLLoader$Element.getValueAdapter(FXMLLoader.java:157)
at javafx.fxml.FXMLLoader$Element.getProperties(FXMLLoader.java:165)
at javafx.fxml.FXMLLoader$ValueElement.processValue(FXMLLoader.java:647)
at javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:570)
at javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2314)
at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2131)
at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2028)
at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2744)
at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2723)
at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2709)
at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2696)
at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2685)
at fxml.DisposeExample$3.run(DisposeExample.java:65)
at com.sun.javafx.application.PlatformImpl$4$1.run(PlatformImpl.java:179)
at com.sun.javafx.application.PlatformImpl$4$1.run(PlatformImpl.java:176)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.application.PlatformImpl$4.run(PlatformImpl.java:176)
at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at com.sun.glass.ui.win.WinApplication.access$100(WinApplication.java:29)
Edit
fyi: same behaviour in 8u113, so filed an issue in fx-jira, incurable optimist that I am :-)
anything wrong with the example?
a resounding YES - citing Martin's (very quick and clear :-) comment to the issue:
The problem is in your fxml file.
"fx:controller" attribute takes the class and creates a new instance
of it (using the default contructor). The default constructor of your
DisposeExample class posts a new Runnable that will load the same fxml
file again a create yet another instance of DisposeExample class.
You should either use a different class for your controller or set the
controller manually using the setController() call or using a
controller factory (setControllerFactory). Otherwise, there is no way
for FXMLLoader to know that you wanted to use your particular
DisposeExample object.
Bug alert - remove the controller from the FXML-file and set it in the code instead.
FXMLLoader fxmlLoader = new FXMLLoader(DisposeExample.class.getResource("DisposeController.fxml"));
fxmlLoader.setController(DisposeExample.this);
parent = (FlowPane)fxmlLoader.load();
Unfortunately this will also destroy the FX-CPU-heating on these cold days ;-)
So why are you not executing the dispose on the swing thread?
Related
I am using TitledPanes ScrollPanes and TableViews and I have the problem, when I collapse a titledPane, the horizontal ScrollBar of the TableView resets.
Here is a code example where you can verify it:
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TableView;
import javafx.scene.control.TitledPane;
import javafx.scene.layout.AnchorPane;
import java.net.URL;
import java.util.ResourceBundle;
public class Controller implements Initializable {
#FXML
private AnchorPane content;
#FXML
private TitledPane titledPane;
#FXML
private TableView<Object> tableView;
#Override
public void initialize(URL location, ResourceBundle resources) {
titledPane.prefHeightProperty().bind(content.heightProperty());
tableView.prefWidthProperty().bind(content.widthProperty());
tableView.getColumns().forEach(col -> col.setPrefWidth(300)); // to have enough "space" to scroll
tableView.setItems(FXCollections.observableArrayList(new Object()));
}
}
FXML:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="stackoverflow.testscroll.Controller"
fx:id="content">
<TitledPane fx:id="titledPane">
<TableView fx:id="tableView">
<columns>
<TableColumn/>
<TableColumn/>
<TableColumn/>
<TableColumn/>
<TableColumn/>
<TableColumn/>
<TableColumn/>
<TableColumn/>
</columns>
</TableView>
</TitledPane>
</AnchorPane>
Any idea how can I prevent the scroll of the tableview to reset every time I collapse the pane?
After a bit of digging, it looks like some layout optimization in VirtualFlow might be the reason (all seems to be fine if the scrolled content is not a TableView - not thoroughly analyzed, though)
What happens is:
during collapse, the TitledPane's content is resized vertically to 0
in VirtualFlow's layoutChildren a zero height/width is special cased to do nothing except hide everything, including the scrollBars
an internal listener to the scrollBar's visiblilty resets its value to 0
A tentative (read: dirty and might have unwanted side-effects, totally untested beyond this quick outline!) hack around is a custom TableViewSkin that tries to "remember" the last not-zero value and resets it on getting visible again.
An example:
public class TitledPaneTableScroll extends Application {
public static class TableViewScrollSkin<T> extends TableViewSkin<T> {
DoubleProperty hvalue = new SimpleDoubleProperty();
public TableViewScrollSkin(TableView<T> control) {
super(control);
installHBarTweak();
}
private void installHBarTweak() {
// Note: flow and bar could be legally retrieved via lookup
// protected api pre-fx9 and post-fx9
VirtualFlow<?> flow = getVirtualFlow();
// access scrollBar via reflection
// this is my personal reflective access utility method - use your own :)
ScrollBar bar = (ScrollBar) FXUtils
.invokeGetFieldValue(VirtualFlow.class, flow, "hbar");
bar.valueProperty().addListener((s, o, n) -> {
if (n.intValue() != 0) {
hvalue.set(n.doubleValue());
// debugging
// new RuntimeException("who is calling? \n").printStackTrace();
}
//LOG.info("hbar value: " + n + "visible? " + bar.isVisible());
});
bar.visibleProperty().addListener((s, o, n) -> {
if (n) {
bar.setValue(hvalue.get());
}
});
}
}
int counter;
private Parent createContent() {
TableView<Object> table = new TableView<>(FXCollections.observableArrayList(new Object()) ) {
#Override
protected Skin<?> createDefaultSkin() {
return new TableViewScrollSkin<>(this);
}
};
table.getColumns().addAll(Stream
.generate(TableColumn::new)
.limit(10)
.map(col -> {
col.setPrefWidth(50);
col.setText("" + counter++);
return col;
})
.collect(Collectors.toList()));
TitledPane titled = new TitledPane("title", table);
titled.setAnimated(true);
BorderPane content = new BorderPane(titled);
return content;
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent(), 400, 400));
// stage.setTitle(FXUtils.version());
stage.show();
}
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(TitledPaneTableScroll.class.getName());
}
I'm new to Java FX and am creating an application for fun. I'm trying to add a TitledPane dynamically and am getting Null Pointer Exceptions when attempting to lookup the title of the TitledPane about 70% of the time. I tried to create a simple demo of my issue, but was unable to reproduce the issue outside of my application, but I could solve my issue. I'm hoping someone could help me understand why my solution works and maybe point me in the direction of a better solution. I'm using an FXML file with a Controller. I'm attempting to lookup the title inside of Platform.runLater() because I'm manually editing the layout and elements of the title. Inside of the Controller's initialize function, I do the following to get null pointer exceptions:
Platform.runLater(new Runnable() {
#Override
public void run() {
titledpane.lookup(".title"); // will return null about 70% of the time
}
});
// Do more stuff
However, if I wrap that call in a timer and execute it in 500 ms, it doesn't seem to ever return Null.
new java.util.Timer().schedule(new java.util.TimerTask() {
#Override
public void run() {
Platform.runLater(new Runnable() {
#Override
public void run() {
titledpane.lookup(".title"); // doesn't seem to return null
}
});
}
}, 500);
One forum mentioned that CSS had to be applied to the element prior to calling a lookup on the title, so I tried manually applying CSS to the title, but titledpane.lookup(".title") still returned null. Can anyone help me understand what is happening here? Thanks in advance!
I had the same issue. I resolved it by calling applyCss() and layout() on the pane that contains the TitledPane:
Node loadedPane = paneLoader.load();
bodyPane.setCenter(loadedPane);
bodyPane.applyCss();
bodyPane.layout();
You should read the article Creating a Custom Control with FXML.
Here's an example about how you can load a TitledPane dynamically. A TitledPane is added each time you click the "Add Task" button.
Task.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.text.*?>
<?import javafx.scene.effect.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<fx:root collapsible="false" prefHeight="72.0" prefWidth="202.0" text="Task" type="TitledPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<content>
<Pane prefHeight="43.0" prefWidth="200.0">
<children>
</children>
</Pane>
</content>
</fx:root>
Task.java
import java.io.IOException;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.TitledPane;
import javafx.scene.layout.Region;
public class Task extends Region {
TitledPane titledPane;
public Task( String title) {
final FXMLLoader fxmlLoader = new FXMLLoader( getClass().getResource( "Task.fxml"));
titledPane = new TitledPane();
fxmlLoader.setRoot( titledPane);
fxmlLoader.setController( this);
try {
fxmlLoader.load();
} catch( IOException exception) {
throw new RuntimeException( exception);
}
titledPane.setText(title);
getChildren().add( titledPane);
}
}
Demo.java
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;
public class Demo extends Application {
#Override
public void start(Stage primaryStage) {
Group root = new Group();
Button addTaskButton = new Button( "Add Task");
addTaskButton.setOnAction(new EventHandler<ActionEvent>() {
double x=0;
double y=0;
int count=0;
#Override
public void handle(ActionEvent event) {
// calculate new position
x+=50;
y+=50;
// task counter
count++;
Task task = new Task( "Task " + count);
task.relocate(x, y);
root.getChildren().add( task);
}
});
root.getChildren().add( addTaskButton);
Scene scene = new Scene( root, 1024, 768);
primaryStage.setScene( scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Screenshot:
There are various solutions in order to create a custom title. Here's one. Note: You need to provide an icon in the proper path or adapt the path. Alternatively you can just disable the ImageView node and instead use the Rectangle for demonstration purposes. It's just another node that's displayed.
public class Task extends Region {
TitledPane titledPane;
public Task( String title) {
final FXMLLoader fxmlLoader = new FXMLLoader( getClass().getResource( "Task.fxml"));
titledPane = new TitledPane();
fxmlLoader.setRoot( titledPane);
fxmlLoader.setController( this);
try {
fxmlLoader.load();
} catch( IOException exception) {
throw new RuntimeException( exception);
}
// create custom title with text left and icon right
AnchorPane anchorpane = new AnchorPane();
double offsetRight = 20; // just an arbitrary value. usually the offset from the left of the title
anchorpane.prefWidthProperty().bind(titledPane.widthProperty().subtract( offsetRight));
// create text for title
Label label = new Label( title);
// create icon for title
Image image = new Image( getClass().getResource( "title.png").toExternalForm());
ImageView icon = new ImageView();
icon.setImage(image);
// Rectangle icon = new Rectangle(16, 16);
// set text and icon positions
AnchorPane.setLeftAnchor(label, 0.0);
AnchorPane.setRightAnchor(icon, 0.0);
// add text and icon to custom title
anchorpane.getChildren().addAll( label, icon);
// set custom title
titledPane.setGraphic( anchorpane);
// show only our custom title, don't show the text of titlePane
titledPane.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
getChildren().add( titledPane);
}
}
JavaFX has a method that is added to the controller:
public void initialize(URL url, ResourceBundle rb)
This seems to run before any of the controls are added to the scene, because when I add this to it:
#Override
public void initialize(URL url, ResourceBundle rb){
String treeItemCss = getClass().getResource("/media/css/TreeItem.css").getPath();
main.getScene().getStylesheets().add(treeItemCss);
}
The CSS:
.tree-cell{
-fx-indent: 100;
-fx-underline: true;
}
I get an error from this method: getStylesheets(). But if I move that to an OnAction and execute that action I get no errors.
So my question is, is there a method that runs after all the controls are added to the scene, or a good way to add css to items that are created from a user action, such as a button click?
The initialize() method runs at the end of the FXMLLoader's load() method. Since you don't get a reference to the root of the FXML until that completes, there's obviously no way you can add it to a scene until after then.
You can:
Add the css to the Scene in the application code. I.e. Somewhere you create an FXMLLoader, call load(), and add the result to the Scene. Just set the css file on the scene right there, or:
Add the css stylesheet to the root node instead of to the scene (assuming main is a Parent):
public void initialize() {
String treeItemCss = ... ;
main.getStylesheets().add(treeItemCss);
}
or:
Observe the Scene property and add the stylesheet when it changes to something not null:
public void initialize() {
String treeItemCss = ... ;
main.sceneProperty().addListener((obs, oldScene, newScene) -> {
if (newScene != null) {
newScene.getStylesheets().add(treeItemCss);
}
});
}
Update Here is a complete example to demonstrate the second option. Everything is in the "application" package:
Main.java:
package application;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
BorderPane root = FXMLLoader.load(getClass().getResource("Main.fxml"));
Scene scene = new Scene(root,400,400);
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
Main.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.TreeView?>
<?import javafx.scene.control.TreeItem?>
<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.MainController" fx:id="root">
<center>
<TreeView>
<root>
<TreeItem value="Root">
<children>
<TreeItem value="One"/>
<TreeItem value="Two"/>
<TreeItem value="Three"/>
</children>
</TreeItem>
</root>
</TreeView>
</center>
</BorderPane>
MainController.java:
package application;
import javafx.fxml.FXML;
import javafx.scene.layout.BorderPane;
public class MainController {
#FXML
private BorderPane root ;
public void initialize() {
root.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
}
}
application.css:
.tree-cell{
-fx-indent: 100;
-fx-underline: true;
}
Note that you can add the stylesheet directly in the FXML file with
<BorderPane xmlns:fx="..." fx:controller="..." stylesheets="#application.css">
and then omit it completely from the controller logic.
I´m newbe in JavaFX.
I have a problem with my JavaFX application, I need to start my ProgressIndicator (type INDETERMINATE) before a database query.
This is part of my code:
spinner.setVisible(true);
passCripto = cripto.criptoPass(pass);
MyDao dao = new MyDaoImpl();
boolean isLoginOk = dao.isUserOk(user, passCripto);
boolean isStatusOk = dao.idStatusUser(user);
When finished, I need to set spinner to FALSE but the spinner only is showed when database query is finished. How can I solve this ?
Thanks.
I do this, but not resolved:
Task<Boolean> taskValidaLogin = new Task<Boolean>() {
#Override
protected Boolean call() throws Exception {
boolean validaLogin = ACDCDao.validaUsuario(usuario, senhaCripto);
return validaLogin;
}
};
Task<Boolean> taskValidaStatus = new Task<Boolean>() {
#Override
protected Boolean call() throws Exception {
boolean validaStatus = ACDCDao.validaStatusUsuario(usuario);
return validaStatus;
}
};
new Thread(taskValidaLogin).start();
new Thread(taskValidaStatus).start();
if (taskValidaLogin.getValue()) {
if (taskValidaStatus.getValue()) {
//do something
}
You have to do your database stuff into an other thread, cause if the operation take time it will freez the JavaFX thread (The GUI)
In JavaFx you can use Service and Task to do background stuff. You should read
Concurency in javafx
Service
By executing your database stuff into a service, you will be able to monitor it easely cause Service provide the necessary to do that, and have method like onSuccedeed, onFailed...
Really have a look to that, is an essential part if you want to do JavaFx correctly.
Main.java
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
ServiceExample.java
import javafx.concurrent.Service;
import javafx.concurrent.Task;
public class ServiceExample extends Service<String> {
#Override
protected Task<String> createTask() {
return new Task<String>() {
#Override
protected String call() throws Exception {
//DO YOU HARD STUFF HERE
String res = "toto";
Thread.sleep(5000);
return res;
}
};
}
}
Controller.java
import javafx.concurrent.WorkerStateEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.control.ProgressIndicator;
public class Controller {
#FXML
private ProgressIndicator progressIndicator;
public void initialize() {
final ServiceExample serviceExample = new ServiceExample();
//Here you tell your progress indicator is visible only when the service is runing
progressIndicator.visibleProperty().bind(serviceExample.runningProperty());
serviceExample.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
#Override
public void handle(WorkerStateEvent workerStateEvent) {
String result = serviceExample.getValue(); //here you get the return value of your service
}
});
serviceExample.setOnFailed(new EventHandler<WorkerStateEvent>() {
#Override
public void handle(WorkerStateEvent workerStateEvent) {
//DO stuff on failed
}
});
serviceExample.restart(); //here you start your service
}
}
sample.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ProgressIndicator?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane prefHeight="200.0" prefWidth="200.0" xmlns:fx="http://javafx.com/fxml/1"
xmlns="http://javafx.com/javafx/2.2" fx:controller="Controller">
<ProgressIndicator fx:id="progressIndicator" layoutX="78.0" layoutY="55.0"/>
</AnchorPane>
I do that example quickly it's basic but I think it's what you want. (I don't add my progressIndicator to a node it's just for the example)
After spinner.setVisible(true);, start the spinner spinning by setting it to an indeterminate value (-1). spinner.setProgress(-1d);
Then I guess when the task is done you want it stopped. There are two events to use, succeeded and failed. That means the task failed, not the login. Here's an example for succeeded.
taskValidaStatus.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
#Override
public void handle(WorkerStateEvent event) {
spinner.setProgress(0d);
//and now you can check valida here after the task is finished
if (taskValidaLogin.getValue()) {
if (taskValidaStatus.getValue()) {
//do something
}
}
});
//now start the tasks after they're all set up.
new Thread(taskValidaLogin).start();
new Thread(taskValidaStatus).start();
I didn't test this code because you didn't provide a working example.
There's another issue in that validaLogin may not be finished yet. I only checked if one thread was finished. What you should do is start one thread in the other threads onSucceeded event. I'm not sure of the order you need, but it might look like this in the taskLogin onSucceeded.
if (taskValidaLogin.getValue()) {
new Thread(taskValidaStatus).start();
}
First of all I'm new at JavaFX, so sorry if this question is stupid. How can I get an return Object from a Task?
Heres my Problem:
I want to get a List of Objects from a Mock. The Mock has a delay from 1 up to 5 seconds. But I dont want, that my GUI freeze in this time.
In Java.Swing it was easy with an Thread, but JavaFX has, as far as I know, Tasks.
I've read much tutorials, but everywhere they return a text property. So here is my question: how can I set the value of an Object with the result of the calculation from a Task/Thread (in my case a List)
Thank you.
Ray,
You are right in that the examples seem to gloss over getting back results from tasks. There are two ways that you can get back results that I know of:
Through the getValue() method of the Task class (this is the way I have done it)
Through the get() method of the parent FutureTask class (I haven't used this, but in principle it should work).
With the first getValue() aproach you need to make sure the task sets the value through the updateValue(...) method in the call method of the task. Then put a listener on the WorkerStateEvent
myTask.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
#SuppressWarnings("unchecked")
#Override
public void handle(WorkerStateEvent event) {
ReturnType rt = (ReturnType) event.getSource().getValue()
// ... other stuff to do here ...
}
});
The first approach is a little verbose but it works and allows for some more complicated operations after the task has finished.
The second approach is a little simpler and straightforward but doesn't give you as much control over what to do when the task finishes. With the get() method of FutureTask, the code should block until the Task returns with the value. So using it should be as simple as:
//
// Start the task in a thread (using whatever approach you like)
//before calling the get() method.
//
ReturnType rt = myTask.get();
I have used Future objects with other code, but I have not used FutureTask with the FX api, so I can not tell you if there are hidden gotchas in it.
Good luck,
chooks
The Task is a generic type. That means that if you apply a type to a Task like Task<Integer> the Task class will have functions that returns you an Integer. One of this functions is the valueProperty(), that can be bind to other scene elements. Being shown in a Label, or whatever. I recommend you to read the javafx binding tutorial, to get a better comprehension.
Here is a sample of Task using or their properties.
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TestingTasks extends Application{
public static void main(String[] args) {launch(args);}
#Override
public void start(Stage stage) throws Exception {
VBox vbox = new VBox(10);
vbox.setAlignment(Pos.TOP_CENTER);
ListView<String> list = new ListView<>();
HBox hbox = new HBox(10);
hbox.setAlignment(Pos.CENTER_LEFT);
Label labelMessage = new Label();
hbox.getChildren().addAll(new Label("Message: "), labelMessage);
ProgressBar progress = new ProgressBar(-1);
progress.setVisible(false);
Button button = new Button("Executing Task");
button.setOnAction(event(button, list, progress, labelMessage));
vbox.getChildren().addAll(list, hbox, button, progress);
Scene scene = new Scene(vbox, 400, 300);
stage.setScene(scene);
stage.show();
}
private EventHandler<ActionEvent> event(final Button button, final ListView<String> list, final ProgressBar progress, final Label labelMessage) {
return new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
Task<ObservableList<String>> task = generateTask();
list.itemsProperty().bind(task.valueProperty());
progress.visibleProperty().bind(task.runningProperty());
labelMessage.textProperty().bind(task.messageProperty());
button.disableProperty().bind(task.runningProperty());
task.runningProperty().addListener(listenerRunningTask());
Thread t = new Thread(task);
t.setDaemon(true);
t.start();
}
};
}
private Task<ObservableList<String>> generateTask() {
return new Task<ObservableList<String>>() {
#Override
protected ObservableList<String> call() throws Exception {
updateMessage("Waiting...");
Thread.sleep(5000);
updateMessage("Waking up");
return FXCollections.observableArrayList("One", "Two", "Three");
}
};
}
private ChangeListener<? super Boolean> listenerRunningTask() {
return new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if(oldValue && !newValue){
//TODO when finish
}
}
};
}
}
So basically, you can return a variable in the Task, or wait the Task ends and execute something, create your own bindings...
If you want to modify something of the screen from the thread, you need to do it from the FX Thread, the Task function call is outside the FX Thread, for that reason the screen it isn't freeze. But all the bind elements will occur in the FX Thread, so are safe to modify the GUI.
If you want to modify safely the GUI from a not FX Thread, just do:
Platform.runLater(new Runnable() {
#Override
public void run() {
//Safe modification in the FX Thread
}
});
Also take a look on concurrency in JavaFX2. This explain more deeply the concurrency, Service, Task...
Hope it helps!