简体   繁体   English

使用自定义 DatePicker TableCell JavaFX 过滤表时出现问题

[英]problem when Filtering Table with custom DatePicker TableCell JavaFX

i'm trying to make custom table cell with a DatePicker.我正在尝试使用 DatePicker 制作自定义表格单元格。 and having Filtered list in the table that filters the contents depending on radio button selection.并在表格中有过滤列表,根据单选按钮选择过滤内容。 when i select a date for a cell everything works fine.当我为单元格选择日期时,一切正常。

Screenshot 1截图 1

截屏

but when trying to filter and fire the predicate, the date looks going down to the bottom of the table.但是当试图过滤和触发谓词时,日期看起来会下降到表格的底部。 and when re-selecting the old radio button, it appears in the correct row.并且当重新选择旧的单选按钮时,它会出现在正确的行中。

Screenshot 2截图 2

截图 2

any Ideas?有任何想法吗?

my controller :我的控制器:

import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.ComboBoxTableCell;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.util.Callback;
import javafx.util.converter.IntegerStringConverter;

import java.net.URL;
import java.time.LocalDate;
import java.util.ResourceBundle;

public class Controller implements Initializable {

    @FXML
    private TableColumn<SetterGetter, String> colStatus;
    @FXML
    private TableColumn<SetterGetter, Integer> colCode;

    @FXML
    private TableColumn<SetterGetter, LocalDate> colDueDate;

    @FXML
    private TableColumn<SetterGetter, String> colName;

    @FXML
    private RadioButton rdAll;

    @FXML
    private RadioButton rdDelayed;

    @FXML
    private RadioButton rdDone;

    @FXML
    private TableView<SetterGetter> tableTasks;

    private RadioButton selectedRadioButton;

    ObservableList<SetterGetter> mainTaskList = FXCollections.observableArrayList();
    FilteredList<SetterGetter> tableTaskList = new FilteredList<>(mainTaskList, p -> true);

    @Override
    public void initialize(URL url, ResourceBundle resourceBundle) {

        selectedRadioButton = rdAll;

        colCode.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<SetterGetter, Integer>>() {
            @Override
            public void handle(TableColumn.CellEditEvent<SetterGetter, Integer> event) {
                SetterGetter row = event.getRowValue();
                row.setId(event.getNewValue());
            }
        });

        colName.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<SetterGetter, String>>() {
            @Override
            public void handle(TableColumn.CellEditEvent<SetterGetter, String> event) {
                SetterGetter row = event.getRowValue();
                row.setName(event.getNewValue());
            }
        });

        colStatus.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<SetterGetter, String>>() {
            @Override
            public void handle(TableColumn.CellEditEvent<SetterGetter, String> event) {
                SetterGetter row = event.getRowValue();
                row.setStatus(event.getNewValue().equals("منجز") ? 0 : 1);
            }
        });
        colCode.setCellValueFactory(b -> new SimpleIntegerProperty(b.getValue().getId()).asObject());
        colDueDate.setCellValueFactory(b -> b.getValue().getDate());
        colName.setCellValueFactory(b -> new SimpleStringProperty(b.getValue().getName()));
        colStatus.setCellValueFactory(b -> new SimpleStringProperty(
                b.getValue().getStatus() == 0 ? "منجز"
                        : "لم ينجز بعد")
        );

        colCode.setCellFactory(TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
        colName.setCellFactory(TextFieldTableCell.forTableColumn());
        colStatus.setCellFactory(ComboBoxTableCell.forTableColumn(FXCollections.observableArrayList(
                "منجز",
                "لم ينجز بعد")
                )
        );
        colDueDate.setCellFactory(new Callback<TableColumn<SetterGetter, LocalDate>, TableCell<SetterGetter, LocalDate>>() {
            @Override
            public TableCell<SetterGetter, LocalDate> call(TableColumn<SetterGetter, LocalDate> setterGetterStringTableColumn) {
                return new DatePickerTableCell();
            }
        });

        mainTaskList.addAll(
                new SetterGetter(0, null, null, 1),
                new SetterGetter(1, null, null, 1)
        );

        tableTasks.setItems(tableTaskList);
    }

    @FXML
    void addCheck(ActionEvent event) {
        mainTaskList.add(new SetterGetter(0,
                null,
                null,
                0)
        );
    }

    @FXML
    void selectRadioButton(ActionEvent event) {
        if (event.getSource() != selectedRadioButton) {
            RadioButton newRadio = (RadioButton) event.getSource();
            newRadio.setStyle("-fx-font-family:arial;-fx-font-size:14;-fx-font-weight:bold;-fx-border-color:red;-fx-border-radius:20;");
            selectedRadioButton.setStyle("-fx-font-family:arial;-fx-font-size:14;-fx-font-weight:bold;");
            selectedRadioButton = newRadio;
        }
        firePredicate();
    }

    private void firePredicate() {
        tableTaskList.setPredicate(p -> {
            if (selectedRadioButton.equals(rdDone) && p.getStatus() != 0)
                return false;
            else if (selectedRadioButton.equals(rdDelayed) && p.getStatus() != 1)
                return false;
            else return true;
        });
    }

}

DatePickerTableCell class: DatePickerTableCell 类:

import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.DatePicker;
import javafx.scene.control.TableCell;

import java.time.LocalDate;

public class DatePickerTableCell extends TableCell<SetterGetter, LocalDate> {
    private final DatePicker datePicker = new DatePicker();

    public DatePickerTableCell() {

        super();
    }

    @Override
    public void startEdit() {
        super.startEdit();
        setGraphic(datePicker);
        setText(null);
        datePicker.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent actionEvent) {
                commitEdit(datePicker.getValue());
            }
        });
    }

     @Override
      public void commitEdit(LocalDate s) {
          super.commitEdit(s);
          setText(s.toString());
          setGraphic(null);
          setItem(s);
     }


    @Override
    public void cancelEdit() {
        super.cancelEdit();
        setText(datePicker.getValue() == null ? null : datePicker.getValue().toString());
        setGraphic(null);
    }
}

SetterGetter class: SetterGetter 类:

import javafx.beans.property.ObjectProperty;

import java.time.LocalDate;

public class SetterGetter {
    int id;
    String name;
    ObjectProperty<LocalDate> date;
    int status;

    public SetterGetter(int id, String name, ObjectProperty<LocalDate> date, int status) {
        this.id = id;
        this.name = name;
        this.date = date;
        this.status = status;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public ObjectProperty<LocalDate> getDate() {
        return date;
    }

    public void setDate(ObjectProperty<LocalDate> date) {
        this.date = date;
    }
}

Main class :主要课程:

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception{
        Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
        primaryStage.setScene(new Scene(root, 565, 551));
        primaryStage.show();
    }


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

fxml file: fxml 文件:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="551.0" prefWidth="565.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
   <children>
      <TableView fx:id="tableTasks" editable="true" layoutX="16.0" layoutY="163.0" nodeOrientation="LEFT_TO_RIGHT" prefHeight="400.0" prefWidth="554.0" style="-fx-font-family: arial; -fx-font-size: 15;" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0">
         <columns>
            <TableColumn fx:id="colDueDate" prefWidth="112" style="-fx-font-family: arial; -fx-font-size: 15;" text="التاريخ" />
            <TableColumn fx:id="colStatus" prefWidth="112" style="-fx-font-family: arial; -fx-font-size: 15;" text="الحالة" />
            <TableColumn fx:id="colName" prefWidth="112" style="-fx-font-family: arial; -fx-font-size: 15;" text="الأسم" />
            <TableColumn fx:id="colCode" prefWidth="112" style="-fx-font-family: arial; -fx-font-size: 15;" text="الكود" />
         </columns>
         <columnResizePolicy>
            <TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
         </columnResizePolicy>
      </TableView>
      <HBox alignment="CENTER" layoutX="160.0" layoutY="27.0" spacing="15.0">
         <children>
            <RadioButton fx:id="rdDelayed" mnemonicParsing="false" onAction="#selectRadioButton" style="-fx-font-family: arial; -fx-font-size: 14; -fx-font-weight: bold;" text="لم ينجز بعد">
               <padding>
                  <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
               </padding>
               <toggleGroup>
                  <ToggleGroup fx:id="radioGroup" />
               </toggleGroup>
            </RadioButton>
            <RadioButton fx:id="rdDone" mnemonicParsing="false" onAction="#selectRadioButton" style="-fx-font-family: arial; -fx-font-size: 14; -fx-font-weight: bold;" text="منجز" toggleGroup="$radioGroup">
               <padding>
                  <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
               </padding>
            </RadioButton>
            <RadioButton fx:id="rdAll" mnemonicParsing="false" onAction="#selectRadioButton" selected="true" style="-fx-font-family: arial; -fx-font-size: 14; -fx-font-weight: bold; -fx-border-color: red; -fx-border-radius: 20 20 20 20;" text="الكل" toggleGroup="$radioGroup">
               <padding>
                  <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
               </padding>
            </RadioButton>
         </children>
      </HBox>
      <Button layoutX="473.0" layoutY="31.0" mnemonicParsing="false" onAction="#addCheck" prefHeight="34.0" prefWidth="77.0" style="-fx-font-family: arial; -fx-font-size: 18;" text="إضافة">
         <font>
            <Font name="Arial" size="17.0" />
         </font>
      </Button>
   </children>
</AnchorPane>

There are multiple things missing in your demo.您的演示中缺少很多东西。 Among that, one is already mentioned by @jewelsea in his comment.其中,@jewelsea 在他的评论中已经提到了一个。

Issue#1:问题#1:

I noticed that you have not added a commit event for the date column.我注意到您没有为日期列添加提交事件。 You need to persist the value(date) into the row object to get correct results when filtering.您需要将值(日期)保存到行对象中,以便在过滤时获得正确的结果。

Your code in the initialize method will be something like this:您在初始化方法中的代码将是这样的:

colDueDate.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<SetterGetter, LocalDate>>() {
    @Override
    public void handle(TableColumn.CellEditEvent<SetterGetter, LocalDate> event) {
        SetterGetter row = event.getRowValue();
        if(row.date==null){
            row.setDate(new SimpleObjectProperty<>());
        }
        row.date.set(event.getNewValue());
    }
});

Issue#2 :问题#2

As mentioned by @jewelsea, you need to override the updateItem() method of the TableCell when defining a custom cell.正如@jewelsea 所提到的,在定义自定义单元格时,您需要覆盖 TableCell 的 updateItem() 方法。 The code in the DatePickerTableCell will be like: DatePickerTableCell 中的代码如下:

@Override
protected void updateItem(LocalDate item, boolean empty) {
    super.updateItem(item, empty);
    if (isEditing()) {
        setText(null);
        setGraphic(datePicker);
    } else {
        setGraphic(null);
        if (item != null) {
            setText(item.toString());
        } else {
            setText(null);
        }
    }
}

The missing if-else condition in the updateItem is the main cause for your actual issue of the cell value to be in inappropriate place. updateItem 中缺少 if-else 条件是导致单元格值的实际问题出现在不适当位置的主要原因。

A VirtualFlow reuses the cells and can place it any where. VirtualFlow 重复使用这些单元,并且可以将其放置在任何地方。 It is our duty to ensure that it renders correctly when the updateItem is called.我们有责任确保在调用 updateItem 时正确呈现。 If the if-else condition is missing, you can see cell being used with wrong value.如果缺少 if-else 条件,您可以看到使用错误值的单元格。

Issue#3 : Not exactly an issue ;)问题#3不完全是一个问题;)

Use proper naming conventions for the setter/getter of observable properties in the SetterGetter class.对 SetterGetter 类中可观察属性的 setter/getter 使用正确的命名约定。

public LocalDate getDate() {
    return date.get();
}

public void setDate(LocalDate date) {
    this.date.set(date);
}

public ObjectProperty<LocalDate> dateProperty() {
    return date;
}

The base problem in the question is the visual artefact when hiding a data row that contains a value in the date column.问题中的基本问题是隐藏包含日期列中的值的数据行时的视觉伪影。 The reason for that artefact is the missing override of updateItem(S, boolean) in the custom cell implementation which leads to incorrect cell state when that cell is re-used (as already noted in comments and Sai's answer).该伪影的原因是自定义单元格实现中缺少updateItem(S, boolean)覆盖,这会在重新使用该单元格时导致不正确的单元格状态(如评论和Sai 的回答中已经指出的那样)。 The solution is to implement the method.解决方案是实现该方法。

Hammering in: updateItem is specified in Cell and none of the direct descendants of IndexedCell - that is neither ListCell nor Tree-/TableCell nor TreeCell - override it to do anything useful.锤击: updateItem 在 Cell 中指定,并且IndexedCell的任何直接后代(既不是 ListCell 也不是 Tree-/TableCell 也不是 TreeCell)都不会覆盖它来做任何有用的事情。 Which implies that each and every custom cell implementation extending those has to do it itself.这意味着扩展这些的每个自定义单元实现必须自己完成。 The api doc the updateItem method can be overridden to allow for complete customisation of the cell is not quite correct. api doc updateItem 方法可以被覆盖以允许对单元格进行完全自定义,这并不完全正确。

Problem might be: how to override it correctly?问题可能是:如何正确覆盖它? Its purpose is to update the visual state of the cell based on the item.其目的是根据项目更新单元格的视觉状态。 There are two basic rules:有两个基本规则:

  • always call super before doing custom stuff在做自定义事情之前总是调用 super
  • whatever is set if there is an item must be reversed if there is none如果有项目,则设置任何内容,如果没有项目,则必须反转

Example:例子:

protected void updateItem(T item, boolean empty) {
    super.updateItem(item, empty);

    if (empty || item == null) {
        setText(null);
        setGraphic(null);
    } else {
        setText(item.toString());
        setGraphic(getGraphic(item));
    }
}

Note: this is slightly different from the example in the api doc in actually setting an item-related graphic (without, resetting the graphic for the empty state doesn't make sense ;)注意:这与 api 文档中的示例在实际设置与项目相关的图形方面略有不同(没有,将图形重置为空状态没有意义;)

Next problem might be editing: typically, a cell meant for editing has an input control (fi a TextField) only while in editing mode.下一个问题可能是编辑:通常,用于编辑的单元格只有在编辑模式下才有输入控件(例如 TextField)。 This implies showing/hiding those when toggling editing state.这意味着在切换编辑状态时显示/隐藏那些。 The responsibility for doing so resides in the edit lifecylce methods (start-/cancel-/commitEdit).这样做的责任在于编辑生命周期方法 (start-/cancel-/commitEdit)。

All applied to a custom DatePickerTableCell below:全部应用于下面的自定义 DatePickerTableCell:

  • a DatePicker is the input control for editing and is set as the graphic DatePicker 是编辑的输入控件,设置为图形
  • editing is committed when receiving an action from the picker: note that the handler is set once in the constructor当从选择器接收到一个动作时,编辑被提交:注意处理程序在构造函数中设置一次
  • updateItem is implemented to configure both input control and text, independent of editing state updateItem 被实现来配置输入控件和文本,独立于编辑状态
  • the showing/hiding is controlled exclusively in the edit lifecycle methods by setting the cell's contentDisplay as needed;通过根据需要设置单元格的 contentDisplay 来专门在编辑生命周期方法中控制显示/隐藏; note that we back out off startEdit if super didn't switch into editing mode请注意,如果 super 没有切换到编辑模式,我们会退出 startEdit

Caveat: this strict separation of concerns (editing dependent visuals vs item dependent visuals) is possible since fx17 which fixes a bunch of bugs with cell editing state after cell re-use (see the release notes , fi JDK-8264127 for ListCell, JDK-8265206 for Tree-/TableCell, JDK-8265210 for TreeCell and related)警告:这种严格的关注点分离(编辑依赖的视觉效果与项目依赖的视觉效果)是可能的,因为 fx17 修复了单元格重复使用后单元格编辑状态的一堆错误(请参阅 发行说明,对于 ListCell, JDK -8264127 8265206用于 Tree-/TableCell, JDK-8265210用于 TreeCell 及相关)

An alternative custom DatePickerTableCell:另一种自定义 DatePickerTableCell:

public static class DatePickerTableCellEx1<S> extends TableCell<S, LocalDate> {
    private final DatePicker datePicker = new DatePicker();

    public DatePickerTableCellEx1() {
        datePicker.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent actionEvent) {
                commitEdit(datePicker.getValue());
            }
        });
        setGraphic(datePicker);
        setContentDisplay(ContentDisplay.TEXT_ONLY);
    }

    @Override
    protected void updateItem(LocalDate item, boolean empty) {
        super.updateItem(item, empty);
        if (empty || item == null) {
            setText(null);
            datePicker.setValue(null);
        } else {
            setText(datePicker.getConverter().toString(item));
            datePicker.setValue(item);
        }
    }

    @Override
    public void startEdit() {
        super.startEdit();
        if (!isEditing()) return;
        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
    }

    @Override
    public void commitEdit(LocalDate s) {
        super.commitEdit(s);
        setContentDisplay(ContentDisplay.TEXT_ONLY);
    }

    @Override
    public void cancelEdit() {
        super.cancelEdit();
        setContentDisplay(ContentDisplay.TEXT_ONLY);
    }
}

(Mostly unrelated) note on api design around Properties: (大部分不相关)关于属性的 api 设计的注释:

Typically, property valued fields should be not null and immutable通常,属性值字段不应为空且不可变

 // the field
 Property<Music> music; 

 // API
 Property<Music> gimmeTheMusic() {

 // usage
 Property<Music> music = radio.gimmeTheMusic();
 assertNotNull(music);
 music.addListener(...  // turn radio down/up);

 // __DO NOT__: it will break the usage
 void change(Property<Music> music) {
     this.music = music;
 }

With that in place, TableColumn's default commit handler will work as expected and there's no need to install a custom commit handler.有了这些,TableColumn 的默认提交处理程序将按预期工作,无需安装自定义提交处理程序。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM