使用 JavaFX SceneGraph

Working on the JavaFX SceneGraph

我创建了一个要点,GraphGymnastic 中描述了我的问题。使用 Thread.sleep() 对问题进行了高度抽象,但它达到了目的。

问题描述

我想在过滤事件(使用 EventFilters)时遍历场景图。当一个事件到来时,我想计算图中所有节点上的一些东西。对于 2 个节点,这在 Platform.runLater() 下工作得很好,但稍后会有 n 个节点,计算可能需要一些时间。我不希望 FX 线程在此计算期间被阻塞。

所以我们考虑将计算委托给第二个线程。说了,做了。现在我们面临的问题是,我们在第二个线程上进行计算,但是这个线程持有对图形的引用,它可以同时更改(我们同意,这在大多数情况下不会发生,但需要考虑)。所以说,我们在'dynamic view'上计算。怎么办?复制图表?然后我们在一开始,阻塞UI线程复制图。

这是一个纯粹的设计决定,如果有人已经做过这样的事情,我将非常感激,如果有其他想法或方法,如果是的话,如何解决这个优雅 不是一些建筑工地。

感谢任何能提供帮助的人。

PS:在要点中,您必须取消注释并用我的注释注释这两行才能看到问题(按下按钮后,尝试移动window)

编辑:代替要点,这是代码。

public class GraphGymnastic extends Application {
  final ExecutorService serv = Executors.newFixedThreadPool(2);
  public static void main(String argv[]) {
    launch(argv);
  }

  @Override public void start(Stage primaryStage) throws Exception {
    //Setup UI
    primaryStage.setTitle("Demo");
    final List<Node> nodesInGraph = new ArrayList<>();
    final FlowPane p = new FlowPane() {{
      setId("flowpane");
      getChildren().addAll(new Label("Label") {{
        setId("label");
      }}, new Button("Button") {{
        setId("button");
        // setOnMouseClicked(event -> handle(event, nodesInGraph)); //Uncomment and comment below to see effects!
        setOnMouseClicked(event -> handleAsync(event, nodesInGraph));
      }});
      setHgap(5);
    }};

    //Assume that this goes recursive and deep into a scene graph but still
    // returns a list
    //Here it takes the two childs for simplicity
    nodesInGraph.addAll(p.getChildrenUnmodifiable());

   //Show stage
    primaryStage.setScene(new Scene(p));
    primaryStage.show();
  }

  public void handle(MouseEvent ev, List<Node> nodesInGraph) {
    if (null != nodesInGraph)
      Platform.runLater(() -> nodesInGraph.forEach(node -> {
        //This will block the UI thread, so there is the need for a second
        //thread
        System.out.println(
            "Calculating heavy on node " + node.toString() + " with event from "
                + ev.getSource().toString());
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }));
  }

  public void handleAsync(MouseEvent ev, List<Node> nodesInGraph) {
    if (null != nodesInGraph)
      serv.submit(() -> nodesInGraph.forEach(node -> {
        //Now there is a second thread but it works on a LIVE view object
        // list, which is ugly
        //Option 1: Keep it like this, drawbacks? :S
        //Option 2: Copy the graph, costs performance... How deep should it
        // copy? :S
        System.out.println(
            "Calculating heavy on node " + node.toString() + " with event from "
                + ev.getSource().toString());
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }));
  }
}

不是并发专家,所以这只是为了说明我的评论:在 fx 线程上提取一些状态,然后将该状态传递给另一个线程进行处理

成分:

  • 实时节点列表
  • 一个状态对象,它可以携带节点属性的快照(在询问时),然后对其进行一些冗长的处理
  • a Task (as of fx concurrency): 在后台线程中,它创建一个状态对象,切换到fx线程,复制相关状态,切换回后台,开始处理并在就绪时设置它的值
  • 侦听任务值的处理程序

代码:

// the worker
public static class SceneGraphWorker extends Task<NodeState> {

    private List<Node> nodes;
    private int current;

    public SceneGraphWorker(List<Node> nodes) {
        this.nodes = nodes;
    }

    @Override
    protected NodeState call() throws Exception {
        while (current >= 0) {
            NodeState state = new NodeState(current);
            CountDownLatch latch = new CountDownLatch(1);
            Platform.runLater(() -> {
                Node node = nodes.get(current);
                Bounds bounds = node.localToScene(node.getBoundsInLocal());
                state.setState(bounds.getMinX(), bounds.getMinY());
                state.setID(node.getId());
                current = current == nodes.size() - 1 ? -1 : current + 1;
                latch.countDown(); 
            });
            latch.await();
            state.process();
            updateValue(state);
        }
        return null;
    }

}

// the handler: listens to value
public void handleWithWorker(MouseEvent ev, List<Node> nodesInGraph) {
    Task worker = new SceneGraphWorker(nodesInGraph);
    worker.valueProperty().addListener((src, ov, nv) -> {
        progress.setText(nv != null ? nv.toString() : "empty");
    });
    new Thread(worker).start();
}

// the state object
public static class NodeState {
    double x, y;
    int index;
    private String name;
    public NodeState(int index) {
        this.index = index;
    }
    public void setState(double x, double y) {
        this.x = x;
        this.y = y;
    }
    public void setID(String name) {
        this.name = name;
    }
    public void process() throws InterruptedException {
        Thread.sleep(2000);
    }
    @Override
    public String toString() {
        return "[" + name + " x: " + x + " y: " + y + "]";
    }
}