JavaFX 中的图形可视化(如 yFiles)

Posted

技术标签:

【中文标题】JavaFX 中的图形可视化(如 yFiles)【英文标题】:Graph Visualisation (like yFiles) in JavaFX 【发布时间】:2015-08-21 03:23:55 【问题描述】:

类似于 Graphviz,但更具体地说,是 yFiles。

我想要一个节点/边类型的图形可视化。

我正在考虑将节点设为Circle,将边设为Line。问题是在节点/边缘出现的区域使用什么。我应该使用ScrollPane、常规PaneCanvas 等...

我将添加滚动功能、缩放、选择节点和拖动节点。

感谢您的帮助。

【问题讨论】:

你有你想要的东西,但你的问题是什么?您应该使用 ScrollPane,但这只是一项非常复杂的任务中的一个非常小的决定,因此这不是一般问题标题的答案,这对于 *** 问题来说过于宽泛。顺便说一句,yFiles for JavaFX 可从 yWorks 获得,但总的来说,库推荐超出了 *** 的范围,这是 *** 的一组限制性规则 ;-) @jewelsea 问题是我将如何为项目选择组件。 您可以在 ScrollPane 中将圆和线添加到组中并创建一个基本的图形查看器。无论如何,它都不是 yFiles,它可以让您查看节点图。但是您可能需要节点中的一些信息(如文本),因此不要使用 Circle,而是使用 Label。缩放很棘手,所以您可能想要这样做:***.com/questions/16680295/javafx-correct-scaling。获得适合布局的几何图形也很棘手,您可能需要使用一些库来帮助解决这个问题。不确定这是评论还是对您问题的实际回答。 @jewelsea 正是我想要的。谢谢!不过我不得不问 - 为什么要把它们放在Group 中? 阅读ScrollPane documentation:“ScrollPane 布局计算是基于 layoutBounds 而不是滚动节点的 boundsInParent(视觉边界)。如果应用程序希望滚动基于视觉边界节点(对于缩放的内容等),他们需要将滚动节点包装在一个组中。”您想根据视觉范围滚动缩放的内容,因此您需要一个 Group... 【参考方案1】:

我有 2 个小时的时间要杀,所以我想我会试一试。事实证明,想出一个原型很容易。

这是你需要的:

使用您创建的图形库的主类 带有数据模型的图表 轻松添加和删除节点和边(事实证明,最好命名节点单元格,以避免在编程过程中与 JavaFX 节点混淆) zoomable scrollpane 图形的布局算法

在SO上问的实在是太多了,所以我就加几个cmets的代码吧。

应用程序实例化图表,添加单元格并通过边连接它们。

应用程序/Main.java

package application;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

import com.fxgraph.graph.CellType;
import com.fxgraph.graph.Graph;
import com.fxgraph.graph.Model;
import com.fxgraph.layout.base.Layout;
import com.fxgraph.layout.random.RandomLayout;

public class Main extends Application 

    Graph graph = new Graph();

    @Override
    public void start(Stage primaryStage) 
        BorderPane root = new BorderPane();

        graph = new Graph();

        root.setCenter(graph.getScrollPane());

        Scene scene = new Scene(root, 1024, 768);
        scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());

        primaryStage.setScene(scene);
        primaryStage.show();

        addGraphComponents();

        Layout layout = new RandomLayout(graph);
        layout.execute();

    

    private void addGraphComponents() 

        Model model = graph.getModel();

        graph.beginUpdate();

        model.addCell("Cell A", CellType.RECTANGLE);
        model.addCell("Cell B", CellType.RECTANGLE);
        model.addCell("Cell C", CellType.RECTANGLE);
        model.addCell("Cell D", CellType.TRIANGLE);
        model.addCell("Cell E", CellType.TRIANGLE);
        model.addCell("Cell F", CellType.RECTANGLE);
        model.addCell("Cell G", CellType.RECTANGLE);

        model.addEdge("Cell A", "Cell B");
        model.addEdge("Cell A", "Cell C");
        model.addEdge("Cell B", "Cell C");
        model.addEdge("Cell C", "Cell D");
        model.addEdge("Cell B", "Cell E");
        model.addEdge("Cell D", "Cell F");
        model.addEdge("Cell D", "Cell G");

        graph.endUpdate();

    

    public static void main(String[] args) 
        launch(args);
    

滚动窗格应具有白色背景。

应用程序/application.css

.scroll-pane > .viewport 
   -fx-background-color: white;

可缩放的滚动窗格,我得到了code base from pixel duke:

ZoomableScrollPane.java

package com.fxgraph.graph;

import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.ScrollEvent;
import javafx.scene.transform.Scale;

public class ZoomableScrollPane extends ScrollPane 
    Group zoomGroup;
    Scale scaleTransform;
    Node content;
    double scaleValue = 1.0;
    double delta = 0.1;

    public ZoomableScrollPane(Node content) 
        this.content = content;
        Group contentGroup = new Group();
        zoomGroup = new Group();
        contentGroup.getChildren().add(zoomGroup);
        zoomGroup.getChildren().add(content);
        setContent(contentGroup);
        scaleTransform = new Scale(scaleValue, scaleValue, 0, 0);
        zoomGroup.getTransforms().add(scaleTransform);

        zoomGroup.setOnScroll(new ZoomHandler());
    

    public double getScaleValue() 
        return scaleValue;
    

    public void zoomToActual() 
        zoomTo(1.0);
    

    public void zoomTo(double scaleValue) 

        this.scaleValue = scaleValue;

        scaleTransform.setX(scaleValue);
        scaleTransform.setY(scaleValue);

    

    public void zoomActual() 

        scaleValue = 1;
        zoomTo(scaleValue);

    

    public void zoomOut() 
        scaleValue -= delta;

        if (Double.compare(scaleValue, 0.1) < 0) 
            scaleValue = 0.1;
        

        zoomTo(scaleValue);
    

    public void zoomIn() 

        scaleValue += delta;

        if (Double.compare(scaleValue, 10) > 0) 
            scaleValue = 10;
        

        zoomTo(scaleValue);

    

    /**
     * 
     * @param minimizeOnly
     *            If the content fits already into the viewport, then we don't
     *            zoom if this parameter is true.
     */
    public void zoomToFit(boolean minimizeOnly) 

        double scaleX = getViewportBounds().getWidth() / getContent().getBoundsInLocal().getWidth();
        double scaleY = getViewportBounds().getHeight() / getContent().getBoundsInLocal().getHeight();

        // consider current scale (in content calculation)
        scaleX *= scaleValue;
        scaleY *= scaleValue;

        // distorted zoom: we don't want it => we search the minimum scale
        // factor and apply it
        double scale = Math.min(scaleX, scaleY);

        // check precondition
        if (minimizeOnly) 

            // check if zoom factor would be an enlargement and if so, just set
            // it to 1
            if (Double.compare(scale, 1) > 0) 
                scale = 1;
            
        

        // apply zoom
        zoomTo(scale);

    

    private class ZoomHandler implements EventHandler<ScrollEvent> 

        @Override
        public void handle(ScrollEvent scrollEvent) 
            // if (scrollEvent.isControlDown())
            

                if (scrollEvent.getDeltaY() < 0) 
                    scaleValue -= delta;
                 else 
                    scaleValue += delta;
                

                zoomTo(scaleValue);

                scrollEvent.consume();
            
        
    

每个单元格都表示为窗格,您可以将任何节点作为视图(矩形、标签、图像视图等)放入其中

Cell.java

package com.fxgraph.graph;

import java.util.ArrayList;
import java.util.List;

import javafx.scene.Node;
import javafx.scene.layout.Pane;

public class Cell extends Pane 

    String cellId;

    List<Cell> children = new ArrayList<>();
    List<Cell> parents = new ArrayList<>();

    Node view;

    public Cell(String cellId) 
        this.cellId = cellId;
    

    public void addCellChild(Cell cell) 
        children.add(cell);
    

    public List<Cell> getCellChildren() 
        return children;
    

    public void addCellParent(Cell cell) 
        parents.add(cell);
    

    public List<Cell> getCellParents() 
        return parents;
    

    public void removeCellChild(Cell cell) 
        children.remove(cell);
    

    public void setView(Node view) 

        this.view = view;
        getChildren().add(view);

    

    public Node getView() 
        return this.view;
    

    public String getCellId() 
        return cellId;
    

细胞应该是通过某种工厂创建的,所以它们是按类型分类的:

CellType.java

package com.fxgraph.graph;

public enum CellType 

    RECTANGLE,
    TRIANGLE
    ;


实例化它们非常容易:

矩形单元格.java

package com.fxgraph.cells;

import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;

import com.fxgraph.graph.Cell;

public class RectangleCell extends Cell 

    public RectangleCell( String id) 
        super( id);

        Rectangle view = new Rectangle( 50,50);

        view.setStroke(Color.DODGERBLUE);
        view.setFill(Color.DODGERBLUE);

        setView( view);

    


TriangleCell.java

package com.fxgraph.cells;

import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;

import com.fxgraph.graph.Cell;

public class TriangleCell extends Cell 

    public TriangleCell( String id) 
        super( id);

        double width = 50;
        double height = 50;

        Polygon view = new Polygon( width / 2, 0, width, height, 0, height);

        view.setStroke(Color.RED);
        view.setFill(Color.RED);

        setView( view);

    


那么你当然需要边缘。你可以使用任何你喜欢的连接,甚至三次曲线。为简单起见,我使用一行:

Edge.java

package com.fxgraph.graph;

import javafx.scene.Group;
import javafx.scene.shape.Line;

public class Edge extends Group 

    protected Cell source;
    protected Cell target;

    Line line;

    public Edge(Cell source, Cell target) 

        this.source = source;
        this.target = target;

        source.addCellChild(target);
        target.addCellParent(source);

        line = new Line();

        line.startXProperty().bind( source.layoutXProperty().add(source.getBoundsInParent().getWidth() / 2.0));
        line.startYProperty().bind( source.layoutYProperty().add(source.getBoundsInParent().getHeight() / 2.0));

        line.endXProperty().bind( target.layoutXProperty().add( target.getBoundsInParent().getWidth() / 2.0));
        line.endYProperty().bind( target.layoutYProperty().add( target.getBoundsInParent().getHeight() / 2.0));

        getChildren().add( line);

    

    public Cell getSource() 
        return source;
    

    public Cell getTarget() 
        return target;
    


对此的扩展是将边缘绑定到单元的端口(北/南/东/西)。

然后你想拖动节点,所以你必须添加一些鼠标手势。重要的部分是考虑缩放系数,以防图形画布被缩放

MouseGestures.java

package com.fxgraph.graph;

import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.input.MouseEvent;

public class MouseGestures 

    final DragContext dragContext = new DragContext();

    Graph graph;

    public MouseGestures( Graph graph) 
        this.graph = graph;
    

    public void makeDraggable( final Node node) 


        node.setOnMousePressed(onMousePressedEventHandler);
        node.setOnMouseDragged(onMouseDraggedEventHandler);
        node.setOnMouseReleased(onMouseReleasedEventHandler);

    

    EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() 

        @Override
        public void handle(MouseEvent event) 

            Node node = (Node) event.getSource();

            double scale = graph.getScale();

            dragContext.x = node.getBoundsInParent().getMinX() * scale - event.getScreenX();
            dragContext.y = node.getBoundsInParent().getMinY()  * scale - event.getScreenY();

        
    ;

    EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() 

        @Override
        public void handle(MouseEvent event) 

            Node node = (Node) event.getSource();

            double offsetX = event.getScreenX() + dragContext.x;
            double offsetY = event.getScreenY() + dragContext.y;

            // adjust the offset in case we are zoomed
            double scale = graph.getScale();

            offsetX /= scale;
            offsetY /= scale;

            node.relocate(offsetX, offsetY);

        
    ;

    EventHandler<MouseEvent> onMouseReleasedEventHandler = new EventHandler<MouseEvent>() 

        @Override
        public void handle(MouseEvent event) 

        
    ;

    class DragContext 

        double x;
        double y;

    

然后您需要一个模型来存储单元格和边缘。任何时候都可以添加新单元格并删除现有单元格。您需要处理它们与现有的区别(例如,添加鼠标手势,添加它们时为它们设置动画等)。当您实现布局算法时,您将面临根节点的确定。所以你应该创建一个不可见的根节点(graphparent),它不会被添加到图本身,但所有没有父节点的节点都从该节点开始。

模型.java

package com.fxgraph.graph;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.fxgraph.cells.TriangleCell;
import com.fxgraph.cells.RectangleCell;

public class Model 

    Cell graphParent;

    List<Cell> allCells;
    List<Cell> addedCells;
    List<Cell> removedCells;

    List<Edge> allEdges;
    List<Edge> addedEdges;
    List<Edge> removedEdges;

    Map<String,Cell> cellMap; // <id,cell>

    public Model() 

         graphParent = new Cell( "_ROOT_");

         // clear model, create lists
         clear();
    

    public void clear() 

        allCells = new ArrayList<>();
        addedCells = new ArrayList<>();
        removedCells = new ArrayList<>();

        allEdges = new ArrayList<>();
        addedEdges = new ArrayList<>();
        removedEdges = new ArrayList<>();

        cellMap = new HashMap<>(); // <id,cell>

    

    public void clearAddedLists() 
        addedCells.clear();
        addedEdges.clear();
    

    public List<Cell> getAddedCells() 
        return addedCells;
    

    public List<Cell> getRemovedCells() 
        return removedCells;
    

    public List<Cell> getAllCells() 
        return allCells;
    

    public List<Edge> getAddedEdges() 
        return addedEdges;
    

    public List<Edge> getRemovedEdges() 
        return removedEdges;
    

    public List<Edge> getAllEdges() 
        return allEdges;
    

    public void addCell(String id, CellType type) 

        switch (type) 

        case RECTANGLE:
            RectangleCell rectangleCell = new RectangleCell(id);
            addCell(rectangleCell);
            break;

        case TRIANGLE:
            TriangleCell circleCell = new TriangleCell(id);
            addCell(circleCell);
            break;

        default:
            throw new UnsupportedOperationException("Unsupported type: " + type);
        
    

    private void addCell( Cell cell) 

        addedCells.add(cell);

        cellMap.put( cell.getCellId(), cell);

    

    public void addEdge( String sourceId, String targetId) 

        Cell sourceCell = cellMap.get( sourceId);
        Cell targetCell = cellMap.get( targetId);

        Edge edge = new Edge( sourceCell, targetCell);

        addedEdges.add( edge);

    

    /**
     * Attach all cells which don't have a parent to graphParent 
     * @param cellList
     */
    public void attachOrphansToGraphParent( List<Cell> cellList) 

        for( Cell cell: cellList) 
            if( cell.getCellParents().size() == 0) 
                graphParent.addCellChild( cell);
            
        

    

    /**
     * Remove the graphParent reference if it is set
     * @param cellList
     */
    public void disconnectFromGraphParent( List<Cell> cellList) 

        for( Cell cell: cellList) 
            graphParent.removeCellChild( cell);
        
    

    public void merge() 

        // cells
        allCells.addAll( addedCells);
        allCells.removeAll( removedCells);

        addedCells.clear();
        removedCells.clear();

        // edges
        allEdges.addAll( addedEdges);
        allEdges.removeAll( removedEdges);

        addedEdges.clear();
        removedEdges.clear();

    

然后是包含可缩放滚动窗格、模型等的图表本身。在图表中处理添加和删除的节点(鼠标手势、添加到滚动窗格的单元格和边缘等)。

Graph.java

package com.fxgraph.graph;

import javafx.scene.Group;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Pane;

public class Graph 

    private Model model;

    private Group canvas;

    private ZoomableScrollPane scrollPane;

    MouseGestures mouseGestures;

    /**
     * the pane wrapper is necessary or else the scrollpane would always align
     * the top-most and left-most child to the top and left eg when you drag the
     * top child down, the entire scrollpane would move down
     */
    CellLayer cellLayer;

    public Graph() 

        this.model = new Model();

        canvas = new Group();
        cellLayer = new CellLayer();

        canvas.getChildren().add(cellLayer);

        mouseGestures = new MouseGestures(this);

        scrollPane = new ZoomableScrollPane(canvas);

        scrollPane.setFitToWidth(true);
        scrollPane.setFitToHeight(true);

    

    public ScrollPane getScrollPane() 
        return this.scrollPane;
    

    public Pane getCellLayer() 
        return this.cellLayer;
    

    public Model getModel() 
        return model;
    

    public void beginUpdate() 
    

    public void endUpdate() 

        // add components to graph pane
        getCellLayer().getChildren().addAll(model.getAddedEdges());
        getCellLayer().getChildren().addAll(model.getAddedCells());

        // remove components from graph pane
        getCellLayer().getChildren().removeAll(model.getRemovedCells());
        getCellLayer().getChildren().removeAll(model.getRemovedEdges());

        // enable dragging of cells
        for (Cell cell : model.getAddedCells()) 
            mouseGestures.makeDraggable(cell);
        

        // every cell must have a parent, if it doesn't, then the graphParent is
        // the parent
        getModel().attachOrphansToGraphParent(model.getAddedCells());

        // remove reference to graphParent
        getModel().disconnectFromGraphParent(model.getRemovedCells());

        // merge added & removed cells with all cells
        getModel().merge();

    

    public double getScale() 
        return this.scrollPane.getScaleValue();
    

单元层的包装器。您可能需要添加多个图层(例如,突出显示选定单元格的选择图层)

CellLayer.java

package com.fxgraph.graph;

import javafx.scene.layout.Pane;

public class CellLayer extends Pane 


现在您需要一个单元格布局。我建议创建一个简单的抽象类,它会随着您开发图形而得到扩展。

package com.fxgraph.layout.base;

public abstract class Layout 

    public abstract void execute();


为简单起见,这里有一个使用随机坐标的简单布局算法。当然,您必须做更复杂的事情,例如树形布局等。

RandomLayout.java

package com.fxgraph.layout.random;

import java.util.List;
import java.util.Random;

import com.fxgraph.graph.Cell;
import com.fxgraph.graph.Graph;
import com.fxgraph.layout.base.Layout;

public class RandomLayout extends Layout 

    Graph graph;

    Random rnd = new Random();

    public RandomLayout(Graph graph) 

        this.graph = graph;

    

    public void execute() 

        List<Cell> cells = graph.getModel().getAllCells();

        for (Cell cell : cells) 

            double x = rnd.nextDouble() * 500;
            double y = rnd.nextDouble() * 500;

            cell.relocate(x, y);

        

    


示例如下所示:

您可以使用鼠标按钮拖动单元格并使用鼠标滚轮放大和缩小。


添加新的单元格类型就像创建 Cell 的子类一样简单:

package com.fxgraph.cells;

import javafx.scene.control.Button;

import com.fxgraph.graph.Cell;

public class ButtonCell extends Cell 

    public ButtonCell(String id) 
        super(id);

        Button view = new Button(id);

        setView(view);

    



package com.fxgraph.cells;

import javafx.scene.image.ImageView;

import com.fxgraph.graph.Cell;

public class ImageCell extends Cell 

    public ImageCell(String id) 
        super(id);

        ImageView view = new ImageView("http://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/800px-Siberischer_tiger_de_edit02.jpg");
        view.setFitWidth(100);
        view.setFitHeight(80);

        setView(view);

    




package com.fxgraph.cells;

import javafx.scene.control.Label;

import com.fxgraph.graph.Cell;

public class LabelCell extends Cell 

    public LabelCell(String id) 
        super(id);

        Label view = new Label(id);

        setView(view);

    



package com.fxgraph.cells;

import javafx.scene.control.TitledPane;

import com.fxgraph.graph.Cell;

public class TitledPaneCell extends Cell 

    public TitledPaneCell(String id) 
        super(id);

        TitledPane view = new TitledPane();
        view.setPrefSize(100, 80);

        setView(view);

    


并创建类型

package com.fxgraph.graph;

public enum CellType 

    RECTANGLE,
    TRIANGLE,
    LABEL,
    IMAGE,
    BUTTON,
    TITLEDPANE
    ;


并根据类型创建实例:

...
public void addCell(String id, CellType type) 

    switch (type) 

    case RECTANGLE:
        RectangleCell rectangleCell = new RectangleCell(id);
        addCell(rectangleCell);
        break;

    case TRIANGLE:
        TriangleCell circleCell = new TriangleCell(id);
        addCell(circleCell);
        break;

    case LABEL:
        LabelCell labelCell = new LabelCell(id);
        addCell(labelCell);
        break;

    case IMAGE:
        ImageCell imageCell = new ImageCell(id);
        addCell(imageCell);
        break;

    case BUTTON:
        ButtonCell buttonCell = new ButtonCell(id);
        addCell(buttonCell);
        break;

    case TITLEDPANE:
        TitledPaneCell titledPaneCell = new TitledPaneCell(id);
        addCell(titledPaneCell);
        break;

    default:
        throw new UnsupportedOperationException("Unsupported type: " + type);
    

...

你会得到这个

【讨论】:

嗯..你太棒了。非常感谢! 为什么要添加一个图层选定的单元格,以将它们显示在其他所有内容之上? 用于选择矩形和其他不属于图形本身的内容。您不想在图形本身中添加选择 JavaFX Nodes 时乱七八糟。分层解决了这个问题。有了它,您还可以添加 e。 G。直接在矩形的角上调整指示器的大小。 非常好,感谢您的努力。我让它可以很好地处理来自数据库的数据 我为此创建了一个 github 存储库并将其推送到 maven Central。你可以在这里找到它:github.com/sirolf2009/fxgraph【参考方案2】:

我遇到了同样的问题,我设法将 javascript vis.js 库与 JavaFX WebView 一起使用。

如果对某人有用,您可以在 github 上查看:https://github.com/arocketman/VisFX

【讨论】:

【参考方案3】:

我会试试Prefux。它是Prefuse 项目的一个分支。

从 JavaFX 移植开始的原始存储库是 https://github.com/effrafax/Prefux,但维护最多的 fork 似乎是上面的那个 (https://github.com/jchildress/Prefux)。

另一个移植到 JavaFX 的尝试是在 https://github.com/gedeffe/Prefuse 开始的,但它不再处于活动状态。

【讨论】:

【参考方案4】:

您可以使用jfreechart api 来生成图形可视化

它提供线、饼图、条形图。而且非常好用。

【讨论】:

我会...但我不想为 JavaFX 使用 Swing 库。

以上是关于JavaFX 中的图形可视化(如 yFiles)的主要内容,如果未能解决你的问题,请参考以下文章

JavaFX入门及相关问题

在JavaFX中的openstreetmap上显示分层集群

基于javafx如何用线程完成移动一个圆?

JavaFX上手--第1天

eclipse javafx运行界面为空

禁用JavaFX中的所有父节点,同时保持其所有子节点可单击