1

I have a request to display a string in various colours in a table cell, that is one portion of a string in one colour and the rest in another colour (either the background or the text). I have found an article on changing the cell background colour, but not a portion of a cell. That is close to the requirement, but don't meet the requirement.

The only possible solution, I can think of, is to use the Text type which can be set with various colours after splitting a string into two parts. But, how to use the Text type data with the TableView setup as the following?

aColumn.setCellValueFactory(p -> new SimpleStringProperty(...) );
...
aTalbeView.setItems(FXcollections.observableArrayList(...));

I am still new to JavaFX. Is it doable? If so, how shall I approach a solution?

A mock up table is attached below.
enter image description here

vic
  • 2,548
  • 9
  • 44
  • 74
  • 1
    You need to set a `cellFactory` as well as a `cellValueFactory`. – James_D Oct 12 '22 at 17:46
  • 1
    With visual questions like this, it is always good to provide a mock-up image of what the table, with coloring for cells and text, should look like, then you stand a better chance of receiving an answer that will more closely approximate what you are attempting to achieve. – jewelsea Oct 12 '22 at 18:28
  • @jewelsea You are 100% right. I didn't know that I could attach an image file to my question on Stackoverflow. Just add one. – vic Oct 12 '22 at 22:18

2 Answers2

3

The cellValueFactory is used to tell the cell what data to display. To tell the cell how to display its data, use a cellFactory. The two are more or less independent.

So you can do

aColumn.setCellValueFactory(p -> new SimpleStringProperty(...));

and then something like:

aColumn.setCellFactory(tc -> new TableCell<>() {
    private final String[] palette = new String[] {
        "#1B9E77", "#D95F02", "#7570B3", "#E7298A",
        "#66A61E", "#E6AB02", "#A6761D", "#666666" };
    private TextFlow flow = new TextFlow();

    @Override
    protected void updateItem(String item, boolean empty) {
        super.updateItem(item, empty);
        if (empty || item == null) {
            setGraphic(null);
        } else {
            flow.getChildren().clear();
            int i = 0 ;
            for (String word : item.split("\\s")) {
                Text text = new Text(word);
                text.setFill(Color.web(palette[i++ % palette.length]);
                flow.getChildren().add(text);
                flow.getChildren().add(new Text(" "));
            }
            setGraphic(flow);
        }
    }
});

This assumes each cell has multiple words (separated by whitespace) and colors each word a different color. You can implement the different colors any way you like; this shows the basic idea.

vic
  • 2,548
  • 9
  • 44
  • 74
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Thanks very much for your code sample. I will try out the code tomorrow. – vic Oct 12 '22 at 22:17
  • Just wondering what version of org.openjfx do you use for the code. I assume that javafx.scene.text.TextFlow is the class in your code. If so, it doesn't have those methods in the release version 19. – vic Oct 13 '22 at 20:17
  • @vic Yes, sorry. That’ll teach me to post code without testing it (though I will point out that if you want people to post answers they’ve actually run, you make that a lot more likely if you provide a [mre] for them to work from). It should be `flow.getChildren()`, of course. – James_D Oct 13 '22 at 20:32
  • Thanks for your info. I get it compiled. I get an idea on how to chang the text colour in a table. There is a bug in the code though as only green colour is applied to the text. I will try to fix it later. – vic Oct 13 '22 at 21:23
  • @vic Fixed. See edit. – James_D Oct 13 '22 at 22:29
  • Thanks. I edit the code to put space back into the text. And thanks very much for your help. – vic Oct 13 '22 at 23:09
  • Just notice a side effect of the code. A cell is much higher than without the code. – vic Oct 14 '22 at 00:38
3

The approach used in this answer

  • An additional range parameter is added to the backing model to indicate the highlight range for text in the cell.

  • The cellValueFactory uses a binding statement to allow the cell to respond to updates to either the text in the cell or the highlight range.

  • Labels within an HBox are used for the cell graphic rather than a TextFlow as labels have more styling options (e.g. for text background) than text nodes in TextFlow.

  • Using multiple labels within the cells does change some of the eliding behavior of the cell when not enough room is available in the column to include all text, this could be customized by setting properties on the HBOX or label to configure this behavior how you want.

  • CSS stylesheet for styling is included in the code but could be extracted to a separate stylesheet if desired.

  • I didn't thoroughly test the solution, so there may be logic errors around some of the boundary conditions.

Screenshots

Highlighted a subset of text within a cell in a non-selected row:

enter image description here

Highlighted a subset of text within a cell in a selected row:

enter image description here

Example code

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class HighlightedTextTableViewer extends Application {

    private static final String CSS_DATA_URL = "data:text/css,";
    private static final String HIGHLIGHTABLE_LABEL_CSS = CSS_DATA_URL + // language=CSS
            """
            .highlightable {
                -fx-font-family: monospace; 
                -fx-font-weight: bold;
            }
            
            .highlight {
                 -fx-background-color: cornflowerblue;
                 -fx-text-fill: white;
            }
            """;

    private static final String HIGHLIGHTABLE_STYLE_CLASS = "highlightable";
    private static final String HIGHLIGHTED_STYLE_CLASS = "highlight";

    @Override
    public void start(Stage stage) {
        TableView<Field> table = createTable();
        populateTable(table);

        VBox layout = new VBox(
                10,
                table
        );
        layout.setPadding(new Insets(10));
        layout.setPrefHeight(100);

        stage.setScene(new Scene(layout));
        stage.show();
    }

    private TableView<Field> createTable() {
        TableView<Field> table = new TableView<>();

        TableColumn<Field, String> nameColumn = new TableColumn<>("Name");
        nameColumn.setCellValueFactory(
                p -> p.getValue().nameProperty()
        );

        TableColumn<Field, Field> valueColumn = new TableColumn<>("Value");
        valueColumn.setCellValueFactory(
                p -> Bindings.createObjectBinding(
                        p::getValue,
                        p.getValue().valueProperty(), p.getValue().highlightRangeProperty()
                )
        );
        valueColumn.setCellFactory(param -> new HighlightableTextCell());

        //noinspection unchecked
        table.getColumns().setAll(nameColumn, valueColumn);

        return table;
    }

    public static class HighlightableTextCell extends TableCell<Field, Field> {
        protected void updateItem(Field item, boolean empty) {
            super.updateItem(item, empty);

            if (item == null || empty || item.getValue() == null) {
                setGraphic(null);
            } else {
                setGraphic(constructTextBox(item));
            }
        }

        private Node constructTextBox(Field item) {
            HBox textBox = new HBox();
            textBox.getStylesheets().setAll(HIGHLIGHTABLE_LABEL_CSS);
            textBox.getStyleClass().add(HIGHLIGHTABLE_STYLE_CLASS);

            int from = item.getHighlightRange() != null ? item.getHighlightRange().from() : -1;
            int valueLen = item.getValue() != null ? item.getValue().length() : -1;
            int to = item.getHighlightRange() != null ? item.getHighlightRange().to() : -1;

            if (item.highlightRangeProperty() == null
                    || from >= to
                    || from > valueLen
            ) { // no highlight specified or no highlight in range.
                textBox.getChildren().add(
                        createStyledLabel(
                                item.getValue()
                        )
                );
            } else {
                textBox.getChildren().add(
                        createStyledLabel(
                                item.getValue().substring(
                                        0,
                                        from
                                )
                        )
                );

                if (from >= valueLen) {
                    return textBox;
                }

                textBox.getChildren().add(
                        createStyledLabel(
                                item.getValue().substring(
                                        from,
                                        Math.min(valueLen, to)
                                ), HIGHLIGHTED_STYLE_CLASS
                        )
                );

                if (to >= valueLen) {
                    return textBox;
                }

                textBox.getChildren().add(
                        createStyledLabel(
                                item.getValue().substring(
                                        to
                                )
                        )
                );
            }

            return textBox;
        }

        private Label createStyledLabel(String value, String... styleClasses) {
            Label label = new Label(value);
            label.getStyleClass().setAll(styleClasses);

            return label;
        }
    }

    private void populateTable(TableView<Field> table) {
        table.getItems().addAll(
                new Field("Dragon", "93 6d 6d da", null),
                new Field("Rainbow", "0c fb ff 1c", new Range(3, 8))
        );
    }

}

class Field {
    private final StringProperty name;
    private final StringProperty value;
    private final ObjectProperty<Range> highlightRange;

    public Field(String name, String value, Range highlightRange) {
        this.name = new SimpleStringProperty(name);
        this.value = new SimpleStringProperty(value);
        this.highlightRange = new SimpleObjectProperty<>(highlightRange);
    }

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

    public StringProperty nameProperty() {
        return name;
    }

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

    public String getValue() {
        return value.get();
    }

    public StringProperty valueProperty() {
        return value;
    }

    public void setValue(String value) {
        this.value.set(value);
    }

    public Range getHighlightRange() {
        return highlightRange.get();
    }

    public ObjectProperty<Range> highlightRangeProperty() {
        return highlightRange;
    }

    public void setHighlightRange(Range highlightRange) {
        this.highlightRange.set(highlightRange);
    }
}

record Range(int from, int to) {}

Alternative using TextField

An alternative to the HBox for displaying highlighted text would be to use a TextField (non-editable), which allows a selection to be set (via APIs on the text field), however, I did not attempt a solution with a TextField approach. A TextField may allow a user to drag the mouse to select text (perhaps could be disabled if desired by making the field mouse transparent).

Related Questions (uses TextFlow)

jewelsea
  • 150,031
  • 14
  • 366
  • 406
  • Thanks for your input. I didn't refresh my browser to see your post before I made another post. I need to take time to study your code. I use FXML along with Java for the TableView. So, I don't know whether your code is applicable to me or not. And, I have customized TableView related classes to whole data even if it is very long (the TableView will truncate data if it is longer than what it can handle). – vic Oct 14 '22 at 23:26