Something like Graphviz but more specifically, yFiles.
I want a node/edge type of graph visualization.
I was thinking about making the node a Circle and the edge a Line. The problem is what to use for the area where the nodes/edges appear. Should I use a ScrollPane, a regular Pane, a Canvas, etc...
I will add scrolling functionality, zooming, selecting nodes & dragging nodes.
Thanks for the help.
I had 2 hours to kill, so I thought I'd give it a shot. Turns out that it's easy to come up with a prototype.
Here's what you need:
a main class to use the graph library you create
a graph with a data model
easy adding and removing of nodes and edges (turns out that it's better to name the nodes cells in order to avoid confusion with JavaFX nodes during programming)
a zoomable scrollpane
a layout algorithm for the graph
It's really too much to be asked on SO, so I'll just add the code with a few comments.
The application instantiates the graph, adds cells and connects them via edges.
application/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);
}
}
The scrollpane should have a white background.
application/application.css
.scroll-pane > .viewport {
-fx-background-color: white;
}
The zoomable scrollpane, I got the 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();
}
}
}
}
Every cell is represented as Pane into which you can put any Node as view (rectangle, label, imageview, etc)
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;
}
}
The cells should be created via some kind of factory, so they are classified by type:
CellType.java
package com.fxgraph.graph;
public enum CellType {
RECTANGLE,
TRIANGLE
;
}
Instantiating them is quite easy:
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);
}
}
Then of course you need the edges. You can use any connection you like, even cubic curves. For sake of simplicity I use a line:
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;
}
}
An extension to this would be to bind the edge to ports (north/south/east/west) of the cells.
Then you'd want to drag the nodes, so you'd have to add some mouse gestures. The important part is to consider a zoom factor in case the graph canvas is zoomed
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;
}
}
Then you need a model in which you store the cells and the edges. Any time new cells can be added and existing ones can be deleted. You need to process them distinguished from the existing ones (e. g. to add mouse gestures, animate them when you add them, etc). When you implement the layout algorithm you'll be faced with the determination of a root node. So you should make an invisible root node (graphParent) which won't be added to the graph itself, but at which all nodes start that don't have a parent.
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();
}
}
And then there's the graph itself which contains the zoomable scrollpane, the model, etc. In the graph the added and removed nodes are handled (mouse gestures, cells and edges added to the scrollpane, etc).
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();
}
}
A wrapper for the cell layer. You'll probably want to add multiple layers (e. g. a selection layer which highlights selected cells)
CellLayer.java
package com.fxgraph.graph;
import javafx.scene.layout.Pane;
public class CellLayer extends Pane {
}
Now you need a layout for the cells. I suggest to create a simple abstract class which will get extended as you develop the graph.
package com.fxgraph.layout.base;
public abstract class Layout {
public abstract void execute();
}
For sake of simplicity here's a simple layout algorithm in which random coordinates are used. Of course you'd have to do more complex stuff like tree layouts, etc.
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);
}
}
}
The example looks like this:
You can drag the cells with the mouse button and zoom in and out with the mouse wheel.
Adding new cell types is as easy as creating subclasses of 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);
}
}
and creating the types
package com.fxgraph.graph;
public enum CellType {
RECTANGLE,
TRIANGLE,
LABEL,
IMAGE,
BUTTON,
TITLEDPANE
;
}
and creating instances depending on the type:
...
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);
}
}
...
and you'll get this
I had the same problem, I managed to use the javascript vis.js library along with JavaFX WebView.
You can check it out on github if that's useful to someone: https://github.com/arocketman/VisFX
I would give Prefux a try. It is a fork of the Prefuse project.
The original repository starting with JavaFX porting is https://github.com/effrafax/Prefux, but the most maintained fork seems to be the one above (https://github.com/jchildress/Prefux).
Another attempt to port to JavaFX was started at https://github.com/gedeffe/Prefuse, but it is not active anymore.
You can use jfreechart api for generating graph visualization
It provides, Line , Pie, bars. and it is very eary to use.
Related
I have a JavaFX program in which I am looking to assign an event handler for a button but I want the event to trigger an action in a parent unit.
I have a GridPane as my main pane.
When I start my program, I have another Group of shapes (Called a Block) embedded in this gridpane at a fixed location (0,0).
As part of my Block, I have a Triangle button called DownArrow. My Plan is that when this button is pressed, the Doc controller will place a new Block beneath the one where the triangle was pressed.
I am looking at using the DownArrow.setOnMouseClicked event handler and map this back to the routine in the Controller to Add a new Block... However, I need some sort of info callback to let the controller know what Block was pressed.
I'm Stumped.
I was looking at maybe creating a custom event handler that I can pass more parameters but it seems clumsy - is there something else I should be doing?
How to pass paremeter with an event in javafx?
See Code Below in full:
package editorscratchpad;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.scene.text.Font;
import javafx.stage.Stage;
/**
*
* #author a_curley
*/
public class EditorScratchPad extends Application {
HBox mainPanel;
VBox flowChart;
GridPane flowchartGrid;
VBox controlPanel;
Integer stepCount;
TextField descData;
#Override
public void start(Stage primaryStage) {
Button btn = new Button();
descData = new TextField();
stepCount = 0;
btn.setText("Add Step");
btn.setOnAction((ActionEvent event) -> {
buttonClicked(event);
});
ScrollPane sp = new ScrollPane();
mainPanel = new HBox(20);
//mainPanel.setPrefWidth(400);
controlPanel = new VBox(5);
controlPanel.setAlignment(Pos.CENTER);
flowChart = new VBox();
flowChart.setPrefWidth(600);
flowChart.setAlignment(Pos.CENTER);
flowchartGrid = new GridPane();
flowchartGrid.setPrefWidth(600);
flowchartGrid.setHgap(10);
flowchartGrid.setVgap(10);
flowchartGrid.setPadding(new Insets(0, 10, 0, 10));
//sp.setContent(flowChart);
sp.setContent(flowchartGrid);
controlPanel.getChildren().add(btn);
controlPanel.getChildren().add(descData);
mainPanel.getChildren().add(sp);
mainPanel.getChildren().add(controlPanel);
Scene scene = new Scene(mainPanel, 800, 500);
primaryStage.sizeToScene();
primaryStage.setTitle("Flow chart");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
public void buttonClicked(ActionEvent event) {
//Integer Step = stepCount*10;
//Integer Step = (flowChart.getChildren().size()+1)*10;
Integer Step = (flowchartGrid.getChildren().size() + 1);
String stateDesc = descData.getText();
if (stateDesc.length() < 3) {
stateDesc = "State " + Step.toString();
}
blockComponent newBlock = new blockComponent("S" + Integer.toString(Step * 10), stateDesc, Step); //<<<< Create the Block.
//flowChart.getChildren().add(newBlock.getComponent());
flowchartGrid.add(newBlock.getComponent(), 0, Step);
System.out.println("Added S" + Integer.toString(Step * 10));
stepCount++;
}
public void addNewBlock(ActionEvent event) {
Integer Step = (flowchartGrid.getChildren().size() + 1);
String stateDesc = "Step " + Integer.toString(Step * 10);
}
/**
* Subclass representing a Block Graphics only.
*/
public class blockComponent {
String Name;
String Desc;
Integer StateNo;
Integer XLoc;
Integer YLoc;
Integer blockHeight;
Integer blockWidth;
Integer stLabRad;
public blockComponent(String newName, String newDesc, Integer newSt) {
Name = newName;
Desc = newDesc;
StateNo = newSt;
XLoc = 0;
YLoc = 0;
blockHeight = 60;
blockWidth = 120;
stLabRad = 10;
}
public blockComponent(String newName, String newDesc, Integer newSt, Integer xCoOrd, Integer yCoOrd) {
this(newName, newDesc, newSt);
XLoc = xCoOrd;
YLoc = yCoOrd;
}
public Group getComponent() {
Group thisGroup = new Group();
// Define Rectangle
Rectangle Block = new Rectangle();
Block.setY(stLabRad);
Block.setX(stLabRad);
Block.setHeight(blockHeight);
Block.setWidth(blockWidth);
Block.setStroke(Color.BLACK);
Block.setStrokeWidth(2);
Block.setFill(Color.WHITESMOKE);
// Define state label
Circle stLab = new Circle();
stLab.setCenterX(stLabRad);
stLab.setCenterY(stLabRad);
stLab.setRadius(stLabRad);
stLab.setStroke(Color.PALEGREEN);
stLab.setFill(Color.PALEGREEN);
// Define State No.
Label stNo = new Label();
stNo.setFont(Font.font("Impact"));
stNo.setTextFill(Color.WHITE);
stNo.setLayoutX(0);
stNo.setLayoutY(0);
stNo.setText(StateNo.toString());
// Define the description
Label stD = new Label();
stD.setFont(Font.font("Impact"));
stD.setTextFill(Color.BLACK);
stD.setLayoutX(15);
stD.setLayoutY(15);
stD.setText(Desc.toString());
//---- Three Triangles for drawing. ----
// Down Arrow
Polygon downArrow = new Polygon();
downArrow.getPoints().addAll(new Double[]{
//X //Y
(blockWidth.doubleValue() / 2) + stLabRad, (blockHeight.doubleValue()),
((blockWidth.doubleValue() / 2) + 10) + stLabRad, (blockHeight.doubleValue() - 10),
((blockWidth.doubleValue() / 2) - 10) + stLabRad, (blockHeight.doubleValue() - 10)
// 70.0,60.0,
// 60.0,50.0,
// 80.0,50.0
});
downArrow.setStroke(Color.BLACK);
downArrow.setStrokeWidth(1);
downArrow.setFill(Color.LIGHTCYAN);
downArrow.setOnMouseEntered(new EventHandler<MouseEvent>() {
public void handle(MouseEvent me) {
downArrow.setStroke(Color.BLACK);
downArrow.setFill(Color.LIGHTCYAN);
}
});
downArrow.setOnMouseExited(new EventHandler<MouseEvent>() {
public void handle(MouseEvent me) {
downArrow.setStroke(Color.WHITESMOKE);
downArrow.setFill(Color.WHITESMOKE);
}
});
// try: https://stackoverflow.com/questions/35372236/how-to-pass-paremeter-with-an-event-in-javafx
//add all components to the group to display
thisGroup.getChildren().add(Block);
thisGroup.getChildren().add(stD);
thisGroup.getChildren().add(stLab);
thisGroup.getChildren().add(stNo);
thisGroup.getChildren().add(downArrow);
return thisGroup;
}
}
}
Not sure if this was the correct approach I was looking for but #0009laH response above for getSource() was the best that worked for me.
I'm trying to animate elements in GridPane. I have a class Unit that represents the things that I'm trying to move.
public class Unit {
private Text text;
private Rectangle rectangle;
private StackPane stackPane;
public Unit(Text text, Rectangle rectangle) {
this.text = text;
this.rectangle = rectangle;
text.setFill(Color.WHITE);
stackPane = new StackPane(rectangle, text);
}
public Text getText() {
return text;
}
public void setText(Text text) {
this.text = text;
}
public Rectangle getRectangle() {
return rectangle;
}
public void setRectangle(Rectangle rectangle) {
this.rectangle = rectangle;
}
public StackPane getStackPane() {
return stackPane;
}
public void setStackPane(StackPane stackPane) {
this.stackPane = stackPane;
}
}
This is how i'm moving things now
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception{
GridPane gridPane = new GridPane();
gridPane.setVgap(5);
gridPane.setHgap(5);
Unit unit = new Unit(new Text("1"), new Rectangle(50, 50));
gridPane.add(unit.getStackPane(), 0, 0);
TranslateTransition translateTransition = new TranslateTransition();
translateTransition.setDuration(Duration.seconds(6));
translateTransition.setToX(200);
translateTransition.setToY(200);
translateTransition.setNode(unit.getStackPane());
translateTransition.play();
Scene scene = new Scene(gridPane, 300, 275);
primaryStage.setTitle("Hello World");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Is there a way to move the unit to a specific row - column location. I assume that gridpane might not be suitable for my purpose but it's easy way to layout things the way i want.
Here is an example based upon the layout animator at Animation upon layout changes (also in the gist https://gist.github.com/jewelsea/5683558). I don't really know if it is really what you are looking for (it might be pretty close). But in any case it is pretty neat ;-)
I won't explain it too much here as the main explanation on what it is and how it works is at the previously linked question.
The referenced Unit class is the one from your question.
AnimatedSparseGrid.java
import javafx.animation.*;
import javafx.application.Application;
import javafx.collections.*;
import javafx.geometry.Point2D;
import javafx.scene.*;
import javafx.scene.layout.GridPane;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.*;
public class AnimatedSparseGrid extends Application {
private static final int NUM_UNITS = 10;
private static final int UNIT_SIZE = 30;
private static final int GRID_SIZE = 5;
private static final int GAP = 5;
private static final Duration PAUSE_DURATION = Duration.seconds(3);
private Random random = new Random(42);
private ObservableList<Unit> units = FXCollections.observableArrayList();
private GridPane gridPane = new GridPane();
#Override
public void start(Stage stage) throws Exception {
configureGrid();
LayoutAnimator animator = new LayoutAnimator();
animator.observe(gridPane.getChildren());
generateUnits();
relocateUnits();
continuouslyAnimateGrid();
stage.setScene(new Scene(gridPane));
stage.setResizable(false);
stage.show();
}
private void configureGrid() {
gridPane.setVgap(GAP);
gridPane.setHgap(GAP);
int size = GRID_SIZE * UNIT_SIZE + GAP * (GRID_SIZE - 1);
gridPane.setMinSize(size, size);
gridPane.setMaxSize(size, size);
}
private void generateUnits() {
for (int i = 0; i < NUM_UNITS; i++) {
Unit unit = new Unit(
new Text((i + 1) + ""),
new Rectangle(UNIT_SIZE, UNIT_SIZE)
);
units.add(unit);
}
}
private void relocateUnits() {
Set<Point2D> usedLocations = new HashSet<>();
for (Unit unit : units) {
Node node = unit.getStackPane();
int col;
int row;
do {
col = random.nextInt(GRID_SIZE);
row = random.nextInt(GRID_SIZE);
} while (usedLocations.contains(new Point2D(col, row)));
usedLocations.add(new Point2D(col, row));
GridPane.setConstraints(unit.getStackPane(), col, row);
if (!gridPane.getChildren().contains(node)) {
gridPane.add(node, col, row);
}
}
}
private void continuouslyAnimateGrid() {
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO, event -> relocateUnits()),
new KeyFrame(PAUSE_DURATION)
);
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
}
public static void main(String[] args) {
launch(args);
}
}
Some what hackish, but you can move the unit to the desired row - column, measure the new X,Y and use it for the translation.
The following code demonstrates animating from 0,0 to 20,20 :
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Main extends Application {
private Button move;
private Pane unitPane, root;
private GridPane gridPane;
#Override
public void start(Stage primaryStage) throws Exception{
gridPane = new GridPane();
gridPane.setVgap(5);
gridPane.setHgap(5);
unitPane = new Unit(new Text("1"), new Rectangle(50, 50)).getStackPane();
gridPane.add(unitPane, 0, 0);
move = new Button("Move");
move.setOnAction(e->animate());
root = new BorderPane(gridPane, null, null, move, null);
Scene scene = new Scene(root, 300, 275);
primaryStage.setTitle("Hello World");
primaryStage.setScene(scene);
primaryStage.show();
}
private void animate() {
//remove unit, make it invisible, and add it to desired location
gridPane.getChildren().remove(unitPane);
unitPane.setVisible(false);
gridPane.add(unitPane, 20, 20);
root.layout(); //apply top down layout pass
//get x y of new location
double x = unitPane.getLayoutX(); double y = unitPane.getLayoutY(); //measure new location
//return to original location
gridPane.getChildren().remove(unitPane);
gridPane.add(unitPane, 0, 0);
unitPane.setVisible(true);
//apply translation to x,y of new location
TranslateTransition translateTransition = new TranslateTransition();
translateTransition.setDuration(Duration.seconds(3));
translateTransition.setToX(x);
translateTransition.setToY(y);
translateTransition.setNode(unitPane);
translateTransition.play();
//when translation is finished remove from original location
//add to desired location and set translation to 0
translateTransition.setOnFinished(e->{
gridPane.getChildren().remove(unitPane);
unitPane.setTranslateX(0);unitPane.setTranslateY(0);
gridPane.add(unitPane, 20, 20);
});
}
public static void main(String[] args) {
launch(args);
}
}
class Unit {
private Text text;
private Rectangle rectangle;
private StackPane stackPane;
Unit(Text text, Rectangle rectangle) {
this.text = text;
this.rectangle = rectangle;
text.setFill(Color.WHITE);
stackPane = new StackPane(rectangle, text);
}
public Text getText() {
return text;
}
public void setText(Text text) {
this.text = text;
}
public Rectangle getRectangle() {
return rectangle;
}
public void setRectangle(Rectangle rectangle) {
this.rectangle = rectangle;
}
public StackPane getStackPane() {
return stackPane;
}
public void setStackPane(StackPane stackPane) {
this.stackPane = stackPane;
}
}
Using this example here:
https://stackoverflow.com/a/30509195
Works great to create multiple tables for summation rows. I also needed the scrollbar visible on the bottom table as well. However, the bottom table's scrollbar doesn't sync with the main table (at first with empty content). When there is data, the scrollbar syncs properly.
When you add data to the table, then remove the data, again, scroll bars sync properly. So I know they can still be synced with a table with empty content.
Here is the example code (with two buttons on the top to add and clear items)
package testsummary;
import java.text.Format;
import java.time.LocalDate;
import java.time.Month;
import java.util.Set;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.text.TextAlignment;
import javafx.stage.Stage;
import javafx.util.Callback;
/**
* Table with a summary table. The summary table is a 2nd table which is
* synchronized with the primary table.
*
* TODO: + always show vertical bars for both the primary and the summary table,
* otherweise the width of both tables wouldn't be the same + hide the
* horizontal scrollbar of the summary table
*
*/
public class SummaryTableDemo extends Application
{
private TableView<Data> mainTable = new TableView<>();
private TableView<SumData> sumTable = new TableView<>();
private final ObservableList<Data> data
= FXCollections.observableArrayList();
// TODO: calculate values
private final ObservableList<SumData> sumData
= FXCollections.observableArrayList(
new SumData("Sum", 0.0, 0.0, 0.0),
new SumData("Min", 0.0, 0.0, 0.0),
new SumData("Max", 0.0, 0.0, 0.0)
);
final HBox hb = new HBox();
public static void main(String[] args)
{
launch(args);
}
#Override
public void start(Stage stage)
{
Scene scene = new Scene(new Group());
// load css
// scene.getStylesheets().addAll(getClass().getResource("application.css").toExternalForm());
stage.setTitle("Table View Sample");
stage.setWidth(250);
stage.setHeight(550);
// setup table columns
setupMainTableColumns();
setupSumTableColumns();
// fill tables with data
mainTable.setItems(data);
sumTable.setItems(sumData);
// set dimensions
sumTable.setPrefHeight(90);
// bind/sync tables
for (int i = 0; i < mainTable.getColumns().size(); i++)
{
TableColumn<Data, ?> mainColumn = mainTable.getColumns().get(i);
TableColumn<SumData, ?> sumColumn = sumTable.getColumns().get(i);
// sync column widths
sumColumn.prefWidthProperty().bind(mainColumn.widthProperty());
// sync visibility
sumColumn.visibleProperty().bindBidirectional(mainColumn.visibleProperty());
}
// allow changing of column visibility
//mainTable.setTableMenuButtonVisible(true);
// hide header (variation of jewelsea's solution: http://stackoverflow.com/questions/12324464/how-to-javafx-hide-background-header-of-a-tableview)
sumTable.getStyleClass().add("tableview-header-hidden");
// hide horizontal scrollbar via styles
// sumTable.getStyleClass().add("sumtable");
// create container
BorderPane bp = new BorderPane();
Button addButton = new Button("+");
Button clearButton = new Button("X");
addButton.setOnAction((ActionEvent c) ->
{
data.add(new Data(LocalDate.of(2015, Month.JANUARY, 11), 40.0, 50.0, 60.0));
});
clearButton.setOnAction((ActionEvent c) ->
{
data.clear();
});
HBox buttonBar = new HBox(clearButton, addButton);
bp.setTop(buttonBar);
bp.setCenter(mainTable);
bp.setBottom(sumTable);
// fit content
bp.prefWidthProperty().bind(scene.widthProperty());
bp.prefHeightProperty().bind(scene.heightProperty());
((Group) scene.getRoot()).getChildren().addAll(bp);
stage.setScene(scene);
stage.show();
// synchronize scrollbars (must happen after table was made visible)
ScrollBar mainTableHorizontalScrollBar = findScrollBar(mainTable, Orientation.HORIZONTAL);
ScrollBar sumTableHorizontalScrollBar = findScrollBar(sumTable, Orientation.HORIZONTAL);
mainTableHorizontalScrollBar.valueProperty().bindBidirectional(sumTableHorizontalScrollBar.valueProperty());
}
/**
* Primary table column mapping.
*/
private void setupMainTableColumns()
{
TableColumn<Data, LocalDate> dateCol = new TableColumn<>("Date");
dateCol.setPrefWidth(120);
dateCol.setCellValueFactory(new PropertyValueFactory<>("date"));
TableColumn<Data, Double> value1Col = new TableColumn<>("Value 1");
value1Col.setPrefWidth(90);
value1Col.setCellValueFactory(new PropertyValueFactory<>("value1"));
value1Col.setCellFactory(new FormattedTableCellFactory<>(TextAlignment.RIGHT));
TableColumn<Data, Double> value2Col = new TableColumn<>("Value 2");
value2Col.setPrefWidth(90);
value2Col.setCellValueFactory(new PropertyValueFactory<>("value2"));
value2Col.setCellFactory(new FormattedTableCellFactory<>(TextAlignment.RIGHT));
TableColumn<Data, Double> value3Col = new TableColumn<>("Value 3");
value3Col.setPrefWidth(90);
value3Col.setCellValueFactory(new PropertyValueFactory<>("value3"));
value3Col.setCellFactory(new FormattedTableCellFactory<>(TextAlignment.RIGHT));
mainTable.getColumns().addAll(dateCol, value1Col, value2Col, value3Col);
}
/**
* Summary table column mapping.
*/
private void setupSumTableColumns()
{
TableColumn<SumData, String> textCol = new TableColumn<>("Text");
textCol.setCellValueFactory(new PropertyValueFactory<>("text"));
TableColumn<SumData, Double> value1Col = new TableColumn<>("Value 1");
value1Col.setCellValueFactory(new PropertyValueFactory<>("value1"));
value1Col.setCellFactory(new FormattedTableCellFactory<>(TextAlignment.RIGHT));
TableColumn<SumData, Double> value2Col = new TableColumn<>("Value 2");
value2Col.setCellValueFactory(new PropertyValueFactory<>("value2"));
value2Col.setCellFactory(new FormattedTableCellFactory<>(TextAlignment.RIGHT));
TableColumn<SumData, Double> value3Col = new TableColumn<>("Value 3");
value3Col.setCellValueFactory(new PropertyValueFactory<>("value3"));
value3Col.setCellFactory(new FormattedTableCellFactory<>(TextAlignment.RIGHT));
sumTable.getColumns().addAll(textCol, value1Col, value2Col, value3Col);
}
/**
* Find the horizontal scrollbar of the given table.
*
* #param table
* #return
*/
private ScrollBar findScrollBar(TableView<?> table, Orientation orientation)
{
// this would be the preferred solution, but it doesn't work. it always gives back the vertical scrollbar
// return (ScrollBar) table.lookup(".scroll-bar:horizontal");
//
// => we have to search all scrollbars and return the one with the proper orientation
Set<Node> set = table.lookupAll(".scroll-bar");
for (Node node : set)
{
ScrollBar bar = (ScrollBar) node;
if (bar.getOrientation() == orientation)
{
return bar;
}
}
return null;
}
/**
* Data for primary table rows.
*/
public static class Data
{
private final ObjectProperty<LocalDate> date;
private final SimpleDoubleProperty value1;
private final SimpleDoubleProperty value2;
private final SimpleDoubleProperty value3;
public Data(LocalDate date, double value1, double value2, double value3)
{
this.date = new SimpleObjectProperty<LocalDate>(date);
this.value1 = new SimpleDoubleProperty(value1);
this.value2 = new SimpleDoubleProperty(value2);
this.value3 = new SimpleDoubleProperty(value3);
}
public final ObjectProperty<LocalDate> dateProperty()
{
return this.date;
}
public final LocalDate getDate()
{
return this.dateProperty().get();
}
public final void setDate(final LocalDate date)
{
this.dateProperty().set(date);
}
public final SimpleDoubleProperty value1Property()
{
return this.value1;
}
public final double getValue1()
{
return this.value1Property().get();
}
public final void setValue1(final double value1)
{
this.value1Property().set(value1);
}
public final SimpleDoubleProperty value2Property()
{
return this.value2;
}
public final double getValue2()
{
return this.value2Property().get();
}
public final void setValue2(final double value2)
{
this.value2Property().set(value2);
}
public final SimpleDoubleProperty value3Property()
{
return this.value3;
}
public final double getValue3()
{
return this.value3Property().get();
}
public final void setValue3(final double value3)
{
this.value3Property().set(value3);
}
}
/**
* Data for summary table rows.
*/
public static class SumData
{
private final SimpleStringProperty text;
private final SimpleDoubleProperty value1;
private final SimpleDoubleProperty value2;
private final SimpleDoubleProperty value3;
public SumData(String text, double value1, double value2, double value3)
{
this.text = new SimpleStringProperty(text);
this.value1 = new SimpleDoubleProperty(value1);
this.value2 = new SimpleDoubleProperty(value2);
this.value3 = new SimpleDoubleProperty(value3);
}
public final SimpleStringProperty textProperty()
{
return this.text;
}
public final java.lang.String getText()
{
return this.textProperty().get();
}
public final void setText(final java.lang.String text)
{
this.textProperty().set(text);
}
public final SimpleDoubleProperty value1Property()
{
return this.value1;
}
public final double getValue1()
{
return this.value1Property().get();
}
public final void setValue1(final double value1)
{
this.value1Property().set(value1);
}
public final SimpleDoubleProperty value2Property()
{
return this.value2;
}
public final double getValue2()
{
return this.value2Property().get();
}
public final void setValue2(final double value2)
{
this.value2Property().set(value2);
}
public final SimpleDoubleProperty value3Property()
{
return this.value3;
}
public final double getValue3()
{
return this.value3Property().get();
}
public final void setValue3(final double value3)
{
this.value3Property().set(value3);
}
}
/**
* Formatter for table cells: allows you to align table cell values
* left/right/center
*
* Example for alignment form
* http://docs.oracle.com/javafx/2/fxml_get_started/fxml_tutorial_intermediate.htm
*
* #param <S>
* #param <T>
*/
public static class FormattedTableCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>>
{
private TextAlignment alignment = TextAlignment.LEFT;
private Format format;
public FormattedTableCellFactory()
{
}
public FormattedTableCellFactory(TextAlignment alignment)
{
this.alignment = alignment;
}
public TextAlignment getAlignment()
{
return alignment;
}
public void setAlignment(TextAlignment alignment)
{
this.alignment = alignment;
}
public Format getFormat()
{
return format;
}
public void setFormat(Format format)
{
this.format = format;
}
#Override
#SuppressWarnings("unchecked")
public TableCell<S, T> call(TableColumn<S, T> p)
{
TableCell<S, T> cell = new TableCell<S, T>()
{
#Override
public void updateItem(Object item, boolean empty)
{
if (item == getItem())
{
return;
}
super.updateItem((T) item, empty);
if (item == null)
{
super.setText(null);
super.setGraphic(null);
} else if (format != null)
{
super.setText(format.format(item));
} else if (item instanceof Node)
{
super.setText(null);
super.setGraphic((Node) item);
} else
{
super.setText(item.toString());
super.setGraphic(null);
}
}
};
cell.setTextAlignment(alignment);
switch (alignment)
{
case CENTER:
cell.setAlignment(Pos.CENTER);
break;
case RIGHT:
cell.setAlignment(Pos.CENTER_RIGHT);
break;
default:
cell.setAlignment(Pos.CENTER_LEFT);
break;
}
return cell;
}
}
}
No solution (which probably would require some real work in the bowels of VirtualFlow and/or TableViewSkin) but a dirty trick: add/remove data after wiring the scrollBars
addButton.fire();
Platform.runLater(( ) -> {
clearButton.fire();
});
The drawback is a short but perceptible flicker ...
Update
After a bit of digging ("geht-nicht-gibt's-nicht" - don't know the English idiom, sry) I found a way to force the VirtualFlow into an initial layout pass even if there are no items: the basic idea is to temporarily set the flow's cellCount > 0 even if there are no items. The tricky part is to do it at the right time in the skin's life: only once, sometime early but only after the normal layout has happened.
The implementation below
has a flag to indicate whether or not to fake the itemCount
sets the flag in a listener to the containing window's showing property: this assumes a normal setting of the default skin while being added to the scenegraph
overridden getItemCount to return at least 1 if the flag is set
overridden layoutChildren that forces a fake layout if the flag is set
the forced layout is achieved by calling updateItemCount twice: once with and once without flag
Still dirty, but more fun :)
public static class TweakedTableSkin<T> extends TableViewSkin<T> {
private boolean forceNotEmpty = false;
ChangeListener showingListener = (src, ov, nv) -> {
initForceNotEmpty(src);
};
public TweakedTableSkin(TableView<T> control) {
super(control);
Window window = getSkinnable().getScene().getWindow();
if (window != null)
window.showingProperty().addListener(showingListener);
}
/**
* Overridden to force a re-layout with faked itemCount after calling
* super if the fake flag is true.
*/
#Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
if (forceNotEmpty) {
forceNotEmptyLayout();
}
}
/**
* Callback from listener installed on window's showing property.
* Implemented to set the forceNotEmpty flag and remove the listener.
*/
private void initForceNotEmpty(ObservableValue src) {
forceNotEmpty = true;
src.removeListener(showingListener);
}
/**
* Enforces a layout pass on the flow with at least one row.
* Resets the forceNotEmpty flag and triggers a second
* layout pass with the correct count.
*/
private void forceNotEmptyLayout() {
if (!forceNotEmpty) return;
updateItemCount();
forceNotEmpty = false;
updateItemCount();
}
/**
* Overridden to return at least 1 if forceNotEmpty is true.
*/
#Override
protected int getItemCount() {
int itemCount = super.getItemCount();
if (forceNotEmpty && itemCount == 0) {
itemCount = 1;
}
return itemCount;
}
}
Usage by extending TableView with an overridden createDefaultSkin:
private TableView<Data> mainTable = new TableView<>() {
#Override
protected Skin<?> createDefaultSkin() {
return new TweakedTableSkin<>(this);
}
};
I wrote a thread that checks constantly if the mouse is over a ListView, because I want to show a Popup containing info about the cell I point with the mouse.
So no problem to check if the mouse is over the ListView.
But how do I check if the mouse is over a certain cell since I cannot use ListCell.localToScreen(ListCell.getBoundsInLocal()); to get the cell coordinates on screen?
I prefer not to use ListCell event such as onMouseEntered.
Either register handlers for mouseEntered and mouseExited events on each ListCell, or observe the ListCell's hoverProperty. Here's an example using the second method:
import java.util.stream.IntStream;
import javafx.animation.FadeTransition;
import javafx.application.Application;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.Popup;
import javafx.stage.Stage;
import javafx.util.Duration;
public class PopupOnListCellHover extends Application {
private Popup popup ;
private Node popupContent ;
private Label titleLabel ;
private Label detailsLabel ;
private FadeTransition fadeOut ;
#Override
public void start(Stage primaryStage) {
ListView<Item> listView = new ListView<>();
popup = new Popup();
titleLabel = new Label();
titleLabel.setStyle("-fx-font-size: 1.5em ; -fx-font-weight: bold;");
detailsLabel = new Label();
popupContent = new VBox(10, titleLabel, detailsLabel);
popupContent.setStyle("-fx-background-color: -fx-background; "+
"-fx-background: lightskyblue; -fx-padding:12px;");
popup.getContent().add(popupContent);
fadeOut = new FadeTransition(Duration.millis(500), popupContent);
fadeOut.setFromValue(1.0);
fadeOut.setToValue(0.0);
fadeOut.setOnFinished(e -> popup.hide());
listView.setCellFactory(lv -> {
ListCell<Item> cell = new ListCell<Item>() {
#Override
public void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
setText(item.getName());
}
}
};
cell.hoverProperty().addListener((obs, wasHovered, isNowHovered) -> {
if (isNowHovered && ! cell.isEmpty()) {
showPopup(cell);
} else {
hidePopup();
}
});
return cell ;
});
IntStream.rangeClosed(1, 100).mapToObj(i -> new Item("Item "+i, i))
.forEach(listView.getItems()::add);
BorderPane root = new BorderPane(listView);
Scene scene = new Scene(root, 250, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
private void showPopup(ListCell<Item> cell) {
fadeOut.stop();
popupContent.setOpacity(1.0);
Bounds bounds = cell.localToScreen(cell.getBoundsInLocal());
popup.show(cell, bounds.getMaxX(), bounds.getMinY());
Item item = cell.getItem() ;
titleLabel.setText(item.getName());
detailsLabel.setText(String.format("This is %s.%nIt has value %d.",
item.getName(), item.getValue()));
}
private void hidePopup() {
fadeOut.playFromStart();
}
public static class Item {
private final int value ;
private final String name ;
public Item(String name, int value) {
this.name = name ;
this.value = value ;
}
public int getValue() {
return value ;
}
public String getName() {
return name ;
}
}
public static void main(String[] args) {
launch(args);
}
}
To use handlers for mouseEntered and mouseExited, replace
cell.hoverProperty().addListener((obs, wasHovered, isNowHovered) -> {
if (isNowHovered && ! cell.isEmpty()) {
showPopup(cell);
} else {
hidePopup();
}
});
with
cell.setOnMouseEntered(e -> showPopup(cell));
cell.setOnMouseExited(e -> hidePopup());
I'd like to mark a number of rows in my TreeTableView with red borders but I've run into the problem of table cells shifting away from their respective columns.
Visually it looks like this:
http://i.imgur.com/KBK3hvM.png
style.css:
.style {
-fx-border-style: solid line-join round ;
-fx-border-color: ...;
}
For every column in the tree it seems to shift a little further from the right by what appears to be the width of the border (1px default). Not a problem with only 2 columns but the final application is supposed to hold a dozen of them.
I can set the insets of the border to be on the outside of the cells and that fixes the shifting but then you can't see the side borders anymore which also looks odd.
I'm guessing setting styles for a row is just convenience for having the engine set it for every cell.
Is there a way to stop the TreeTableCells from shifting? Maybe setting individual styles for the cells rather than styling the whole row?
Assuming .style is applied to TreeTableRows:
.style {
-fx-background-color: red, -fx-background ;
-fx-background-insets: 0, 1 ;
}
I usually find I have to dig into the source for the default stylesheet to figure these out. You may want to mess with the insets and implement some kind of logic to prevent the double border on adjacent rows with the style class applied.
Here's an SSCCE:
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableRow;
import javafx.scene.control.TreeTableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class StyledTreeTableView extends Application {
private static final int MAX_VALUE = 1000 ;
#Override
public void start(Stage primaryStage) {
TreeTableView<Item> treeTable = new TreeTableView<>();
treeTable.setRoot(createTreeItem(1));
treeTable.setRowFactory(ttv -> new TreeTableRow<Item>() {
#Override
public void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
getStyleClass().remove("highlight");
} else {
setText(item.toString());
if (item.getValue() % 10 == 3 || item.getValue() % 10 == 4) {
if (! getStyleClass().contains("highlight")) {
getStyleClass().add("highlight");
}
} else {
getStyleClass().remove("highlight");
}
}
}
});
TreeTableColumn<Item, String> nameCol = new TreeTableColumn<>("Item");
nameCol.setCellValueFactory(cellData -> cellData.getValue().getValue().nameProperty());
treeTable.getColumns().add(nameCol);
for (int colIndex = 1 ; colIndex < 10 ; colIndex++) {
TreeTableColumn<Item, Number> valueCol = new TreeTableColumn<>("Value * "+colIndex);
final int multiplier = colIndex ;
valueCol.setCellValueFactory(cellData -> cellData.getValue().getValue().valueProperty().multiply(multiplier));
treeTable.getColumns().add(valueCol);
}
BorderPane root = new BorderPane(treeTable);
Scene scene = new Scene(root, 600, 600);
scene.getStylesheets().add("styled-tree-table.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private TreeItem<Item> createTreeItem(int value) {
Item item = new Item("Item "+ value, value);
TreeItem<Item> treeItem = new TreeItem<>(item);
if (value < MAX_VALUE) {
for (int i = 0 ; i < 10; i++) {
treeItem.getChildren().add(createTreeItem(value * 10 + i));
}
}
return treeItem ;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
public final StringProperty nameProperty() {
return this.name;
}
public final java.lang.String getName() {
return this.nameProperty().get();
}
public final void setName(final java.lang.String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
with the stylesheet:
.highlight {
-fx-background-color: red, -fx-background ;
-fx-background-insets: 0, 1 ;
}