Editable Tables in JavaFX
在听到我的同事不断谈论我工作的公司很久以前设计的可编辑网格之后,我决定做点什么使他为我感到骄傲。 因此,我编写了一个应用程序,该应用程序可以在JavaFX中创建可编辑的TableView,如果需要的话,可以用来无脑地输入数据。 不幸的是,JavaFX并不想使它变得简单好用,我甚至在编写示例代码时在JavaFX代码中发现了一个错误。 不用担心,我会在其中添加一些说明甚至照片,以免您迷路。
如果您需要一些背景知识,请查阅使用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
必须使用100%的getter才能在表中显示值,而setter是可选的,并且仅在您要进行某些设置(例如编辑值)时才需要。 还要注意,get()和set()方法访问属性的包装值,而不是返回或更改实际属性。 模型内部的属性名称无关紧要,但是您还记得我们之前在fxml的PropertyValueFactory标记中定义名称的位置吗? 我们需要在这些标记中定义的名称与模型中的getter和setter相匹配。 如果它们不匹配,则不会崩溃或发生任何事情,但是您不会在不匹配的列中显示任何数据。
为了测试这一点,如果您更改了其中一个字段的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保存提交的值。
薪水列采用与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(); } }); |
我想让您注意这段非常丑陋的代码。 这需要处理在编辑单元格时发生的按键操作。 在单元格之间移动时,不会触发事件-仅在您开始编辑值时才触发。 使用这段代码,我们可以使用箭头键或选项卡移动到相邻的单元格,当您需要在不同的单元格中输入大量值时,这是一个很好的功能,这意味着您无需使用鼠标来选择 他们。
回到我前面提到的转换器,它们非常简单,因为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上所有示例代码的链接。