28

I'm converting a Swing/Graphics2D app with a lot of custom painting to a JavaFX2 app. Although I absolutely love the new API, I seem to have a performance problem when painting an ellipse that I want to paint below the mouse cursor wherever the mouse is moved. When I move my mouse in a steady way, not ridicously fast, I notice the ellipse is always drawn a few centimeters behind on the mouse trail, and only catches up when I stop moving the cursor. This in a scenegraph with only a handful nodes. In my Swing app I didn't have that problem.

I'm wondering if this is the correct approach for drawing a shape where the mousecursor is?

import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.SceneBuilder;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Ellipse;
import javafx.scene.shape.EllipseBuilder;
import javafx.stage.Stage;

public class TestApp extends Application {
public static void main(String[] args) {
    launch(args);
}

@Override
public void start(Stage primaryStage) throws Exception {
    Pane p = new Pane();

    final Ellipse ellipse = EllipseBuilder.create().radiusX(10).radiusY(10).fill(Color.RED).build();
    p.getChildren().add(ellipse);

    p.setOnMouseMoved(new EventHandler<MouseEvent>() {
        public void handle(MouseEvent event) {
            ellipse.setCenterX(event.getX());
            ellipse.setCenterY(event.getY());
        }
    });

    Scene scene = SceneBuilder.create().root(p).width(1024d).height(768d).build();
    primaryStage.setScene(scene);

    primaryStage.show();
}
}

Small update: I upgraded to JavaFX 2.2 and Java7u6 (on Windows 7 64bit), doesn't seem to make a difference though.

Nicolas Mommaerts
  • 3,207
  • 4
  • 35
  • 55

9 Answers9

28

Here is some code I use to allow a Label to be dragged around in a Pane. I don't notice any significant lag behind the mouse trail with it.

// allow the label to be dragged around.
final Delta dragDelta = new Delta();
label.setOnMousePressed(new EventHandler<MouseEvent>() {
  @Override public void handle(MouseEvent mouseEvent) {
    // record a delta distance for the drag and drop operation.
    dragDelta.x = label.getLayoutX() - mouseEvent.getSceneX();
    dragDelta.y = label.getLayoutY() - mouseEvent.getSceneY();
    label.setCursor(Cursor.MOVE);
  }
});
label.setOnMouseReleased(new EventHandler<MouseEvent>() {
  @Override public void handle(MouseEvent mouseEvent) {
    label.setCursor(Cursor.HAND);
  }
});
label.setOnMouseDragged(new EventHandler<MouseEvent>() {
  @Override public void handle(MouseEvent mouseEvent) {
    label.setLayoutX(mouseEvent.getSceneX() + dragDelta.x);
    label.setLayoutY(mouseEvent.getSceneY() + dragDelta.y);
  }
});
label.setOnMouseEntered(new EventHandler<MouseEvent>() {
  @Override public void handle(MouseEvent mouseEvent) {
    label.setCursor(Cursor.HAND);
  }
});

. . .

// records relative x and y co-ordinates.
class Delta { double x, y; }

Here is a small complete example app using the above code.

Update The above example, will still lag the object being dragged behind the cursor when the objects being dragged are small.

An alternate approach is to use an ImageCursor comprising of a MousePointer superimposed over the an image representation of the node being dragged, then hide and show the actual node at the start and completion of the drag. This means that the node drag rendering will not lag the cursor (as the image representation of the node is now the cursor). However this approach does have drawbacks => there are restrictions on the size and format of ImageCursors, plus you need to convert your Node to an Image to place it in an ImageCursor, for which you may need advanced Node => Image conversion operations only to available in JavaFX 2.2+.

jewelsea
  • 150,031
  • 14
  • 366
  • 406
  • Thanks for the elaborate answer, but using your method I get the lag too. Weird! – Nicolas Mommaerts May 21 '12 at 18:17
  • I've added a complete simple example of what I mean. Even this small program displays lag when moving the mouse in a steady way. Note that I don't want to drag the ellipse, but I want to show it beneath the mouse cursor even when just moving the mouse and not clicking. – Nicolas Mommaerts May 21 '12 at 18:27
  • OK, when I made the objects dragging around small enough I could notice the same lag with my approach which I see in your sample code (it was just getting masked when the object being dragged was larger). – jewelsea May 21 '12 at 18:58
  • I added an outline of an alternate solution using an ImageCursor. – jewelsea May 21 '12 at 19:06
  • Thanks, I thought about the same but it just seems so cumbersome. I just can't stop thinking there must be another, more simple way, to achieve normal performance. I'm going to come back on this in a while.. – Nicolas Mommaerts May 22 '12 at 07:24
10

The lag that you're describing (between your mouse and the dragged shape) is a known JavaFX bug:

https://bugs.openjdk.java.net/browse/JDK-8087922

You can work around it (on Windows, at least) by using an undocumented JVM flag:

-Djavafx.animation.fullspeed=true

This flag is normally for internal performance testing, which is why it is undocumented, but we've been using it for months and haven't had any problems with it so far.

EDIT:

There's another, similar way to workaround this bug that might be a little easier on CPU usage. Simply turn off Prism's vertical sync:

-Dprism.vsync=false

In our app, either of these workarounds solves the lag; there's no need to do both.

jewelsea
  • 150,031
  • 14
  • 366
  • 406
Xanatos
  • 1,576
  • 18
  • 28
  • I'm a bit of a beginner. Where would I insert that code? Has the bug been fixed since this question was answered? – qwerty Aug 11 '20 at 15:08
4

To me it doesn't look like a question of painting performance, but how the sequence of mouse events is generated. The events are not generated in real time, some are skipped, when the mouse moves fast. For the most applications this will be the sufficent way. The mouse pointer moves in real time without any time lag.

If you don't want this effect you will have to listen to the mouse pointer directly or find a way to get the events in higher density. I don't know how myself.

Dieter Tremel
  • 85
  • 1
  • 8
  • 1
    Hmm, do you have any documentation to back those claims? I'd find it weird considering you can take an element in the SceneBuilder, drag your mouse around as fast as you can and it will constantly stick to the mouse pointer. – Nicolas Mommaerts Aug 29 '12 at 18:16
  • 1
    Sorry, couldn't find anything. Wrote a test, there were about 10 milliseconds between two events on my machine. In SceneBuilder I regognize a similar lag. – Dieter Tremel Aug 30 '12 at 08:23
  • I think you are right, the problem is related to MouseEvents. There is a bug in the JavaFX JIRA repository related to this: https://javafx-jira.kenai.com/browse/RT-34608 – Xanatos Jan 23 '14 at 22:16
2

There's this "cacheHint" property, available on all Nodes and that may help ?

http://docs.oracle.com/javafx/2/api/javafx/scene/Node.html#cacheHintProperty

Under certain circumstances, such as animating nodes that are very expensive to render, it is desirable to be able to perform transformations on the node without having to regenerate the cached bitmap. An option in such cases is to perform the transforms on the cached bitmap itself.

This technique can provide a dramatic improvement to animation performance, though may also result in a reduction in visual quality. The cacheHint variable provides a hint to the system about how and when that trade-off (visual quality for animation performance) is acceptable.

If your ellipse remains the same the whole time, but is redrawn every time you move it by one pixel, this seems to be a huge slowdown.

Community
  • 1
  • 1
QuidNovi
  • 609
  • 6
  • 20
2

I was having the same problem while trying to make nodes on a chart draggable. I fixed it by calling chart.setAnimated(false); In my case the lag was being caused by JavaFX applying a nice smooth animation to the changes my code was making.

f4lco
  • 3,728
  • 5
  • 28
  • 53
Ambrose
  • 21
  • 1
1

here is the code to drag and drop label using mouse in javafx

@FXML
public void lblDragMousePressed(MouseEvent m)
{
    System.out.println("Mouse is pressed");

    prevLblCordX= (int) lblDragTest.getLayoutX();
    prevLblCordY= (int) lblDragTest.getLayoutY();
    prevMouseCordX= (int) m.getX();
    prevMouseCordY= (int) m.getY();     
}
//set this method on mouse released event for lblDrag
@FXML
public void lblDragMouseReleased(MouseEvent m)
{       
    System.out.println("Label Dragged");    
}
// set this method on Mouse Drag event for lblDrag
@FXML
public void lblDragMouseDragged(MouseEvent m)
{   
    diffX= (int) (m.getX()- prevMouseCordX);
    diffY= (int) (m.getY()-prevMouseCordY );        
    int x = (int) (diffX+lblDragTest.getLayoutX()-rootAnchorPane.getLayoutX());
    int y = (int) (diffY+lblDragTest.getLayoutY()-rootAnchorPane.getLayoutY());     
    if (y > 0 && x > 0 && y < rootAnchorPane.getHeight() && x < rootAnchorPane.getWidth()) 
    { 
     lblDragTest.setLayoutX(x);
     lblDragTest.setLayoutY(y);
    }
}
Kundan
  • 261
  • 4
  • 11
1

you can use : Node.setCache(true); (i use it with a Pane with many childrens like a TextField)

LoloDuSud
  • 11
  • 1
0

Drag Sphere

@Override
public void initialize(URL location, ResourceBundle resources) {
    super.initialize(location, resources);
    labelTableName.setText("Table Categories");

    final PhongMaterial blueMaterial = new PhongMaterial();
    blueMaterial.setDiffuseColor(Color.BLUE);
    blueMaterial.setSpecularColor(Color.LIGHTBLUE);

    final Sphere sphere = new Sphere(50);
    sphere.setMaterial(blueMaterial);

    final Measure dragMeasure = new Measure();
    final Measure position = new Measure();
    sphere.setOnMousePressed(mouseEvent -> {
        dragMeasure.x = mouseEvent.getSceneX() - position.x;
        dragMeasure.y = mouseEvent.getSceneY() - position.y;
        sphere.setCursor(Cursor.MOVE);
    });
    sphere.setOnMouseDragged(mouseEvent -> {
        position.x = mouseEvent.getSceneX() - dragMeasure.x;
        position.y = mouseEvent.getSceneY() - dragMeasure.y;
        sphere.setTranslateX(position.x);
        sphere.setTranslateY(position.y);
    });
    sphere.setOnMouseReleased(mouseEvent -> sphere.setCursor(Cursor.HAND));
    sphere.setOnMouseEntered(mouseEvent -> sphere.setCursor(Cursor.HAND));

    bottomHeader.getChildren().addAll( sphere);

}

class Measure {
    double x, y;

    public Measure() {
        x = 0; y = 0;
    }
}
Vahe Gharibyan
  • 5,277
  • 4
  • 36
  • 47
0

this is modified Kotlin code based on answer from @jewelsea

    var dragDelta = Delta()
    var releasedDelta = Delta()

    scene.setOnMousePressed {
        if (releasedDelta.x > 0 && releasedDelta.y > 0) {
            val offsetX = it.sceneX - releasedDelta.x
            var offsetY = it.sceneY - releasedDelta.y
            dragDelta.x = dragDelta.x + offsetX
            dragDelta.y = dragDelta.y + offsetY
        } else {
            dragDelta.x = it.sceneX
            dragDelta.y = it.sceneY
        }
        scene.cursor = Cursor.MOVE;
        releasedDelta = Delta()
    }

    scene.setOnMouseReleased {
        releasedDelta.x = it.sceneX
        releasedDelta.y = it.sceneY
        scene.cursor = Cursor.HAND;
    }

    scene.setOnMouseDragged {
        scene.translateX = it.sceneX - dragDelta.x;
        scene.translateY = it.sceneY - dragDelta.y;
    }

    scene.setOnMouseEntered {
        scene.cursor = Cursor.HAND
    }
Jesse
  • 517
  • 5
  • 24