Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Label rendering issue in TableCell

Tags:

javafx

I am encountering an issue with rendering a Label in TableCell. The Label flickers when the TableCell gets an update. This happens if I wrap the Label in a Pane and set it as graphic to the cell.

On the other hand, if I set the Label directly as the graphic(without wrapper), then it doesn't show the flickering effect.

Do any of you have any idea why this happens.

Below is a quick demo that showcases the flickering effect of the Label. You can uncomment the other line and can see the flickering does not happen. The issue happens on both JavaFX8 and JavaFX20

When wrapped with StackPane (flickering):

enter image description here

(some frames might be skipped in gif, but the flickering happens on every update)

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;

public class LabelRenderingInCell_Demo extends Application {

    class Person {
        private final StringProperty name = new SimpleStringProperty();

        public Person(final String n) {
            setName(n);
        }

        public String getName() {
            return name.get();
        }

        public StringProperty nameProperty() {
            return name;
        }

        public void setName(final String name) {
            this.name.set(name);
        }

        @Override
        public String toString() {
            return name.get();
        }
    }

    private int count = 1;

    @Override
    public void start(final Stage primaryStage) throws Exception {
        Person person = new Person("Name");
        final ObservableList<Person> items = FXCollections.observableArrayList();
        items.add(person);

        final TableView<Person> tableView = new TableView<>();
        final TableColumn<Person, String> col = new TableColumn<>("Name");
        col.setPrefWidth(150);
        col.setCellValueFactory(param -> param.getValue().nameProperty());
        col.setCellFactory(new Callback<>() {
            @Override
            public TableCell<Person, String> call(final TableColumn<Person, String> param) {
                return new TableCell<>() {
                    @Override
                    protected void updateItem(final String item, final boolean empty) {
                        super.updateItem(item, empty);
                        setText(null);
                        if (item != null) {
                            setGraphic(new StackPane(new Label(item)));
                            // Uncomment the below code for no flickering
                           // setGraphic(new Label(item));
                        } else {
                            setGraphic(null);
                        }
                    }
                };
            }
        });

        tableView.getColumns().add(col);
        tableView.setItems(items);
        VBox.setVgrow(tableView, Priority.ALWAYS);

        Button btn = new Button("Update");
        btn.setOnAction(e -> {
            person.setName("Name " + count++);
        });

        Scene scene = new Scene(new VBox(btn, tableView));
        primaryStage.setScene(scene);
        primaryStage.setTitle("Label In TableCell");
        primaryStage.show();
    }
}

Update:

The content of is very dynamic based on the value associated with the property. It can be combination of different colored labels with some images as well

enter image description here

Update #2: Based on the suggestions in the comments, I changed my code to reuse the labels or atleast maintain a pool of Labels. But I still see the flickering everytime a new Label is added to cell.

Below is a quick modified demo with the logic. I am using a pool of 5 Labels that needs to be displayed based on the grade. Each Label will have its own styling, interactions, icons etc. So using only 1 Label for all grades will make things too messy here.

The Label flickers everytime a new grade Label is generated and added. For testing purpose included a refresh button that generates a new cell for checking the cycle again. (You may not see the flicker in the below gif if the frames gets skipped, but you can notice when running the application)

It may look like it is just a blink. When the behavior is not systematic, it looks a bit odd/worse when seen on larger scale.

enter image description here

import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;

import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;

public class LabelRenderingInCell_Demo extends Application {

    class Person {
        private final ObjectProperty<Integer> score = new SimpleObjectProperty<>(0);

        public ObjectProperty<Integer> scoreProperty() {
            return score;
        }

        public void setScore(final int score) {
            this.score.set(score);
        }
    }

    @Override
    public void start(final Stage primaryStage) throws Exception {
        Person person = new Person();
        final ObservableList<Person> items = FXCollections.observableArrayList();
        items.add(person);

        final TableView<Person> tableView = new TableView<>();
        final TableColumn<Person, Integer> col = new TableColumn<>("Grade");
        col.setPrefWidth(150);
        col.setCellValueFactory(param -> param.getValue().scoreProperty());
        col.setCellFactory(new Callback<>() {
            @Override
            public TableCell<Person, Integer> call(final TableColumn<Person, Integer> param) {
                return new TableCell<>() {
                    private Map<String, Label> labelPool = new HashMap<>();
                    private HBox pane = new HBox();

                    @Override
                    protected void updateItem(final Integer item, final boolean empty) {
                        super.updateItem(item, empty);
                        setText(null);
                        if (item != null) {
                            pane.getChildren().clear();
                            Label label = getLabel(item);
                            label.setText(buildGrade(item) + "(" + item + ")");
                            pane.getChildren().addAll(label);
                            setGraphic(pane);
                        } else {
                            setGraphic(null);
                        }
                    }

                    private String buildGrade(int score) {
                        if (score < 3) {
                            return "Poor";
                        } else if (score < 5) {
                            return "Average";
                        } else if (score < 7) {
                            return "Good";
                        } else if (score < 9) {
                            return "Better";
                        } else {
                            return "Excellent";
                        }
                    }

                    private Label getLabel(int score) {
                        String grade = buildGrade(score);
                        Label lbl = labelPool.get(grade);
                        if (lbl == null) {
                            System.out.println("Cell-"+this.hashCode() + " ::  New label created.... " + grade);
                            lbl = new Label(grade + "(" + score + ")");
                            // Add other styling and interactions based on the grade
                            labelPool.put(grade, lbl);
                        }
                        return lbl;
                    }
                };
            }
        });

        tableView.getColumns().add(col);
        tableView.setItems(items);
        VBox.setVgrow(tableView, Priority.ALWAYS);

        SecureRandom rnd = new SecureRandom();
        Button btn = new Button("Update Score");
        btn.setOnAction(e -> {
            person.setScore(rnd.nextInt(11));
        });

        Button refresh = new Button("Refresh Table to start from scratch");
        refresh.setOnAction(e -> {
            System.out.println("--------------------------------");
            tableView.refresh();
        });

        VBox root = new VBox(btn, refresh, tableView);
        root.setSpacing(5);
        root.setPadding(new Insets(5));
        Scene scene = new Scene(root, 350, 200);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Label In TableCell");
        primaryStage.show();
    }
}
like image 750
Sai Dandem Avatar asked Oct 31 '25 00:10

Sai Dandem


2 Answers

Don't create node instances in the updateItem() method. This method may be called frequently and is not designed for "heavy" operations such as creating UI nodes.

Instead, create any nodes you might need at the creation time of the cell. Then use the updateItem() method solely to configure those nodes and choose which to display, as necessary.

Here is an example cell factory which switches nodes on each button click, without any flicker:

    col.setCellFactory(new Callback<>() {
        @Override
        public TableCell<Person, String> call(final TableColumn<Person, String> param) {
            return new TableCell<>() {

                private final Label label = new Label();
                private final StackPane even = new StackPane();
                private final StackPane odd = new StackPane();
                @Override
                protected void updateItem(final String item, final boolean empty) {
                    super.updateItem(item, empty);
                    setText(null);
                    if (item != null) {
                        StackPane graphic ;
                        String[] tokens = item.split(" ");
                        int value = tokens.length == 2 ? Integer.parseInt(tokens[1]) : 0;
                        graphic = value % 2 == 0 ? even : odd ;
                        even.getChildren().clear();
                        odd.getChildren().clear();
                        graphic.getChildren().add(label);
                        label.setText(item);
                        setGraphic(graphic);
                        // Uncomment the below code for no flickering
                        // setGraphic(new Label(item));
                    } else {
                        setGraphic(null);
                    }
                }
            };
        }
    });
like image 173
James_D Avatar answered Nov 02 '25 22:11

James_D


This seems to be really a nasty problem:

  • multiple nodes of which only a single one is added at any time
  • added deeper down the cell hierarchy
  • switching them out is done in updateItem

The very first time such a switched-in node is showing, it flickers - doesn't happen if switched again. Creating all nodes up front doesn't seem to help.

What does help (not formally tested, beware!) is to add the cell

  • build all nodes up front
  • have a listener on the tableView property, when triggered with tableView != null and its skin != null
  • add all nodes into the pane and add the pane to the table
  • cleanup again, probably need to remove the listener again

in code (note that the contentDisplay isn't really important, just something I prefer to do when it's graphic only :)

col.setCellFactory(new Callback<>() {
    @Override
    public TableCell<Person, Integer> call(final TableColumn<Person, Integer> param) {
        return new TableCell<>() {
            private Map<String, Label> labelPool = new HashMap<>();
            private HBox pane = new HBox();

            private ChangeListener<TableView<?>> tableListener;

            {
                buildPool();
                setGraphic(pane);
                setText("");
                setContentDisplay(ContentDisplay.GRAPHIC_ONLY);

                tableListener = (src, ov, nv) -> {
                    if (nv != null) {
                        System.out.println(nv.getSkin());
                        if (nv.getSkin() != null) {
                            pane.getChildren().addAll(labelPool.values());
                            ((TableViewSkin<?>) nv.getSkin()).getChildren().add(this);
                            pane.getChildren().clear();
                        }
                    }
                };
                tableViewProperty().addListener(tableListener);
            }

            @Override
            protected void updateItem(final Integer item, final boolean empty) {
                super.updateItem(item, empty);
                if (item != null) {
                    Label label = getLabel(item);
                    label.setText(buildGrade(item) + "(" + item + ")");
                    pane.getChildren().setAll(label);
                } else {
                    pane.getChildren().clear();
                }
            }

            private void buildPool() {
                for (int i = 0; i < 11; i++) {
                    getLabel(i);
                }
            }
            private String buildGrade(int score) {
                if (score < 3) {
                    return "Poor";
                } else if (score < 5) {
                    return "Average";
                } else if (score < 7) {
                    return "Good";
                } else if (score < 9) {
                    return "Better";
                } else {
                    return "Excellent";
                }
            }

            private Label getLabel(int score) {
                String grade = buildGrade(score);
                Label lbl = labelPool.get(grade);
                if (lbl == null) {
                    System.out.println("Cell-"+this.hashCode() + " ::  New label created.... " + grade);
                    lbl = new Label(grade + "(" + score + ")");
                    // Add other styling and interactions based on the grade
                    labelPool.put(grade, lbl);
                }
                return lbl;
            }
        };
    }
});
like image 29
kleopatra Avatar answered Nov 02 '25 23:11

kleopatra



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!