如何在JavaFX 2中通过拖动正确移动节点?

28
我正在将一个有很多自定义绘画的Swing/Graphics2D应用程序转换为JavaFX2应用程序。虽然我非常喜欢新的API,但当我想要在鼠标光标下方绘制椭圆形时,似乎出现了性能问题。当我以平稳的方式移动鼠标,不是特别快时,我注意到椭圆总是在鼠标轨迹后面绘制几厘米,并且只在我停止移动光标时才追上来。这个场景图中只有几个节点。在我的Swing应用程序中,我没有遇到这个问题。
我想知道这是否是在绘制鼠标光标处形状的正确方法?
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();
}
}

小更新:我升级到JavaFX 2.2和Java7u6(在Windows 7 64位上),但似乎并没有什么区别。


你的解决方案帮了我很多,谢谢! - Lampione
我很高兴它能帮到你 :) - Nicolas Mommaerts
9个回答

28
这是我用来允许在Pane中拖动Label的一些代码。 我没有注意到它会有任何明显的鼠标延迟。
// 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; }

这里有一个使用上述代码的小型完整示例应用程序

更新 以上示例仍会在拖动的对象较小时导致拖动对象滞后于光标。

另一种方法是使用ImageCursor,其中包含一个鼠标指针覆盖在被拖动节点的图像表示上,然后在拖动开始和完成时隐藏和显示实际节点。这意味着节点拖动渲染不会滞后于光标(因为节点的图像表示现在是光标)。但是,这种方法也有缺点 => ImageCursors 的大小和格式受到限制,而且您需要将节点转换为图像才能将其放置在 ImageCursor 中,对此您可能需要高级 Node => Image 转换操作,这些操作仅在 JavaFX 2.2+ 中可用。


谢谢您详细的回答,但是使用您的方法我也会出现延迟。很奇怪! - Nicolas Mommaerts
我已经添加了一个完整的简单示例来说明我的意思。即使是这个小程序在稳定移动鼠标时也会出现延迟。请注意,我不想拖动椭圆形,而是想在仅移动鼠标而不点击时,在鼠标光标下显示它。 - Nicolas Mommaerts
好的,当我将物体拖动变得足够小,我可以注意到与您的示例代码中看到的相同延迟的问题(当被拖动的对象较大时,它只是被掩盖了)。 - jewelsea
我添加了一个使用ImageCursor的备选解决方案的概述。 - jewelsea
谢谢,我也考虑过这个方法,但它看起来太繁琐了。我一直在想,一定有另一种更简单的方法来实现正常的性能。我会稍后再回来处理这个问题。 - Nicolas Mommaerts

10
您描述的拖动形状时出现的延迟是已知的JavaFX错误: https://bugs.openjdk.java.net/browse/JDK-8087922 您可以通过使用一个未记录的JVM标志来解决此问题(至少在Windows上)。
-Djavafx.animation.fullspeed=true

这个标记通常用于内部性能测试,这就是为什么它没有文档记录的原因,但我们已经使用了数月,并且迄今为止没有遇到任何问题。

编辑:

还有另一种类似的方法可以解决这个 bug,可能对 CPU 使用更加轻松。只需关闭 Prism 的垂直同步即可:

-Dprism.vsync=false
在我们的应用程序中,这些解决方法中的任何一个都可以解决延迟问题;不需要两个都做。

我有点新手。我应该在哪里插入那段代码?自从这个问题被回答以后,这个 bug 修复了吗? - qwerty

4
对我来说,这不是绘画性能的问题,而是鼠标事件序列是如何生成的问题。当鼠标移动速度很快时,事件并非实时生成,有些事件会被跳过。对于大多数应用程序来说,这将是足够的方式。鼠标指针实时移动,没有任何时间延迟。
如果您不想出现这种效果,您将需要直接监听鼠标指针或找到一种获取更高密度事件的方式。我自己也不知道如何做到这一点。

1
嗯,你有任何文件可以支持这些说法吗?我觉得很奇怪,因为你可以在SceneBuilder中选择一个元素,用鼠标快速拖动,它会一直跟随鼠标指针。 - Nicolas Mommaerts
1
抱歉,找不到任何东西。我写了一个测试,在我的机器上两个事件之间大约有10毫秒的时间。在SceneBuilder中,我也注意到了类似的延迟。 - Dieter Tremel
我认为你是正确的,这个问题与MouseEvents有关。JavaFX JIRA存储库中有一个与此相关的错误: https://javafx-jira.kenai.com/browse/RT-34608 - Xanatos

2

在尝试使图表上的节点可拖动时,我遇到了同样的问题。我通过调用chart.setAnimated(false);来解决它。在我的情况下,滞后是由JavaFX对我的代码所做的更改应用了一个漂亮平滑的动画引起的。


我认为这仅适用于图表 API。 - Nicolas Mommaerts

2

所有节点都有一个名为“cacheHint”的属性,这可能会有所帮助。

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

在某些情况下,例如对于非常昂贵的渲染节点进行动画处理时,希望能够对节点执行变换而无需重新生成缓存位图。在这种情况下的一种选择是直接在缓存位图上执行变换。

这种技术可以大大提高动画性能,但也可能导致视觉质量降低。cacheHint变量向系统提供关于何时以及何种方式实现(视觉质量与动画性能之间)权衡的提示。

如果您的椭圆形状始终保持不变,但每次将其移动一个像素就重新绘制它,那么这似乎会导致巨大的减速。


1
您可以使用:Node.setCache(true);(我在具有许多子项的Pane中,如TextField一样使用它)

0

这是基于 @jewelsea 回答修改后的 Kotlin 代码

    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
    }

0

这里是使用鼠标在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);
    }
}

-1

拖动球体

@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;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接