JavaFX中的可编辑表

Editable Tables in JavaFX

在听到我的同事不断谈论我工作的公司很久以前设计的可编辑网格之后,我决定做点什么使他为我感到骄傲。 因此,我编写了一个应用程序,该应用程序可以在JavaFX中创建可编辑的TableView,如果需要的话,可以用来无脑地输入数据。 不幸的是,JavaFX并不想使它变得简单好用,我甚至在编写示例代码时在JavaFX代码中发现了一个错误。 不用担心,我会在其中添加一些说明甚至照片,以免您迷路。

Image title

如果您需要一些背景知识,请查阅使用JavaFX制作应用程序。

让我们从设置表的fxml开始。 其余的fxml代码可在我的GitHub上找到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<TableView fx:id="table" prefHeight="255.0" prefWidth="501.0">
  <columns>
    <TableColumn prefWidth="98.0" text="First Name">
      <cellValueFactory>
        <PropertyValueFactory property="firstName" />
      </cellValueFactory>
    </TableColumn>
        <TableColumn prefWidth="111.0" text="Surname">
          <cellValueFactory>
            <PropertyValueFactory property="surname" />
      </cellValueFactory>
    </TableColumn>
     <TableColumn fx:id="dateOfBirthColumn" prefWidth="99.0" text="Date of Birth">
      <cellValueFactory>
         <PropertyValueFactory property="dateOfBirth" />
      </cellValueFactory>
    </TableColumn>
          <TableColumn prefWidth="106.0" text="Occupation">
          <cellValueFactory>
            <PropertyValueFactory property="occupation" />
      </cellValueFactory>
    </TableColumn>
          <TableColumn fx:id="salaryColumn" prefWidth="84.0" text="Salary">
          <cellValueFactory>
            <PropertyValueFactory property="salary" />
      </cellValueFactory>
    </TableColumn>
  </columns>
</TableView>

通过快速查看代码,您可以看到一些简单的东西。 TableView已定义有一些列,每个列都有名称。 有些具有fx:id的名称将在以后的控制器中使用。 我们需要注意的一个重要功能是在每个TableColumn标记内定义的cellValueFactory和PropertyValueFactory。 这些映射到表的数据将显示在模型上的模型,其中PropertyValueFactory标记中定义的属性与模型的字段匹配。 这将在下面变得更加清楚。

现在,表格的基本布局已构建完毕,让我们进行设置并表示将在其中显示的数据。 我们需要做的第一件事是创建一个对象/模型,该对象/模型将代表表中的每一行,并且每一列将与模型中的属性相匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class PersonTableData {

    private SimpleStringProperty firstName;
    private SimpleStringProperty surname;
    private SimpleObjectProperty < Date > dateOfBirth;
    private SimpleStringProperty occupation;
    private SimpleDoubleProperty salary;

    // added to create the model from the Person object, which might be data retrieved from a database
    public PersonTableData(Person person) {
        this.firstName = new SimpleStringProperty(person.getFirstName());
        this.surname = new SimpleStringProperty(person.getSurname());
        this.dateOfBirth = new SimpleObjectProperty < Date > (
            person.getDateOfBirth());
        this.occupation = new SimpleStringProperty(person.getOccupation());
        this.salary = new SimpleDoubleProperty(person.getSalary());
    }

    public PersonTableData(final String firstName, final String surname,
        final Date dateOfBirth, final String occupation,
        final double salary) {
        this.firstName = new SimpleStringProperty(firstName);
        this.surname = new SimpleStringProperty(surname);
        this.dateOfBirth = new SimpleObjectProperty < Date > (dateOfBirth);
        this.occupation = new SimpleStringProperty(occupation);
        this.salary = new SimpleDoubleProperty(salary);
    }

    public String getFirstName() {
        return firstName.get();
    }

    public void setFirstName(final String firstName) {
        this.firstName.set(firstName);
    }

    public String getSurname() {
        return surname.get();
    }

    public void setSurname(final String surname) {
        this.surname.set(surname);
    }

    public Date getDateOfBirth() {
        return dateOfBirth.get();
    }

    public void setDateOfBirth(final Date dateOfBirth) {
        this.dateOfBirth.set(dateOfBirth);
    }

    public String getOccupation() {
        return occupation.get();
    }

    public void setOccupation(final String occupation) {
        this.occupation.set(occupation);
    }

    public double getSalary() {
        return salary.get();
    }

    public void setSalary(final double salary) {
        this.salary.set(salary);
    }

}

需要注意的第一件事是字段的类型通常不是您期望的,例如String或double。 相反,它们被定义为属性。 这不是100%必需使用的,可以使用String代替SimpleStringProperty或Date代替SimpleObjectProperty 。 该属性的作用是包装它保存的值,该值可以通过get()或set()访问,并侦听在该值上触发的事件。 这意味着您可以添加侦听器或将其绑定到其他属性,而使用简单的String则无法做到这一点。

必须使用100%的getter才能在表中显示值,而setter是可选的,并且仅在您要进行某些设置(例如编辑值)时才需要。 还要注意,get()和set()方法访问属性的包装值,而不是返回或更改实际属性。 模型内部的属性名称无关紧要,但是您还记得我们之前在fxml的PropertyValueFactory标记中定义名称的位置吗? 我们需要在这些标记中定义的名称与模型中的getter和setter相匹配。 如果它们不匹配,则不会崩溃或发生任何事情,但是您不会在不匹配的列中显示任何数据。

Image title

为了测试这一点,如果您更改了其中一个字段的PropertyValueFactory名称并运行了代码,它将不会在该列中显示任何内容。 但是,如果您随后更改了吸气剂的名称,同时保持模型属性的名称不变,则它将正确显示。

也可以在Java代码中而不是fxml中将列添加到TableView中。 下面的代码段是如何将dateOfBirth列添加到表中的示例。

1
2
3
4
5
6
7
8
9
10
private void createColumnManually() {
    TableColumn < PersonTableData, Date > dateOfBirthColumn = new TableColumn < > (
       "Date of Birth");
    dateOfBirthColumn.setCellValueFactory(person - > {
        SimpleObjectProperty < Date > property = new SimpleObjectProperty < > ();
        property.setValue(person.getValue().getDateOfBirth());
        return property;
    });
    table.getColumns().add(2, dateOfBirthColumn);
}

现在已经在fxml中定义了该表,并且已经成功创建了表示数据的模型。 现在是时候进入主要功能并使表格可编辑了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
public class TableAppController implements Initializable {

    @FXML
    private TableView < PersonTableData > table;

    @FXML
    private TextField firstNameTextField;

    @FXML
    private TextField surnameTextField;

    @FXML
    private TextField dateOfBirthTextField;

    @FXML
    private TextField occupationTextField;

    @FXML
    private TextField salaryTextField;

    @FXML
    private Button submitButton;

    private ObservableList < PersonTableData > data = FXCollections
        .observableArrayList();

    private static final String DATE_PATTERN ="dd/MM/yyyy";

    private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat(
        DATE_PATTERN);

    @FXML
    private TableColumn < PersonTableData, Date > dateOfBirthColumn;

    @FXML
    private TableColumn < PersonTableData, Double > salaryColumn;

    @Override
    public void initialize(final URL url, final ResourceBundle rb) {
        DATE_FORMATTER.setLenient(false);
        table.setItems(data);
        populate(retrieveData());
        setupDateOfBirthColumn();
        setupSalaryColumn();
        setTableEditable();

    }

    private List < Person > retrieveData() {
        try {
            return Arrays.asList(
                new Person("Dan","Newton",
                    DATE_FORMATTER.parse("06/01/1994"),
                   "Java Developer", 22000),
                new Person("George","Newton",
                    DATE_FORMATTER.parse("24/01/1995"),"Bro", 15021),
                new Person("Laura","So",
                    DATE_FORMATTER.parse("24/04/1995"),"Student", 0),
                new Person("Jamie","Harwood",
                    DATE_FORMATTER.parse("15/12/9999"),
                   "Java Developer", 30000),
                new Person("Michael","Collins",
                    DATE_FORMATTER.parse("01/01/0001"),"Developer",
                    299),
                new Person("Stuart","Kerrigan",
                    DATE_FORMATTER.parse("06/10/1894"),
                   "Teaching Fellow", 100000));
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return new ArrayList < Person > ();
    }

    private void populate(final List < Person > people) {
        people.forEach(p - > data.add(new PersonTableData(p)));
    }

    private void setupDateOfBirthColumn() {
        // formats the display value to display dates in the form of dd/MM/yyyy
        dateOfBirthColumn
            .setCellFactory(EditCell. < PersonTableData, Date > forTableColumn(
                new MyDateStringConverter(DATE_PATTERN)));
        // updates the dateOfBirth field on the PersonTableData object to the
        // committed value
        dateOfBirthColumn.setOnEditCommit(event - > {
            final Date value = event.getNewValue() != null ? event.getNewValue() :
                event.getOldValue();
            ((PersonTableData) event.getTableView().getItems()
                .get(event.getTablePosition().getRow()))
            .setDateOfBirth(value);
            table.refresh();
        });
    }

    private void setupSalaryColumn() {
        // sets the cell factory to use EditCell which will handle key presses
        // and firing commit events
        salaryColumn.setCellFactory(
            EditCell. < PersonTableData, Double > forTableColumn(
                new MyDoubleStringConverter()));
        // updates the salary field on the PersonTableData object to the
        // committed value
        salaryColumn.setOnEditCommit(event - > {
            final Double value = event.getNewValue() != null ?
            event.getNewValue() : event.getOldValue();
            ((PersonTableData) event.getTableView().getItems()
                .get(event.getTablePosition().getRow())).setSalary(value);
            table.refresh();
        });
    }

    private void setTableEditable() {
        table.setEditable(true);
        // allows the individual cells to be selected
        table.getSelectionModel().cellSelectionEnabledProperty().set(true);
        // when character or numbers pressed it will start edit in editable
        // fields
        table.setOnKeyPressed(event - > {
            if (event.getCode().isLetterKey() || event.getCode().isDigitKey()) {
                editFocusedCell();
            } else if (event.getCode() == KeyCode.RIGHT ||
                event.getCode() == KeyCode.TAB) {
                table.getSelectionModel().selectNext();
                event.consume();
            } else if (event.getCode() == KeyCode.LEFT) {
                // work around due to
                // TableView.getSelectionModel().selectPrevious() due to a bug
                // stopping it from working on
                // the first column in the last row of the table
                selectPrevious();
                event.consume();
            }
        });
    }

    @SuppressWarnings("unchecked")
    private void editFocusedCell() {
        final TablePosition < PersonTableData, ? > focusedCell = table
            .focusModelProperty().get().focusedCellProperty().get();
        table.edit(focusedCell.getRow(), focusedCell.getTableColumn());
    }

    @SuppressWarnings("unchecked")
    private void selectPrevious() {
        if (table.getSelectionModel().isCellSelectionEnabled()) {
            // in cell selection mode, we have to wrap around, going from
            // right-to-left, and then wrapping to the end of the previous line
            TablePosition < PersonTableData, ? > pos = table.getFocusModel()
                .getFocusedCell();
            if (pos.getColumn() - 1 >= 0) {
                // go to previous row
                table.getSelectionModel().select(pos.getRow(),
                    getTableColumn(pos.getTableColumn(), -1));
            } else if (pos.getRow() < table.getItems().size()) {
                // wrap to end of previous row
                table.getSelectionModel().select(pos.getRow() - 1,
                    table.getVisibleLeafColumn(
                        table.getVisibleLeafColumns().size() - 1));
            }
        } else {
            int focusIndex = table.getFocusModel().getFocusedIndex();
            if (focusIndex == -1) {
                table.getSelectionModel().select(table.getItems().size() - 1);
            } else if (focusIndex > 0) {
                table.getSelectionModel().select(focusIndex - 1);
            }
        }
    }

    private TableColumn < PersonTableData, ? > getTableColumn(
        final TableColumn < PersonTableData, ? > column, int offset) {
        int columnIndex = table.getVisibleLeafIndex(column);
        int newColumnIndex = columnIndex + offset;
        return table.getVisibleLeafColumn(newColumnIndex);
    }

    @FXML
    private void submit(final ActionEvent event) {
        if (allFieldsValid()) {
            final String firstName = firstNameTextField.getText();
            final String surname = surnameTextField.getText();
            Date dateOfBirth = null;
            try {
                dateOfBirth = DATE_FORMATTER
                    .parse(dateOfBirthTextField.getText());
            } catch (final ParseException e) {}
            final String occupation = occupationTextField.getText();
            final double salary = Double.parseDouble(salaryTextField.getText());
            data.add(new PersonTableData(firstName, surname, dateOfBirth,
                occupation, salary));
        }
    }

    private boolean allFieldsValid() {
        return !firstNameTextField.getText().isEmpty() &&
            !surnameTextField.getText().isEmpty() &&
            dateOfBirthFieldValid() &&
            !occupationTextField.getText().isEmpty() &&
            !salaryTextField.getText().isEmpty();
    }

    private boolean dateOfBirthFieldValid() {
        if (!dateOfBirthTextField.getText().isEmpty()) {
            try {
                DATE_FORMATTER.parse(dateOfBirthTextField.getText());
                return true;
            } catch (ParseException e) {
                return false;
            }
        }
        return false;
    }
}

让我们将其分解为更小的代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@FXML
private TableView<PersonTableData> table;

private ObservableList<PersonTableData> data = FXCollections.observableArrayList();

@Override
public void initialize(final URL url, final ResourceBundle rb) {
table.setItems(data);
populate(retrieveData());
}

private List<Person> retrieveData() {
try {
return Arrays.asList(
new Person("Dan","Newton",
    DATE_FORMATTER.parse("06/01/1994"),
   "Java Developer", 22000),
new Person("George","Newton",
    DATE_FORMATTER.parse("24/01/1995"),"Bro", 15021),
new Person("Laura","So",
    DATE_FORMATTER.parse("24/04/1995"),"Student", 0),
new Person("Jamie","Harwood",
    DATE_FORMATTER.parse("15/12/9999"),
   "Java Developer", 30000),
new Person("Michael","Collins",
    DATE_FORMATTER.parse("01/01/0001"),"Developer",
    299),
new Person("Stuart","Kerrigan",
    DATE_FORMATTER.parse("06/10/1894"),
   "Teaching Fellow", 100000));
} catch (ParseException e) {
e.printStackTrace();
}
return new ArrayList<Person>();
}

private void populate(final List<Person> people) {
people.forEach(p -> data.add(new PersonTableData(p)));
}

这是将一些数据传递到表中所需的基本代码。 请注意,该表被定义为TableView

,显示其存储的数据由模型PersonTableData表示。 将存储在表中的数据保存在anObservableList


,就像之前的属性一样,允许我们使用侦听器侦听更改。 一旦将它们都设置好,我们将通过调用table.setItems(data)并填充数据将它们链接在一起。 我在此示例中创建了populateData()方法,在实际情况下,该方法可能是对数据库的调用,该数据库将返回数据或对象,然后将这些数据或对象转换为存储在表中的模型(此示例中为PersonTableData)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static final String DATE_PATTERN ="dd/MM/yyyy";

@FXML
private TableColumn < PersonTableData, Date > dateOfBirthColumn;

private void setupDateOfBirthColumn() {
    // formats the display value to display dates in the form of dd/MM/yyyy
    dateOfBirthColumn
        .setCellFactory(EditCell. < PersonTableData, Date > forTableColumn(
            new MyDateStringConverter(DATE_PATTERN)));
    // updates the dateOfBirth field on the PersonTableData object to the
    // committed value
    dateOfBirthColumn.setOnEditCommit(event - > {
        final Date value = event.getNewValue() != null ? event.getNewValue() :
            event.getOldValue();
        ((PersonTableData) event.getTableView().getItems()
            .get(event.getTablePosition().getRow()))
        .setDateOfBirth(value);
        table.refresh();
    });
}

设置单元格工厂将覆盖默认设置,从而使我们能够更改表中数据显示方式的功能。 这段代码允许dateOfBirth列的值以" dd / MM / yyyy"的形式显示,而不是默认的Date.toString()输出,该输出通常看起来很难存储在表中。 为此,我定义了自己的TableCell版本(称为EditCell)和一个转换器(MyDateStringConverter),以将日期转换为所需的格式。 这些将在后面说明。 当用户通过更新已编辑行的PersonTableData模型来更改列中的值时,将使用setOnEditCommit保存提交的值。

Image title

薪水列采用与dateOfBirth列相同的格式,但改用MyDoubleStringConverter将输入转换为双精度值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@FXML
private TableColumn < PersonTableData, Double > salaryColumn;

private void setupSalaryColumn() {
    salaryColumn.setCellFactory(
        EditCell. < PersonTableData, Double > forTableColumn(
            new MyDoubleStringConverter()));
    // updates the salary field on the PersonTableData object to the
    // committed value
    salaryColumn.setOnEditCommit(event - > {
        final Double value = event.getNewValue() != null ?
        event.getNewValue() : event.getOldValue();
        ((PersonTableData) event.getTableView().getItems()
            .get(event.getTablePosition().getRow())).setSalary(value);
        table.refresh();
    });
}

回到我前面提到的EditCell,这是一个扩展TextFieldTableCell的TableCell。 感谢james-d编写下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
public class EditCell < S, T > extends TextFieldTableCell < S, T > {

    private TextField textField;
    private boolean escapePressed = false;
    private TablePosition < S,
    ? > tablePos = null;

    public EditCell(final StringConverter < T > converter) {
        super(converter);
    }

    public static < S > Callback < TableColumn < S,
    String > ,
    TableCell < S,
    String >> forTableColumn() {
        return forTableColumn(new DefaultStringConverter());
    }

    public static < S,
    T > Callback < TableColumn < S,
    T > ,
    TableCell < S,
    T >> forTableColumn(
        final StringConverter < T > converter) {
        return list - > new EditCell < S, T > (converter);
    }

    @Override
    public void startEdit() {
        if (!isEditable() || !getTableView().isEditable() ||
            !getTableColumn().isEditable()) {
            return;
        }
        super.startEdit();

        if (isEditing()) {
            if (textField == null) {
                textField = getTextField();
            }
            escapePressed = false;
            startEdit(textField);
            final TableView < S > table = getTableView();
            tablePos = table.getEditingCell();
        }
    }

    /** {@inheritDoc} */
    @Override
    public void commitEdit(T newValue) {
        if (!isEditing())
            return;
        final TableView < S > table = getTableView();
        if (table != null) {
            // Inform the TableView of the edit being ready to be committed.
            CellEditEvent editEvent = new CellEditEvent(table, tablePos,
                TableColumn.editCommitEvent(), newValue);

            Event.fireEvent(getTableColumn(), editEvent);
        }
        // we need to setEditing(false):
        super.cancelEdit(); // this fires an invalid EditCancelEvent.
        // update the item within this cell, so that it represents the new value
        updateItem(newValue, false);
        if (table != null) {
            // reset the editing cell on the TableView
            table.edit(-1, null);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void cancelEdit() {
        if (escapePressed) {
            // this is a cancel event after escape key
            super.cancelEdit();
            setText(getItemText()); // restore the original text in the view
        } else {
            // this is not a cancel event after escape key
            // we interpret it as commit.
            String newText = textField.getText();
            // commit the new text to the model
            this.commitEdit(getConverter().fromString(newText));
        }
        setGraphic(null); // stop editing with TextField
    }

    /** {@inheritDoc} */
    @Override
    public void updateItem(T item, boolean empty) {
        super.updateItem(item, empty);
        updateItem();
    }

    private TextField getTextField() {

        final TextField textField = new TextField(getItemText());

        textField.setOnAction(new EventHandler < ActionEvent > () {

            @Override
            public void handle(ActionEvent event) {
                System.out.println("hi");
            }
        });

        // Use onAction here rather than onKeyReleased (with check for Enter),
        textField.setOnAction(event - > {
            if (getConverter() == null) {
                throw new IllegalStateException("StringConverter is null.");
            }
            this.commitEdit(getConverter().fromString(textField.getText()));
            event.consume();
        });

        textField.focusedProperty().addListener(new ChangeListener < Boolean > () {
            @Override
            public void changed(ObservableValue << ? extends Boolean > observable,
                Boolean oldValue, Boolean newValue) {
                if (!newValue) {
                    commitEdit(getConverter().fromString(textField.getText()));
                }
            }
        });

        textField.setOnKeyPressed(t - > {
            if (t.getCode() == KeyCode.ESCAPE)
                escapePressed = true;
            else
                escapePressed = false;
        });
        textField.setOnKeyReleased(t - > {
            if (t.getCode() == KeyCode.ESCAPE) {
                throw new IllegalArgumentException(
                   "did not expect esc key releases here.");
            }
        });

        textField.addEventFilter(KeyEvent.KEY_PRESSED, event - > {
            if (event.getCode() == KeyCode.ESCAPE) {
                textField.setText(getConverter().toString(getItem()));
                cancelEdit();
                event.consume();
            } else if (event.getCode() == KeyCode.RIGHT ||
                event.getCode() == KeyCode.TAB) {
                getTableView().getSelectionModel().selectNext();
                event.consume();
            } else if (event.getCode() == KeyCode.LEFT) {
                getTableView().getSelectionModel().selectPrevious();
                event.consume();
            } else if (event.getCode() == KeyCode.UP) {
                getTableView().getSelectionModel().selectAboveCell();
                event.consume();
            } else if (event.getCode() == KeyCode.DOWN) {
                getTableView().getSelectionModel().selectBelowCell();
                event.consume();
            }
        });

        return textField;
    }

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

    private void updateItem() {
        if (isEmpty()) {
            setText(null);
            setGraphic(null);
        } else {
            if (isEditing()) {
                if (textField != null) {
                    textField.setText(getItemText());
                }
                setText(null);
                setGraphic(textField);
            } else {
                setText(getItemText());
                setGraphic(null);
            }
        }
    }

    private void startEdit(final TextField textField) {
        if (textField != null) {
            textField.setText(getItemText());
        }
        setText(null);
        setGraphic(textField);
        textField.selectAll();
        // requesting focus so that key input can immediately go into the
        // TextField
        textField.requestFocus();
    }
}

为了使表格中的单元格可编辑,需要覆盖单元格的默认功能。 需要更改其默认值的最重要方法是commitEdit和cancelEdit方法,因为默认情况下,cancelEdit不会尝试在单元格中提交新值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
textField.addEventFilter(KeyEvent.KEY_PRESSED, event - > {
    if (event.getCode() == KeyCode.ESCAPE) {
        textField.setText(getConverter().toString(getItem()));
        cancelEdit();
        event.consume();
    } else if (event.getCode() == KeyCode.RIGHT ||
        event.getCode() == KeyCode.TAB) {
        getTableView().getSelectionModel().selectNext();
        event.consume();
    } else if (event.getCode() == KeyCode.LEFT) {
        getTableView().getSelectionModel().selectPrevious();
        event.consume();
    } else if (event.getCode() == KeyCode.UP) {
        getTableView().getSelectionModel().selectAboveCell();
        event.consume();
    } else if (event.getCode() == KeyCode.DOWN) {
        getTableView().getSelectionModel().selectBelowCell();
        event.consume();
    }
});

我想让您注意这段非常丑陋的代码。 这需要处理在编辑单元格时发生的按键操作。 在单元格之间移动时,不会触发事件-仅在您开始编辑值时才触发。 使用这段代码,我们可以使用箭头键或选项卡移动到相邻的单元格,当您需要在不同的单元格中输入大量值时,这是一个很好的功能,这意味着您无需使用鼠标来选择 他们。

Image title

回到我前面提到的转换器,它们非常简单,因为JavaFX中已经有转换器。 但是,如果您输入无效的输入,它们将不起作用。 因此,这些扩展了它们的功能,但能够处理错误的输入。

MyDateStringConverter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyDateStringConverter extends DateStringConverter {

    public MyDateStringConverter(final String pattern) {
        super(pattern);
    }

    @Override
    public Date fromString(String value) {
        // catches the RuntimeException thrown by
        // DateStringConverter.fromString()
        try {
            return super.fromString(value);
        } catch (RuntimeException ex) {
            return null;
        }
    }
}

MyDoubleStringConverter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyDoubleStringConverter extends DoubleStringConverter {

    @Override
    public Double fromString(final String value) {
        return value.isEmpty() || !isNumber(value) ? null :
            super.fromString(value);
    }

    public boolean isNumber(String value) {
        int size = value.length();
        for (int i = 0; i < size; i++) {
            if (!Character.isDigit(value.charAt(i))) {
                return false;
            }
        }
        return size > 0;
    }
}

因此,我们在表中定义了一些可编辑的单元格,但是我们实际上需要准备表以使其可编辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@FXML
private TableView < PersonTableData > table;

private void setTableEditable() {
    table.setEditable(true);
    // allows the individual cells to be selected
    table.getSelectionModel().cellSelectionEnabledProperty().set(true);
    // when character or numbers pressed it will start edit in editable
    // fields
    table.setOnKeyPressed(event - > {
        if (event.getCode().isLetterKey() || event.getCode().isDigitKey()) {
            editFocusedCell();
        } else if (event.getCode() == KeyCode.RIGHT ||
            event.getCode() == KeyCode.TAB) {
            table.getSelectionModel().selectNext();
            event.consume();
        } else if (event.getCode() == KeyCode.LEFT) {
            // work around due to
            // TableView.getSelectionModel().selectPrevious() due to a bug
            // stopping it from working on
            // the first column in the last row of the table
            selectPrevious();
            event.consume();
        }
    });
}

@SuppressWarnings("unchecked")
private void editFocusedCell() {
    final TablePosition < PersonTableData, ? > focusedCell = table
        .focusModelProperty().get().focusedCellProperty().get();
    table.edit(focusedCell.getRow(), focusedCell.getTableColumn());
}

@SuppressWarnings("unchecked")
private void selectPrevious() {
    if (table.getSelectionModel().isCellSelectionEnabled()) {
        // in cell selection mode, we have to wrap around, going from
        // right-to-left, and then wrapping to the end of the previous line
        TablePosition < PersonTableData, ? > pos = table.getFocusModel()
            .getFocusedCell();
        if (pos.getColumn() - 1 >= 0) {
            // go to previous row
            table.getSelectionModel().select(pos.getRow(),
                getTableColumn(pos.getTableColumn(), -1));
        } else if (pos.getRow() < table.getItems().size()) {
            // wrap to end of previous row
            table.getSelectionModel().select(pos.getRow() - 1,
                table.getVisibleLeafColumn(
                    table.getVisibleLeafColumns().size() - 1));
        }
    } else {
        int focusIndex = table.getFocusModel().getFocusedIndex();
        if (focusIndex == -1) {
            table.getSelectionModel().select(table.getItems().size() - 1);
        } else if (focusIndex > 0) {
            table.getSelectionModel().select(focusIndex - 1);
        }
    }
}

private TableColumn < PersonTableData, ? > getTableColumn(
    final TableColumn < PersonTableData, ? > column, int offset) {
    int columnIndex = table.getVisibleLeafIndex(column);
    int newColumnIndex = columnIndex + offset;
    return table.getVisibleLeafColumn(newColumnIndex);
}

您在上面的代码中看到的方法调用允许编辑表,然后允许选择单个单元格,而不是一次选择整个行。 setOnKeyPressed事件是必需的,它使我们能够在前面提到的单元格之间遍历,而无需先对其进行编辑。

不幸的是,方法TableView.getSelectionModel()。selectPrevious()无法正常工作。 当您位于表格最后一行的第一个单元格中时,它不允许您选择上一个单元格。 似乎不必要地留下了-1,因此我复制了该方法并删除了-1…

所以...

1
pos.getRow() < table.getItems().size() - 1

...成为:

1
pos.getRow() < table.getItems().size()

本示例中的最后一段代码是通过从文本字段中获取一些值来向表中添加新行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@FXML
private TextField firstNameTextField;

@FXML
private TextField surnameTextField;

@FXML
private TextField dateOfBirthTextField;

@FXML
private TextField occupationTextField;

@FXML
private TextField salaryTextField;

@FXML
private Button submitButton;

private ObservableList < PersonTableData > data = FXCollections
    .observableArrayList();

private static final String DATE_PATTERN ="dd/MM/yyyy";

private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat(
    DATE_PATTERN);

@FXML
private void submit(final ActionEvent event) {
    if (allFieldsValid()) {
        final String firstName = firstNameTextField.getText();
        final String surname = surnameTextField.getText();
        Date dateOfBirth = null;
        try {
            dateOfBirth = DATE_FORMATTER
                .parse(dateOfBirthTextField.getText());
        } catch (final ParseException e) {}
        final String occupation = occupationTextField.getText();
        final double salary = Double.parseDouble(salaryTextField.getText());
        data.add(new PersonTableData(firstName, surname, dateOfBirth,
            occupation, salary));
    }
}

private boolean allFieldsValid() {
    return !firstNameTextField.getText().isEmpty() &&
        !surnameTextField.getText().isEmpty() &&
        dateOfBirthFieldValid() &&
        !occupationTextField.getText().isEmpty() &&
        !salaryTextField.getText().isEmpty();
}

private boolean dateOfBirthFieldValid() {
    if (!dateOfBirthTextField.getText().isEmpty()) {
        try {
            DATE_FORMATTER.parse(dateOfBirthTextField.getText());
            return true;
        } catch (ParseException e) {
            return false;
        }
    }
    return false;
}

在此示例中没有太多要解释的内容,因为它只是从文本字段中获取值,并且如果有效,则将创建一个新的PersonTableData模型并将其添加到ObservableList中,然后将其显示在表中。

如果您已经达到了这一点,那就好! 我知道这是很多要阅读的代码,我在写这篇文章时正在查看字数统计,看来我写了一篇论文。 因此,无论如何,通过使用本教程中看到的代码,您应该能够制作一个完全可编辑的网格,不幸的是,这需要大量的配置才能正常工作……但是,您不必为此感到难过 看过一个怎么做的例子! 如果这还不够,这是指向GitHub上所有示例代码的链接。