Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFX table row update

Tags:

java

javafx

The scenario i am trying to achieve is,

  1. Whenever a particular TableCell in a TableRow gets updated , the row color will be changed to red and after 3 seconds the color should be automatically reverted to original.

below is MCVE,

Main Class

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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.stage.Stage;

public class TestProjectWin10 extends Application {
    private final ObservableList<Element> data = FXCollections.observableArrayList();

    public final Runnable changeValues = () -> {
        int i = 0;
        while (i <= 100000) {
            if (Thread.currentThread().isInterrupted()) {
                break;
            }
            data.get(0).setOccurence(System.currentTimeMillis());
            data.get(0).count();
            i = i + 1;
        }
    };

    private final ExecutorService executor = Executors.newSingleThreadExecutor(runnable -> {
        Thread t = new Thread(runnable);
        t.setDaemon(true);
        return t;
    });

    @Override
    public void start(Stage primaryStage) {

        TableView<Element> table = new TableView<>();
        table.getStylesheets().add(this.getClass().getResource("tableColor.css").toExternalForm());
        table.setEditable(true);

        TableColumn<Element, String> nameCol = new TableColumn<>("Name");
        nameCol.setPrefWidth(200);
        nameCol.setCellValueFactory(cell -> cell.getValue().nameProperty());
        nameCol.setCellFactory((TableColumn<Element, String> param) -> new ColorCounterTableCellRenderer(table));
        table.getColumns().add(nameCol);

        this.data.add(new Element());
        table.setItems(this.data);

        this.executor.submit(this.changeValues);

        Scene scene = new Scene(table, 600, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

The Element Class:

import java.util.concurrent.atomic.AtomicReference;

import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Element {
    int x = 0;

    private final StringProperty nameProperty = new SimpleStringProperty("");

    private final AtomicReference<String> name = new AtomicReference<>();

    private final DoubleProperty occurence = new SimpleDoubleProperty();

    public void count() {
        x = x + 1;
        if (name.getAndSet(Integer.toString(x)) == null) {
            Platform.runLater(() -> nameProperty.set(name.getAndSet(null)));
        }
    }

    public void setOccurence(double value) {
        occurence.set(value);
    }

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

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

    public StringProperty nameProperty() {
        return nameProperty;
    }

    double getOccurrenceTime() {
        return occurence.get();
    }
}

CellFactory code:

import java.util.Timer;
import java.util.TimerTask;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;

public class ColorCounterTableCellRenderer extends TableCell<Element, String> {

    private final static long MAX_MARKED_TIME = 3000;
    private final static long UPDATE_INTERVAL = 1000;

    private static Timer t = null;
    private final String highlightedStyle = "highlightedRow";

    private final TableView tv;

    public ColorCounterTableCellRenderer(TableView tv) {
        this.tv = tv;
        createTimer();
        setAlignment(Pos.CENTER_RIGHT);
    }

    private void createTimer() {
        if (t == null) {
            t = new Timer("Hightlight", true);
            t.schedule(new TimerTask() {
                @Override
                public void run() {

                    final long currentTime = System.currentTimeMillis();

                    TableRow tr = getTableRow();
                    if (tr.getItem() != null) {

                        if (currentTime - ((Element) tr.getItem()).getOccurrenceTime() > MAX_MARKED_TIME) {
                            Platform.runLater(() -> {
                                tr.getStyleClass().remove(highlightedStyle);
                            });
                        }
                    }
                }
            }, 0, UPDATE_INTERVAL);
        }
    }

    @Override
    protected void updateItem(String item, boolean empty) {
        if (empty || getTableRow() == null || getTableRow().getItem() == null) {
            setText(null);
            return;
        }

        long currentTime = System.currentTimeMillis();

        TableRow<Element> row = getTableRow();
        Element elementRow = row.getItem();

        double occurrenceTime = elementRow.getOccurrenceTime();

        if (currentTime - occurrenceTime < MAX_MARKED_TIME) {
            if (!row.getStyleClass().contains(highlightedStyle)) {
                row.getStyleClass().add(highlightedStyle);
            }
        }

        super.updateItem(item, empty);
        setText(item + "");

    }
}

and the css file tableColor.css

.highlightedRow {
    -fx-background-color: rgba(255,0,0, 0.25);
    -fx-background-insets: 0, 1, 2;
    -fx-background: -fx-accent;
    -fx-text-fill: -fx-selection-bar-text;    
}

What is the issue..?

  1. I check whether difference between the current time and update happened time is less than 3 seconds - row color to red (in ColorCounterTableCellRenderer - updateItem method)

  2. in a separate timer (ColorCounterTableCellRenderer) , i try check whether difference between current time and update happened time is more than 3 seconds - Removing red color.

But in the timer (createTimer - method) code : the tr.getItem() is always null and hence not removing red color.

Is this the correct way to achieve what i want? Why tr.getItem() returns null.

To test : I ran the code and waited for executor code to end and checked whether red color is removed after 3 seconds.

like image 636
user3164187 Avatar asked Sep 26 '18 13:09

user3164187


1 Answers

Any updates to the UI, even if it's triggered through listeners, needs to be done from the application thread. (You can overcome this issue by doing the updates using Platform.runLater.)

Furthermore you cannot rely on the same cell keeping the same cell for the complete time it's supposed to be shown as marked.

To overcome this issue you need to store the info about the marked cells either in the item itself or in some observable external data structure.

The following example stores the times of the last update in a ObservableMap and uses a AnimationTimer to clear expired entries from the map. Furthermore it uses TableRows to update a pseudoclass based on the contents of the map.

private static class Item {

    private final IntegerProperty value = new SimpleIntegerProperty();
}

private final ObservableMap<Item, Long> markTimes = FXCollections.observableHashMap();
private AnimationTimer updater;

private void updateValue(Item item, int newValue) {
    int oldValue = item.value.get();
    if (newValue != oldValue) {
        item.value.set(newValue);

        // update time of item being marked
        markTimes.put(item, System.nanoTime());

        // timer for removal of entry
        updater.start();
    }
}

@Override
public void start(Stage primaryStage) {
    Item item = new Item(); // the item that is updated
    TableView<Item> table = new TableView<>();
    table.getItems().add(item);

    // some additional items to make sure scrolling effects can be tested
    IntStream.range(0, 100).mapToObj(i -> new Item()).forEach(table.getItems()::add);

    TableColumn<Item, Number> column = new TableColumn<>();
    column.getStyleClass().add("mark-column");
    column.setCellValueFactory(cd -> cd.getValue().value);
    table.getColumns().add(column);

    final PseudoClass marked = PseudoClass.getPseudoClass("marked");

    table.setRowFactory(tv -> new TableRow<Item>() {

        final InvalidationListener reference = o -> {
            pseudoClassStateChanged(marked, !isEmpty() && markTimes.containsKey(getItem()));
        };
        final WeakInvalidationListener listener = new WeakInvalidationListener(reference);

        @Override
        protected void updateItem(Item item, boolean empty) {
            boolean wasEmpty = isEmpty();
            super.updateItem(item, empty);

            if (empty != wasEmpty) {
                if (empty) {
                    markTimes.removeListener(listener);
                } else {
                    markTimes.addListener(listener);
                }
            }

            reference.invalidated(null);
        }

    });

    Scene scene = new Scene(table);
    scene.getStylesheets().add("style.css");
    primaryStage.setScene(scene);
    primaryStage.show();

    updater = new AnimationTimer() {

        @Override
        public void handle(long now) {
            for (Iterator<Map.Entry<Item, Long>> iter = markTimes.entrySet().iterator(); iter.hasNext();) {
                Map.Entry<Item, Long> entry = iter.next();

                if (now - entry.getValue() > 2_000_000_000L) { // remove after 1 sec
                    iter.remove();
                }
            }

            // pause updates, if there are no entries left
            if (markTimes.isEmpty()) {
                stop();
            }
        }
    };

    final Random random = new Random();

    Thread t = new Thread(() -> {

        while (true) {
            try {
                Thread.sleep(4000);
            } catch (InterruptedException ex) {
                continue;
            }
            Platform.runLater(() -> {
                updateValue(item, random.nextInt(4));
            });
        }
    });
    t.setDaemon(true);
    t.start();
}

style.css

.table-row-cell:marked .table-cell.mark-column {
    -fx-background-color: red;
}
like image 79
fabian Avatar answered Nov 17 '22 07:11

fabian