Reputation: 571
I want to paint a tree (n-ary) in dialog box starting from a set of TreeItems that i define and structure in a tree.
The full JavaFx code for defining the TreeItems and drawing the tree in a dialog box is the following:
public class treeController{
@fxml
public void openDialogWindowTreePreview() {
AnchorPane treeGraphPane = new AnchorPane();
treeGraphPane.setPrefWidth(600);
treeGraphPane.setPrefHeight(400);
List<TreeItemNode> children = new ArrayList<>();
List<TreeItemNode> children5 = new ArrayList<>();
List<TreeItemNode> children4 = new ArrayList<>();
TreeItemNode treeItemFirstChild = new TreeItemNode("child1", new ArrayList<TreeItemNode>(), true);
TreeItemNode treeItemSecondChild = new TreeItemNode("child2", new ArrayList<TreeItemNode>(), true);
TreeItemNode treeItemThirdChild = new TreeItemNode("child3", new ArrayList<TreeItemNode>(), true);
children5.add(treeItemFirstChild);
children5.add(treeItemSecondChild);
children5.add(treeItemThirdChild);
children4.add(treeItemFirstChild);
children4.add(treeItemSecondChild);
children4.add(treeItemThirdChild);
TreeItemNode treeItemForthChild = new TreeItemNode("child4", children4, true);
TreeItemNode treeItemFifthChild = new TreeItemNode("child5", children5, true);
TreeItemNode treeItemsixthChild = new TreeItemNode("child6", new ArrayList<TreeItemNode>(), true);
TreeItemNode treeItemsevthChild = new TreeItemNode("child7", new ArrayList<TreeItemNode>(), true);
children.add(treeItemFirstChild);
children.add(treeItemSecondChild);
children.add(treeItemThirdChild);
children.add(treeItemForthChild);
children.add(treeItemFifthChild);
children.add(treeItemsixthChild);
children.add(treeItemsevthChild);
TreeItemNode treeItemRoot = new TreeItemNode(300/*the 1/2 of 400 width of dialog*/, 50, "root", children);
treeItemRoot.setChild(false);
Alert dialog = new Alert(Alert.AlertType.INFORMATION);
dialog.setWidth(1050);
dialog.setHeight(600);
drawNodeRecursive(treeGraphPane,treeItemRoot.getX(),/*10*/treeItemRoot.getY(), treeItemRoot);
dialog.getDialogPane().setContent(treeGraphPane);
dialog.setResizable(true);
Optional<ButtonType> result = dialog.showAndWait();
}
public int drawNodeRecursive(AnchorPane mainGraphTreeLayout, int x, int y, TreeItemNode parentNode) {
Circle circle = new Circle(parentNode.getX(),parentNode.getY(), 15, Paint.valueOf("blue"));
mainGraphTreeLayout.getChildren().add(circle);
Text txt = new Text(x-3,y+3, String.valueOf(parentNode.getValue()));
mainGraphTreeLayout.getChildren().add(txt);
List<TreeItemNode> nodeChildrenList = parentNode.getChildren();
int xPrime = parentNode.getX()+25;
for (TreeItemNode child : nodeChildrenList) {
child.setX(xPrime);
child.setY(parentNode.getY()+50);
Line line = new Line(parentNode.getX(), parentNode.getY()+5, child.getX(), child.getY());
mainGraphTreeLayout.getChildren().add(line);
xPrime = drawNodeRecursive(mainGraphTreeLayout,child.getX(), child.getY(), child) - 50;
}
return x;
}
the fxml code in main.fxml consists of an anchorPane with a button as follows:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.Pane?>
<AnchorPane xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="treeController">
<children>
<Pane prefHeight="206.0" prefWidth="281.0">
<children>
<Button fx:id="button1" layoutX="59.0" layoutY="63.0" mnemonicParsing="false" prefHeight="63.0" prefWidth="164.0" text="Button" onAction="#onDialogWindowTreePreview" />
</children>
</Pane>
</children>
</AnchorPane>
But unfortunately the tree drawing contains overlapping nodes as shown in the attached image:
What is wrong with my drawNodeRecursive()
method?
EDIT: as per @kleopatra request this is the remainder of the code:
public class TreeItemNode {
public int x;
public int y;
public String value;
public List<TreeItemNode> children;
public boolean isChild;
public boolean isChild() {
return isChild;
}
public void setChild(boolean child) {
isChild = child;
}
public TreeItemNode( String value, List<TreeItemNode> children,boolean isChild) {
this.value = value;
this.isChild = isChild;
this.children = children;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public String getValue() {
return value;
}
public List<TreeItemNode> getChildren() {
return children;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
public void setValue(String value) {
this.value = value;
}
public void setChildren(List<TreeItemNode> children) {
this.children = children;
}
}
And finally
public class UInterface extends Application {
@Override
public void start(Stage stage) throws IOException {
String fxmlFile = "main.fxml";
FXMLLoader loader = new FXMLLoader();
Parent rootNode = (Parent) loader.load(getClass().getResource(fxmlFile));
Scene scene = new Scene(rootNode, 1024, 843);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Upvotes: 2
Views: 361
Reputation: 159576
I don't advise trying to write your own general-purpose tree layout library. That is a difficult and error-prone thing, which has already been done before. Instead, use an existing library that calculates the correct layout for you. In this answer, this is what I do, I do not provide information on how to correct your current algorithm.
The tree layout library used is the abego library:
Sample code to use this in JavaFX follows.
Much of the code is from the existing abego demo applications (I did not write it). I left out the license comment from some of the copied code for brevity, but the license (BSD) can be found at the abego github link.
I didn't make a lot of effort to make it work nicely with JavaFX. In particular, there are no dynamic properties or listeners to changes of the trees or CSS stylesheet styling of the trees, which is what would be provided in a more feature-rich implementation. It also follows the abego sample, which uses node sizes that are configured in the tree setup rather than dynamically calculating the size of each node (which would be an alternative implementation).
The trees themselves are drawn using the JavaFX scene graph in a Group node, there are other technologies, such as the canvas which could be used to draw the trees, I just chose to use a Group.
The example follows the sample code from abego which is capable of drawing multiple trees. If you just want to generate and draw a single tree, it is simpler.
All of the JavaFX code is in the com.example.treelayout.javafx
package, the rest of the code doesn't have anything to do with JavaFX.
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>treelayout</artifactId>
<version>1.0-SNAPSHOT</version>
<name>treelayout</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.abego.treelayout</groupId>
<artifactId>org.abego.treelayout.core</artifactId>
<version>1.0.3</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>19</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<source>19</source>
<target>19</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
module-info.java
module com.example.treelayout {
requires javafx.controls;
requires org.abego.treelayout.core;
requires java.desktop;
exports com.example.treelayout.javafx;
}
com.example.treelayout.javafx.JavaFXDemo
package com.example.treelayout.javafx;
import com.example.treelayout.SampleTreeFactory;
import com.example.treelayout.TextInBox;
import com.example.treelayout.TextInBoxNodeExtentProvider;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
import org.abego.treelayout.TreeForTreeLayout;
import org.abego.treelayout.TreeLayout;
import org.abego.treelayout.util.DefaultConfiguration;
public class JavaFXDemo extends Application {
private static final String[] TREE_NAMES = { "semtab", "semtab2", "2", "" };
@Override
public void start(Stage stage) {
boolean boxVisible = true;
for (String s: getParameters().getRaw()) {
if (s.equalsIgnoreCase("--nobox")) {
boxVisible = false;
break;
}
}
FlowPane layout = new FlowPane();
layout.setPadding(new Insets(20));
layout.setHgap(20);
layout.setVgap(20);
for (String treeName: TREE_NAMES) {
layout.getChildren().add(
buildTree(treeName, boxVisible)
);
}
stage.setScene(new Scene(layout));
stage.show();
}
/**
* Shows a tree in a layout created by {@link TreeLayout},
* using the JavaFX component {@link TreeLayoutView}.
*/
public TreeLayoutView buildTree(String treeName, boolean boxVisible) {
TreeForTreeLayout<TextInBox> tree = getSampleTree(treeName);
// setup the tree layout configuration
double gapBetweenLevels = treeName.startsWith("semtab") ? 15 : 50;
double gapBetweenNodes = 10;
DefaultConfiguration<TextInBox> configuration = new DefaultConfiguration<>(
gapBetweenLevels,
gapBetweenNodes
);
// create the NodeExtentProvider for TextInBox nodes
TextInBoxNodeExtentProvider nodeExtentProvider = new TextInBoxNodeExtentProvider();
// create the layout
TreeLayout<TextInBox> treeLayout = new TreeLayout<>(
tree,
nodeExtentProvider,
configuration
);
// Create a panel that draws the nodes and edges and show the panel
TreeLayoutView treePane = new TreeLayoutView(treeLayout);
treePane.setBoxVisible(boxVisible);
return treePane;
}
private TreeForTreeLayout<TextInBox> getSampleTree(String treeName) {
return switch (treeName) {
case "semtab" -> SampleTreeFactory.createSemanticTableaux();
case "semtab2" -> SampleTreeFactory.createSemanticTableaux2();
case "2" -> SampleTreeFactory.createSampleTree2();
case "" -> SampleTreeFactory.createSampleTree();
default -> throw new RuntimeException(
String.format(
"Invalid tree name: '%s'", treeName
)
);
};
}
}
com.example.treelayout.javafx.TreeLayoutView
package com.example.treelayout.javafx;
import com.example.treelayout.TextInBox;
import javafx.scene.Group;
import javafx.scene.control.Label;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import org.abego.treelayout.TreeForTreeLayout;
import org.abego.treelayout.TreeLayout;
import java.awt.geom.Rectangle2D;
/**
* A JavaFX group displaying a tree of TextInBoxes, given by a {@link TreeLayout}.
*/
public class TreeLayoutView extends Group {
private final TreeLayout<TextInBox> treeLayout;
private boolean boxVisible = true;
private TreeForTreeLayout<TextInBox> getTree() {
return treeLayout.getTree();
}
private Iterable<TextInBox> getChildren(TextInBox parent) {
return getTree().getChildren(parent);
}
private Rectangle2D.Double getBoundsOfNode(TextInBox node) {
return treeLayout.getNodeBounds().get(node);
}
/**
* Specifies the tree to be displayed by passing in a {@link TreeLayout} for
* that tree.
*
* @param treeLayout the {@link TreeLayout} to be displayed
*/
public TreeLayoutView(TreeLayout<TextInBox> treeLayout) {
this.treeLayout = treeLayout;
addEdges(getTree().getRoot());
// paint the boxes
for (TextInBox textInBox : treeLayout.getNodeBounds().keySet()) {
addBox(textInBox);
}
}
public boolean isBoxVisible() {
return boxVisible;
}
public void setBoxVisible(boolean boxVisible) {
this.boxVisible = boxVisible;
}
private final static int ARC_SIZE = 10;
private final static Color BOX_COLOR = Color.CORNSILK;
private final static Color BORDER_COLOR = Color.DARKGRAY;
private final static Color TEXT_COLOR = Color.BLACK;
private void addEdges(TextInBox parent) {
if (!getTree().isLeaf(parent)) {
Rectangle2D.Double b1 = getBoundsOfNode(parent);
double x1 = b1.getCenterX();
double y1 = b1.getCenterY();
for (TextInBox child : getChildren(parent)) {
Rectangle2D.Double b2 = getBoundsOfNode(child);
getChildren().add(
new Line(
x1, y1,
b2.getCenterX(), b2.getCenterY()
)
);
addEdges(child);
}
}
}
private void addBox(TextInBox textInBox) {
Rectangle2D.Double box = getBoundsOfNode(textInBox);
Rectangle rectangle = new Rectangle(
box.x, box.y, box.width - 1, box.height - 1
);
rectangle.setArcWidth(ARC_SIZE);
rectangle.setArcHeight(ARC_SIZE);
rectangle.setFill(BOX_COLOR);
rectangle.setStroke(BORDER_COLOR);
if (isBoxVisible()) {
getChildren().add(
rectangle
);
}
// draw the text on top of the box (possibly multiple lines)
Label label = new Label(textInBox.text);
label.setStyle("-fx-alignment: center; -fx-text-alignment: center; -fx-text-fill: " + colorStyleString(TEXT_COLOR) + ";");
label.relocate(box.x, box.y);
label.setMinSize(Label.USE_PREF_SIZE, Label.USE_PREF_SIZE);
label.setPrefSize(box.width, box.height);
label.setMaxSize(Label.USE_PREF_SIZE, Label.USE_PREF_SIZE);
label.setWrapText(true);
getChildren().add(label);
}
private String colorStyleString(Color color) {
return "rgba("
+ ((int) (color.getRed() * 255)) + ","
+ ((int) (color.getGreen() * 255)) + ","
+ ((int) (color.getBlue() * 255)) + ","
+ color.getOpacity() +
")";
}
}
com.example.treelayout.TextInBox
package com.example.treelayout;
/**
* Represents a text to be displayed in a box of a given size.
*
* @author Udo Borkowski ([email protected])
*/
public class TextInBox {
public final String text;
public final int height;
public final int width;
public TextInBox(String text, int width, int height) {
this.text = text;
this.width = width;
this.height = height;
}
}
com.example.TextInBoxNodeExtentProvider
package com.example.treelayout;
import org.abego.treelayout.NodeExtentProvider;
/**
* A {@link NodeExtentProvider} for nodes of type {@link TextInBox}.
* <p>
* As one would expect this NodeExtentProvider returns the width and height as
* specified with each TextInBox.
*
* @author Udo Borkowski ([email protected])
*/
public class TextInBoxNodeExtentProvider implements
NodeExtentProvider<TextInBox> {
@Override
public double getWidth(TextInBox treeNode) {
return treeNode.width;
}
@Override
public double getHeight(TextInBox treeNode) {
return treeNode.height;
}
}
com.example.treelayout.SampleTreeFactory
package com.example.treelayout;
import org.abego.treelayout.TreeForTreeLayout;
import org.abego.treelayout.util.DefaultTreeForTreeLayout;
/**
* Creates "Sample" trees, e.g. to be used in demonstrations.
*
* @author Udo Borkowski ([email protected])
*/
public class SampleTreeFactory {
/**
* @return a "Sample" tree with {@link TextInBox} items as nodes.
*/
public static TreeForTreeLayout<TextInBox> createSampleTree() {
TextInBox root = new TextInBox("root", 40, 20);
TextInBox n1 = new TextInBox("n1", 30, 20);
TextInBox n1_1 = new TextInBox("n1.1\n(first node)", 80, 36);
TextInBox n1_2 = new TextInBox("n1.2", 40, 20);
TextInBox n1_3 = new TextInBox("n1.3\n(last node)", 80, 36);
TextInBox n2 = new TextInBox("n2", 30, 20);
TextInBox n2_1 = new TextInBox("n2", 30, 20);
DefaultTreeForTreeLayout<TextInBox> tree = new DefaultTreeForTreeLayout<TextInBox>(
root);
tree.addChild(root, n1);
tree.addChild(n1, n1_1);
tree.addChild(n1, n1_2);
tree.addChild(n1, n1_3);
tree.addChild(root, n2);
tree.addChild(n2, n2_1);
return tree;
}
/**
* @return a "Sample" tree with {@link TextInBox} items as nodes.
*/
public static TreeForTreeLayout<TextInBox> createSampleTree2() {
TextInBox root = new TextInBox("prog", 40, 20);
TextInBox n1 = new TextInBox("classDef", 65, 20);
TextInBox n1_1 = new TextInBox("class", 50, 20);
TextInBox n1_2 = new TextInBox("T", 20, 20);
TextInBox n1_3 = new TextInBox("{", 20, 20);
TextInBox n1_4 = new TextInBox("member", 60, 20);
TextInBox n1_5 = new TextInBox("member", 60, 20);
TextInBox n1_5_1 = new TextInBox("<ERROR:int>", 90, 20);
TextInBox n1_6 = new TextInBox("member", 60, 20);
TextInBox n1_6_1 = new TextInBox("int", 30, 20);
TextInBox n1_6_2 = new TextInBox("i", 20, 20);
TextInBox n1_6_3 = new TextInBox(";", 20, 20);
TextInBox n1_7 = new TextInBox("}", 20, 20);
DefaultTreeForTreeLayout<TextInBox> tree = new DefaultTreeForTreeLayout<TextInBox>(
root);
tree.addChild(root, n1);
tree.addChild(n1, n1_1);
tree.addChild(n1, n1_2);
tree.addChild(n1, n1_3);
tree.addChild(n1, n1_4);
tree.addChild(n1, n1_5);
tree.addChild(n1_5, n1_5_1);
tree.addChild(n1, n1_6);
tree.addChild(n1_6,n1_6_1);
tree.addChild(n1_6,n1_6_2);
tree.addChild(n1_6,n1_6_3);
tree.addChild(n1, n1_7);
return tree;
}
public static TreeForTreeLayout<TextInBox> createSemanticTableaux() {
TextInBox root = new TextInBox(""+
"((A -> B) & (B -> A))\n"+
"(A -> B)\n"+
"(B -> A)",
140, 52);
TextInBox n1_1_1 = new TextInBox("~A", 30, 20);
TextInBox n1_1_2 = new TextInBox("B", 30, 20);
TextInBox n1_1_1_1 = new TextInBox(""+
"~B\n" +
"OPEN", 44, 35);
TextInBox n1_1_1_2 = new TextInBox(""+
"A\n" +
"X", 30, 35);
TextInBox n1_1_2_1 = new TextInBox(""+
"~B\n" +
"X", 30, 35);
TextInBox n1_1_2_2 = new TextInBox(""+
"A\n" +
"OPEN", 44, 35);
DefaultTreeForTreeLayout<TextInBox> tree = new DefaultTreeForTreeLayout<TextInBox>(
root);
tree.addChild(root, n1_1_1);
tree.addChild(root, n1_1_2);
tree.addChild(n1_1_1, n1_1_1_1);
tree.addChild(n1_1_1, n1_1_1_2);
tree.addChild(n1_1_2, n1_1_2_1);
tree.addChild(n1_1_2, n1_1_2_2);
return tree;
}
public static TreeForTreeLayout<TextInBox> createSemanticTableaux2() {
TextInBox root = new TextInBox("((A -> B) & (B -> A))", 140, 20);
TextInBox n1 = new TextInBox("(A -> B)", 65, 20);
TextInBox n1_1 = new TextInBox("(B -> A)", 65, 20);
TextInBox n1_1_1 = new TextInBox("~A", 30, 20);
TextInBox n1_1_2 = new TextInBox("B", 30, 20);
TextInBox n1_1_1_1 = new TextInBox("~B", 30, 20);
TextInBox n1_1_1_2 = new TextInBox("A", 30, 20);
TextInBox n1_1_2_1 = new TextInBox("~B", 30, 20);
TextInBox n1_1_2_2 = new TextInBox("A", 30, 20);
TextInBox n1_1_1_1_1 = new TextInBox("OPEN", 44, 20);
TextInBox n1_1_1_2_1 = new TextInBox("X", 20, 20);
TextInBox n1_1_2_1_1 = new TextInBox("X", 20, 20);
TextInBox n1_1_2_2_1 = new TextInBox("OPEN", 44, 20);
DefaultTreeForTreeLayout<TextInBox> tree = new DefaultTreeForTreeLayout<TextInBox>(
root);
tree.addChild(root, n1);
tree.addChild(n1, n1_1);
tree.addChild(n1_1, n1_1_1);
tree.addChild(n1_1, n1_1_2);
tree.addChild(n1_1_1, n1_1_1_1);
tree.addChild(n1_1_1, n1_1_1_2);
tree.addChild(n1_1_2, n1_1_2_1);
tree.addChild(n1_1_2, n1_1_2_2);
tree.addChild(n1_1_1_1, n1_1_1_1_1);
tree.addChild(n1_1_1_2, n1_1_1_2_1);
tree.addChild(n1_1_2_1, n1_1_2_1_1);
tree.addChild(n1_1_2_2,n1_1_2_2_1);
return tree;
}
}
Upvotes: 2