Problems reseting scene graph in JavaFX - javafx

I'm having a problem with a large scene graph rapidly changing.
While responding to an event, when I clear the scene graph (getChildren().clear), I sometimes get this exception:
Exception in thread "JavaFX Application Thread" java.lang.ClassCastException: class javafx.scene.Scene cannot be cast to class javafx.scene.Node (javafx.scene.Scene and javafx.scene.Node are in module javafx.graphics of loader 'app')
at javafx.graphics/javafx.scene.Scene$MouseHandler.handleNodeRemoval(Scene.java:3709)
at javafx.graphics/javafx.scene.Scene.generateMouseExited(Scene.java:3581)
at javafx.graphics/javafx.scene.Parent$3.onProposedChange(Parent.java:593)
at javafx.base/com.sun.javafx.collections.VetoableListDecorator.clear(VetoableListDecorator.java:294)
at bit.fxzoomer/bit.fxzoomer.SectorPane12.populateMap(SectorPane12.java:72)
at bit.fxzoomer/bit.fxzoomer.SectorPane12.lambda$addListeners$1(SectorPane12.java:247)
at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)
at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
at javafx.base/javafx.beans.property.ObjectPropertyBase.fireValueChangedEvent(ObjectPropertyBase.java:106)
at javafx.base/javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:113)
at javafx.base/javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:147)
at javafx.base/javafx.beans.property.ObjectProperty.setValue(ObjectProperty.java:72)
at bit.fxzoomer/bit.fxzoomer.SectorPane12.setViewport(SectorPane12.java:62)
at bit.fxzoomer/bit.fxzoomer.App.lambda$start$0(App.java:31)
at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)
at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
at javafx.base/javafx.beans.property.ObjectPropertyBase.fireValueChangedEvent(ObjectPropertyBase.java:106)
at javafx.base/javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:113)
at javafx.base/javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:147)
at javafx.base/javafx.beans.property.ObjectProperty.setValue(ObjectProperty.java:72)
at bit.fxzoomer/bit.fxzoomer.PanZoomPane.setViewport(PanZoomPane.java:99)
at bit.fxzoomer/bit.fxzoomer.PanZoomPane.lambda$new$3(PanZoomPane.java:82)
at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)
at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
at javafx.base/javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:170)
at javafx.base/com.sun.javafx.binding.BindingHelperObserver.invalidated(BindingHelperObserver.java:52)
at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)
at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
at javafx.graphics/javafx.scene.Node$LazyBoundsProperty.invalidate(Node.java:9785)
at javafx.graphics/javafx.scene.Node$MiscProperties.invalidateBoundsInLocal(Node.java:6876)
at javafx.graphics/javafx.scene.Node.invalidateBoundsInLocal(Node.java:3469)
at javafx.graphics/javafx.scene.Node.localBoundsChanged(Node.java:4041)
at javafx.graphics/javafx.scene.Node.doGeomChanged(Node.java:4028)
at javafx.graphics/javafx.scene.Node$1.doGeomChanged(Node.java:461)
at javafx.graphics/com.sun.javafx.scene.NodeHelper.geomChangedImpl(NodeHelper.java:184)
at javafx.graphics/com.sun.javafx.scene.NodeHelper.geomChanged(NodeHelper.java:137)
at javafx.graphics/javafx.scene.Parent.childBoundsChanged(Parent.java:1872)
at javafx.graphics/javafx.scene.Node.notifyParentOfBoundsChange(Node.java:4099)
at javafx.graphics/javafx.scene.Node.transformedBoundsChanged(Node.java:4060)
at javafx.graphics/javafx.scene.Node.doTransformsChanged(Node.java:5003)
at javafx.graphics/javafx.scene.Node$1.doTransformsChanged(Node.java:444)
at javafx.graphics/com.sun.javafx.scene.NodeHelper.transformsChangedImpl(NodeHelper.java:170)
at javafx.graphics/com.sun.javafx.scene.NodeHelper.transformsChanged(NodeHelper.java:119)
at javafx.graphics/javafx.scene.transform.Transform.transformChanged(Transform.java:2109)
at javafx.graphics/javafx.scene.transform.Affine$AffineAtomicChange.end(Affine.java:5778)
at javafx.graphics/javafx.scene.transform.Affine.appendTranslation(Affine.java:2038)
at javafx.graphics/javafx.scene.transform.Translate.appendTo(Translate.java:539)
at javafx.graphics/javafx.scene.transform.Affine.append(Affine.java:1502)
at bit.fxzoomer/bit.fxzoomer.PanZoomPane.translate(PanZoomPane.java:123)
at bit.fxzoomer/bit.fxzoomer.PanZoomPane.lambda$new$1(PanZoomPane.java:58)
at javafx.base/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)
at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
at javafx.base/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at javafx.base/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
at javafx.base/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
at javafx.base/javafx.event.Event.fireEvent(Event.java:198)
at javafx.graphics/javafx.scene.Scene$MouseHandler.process(Scene.java:3862)
at javafx.graphics/javafx.scene.Scene.processMouseEvent(Scene.java:1849)
at javafx.graphics/javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2590)
at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:409)
at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:299)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$2(GlassViewEventHandler.java:447)
at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:412)
at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:446)
at javafx.graphics/com.sun.glass.ui.View.handleMouseEvent(View.java:556)
at javafx.graphics/com.sun.glass.ui.View.notifyMouse(View.java:942)
at javafx.graphics/com.sun.glass.ui.mac.MacView.notifyMouse(MacView.java:127)
It starts with that exception, then I get a cascade of these:
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException: Cannot invoke "javafx.scene.Scene.isDepthBuffer()" because the return value of "javafx.scene.Node.getScene()" is null
at javafx.graphics/com.sun.javafx.scene.input.PickResultChooser.processOffer(PickResultChooser.java:185)
at javafx.graphics/com.sun.javafx.scene.input.PickResultChooser.offer(PickResultChooser.java:143)
at javafx.graphics/javafx.scene.Node.doComputeIntersects(Node.java:5263)
at javafx.graphics/javafx.scene.Node$1.doComputeIntersects(Node.java:456)
at javafx.graphics/com.sun.javafx.scene.NodeHelper.computeIntersectsImpl(NodeHelper.java:180)
at javafx.graphics/com.sun.javafx.scene.NodeHelper.computeIntersects(NodeHelper.java:133)
at javafx.graphics/javafx.scene.Node.intersects(Node.java:5234)
at javafx.graphics/javafx.scene.Node.doPickNodeLocal(Node.java:5171)
at javafx.graphics/javafx.scene.Node$1.doPickNodeLocal(Node.java:450)
at javafx.graphics/com.sun.javafx.scene.NodeHelper.pickNodeLocalImpl(NodeHelper.java:175)
at javafx.graphics/com.sun.javafx.scene.NodeHelper.pickNodeLocal(NodeHelper.java:128)
at javafx.graphics/javafx.scene.Node.pickNode(Node.java:5203)
at javafx.graphics/javafx.scene.Parent.pickChildrenNode(Parent.java:805)
at javafx.graphics/javafx.scene.Parent$1.pickChildrenNode(Parent.java:136)
at javafx.graphics/com.sun.javafx.scene.ParentHelper.pickChildrenNode(ParentHelper.java:113)
at javafx.graphics/javafx.scene.layout.Region.doPickNodeLocal(Region.java:3160)
at javafx.graphics/javafx.scene.layout.Region$1.doPickNodeLocal(Region.java:184)
at javafx.graphics/com.sun.javafx.scene.layout.RegionHelper.pickNodeLocalImpl(RegionHelper.java:104)
at javafx.graphics/com.sun.javafx.scene.NodeHelper.pickNodeLocal(NodeHelper.java:128)
at javafx.graphics/javafx.scene.Node.pickNode(Node.java:5203)
at javafx.graphics/javafx.scene.Parent.pickChildrenNode(Parent.java:805)
at javafx.graphics/javafx.scene.Parent$1.pickChildrenNode(Parent.java:136)
at javafx.graphics/com.sun.javafx.scene.ParentHelper.pickChildrenNode(ParentHelper.java:113)
at javafx.graphics/javafx.scene.layout.Region.doPickNodeLocal(Region.java:3160)
at javafx.graphics/javafx.scene.layout.Region$1.doPickNodeLocal(Region.java:184)
at javafx.graphics/com.sun.javafx.scene.layout.RegionHelper.pickNodeLocalImpl(RegionHelper.java:104)
at javafx.graphics/com.sun.javafx.scene.NodeHelper.pickNodeLocal(NodeHelper.java:128)
at javafx.graphics/javafx.scene.Node.pickNode(Node.java:5203)
at javafx.graphics/javafx.scene.Scene$MouseHandler.pickNode(Scene.java:4005)
at javafx.graphics/javafx.scene.Scene.pick(Scene.java:2029)
at javafx.graphics/javafx.scene.Scene$MouseHandler.process(Scene.java:3815)
at javafx.graphics/javafx.scene.Scene.processMouseEvent(Scene.java:1849)
at javafx.graphics/javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2590)
at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:409)
at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:299)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$2(GlassViewEventHandler.java:447)
at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:412)
at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:446)
at javafx.graphics/com.sun.glass.ui.View.handleMouseEvent(View.java:556)
at javafx.graphics/com.sun.glass.ui.View.notifyMouse(View.java:942)
at javafx.graphics/com.sun.glass.ui.mac.MacView.notifyMouse(MacView.java:127)
Specifically, I'm panning the scene graph (see the answer here Get Viewport of translated and scaled node ). Each time the viewport changes, I redo the scene graph.
The scene graph is quite large, 20-30,000 nodes.
When things are busy, when I'm dragging the scene around rapidly, clicking a lot, infrequently, I will get those exceptions.
This is single threaded, so I don't think I'm seeing a synchronization problem, but perhaps I'm fighting the redisplay thread. Clearly something is happening at the wrong time.
Sometimes it will recover from these errors (it makes a lot of noise of no consequence), other times it seems to wreck the scene and nothing works anymore.
I have tried delaying the update via a timer that gets continually refreshed while dragging, firing only after the dragging has stopped (via Platform.runLater) but even that can fire this exception. I put it all back on the main thread to rule out a synchronization issue.
It's very sporadic. I don't know what's fighting what.
What's causing these, and how can I prevent it?
EDIT: This is a self contained example of it failing. It doesn't even have a large scene graph. Simply run the program, grab the lower left corner and resize it back and forth, and it fails.
Tested with JDK 11 and JFX 17.0.2.
I tried a large scene graph (25,000 elements), tied simply to a resize event, and could not get it to fail. This fails readily.
package bit.fxtest2;
import javafx.application.Application;
import javafx.beans.binding.Binding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Transform;
import javafx.stage.Stage;
public class TransformTest9 extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
BigGridPane gridPane = new BigGridPane();
PanZoomPane pzPane = new PanZoomPane(gridPane);
pzPane.getViewportProperty().addListener((ov, t, t1) -> {
gridPane.setViewPort(t1);
});
Scene scene = new Scene(pzPane, 250, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
class BigGridPane extends Region {
ObjectProperty<Bounds> viewPortProperty = new SimpleObjectProperty<>();
public BigGridPane() {
viewPortProperty.addListener((ov, t, t1) -> {
populate();
});
}
public void setViewPort(Bounds viewPort) {
viewPortProperty.setValue(viewPort);
}
public void populate() {
ObservableList<Node> children = getChildren();
children.clear();
for (int i = 0; i < 20; i++) {
for (int j = 0; j < 20; j++) {
Rectangle r = new Rectangle(i * 20, j * 20, 20, 20);
r.setFill(Color.WHITE);
r.setStroke(Color.BLACK);
children.add(r);
}
}
System.out.println(children.size());
}
}
class PanZoomPane extends Region {
private final Node content;
private final Rectangle clip;
private Affine transform;
private Point2D mouseDown;
private static final double SCALE = 1.01; // zoom factor per pixel scrolled
Binding<Bounds> viewportBinding;
ObjectProperty<Bounds> viewportProperty = new SimpleObjectProperty<>();
public PanZoomPane(Node content) {
Background background = new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY));
setBackground(background);
this.content = content;
getChildren().add(content);
clip = new Rectangle();
setClip(clip);
transform = Affine.affine(1, 0, 0, 1, 0, 0);
content.getTransforms().setAll(transform);
content.setOnMousePressed(event -> mouseDown = new Point2D(event.getX(), event.getY()));
content.setOnMouseDragged(event -> {
double deltaX = event.getX() - mouseDown.getX();
double deltaY = event.getY() - mouseDown.getY();
translate(deltaX, deltaY);
});
content.setOnScroll(event -> {
double pivotX = event.getX();
double pivotY = event.getY();
double scale = Math.pow(SCALE, event.getDeltaY());
scale(pivotX, pivotY, scale);
});
viewportBinding = new ObjectBinding<>() {
{
bind(
localToSceneTransformProperty(),
boundsInLocalProperty(),
content.localToSceneTransformProperty()
);
}
#Override
protected Bounds computeValue() {
return content.sceneToLocal(localToScene(getBoundsInLocal()));
}
};
viewportBinding.addListener((obs, oldViewport, newViewport) -> setViewport(newViewport));
}
public ObjectProperty<Bounds> getViewportProperty() {
return viewportProperty;
}
public void setViewportProperty(ObjectProperty<Bounds> viewportProperty) {
this.viewportProperty = viewportProperty;
}
public Bounds getViewport() {
return viewportProperty.getValue();
}
public void setViewport(Bounds bounds) {
viewportProperty.setValue(bounds);
}
public Node getContent() {
return content;
}
#Override
protected void layoutChildren() {
clip.setWidth(getWidth());
clip.setHeight(getHeight());
}
public void scale(double pivotX, double pivotY, double scale) {
Affine t = transform.clone();
t.append(Transform.scale(scale, scale, pivotX, pivotY));
}
public void translate(double x, double y) {
transform.append(Transform.translate(x, y));
}
}
}
Edit:
I think I have a solution. I've posted it below.

I think this is the answer.
EDIT: No, it's not. Still having issues. Maybe I'll just destroy and recreate the pane every time -- seems excessive.
#kleopatra mentioned a call to super.layoutChildren in the `PanZoomPane. And while that worked for the test, it did not work for my code.
#jewelsea pointed me to this post and it has this important assumption:
On the third hand, we expect people to set up scene graphs at
initialization time and modify nodes in-place, instead of doing scene
graph surgery.
This is clearly not what I'm doing. I'm not only doing scene graph surgery, arguably its outright butchering.
I've seen mentions before from #James_D about how some work is better done in layoutChildren, rather than event handlers. So maybe layoutChildren is where we should be looking.
When the PanZoomPane.layoutChildren is called, it will, inevitably, call my custom pane's layoutChildren.
When the PanZoomPane is resized, layoutChildren is called. But when it is panned or zoomed, it will not. To force the issue, in the scale and translate method, I tickle height of the pane:
double height = getHeight();
setHeight(height + 1);
setHeight(height);
This is enough to trigger the layoutChildren call. Apparently layoutChildren is where scene graph surgery is acceptable. So, I moved all of my scene graph code to layoutChildren.
Arguably I should move the tickle in to my content pane, rather than force it from the PanZoomPane.
But the heart of it is that I guess scene graph surgery is frowned upon in event handlers. We should just be playing with properties of the existing scene graph.
I don't know if there is a better solution to this, but for the moment, it seems to be working and seems to make a bit of sense.

Related

Background thread directly accessing UI anyway

Here is my code, can someone explain why it works every time?
package dingding;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class Dingding extends Application {
TextField tfAuto = new TextField("0");
AutoRunThread runner = new AutoRunThread();
boolean shouldStop = false;
private class AutoRunThread extends Thread {
#Override
public void run() {
while (true) {
int i = Integer.parseInt(tfAuto.getText());
++i;
tfAuto.setText(String.valueOf(i));
try {
Thread.sleep(1000);
} catch (Throwable t) {
}
if (shouldStop) {
runner = null;
shouldStop = false;
return;
}
}
}
}
#Override
public void start(Stage primaryStage) {
Button btnStart = new Button("Increment Automatically");
Button btnStop = new Button("Stop Autotask");
btnStart.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
if (runner == null) {
runner = new AutoRunThread();
runner.setDaemon(true);
}
if (runner != null && !(runner.isAlive())) {
runner.start();
}
}
});
btnStop.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
shouldStop = true;
}
});
VBox rootBox = new VBox();
HBox autoBox = new HBox();
autoBox.getChildren().addAll(tfAuto, btnStart, btnStop);
rootBox.getChildren().addAll(autoBox);
Scene scene = new Scene(rootBox, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
As I said in my comment, improperly synchronized code doesn't guarantee errors per se. However, that doesn't mean said code, when used in a multi-threaded context, is actually working—you're merely getting lucky. Eventually you'll run into undefined behavior such as corrupted state, stale values, and unexpected exceptions. This is because, without synchronization, actions performed by one thread are not guaranteed to be visible to any other thread. You need a happens-before relationship, better described in the package documentation of java.util.concurrent and this SO question.
JavaFX, like most UI frameworks/toolkits, is single threaded. This means there's a special thread—in this case, the JavaFX Application Thread— that is responsible for all UI related actions1. It is this thread, and this thread only, that must be used to access and/or modify state related to a "live" scene graph (i.e. nodes that are in a scene that's in a window that's showing2). Using any other thread can lead to the undefined behavior described above.
Some UI related functions actually ensure they're being called on the JavaFX Application Thread, usually throwing an IllegalStateException if not. However, the remaining functions will silently let you call them from any thread—but that doesn't mean it's safe to do so. This is done this way, I believe, because checking the thread in every UI related function is a maintenance nightmare and would incur a not-insignificant performance cost.
1. It's slightly more complicated that this; JavaFX also has a "prism render thread" and a "media thread". See Understanding JavaFX Architecture for more information. But note that, from an application developer's point of view, the only thread that matters is the JavaFX Application Thread.
2. This is documented by Node. Note that some nodes, such as WebView, are more restrictive when it comes to threading; this will be documented in the appropriate places.

lanching same JavaFx window after closing it

I am open A JavaFX application using a Jframe.And after using JavaFx application window i close the window .And want to again open this same Javafx window but an error occur-
java.lang.IllegalStateException: Application launch must not be called more than once
As stated in the documentation, calling Application.launch(...) more than once will result in an exception:
public static void launch(String... args)
Launch a standalone application. This method is typically called from
the main method(). It must not be called more than once or an
exception will be thrown.
If you need to mix Swing and JavaFX, you should embed the JavaFX pieces in a JFXPanel and place it in a JFrame. You can then show and hide the JFrame as often as you need, using setVisible(...). An application working this way will not have an Application subclass at all.
Mixing Swing and JavaFX is tricky, and not recommended for beginners. The problem is that each toolkit has its own UI thread, and all access of the UI must be executed on the correct UI thread (i.e. the AWT event dispatch thread for Swing/AWT components, and the JavaFX Application Thread for JavaFX components). Data that is shared between both must provide proper synchronization to ensure that it is safely accessible from multiple threads.
Here is a very simple example. Clicking the button will show the window with FX content. If you close that window, and then click the button again, it will be shown again.
import java.awt.BorderLayout;
import java.util.Random;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class SwingFXExample {
private JFrame mainFrame ;
private JFrame fxFrame ;
private JFXPanel fxPanel ;
public SwingFXExample() {
// must be on Swing thread...
if (! SwingUtilities.isEventDispatchThread()) {
throw new IllegalStateException("Not on Event Dispatch Thread");
}
mainFrame = new JFrame();
JButton showFX = new JButton("Show FX Window");
JPanel content = new JPanel();
content.add(showFX);
mainFrame.add(content, BorderLayout.CENTER);
mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
fxFrame = new JFrame();
fxPanel = new JFXPanel();
fxFrame.add(fxPanel);
fxFrame.setSize(640, 640);
fxFrame.setLocationRelativeTo(null);
Platform.runLater(() -> initFX());
showFX.addActionListener(event -> fxFrame.setVisible(true));
}
private void initFX() {
// must be on FX Application Thread...
if (! Platform.isFxApplicationThread()) {
throw new IllegalStateException("Not on FX Application Thread");
}
LineChart<Number, Number> chart = new LineChart<>(new NumberAxis(), new NumberAxis());
Series<Number, Number> series = new Series<>();
series.setName("Random data");
Random rng = new Random();
for (int i = 0; i <= 10; i++) {
series.getData().add( new Data<>(i, 100*rng.nextDouble()) );
}
chart.getData().add(series);
chart.setOnMouseClicked(evt -> {
if (evt.getClickCount() == 2) {
System.out.println("Double click!");
}
});
fxPanel.setScene(new Scene(chart, 400, 400));
}
public void showMainWindow() {
mainFrame.setSize(350, 120);
mainFrame.setLocationRelativeTo(null);
mainFrame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
SwingFXExample app = new SwingFXExample();
app.showMainWindow();
});
}
}

Javafx RotateTransition rendering smooth

I’m making some trials with JavaFX RotateTransition, applying a simple model found on a jfx itself documentation file:
rotateTransition = RotateTransitionBuilder.create()
.node(elements)
.duration(Duration.seconds(4))
.fromAngle(0)
.toAngle(720)
.cycleCount(3)
.autoReverse(true)
.build();
Above, elements is a Group of bare Arc primitives.
When this group has a limited number of nodes, say 20, the animation goes smooth but when I increase the number of nodes to 500 (nested actually, Group of Group) the animation still works but does not result any more fluid.
The question is: does this nodes limit can be considered too much for this task? How to speed up the rendering?
I have found the thread below that in a similar context asserts that could be a matter of using the right Animation class, but I’m not sure that the proposed AnimationTimer does apply well to a rotation.
http://mail.openjdk.java.net/pipermail/openjfx-dev/2013-June/008104.html
I have also tried to use setCache(true) to every node with no visible improvements.
Thank you!
Edit: Arc generation. No strange things but a binder and a EventHandler.
Arc arc = new Arc();
arc.centerXProperty().bind(plotRadiusBinding);
arc.centerYProperty().bind(plotRadiusBinding);
arc.radiusXProperty().bind(plotRadiusBinding);
arc.radiusYProperty().bind(plotRadiusBinding);
arc.setStartAngle(startAngle * 180 / PI);
arc.setLength(radiansLength * 180 / PI);
arc.setType(ArcType.ROUND);
arc.setStroke(defaultArcColor);
arc.setStrokeType(StrokeType.INSIDE);
arc.setFill(null);
arc.setOnMouseClicked(arcEventHandler);
I had similar issues when moving images around when I used KeyFrames (KeyFrames)
I could improve it by using
I had success with implementing the stuff by myself:
Animation animation = new Transition() {
{
setCycleDuration(Duration.millis(1000));
}
#Override
protected void interpolate(double frac) {
// your rotation code here
}
}
If you could provide your arc generation source code it might be helpful.
for me this works flawlessly for 500 arcs:
package application;
import java.util.Random;
import javafx.animation.RotateTransition;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Arc;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
Pane root = new Pane();
Group group = new Group();
Random rand = new Random();
for(int i=0; i<1000; i++){
Arc c = new Arc(200 + rand.nextInt(400), 200 + rand.nextInt(400), 10, 10, 0, 360);
group.getChildren().add(c);
}
root.getChildren().add(group);
RotateTransition rotateTransition = new RotateTransition(Duration.millis(5000), group);
rotateTransition.setFromAngle(0);
rotateTransition.setToAngle(720);
rotateTransition.setCycleCount(3);
rotateTransition.setAutoReverse(true);
rotateTransition.play();
Scene scene = new Scene(root,800,800);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
if this is still to laggy and you only want to draw arcs you could directly invoke the graphics-context of a canvas.

Make JavaFX wait and continue with code

Basically I am trying to make a short effect using JavaFX. I have the shape of a heart (added together from two circles and a polygon) that I can vary in size using the double value p. "Standart Size" would be p = 1.0;.
I am trying to add a pumping effect to the heart. I have the method pumpOnce():
public void pumpOnce(){
p = p + 1;
initHeart();
//Here goes what ever it takes to make stuff working!!
p = p - 1;
initHeart();
}
initHeart() draws the heart based on p.
I have found out that Thread.sleep(); or similar methods will not work due to the thread philosophy in JavaFX.
But what can I use instead?
The JavaFX animations are probably the way to go, but the "thread philosophy" in JavaFX isn't hard to work with if you want to roll your own, or do other, more complicated things in background threads.
The following code will pause and change the value in a label (full disclosure, I'm reusing code I wrote for another question):
import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javax.xml.datatype.Duration;
public class DelayWithTask extends Application {
private static Label label;
public static void main(String[] args) { launch(args); }
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World!");
label = new Label();
label.setText("Waiting...");
StackPane root = new StackPane();
root.getChildren().add(label);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
delay(5000, () -> label.setText("Hello World"));
}
public static void delay(long millis, Runnable continuation) {
Task<Void> sleeper = new Task<Void>() {
#Override
protected Void call() throws Exception {
try { Thread.sleep(millis); }
catch (InterruptedException e) { }
return null;
}
};
sleeper.setOnSucceeded(event -> continuation.run());
new Thread(sleeper).start();
}
}
The basic JavaFX background tool is the Task, any JavaFX application that actually does anything will probably be littered with these all over. Learn how to use them.
Dave's solution is great for general purpose off thread based work in JavaFX.
If you wish to use the animation facilities of JavaFX, the solutions below demonstrate this using a Timeline or a ScaleTransition. The timeline implements a discrete scale of the UI element, so every quarter of a second the UI element is scaled larger or back to it's original size. The scale transition implements a smooth scale of the UI element, so the UI element gradually gets larger then smaller using an interpolated scale factor with the default easing interpolator.
import javafx.animation.*;
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;
public class BeatingHeart extends Application {
public static void main(String[] args) {
launch(args);
}
public void start(Stage stage) {
ImageView heart = new ImageView(HEART_IMAGE_LOC);
animateUsingTimeline(heart);
// animateUsingScaleTransition(heart);
StackPane layout = new StackPane(heart);
layout.setPrefWidth(heart.getImage().getWidth() * 2);
layout.setPrefHeight(heart.getImage().getHeight() * 2);
Scene scene = new Scene(layout);
stage.setScene(scene);
stage.show();
}
private void animateUsingTimeline(ImageView heart) {
DoubleProperty scale = new SimpleDoubleProperty(1);
heart.scaleXProperty().bind(scale);
heart.scaleYProperty().bind(scale);
Timeline beat = new Timeline(
new KeyFrame(Duration.ZERO, event -> scale.setValue(1)),
new KeyFrame(Duration.seconds(0.5), event -> scale.setValue(1.1))
);
beat.setAutoReverse(true);
beat.setCycleCount(Timeline.INDEFINITE);
beat.play();
}
private void animateUsingScaleTransition(ImageView heart) {
ScaleTransition scaleTransition = new ScaleTransition(
Duration.seconds(1), heart
);
scaleTransition.setFromX(1);
scaleTransition.setFromY(1);
scaleTransition.setFromZ(1);
scaleTransition.setToX(1.1);
scaleTransition.setToY(1.1);
scaleTransition.setToZ(1.1);
scaleTransition.setAutoReverse(true);
scaleTransition.setCycleCount(Animation.INDEFINITE);
scaleTransition.play();
}
private static final String HEART_IMAGE_LOC =
"http://icons.iconarchive.com/icons/mirella-gabriele/valentine/128/Heart-red-icon.png";
// icon obtained from: http://www.iconarchive.com/show/valentine-icons-by-mirella-gabriele/Heart-red-icon.html
// icon license: Free for non-commercial use, commercial use not allowed.
}

JavaFX + AWT Canvas

I read that running AWT With JavaFX is a bad idea.
But we have an old application that runs on Swing and uses the AWT canvas(Cannot change due to an external library that uses the canvas)
Is it really such a horrible idea?
Is there a workaround for this?
Update
Although the code in this answer used to work on Windows with an earlier version of JavaFX, I retested the same same code on OS X 10.9.5 + JavaFX 8u72 and the code no longer works.
The line swingNode.setContent(awtInitializerTask.get()); which instructs the JavaFX thread to wait on the awt thread to initialize the awt canvas never returns, blocking execution and startup of the app.
Just put your AWT canvas in a SwingNode and watch your thread management and you'll be fine.
import javafx.application.Application;
import javafx.embed.swing.SwingNode;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javax.swing.*;
import java.awt.*;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class AwtCanvasWrapper extends Application {
private static final int W = 200;
private static final int H = 100;
#Override public void start(final Stage stage) throws Exception {
final AwtInitializerTask awtInitializerTask = new AwtInitializerTask(() -> {
JPanel jPanel = new JPanel();
jPanel.add(new CustomAwtCanvas(W, H));
return jPanel;
});
SwingUtilities.invokeLater(awtInitializerTask);
SwingNode swingNode = new SwingNode();
swingNode.setContent(awtInitializerTask.get());
stage.setScene(new Scene(new Group(swingNode), W, H));
stage.setResizable(false);
stage.show();
}
private class AwtInitializerTask extends FutureTask<JPanel> {
public AwtInitializerTask(Callable<JPanel> callable) {
super(callable);
}
}
private class CustomAwtCanvas extends Canvas {
public CustomAwtCanvas(int width, int height) {
setSize(width, height);
}
public void paint(Graphics g) {
Graphics2D g2;
g2 = (Graphics2D) g;
g2.setColor(Color.GRAY);
g2.fillRect(
0, 0,
(int) getSize().getWidth(), (int) getSize().getHeight()
);
g2.setColor(Color.BLACK);
g2.drawString("It is a custom canvas area", 25, 50);
}
}
public static void main(String[] args) {
Application.launch(args);
}
}
Here is the output:
Related Question
Interoperability between Graphics2D and GraphicsContext
Answering some additional questions
But that one is for swing components.
Yes, but awt components can be wrapped in Swing components.
furthermore It says in the docs that it should not be used of heavyweight components
Regardless, it seems to work for me, your mileage may vary.
performance is crucial for my app
Then try the approach with your app and check:
The painting is reliable.
The performance is acceptable.
If either of the above checks fail then you may need to use a different approach (though I do not know what that approach would be, maybe just spawn Frame as a new window in which to include the the AWT canvas content rather than embedding the canvas inside the JavaFX scene).

Resources