Knut Arne Vedaa
Knut Arne Vedaa

Reputation: 15742

JavaFX TableView: copy text as rendered in cell

I want to implement copy functionality in a TableView. The text to be copied should be the actual text that is rendered in the cell, not the .toString version of the data model to be rendered, that is, it should be the .getText of the cell.

There are several ways of getting the data from a cell. However to get the rendered cell text contents, the procedure seems to be like this:

The last step is not possible due to updateItem being protected.

How can I access the rendered text of any given cell in a TableView?

Upvotes: 0

Views: 1166

Answers (1)

James_D
James_D

Reputation: 209694

The process you outline involves getting the text (i.e. data) from the view (the cell), which violates the principles behind the MVC/MVP design. From a practical perspective, it involves creating UI elements (which are expensive to create) to essentially manipulate data (which is typically much less expensive to create and process). Additionally, depending on exactly what you're doing, the UI elements may impose additional threading constraints on your code (as they are essentially single-threaded).

If you need to use the "formatting text" functionality outside of the cell, you should factor it out elsewhere and reuse it in both the "copy" functionality you need and in the cell. At a minimum, this could be done by making the "format text" functionality part of the cell factory:

import java.util.function.Function;

import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;

public class FormattingTableCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {

    private final Function<T, String> formatter ;

    public FormattingTableCellFactory(Function<T, String> formatter) {
        this.formatter = formatter ;
    }

    public FormattingTableCellFactory() {
        this(T::toString);
    }

    public final Function<T, String> getFormatter() {
        return formatter ;
    }

    @Override
    public TableCell<S,T> call(TableColumn<S,T> col) {
        return new TableCell<S,T>() {
            @Override
            protected void updateItem(T item, boolean empty) {
                super.updateItem(item, empty);
                setText(item == null ? null : formatter.apply(item));
            }
        };
    }
}

(Obviously you could extend this to produce more sophisticated cells with graphical content, etc.)

And now your copy functionality can simply apply the formatter to the data, without reference to any actual cells. Here's a SSCCE:

import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.Function;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class Main extends Application {

    private String copy(TableView<Product> table) {

        StringBuilder sb = new StringBuilder();
        for (Product p : table.getSelectionModel().getSelectedItems()) {
            List<String> data = new ArrayList<>();
            for (TableColumn<Product, ?> column : table.getColumns()) {
                Function<Object, String> formatter = ((FormattingTableCellFactory) column.getCellFactory()).getFormatter();
                data.add(formatter.apply(column.getCellObservableValue(p).getValue()));
            }
            sb.append(String.join("\t", data)).append("\n");
        }
        return sb.toString() ;
    }

    @Override
    public void start(Stage primaryStage) {
        TableView<Product> table = new TableView<>();
        table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

        table.getColumns().add(column("Product", Product::nameProperty, String::toString));
        NumberFormat currencyFormat = NumberFormat.getCurrencyInstance();
        table.getColumns().add(column("Price", Product::priceProperty, currencyFormat::format));

        Random rng = new Random();
        for (int i = 1; i <= 100; i++) {
            table.getItems().add(new Product("Product "+i, rng.nextDouble()*100));
        }

        Button copy = new Button("Copy");
        copy.setOnAction(e -> System.out.println(copy(table)));
        copy.disableProperty().bind(Bindings.isEmpty(table.getSelectionModel().getSelectedItems()));

        BorderPane root = new BorderPane(table);
        BorderPane.setAlignment(copy, Pos.CENTER);
        BorderPane.setMargin(copy, new Insets(10));
        root.setBottom(copy);
        Scene scene = new Scene(root, 600, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }


    private static <S,T> TableColumn<S,T> column(String title, Function<S,ObservableValue<T>> property, Function<T,String> formatter) {
        TableColumn<S,T> col = new TableColumn<>(title);
        col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
        col.setCellFactory(new FormattingTableCellFactory<>(formatter));
        return col ;
    }

    public static class Product {
        private final StringProperty name = new SimpleStringProperty();
        private final DoubleProperty price = new SimpleDoubleProperty() ;

        public Product(String name, double price) {
            setName(name);
            setPrice(price);
        }

        public final StringProperty nameProperty() {
            return this.name;
        }


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


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


        public final DoubleProperty priceProperty() {
            return this.price;
        }


        public final double getPrice() {
            return this.priceProperty().get();
        }


        public final void setPrice(final double price) {
            this.priceProperty().set(price);
        }



    }

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

You can get rid of the less typesafe code at the expense of less flexibility:

private final Function<String, String> defaultFormatter = Function.identity() ;
private final Function<Number, String> priceFormatter = DecimalFormat.getCurrencyInstance()::format  ;

private String copy(TableView<Product> table) {
    return table.getSelectionModel().getSelectedItems().stream().map(product -> 
        String.format("%s\t%s", 
                defaultFormatter.apply(product.getName()),
                priceFormatter.apply(product.getPrice()))
    ).collect(Collectors.joining("\n"));
}

and

    table.getColumns().add(column("Product", Product::nameProperty, defaultFormatter));
    table.getColumns().add(column("Price", Product::priceProperty, priceFormatter));

Upvotes: 3

Related Questions