zoukankan      html  css  js  c++  java
  • 【JavaFx教程】第五部分:将数据用 XML 格式存储

    Screenshot AddressApp Part 5

    第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。我们使用称为JAXBJava 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注释作为最顶层的类。personDataObservableList类,我们不能把任何注释放到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:
    " + 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:
    " + file.getPath())
                    .showException(e);
        }
    }
    

    *编组和解组*已经准备好,让我们创建保存和加载的菜单实际的使用它。

    处理菜单响应

    在我们RootLayout.fxml中,这里已经有一个菜单,但是我们没有使用它。在我们添加响应到菜单中之前,我们首先创建所有的菜单项。

    在Scene Builder中打开RootLayout.fxml,从*library*组中拖曳必要的菜单到*Hierarchy*组的MemuBar中。创建NewOpen…SaveSave 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
    Website: http://code.makery.ch")
                .showInformation();
        }
    
        /**
         * Closes the application.
         */
        @FXML
        private void handleExit() {
            System.exit(0);
        }
    }
    

    FileChooser

    注意在上面的RootLayoutController中使用FileCooser的方法。首先,创建新的FileChooser类对象的,然后,添加扩展名过滤器,以至于只显示以.xml结尾的文件。最后,文件选择器显示在主Stage的上面。

    如果用户没有选择一个文件关闭对话框,返回null。否则,我们获得选择的文件,我们能传递它到MainApploadPersonDataFromFile(…)savePersonDataToFile()方法中。

    连接fxml视图到控制器

    1. 在Scene Builder中打开RootLayout.fxml。在*Controller*组中选择RootLayoutController作为控制器类。

    2. 回到*Hierarchy*组中,选择一个菜单项。在*Code*组中On Action下,应该看到所有可用控制器方法的选择。为每个菜单项选择响应的方法。
      菜单动作

    3. 为每个菜单项重复第2步。

    4. 关闭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文件。在重启之后,它应该自动加载最后使用的文件。

    它如何工作

    让我们看下它是如何一起工作的:

    1. 应用程序使用MainApp中的main(…)方法启动。
    2. 调用public MainApp()构造函数添加一些样例数据。
    3. 调用MainAppstart(…)方法,调用initRootLayout()RootLayout.fxml中初始化根布局。fxml文件有关于使用控制器的信息,连接视图到RootLayoutController
    4. MainApp从fxml加载器中获取RootLayoutController,传递自己的引用到控制器中。使用这些引用,控制器随后可以访问MainApp的公开方法。
    5. initRootLayout方法结束,我们试着从Perferences中获取*最后打开的人员文件*。如果Perferences知道有这样一个XML文件,我们将从这个XML文件中加载数据。这显然会覆盖掉构造函数中的样例数据。

    下一步是什么?

    在本教程的第6部分,我们添加一个*生日统计图表*。

    --------------------- 本文来自 jobbible 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/moshenglv/article/details/82877718?utm_source=copy 

  • 相关阅读:
    socket.io带中文时客户端无法响应
    JQ树插件 — zTree笔记
    cecium 笔记
    express处理跨域问题,中间件 CORS
    一些接口
    express 3.5 Err: request aborted
    ovirt kvm嵌套虚拟化
    kvm实现快速增量盘模式的克隆脚本
    kvm命令
    kvm 中 Guest Is already in use 处理办法
  • 原文地址:https://www.cnblogs.com/jobbible/p/9718475.html
Copyright © 2011-2022 走看看