Javafx: Как я могу заставить TableCell Edit возвращать double вместо строки, а шрифт меняет цвет на основе условия?

У меня есть класс объектов Trade с

public class Trade {
    private DoubleProperty price;
    private ReadOnlyBooleanWrapper caution;

    public Trade(double price){
        this.price = new SimpleDoubleProperty(price);
        this.caution = new ReadOnlyBooleanWrapper();
        this.caution.bind(this.volume.greaterThan(0));
    }   

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

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

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

В моем классе Controller у меня есть следующие TableView и TableColumn

Проблема двояка:

  • В столбце цены и цены принимается double. Но код EditingDoubleCell ниже возвращает только String. Как я могу заставить его возвращать double и все String введенный вами пользователь будет проигнорирован?
  • Вторая функция, которую я хотел бы иметь, заключается в следующем: шрифт внутри ячейки столбца Price (, говорящий о той же самой ценовой ячейке) изменит свой цвет на синий, когда caution свойство true и красное, если свойство caution является ложным?

public class EditingDoubleCell extends TableCell<Trade,String>{

    private TextField textField;

    public EditingDoubleCell() {
    }

    @Override
    public void startEdit() {
        if (!isEmpty()) {
            super.startEdit();
            createTextField();
            setText(null);
            setGraphic(textField);
            textField.requestFocus();
            //textField.selectAll();
        }
    }

    @Override
    public void cancelEdit() {
        super.cancelEdit();
        setText((String) getItem());
        setGraphic(null);
    }


    @Override
    public void updateItem(String item, boolean empty) {
        super.updateItem(item, empty);

        if (empty) {
            setText(null);
            setGraphic(null);
        } else {
            if (isEditing()) {
                if (textField != null) {
                    textField.setText(getString());

                }
                setText(null);
                setGraphic(textField);
            } else {
                setText(getString());
                setGraphic(null);
            }
        }
    }

    private String getString() {
        return getItem() == null ? "" : getItem().toString();
    }

    private void createTextField(){

        Locale locale  = new Locale("en", "UK");
        String pattern = "###,###.###";
        DecimalFormat df = (DecimalFormat) NumberFormat.getNumberInstance(locale);
        df.applyPattern(pattern);
        //String format = df.format(123456789.123);
        //System.out.println(format);

        //NumberFormat nf = NumberFormat.getIntegerInstance();        
        textField = new TextField();

        // add filter to allow for typing only integer
        textField.setTextFormatter( new TextFormatter<>( c ->
        {
            if (c.getControlNewText().isEmpty()) {
                return c;
            }
            ParsePosition parsePosition = new ParsePosition( 0 );
            Object object = df.parse( c.getControlNewText(), parsePosition );

            if ( object == null || parsePosition.getIndex() < c.getControlNewText().length() )
            {
                return null;
            }
            else
            {
                return c;
            }
        } ) );

        textField.setText( getString() );

        textField.setMinWidth( this.getWidth() - this.getGraphicTextGap() * 2 );

        // commit on Enter
        textField.setOnAction( new EventHandler<ActionEvent>()
        {
            @Override
            public void handle( ActionEvent event )
            {
                commitEdit( textField.getText() );
            }
        } );

        textField.focusedProperty().addListener( new ChangeListener<Boolean>()
        {
            @Override
            public void changed( ObservableValue<? extends Boolean> arg0,
                    Boolean arg1, Boolean arg2 )
            {
                if ( !arg2 )
                {
                    commitEdit( textField.getText() );
                }
            }
        } );

    }
}

Ответ 1

Для первой части проблемы вы должны создать TextFormatter как TextFormatter<Double>. Это делает valueProperty TextFormatter в Property<Double>, поэтому вы можете совершать свои изменения, вызывая getValue() в форматировании. Вам нужно указать StringConverter<Double>, чтобы он знал, как перейти от текста к Double, и наоборот. Итак, это выглядит так:

        StringConverter<Double> converter = new StringConverter<Double>() {

            @Override
            public String toString(Double number) {
                return df.format(number);
            }

            @Override
            public Double fromString(String string) {
                try {
                    double value = df.parse(string).doubleValue() ;
                    return value;
                } catch (ParseException e) {
                    e.printStackTrace();
                    return 0.0 ;
                }
            }

        };

        textFormatter = new TextFormatter<>(converter,  0.0, c -> {
            if (partialInputPattern.matcher(c.getControlNewText()).matches()) {
                return c ;
            } else {
                return null ;
            }
        }) ;

Я изменил фильтр здесь, потому что ваш фильтр соответствовал только "полному" вводу. Поскольку фильтр применяется к каждому отдельному редактированию, вы должны разрешить "частичный" ввод, например "100,". Фильтр, который у вас был, не позволит этого (например). Фильтр в версии здесь использует регулярное выражение: вы можете возиться с этим, чтобы получить его правильно, но я использую

Pattern partialInputPattern = Pattern.compile(""[-+]?[,0-9]*(\\.[0-9]*)?");

что довольно мягко с тем, что позволяет.

Теперь вместо того, чтобы прямое редактирование, когда пользователь нажимает Enter, просто зафиксируйте редактирование при изменении значения текстового форматирования:

    // commit on Enter
    textFormatter.valueProperty().addListener((obs, oldValue, newValue) -> {
        commitEdit(newValue);
    });

Весь класс ячейки теперь выглядит как

public static class EditingDoubleCell extends TableCell<Trade,Double>{

    private TextField textField;
    private TextFormatter<Double> textFormatter ;

    private DecimalFormat df ;

    public EditingDoubleCell(String...styleClasses) {
        Locale locale  = new Locale("en", "UK");
        String pattern = "###,###.###";
        df = (DecimalFormat) NumberFormat.getNumberInstance(locale);
        df.applyPattern(pattern);

        getStyleClass().addAll(styleClasses);
    }

    @Override
    public void startEdit() {
        if (!isEmpty()) {
            super.startEdit();
            createTextField();
            setText(null);
            setGraphic(textField);
            textField.requestFocus();
        }
    }

    @Override
    public void cancelEdit() {
        super.cancelEdit();
        setText(df.format(getItem()));
        setGraphic(null);
    }


    @Override
    public void updateItem(Double item, boolean empty) {
        super.updateItem(item, empty);

        if (empty) {
            setText(null);
            setGraphic(null);
        } else {
            if (isEditing()) {
                if (textField != null) {
                    textField.setText(getString());

                }
                setText(null);
                setGraphic(textField);
            } else {
                setText(getString());
                setGraphic(null);
            }
        }
    }

    private String getString() {
        return getItem() == null ? "" : df.format(getItem());
    }

    private void createTextField(){

        textField = new TextField();

        StringConverter<Double> converter = new StringConverter<Double>() {

            @Override
            public String toString(Double number) {
                return df.format(number);
            }

            @Override
            public Double fromString(String string) {
                try {
                    double value = df.parse(string).doubleValue() ;
                    return value;
                } catch (ParseException e) {
                    e.printStackTrace();
                    return 0.0 ;
                }
            }

        };

        textFormatter = new TextFormatter<>(converter,  0.0, c ->
        {
            if (c.getControlNewText().isEmpty()) {
                return c;
            }
            ParsePosition parsePosition = new ParsePosition( 0 );
            Object object = df.parse( c.getControlNewText(), parsePosition );

            if ( object == null || parsePosition.getIndex() < c.getControlNewText().length() )
            {
                return null;
            }
            else
            {
                return c;
            }
        } ) ;

        // add filter to allow for typing only integer
        textField.setTextFormatter( textFormatter);

        textField.setText( getString() );

        textField.setMinWidth( this.getWidth() - this.getGraphicTextGap() * 2 );

        // commit on Enter
        textFormatter.valueProperty().addListener((obs, oldValue, newValue) -> {
            commitEdit(newValue);
        });
    }
}

(Я добавил параметр конструктора, чтобы он работал с решением вашего второго вопроса.)

Вторая часть ответит в другом месте, но я бы просто создал rowFactory для вашей таблицы, которая устанавливает псевдослот CSS на основе состояния свойства предостережения:

PseudoClass caution = PseudoClass.getPseudoClass("caution");

table.setRowFactory(tv -> {
    TableRow<Trade> row = new TableRow<>();

    ChangeListener<Boolean> cautionListener = (obs, wasCaution, isNowCaution) -> 
        row.pseudoClassStateChanged(caution, isNowCaution);

    row.itemProperty().addListener((obs, oldTrade, newTrade) -> {
        if (oldTrade != null) {
            oldTrade.cautionProperty().removeListener(cautionListener);
        }
        if (newTrade == null) {
            row.pseudoClassStateChanged(caution, false);
        } else {
            row.pseudoClassStateChanged(caution, newTrade.isCaution());
            newTrade.cautionProperty().addListener(cautionListener);
        }
    });

    return row ;
});

Затем просто установите класс стиля в ячейке, в которую вы хотите изменить стиль (например, добавьте класс стиля "price-cell" к EditingDoubleCell, который вы определили). Затем вы можете просто использовать таблицу стилей CSS, чтобы изменить стиль по мере необходимости, например.

.table-row-cell .price-cell {
    -fx-text-fill: red ;
}

.table-row-cell:caution .price-cell {
    -fx-text-fill: blue ;
}

сделает текст красным для ценовых ячеек в строках, которые не имеют caution, и сделают его синим в строках, которые делают.

Вот полный SSCCE:

import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.function.Function;
import java.util.regex.Pattern;

import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.StringConverter;

public class TradeTable extends Application {

    private final Random rng = new Random();

    @Override
    public void start(Stage primaryStage) {
        TableView<Trade> table = new TableView<>();
        table.setEditable(true);
        TableColumn<Trade, Integer> volumeCol = column("Volume", trade -> trade.volumeProperty().asObject());
        TableColumn<Trade, Double> priceCol = column("Price", trade -> trade.priceProperty().asObject());

        priceCol.setCellFactory(col -> new EditingDoubleCell("price-cell"));

        table.getColumns().add(volumeCol);
        table.getColumns().add(priceCol);

        PseudoClass caution = PseudoClass.getPseudoClass("caution");

        table.setRowFactory(tv -> {
            TableRow<Trade> row = new TableRow<>();

            ChangeListener<Boolean> cautionListener = (obs, wasCaution, isNowCaution) -> 
                row.pseudoClassStateChanged(caution, isNowCaution);

            row.itemProperty().addListener((obs, oldTrade, newTrade) -> {
                if (oldTrade != null) {
                    oldTrade.cautionProperty().removeListener(cautionListener);
                }
                if (newTrade == null) {
                    row.pseudoClassStateChanged(caution, false);
                } else {
                    row.pseudoClassStateChanged(caution, newTrade.isCaution());
                    newTrade.cautionProperty().addListener(cautionListener);
                }
            });

            return row ;
        });

        table.getItems().addAll(createRandomData());

        Button button = new Button("Change Data");
        button.setOnAction(e -> table.getItems().forEach(trade -> {
            if (rng.nextDouble() < 0.5) {
                trade.setVolume(0);
            } else {
                trade.setVolume(rng.nextInt(10000));
            }
            trade.setPrice(rng.nextDouble() * 1000);
        }));
        BorderPane.setAlignment(button, Pos.CENTER);
        BorderPane.setMargin(button, new Insets(10));

        BorderPane root = new BorderPane(table, null, null, button, null);
        Scene scene = new Scene(root, 600, 600);
        scene.getStylesheets().add("trade-table.css");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private  List<Trade> createRandomData() {
        List<Trade> trades = new ArrayList<>(50);
        for (int i = 0 ; i < 50; i++) {
            int volume = rng.nextDouble() < 0.5 ? 0 : rng.nextInt(10000) ;
            double price = rng.nextDouble() * 10000 ;
            trades.add(new Trade(price, volume));
        }
        return trades ;
    }

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

    public static class Trade {
        private DoubleProperty price;
        private IntegerProperty volume ;
        private ReadOnlyBooleanWrapper caution;

        public Trade(double price, int volume){
            this.price = new SimpleDoubleProperty(price);
            this.volume = new SimpleIntegerProperty(volume);
            this.caution = new ReadOnlyBooleanWrapper();
            this.caution.bind(this.volume.greaterThan(0));
        }   

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

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

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

        public final IntegerProperty volumeProperty() {
            return this.volume;
        }

        public final int getVolume() {
            return this.volumeProperty().get();
        }

        public final void setVolume(final int volume) {
            this.volumeProperty().set(volume);
        }

        public final ReadOnlyBooleanProperty cautionProperty() {
            return this.caution.getReadOnlyProperty();
        }

        public final boolean isCaution() {
            return this.cautionProperty().get();
        }


    }

    public static class EditingDoubleCell extends TableCell<Trade,Double>{

        private TextField textField;
        private TextFormatter<Double> textFormatter ;

        private Pattern partialInputPattern = Pattern.compile(
                "[-+]?[,0-9]*(\\.[0-9]*)?");

        private DecimalFormat df ;

        public EditingDoubleCell(String...styleClasses) {
            Locale locale  = new Locale("en", "UK");
            String pattern = "###,###.###";
            df = (DecimalFormat) NumberFormat.getNumberInstance(locale);
            df.applyPattern(pattern);

            getStyleClass().addAll(styleClasses);
        }

        @Override
        public void startEdit() {
            if (!isEmpty()) {
                super.startEdit();
                createTextField();
                setText(null);
                setGraphic(textField);
                textField.requestFocus();
            }
        }

        @Override
        public void cancelEdit() {
            super.cancelEdit();
            setText(df.format(getItem()));
            setGraphic(null);
        }


        @Override
        public void updateItem(Double item, boolean empty) {
            super.updateItem(item, empty);

            if (empty) {
                setText(null);
                setGraphic(null);
            } else {
                if (isEditing()) {
                    if (textField != null) {
                        textField.setText(getString());

                    }
                    setText(null);
                    setGraphic(textField);
                } else {
                    setText(getString());
                    setGraphic(null);
                }
            }
        }

        private String getString() {
            return getItem() == null ? "" : df.format(getItem());
        }

        private void createTextField(){

            textField = new TextField();

            StringConverter<Double> converter = new StringConverter<Double>() {

                @Override
                public String toString(Double number) {
                    return df.format(number);
                }

                @Override
                public Double fromString(String string) {
                    try {
                        double value = df.parse(string).doubleValue() ;
                        return value;
                    } catch (ParseException e) {
                        e.printStackTrace();
                        return 0.0 ;
                    }
                }

            };

            textFormatter = new TextFormatter<>(converter,  0.0, c -> {
                if (partialInputPattern.matcher(c.getControlNewText()).matches()) {
                    return c ;
                } else {
                    return null ;
                }
            }) ;

            // add filter to allow for typing only integer
            textField.setTextFormatter( textFormatter);

            textField.setText( getString() );

            textField.setMinWidth( this.getWidth() - this.getGraphicTextGap() * 2 );

            // commit on Enter
            textFormatter.valueProperty().addListener((obs, oldValue, newValue) -> {
                commitEdit(newValue);
            });
        }
    }

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

С CSS-кодом выше в trade-table.css.

Ответ 2

первая часть вопроса: вы можете попробовать следующий класс (он работал у меня):

     public class EditingDoubleCell extends TableCell<Trade, Double> {

        private TextField textField;

        public EditingDoubleCell() {
            textField = new TextField();
            textField.setOnAction(e -> commitEdit(Double.valueOf(textField.getText())));
        }

        @Override
        public void startEdit() {
            if (!isEmpty()) {
                super.startEdit();
                setText(null);
                setGraphic(textField);
                textField.requestFocus();

            }
        }

        @Override
        public void cancelEdit() {
            super.cancelEdit();
            setText(getString());
            setGraphic(null);
        }

        @Override
        public void commitEdit(Double newValue) {
            super.commitEdit(newValue);
        }

        @Override
        public void updateItem(Double item, boolean empty) {
            super.updateItem(item, empty);

            if (empty) {
                setText(null);
                setGraphic(null);
            } else {

                Locale locale = new Locale("en", "UK");
                String pattern = "###,###.###";
                DecimalFormat df = (DecimalFormat) NumberFormat.getNumberInstance(locale);
                df.applyPattern(pattern);
                String s = df.format(getItem());
                setText(s);
                setGraphic(null);
              // set font of Price cell to a color
            TableRow<Trade> row = getTableRow();
            if (row.getItem().getCaution()) {
                setStyle("-fx-background-color:blue;");
            } else {
                setStyle("-fx-background-color: red;");
                    }
            }
        }

        private String getString() {
            return getItem() == null ? "" : getItem().toString();
        }

    }

вторая часть вопроса: просто вызовите setcellfactory(...) для столбца предостережения, и вы должны переопределить метод updateItem(...):

 cautionCol.setCellFactory(column -> new TableCell<Trade, Boolean>() {

        @Override
        protected void updateItem(Boolean item, boolean empty) {
            super.updateItem(item, empty);
            if (item == null || empty) {
                setText(null);
            } else {
                setText(String.valueOf(item));
                //TableRow<Trade> row = getTableRow();
                if (item) {
                    setStyle("-fx-background-color:blue;");
                } else {
                    setStyle("-fx-background-color: red;");
                }

            }
        }

    });

Ответ 3

У меня была аналогичная проблема, я сделал следующее:

SimpleDoubleProperty price = new SimpleDoubleProperty();
price.setValue(Double.parseDouble(EditingDoubleCell().getString()));
ObservableValue<Double> g = price.asObject();
return g;

Этот метод предполагает, что вы можете проанализировать свою строку в double. Работает для меня, скажите, помогло ли это:)