
第5部分的主題
- 持久化數據為XML
- 使用JavaFX的FileChooser
- 使用JavaFX的菜單
- 在用戶設置中保存最后打開的文件路徑。
現在我們的地址應用程序的數據只保存在內存中。每次我們關閉應用程序,數據將丟失,因此是時候開始考慮持久化存儲數據了。
保存用戶設置
Java允許我們使用Preferences類保存一些應用狀態。依賴於操作系統,Perferences保存在不同的地方(例如:Windows中的注冊文件)。
我們不能使用Preferences來保存全部地址簿。但是它允許我們保存一些簡單的應用狀態。一件這樣事情是最后打開文件的路徑。使用這個信息,我們能加載最后應用的狀態,不管用戶什么時候重啟應用程序。
下面兩個方法用於保存和檢索Preference。添加它們到你的MainApp類的最后:
MainApp.java
/**
* Returns the person file preference, i.e. the file that was last opened.
* The preference is read from the OS specific registry. If no such
* preference can be found, null is returned.
*
* @return
*/
public File getPersonFilePath() {
Preferences prefs = Preferences.userNodeForPackage(MainApp.class);
String filePath = prefs.get("filePath", null);
if (filePath != null) {
return new File(filePath);
} else {
return null;
}
}
/**
* Sets the file path of the currently loaded file. The path is persisted in
* the OS specific registry.
*
* @param file the file or null to remove the path
*/
public void setPersonFilePath(File file) {
Preferences prefs = Preferences.userNodeForPackage(MainApp.class);
if (file != null) {
prefs.put("filePath", file.getPath());
// Update the stage title.
primaryStage.setTitle("AddressApp - " + file.getName());
} else {
prefs.remove("filePath");
// Update the stage title.
primaryStage.setTitle("AddressApp");
}
}
持久性數據到XML
為什么是XML?
持久性數據的一種最常用的方法是使用數據庫。數據庫通常包含一些類型的關系數據(例如:表),當我們需要保存的數據是對象時。這稱object-relational impedance mismatch。匹配對象到關系型數據庫表有很多工作要做。這里有一些框架幫助我們匹配(例如:Hibernate,最流行的一個)。但是它仍然需要相當多的設置工作。
對於簡單的數據模型,非常容易使用XML。我們使用稱為JAXB(Java Architecture for XML Binding)的庫。只需要幾行代碼,JAXB將允許我們生成XML輸出,如下所示:
示例XML輸出
<persons>
<person>
<birthday>1999-02-21</birthday>
<city>some city</city>
<firstName>Hans</firstName>
<lastName>Muster</lastName>
<postalCode>1234</postalCode>
<street>some street</street>
</person>
<person>
<birthday>1999-02-21</birthday>
<city>some city</city>
<firstName>Anna</firstName>
<lastName>Best</lastName>
<postalCode>1234</postalCode>
<street>some street</street>
</person>
</persons>
使用JAXB
JAXB已經包含在JDK中。這意味着我們不需要包含任何其它的庫。
JAXB提供兩個主要特征:編列(marshal)Java對象到XML的能力,反編列(unmarshal)XML到Java對象。
為了讓JAXB能夠做轉換,我們需要准備我們的模型。
准備JAXB的模型類
我們希望保持的數據位於MainApp類的personData變量中。JAXB要求使用@XmlRootElement注釋作為最頂層的類。personData是ObservableList類,我們不能把任何注釋放到ObservableList上。因此,我們需要創建另外一個類,它只用於保存Person列表,用於存儲成XML文件。
創建的新類名為PersonListWrapper,把它放入到ch.makery.address.model包中。
PersonListWrapper.java
package ch.makery.address.model;
import java.util.List;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* Helper class to wrap a list of persons. This is used for saving the
* list of persons to XML.
*
* @author Marco Jakob
*/
@XmlRootElement(name = "persons")
public class PersonListWrapper {
private List<Person> persons;
@XmlElement(name = "person")
public List<Person> getPersons() {
return persons;
}
public void setPersons(List<Person> persons) {
this.persons = persons;
}
}
注意兩個注釋:
@XmlRootElement定義根元素的名稱。@XmlElement一個可選的名稱,用來指定元素。
使用JAXB讀寫數據
我們讓MainApp類負責讀寫人員數據。添加下面兩個方法到MainApp.java的最后:
/**
* Loads person data from the specified file. The current person data will
* be replaced.
*
* @param file
*/
public void loadPersonDataFromFile(File file) {
try {
JAXBContext context = JAXBContext
.newInstance(PersonListWrapper.class);
Unmarshaller um = context.createUnmarshaller();
// Reading XML from the file and unmarshalling.
PersonListWrapper wrapper = (PersonListWrapper) um.unmarshal(file);
personData.clear();
personData.addAll(wrapper.getPersons());
// Save the file path to the registry.
setPersonFilePath(file);
} catch (Exception e) { // catches ANY exception
Dialogs.create()
.title("Error")
.masthead("Could not load data from file:\n" + file.getPath())
.showException(e);
}
}
/**
* Saves the current person data to the specified file.
*
* @param file
*/
public void savePersonDataToFile(File file) {
try {
JAXBContext context = JAXBContext
.newInstance(PersonListWrapper.class);
Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
// Wrapping our person data.
PersonListWrapper wrapper = new PersonListWrapper();
wrapper.setPersons(personData);
// Marshalling and saving XML to the file.
m.marshal(wrapper, file);
// Save the file path to the registry.
setPersonFilePath(file);
} catch (Exception e) { // catches ANY exception
Dialogs.create().title("Error")
.masthead("Could not save data to file:\n" + file.getPath())
.showException(e);
}
}
*編組和解組*已經准備好,讓我們創建保存和加載的菜單實際的使用它。
處理菜單響應
在我們RootLayout.fxml中,這里已經有一個菜單,但是我們沒有使用它。在我們添加響應到菜單中之前,我們首先創建所有的菜單項。
在Scene Builder中打開RootLayout.fxml,從*library*組中拖曳必要的菜單到*Hierarchy*組的MemuBar中。創建New,Open…,Save,Save As…和Exit菜單項。

提示:使用*Properties*組下的*Accelerator*設置,你能設置菜單項的快捷鍵。
RootLayoutController
為了處理菜單動作,我們需要創建一個新的控制器類。在控制器包ch.makery.address.view中創建一個類RootLayoutController。
添加下面的內容到控制器中:
RootLayoutController.java
package ch.makery.address.view;
import java.io.File;
import javafx.fxml.FXML;
import javafx.stage.FileChooser;
import org.controlsfx.dialog.Dialogs;
import ch.makery.address.MainApp;
/**
* The controller for the root layout. The root layout provides the basic
* application layout containing a menu bar and space where other JavaFX
* elements can be placed.
*
* @author Marco Jakob
*/
public class RootLayoutController {
// Reference to the main application
private MainApp mainApp;
/**
* Is called by the main application to give a reference back to itself.
*
* @param mainApp
*/
public void setMainApp(MainApp mainApp) {
this.mainApp = mainApp;
}
/**
* Creates an empty address book.
*/
@FXML
private void handleNew() {
mainApp.getPersonData().clear();
mainApp.setPersonFilePath(null);
}
/**
* Opens a FileChooser to let the user select an address book to load.
*/
@FXML
private void handleOpen() {
FileChooser fileChooser = new FileChooser();
// Set extension filter
FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter(
"XML files (*.xml)", "*.xml");
fileChooser.getExtensionFilters().add(extFilter);
// Show save file dialog
File file = fileChooser.showOpenDialog(mainApp.getPrimaryStage());
if (file != null) {
mainApp.loadPersonDataFromFile(file);
}
}
/**
* Saves the file to the person file that is currently open. If there is no
* open file, the "save as" dialog is shown.
*/
@FXML
private void handleSave() {
File personFile = mainApp.getPersonFilePath();
if (personFile != null) {
mainApp.savePersonDataToFile(personFile);
} else {
handleSaveAs();
}
}
/**
* Opens a FileChooser to let the user select a file to save to.
*/
@FXML
private void handleSaveAs() {
FileChooser fileChooser = new FileChooser();
// Set extension filter
FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter(
"XML files (*.xml)", "*.xml");
fileChooser.getExtensionFilters().add(extFilter);
// Show save file dialog
File file = fileChooser.showSaveDialog(mainApp.getPrimaryStage());
if (file != null) {
// Make sure it has the correct extension
if (!file.getPath().endsWith(".xml")) {
file = new File(file.getPath() + ".xml");
}
mainApp.savePersonDataToFile(file);
}
}
/**
* Opens an about dialog.
*/
@FXML
private void handleAbout() {
Dialogs.create()
.title("AddressApp")
.masthead("About")
.message("Author: Marco Jakob\nWebsite: http://code.makery.ch")
.showInformation();
}
/**
* Closes the application.
*/
@FXML
private void handleExit() {
System.exit(0);
}
}
FileChooser
注意在上面的RootLayoutController中使用FileCooser的方法。首先,創建新的FileChooser類對象的,然后,添加擴展名過濾器,以至於只顯示以.xml結尾的文件。最后,文件選擇器顯示在主Stage的上面。
如果用戶沒有選擇一個文件關閉對話框,返回null。否則,我們獲得選擇的文件,我們能傳遞它到MainApp的loadPersonDataFromFile(…)或savePersonDataToFile()方法中。
連接fxml視圖到控制器
-
在Scene Builder中打開
RootLayout.fxml。在*Controller*組中選擇RootLayoutController作為控制器類。 -
回到*Hierarchy*組中,選擇一個菜單項。在*Code*組中On Action下,應該看到所有可用控制器方法的選擇。為每個菜單項選擇響應的方法。

-
為每個菜單項重復第2步。
-
關閉Scene Builder,並且在項目的根目錄上按下刷新F5。這讓Eclipse知道在Scene Builder中所做的修改。
連接MainApp和RootLayoutController
在幾個地方,RootLayoutController需要引用MainApp類。我們也沒有傳遞一個MainApp的引用到RootLayoutController。
打開MainApp類,使用下面的替代initRootLayout()方法:
/**
* Initializes the root layout and tries to load the last opened
* person file.
*/
public void initRootLayout() {
try {
// Load root layout from fxml file.
FXMLLoader loader = new FXMLLoader();
loader.setLocation(MainApp.class
.getResource("view/RootLayout.fxml"));
rootLayout = (BorderPane) loader.load();
// Show the scene containing the root layout.
Scene scene = new Scene(rootLayout);
primaryStage.setScene(scene);
// Give the controller access to the main app.
RootLayoutController controller = loader.getController();
controller.setMainApp(this);
primaryStage.show();
} catch (IOException e) {
e.printStackTrace();
}
// Try to load last opened person file.
File file = getPersonFilePath();
if (file != null) {
loadPersonDataFromFile(file);
}
}
注意兩個修改:一行*給控制器訪問MainApp*和最后三行*加載最新打開的人員文件*。
測試
做應用程序的測試驅動,你應該能夠使用菜單保存人員數據到文件中。
當你在編輯器中打開一個xml文件,你將注意到生日沒有正確保存,這是一個空的<birthday/>標簽。原因是JAXB不只奧如何轉換LocalDate到XML。我們必須提供一個自定義的LocalDateAdapter定義這個轉換。
在ch.makery.address.util中創建新的類,稱為LocalDateAdapter,內容如下:
LocalDateAdapter.java
package ch.makery.address.util;
import java.time.LocalDate;
import javax.xml.bind.annotation.adapters.XmlAdapter;
/**
* Adapter (for JAXB) to convert between the LocalDate and the ISO 8601
* String representation of the date such as '2012-12-03'.
*
* @author Marco Jakob
*/
public class LocalDateAdapter extends XmlAdapter<String, LocalDate> {
@Override
public LocalDate unmarshal(String v) throws Exception {
return LocalDate.parse(v);
}
@Override
public String marshal(LocalDate v) throws Exception {
return v.toString();
}
}
然后打開Person.jar,添加下面的注釋到getBirthday()方法上:
@XmlJavaTypeAdapter(LocalDateAdapter.class)
public LocalDate getBirthday() {
return birthday.get();
}
現在,再次測試。試着保存和加載XML文件。在重啟之后,它應該自動加載最后使用的文件。
它如何工作
讓我們看下它是如何一起工作的:
- 應用程序使用
MainApp中的main(…)方法啟動。 - 調用
public MainApp()構造函數添加一些樣例數據。 - 調用
MainApp的start(…)方法,調用initRootLayout()從RootLayout.fxml中初始化根布局。fxml文件有關於使用控制器的信息,連接視圖到RootLayoutController。 MainApp從fxml加載器中獲取RootLayoutController,傳遞自己的引用到控制器中。使用這些引用,控制器隨后可以訪問MainApp的公開方法。- 在
initRootLayout方法結束,我們試着從Perferences中獲取*最后打開的人員文件*。如果Perferences知道有這樣一個XML文件,我們將從這個XML文件中加載數據。這顯然會覆蓋掉構造函數中的樣例數據。
下一步是什么?
在本教程的第6部分,我們添加一個*生日統計圖表*。
--------------------- 本文來自 jobbible 的CSDN 博客 ,全文地址請點擊:https://blog.csdn.net/moshenglv/article/details/82877718?utm_source=copy
