JavaFX8: The last TableColumn's header text get clipped off if there's more than 45 TableColumns - javafx

Important disclaimer: I'm using fx that's packaged with JDK 8u202
I'm creating a TableView with about 100 TableColumns but when I scroll horizontally to the last TableColumn, that last TableColumn is clipped in half. (for reference, the 99th column is the last column)
Here's a SSCCE of this:
import javafx.application.Application;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class Main extends Application {
private static final int N_COLS = 100;
private static final int N_ROWS = 1_000;
public void start(Stage stage) throws Exception {
TableView<ObservableList<String>> tableView = new TableView<>();
// add columns
for (int i = 0; i < N_COLS; i++) {
final int finalIdx = i;
TableColumn<ObservableList<String>, String> column = new TableColumn<>(String.valueOf(i));
column.setCellValueFactory(param -> new ReadOnlyObjectWrapper<>(param.getValue().get(finalIdx)));
tableView.getColumns().add(column);
// column.setMinWidth(100);
}
// create rowData
String[] rowData = new String[N_COLS];
for (int i = 0; i < N_COLS; i++)
rowData[i] = "Longggggg string";
// add data to TableView
for (int i = 0; i < N_ROWS; i++)
tableView.getItems().add(FXCollections.observableArrayList(rowData));
// Explicitly set PrefWidth for each TableColumn based on each column's header text, and every cell's width in that column
// autoResizeColumns(tableView);
Scene scene = new Scene(tableView, 800, 800);
stage.setScene(scene);
stage.show();
}
public static void autoResizeColumns( TableView<?> tableView )
{
//Set the right policy
tableView.getColumns().forEach( (column) ->
{
Text t = new Text( column.getText() );
double max = 0.0f;
max = t.getLayoutBounds().getWidth();
for ( int i = 0; i < tableView.getItems().size(); i++ )
{
//cell must not be empty
if ( column.getCellData( i ) != null )
{
t = new Text( column.getCellData( i ).toString() );
double calcwidth = t.getLayoutBounds().getWidth();
//remember new max-width
if ( calcwidth > max )
max = calcwidth;
}
}
// set the new max-widht with some extra space
column.setPrefWidth( max + 15.0d );
} );
}
public static void main(String[] args) {
launch(args);
}
}
I've tried explicitly setting the PrefWidth for each TableColumn based on the header's width, and the cell content's width and it appears that the width is indeed being updated, but the last TableColumn is still clipped in half. I did this by calling this method:
autoResizeColumns(tableView);
So far I've seen that setting each TableColumn's MinWidth to 100 fixes this bug. But I do not wish to use this method since I still prefer having each column's width being computed automatically given that some of the fields will be much shorter than these long string values I have here.
This is the method I'm referring to:
column.setMinWidth(100);

Related

Get Viewport of translated and scaled node

The ask: How do I get the viewing rectangle in the coordinates of a transformed and scaled node?
The code is attached below, it is based upon the code from this answer: JavaFX 8 Dynamic Node scaling
The details:
I have a simple pane, BigGridPane that contains a collection of squares, all 50x50.
I have it within this PanAndZoomPane construct that was lifted from the answer referenced above. I can not honestly say I fully understand the PanAndZoomPane implementation. For example, it's not clear to me why it needs a ScrollPane at all, but I have not delved in to trying without it.
The PanAndZoomPane lets me pan and zoom my BigGridPane. This works just dandy.
There are 4 Panes involved in this total construct, in this heirarchy: ScrollPane contains PanAndZoomPane which contains Group which contains BigGridPane.
ScrollPane
PanAndZoomPane
Group
BigGridPane
I have put listeners on the boundsInLocalProperty and boundsInParentProperty of all of these, and the only one of these that changes while panning and zooming, is the boundsInParentProperty of the PanAndZoomPane. (For some reason I've seen it trigger on the scroll pane, but all of the values are the same, so I don't include that here).
Along with the boundsInParentProperty changes, the translateX, translateY, and myScale properties of the PanAndZoomPane change as things move around. This is expected, of course. myScale is bound to the scaleX and scaleY properties of the PanAndZoomPane.
This is what it looks like at startup.
If I pan the grid as shown, putting 2-2 in the upper left:
We can see the properties of the PanAndZoomPane.
panAndZoom in parent: BoundingBox [minX:-99.5, minY:-99.5, minZ:0.0,
width:501.5, height:501.5, depth:0.0,
maxX:402.0, maxY:402.0, maxZ:0.0]
paz scale = 1.0 - tx: -99.0 - ty: -99.0
Scale is 1 (no zoom), and we've translated ~100x100. That is, the origin of the BigGridPane is at -100,-100. This all makes complete sense. Similarly, the bounding box shows the same thing. The origin is at -100,-100.
In this scenario, I would like to derive a rectangle that shows me what I'm seeing in the window, in the coordinates of the BigGridPane. That would mean a rectangle of
x:100 y:100 width:250 height:250
Normally, I think, this would be the viewport of the ScrollPane, but since this code isn't actually using the ScrollPane for scrolling (again, I'm not quite exactly what it's role is here), the ScrollPane viewport never changes.
I should note that there are shenanigans happening right now because of the retina display on my mac. If you look at the rectangles, showing 5x5, they're 50x50 rectangles, so we should be seeing 10x10, but because of the retina display on my iMac, everything is doubled. What we're seeing in BigGridPane coordinates is a 250x250 block of 5 squares, offset by 100x100. The fact that this is being showing in a window of 500x500 is a detail (but unlikely one we can ignore).
But to reiterate what my question is, that's what I'm trying to get: that 250x250 square at 100x100.
It's odd that it's offset by 100x100 even though the frame is twice as big (500 vs 250). If I pan to where 1-1 is the upper left, the offset is -50,-50, like it should be.
Now, let's add zooming, and pan again to 2-2.
1 click of the scroll wheel and the scale jumps to 1.5.
panAndZoom in parent: BoundingBox [minX:-149.375, minY:-150.375, minZ:0.0,
width:752.25, height:752.25, depth:0.0,
maxX:602.875, maxY:601.875, maxZ:0.0]
paz scale = 1.5 - tx: -23.375 - ty: -24.375
What I want, again, in this case, is a rectangle in BigGridPane coordinates. Roughly:
x:100 y:100 w:150 h:150
We see we're offset by 2x2 boxes (100x100) and we see 3+ boxes (150x150).
So. Back to the bounding box. MinX and minY = -150,-150. This is good. 100 x 1.5 = 150. Similarly the width and height are 750. 500 x 1.5 = 750. So, that is good.
The translates are where we go off the rails. -23.375, -24.375. I have no idea where these numbers come from. I can't seem to correlate them to anything in regards to 100, 150, 1.5 zoom, etc.
Worse, if we pan (while still at 1.5 scale) to "0,0", before, at scale=1, tx and ty were both 0. That's good.
panAndZoom in parent: BoundingBox [minX:0.625, minY:0.625, minZ:0.0,
width:752.25, height:752.25, depth:0.0,
maxX:752.875, maxY:752.875, maxZ:0.0]
paz scale = 1.5 - tx: 126.625 - ty: 126.625
Now, they're 126.625 (probably should be rounded to 125). I have no idea where those numbers come from.
I've tried all sorts of runs on the numbers to see where these numbers come from.
JavaFX knows what the numbers are! (even if the whole retina thing is kind of messing with my head, I'm going to ignore it for the moment).
And I don't see anything in the transforms of any of the panes.
So, my coordinate systems are all over the map, and I'd like to know what part of my BigGridPane is being shown in my panned and scaled view.
Code:
package pkg;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;
public class PanZoomTest extends Application {
private ScrollPane scrollPane = new ScrollPane();
private final DoubleProperty zoomProperty = new SimpleDoubleProperty(1.0d);
private final DoubleProperty deltaY = new SimpleDoubleProperty(0.0d);
private final Group group = new Group();
PanAndZoomPane panAndZoomPane = null;
BigGridPane1 bigGridPane = new BigGridPane1(10, 10, 50);
#Override
public void start(Stage primaryStage) throws Exception {
scrollPane.setPannable(true);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
group.getChildren().add(bigGridPane);
panAndZoomPane = new PanAndZoomPane();
zoomProperty.bind(panAndZoomPane.myScale);
deltaY.bind(panAndZoomPane.deltaY);
panAndZoomPane.getChildren().add(group);
SceneGestures sceneGestures = new SceneGestures(panAndZoomPane);
scrollPane.setContent(panAndZoomPane);
panAndZoomPane.toBack();
addListeners("panAndZoom", panAndZoomPane);
scrollPane.addEventFilter(MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scrollPane.addEventFilter(MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scrollPane.addEventFilter(ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
AnchorPane anchorPane = new AnchorPane();
anchorPane.getChildren().add(scrollPane);
anchorPane.setTopAnchor(scrollPane, 1.0d);
anchorPane.setRightAnchor(scrollPane, 1.0d);
anchorPane.setBottomAnchor(scrollPane, 1.0d);
anchorPane.setLeftAnchor(scrollPane, 1.0d);
BorderPane root = new BorderPane(anchorPane);
Label label = new Label("Pan and Zoom Test");
root.setTop(label);
Scene scene = new Scene(root, 250, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
private void addListeners(String label, Node node) {
node.boundsInLocalProperty().addListener((o) -> {
System.out.println(label + " in local: " + node.getBoundsInLocal());
});
node.boundsInParentProperty().addListener((o) -> {
System.out.println(label + " in parent: " + node.getBoundsInParent());
System.out.println("paz scale = " + panAndZoomPane.getScale() + " - "
+ panAndZoomPane.getTranslateX() + " - "
+ panAndZoomPane.getTranslateY());
System.out.println(group.getTransforms());
});
}
class BigGridPane extends Region {
int rows;
int cols;
int size;
Font numFont = Font.font("sans-serif", 8);
FontMetrics numMetrics = new FontMetrics(numFont);
public BigGridPane(int cols, int rows, int size) {
this.rows = rows;
this.cols = cols;
this.size = size;
int sizeX = cols * size;
int sizeY = rows * size;
setMinSize(sizeX, sizeY);
setMaxSize(sizeX, sizeY);
setPrefSize(sizeX, sizeY);
populate();
}
#Override
protected void layoutChildren() {
System.out.println("grid layout");
super.layoutChildren();
}
private void populate() {
ObservableList<Node> children = getChildren();
children.clear();
for (int i = 0; i < cols; i++) {
for (int j = 0; j < rows; j++) {
Rectangle r = new Rectangle(i * size, j * size, size, size);
r.setFill(null);
r.setStroke(Color.BLACK);
String label = i + "-" + j;
Point2D p = new Point2D(r.getBoundsInLocal().getCenterX(), r.getBoundsInLocal().getCenterY());
Text t = new Text(label);
t.setX(p.getX() - numMetrics.computeStringWidth(label) / 2);
t.setY(p.getY() + numMetrics.getLineHeight() / 2);
t.setFont(numFont);
children.add(r);
children.add(t);
}
}
}
}
class PanAndZoomPane extends Pane {
public static final double DEFAULT_DELTA = 1.5d; //1.3d
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public DoubleProperty deltaY = new SimpleDoubleProperty(0.0);
private Timeline timeline;
public PanAndZoomPane() {
this.timeline = new Timeline(30);//60
// add scale transform
scaleXProperty().bind(myScale);
scaleYProperty().bind(myScale);
}
public double getScale() {
return myScale.get();
}
public void setScale(double scale) {
myScale.set(scale);
}
public void setPivot(double x, double y, double scale) {
// note: pivot value must be untransformed, i. e. without scaling
// timeline that scales and moves the node
timeline.getKeyFrames().clear();
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.millis(200), new KeyValue(translateXProperty(), getTranslateX() - x)), //200
new KeyFrame(Duration.millis(200), new KeyValue(translateYProperty(), getTranslateY() - y)), //200
new KeyFrame(Duration.millis(200), new KeyValue(myScale, scale)) //200
);
timeline.play();
}
public double getDeltaY() {
return deltaY.get();
}
public void setDeltaY(double dY) {
deltaY.set(dY);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
public class SceneGestures {
private DragContext sceneDragContext = new DragContext();
PanAndZoomPane panAndZoomPane;
public SceneGestures(PanAndZoomPane canvas) {
this.panAndZoomPane = canvas;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
public EventHandler<ScrollEvent> getOnScrollEventHandler() {
return onScrollEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
sceneDragContext.mouseAnchorX = event.getX();
sceneDragContext.mouseAnchorY = event.getY();
sceneDragContext.translateAnchorX = panAndZoomPane.getTranslateX();
sceneDragContext.translateAnchorY = panAndZoomPane.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
panAndZoomPane.setTranslateX(sceneDragContext.translateAnchorX + event.getX() - sceneDragContext.mouseAnchorX);
panAndZoomPane.setTranslateY(sceneDragContext.translateAnchorY + event.getY() - sceneDragContext.mouseAnchorY);
event.consume();
}
};
/**
* Mouse wheel handler: zoom to pivot point
*/
private EventHandler<ScrollEvent> onScrollEventHandler = new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double delta = PanAndZoomPane.DEFAULT_DELTA;
double scale = panAndZoomPane.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
panAndZoomPane.setDeltaY(event.getDeltaY());
if (panAndZoomPane.deltaY.get() < 0) {
scale /= delta;
} else {
scale *= delta;
}
double f = (scale / oldScale) - 1;
double dx = (event.getX() - (panAndZoomPane.getBoundsInParent().getWidth() / 2 + panAndZoomPane.getBoundsInParent().getMinX()));
double dy = (event.getY() - (panAndZoomPane.getBoundsInParent().getHeight() / 2 + panAndZoomPane.getBoundsInParent().getMinY()));
panAndZoomPane.setPivot(f * dx, f * dy, scale);
event.consume();
}
};
}
class FontMetrics {
final private Text internal;
public float lineHeight;
public FontMetrics(Font fnt) {
internal = new Text();
internal.setFont(fnt);
Bounds b = internal.getLayoutBounds();
lineHeight = (float) b.getHeight();
}
public float computeStringWidth(String txt) {
internal.setText(txt);
return (float) internal.getLayoutBounds().getWidth();
}
public float getLineHeight() {
return lineHeight;
}
}
}
Generally, you can get the bounds of node1 in the coordinate system of node2 if both are in the same scene using
node2.sceneToLocal(node1.localToScene(node1.getBoundsInLocal()));
I don't understand all the code you posted; I don't really know why you are using a scroll pane when you seem to be implementing all the panning and zooming yourself. Here is a simpler version of a PanZoomPane and then a test which shows how to use the idea above to get the bounds of the viewport in the coordinate system of the panning/zooming content. The "viewport" is just the bounds of the panning/zooming pane in the coordinate system of the content.
If you need the additional functionality in your version of panning and zooming, you should be able to adapt this idea to that; but it would take me too long to understand everything you are doing there.
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Transform;
public class PanZoomPane extends Region {
private final Node content ;
private final Rectangle clip ;
private Affine transform ;
private Point2D mouseDown ;
private static final double SCALE = 1.01 ; // zoom factor per pixel scrolled
public PanZoomPane(Node content) {
this.content = content ;
getChildren().add(content);
clip = new Rectangle();
setClip(clip);
transform = Affine.affine(1, 0, 0, 1, 0, 0);
content.getTransforms().setAll(transform);
content.setOnMousePressed(event -> mouseDown = new Point2D(event.getX(), event.getY()));
content.setOnMouseDragged(event -> {
double deltaX = event.getX() - mouseDown.getX();
double deltaY = event.getY() - mouseDown.getY();
translate(deltaX, deltaY);
});
content.setOnScroll(event -> {
double pivotX = event.getX();
double pivotY = event.getY();
double scale = Math.pow(SCALE, event.getDeltaY());
scale(pivotX, pivotY, scale);
});
}
public Node getContent() {
return content ;
}
#Override
protected void layoutChildren() {
clip.setWidth(getWidth());
clip.setHeight(getHeight());
}
public void scale(double pivotX, double pivotY, double scale) {
transform.append(Transform.scale(scale, scale, pivotX, pivotY));
}
public void translate(double x, double y) {
transform.append(Transform.translate(x, y));
}
public void reset() {
transform.setToIdentity();
}
}
import javafx.application.Application;
import javafx.beans.binding.Binding;
import javafx.beans.binding.ObjectBinding;
import javafx.geometry.Bounds;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.RowConstraints;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class PanZoomTest extends Application {
private Binding<Bounds> viewport ;
#Override
public void start(Stage stage) {
Node content = createContent(50, 50, 50) ;
PanZoomPane pane = new PanZoomPane(content);
viewport = new ObjectBinding<>() {
{
bind(
pane.localToSceneTransformProperty(),
pane.boundsInLocalProperty(),
content.localToSceneTransformProperty()
);
}
#Override
protected Bounds computeValue() {
return content.sceneToLocal(pane.localToScene(pane.getBoundsInLocal()));
}
};
viewport.addListener((obs, oldViewport, newViewport) -> System.out.println(newViewport));
BorderPane root = new BorderPane(pane);
Button reset = new Button("Reset");
reset.setOnAction(event -> pane.reset());
HBox buttons = new HBox(reset);
buttons.setAlignment(Pos.CENTER);
buttons.setPadding(new Insets(10));
root.setTop(buttons);
Scene scene = new Scene(root, 800, 800);
stage.setScene(scene);
stage.show();
}
private Node createContent(int columns, int rows, double cellSize) {
GridPane grid = new GridPane() ;
ColumnConstraints cc = new ColumnConstraints();
cc.setMinWidth(cellSize);
cc.setPrefWidth(cellSize);
cc.setMaxWidth(cellSize);
cc.setFillWidth(true);
cc.setHalignment(HPos.CENTER);
for (int column = 0 ; column < columns ; column++) {
grid.getColumnConstraints().add(cc);
}
RowConstraints rc = new RowConstraints();
rc.setMinHeight(cellSize);
rc.setPrefHeight(cellSize);
rc.setMaxHeight(cellSize);
rc.setFillHeight(true);
rc.setValignment(VPos.CENTER);
for (int row = 0 ; row < rows ; row++) {
grid.getRowConstraints().add(rc);
}
for (int x = 0 ; x < columns ; x++) {
for (int y = 0 ; y < rows ; y++) {
Label label = new Label(String.format("[%d, %d]", x, y));
label.setBackground(new Background(
new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY),
new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, new Insets(1,1,0,0))
));
label.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
grid.add(label, x, y);
}
}
return grid ;
}
public static void main(String[] args) {
launch();
}
}

Erasing Antialiased Shapes from a JavaFX Canvas

I have inherited a simulation program to extend with new features. The original was written as an Applet using the AWT library for graphics. Before adding the new features I want to adapt the program to the desktop and use JavaFX instead of AWT.
The simulation paints hundreds or thousands of objects dozens of times per second, then erases them and repaints them at new locations, effectively animating them. I am using a Canvas object for that part of the UI. Erasing is done by repainting the object with the background color. What I am seeing though is that erasing objects is incomplete. A kind of "halo" gets left behind though.
The following program illustrates the problem. Clicking the "Draw" button causes it to draw a few hundred circles on the Canvas using the foreground color. After drawing, clicking the button again will erase the circles by re-drawing them in the background color. Multiple cycles of draw/erase will build up a visible background of "ghost" images.
package com.clartaq.antialiasingghosts;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.Random;
public class Main extends Application {
static final int NUM_CIRCLES = 500;
static final int CIRCLE_DIAMETER = 10;
static final double PANEL_WIDTH = 75.0;
static final double PANEL_HEIGHT = 40.0;
static final Color FG_COLOR = Color.rgb(10, 0, 200);
static final Color BG_COLOR = Color.rgb(255, 255, 255);
static final double BUTTON_WIDTH = 50.0;
GraphicsContext gc;
Random rand = new Random();
double[] px = new double[NUM_CIRCLES];
double[] py = new double[NUM_CIRCLES];
void randomizeParticlePositions() {
for (int i = 0; i < NUM_CIRCLES; i++) {
px[i] = rand.nextDouble() * PANEL_WIDTH;
py[i] = rand.nextDouble() * PANEL_HEIGHT;
}
}
void drawCircles(Color color) {
gc.setFill(color);
for (int i = 0; i < NUM_CIRCLES; i++) {
var screenX = px[i] * CIRCLE_DIAMETER;
var screenY = py[i] * CIRCLE_DIAMETER;
gc.fillOval(screenX, screenY, CIRCLE_DIAMETER, CIRCLE_DIAMETER);
}
}
#Override
public void start(Stage stage) {
String javaVersion = System.getProperty("java.version");
String javafxVersion = System.getProperty("javafx.version");
stage.setTitle("AntiAliasingGhosts -- erasing objects leaves ghosts in JavaFX");
Label versionLabel = new Label("JavaFX " + javafxVersion
+ ", running on Java " + javaVersion + ".");
double canvasWidth = (PANEL_WIDTH * CIRCLE_DIAMETER);
double canvasHeight = (PANEL_HEIGHT * CIRCLE_DIAMETER);
Canvas canvasRef = new Canvas(canvasWidth, canvasHeight);
gc = canvasRef.getGraphicsContext2D();
Button deBtn = new Button("Draw");
deBtn.setPrefWidth(BUTTON_WIDTH);
deBtn.setOnAction(e -> {
String txt = deBtn.getText();
switch (txt) {
case "Draw" -> {
randomizeParticlePositions();
drawCircles(FG_COLOR);
deBtn.setText("Erase");
}
case "Erase" -> {
drawCircles(BG_COLOR);
deBtn.setText("Draw");
}
default -> Platform.exit();
}
});
Button exBtn = new Button("Exit");
exBtn.setPrefWidth(BUTTON_WIDTH);
exBtn.setOnAction(e -> Platform.exit());
TilePane tp = new TilePane();
tp.setAlignment(Pos.CENTER);
tp.setHgap(10);
tp.getChildren().addAll(deBtn, exBtn);
VBox root = new VBox();
root.setPadding(new Insets(7));
root.setSpacing(10);
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(versionLabel, canvasRef, tp);
StackPane sp = new StackPane(root);
BackgroundFill bf = new BackgroundFill(BG_COLOR, CornerRadii.EMPTY, Insets.EMPTY);
Background bg = new Background(bf);
sp.setBackground(bg);
Scene scene = new Scene(sp, 640.0, 480.0);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
I can get good erasure by expanding the diameter of the circles by 2 pixels when erasing. Of course, that can affect nearby shapes too.
Also, using the fillRect method to erase the entire Canvas seems reasonable, but that means everything has to be re-drawn if anything has to be re-drawn. I suppose it is possible to optimize the re-draw by erasing and re-drawing a smaller section of the Canvas but I don't want to do that if it isn't necessary.
Magnifying sections of the program display shows that it is really an antialiasing effect. Constructing the Scene with the SceneAntialiasing.DISABLED parameter does not seem to have any effect.
Attempting to turn off image smoothing as suggested in this question does not help.
Is possible to erase a single shape drawn on a Canvas by re-drawing it in the background color?
I am using Java 17.0.1, JavaFX 17.0.1, and a 5K Mac display if that is relevant.
For expedience, note the difference between fillOval and strokeOval() in the GraphicsContext. You can conditionally erase the outline in drawCircles() as a function of a suitable boolean value:
if (stroke) {
gc.setStroke(BG_COLOR);
gc.strokeOval(screenX, screenY, CIRCLE_DIAMETER, CIRCLE_DIAMETER);
}
Try a few representative shapes, e.g. fillRect, to verify the desired result.
A better alternative, IMO, is to pursue the erase -> render strategy. Complete examples seen here and here may help you establish whether the approach is scalable to your use-case. See also this related examination of resampling artifact.
Expedient approach, as tested:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.Random;
public class Main extends Application {
static final int NUM_CIRCLES = 500;
static final int CIRCLE_DIAMETER = 10;
static final double PANEL_WIDTH = 75.0;
static final double PANEL_HEIGHT = 40.0;
static final Color FG_COLOR = Color.rgb(10, 0, 200);
static final Color BG_COLOR = Color.rgb(255, 255, 255);
static final double BUTTON_WIDTH = 50.0;
GraphicsContext gc;
Random rand = new Random();
private boolean stroke;
double[] px = new double[NUM_CIRCLES];
double[] py = new double[NUM_CIRCLES];
void randomizeParticlePositions() {
for (int i = 0; i < NUM_CIRCLES; i++) {
px[i] = rand.nextDouble() * PANEL_WIDTH;
py[i] = rand.nextDouble() * PANEL_HEIGHT;
}
}
void drawCircles(Color color) {
gc.setFill(color);
for (int i = 0; i < NUM_CIRCLES; i++) {
var screenX = px[i] * CIRCLE_DIAMETER;
var screenY = py[i] * CIRCLE_DIAMETER;
gc.fillOval(screenX, screenY, CIRCLE_DIAMETER, CIRCLE_DIAMETER);
if (stroke) {
gc.setStroke(BG_COLOR);
gc.strokeOval(screenX, screenY, CIRCLE_DIAMETER, CIRCLE_DIAMETER);
}
}
}
#Override
public void start(Stage stage) {
String javaVersion = System.getProperty("java.version");
String javafxVersion = System.getProperty("javafx.version");
stage.setTitle("AntiAliasingGhosts -- erasing objects leaves ghosts in JavaFX");
Label versionLabel = new Label("JavaFX " + javafxVersion
+ ", running on Java " + javaVersion + ".");
double canvasWidth = (PANEL_WIDTH * CIRCLE_DIAMETER);
double canvasHeight = (PANEL_HEIGHT * CIRCLE_DIAMETER);
Canvas canvasRef = new Canvas(canvasWidth, canvasHeight);
gc = canvasRef.getGraphicsContext2D();
Button deBtn = new Button("Draw");
deBtn.setPrefWidth(BUTTON_WIDTH);
deBtn.setOnAction(e -> {
String txt = deBtn.getText();
switch (txt) {
case "Draw" -> {
randomizeParticlePositions();
drawCircles(FG_COLOR);
deBtn.setText("Erase");
stroke = true;
}
case "Erase" -> {
drawCircles(BG_COLOR);
deBtn.setText("Draw");
stroke = false;
}
default ->
Platform.exit();
}
});
Button exBtn = new Button("Exit");
exBtn.setPrefWidth(BUTTON_WIDTH);
exBtn.setOnAction(e -> Platform.exit());
TilePane tp = new TilePane();
tp.setAlignment(Pos.CENTER);
tp.setHgap(10);
tp.getChildren().addAll(deBtn, exBtn);
VBox root = new VBox();
root.setPadding(new Insets(7));
root.setSpacing(10);
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(versionLabel, canvasRef, tp);
StackPane sp = new StackPane(root);
BackgroundFill bf = new BackgroundFill(BG_COLOR, CornerRadii.EMPTY, Insets.EMPTY);
Background bg = new Background(bf);
sp.setBackground(bg);
Scene scene = new Scene(sp, 640.0, 480.0);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}

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

JavaFX: Fit Columns of TableView to content to avoid cut the content

I have following problem: When i create a TableView and put data in the columns, the columns fit to content automatically. But if there many rows (more than 30) JavaFX optimize the columnwidth to the average length of all content.
In the first example i put the long strings first in the table and everything is fine.
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Example extends Application {
#Override
public void start(Stage stage) {
// Init data
ObservableList<Person> persons = FXCollections.observableArrayList();
persons.add(new Person("Maximus-Superman", "Power", "Maximus-Superman.Power#test.com"));
persons.add(new Person("Max", "Powerstrongsupercool", "Max.Powerstrongsupercool#test.com"));
persons.add(new Person("Maximus-Superman", "Powerstrongsupercool", "Maximus-Superman.Powerstrongsupercool#test.com"));
for (int i = 0; i < 100; i++) {
persons.add(new Person("Max", "Power", "Max.Power#test.com"));
}
// Init table
TableView<Person> table = new TableView<Person>();
table.setMaxWidth(Double.MAX_VALUE);
table.setMaxHeight(Double.MAX_VALUE);
table.setItems(persons);
// Init columns
TableColumn<Person, String> firstname = new TableColumn<Person, String>("Firstname");
firstname.setCellValueFactory(new PropertyValueFactory<Person, String>("firstname"));
table.getColumns().add(firstname);
TableColumn<Person, String> lastname = new TableColumn<Person, String>("Lastname");
lastname.setCellValueFactory(new PropertyValueFactory<Person, String>("lastname"));
table.getColumns().add(lastname);
TableColumn<Person, String> email = new TableColumn<Person, String>("E-Mail");
email.setCellValueFactory(new PropertyValueFactory<Person, String>("email"));
table.getColumns().add(email);
// Init Stage
Scene scene = new Scene(table, 400, 150);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
public class Person {
private String firstname;
private String lastname;
private String email;
public Person(String firstname, String lastname, String email) {
this.firstname = firstname;
this.lastname = lastname;
this.email = email;
}
// Getters and Setters
}
}
Looks good...
In the second example i put short strings first in the table and at least the long strings. In this example JavaFX choose columnwidth to small and the content gets cut.
public class Example extends Application {
#Override
public void start(Stage stage) {
// Init data
ObservableList<Person> persons = FXCollections.observableArrayList();
for (int i = 0; i < 100; i++) {
persons.add(new Person("Max", "Power", "Max.Power#test.com"));
}
persons.add(new Person("Maximus-Superman", "Power", "Maximus-Superman.Power#test.com"));
persons.add(new Person("Max", "Powerstrongsupercool", "Max.Powerstrongsupercool#test.com"));
persons.add(new Person("Maximus-Superman", "Powerstrongsupercool", "Maximus-Superman.Powerstrongsupercool#test.com"));
[...]
Looks bad...
How can i avoid this?​
EDIT 19.05.2018
The links in the comments didn`t work.
So, i have found the problem in the source of JavaFx:
In the updateScene() method the maxRows are set to 30. There is no way to change the value because every methods that are involved are protected or private.
The comment is right. It can be take much time to create a table if there many rows in the table. But sometimes a developer knows the much possible rows of a table or it´s okay to risk higher loadingtime.
One solution is to contact Oracle to create a setter() for the value of max. rows. So the developer can choose the max. rows for each column individually.
public class TableColumnHeader extends Region {
[...]
private void updateScene() {
// RT-17684: If the TableColumn widths are all currently the default,
// we attempt to 'auto-size' based on the preferred width of the first
// n rows (we can't do all rows, as that could conceivably be an unlimited
// number of rows retrieved from a very slow (e.g. remote) data source.
// Obviously, the bigger the value of n, the more likely the default
// width will be suitable for most values in the column
final int n = 30; // ------------------------------------------> This is the problem!
if (! autoSizeComplete) {
if (getTableColumn() == null || getTableColumn().getWidth() != DEFAULT_COLUMN_WIDTH || getScene() == null) {
return;
}
doColumnAutoSize(getTableColumn(), n);
autoSizeComplete = true;
}
}
[...]
private void doColumnAutoSize(TableColumnBase<?,?> column, int cellsToMeasure) {
double prefWidth = column.getPrefWidth();
// if the prefWidth has been set, we do _not_ autosize columns
if (prefWidth == DEFAULT_COLUMN_WIDTH) {
getTableViewSkin().resizeColumnToFitContent(column, cellsToMeasure);
}
}
[...]
}
public class TableViewSkin<T> extends TableViewSkinBase<T, T, TableView<T>, TableViewBehavior<T>, TableRow<T>, TableColumn<T, ?>> {
[...]
#Override protected void resizeColumnToFitContent(TableColumn<T, ?> tc, int maxRows) {
if (!tc.isResizable()) return;
// final TableColumn<T, ?> col = tc;
List<?> items = itemsProperty().get();
if (items == null || items.isEmpty()) return;
Callback/*<TableColumn<T, ?>, TableCell<T,?>>*/ cellFactory = tc.getCellFactory();
if (cellFactory == null) return;
TableCell<T,?> cell = (TableCell<T, ?>) cellFactory.call(tc);
if (cell == null) return;
// set this property to tell the TableCell we want to know its actual
// preferred width, not the width of the associated TableColumnBase
cell.getProperties().put(TableCellSkin.DEFER_TO_PARENT_PREF_WIDTH, Boolean.TRUE);
// determine cell padding
double padding = 10;
Node n = cell.getSkin() == null ? null : cell.getSkin().getNode();
if (n instanceof Region) {
Region r = (Region) n;
padding = r.snappedLeftInset() + r.snappedRightInset();
}
int rows = maxRows == -1 ? items.size() : Math.min(items.size(), maxRows); // ------------------> if maxRows equals -1 every item will be checked
double maxWidth = 0;
for (int row = 0; row < rows; row++) {
cell.updateTableColumn(tc);
cell.updateTableView(tableView);
cell.updateIndex(row);
if ((cell.getText() != null && !cell.getText().isEmpty()) || cell.getGraphic() != null) {
getChildren().add(cell);
cell.applyCss();
maxWidth = Math.max(maxWidth, cell.prefWidth(-1));
getChildren().remove(cell);
}
}
// dispose of the cell to prevent it retaining listeners (see RT-31015)
cell.updateIndex(-1);
// RT-36855 - take into account the column header text / graphic widths.
// Magic 10 is to allow for sort arrow to appear without text truncation.
TableColumnHeader header = getTableHeaderRow().getColumnHeaderFor(tc);
double headerTextWidth = Utils.computeTextWidth(header.label.getFont(), tc.getText(), -1);
Node graphic = header.label.getGraphic();
double headerGraphicWidth = graphic == null ? 0 : graphic.prefWidth(-1) + header.label.getGraphicTextGap();
double headerWidth = headerTextWidth + headerGraphicWidth + 10 + header.snappedLeftInset() + header.snappedRightInset();
maxWidth = Math.max(maxWidth, headerWidth);
// RT-23486
maxWidth += padding;
if(tableView.getColumnResizePolicy() == TableView.CONSTRAINED_RESIZE_POLICY) {
maxWidth = Math.max(maxWidth, tc.getWidth());
}
tc.impl_setWidth(maxWidth);
}
[...]
}
Another solution is to fire a MouseEvent on the header of the rect of the TableColumn.
If there a MouseEvent with a ClickCount equals 2 and the PrimaryButton is down the resizeColumnToFitContent() method is called with a value for maxRows of -1.
int rows = maxRows == -1 ? items.size() : Math.min(items.size(), maxRows);
-1 means all rows that are in the TableView.
public class NestedTableColumnHeader extends TableColumnHeader {
[...]
private static final EventHandler<MouseEvent> rectMousePressed = new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent me) {
Rectangle rect = (Rectangle) me.getSource();
TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
if (! header.isColumnResizingEnabled()) return;
if (me.getClickCount() == 2 && me.isPrimaryButtonDown()) {
// the user wants to resize the column such that its
// width is equal to the widest element in the column
header.getTableViewSkin().resizeColumnToFitContent(column, -1); // -----------------------> this method should be call and everything is fine
} else {
// rather than refer to the rect variable, we just grab
// it from the source to prevent a small memory leak.
Rectangle innerRect = (Rectangle) me.getSource();
double startX = header.getTableHeaderRow().sceneToLocal(innerRect.localToScene(innerRect.getBoundsInLocal())).getMinX() + 2;
header.dragAnchorX = me.getSceneX();
header.columnResizingStarted(startX);
}
me.consume();
}
};
[...]
}
So, is it possible to create a new MouseEvent with a ClickCount of 2 and the PrimaryButtonDown-boolean is true and fire this to the TableColumn?
And: How can i contact Oracle to please them to create a setter() for the maxRows in the next release?

Resources