How to create Grid layout in JUNG - grid-layout

I'm trying to create a graph which is GRID layout by using JUNG.
and i couldn't find how, Is there anyway to create a graph by using JUNG
Thanks in advance

I am not sure whether JUNG directly supports a GridLayout using any of its layout managers. However it is possible to create a grid based layout by adding vertices to a StaticLayout and then changing the position of each vertex according to its coordinates in the grid.
import edu.uci.ics.jung.algorithms.layout.*;
import edu.uci.ics.jung.graph.*;
import edu.uci.ics.jung.visualization.*;
import edu.uci.ics.jung.visualization.control.*;
import java.awt.*;
import javax.swing.*;
public class JungGridLayout extends JFrame {
Graph graph;
StaticLayout layout;
VisualizationViewer vv;
public static void main(String[] args) {
JungGridLayout g = new JungGridLayout(25,5,5);
}
public JungGridLayout(int numNodes, int numRows, int numColumns) {
graph = new SparseMultigraph();
layout = new StaticLayout(graph);
//distance between the nodes
int distX=100;
int distY=100;
//idea is to add the vertices and change and the position of each vertex to a coordinate in a grid
for (int n=0;n<numNodes;n++) {
graph.addVertex(String.valueOf(n));
}
int operatingNode = 0;
for (int i=0;i<numRows;i++) {
for (int j=0;j<numColumns;j++) {
layout.setLocation(String.valueOf(operatingNode++), i*distX, j*distY);
}
}
createVisualization();
createFrame();
}
public void createFrame() {
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.pack();
this.setVisible(true);
}
public void createVisualization() {
vv = new VisualizationViewer(layout, new Dimension(800, 600));
//zooming and transforming
GraphZoomScrollPane zoomPane = new GraphZoomScrollPane(vv);
DefaultModalGraphMouse graphMouse = new DefaultModalGraphMouse();
vv.setGraphMouse(graphMouse);
this.getContentPane().add(zoomPane);
}
}

Related

java.lang.IllegalArgumentException: Problem and Hitbox

I have two problems one this that, if i want to show score with the circle object:
layoutV.getChildren().addAll(virus, score);
I get the following error:
Exception in thread "JavaFX Application Thread" java.lang.IllegalArgumentException: Children: duplicate children added: parent = Pane#6661fc86[styleClass=root].
As far as I understand it is because the Task wants to show multiple scores. So should I use another scene or layout to show score?
My other problem is the hitbox of the object, right know everytime i click the score goes up. I looked up the mouse event getTarget but it does not seem like I can make it so that my object is the only target to use the mouse event on.
public class Main extends Application {
private Stage window;
private Pane layoutV;
private Scene scene;
private Circle virus;
private int score;
private Label scores;
#Override
public void start(Stage primaryStage) {
window = primaryStage;
window.setTitle("Enemy TEST");
this.score = 0;
scores = new Label("Score "+ score);
layoutV = new Pane();
scene = new Scene(layoutV, 600, 600);
window.setScene(scene);
window.show();
Thread th = new Thread(task);
th.setDaemon(true);
th.start();
}
Task task = new Task<Void>() {
#Override
protected Void call() throws Exception {
while (true) {
Platform.runLater(new Runnable() {
#Override
public void run() {
drawCircles();
}
});
Thread.sleep(1000);
}
}
};
public void drawCircles() {
double x = (double)(Math.random() * ((550 - 50) + 1)) + 50;
double y = (double)(Math.random() * ((550 - 50) + 1)) + 50;
double r = (double)(Math.random() * ((30 - 10) + 1)) + 10;
virus = new Circle(x, y, r, Color.VIOLET);
layoutV.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
layoutV.getChildren().remove(e.getTarget());
this.score++;
System.out.println("score: "+ this.score);
}
});
layoutV.getChildren().addAll(virus);
scene.setRoot(layoutV);
window.setScene(scene);
}
public static void main(String[] args) {
launch(args);
}
}
You have lots of issues, not just the ones from your question:
Although it will work as you coded
it, I don't advise spawning a thread to draw your circles, instead
see:
JavaFX periodic background task
You don't need to set the root in the scene and the scene in the
window every time you draw a new circle.
Nor do you need to set the
mouse handler on the layout every time you draw a circle.
Rather than setting a mouse handler on the layout, you are better off setting a mouse handler on the circles themselves (which you can do before you add them to the scene).
score is an int, not a node you can only add nodes to the scene
graph.
See the documentation for the scene package:
A node may occur at most once anywhere in the scene graph. Specifically, a node must appear no more than once in the children list of a Parent or as the clip of a Node. See the Node class for more details on these restrictions.
How you are adding the node more than once is not clear to me, because you are probably doing it in code different than the Main class you provided.
To add a circle with a score on top, use a StackPane with the score in a label, but make the label mouse transparent, so that it does not register any clicks:
Label scoreLabel = new Label(score + "");
scoreLabel.setMouseTransparent(true);
StackPane balloon = new StackPane(circle, scoreLabel);
layoutV.getChildren.add(balloon);
Add the click handler on the balloon.
And additional issues I don't detail here but are solved in the demo code provided.
To fix all your errors, I would write some code like below. Perhaps you can review it and compare it with your code to help understand one way to create this game.
The example code might not be exactly the functionality you are looking for (that is not really its purpose), but it should be enough to keep you on the right track for implementing your application.
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.*;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.concurrent.ThreadLocalRandom;
public class Inoculation extends Application {
public static final int W = 600;
public static final int H = 600;
private final IntegerProperty score = new SimpleIntegerProperty(0);
private final Pane playingField = new Pane();
#Override
public void start(Stage stage) {
StackPane overlay = createOverlay();
Pane layout = new StackPane(playingField, overlay);
stage.setScene(new Scene(layout, W, H));
stage.show();
Infection infection = new Infection(playingField, score);
infection.begin();
}
private StackPane createOverlay() {
Label totalScoreLabel = new Label();
totalScoreLabel.textProperty().bind(
Bindings.concat(
"Score ", score.asString()
)
);
StackPane overlay = new StackPane(totalScoreLabel);
StackPane.setAlignment(totalScoreLabel, Pos.TOP_LEFT);
overlay.setMouseTransparent(true);
return overlay;
}
public static void main(String[] args) {
launch(args);
}
}
class Infection {
private static final Duration SPAWN_PERIOD = Duration.seconds(1);
private static final int NUM_SPAWNS = 10;
private final Timeline virusGenerator;
public Infection(Pane playingField, IntegerProperty score) {
virusGenerator = new Timeline(
new KeyFrame(
SPAWN_PERIOD,
event -> spawnVirus(
playingField,
score
)
)
);
virusGenerator.setCycleCount(NUM_SPAWNS);
}
public void begin() {
virusGenerator.play();
}
private void spawnVirus(Pane playingField, IntegerProperty score) {
Virus virus = new Virus();
virus.setOnMouseClicked(
event -> {
score.set(score.get() + virus.getVirusScore());
playingField.getChildren().remove(virus);
}
);
playingField.getChildren().add(virus);
}
}
class Virus extends StackPane {
private static final int MAX_SCORE = 3;
private static final int RADIUS_INCREMENT = 10;
private final int virusScore = nextRandInt(MAX_SCORE) + 1;
public Virus() {
double r = (MAX_SCORE + 1 - virusScore) * RADIUS_INCREMENT;
Circle circle = new Circle(
r,
Color.VIOLET
);
Text virusScoreText = new Text("" + virusScore);
virusScoreText.setBoundsType(TextBoundsType.VISUAL);
virusScoreText.setMouseTransparent(true);
getChildren().setAll(
circle,
virusScoreText
);
setLayoutX(nextRandInt((int) (Inoculation.W - circle.getRadius() * 2)));
setLayoutY(nextRandInt((int) (Inoculation.H - circle.getRadius() * 2)));
setPickOnBounds(false);
}
public int getVirusScore() {
return virusScore;
}
// next random int between 0 (inclusive) and bound (exclusive)
private int nextRandInt(int bound) {
return ThreadLocalRandom.current().nextInt(bound);
}
}
Some additional notes on this implementation that might be useful to know:
The total score is placed in an overlayPane so that it is not obscured by elements added to the playingField (which contains the virus spawns).
The overlayPane is made mouseTransparent, so that it won't intercept any mouse events, and the clicks will fall through to the items in the playing field.
The app currently generates viruses within a fixed field size, regardless of whether you resize the window. That is just the way it is designed and coded, you could code it otherwise if wished. It would be more work to do so.
The Bindings class is used to create a string expression binding which concatenates the static string "Score " with an integer property representing the score. This allows the string representing the score to be bound to the score label text in the overlay so that it automatically updates whenever the score is changed.
The virus generation uses a timeline and is based on the concepts from:
JavaFX periodic background task
The application class is kept deliberately simple to handle mostly just the core application lifecycle, and the actual functionality of the application is abstracted to an Infection class which handles the spawning of the virus and a Virus class that generates a new virus.
This technique is used to center a score for each individual virus on the virus:
how to put a text into a circle object to display it from circle's center?
The virus itself is laid out in a StackPane. The pane has pick on bounds set to false. To remove the virus infection, you must click on the circle which represents the virus and not just anywhere in the square for the stack pane.
Because circle coordinates are in local co-ordinates and the circle is in a parent stack pane representing the virus, the circle itself does not need x and y values set, instead layout x and y values are set on the enclosing pane to allow positioning of the pane representing the entire virus.
The following technique is used to generate random integers in acceptable ranges using ThreadLocalRandom:
How do I generate random integers within a specific range in Java?

Fixed size JavaFX component

Creating new components in JavaFX is still a but muddy to me compared to "Everything is a JPanel" in Swing.
I'm trying to make a fixed size component. I hesitate to call it a control, it's a pane of activity, not a button.
But here's my problem.
The fixed size I want is smaller than the contents of the element.
The grid is, in truth, 200x200. I'm shifting it up and left 25x25, and I'm trying to make the fixed size of 150x150. You can see in my example I've tried assorted ways of forcing it to 150, but in my tests, the size never sticks. Also, to be clear, I would expect the lines to clip at the boundary of the component.
This is, roughly, what I'm shooting for in my contrived case (note this looks bigger than 150x150 because of the retina display on my Mac, which doubles everything):
I've put some in to a FlowPane, and they stack right up, but ignore the 150x150 dimensions.
FlowPane fp = new FlowPane(new TestPane(), new TestPane(), new TestPane());
var scene = new Scene(fp, 640, 480);
stage.setScene(scene);
I tried sticking one in a ScrollPane, and the scroll bars never appear, even after resizing the window.
TestPane pane = new TestPane();
ScrollPane sp = new ScrollPane(pane);
var scene = new Scene(sp, 640, 480);
stage.setScene(scene);
And I struggle to discern whether I should be extending Region or Control in these cases.
I am missing something fundamental.
package pkg;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.shape.Line;
import javafx.scene.transform.Translate;
public class TestPane extends Control {
public TestPane() {
setMinHeight(150);
setMaxHeight(150);
setMinWidth(150);
setMaxWidth(150);
setPrefHeight(150);
setPrefWidth(150);
populate();
}
#Override
protected double computePrefHeight(double width) {
return 150;
}
#Override
protected double computePrefWidth(double height) {
return 150;
}
#Override
protected double computeMaxHeight(double width) {
return 150;
}
#Override
protected double computeMaxWidth(double height) {
return 150;
}
#Override
protected double computeMinHeight(double width) {
return 150;
}
#Override
protected double computeMinWidth(double height) {
return 150;
}
#Override
public boolean isResizable() {
return false;
}
private void populate() {
Translate translate = new Translate();
translate.setX(-25);
translate.setY(-25);
getTransforms().clear();
getTransforms().addAll(translate);
ObservableList<Node> children = getChildren();
for (int i = 0; i < 4; i++) {
Line line = new Line(0, i * 50, 200, i * 50);
children.add(line);
line = new Line(i * 50, 0, i * 50, 200);
children.add(line);
}
}
}
Addenda, to clarify.
I want a fixed sized component. It's a rectangle. I want it X x Y big.
I want to draw things in my box. Lines, circles, text.
I want the things I draw to clip to the boundaries of the component.
I don't want to use Canvas.
More addenda.
What I'm looking for is not much different from what a ScrollPane does, save I don't want any scroll bars, and I don't want the size of the outlying pane to grow or shrink.
TLDR:
Subclass Region,
make isResizable() return true to respect pref, min, and max sizes,
explicitly set a clip to avoid painting outside the local bounds.
Most of the documentation for this is in the package documentation for javafx.scene.layout
First, note the distinction between resizable and non-resizable nodes. Resizable nodes (for which isResizable() returns true) are resized by their parent during layout, and the parent will make a best-effort to respect their preferred, minimum, and maximum sizes.
Non-resizable nodes are not resized by their parent. If isResizable() returns false, then resize() is a no-op and the preferred, minimum, and maximum sizes are effectively ignored. Their sizes are computed internally and reported to the parent via its visual bounds. Ultimately, all JavaFX nodes have a peer node in the underlying graphical system, and AFAIK the only way a non-resizable node can determine its size is by directly setting the size of the peer. (I'm happy to be corrected on this.)
So unless you want to get your hands really dirty with custom peer nodes (and I don't even know if the API has mechanisms for this), I think the preferred way to create a "fixed size node" is by creating a resizable node with preferred, minimum, and maximum sizes all set to the same value. This is likely by design: as noted in a comment to your question, fixed-size nodes in layout-driven UI toolkits are generally discouraged, other than very low-level components (Text, Shape, etc).
Transformations applied to resizable nodes are generally applied after layout (i.e. they don't affect the layout bounds). Therefore using a translation to manage the internal positioning of the child nodes is not a good approach; it will have effects on the layout of the custom node in the parent which you probably don't intend.
As you note, you are not really defining a control here; it has no behavior or skin. Thus subclassing Control is not really the rigth approach. The most appropriate hook in the API is to subclass Region. Override the layoutChildren() method to position the child nodes (for Shapes and Text nodes, set their coordinates, for resizable children call resizeRelocate(...)).
Finally, to prevent the node spilling out of its intended bounds (150x150 in your example), either ensure no child nodes are positioned outside those bounds, or explicitly set the clip.
Here's a refactoring of your example:
import javafx.scene.layout.Region;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
public class TestPane extends Region {
private Line[] verticalLines ;
private Line[] horizontalLines ;
private static final int WIDTH = 150 ;
private static final int HEIGHT = 150 ;
private static final int LINE_GAP = 50 ;
public TestPane() {
populate();
}
#Override
protected double computePrefHeight(double width) {
return HEIGHT;
}
#Override
protected double computePrefWidth(double height) {
return HEIGHT;
}
#Override
protected double computeMaxHeight(double width) {
return HEIGHT;
}
#Override
protected double computeMaxWidth(double height) {
return WIDTH;
}
#Override
protected double computeMinHeight(double width) {
return WIDTH;
}
#Override
protected double computeMinWidth(double height) {
return WIDTH;
}
#Override
public boolean isResizable() {
return true;
}
#Override
public void layoutChildren() {
double w = getWidth();
double h = getHeight() ;
double actualWidth = verticalLines.length * LINE_GAP ;
double actualHeight = horizontalLines.length * LINE_GAP ;
double hOffset = (actualWidth - w) / 2 ;
double vOffset = (actualHeight - h) / 2 ;
for (int i = 0 ; i < verticalLines.length ; i++) {
double x = i * LINE_GAP - hOffset;
verticalLines[i].setStartX(x);
verticalLines[i].setEndX(x);
verticalLines[i].setStartY(0);
verticalLines[i].setEndY(h);
}
for (int i = 0 ; i < horizontalLines.length ; i++) {
double y = i * LINE_GAP - vOffset;
horizontalLines[i].setStartY(y);
horizontalLines[i].setEndY(y);
horizontalLines[i].setStartX(0);
horizontalLines[i].setEndX(w);
}
setClip(new Rectangle(0, 0, w, h));
}
private void populate() {
verticalLines = new Line[4] ;
horizontalLines = new Line[4] ;
for (int i = 0 ; i <verticalLines.length ; i++) {
verticalLines[i] = new Line();
getChildren().add(verticalLines[i]);
}
for (int i = 0 ; i <horizontalLines.length ; i++) {
horizontalLines[i] = new Line();
getChildren().add(horizontalLines[i]);
}
}
}
A more sophisticated example might have, for example, LINE_GAP as a property. When that property changes you would call requestLayout() to mark the component as "dirty", so its layoutChildren() method would be called again on the next frame rendered.
Here's a quick test case:
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage stage) {
FlowPane root = new FlowPane();
root.setAlignment(Pos.TOP_LEFT);
root.setPadding(new Insets(10));
root.setHgap(5);
root.setVgap(5);
for (int i = 0; i < 6 ; i++) {
root.getChildren().add(new TestPane());
}
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Which results in:
This plays nicely with the layout pane; resizing the window gives

JavaFX PieChart Legend Color change

I need to change color of circles in PieChart Legend. I don't know how to get to this property of PieChart. For example I'm able to change color of text in label Legend and I think this is close to the solution.
It shows what I want to change:
#FXML
public PieChart chart;
public ObservableList<PieChart.Data> pieChartData = FXCollections.observableArrayList();
public void chartLoad() {
pieChartData.clear();
List<String> colorList = new ArrayList<>();
for(int i = 0; i < categoryList.getSize(); i++) {
if(categoryList.getByIndex(i).getValue() > 0) {
PieChart.Data data = new PieChart.Data(categoryList.getByIndex(i).getName(),
categoryList.getByIndex(i).getValue());
pieChartData.add(data);
data.getNode().setStyle("-fx-pie-color: " +
categoryList.getByIndex(i).getColor().getName());
colorList.add(categoryList.getByIndex(i).getColor().getName());
}
}
Set<Node> items = chart.lookupAll("Label.chart-legend-item");
int i = 0;
for(Node item : items) {
Label label = (Label) item;
label.setText("sampleText");
label.setStyle("-fx-text-fill: " + colorList.get(i));
System.out.println(label.getChildrenUnmodifiable().toString());
i++;
}
chart.setData(pieChartData);
}
Thank you for your future comments and answers.
Dynamically allocating colors to charts is a bit of a pain. If you have a fixed set of colors, without a predefined mapping from your data to the colors, you can just use an external style sheet, but doing anything else needs (as far as I know) a bit of a hack.
The default modena.css style sheet defines eight constant colors, CHART_COLOR_1 to CHART_COLOR_8. Nodes in a pie chart, including both the "pie slices" and the color swatches in the legend, are assigned a style class from the eight classes default-color0 to default-color7. Each of these style classes by default has -fx-pie-color set to one of the constants. Unfortunately, if the data in the pie chart are changed, these mappings from default-colorx to CHART_COLOR_y change in a way that isn't documented.
So the best approach for your scenario that I can find is:
add the new data to the chart
once all the data are added, for each datum look up the style class that was added to the datum's node
look up all the nodes in the chart that have that style class (this will also give the legend swatches)
update the -fx-pie-color for those nodes to the desired color
The last trap here is that you need to make sure the legend is added to the chart, and that CSS is applied to the chart, so that the lookup works.
public void chartLoad() {
pieChartData.clear();
List<String> colors = new ArrayList<>();
for(int i = 0; i < categoryList.getSize(); i++) {
if(categoryList.getByIndex(i).getValue() > 0) {
PieChart.Data data = new PieChart.Data(categoryList.getByIndex(i).getName(),
categoryList.getByIndex(i).getValue());
pieChartData.add(data);
colors.add(categoryList.getByIndex(i).getColor().getName());
}
}
chart.setData(pieChartData);
chart.requestLayout();
chart.applyCSS();
for (int i = 0 ; i < pieChartData.size() ; i++) {
PieChart.Data d = pieChartData.get(i);
String colorClass = "" ;
for (String cls : d.getNode().getStyleClass()) {
if (cls.startsWith("default-color")) {
colorClass = cls ;
break ;
}
}
for (Node n : chart.lookupAll("."+colorClass)) {
n.setStyle("-fx-pie-color: "+colors.get(i));
}
}
}
Here's a quick, complete demo of this approach:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.PieChart;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class PieChartTest extends Application {
private final Random rng = new Random();
#Override
public void start(Stage primaryStage) throws Exception {
PieChart chart = new PieChart();
Button button = new Button("Generate Data");
button.setOnAction(e -> updateChart(chart));
BorderPane root = new BorderPane(chart);
HBox controls = new HBox(button);
controls.setAlignment(Pos.CENTER);
controls.setPadding(new Insets(5));
root.setTop(controls);
Scene scene = new Scene(root, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private void updateChart(PieChart chart) {
chart.getData().clear();
int numValues = 4 + rng.nextInt(10);
List<String> colors = new ArrayList<>();
List<PieChart.Data> data = new ArrayList<>();
for (int i = 0 ; i < numValues ; i++) {
colors.add(getRandomColor());
PieChart.Data d = new PieChart.Data("Item "+i, rng.nextDouble() * 100);
data.add( d );
chart.getData().add(d) ;
}
chart.requestLayout();
chart.applyCss();
for (int i = 0 ; i < data.size() ; i++) {
String colorClass = "" ;
for (String cls : data.get(i).getNode().getStyleClass()) {
if (cls.startsWith("default-color")) {
colorClass = cls ;
break ;
}
}
for (Node n : chart.lookupAll("."+colorClass)) {
n.setStyle("-fx-pie-color: "+colors.get(i));
}
}
}
private String getRandomColor() {
Color color = Color.hsb(rng.nextDouble() * 360, 1, 1);
int r = (int) (255 * color.getRed()) ;
int g = (int) (255 * color.getGreen());
int b = (int) (255 * color.getBlue()) ;
return String.format("#%02x%02x%02x", r, g, b) ;
}
public static void main(String[] args) {
Application.launch(args);
}
}
This really is a bit of a hack, so better solutions are obviously welcome.

Create hexagonal field with JavaFX

I have the goal to create a field of hexagonal tiles. I have come as far as having a matrix of cells, each high enough to fit the complete hexagon image:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class UITest extends Application {
final private static String TILE_IMAGE_LOCATION = System.getProperty("user.dir") + File.separatorChar +"resources"+ File.separatorChar + "blueTile.png";
final private static Image HEXAGON_IMAGE = initTileImage();
private static Image initTileImage() {
try {
return new Image(new FileInputStream(new File(TILE_IMAGE_LOCATION)));
} catch (FileNotFoundException e) {
throw new IllegalStateException(e);
}
}
public void start(Stage primaryStage) {
int height = 4;
int width = 6;
GridPane tileMap = new GridPane();
Scene content = new Scene(tileMap, 800, 600);
primaryStage.setScene(content);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
ImageView tile = new ImageView(HEXAGON_IMAGE);
GridPane.setConstraints(tile, x, y);
tileMap.getChildren().add(tile);
}
}
primaryStage.show();
}
}
My problem is not the vertical gap, which I can surely figure out by adding the GridPane's vGap() to a proper value. The difficulty for me is shifting each second row half a cellwidth to the right.
I have attempted to lay two GridPanes over eachother, one containing the odd and one the even rows, with the goal to add padding to one of them, shifting it entirely. To my knowledge however, there is no way for this, as well as nesting GridPanes into on another.
How can I best achieve the shifting of only every second row?
(The image I reference in the code which is expected in the ${projectroot}/resources/ folder: )
It took me some time to figure it out. I hope it helps. I don't use an image. It's made of polygons, you can customize the stroke and fill color, as well as the width.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Polygon;
public class UITest extends Application {
public void start(Stage primaryStage) {
int height = 600;
int width = 800;
AnchorPane tileMap = new AnchorPane();
Scene content = new Scene(tileMap, width, height);
primaryStage.setScene(content);
double size = 50,v=Math.sqrt(3)/2.0;
for(double y=0;y<height;y+=size*Math.sqrt(3))
{
for(double x=-25,dy=y;x<width;x+=(3.0/2.0)*size)
{
Polygon tile = new Polygon();
tile.getPoints().addAll(new Double[]{
x,dy,
x+size,dy,
x+size*(3.0/2.0),dy+size*v,
x+size,dy+size*Math.sqrt(3),
x,dy+size*Math.sqrt(3),
x-(size/2.0),dy+size*v
});
tile.setFill(Paint.valueOf("#ffffff"));
tile.setStrokeWidth(2);
tile.setStroke(Paint.valueOf("#000000") );
tileMap.getChildren().add(tile);
dy = dy==y ? dy+size*v : y;
}
}
primaryStage.show();
}
public static void main(String[] args)
{
launch(args);
}
}
For other interested souls out there, I have used the accepted answer by Cthulhu and improved/documented the given code as a short standalone demonstration:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.stage.Stage;
public class UISolution extends Application {
private final static int WINDOW_WIDTH = 800;
private final static int WINDOW_HEIGHT = 600;
private final static double r = 20; // the inner radius from hexagon center to outer corner
private final static double n = Math.sqrt(r * r * 0.75); // the inner radius from hexagon center to middle of the axis
private final static double TILE_HEIGHT = 2 * r;
private final static double TILE_WIDTH = 2 * n;
public static void main(String[] args) {
launch(args);
}
public void start(Stage primaryStage) {
AnchorPane tileMap = new AnchorPane();
Scene content = new Scene(tileMap, WINDOW_WIDTH, WINDOW_HEIGHT);
primaryStage.setScene(content);
int rowCount = 4; // how many rows of tiles should be created
int tilesPerRow = 6; // the amount of tiles that are contained in each row
int xStartOffset = 40; // offsets the entire field to the right
int yStartOffset = 40; // offsets the entire fiels downwards
for (int x = 0; x < tilesPerRow; x++) {
for (int y = 0; y < rowCount; y++) {
double xCoord = x * TILE_WIDTH + (y % 2) * n + xStartOffset;
double yCoord = y * TILE_HEIGHT * 0.75 + yStartOffset;
Polygon tile = new Tile(xCoord, yCoord);
tileMap.getChildren().add(tile);
}
}
primaryStage.show();
}
private class Tile extends Polygon {
Tile(double x, double y) {
// creates the polygon using the corner coordinates
getPoints().addAll(
x, y,
x, y + r,
x + n, y + r * 1.5,
x + TILE_WIDTH, y + r,
x + TILE_WIDTH, y,
x + n, y - r * 0.5
);
// set up the visuals and a click listener for the tile
setFill(Color.ANTIQUEWHITE);
setStrokeWidth(1);
setStroke(Color.BLACK);
setOnMouseClicked(e -> System.out.println("Clicked: " + this));
}
}
}

How to get JavaFX TreeView to behave consistently upon node expansion?

I have a JavaFX TreeView with an invisible root and a handful of 'folder' TreeItems that have many 'file' TreeItems as children. The 'folder' TreeItems typically fit inside the TreeView without there being any scrollbars.
invisible-root/
folder/
folder/
folder/
file
file
file
...
file
Sometimes, when I expand a 'folder' TreeItem, the scrollbars appear but the scroll position remains the same. (This is what I want!) However, sometimes, expanding a TreeItem causes the scrollbars appear and the TableView scrolls to the last child of the expanded TreeItem!
This is very unexpected and surprising, especially since I have difficulty predicting which of the two behaviors I will see: (1) stay put, or (2) scroll to last item. Personally, I think behavior (1) is less surprising and preferable.
Any thoughts on how to deal with this?
I see this behavior on Java8u31.
The problem is in VirtualFlow. In layoutChildren() there is this section:
if (lastCellCount != cellCount) {
// The cell count has changed. We want to keep the viewport
// stable if possible. If position was 0 or 1, we want to keep
// the position in the same place. If the new cell count is >=
// the currentIndex, then we will adjust the position to be 1.
// Otherwise, our goal is to leave the index of the cell at the
// top consistent, with the same translation etc.
if (position == 0 || position == 1) {
// Update the item count
// setItemCount(cellCount);
} else if (currentIndex >= cellCount) {
setPosition(1.0f);
// setItemCount(cellCount);
} else if (firstCell != null) {
double firstCellOffset = getCellPosition(firstCell);
int firstCellIndex = getCellIndex(firstCell);
// setItemCount(cellCount);
adjustPositionToIndex(firstCellIndex);
double viewportTopToCellTop = -computeOffsetForCell(firstCellIndex);
adjustByPixelAmount(viewportTopToCellTop - firstCellOffset);
}
The problem arises if position is 1.0 (== scrolled to bottom), because in that case there is no recalculation. A workaround would be to override the TreeViewSkin to provide your own VirtualFlow and fix the behavior there.
The code below is meant to illustrate the problem, it's not a real solution, just a starting point if you really want to fix it:
import com.sun.javafx.scene.control.skin.TreeViewSkin;
import com.sun.javafx.scene.control.skin.VirtualFlow;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.Skin;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class TreeViewScrollBehaviour extends Application {
#Override
public void start(Stage primaryStage) {
TreeView treeView = new TreeView() {
#Override
protected Skin createDefaultSkin() {
return new TTreeViewSkin(this); //To change body of generated methods, choose Tools | Templates.
}
};
TreeItem<String> treeItem = new TreeItem<String>("Root");
for (int i = 0; i < 20; i++) {
TreeItem<String> treeItem1 = new TreeItem<>("second layer " + i);
treeItem.getChildren().add(treeItem1);
for (int j = 0; j < 20; j++) {
treeItem1.getChildren().add(new TreeItem<>("Third Layer " + j));
}
}
treeView.setRoot(treeItem);
StackPane root = new StackPane();
root.getChildren().addAll(treeView);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
class TTreeViewSkin<T extends IndexedCell> extends TreeViewSkin<T> {
public TTreeViewSkin(TreeView treeView) {
super(treeView);
}
#Override
protected VirtualFlow createVirtualFlow() {
return new TVirtualFlow<T>(); //To change body of generated methods, choose Tools | Templates.
}
}
class TVirtualFlow<T extends IndexedCell> extends VirtualFlow<T> {
#Override
public double getPosition() {
double position = super.getPosition();
if (position == 1.0d) {
return 0.99999999999;
}
return super.getPosition(); //To change body of generated methods, choose Tools | Templates.
}
#Override
public void setPosition(double newPosition) {
if (newPosition == 1.0d) {
newPosition = 0.99999999999;
}
super.setPosition(newPosition); //To change body of generated methods, choose Tools | Templates.
}
}
}

Resources