I apologize for the topic headline, it might not exactly express my thought but i'll give it a try. If someone knows what's the better headline, please suggest an edit.
So i'd like to create rectangles and give values for them after the button has been pressed. Everything's plain and simple if i know how many rectangles i want to create. Here's where thing gets complicated - i get the rectangle count after i've pressed the button.
I'll explain with an example, so it's a bit more clear:
final ArrayList rectList = new ArrayList();
btn.setOnAction(new EventHandler<ActionEvent>() {
public void handle(final ActionEvent event) {
ArrayList getFromMethodAnArrayList = methodWhichReturnsAnArrayList();
for (int i = 0; i<getFromMethodAnArrayList.size();i++){
rectList.add(new Rectangle(0,0,0,30));
}
}
});
HBox box1 = new HBox(1);
for (int i = 0; i<rectList.size();i++){
box1.getChildren().add(rectList.get(i));
}
This code gives an error because when first loaded the rectList is empty. How could i queue adding elements into HBox, so it would be performed after the rectList has been valued.
Recommendation
You don't need a queue here and you don't need to multi-thread either, at least as you have currently described your question - additional requirements on the implementation could imply that the use of both of those things are necessary.
Sample code
What the sample code does is define a source of items which are model data for something you want to display. When you click on the create button, it will generate a random number of new items with random data values for each item. These items will be placed in a queue and a subsequent routine will take the items from the queue, read their data values and create appropriate visual representations (rectangles) for the item data. It uses a queue data structure, but a simple array or list would have worked just fine.
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.Random;
// java 8 code
public class RectangleAddition extends Application {
private final Random random = new Random(42);
public static void main(String[] args) {
launch(args);
}
public void start(Stage stage) {
FlowPane flow = createItemContainer();
ScrollPane scroll = makeContainerScrollable(flow);
ItemSource itemSource = new ItemSource();
Button create = createItemControl(flow, scroll, itemSource);
VBox layout = createLayout(create, scroll);
Scene scene = new Scene(layout);
stage.setScene(scene);
stage.show();
}
private FlowPane createItemContainer() {
FlowPane flow = new FlowPane();
flow.setHgap(5);
flow.setVgap(5);
return flow;
}
/**
* The control will
* retrieve items from the source,
* add them to the scrollable pane,
* scroll the pane to the bottom on each addition.
*/
private Button createItemControl(Pane flow, ScrollPane scroll, ItemSource itemSource) {
Button create = new Button("Create Rectangles (keep pressing to create more)");
create.setOnAction(event -> {
addRectangles(flow, itemSource);
scroll.setVvalue(scroll.getVmax());
});
return create;
}
private VBox createLayout(Button create, ScrollPane scroll) {
VBox layout = new VBox(10, create, scroll);
layout.setStyle("-fx-padding: 10px;");
layout.setPrefSize(300, 300);
VBox.setVgrow(scroll, Priority.ALWAYS);
create.setMinHeight(Button.USE_PREF_SIZE);
return layout;
}
/**
* fetches some items from the source,
* creates rectangle nodes for them
* adds them to the container.
*/
private void addRectangles(Pane container, ItemSource itemSource) {
Queue<Item> items = itemSource.fetchNextItems();
while (!items.isEmpty()) {
Item item = items.remove();
Node rectangle = createRectangle(item);
container.getChildren().add(rectangle);
}
}
private Rectangle createRectangle(Item item) {
Rectangle rectangle = new Rectangle(item.size, item.size, item.color);
rectangle.setRotate(item.rotation);
return rectangle;
}
private ScrollPane makeContainerScrollable(FlowPane flow) {
ScrollPane scroll = new ScrollPane(flow);
scroll.setFitToWidth(true);
scroll.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
return scroll;
}
/** some model data for application items */
class Item {
// item will be colored according to rgb values from the (inclusive) range
// MIN_COLOR_VALUE to MIN_COLOR_VALUE + COLOR_RANGE - 1
private static final int MIN_COLOR_VALUE = 50;
private static final int COLOR_RANGE = 201;
// item will be sized from the (inclusive) range
// MIN_SIZE to MIN_SIZE + SIZE_RANGE - 1
private static final int MIN_SIZE = 5;
private static final int SIZE_RANGE = 21;
// item will be (z-axis) rotated from the (inclusive) range
// - ROTATE_SCOPE to + ROTATE_SCOPE
private static final int ROTATE_SCOPE = 10;
private Color color;
private int size;
private int rotation;
public Item() {
color = Color.rgb(
createColorComponent(),
createColorComponent(),
createColorComponent()
);
size = random.nextInt(SIZE_RANGE) + MIN_SIZE;
rotation = random.nextInt(ROTATE_SCOPE * 2 + 1) - ROTATE_SCOPE;
}
private int createColorComponent() {
return random.nextInt(COLOR_RANGE) + MIN_COLOR_VALUE;
}
}
/** a never-ending source of new items fetched in batches */
class ItemSource {
// will fetch between 1 and MAX_NUM_ITEMS_PER_FETCH (inclusive) items on each fetch call.
private static final int MAX_NUM_ITEMS_PER_FETCH = 5;
public Queue<Item> fetchNextItems() {
int numItems = random.nextInt(MAX_NUM_ITEMS_PER_FETCH) + 1;
Queue<Item> queue = new ArrayDeque<>(numItems);
for (int i = 0; i < numItems; i++) {
queue.add(new Item());
}
return queue;
}
}
}
Thoughts On Multithreading
Where you might want a different implementation which does actually use multi-threading is if the item creation or fetching from the item source takes a long time. For example you need to read the item data from a network, database or very large file. If you don't multi-thread such things, then you will end up freezing the UI while it waits for the I/O to complete. A general rule is if the operation you are performing will finish in less than a sixtieth of a millisecond, then you can do it on the JavaFX UI thread without any issue as there will be no visible lag and stuttering in the UI, but if it takes longer than that then you should use concurrency utilities (which are more tricky to use than single-threaded code).
Java has numerous threading mechanisms, which can be used, but you in many cases, using the JavaFX specific concurrency extensions is the best way to integrate multi-threaded code into your JavaFX application.
The appropriate concurrency utility to use usually would be the JavaFX Task or Service interfaces if you are doing this on demand from the UI. You can read the documentation for these facilities which demonstrates sample code for doing things like "a task which returns partial results" (which is a bit similar to your question).
If the thing which provides the items to be consumed is some background long running network task to which items are pushed, rather than pulled on demand, then running it in it's own thread and calling back into the JavaFX to signal a UI update via platform.runLater() is the way to go. Another data structure which can aid in such cases is a BlockingQueue as demonstrated in this multi-chart creation code - but that is quite a sophisticated solution.
I guess part of my point is that you may not need to use these concurrency utilities for your situation, you need to evaluate it on a case by case basis and use the most appropriate solution.
I think you can simplify your code quite a bit here by getting rid of the ArrayList and populating box1 when the button event happens:
final HBox box1 = new HBox(1);
btn.setOnAction(new EventHandler<ActionEvent>() {
public void handle(final ActionEvent event) {
ArrayList getFromMethodAnArrayList = methodWhichReturnsAnArrayList();
for (int i = 0; i<getFromMethodAnArrayList.size();i++){
box1.getChildren().add(new Rectangle(0,0,0,30));
}
}
});
If it is concurrency that you are interested in, it would be good to read Concurrency in JavaFX, although I don't think that is the right solution for the question you posted.
Related
I have made programmatic changes to a Java FX Line Chart and I need a programmatic way to force a re-layout of the JavaFX Chart to occur. This question has been asked/answered before but not in my context.
I have tried the typical methods that have been presented as answers to this question (see complete, minimal example code below with in-line attempts at solving the problem). None of the typical solutions to this problem work.
Specifically (sp is a StackPane):
sp.requestLayout(); // does not work
and
sp.applyCss();
sp.layout(); // does not work
placing the above code in a .runLater() does not work.
I know that my changes are present in the chart because
(1) When I resize the chart by hand my changes suddenly appear
(2) When I use the "resize" method programmatically my changes appear BUT there is a different error (plus only parent nodes are supposed to use the "resize" method - not us programmers).
Below is a minimal complete set of code which reproduces the problem. When you run the code I programmatically change one of the data points to be larger when the chart is displayed. This resize works correctly. When you right-click on the chart a context menu appears with one choice ("Resize ALL the points"). When you select that single option my code resizes all the points - BUT - none of the data points are resized visually. If I resize the chart manually by dragging a side, the chart does a re-layout and all the data node sizes immediately visually change to the correct size (The size I programmatically set them to).
How can I force the re-layout to occur programmatically that I can force to occur manually? I would NOT like to do a hack (like programmatically set the stage size to be 1 pixel smaller and then set it one pixel larger).
Note: I have read that attempts to do a requestLayout() while a layout is in progress will be ignored so perhaps something like that is going on. I think a requestLayout() inside of a runLater() would fix the issue of an ongoing Layout() but that has not worked either.
Update: Scaling was suggested as an alternative to changing the StackPane size. This solution may be helpful to some but not to me. The Look and Feel of scaling a symbol is different than the look and feel of changing the regions size and allowing the "symbol" to grow into that size.
As a complete aside this is my first stackoverflow post. So thanks for all the previous examples a I have used from this forum in the past & thanks in advance for the answer to this problem.
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.LineChart.SortingPolicy;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class dummy extends Application {
#Override
public void start(Stage primaryStage) {
Random random = new Random();
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
xAxis.setLabel("X");
yAxis.setLabel("Y");
final LineChart<Number,Number> lineChart = new LineChart<Number,Number>(xAxis,yAxis);
Series<Number,Number> series = new Series<Number,Number>();
series.setName("Dummy Data");
// Generate data
double x = 0.0;
double y = 0.0;
for (int i = 0; i < 10; i++) {
Data<Number,Number> data = new Data(x += random.nextDouble(), y+=random.nextDouble());
series.getData().add(data);
}
lineChart.getData().add(series);
lineChart.setTitle("Random Data");
lineChart.setAxisSortingPolicy(SortingPolicy.NONE);
Scene scene = new Scene(lineChart,1200,600);
Stage stage = new Stage();
stage.setScene(scene);
stage.show();
// This resizes the first data point directly (this resize is displayed correctly when program is run)
Node node = series.getData().get(0).getNode();
setSize((StackPane)node,20);
// The context menu is invoked by a right click on the line Chart. It will resize the data point based on a context menu pick
// this resize does not work....unless I resize the window manually which causes a refresh/re-layout of the chart).
lineChart.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
if (MouseButton.SECONDARY.equals(mouseEvent.getButton())) {
Scene scene = ((Node)mouseEvent.getSource()).getScene();
ContextMenu menu = createMenu(lineChart);
menu.show(scene.getWindow(), mouseEvent.getScreenX(), mouseEvent.getScreenY());
}
}
});
}
private void setSize(StackPane sp, int size) {
sp.setMinSize(size, size);
sp.setMaxSize(size, size);
sp.setPrefSize(size, size);
}
// this creates a context menu that will allow you to resize all the data point nodes
private ContextMenu createMenu(LineChart<Number,Number> lineChart) {
final ContextMenu contextMenu = new ContextMenu();
final MenuItem resize = new MenuItem("Resize ALL the points");
resize.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent event) {
for (Series<Number, Number> series : lineChart.getData()) {
for (Data<Number, Number> data : series.getData()) {
StackPane sp = (StackPane)data.getNode();
setSize (sp, 20);
// The above resizes do not take effect unless/until I manually resize the chart.
// the following two calls do not do anything;
sp.applyCss();
sp.layout();
// The request to layout the node does nothing
sp.requestLayout();
// Doing both of the above as runLaters does nothing
Platform.runLater(()->{sp.applyCss();sp.layout();});
Platform.runLater(()->{sp.requestLayout();});
// Going after the parent does nothing either
Group group = (Group)sp.getParent();
group.applyCss();
group.layout();
group.requestLayout();
// Going after the parent in a run later does nothing
Platform.runLater(()->{
group.applyCss();
group.layout();
group.requestLayout();
});
// note... doing a resize [commented out below] will work-ish.
// The documentation says NOT to use it thought that as it is for internal use only.
// By work-ish, the data points are enlarged BUT they are no longer centered on the data point
// When I resize the stage they get centered again - so this "solves" my original problem but causes a different problem
////////////////////////////////////
// sp.resize(20, 20); // Uncomment this line to see how it mostly works but introduces a new issue
////////////////////////////////////
}
}
}
});
contextMenu.getItems().add(resize);
return contextMenu;
}
public static void main(String[] args) {
Application.launch(args);
}
}
You can force a relayout by using e.g. an inner class
class LineChartX<X, Y> extends LineChart<X, Y>
{
public LineChartX(#NamedArg("xAxis") Axis<X> xAxis, #NamedArg("yAxis") Axis<Y> yAxis)
{
super(xAxis, yAxis);
}
#Override
public void layoutPlotChildren()
{
super.layoutPlotChildren();
}
}
and calling
lineChart.layoutPlotChildren();
in your menu action.
Simple one-line Solution:
nodes in LineChart scene graph have these parent-child relationships:
Pane chartContent - Group plotArea - Group plotContent - Path seriesLine;
layout requests for Group plotArea, defined in class XYChart, are suppressed:
private final Group plotArea = new Group(){
#Override public void requestLayout() {} // suppress layout requests
};
but Pane chartContent accepts layout requests:
Node node = series.getNode();
if (node instanceof Path) {
Path seriesLine = (Path) node;
Parent parent = seriesLine.getParent();
if (parent instanceof Group) {
Group plotContent = (Group) parent;
parent = plotContent.getParent();
if (parent instanceof Group) {
Group plotArea = (Group) parent;
parent = plotArea.getParent();
if (parent instanceof Pane) {
Pane chartContent = (Pane) parent;
chartContent.requestLayout();
}
}
}
}
so relayout of your chart can be forced by addding this single line
series.getNode().getParent().getParent().getParent().requestLayout();
to the end of your menu action handler.
You don't need to cast that node into a StackPane and set sizes. You need to use the setScaleX() and setScaleY() Methods
Node node = series.getData().get(0).getNode();
node.setScaleX(20);
node.setScaleY(20);
#Override
public void handle(ActionEvent event) {
for (Series<Number, Number> series : lineChart.getData()) {
for (Data<Number, Number> data : series.getData()) {
Node node = data.getNode();
node.setScaleY(20);
node.setScaleX(20);
}
}
}
#c0der posted a solution in comment form to my original post which worked but produced a runtime warning (in Eclipse). His solution was to add a dummy style sheet at the lineChart level with lineChart.getStylesheets().add(""); after the for loops ended. This code produced the warning "Apr 28, 2020 9:01:12 AM com.sun.javafx.css.StyleManager loadStylesheetUnPrivileged WARNING: Resource "" not found."
What did work without causing a run-time warning was to load an empty .css file and add it as a StyleSheet:
lineChart.getStylesheets().add(CSS.class.getResource("Empty.css").toExternalForm());
// note: I keep my .css resource files at the same location as my CSS class
// which is why I have the code "CSS.class" above
This one line solution worked once but I doubted it would work multiple times. I tested it to by increasing the size of the StackPane by 5 for each successive time "Resize ALL the points" was selected (in my dummy code above). Sure enough, it only worked the first time.
However, I added the no-op code lineChart.getStylesheets().replaceAll((s)->s+" "); before that one line solution and now it works multiple times in a row.
No matter how many times I executed the two lines of code
lineChart.getStylesheets().replaceAll((s)->s+" ");
lineChart.getStylesheets().add(CSS.class.getResource("Empty.css").toExternalForm()); `
it (1) worked and (2) The size of the list of lineChart StyleSheets did not grow beyond a size of 1. So a solution with a mystery.
Note: if you have an existing style sheet (I did not in my dummy example above) lineChart.getStylesheets().replaceAll((s)->s+" "); by itself may work as well. For some reason lineChart.getStylesheets().replaceAll((s)->s); without adding the " " on the end did not work.
Note: I originally thought I would have to code up a toggle solution to add Empty.css and the remove Empty.css with alternate calls but that proved unnecessary.
Bottom Line: if you have an existing StyleSheet lineChart.getStylesheets().replaceAll((s)->s+" "); works. If you do not have an existing StyleSheet adding an empty .css file as a StyleSheet in conjunction with the above replaceAll works.
Thanks again to #c0der for his original approach.
I hope everyone is doing well.
I'm trying to move the drop down arrow in a TitledPane to be laid out on the right, instead of the left like it is by default. I'm using JavaFX 8, and many of the resources I've found don't seem to work.
I have found that I am able to move the arrow a specific amount, like 20 pixels shown below
.accordion .title > .arrow-button .arrow
{
-fx-translate-x: 20;
}
But I want something responsive. Is there some way that I can get the width of the titled pane, and then subtract some pixels so that so that the arrow appears to be laid out on the right when resizing? Is there a better way to it? I added the element using SceneBuilder2 if that matters.
Thanks so much for your time.
Edit: The following was added for clarification
Primarily, I want the arrow to be right justified, like below
Instead of just "to the right" of the arrow. I really appreciate all the assistance.
Unfortunately, there's no public API for moving the arrow to the right side of the TitledPane. This doesn't mean this can't be accomplished, however, we just have to translate the arrow dynamically, using bindings. In order for the rest of the title area to look correct we'll also have to translate the text, and graphic if present, to the left. The easiest way to do all this is by subclassing TitledPaneSkin and accessing the internals of the "title region".
Here's an example implementation. It lets you position the arrow on the left or right side via CSS. It's also responsive to resizing as well as alignment and graphic changes.
package com.example;
import static javafx.css.StyleConverter.getEnumConverter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.scene.control.TitledPane;
import javafx.scene.control.skin.TitledPaneSkin;
import javafx.scene.layout.Region;
import javafx.scene.text.Text;
public class CustomTitledPaneSkin extends TitledPaneSkin {
public enum ArrowSide {
LEFT, RIGHT
}
/* ********************************************************
* *
* Properties *
* *
**********************************************************/
private final StyleableObjectProperty<ArrowSide> arrowSide
= new SimpleStyleableObjectProperty<>(StyleableProperties.ARROW_SIDE, this, "arrowSide", ArrowSide.LEFT) {
#Override protected void invalidated() {
adjustTitleLayout();
}
};
public final void setArrowSide(ArrowSide arrowSide) { this.arrowSide.set(arrowSide); }
public final ArrowSide getArrowSide() { return arrowSide.get(); }
public final ObjectProperty<ArrowSide> arrowSideProperty() { return arrowSide; }
/* ********************************************************
* *
* Instance Fields *
* *
**********************************************************/
private final Region title;
private final Region arrow;
private final Text text;
private DoubleBinding arrowTranslateBinding;
private DoubleBinding textGraphicTranslateBinding;
private Node graphic;
/* ********************************************************
* *
* Constructors *
* *
**********************************************************/
public CustomTitledPaneSkin(TitledPane control) {
super(control);
title = (Region) Objects.requireNonNull(control.lookup(".title"));
arrow = (Region) Objects.requireNonNull(title.lookup(".arrow-button"));
text = (Text) Objects.requireNonNull(title.lookup(".text"));
registerChangeListener(control.graphicProperty(), ov -> adjustTitleLayout());
}
/* ********************************************************
* *
* Skin Stuff *
* *
**********************************************************/
private void adjustTitleLayout() {
clearBindings();
if (getArrowSide() != ArrowSide.RIGHT) {
// if arrow is on the left we don't need to translate anything
return;
}
arrowTranslateBinding = Bindings.createDoubleBinding(() -> {
double rightInset = title.getPadding().getRight();
return title.getWidth() - arrow.getLayoutX() - arrow.getWidth() - rightInset;
}, title.paddingProperty(), title.widthProperty(), arrow.widthProperty(), arrow.layoutXProperty());
arrow.translateXProperty().bind(arrowTranslateBinding);
textGraphicTranslateBinding = Bindings.createDoubleBinding(() -> {
switch (getSkinnable().getAlignment()) {
case TOP_CENTER:
case CENTER:
case BOTTOM_CENTER:
case BASELINE_CENTER:
return 0.0;
default:
return -(arrow.getWidth());
}
}, getSkinnable().alignmentProperty(), arrow.widthProperty());
text.translateXProperty().bind(textGraphicTranslateBinding);
graphic = getSkinnable().getGraphic();
if (graphic != null) {
graphic.translateXProperty().bind(textGraphicTranslateBinding);
}
}
private void clearBindings() {
if (arrowTranslateBinding != null) {
arrow.translateXProperty().unbind();
arrow.setTranslateX(0);
arrowTranslateBinding.dispose();
arrowTranslateBinding = null;
}
if (textGraphicTranslateBinding != null) {
text.translateXProperty().unbind();
text.setTranslateX(0);
if (graphic != null) {
graphic.translateXProperty().unbind();
graphic.setTranslateX(0);
graphic = null;
}
textGraphicTranslateBinding.dispose();
textGraphicTranslateBinding = null;
}
}
#Override
public void dispose() {
clearBindings();
unregisterChangeListeners(getSkinnable().graphicProperty());
super.dispose();
}
/* ********************************************************
* *
* Stylesheet Handling *
* *
**********************************************************/
public static List<CssMetaData<?, ?>> getClassCssMetaData() {
return StyleableProperties.CSS_META_DATA;
}
#Override
public List<CssMetaData<?, ?>> getCssMetaData() {
return getClassCssMetaData();
}
private static class StyleableProperties {
private static final CssMetaData<TitledPane, ArrowSide> ARROW_SIDE
= new CssMetaData<>("-fx-arrow-side", getEnumConverter(ArrowSide.class), ArrowSide.LEFT) {
#Override
public boolean isSettable(TitledPane styleable) {
Property<?> prop = (Property<?>) getStyleableProperty(styleable);
return prop != null && !prop.isBound();
}
#Override
public StyleableProperty<ArrowSide> getStyleableProperty(TitledPane styleable) {
Skin<?> skin = styleable.getSkin();
if (skin instanceof CustomTitledPaneSkin) {
return ((CustomTitledPaneSkin) skin).arrowSide;
}
return null;
}
};
private static final List<CssMetaData<?, ?>> CSS_META_DATA;
static {
List<CssMetaData<?,?>> list = new ArrayList<>(TitledPane.getClassCssMetaData().size() + 1);
list.addAll(TitledPaneSkin.getClassCssMetaData());
list.add(ARROW_SIDE);
CSS_META_DATA = Collections.unmodifiableList(list);
}
}
}
You can then apply this skin to all TitledPanes in your application from CSS, like so:
.titled-pane {
-fx-skin: "com.example.CustomTitledPaneSkin";
-fx-arrow-side: right;
}
/*
* The arrow button has some right padding that's added
* by "modena.css". This simply puts the padding on the
* left since the arrow is positioned on the right.
*/
.titled-pane > .title > .arrow-button {
-fx-padding: 0.0em 0.0em 0.0em 0.583em;
}
Or you could target only certain TitledPanes by adding a style class and using said class instead of .titled-pane.
The above works with JavaFX 11 and likely JavaFX 10 and 9 as well. To get it to compile on JavaFX 8 you need to change some things:
Import com.sun.javafx.scene.control.skin.TitledPaneSkin instead.
The skin classes were made public in JavaFX 9.
Remove the calls to registerChangeListener(...) and unregisterChangeListeners(...). I believe replacing them with the following is correct:
#Override
protected void handleControlPropertyChange(String p) {
super.handleControlPropertyChange(p);
if ("GRAPHIC".equals(p)) {
adjustTitleLayout();
}
}
Use new SimpleStyleableObjectProperty<ArrowSide>(...) {...} and new CssMetaData<TitledPane, ArrowSide>(...) {...}.
Type inference was improved in later versions of Java.
Use (StyleConverter<?, ArrowSide>) getEnumConverter(ArrowSide.class).
There was a bug in the generic signature of getEnumConverter that was fixed in a later version. Using the cast works around the problem. You may wish to #SuppressWarnings("unchecked") the cast.
Issue: Even with the above changes there's a problem in JavaFX 8—the arrow is only translated once the TitledPane is focused. This doesn't appear to be a problem with the above code as even changing the alignment property does not cause the TitledPane to update until it has focus (even when not using the above skin, but rather just the default skin). I've been unable to find a workaround to this problem (while using the custom skin) but maybe you or someone else can. I was using Java 1.8.0_202 when testing for JavaFX 8.
If you don't want to use a custom skin, or you're on JavaFX 8 (this will cause the arrow to be translated without needing to focus the TitledPane first), you can extract the necessary code, with some modifications, into a utility method:
public static void putArrowOnRight(TitledPane pane) {
Region title = (Region) pane.lookup(".title");
Region arrow = (Region) title.lookup(".arrow-button");
Text text = (Text) title.lookup(".text");
arrow.translateXProperty().bind(Bindings.createDoubleBinding(() -> {
double rightInset = title.getPadding().getRight();
return title.getWidth() - arrow.getLayoutX() - arrow.getWidth() - rightInset;
}, title.paddingProperty(), title.widthProperty(), arrow.widthProperty(), arrow.layoutXProperty()));
arrow.setStyle("-fx-padding: 0.0em 0.0em 0.0em 0.583em;");
DoubleBinding textGraphicBinding = Bindings.createDoubleBinding(() -> {
switch (pane.getAlignment()) {
case TOP_CENTER:
case CENTER:
case BOTTOM_CENTER:
case BASELINE_CENTER:
return 0.0;
default:
return -(arrow.getWidth());
}
}, arrow.widthProperty(), pane.alignmentProperty());
text.translateXProperty().bind(textGraphicBinding);
pane.graphicProperty().addListener((observable, oldGraphic, newGraphic) -> {
if (oldGraphic != null) {
oldGraphic.translateXProperty().unbind();
oldGraphic.setTranslateX(0);
}
if (newGraphic != null) {
newGraphic.translateXProperty().bind(textGraphicBinding);
}
});
if (pane.getGraphic() != null) {
pane.getGraphic().translateXProperty().bind(textGraphicBinding);
}
}
Note: While this puts the arrow on the right without having to focus the TitledPane first, the TitledPane still suffers from the issue noted above. For instance, changing the alignment property doesn't update the TitledPane until it's focused. I'm guessing this is simply a bug in JavaFX 8.
This way of doing things is not as "easy" as the skin approach and requires two things:
The TitledPane must be using the default TitledPaneSkin.
The TitledPane must have been displayed in a Window (window was showing) before calling the utility method.
Due to the lazy nature of JavaFX controls, the skin and the associated nodes will not have been created until the control has been displayed in a window. Calling the utility method before the control was displayed will result in a NullPointerException being thrown since the lookup calls will return null.
If using FXML, note that the initialize method is called during a call to FXMLLoader.load (any of the overloads). This means, under normal circumstances, it's not possible for the created nodes to be part of a Scene yet, let alone a showing Window. You must wait for the TitledPane to be displayed first, then call the utility method.
Waiting for the TitledPane to be displayed can be achieved by listening to the Node.scene property, the Scene.window property, and the Window.showing property (or you could listen for WindowEvent.WINDOW_SHOWN events). However, if you immediately put the loaded nodes into a showing Window, then you can forgo observing the properties; call the utility method inside a Platform.runLater call from inside initialize.
When using the skin approach, the whole wait-for-showing-window hassle is avoided.
Usual Warning: This answer relies on the internal structure of TitledPane which may change in a future release. Be cautious when changing JavaFX versions. I only (somewhat) tested this on JavaFX 8u202 and JavaFX 11.0.2.
This isn’t exactly the same, visually, but you can hide the arrow button and create a graphic that acts like an arrow button. TitledPane extends Labeled, so you have control over the placement of the graphic relative to the text, via the contentDisplay property.
First, hide the arrow button in the stylesheet:
.accordion .title > .arrow-button
{
visibility: hidden;
}
In the code, you can create a Label to act as a fake button and set it as the TitledPane’s graphic. The entire title line is sensitive to the mouse, so an interactive control (like a Button) is not needed.
Label collapseButton = new Label();
collapseButton.textProperty().bind(
Bindings.when(titledPane.expandedProperty())
.then("\u25bc").otherwise("\u25b6"));
titledPane.setGraphic(collapseButton);
titledPane.setContentDisplay(ContentDisplay.RIGHT);
In FXML you can just add nodeOrientation="RIGHT_TO_LEFT"
or use yourNode.setNodeOrientation((NodeOrientation orientation)
https://openjfx.io/javadoc/11/javafx.graphics/javafx/scene/Node.html#setNodeOrientation(javafx.geometry.NodeOrientation)
I'm new to the forums, so I hope I'm not asking a question that has been answered in the past. I've tried to be thorough looking for answer before posting.
I am currently working on a pie chart that will eventually be used for tracking financial expenses. Right now I have several categories that make up each slice. I am trying to hide the legend for the zero value slices.
I am doing this in javaFX. I'm still very green when it comes to programming and don't have experience outside of Java. Any help as explained to dummies would be appreciated. Thanks.
Added a picture and complete code to illustrate the problem at hand. Restaurants & Dining, and Shopping & entertainment both have zero values. I want to hide the legend for those items in this example.
package Example;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.chart.PieChart;
public class PieExampleController implements Initializable {
#FXML
private PieChart pieChart;
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
// TODO Auto-generated method stub
ObservableList<PieChart.Data> pieChartData = FXCollections.observableArrayList(
new PieChart.Data("Groceries", 1),
new PieChart.Data("Transportation", 1),
new PieChart.Data("Restaurants & Dining", 0),
new PieChart.Data("Shopping & Entertainment", 0));
pieChart.setData(pieChartData);
}
}
Thats how i do it:
List<PieChart.Data> dataArrayList = new LinkedList<Data>();
if (value1>0) {
Data data = new PieChart.Data("my label", value1);
dataArrayList.add(data);
}
...
ObservableList<PieChart.Data> pieChartData =
FXCollections.observableArrayList(dataArrayList);
Adding only not empty data entries (or removing empty entries) manually once at startup is just fine if the data is immutable and unmodifiable. On the other hand, if it can change during the lifetime of the chart, we need a mechanism that handles the add/remove automagically: FilteredList to the rescue.
Below is an example that
configures a source list with an extractor (on the pieValueProperty): doing so will notify any listChangeListener on change of that value with a change of type update
wraps a FilteredList around the source list
configures the pieChart with the filteredList
With that in place, we can install a predicate on the filteredList that hides items as needed: the example uses a Slider to update the lower threshhold of which data values should be included in the chart.
Unfortunately, PieChart has a couple of bugs (sigh... whatever I touch in FX, they always boil up ...) that interfere with such a simple setup
due to a freaky mixture of node/value plus "optimized" internal data structure plus incorrect implementation of syncing the internal (linked) data structure with changes to the list the chart can't be animated
the sync can't handle changes of type replaced at all (which is what FilteredList fires on resetting the predicate)
In an example both issues can be avoided by disabling animation and clearing out the list (set a predicate that blocks all) before setting the real condition. In producation code such tweaking may or may not be possible.
The example:
public class FilteredPieChartExample extends Application {
#Override
public void start(Stage primaryStage) {
FilteredList<Data> filtered = getChartData();
//ListChangeReport report = new ListChangeReport(filtered);
PieChart pieChart = new PieChart(filtered);
// bug in pieChart: can't handle data modification with animation on
pieChart.setAnimated(false);
// use slider to set lower threshhold for value of data to show in pie
Slider slider = new Slider(-1., 100., -1.);
slider.valueProperty().addListener((src, ov, nv) -> {
// actually, cannot handle data modification at all ... need to clear out first ...
// bug in pieChart.dataChangeListener: doesn't handle replaced correctly
filtered.setPredicate(data -> false);
filtered.setPredicate(data -> data.getPieValue() > nv.doubleValue());
//report.prettyPrint();
});
primaryStage.setTitle("PieChart");
Pane root = new VBox(pieChart, slider);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
private FilteredList<Data> getChartData() {
// use ObservableList with extractor on pieValueProperty
ObservableList<Data> answer = FXCollections.observableArrayList(
e -> new Observable[] {e.pieValueProperty()}
);
answer.addAll(
new Data("java", 17.56),
new Data("C", 17.06),
new Data("C++", 8.25),
new Data("C#", 8.20),
new Data("ObjectiveC", 6.8),
new Data("PHP", 6.0),
new Data("(Visual)Basic", 4.76),
new Data("Other", 31.37),
new Data("empty", 0)
);
return new FilteredList<>(answer);
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger.getLogger(FilteredPieChartExample.class
.getName());
}
Disclaimer: I'm very new to JavaFX and Model-View-Controller design principles.
Here's my basic situation:
I have a ScreenManager similar to the one outlined in this tutorial. Using the screen manager, I switch between several FXML screens using basic action handling on the screen buttons.
The problem is that one of these is a game screen with a map. I'll be drawing a tile-based map onto this game screen, so I went with a TilePane in my FXML file(if there's a better way to draw tiles to an area of a screen, please let me know).
Here's the code where I create the TilePane that I want to draw to the game screen:
public TilePane createRandomMap(boolean centeredRiver)
{
Image atlas = new Image("resources/textures/tile_atlas.jpg");
Random rand = new Random();
int riverPos = 4,
tileHeight = (int)atlas.getHeight()/2,
tileWidth = (int)atlas.getWidth()/3;
if(!centeredRiver)
riverPos = rand.nextInt(10);
for(int i = 0; i < 5; i++)
{
for(int j = 0; j < riverPos; j++)
{
int type = rand.nextInt(4);
Tile tile = new Tile(tileTypes[type], i, j);
tile.setImage(atlas);
tile.setViewport(new Rectangle2D((type%3)*tileWidth, //minX
tileHeight-(type/3)*tileHeight, //minY
tileWidth, tileHeight)); // width, height
tile.setFitHeight(tilePane.getHeight()/5); //fit to the size of the pane
tile.setFitWidth(tilePane.getWidth()/9); //fit to the size of the pane
tile.setPreserveRatio(true);
tilesToAdd.add(tile);
}
if(i == 2)
{
Tile tile = new Tile(tileTypes[5], i, riverPos);
tile.setImage(atlas);
tile.setViewport(new Rectangle2D(2*tileWidth, 0,
tileWidth, tileHeight));
tile.setFitHeight(tilePane.getHeight()/5);
tile.setFitWidth(tilePane.getWidth()/9);
tile.setPreserveRatio(true);
tilesToAdd.add(tile);
}
else
{
Tile tile = new Tile(tileTypes[4], i, riverPos);
tile.setImage(atlas);
tile.setViewport(new Rectangle2D(tileWidth, 0,
tileWidth, tileHeight));
tile.setFitHeight(tilePane.getHeight()/5);
tile.setFitWidth(tilePane.getWidth()/9);
tile.setPreserveRatio(true);
tilesToAdd.add(tile);
}
for(int j = riverPos + 1; j<9; j++)
{
int type = rand.nextInt(4);
Tile tile = new Tile(tileTypes[type], i, j);
tile.setImage(atlas);
tile.setViewport(new Rectangle2D((type%3)*tileWidth, //minX
tileHeight-(type/3)*tileHeight, //minY
tileWidth, tileHeight)); // width, height
tile.setFitHeight(tilePane.getHeight()/5); //fit to the size of the pane
tile.setFitWidth(tilePane.getWidth()/9); //fit to the size of the pane
tile.setPreserveRatio(true);
tilesToAdd.add(tile);
}
}
tilePane.getChildren().addAll(tilesToAdd);
return tilePane;
}
I've been able to access this TilePane in my controller class:
public class GameScreenController implements Initializable, ControlledScreen {
ScreenManager screenManager;
#FXML
TilePane mapPane
/**
* Initializes the controller class.
*/
#Override
public void initialize(URL url, ResourceBundle rb) {
TileEngine engine = new TileEngine();
mapPane = engine.createRandomMap(true);
}
In the above instance, I'm setting the TilePane defined in my FXML screen to the TilePane I created in my model, but I get the following error:
Can not set javafx.scene.layout.TilePane field
screens.gameScreen.GameScreenController.mapPane to
javafx.scene.layout.AnchorPane
Here's the segment of my FXML file dealing with the TilePane:
<TilePane id="MapPane" fx:id="mapPane" layoutX="0.0" layoutY="0.0" prefColumns="9" prefHeight="560.0" prefTileHeight="112.0" prefTileWidth="112.0" prefWidth="1108.0" visible="true" />
I'm really struggling to wrap my head around JavaFX and game design in general, but very much want to learn. I'll be happy to clarify and to take criticism on any part of how this is structured in order to make it better, even things that don't pertain to the question I'm asking.
Thank you in advance!
MVC
Strictly speaking JavaFX does not support MVC (which is dead anyways declared by the inventor himself) but some kind of MVVM (Model-View-ViewModel)
Communication between Model and Controller
I generally use Dependency Injection to do so. If you want a simple FrameWork I can suggest you afterburner.fx or Google Guice. Both pretty light weight frameworks which do what you want. Basically they can handle the instantiation of the classes for you and assure if wanted that only one instance of your model is active.
Some Sample Code:
public class Presenter {
// Injects
#Inject private StateModel stateModel;
}
No need for declaring Constructor or anything. You need a bit more work to test it if you want so, but overall I don't want to miss this feature again, because generally you just want one StateModel where all your information is derived from and all the views just listen to them or change them.
Lets give a further example to extend to posted code
#Singleton
public class StateModel {
// Properties
private final StringProperty name = new SimpleStringProperty();
// Property Getter
public StringProperty nameProperty() { return this.name; }
}
If we have a label now declared in our Presenter/Controller, we can listen to the changes in our model like this
public class Presenter implements Initializable {
// Nodes
#FXML private Label nodeLabelName;
// Injects
#Inject private StateModel stateModel;
#Override
public void initialize(URL location, ResourceBundle resource) {
this.nodeLabelName.textProperty().bind(this.stateModel.nameProperty());
}
}
Another suggestion I can give you is Threading. You generally want your Business Logic to be performed in another Thread else your Application-Thread will start lagging when it is processing
JavaFX's TableView has a placeholder property that is basically a Node that gets displayed in the TableView whenever it is empty. If this property is set to null (its default value), it appears as a Label or some other text based Node that says "There is no content in the table."
But if there are any rows of data in the table, then the placeholder Node disappears and the entire vertical space in the TableView gets filled with rows, including empty rows if there isn't enough data to fill the whole table.
These empty rows are what I want, even when the table is empty. In other words, I don't want to use the placeholder at all. Does anyone know how I can do this?
I'd rather not do something kludgey like put a empty-looking row in the TableView whenever it's supposed to be actually empty.
Unfortunately, the old issue is still not fixed in fx9 nor later versions up and including fx18.
The original hack of the skin (see below the horizontal line) for fx9 seems to still be working in fx18. Though can be slightly improved since fx12 introduced access to the flow and tableHeaderRow.
While trying to adjust it to newer api, I came up with a - just as dirty and untested - approach. The idea is to override the actual layout method, that is the method that's called when laying out individual children of the table, watch out for the placeholder and replace it with the flow:
public class NoPlaceHolderSkin<T> extends TableViewSkin<T> {
public NoPlaceHolderSkin(TableView<T> control) {
super(control);
}
#Override
protected void layoutInArea(Node child, double areaX, double areaY, double areaWidth,
double areaHeight, double areaBaselineOffset, HPos halignment, VPos valignment) {
if (child.getStyleClass().contains("placeholder")) {
child.setVisible(false);
child = getVirtualFlow();
child.setVisible(true);
}
super.layoutInArea(child, areaX, areaY, areaWidth, areaHeight, areaBaselineOffset, halignment, valignment);
}
}
So revisited the hacks in the context of fx9. There had been changes, good and bad ones:
Skins moved into a public package which now allows to subclass them without accessing hidden classes (good)
the move introduced a bug which doesn't allow to install a custom VirtualFlow (fixed in fx10)
reflective access to hidden members will be strongly disallowed (read: not possible) sometime in future
While digging, I noticed ever so slight glitches with the hacks (note: I did not run them against fx8, so these might be due differences in fx8 vs fx9!)
the forced in-/visibility of placeholder/flow worked fine except when starting up with an empty table (placeholder was shown) and enlarging the table while empty (the "new" region looks empty)
faking the itemCount to not-empty lets the rows dissappear on pressing navigation keys (which probably is not a big problem because users tend to not navigate an empty table) - this is definitely introduced in fx9, working fine in fx8
So I decided to go with the visibility enforcement: the reason for the slight glitches is that layoutChildren doesn't layout the flow if it thinks the placeholder is visible. That's handled by including the flow in the layout if super didn't.
The custom skin:
/**
* TableViewSkin that doesn't show the placeholder.
* The basic trick is keep the placeholder/flow in-/visible at all
* times (similar to https://stackoverflow.com/a/27543830/203657).
* <p>
*
* Updated for fx9 plus ensure to update the layout of the flow as
* needed.
*
* #author Jeanette Winzenburg, Berlin
*/
public class NoPlaceHolderTableViewSkin<T> extends TableViewSkin<T>{
private VirtualFlow<?> flowAlias;
private TableHeaderRow headerAlias;
private Parent placeholderRegionAlias;
private ChangeListener<Boolean> visibleListener = (src, ov, nv) -> visibleChanged(nv);
private ListChangeListener<Node> childrenListener = c -> childrenChanged(c);
/**
* Instantiates the skin.
* #param table the table to skin.
*/
public NoPlaceHolderTableViewSkin(TableView<T> table) {
super(table);
flowAlias = (VirtualFlow<?>) table.lookup(".virtual-flow");
headerAlias = (TableHeaderRow) table.lookup(".column-header-background");
// startet with a not-empty list, placeholder not yet instantiatet
// so add alistener to the children until it will be added
if (!installPlaceholderRegion(getChildren())) {
installChildrenListener();
}
}
/**
* Searches the given list for a Parent with style class "placeholder" and
* wires its visibility handling if found.
* #param addedSubList
* #return true if placeholder found and installed, false otherwise.
*/
protected boolean installPlaceholderRegion(
List<? extends Node> addedSubList) {
if (placeholderRegionAlias != null)
throw new IllegalStateException("placeholder must not be installed more than once");
List<Node> parents = addedSubList.stream()
.filter(e -> e.getStyleClass().contains("placeholder"))
.collect(Collectors.toList());
if (!parents.isEmpty()) {
placeholderRegionAlias = (Parent) parents.get(0);
placeholderRegionAlias.visibleProperty().addListener(visibleListener);
visibleChanged(true);
return true;
}
return false;
}
protected void visibleChanged(Boolean nv) {
if (nv) {
flowAlias.setVisible(true);
placeholderRegionAlias.setVisible(false);
}
}
/**
* Layout of flow unconditionally.
*
*/
protected void layoutFlow(double x, double y, double width,
double height) {
// super didn't layout the flow if empty- do it now
final double baselineOffset = getSkinnable().getLayoutBounds().getHeight() / 2;
double headerHeight = headerAlias.getHeight();
y += headerHeight;
double flowHeight = Math.floor(height - headerHeight);
layoutInArea(flowAlias, x, y,
width, flowHeight,
baselineOffset, HPos.CENTER, VPos.CENTER);
}
/**
* Returns a boolean indicating whether the flow should be layout.
* This implementation returns true if table is empty.
* #return
*/
protected boolean shouldLayoutFlow() {
return getItemCount() == 0;
}
/**
* {#inheritDoc} <p>
*
* Overridden to layout the flow always.
*/
#Override
protected void layoutChildren(double x, double y, double width,
double height) {
super.layoutChildren(x, y, width, height);
if (shouldLayoutFlow()) {
layoutFlow(x, y, width, height);
}
}
/**
* Listener callback from children modifications.
* Meant to find the placeholder when it is added.
* This implementation passes all added sublists to
* hasPlaceHolderRegion for search and install the
* placeholder. Removes itself as listener if installed.
*
* #param c the change
*/
protected void childrenChanged(Change<? extends Node> c) {
while (c.next()) {
if (c.wasAdded()) {
if (installPlaceholderRegion(c.getAddedSubList())) {
uninstallChildrenListener();
return;
}
}
}
}
/**
* Installs a ListChangeListener on the children which calls
* childrenChanged on receiving change notification.
*
*/
protected void installChildrenListener() {
getChildren().addListener(childrenListener);
}
/**
* Uninstalls a ListChangeListener on the children:
*/
protected void uninstallChildrenListener() {
getChildren().removeListener(childrenListener);
}
}
Usage example:
public class EmptyPlaceholdersInSkin extends Application {
private Parent createContent() {
// initially populated
//TableView<Person> table = new TableView<>(Person.persons()) {
// initially empty
TableView<Person> table = new TableView<>() {
#Override
protected Skin<?> createDefaultSkin() {
return new NoPlaceHolderTableViewSkin<>(this);
}
};
TableColumn<Person, String> first = new TableColumn<>("First Name");
first.setCellValueFactory(new PropertyValueFactory<>("firstName"));
table.getColumns().addAll(first);
Button clear = new Button("clear");
clear.setOnAction(e -> table.getItems().clear());
clear.disableProperty().bind(Bindings.isEmpty(table.getItems()));
Button fill = new Button("populate");
fill.setOnAction(e -> table.getItems().setAll(Person.persons()));
fill.disableProperty().bind(Bindings.isNotEmpty(table.getItems()));
BorderPane pane = new BorderPane(table);
pane.setBottom(new HBox(10, clear, fill));
return pane;
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
stage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(EmptyPlaceholdersInSkin.class.getName());
}
I think I found a solution. It is definitely not nice, since it is accessing the API in a not wanted way, and I'm probably also making undesired use of the visibleProperty, but here you go:
You can try to hack the TableViewSkin. Basically do this to retrieve a hacked Skin:
public class ModifiedTableView<E> extends TableView<E> {
#Override
protected Skin<?> createDefaultSkin() {
final TableViewSkin<E> skin = new TableViewSkin<E>(this) {
// override method here
}
// modifiy skin here
return skin;
}
}
For the TableViewSkin you then need to override following method:
#Override
protected VirtualFlow<TableRow<E>> createVirtualFlow() {
final VirtualFlow<TableRow<E>> flow = new VirtualFlow<TableRow<E>>();
// make the 'scroll-region' always visible:
flow.visibleProperty().addListener((invalidation) -> {
flow.setVisible(true);
});
return flow;
}
And for the skin using reflection stop showing the placeholder:
final Field privateFieldPlaceholderRegion = TableViewSkinBase.class.getDeclaredField("placeholderRegion");
privateFieldPlaceholderRegion.setAccessible(true);
final StackPane placeholderRegion = (StackPane) privateFieldPlaceholderRegion.get(skin);
// make the 'placeholder' never visible:
placeholderRegion.visibleProperty().addListener((invalidation) -> {
placeholderRegion.setVisible(false);
});
Maybe you can change the visibility of the flow in the same method to make the code shorter... But I think you get the concept
I found a solution for javafx8. It makes use of the non-public api, but it uses no reflection (luckly). Basically you need to set (or replace) the skin of the TableView and return a non-zero value in the method getItemCount(). Like so:
(TableView)t.setSkin(new TableViewSkin<Object>(t)
{
#Override
public int getItemCount()
{
int r = super.getItemCount();
return r == 0 ? 1 : r;
}
});
This method can also be used to add an extra row at the bottom of your last item (for if you want to include an add button for example). Basically return always one higher than the actual item-count.
Eventhough this is an old question, hopefully this was helpfull to someone.
If any one is still looking for an alternate solution apart from what others had provided, below is the one which I worked with. As far as to me, this is the most simpliest approach I can go with (no custom skins, no API tweaking & no heavy controls like ListView).
Set a StackPane with a customized CSS that resembles alternate row coloring.
StackPane placeHolder = new StackPane();
placeHolder.setStyle("-fx-background-color:linear-gradient(from 50px 14px to 50px 40px , repeat, #e8e8e8 49% , #f7f7f7 12% );");
tableView.setPlaceholder(placeHolder);
Below is the quick reference for implementation. The left table is with data and the right table is without data showing customized placeholder.
If your are specific with showing the column lines as well, you can follow the #Shreyas Dave approach of building a HBox of the StackPane(s) with border implementation.
HBox placeHolder = new HBox();
tableView.getColumns().forEach(tc->{
StackPane colHolder = new StackPane();
colHolder.getStyleClass().add("table-view-place-holder");
colHolder.prefWidthProperty().bind(tc.widthProperty());
placeHolder.getChildren().add(colHolder);
});
tableView.setPlaceholder(placeHolder);
And the CSS implementation is as below:
.table-view-place-holder{
-fx-background-color:linear-gradient(from 50px 14px to 50px 40px , repeat, #232A2D 49% , #3D4549 12% );
-fx-border-width: 0px 1px 0px 0px;
-fx-border-color: linear-gradient(from 50px 14px to 50px 40px , repeat, #3D4549 49% , #232a2d 12% );
}
I have a requirement of having contrast border color to row background. I can easily acheive that with the above approach for having border color to my placeholder columns.
Here is a tricky way to perform your task,
HBox box = new HBox();
box.setDisable(true);
for (TableColumn column : patientsTable.getColumns()) {
ListView<String> listView = new ListView<>();
listView.getItems().add("");
listView.setPrefWidth(column.getWidth());
box.getChildren().add(listView);
}
tableView.setPlaceholder(box);