JavaFX - 用戶交互
第3部分的主題:
- 在表中反應選擇的改變(TableView中)。
- 增加增加,編輯和刪除按鈕的功能。
- 創建自定義彈出對話框編輯人員。
- 驗證用戶輸入。
響應表的選擇
顯然,我們還冇有使用應用程序的右邊。想法是當用戶選擇表中的人員時,在右邊顯示人員的詳情。
首先,讓我們在PersonOverviewController
添加一個新的方法,幫助我們使用單個人員的數據填寫標簽。
創建方法showPersonDetails(Person person)
。遍曆所有標簽,並且使用setText(…)
方法設置標簽的文本為個人的詳情。如果null作為參數傳遞,所有的標簽應該被清空。
PersonOverviewController.java
/** * Fills all text fields to show details about the person. * If the specified person is null, all text fields are cleared. * * @param person the person or null */ private void showPersonDetails(Person person) { if (person != null) { // Fill the labels with info from the person object. firstNameLabel.setText(person.getFirstName()); lastNameLabel.setText(person.getLastName()); streetLabel.setText(person.getStreet()); postalCodeLabel.setText(Integer.toString(person.getPostalCode())); cityLabel.setText(person.getCity()); // TODO: We need a way to convert the birthday into a String! // birthdayLabel.setText(...); } else { // Person is null, remove all the text. firstNameLabel.setText(""); lastNameLabel.setText(""); streetLabel.setText(""); postalCodeLabel.setText(""); cityLabel.setText(""); birthdayLabel.setText(""); } }
轉換生日日期為字符串
你注意到我們冇有設置birthday
到標簽中,因為它是LocalDate
類型,不是String
。我們首先需要格式化日期。
在幾個地方上我們使用LocalDate
和String
之間的轉換。好的實踐是創建一個帶有static
方法的幫助類。我們稱它為DateUtil
,並且把它放到單獨的包中,稱為ch.makery.address.util
。
DateUtil.java
package ch.makery.address.util; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; /** * Helper functions for handling dates. * * @author Marco Jakob */ public class DateUtil { /** The date pattern that is used for conversion. Change as you wish. */ private static final String DATE_PATTERN = "dd.MM.yyyy"; /** The date formatter. */ private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_PATTERN); /** * Returns the given date as a well formatted String. The above defined * {@link DateUtil#DATE_PATTERN} is used. * * @param date the date to be returned as a string * @return formatted string */ public static String format(LocalDate date) { if (date == null) { return null; } return DATE_FORMATTER.format(date); } /** * Converts a String in the format of the defined {@link DateUtil#DATE_PATTERN} * to a {@link LocalDate} object. * * Returns null if the String could not be converted. * * @param dateString the date as String * @return the date object or null if it could not be converted */ public static LocalDate parse(String dateString) { try { return DATE_FORMATTER.parse(dateString, LocalDate::from); } catch (DateTimeParseException e) { return null; } } /** * Checks the String whether it is a valid date. * * @param dateString * @return true if the String is a valid date */ public static boolean validDate(String dateString) { // Try to parse the String. return DateUtil.parse(dateString) != null; } }
使用DateUtil
現在,我們需要在PersonOverviewController
的showPersonDetails
方法中使用我們新建的DateUtil
。使用下麵這樣替代我們添加的TODO
。
birthdayLabel.setText(DateUtil.format(person.getBirthday()));
監聽表選擇的改變
為了當用戶在人員表中選擇一個人時獲得通知,我們需要監聽改變。
在JavaFX中有一個接口稱為ChangeListener
,帶有一個方法changed()
。該方法有三個參數:observable
, oldValue
和newValue
。
我們使用Java 8 lambda表達式創建這樣一個ChangeListener
。讓我們添加一些行到PersonOverviewController
的initialize()
方法中。現在看起來是這樣的。
PersonOverviewController.java
@FXML private void initialize() { // Initialize the person table with the two columns. firstNameColumn.setCellValueFactory( cellData -> cellData.getValue().firstNameProperty()); lastNameColumn.setCellValueFactory( cellData -> cellData.getValue().lastNameProperty()); // Clear person details. showPersonDetails(null); // Listen for selection changes and show the person details when changed. personTable.getSelectionModel().selectedItemProperty().addListener( (observable, oldValue, newValue) -> showPersonDetails(newValue)); }
使用showPersonDetails(null)
,我們重設個人詳情。
使用personTable.getSelectionModel...
,我們獲得人員表的selectedItemProperty
,並且添加監聽。不管什麼時候用戶選擇表中的人員,都會執行我們的lambda
表達式。我們獲取新選擇的人員,並且把它傳遞給showPersonDetails(...)
方法。
現在試著運行你的應用程序,驗證當你選擇表中的人員時,關於該人員的詳情是否正確的顯示。
如果有些事情不能工作,你可以對比下PersonOverviewController.java中的PersonOverviewController
類
刪除按鈕
我們的用戶接口已經包含一個刪除按鈕,但是冇有任何功能。我們能在SceneBuilder中的按鈕上選擇動作。在我們控製器中的任何使用@FXML
(或者它是公用的)注釋的方法都可以被Scene Builder訪問。因此,讓我們在PersonOverviewController
類的最後添加一個刪除方法。
PersonOverviewController.java
/** * Called when the user clicks on the delete button. */ @FXML private void handleDeletePerson() { int selectedIndex = personTable.getSelectionModel().getSelectedIndex(); personTable.getItems().remove(selectedIndex); }
現在,使用SceneBuilder打開PersonOverview.fxml
文件,選擇Delete按鈕,打開Code組,在On Actin的下拉菜單中選擇handleDeletePerson
。
錯誤處理
如果你現在運行應用程序,你應該能夠從表中刪除選擇的人員。但是,當你冇有在表中選擇人員時點擊刪除按鈕時會發生什麼呢。
這裡有一個ArrayIndexOutOfBoundsException
,因為它不能刪除掉索引為-1人員項目。索引-1由getSelectedIndex()
返回,它意味著你冇有選擇項目。
當然,忽略這種錯誤不是非常好。我們應該讓用戶知道在刪除時必須選擇一個人員。(更好的是我們應該禁用刪除按鈕,以便用戶冇有機會做錯誤的事情)。
我們添加一個彈出對話框通知用戶,你將需要*添加一個庫Dialogs:
-
下載controlsfx-8.0.6_20.jar (你也能從ControlsFX Website中獲取)。 重要:ControlsFX必須是8.0.6_20以上版本才能在
JDK8U20
以上版本工作。 - 在項目中創建一個lib子目錄,添加controlsf jar文件到該目錄下。
- 添加庫到你的項目classpath中。在Eclipse中右擊jar文件|選擇Build Path| Add to Build Path。現在Eclipse知道這個庫了。
對handleDeletePerson()
方法做一些修改後,不管什麼時候用戶冇有選擇表中的人員時按下刪除按鈕,我們能顯示一個簡單的對話框。
PersonOverviewController.java
/** * Called when the user clicks on the delete button. */ @FXML private void handleDeletePerson() { int selectedIndex = personTable.getSelectionModel().getSelectedIndex(); if (selectedIndex >= 0) { personTable.getItems().remove(selectedIndex); } else { // Nothing selected. Dialogs.create() .title("No Selection") .masthead("No Person Selected") .message("Please select a person in the table.") .showWarning(); } }
新建和編輯對話框
新建和編輯的動作有點工作:我們需要一個自定義帶表單的對話框(例如:新的Stage),詢問用戶關於人員的詳情。
設計對話框
-
在view包中創建新的fxml文件,稱為
PersonEditDialog.fxml
-
使用
GridPan
,Label
,TextField
和Button
創建一個對話框,如下所示:
如果你不能完成工作,你能下載這個PersonEditDialog.fxml.
創建控製器
為對話框創建控製器PersonEditDialogController.java
:
PersonEditDialogController.java
package ch.makery.address.view; import javafx.fxml.FXML; import javafx.scene.control.TextField; import javafx.stage.Stage; import org.controlsfx.dialog.Dialogs; import ch.makery.address.model.Person; import ch.makery.address.util.DateUtil; /** * Dialog to edit details of a person. * * @author Marco Jakob */ public class PersonEditDialogController { @FXML private TextField firstNameField; @FXML private TextField lastNameField; @FXML private TextField streetField; @FXML private TextField postalCodeField; @FXML private TextField cityField; @FXML private TextField birthdayField; private Stage dialogStage; private Person person; private boolean okClicked = false; /** * Initializes the controller class. This method is automatically called * after the fxml file has been loaded. */ @FXML private void initialize() { } /** * Sets the stage of this dialog. * * @param dialogStage */ public void setDialogStage(Stage dialogStage) { this.dialogStage = dialogStage; } /** * Sets the person to be edited in the dialog. * * @param person */ public void setPerson(Person person) { this.person = person; firstNameField.setText(person.getFirstName()); lastNameField.setText(person.getLastName()); streetField.setText(person.getStreet()); postalCodeField.setText(Integer.toString(person.getPostalCode())); cityField.setText(person.getCity()); birthdayField.setText(DateUtil.format(person.getBirthday())); birthdayField.setPromptText("dd.mm.yyyy"); } /** * Returns true if the user clicked OK, false otherwise. * * @return */ public boolean isOkClicked() { return okClicked; } /** * Called when the user clicks ok. */ @FXML private void handleOk() { if (isInputValid()) { person.setFirstName(firstNameField.getText()); person.setLastName(lastNameField.getText()); person.setStreet(streetField.getText()); person.setPostalCode(Integer.parseInt(postalCodeField.getText())); person.setCity(cityField.getText()); person.setBirthday(DateUtil.parse(birthdayField.getText())); okClicked = true; dialogStage.close(); } } /** * Called when the user clicks cancel. */ @FXML private void handleCancel() { dialogStage.close(); } /** * Validates the user input in the text fields. * * @return true if the input is valid */ private boolean isInputValid() { String errorMessage = ""; if (firstNameField.getText() == null || firstNameField.getText().length() == 0) { errorMessage += "No valid first name!\n"; } if (lastNameField.getText() == null || lastNameField.getText().length() == 0) { errorMessage += "No valid last name!\n"; } if (streetField.getText() == null || streetField.getText().length() == 0) { errorMessage += "No valid street!\n"; } if (postalCodeField.getText() == null || postalCodeField.getText().length() == 0) { errorMessage += "No valid postal code!\n"; } else { // try to parse the postal code into an int. try { Integer.parseInt(postalCodeField.getText()); } catch (NumberFormatException e) { errorMessage += "No valid postal code (must be an integer)!\n"; } } if (cityField.getText() == null || cityField.getText().length() == 0) { errorMessage += "No valid city!\n"; } if (birthdayField.getText() == null || birthdayField.getText().length() == 0) { errorMessage += "No valid birthday!\n"; } else { if (!DateUtil.validDate(birthdayField.getText())) { errorMessage += "No valid birthday. Use the format dd.mm.yyyy!\n"; } } if (errorMessage.length() == 0) { return true; } else { // Show the error message. Dialogs.create() .title("Invalid Fields") .masthead("Please correct invalid fields") .message(errorMessage) .showError(); return false; } } }
關於該控製器的一些事情應該注意:
-
setPerson(…)
方法可以從其它類中調用,用來設置編輯的人員。 -
當用戶點擊OK按鈕時,調用
handleOK()
方法。首先,通過調用isInputValid()
方法做一些驗證。隻有驗證成功,Person對象使用輸入的數據填充。這些修改將直接應用到Person對象上,傳遞給setPerson(…)
。 -
布爾值
okClicked
被使用,以便調用者決定用戶是否點擊OK或者Cancel按鈕。
連接視圖和控製器
使用已經創建的視圖(FXML)和控製器,需要連接到一起。
-
使用SceneBuilder打開
PersonEditDialog.fxml
文件 -
在左邊的Controller組中選擇
PersonEditDialogController
作為控製器類 -
設置所有TextField的
fx:id
到相應的控製器字段上。 - 設置兩個按鈕的onAction到相應的處理方法上。
打開對話框
在MainApp
中添加一個方法加載和顯示編輯人員的對話框。
MainApp.java
/** * Opens a dialog to edit details for the specified person. If the user * clicks OK, the changes are saved into the provided person object and true * is returned. * * @param person the person object to be edited * @return true if the user clicked OK, false otherwise. */ public boolean showPersonEditDialog(Person person) { try { // Load the fxml file and create a new stage for the popup dialog. FXMLLoader loader = new FXMLLoader(); loader.setLocation(MainApp.class.getResource("view/PersonEditDialog.fxml")); AnchorPane page = (AnchorPane) loader.load(); // Create the dialog Stage. Stage dialogStage = new Stage(); dialogStage.setTitle("Edit Person"); dialogStage.initModality(Modality.WINDOW_MODAL); dialogStage.initOwner(primaryStage); Scene scene = new Scene(page); dialogStage.setScene(scene); // Set the person into the controller. PersonEditDialogController controller = loader.getController(); controller.setDialogStage(dialogStage); controller.setPerson(person); // Show the dialog and wait until the user closes it dialogStage.showAndWait(); return controller.isOkClicked(); } catch (IOException e) { e.printStackTrace(); return false; } }
添加下麵的方法到PersonOverviewController
中。當用戶按下New或Edit按鈕時,這些方法將從MainApp
中調用showPersonEditDialog(...)
。
PersonOverviewController.java
/** * Called when the user clicks the new button. Opens a dialog to edit * details for a new person. */ @FXML private void handleNewPerson() { Person tempPerson = new Person(); boolean okClicked = mainApp.showPersonEditDialog(tempPerson); if (okClicked) { mainApp.getPersonData().add(tempPerson); } } /** * Called when the user clicks the edit button. Opens a dialog to edit * details for the selected person. */ @FXML private void handleEditPerson() { Person selectedPerson = personTable.getSelectionModel().getSelectedItem(); if (selectedPerson != null) { boolean okClicked = mainApp.showPersonEditDialog(selectedPerson); if (okClicked) { showPersonDetails(selectedPerson); } } else { // Nothing selected. Dialogs.create() .title("No Selection") .masthead("No Person Selected") .message("Please select a person in the table.") .showWarning(); } }
在Scene Builder中打開PersonOverview.fxml
文件,為New和Edit按鈕的On Action中選擇對應的方法。
完成!
現在你應該有一個可以工作的Address應用。應用能夠添加、編輯和刪除人員。這裡甚至有一些文本字段的驗證避免壞的用戶輸入。
我希望本應用的概念和結構讓開始編寫自己的JavaFX應用!玩的開心。