2015-06-06 37 views
24

类似于Graphviz,但更具体地说,是yFiles。JavaFX中的图形可视化(如yFiles)

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

我在考虑让节点为Circle,边缘为Line。问题是在节点/边缘出现的区域使用什么。我应该使用ScrollPane,定期Pane,一个Canvas,等...

我会添加滚动功能,缩放,选择节点&拖动节点。

感谢您的帮助。

+2

你有你想要的东西,但你有什么问题吗?你应该使用一个ScrollPane,但这对于一个非常复杂的任务来说只是一个非常小的决定,所以这不是对一般问题标题的回答,这对于StackOverflow问题来说太广泛了。顺便提一句,yWorks提供了[yFiles for JavaFX](https://www.yworks.com/en/products_yfilesjavafx_about.html),不过一般来说,库建议不在StackOverflow的范围之内,它是这样一个限制性的规则StackOverflow有;-) – jewelsea

+0

@jewelsea问题是我将如何去选择项目的组件。 – 3legit4quit

+1

那么你可以将圆和线添加到ScrollPane中的一个组中,并创建一个基本的图形查看器。它不会是任何想象中的yFiles,它可以让你查看节点图。但是您可能需要在节点中添加一些信息(如文本),因此而不是Circle使用标签。缩放是棘手的,所以你可能想要做的:http://stackoverflow.com/questions/16680295/javafx-correct-scaling。为布局获得正确的几何图形也很棘手,你可能想用一些库来协助。不确定这是对您的问题的评论或实际答案。 – jewelsea

回答

70

我有2个小时杀人,所以我想我会给它一个镜头。原来,很容易想出一个原型。

这里有您需要什么:

  • 主类中使用您创建
  • 一个图表,数据模型的图形库
  • 轻松添加和节点和边缘的移除(事实证明,它是更好地命名节点单元以避免在编程期间与JavaFX节点混淆)
  • a zoomable scrollpane
  • 该图的布局算法

这真的太过分了,所以我只是在代码中添加一些注释。

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

应用/ 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(); 
      } 
     } 
    } 
} 

每一个细胞都被表示为窗格,可以在其中放作为视图的任何节点(矩形,标签,图像视图等)

小区。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 
    ; 

} 

实例化他们是很容易的:

RectangleCell.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),它不会被添加到图本身,而是所有节点都开始没有父节点。

Model.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); 

     } 

    } 

} 

的例子是这样的:

​​

您可以拖动细胞与鼠标按钮,用鼠标滚轮放大和缩小。


添加新的细胞类型与创建细胞的亚类一样简单:

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); 
    } 
} 
... 

你会这等

enter image description here

+8

呃..你真了不起。非常感谢! – 3legit4quit

+0

为什么要添加一个图层选定的单元格,将它们显示在所有其他的顶部? – Josephus87

+0

用于选择矩形和其他不属于图形本身的东西。您不想在图形本身中添加选择JavaFX节点。分层解决了这个问题。有了这个,你也可以添加e。 G。在矩形的角落直接调整指标。 – Roland

-3

可以使用jfreechart API生成图形可视化

提供,线图,饼图,酒吧。并且使用起来非常重要。

+6

我会...但我不想使用JavaFX的Swing库。 – 3legit4quit