当前位置 博文首页 > 文章内容

    JavaFX桌面应用-MVC模式开发,“真香”

    作者: 栏目:未分类 时间:2020-08-10 9:02:01

    本站于2023年9月4日。收到“大连君*****咨询有限公司”通知
    说我们IIS7站长博客,有一篇博文用了他们的图片。
    要求我们给他们一张图片6000元。要不然法院告我们

    为避免不必要的麻烦,IIS7站长博客,全站内容图片下架、并积极应诉
    博文内容全部不再显示,请需要相关资讯的站长朋友到必应搜索。谢谢!

    另祝:版权碰瓷诈骗团伙,早日弃暗投明。

    相关新闻:借版权之名、行诈骗之实,周某因犯诈骗罪被判处有期徒刑十一年六个月

    叹!百花齐放的时代,渐行渐远!



    使用mvc模块开发JavaFX桌面应用在JavaFX系列文章第一篇 JavaFX桌面应用开发-HelloWorld 已经提到过,这里单独整理使用mvc模式开发开发的流程。

    ~ JavaFX桌面应用开发系列文章 ~

    1. JavaFX桌面应用开发-HelloWorld
    2. JavaFX布局神器-SceneBuilder
    3. JavaFX让UI更美观-CSS样式
    4. JavaFX桌面应用-为什么应用老是“未响应”
    5. JavaFX桌面应用-MVC模式开发,“真香” (本文)
    6. JavaFX桌面应用-loading界面
    7. JavaFX桌面应用-表格用法

    对于mvc模式,用struts2或springmvc开发JavaEE项目的程序员来说并不陌生,mvc模式分为control(控制层)、 model(模型层)和view(视图层)。
    以springmvc为例:

    @Controller 对应控制层(struts2对应的是action)
    model 对应模型层(java bean)
    jsp及各种视图模板 对应视图层
    

    那么对JavaFX桌面应用来说,对应关系如下:

    fx:controller  控制层
    javafx.beans.property 模型层
    fxml 视图层
    

    下面是一个简单的mvc模式的JavaFX案例:

    1. 控制层

    JavaFX的控制层可以是一个简单的Java类,如果需要进行初始化那么需要实现Initializable接口。

    public class TableUI implements Initializable {
        // 对应视图层的Label标签,fx:id="time"
        public Label time;
    
        // 模型层的model
        private TableModel model = new TableModel();
        @Override
        public void initialize(URL location, ResourceBundle resources) {
            // 将视图层的Label控件和模型层的time属性进行双向绑定,这个跟vue的双向绑定有点类似。
            time.textProperty().bindBidirectional(model.timeProperty());
            // 启动新线程定时改变模型中的time属性,
            executeTimeWork();
        }
    
        private void executeTimeWork() {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            new Thread(() -> {
                while (true) {
                    // 注意:这里只需要改变model中的time属性即可,视图层的Label信息会跟着调整
                    // 因为在initialize方法中已经将time属性绑定在Label控件中了。
                    Platform.runLater(() -> model.setTime(sdf.format(new Date())));
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException ignore) {
                    }
                }
            }).start();
        }
    }
    

    2. 模型层

    模型层其实就是一个贫血模式的Java Bean,不过需要注意的是模型字段声明需要用到javafx.beans.property的相关类,因为这些类实现了观察者模式,当model的值更新时,会更新UI。

    public class TableModel {
        // 这里必须使用property的相关类
        private StringProperty time = new SimpleStringProperty();
        // Getter/Setter推荐使用IDEA来生成会生成下面3个方法,如果是用eclipse将只生成普通的getter/setter
        public String getTime() {
            return time.get();
        }
        public StringProperty timeProperty() {
            return time;
        }
        public void setTime(String time) {
            this.time.set(time);
        }
    }
    

    3 视图层

    视图层比较简单,使用fxml排版即可,这里仅使用一个Label来显示时间。

    <BorderPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1" 
      fx:controller="com.itqn.gui.javafx.wx.table.TableUI">
    <!-- 上面的fx:controller绑定了Controller,即绑定了控制层 -->
       <top>
          <HBox alignment="CENTER" prefHeight="40.0" spacing="20.0" BorderPane.alignment="CENTER">
             <children>
                <!-- fx:id跟控制层的time属性绑定 -->
                <Label fx:id="time" alignment="CENTER" contentDisplay="CENTER" prefWidth="200.0" text="Label" />
             </children>
             <BorderPane.margin>
                <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
             </BorderPane.margin>
          </HBox>
       </top>
    </BorderPane>
    

    ~ 最终效果图 ~

    总的来说,使用mvc模式来开发JavaFX要比手动管理UI控件也业务数据简便很多很多,而且model和UI控件是双向绑定的。

    4. 复合模型层

    如果只是简单的model数据,可以为每个控件声明一个队名的属性,并将双方绑定即可,但是如果遇到一些复杂的模型层数据,可能就要用到复合模型了,比如表格等。

    对于UI层中还有表格且还有其他UI控件的情况,可以使用复合模型层,因为表格这些控件需要为行单独定义模型,所以需要将多个模型进行组合。

    1. 单独定义行模型TableColumnModel

    假设表格的每一行是一项任务,由id和标题(title)组成,可以将模型定义如下, 其中selected是每一个行前面的复选框,progress是任务完成的进度。

    public class TableColumnModel {
    
        private Work work;
        private BooleanProperty selected = new SimpleBooleanProperty();
        private IntegerProperty id = new SimpleIntegerProperty();
        private StringProperty title = new SimpleStringProperty();
        private DoubleProperty progress = new SimpleDoubleProperty();
    
        public static TableColumnModel fromWork(Work work) {
            TableColumnModel model = new TableColumnModel();
            model.work = work;
            model.setSelected(false);
            model.setId(work.getId());
            model.setTitle(work.getTitle());
            model.setProgress(0);
            return model;
        }
    
        // 这里省略getter/setter
    }
    
    1. 使用复合模型定义整个UI的模型

    这里UI有一个Label和一个Table,而Table的模型是一个List集合,Table的行模型使用TableColumnModel。

    public class TableModel {
        // 时间Label模型
        private StringProperty time = new SimpleStringProperty();
        // 表格组合TableColumnModel模型
        private ObservableList<TableColumnModel> tableList = FXCollections.observableArrayList();
        public String getTime() {
            return time.get();
        }
        public StringProperty timeProperty() {
            return time;
        }
        public void setTime(String time) {
            this.time.set(time);
        }
        public ObservableList<TableColumnModel> getTableList() {
            return tableList;
        }
        public void setTableList(ObservableList<TableColumnModel> tableList) {
            this.tableList = tableList;
        }
    }
    

    这样复合模型就定义好了。

    5. 自定义表格列控件

    表格列控件可以使用fxml直接定义,也可以使用java代码来构建,一般来说只是简单的显示一些数据的表格可以在fxml中直接定义控件,如果需要有复杂的控件或者组合控件,那么推荐使用java代码来构建。

    像这种比较复杂的列控件,就可以使用java代码来构建了,列控件使用TableCell来构建,JavaFX提供了一些默认的实现:

    CheckBoxTableCell
    ChoiceBoxTableCell
    ComboBoxTableCell
    ProgressBarTableCell
    TextFieldTableCell
    

    除了JavaFX提供的TableCell,可以通过column.setCellFactory()构建自定义的TabelCell。
    对于上面的4种控件,可以分别通过以下方式来构建:

    1. 复选框

    表格中的复选框直接使用CheckBoxTableCell来构建即可。

    public static TableColumn checkboxColumn(String text, String field, int width) {
        TableColumn column = new TableColumn();
        column.setText(text);
        column.setPrefWidth(width);
        column.setCellValueFactory(new PropertyValueFactory(field));
        column.setCellFactory(CheckBoxTableCell.forTableColumn(column));
        return column;
    }
    
    1. 普通文本

    表格中的普通文本不需要额外设置CellFactory。

    public static TableColumn textColumn(String text, String field, int width) {
        TableColumn column = new TableColumn();
        column.setText(text);
        column.setPrefWidth(width);
        column.setCellValueFactory(new PropertyValueFactory(field));
        return column;
    }
    
    1. 进度条

    进度条可以需要改变一下显示效果,即在进度条后面显示进度,采用Label和ProgressBar组合而成。

    public static TableColumn progressColumn(String text, String field, int width) {
        TableColumn column = new TableColumn();
        column.setText(text);
        column.setPrefWidth(width);
        column.setCellValueFactory(new PropertyValueFactory(field));
        column.setCellFactory(v -> {
            return new TableCell<Object, Double>() {
                private HBox hBox = new HBox();
                private Label progressLabel = new Label("0% ");
                private ProgressBar progressBar = new ProgressBar();
                {
                    progressLabel.setPrefWidth(50);
                    progressLabel.setAlignment(Pos.CENTER_RIGHT);
                    hBox.getChildren().addAll(progressBar, progressLabel);
                }
                @Override
                    protected void updateItem(Double item, boolean empty) {
                    super.updateItem(item, empty);
                    if (empty) {
                        setGraphic(null);
                    } else {
                        progressBar.setProgress(item);
                        progressLabel.setText((int) ((item * 100)) + "% ");
                        setGraphic(hBox);
                    }
                    setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
                }
            };
        });
        return column;
    }
    
    1. 操作按钮组

    操作按钮组的实现跟进度条的构建方式是一样的,只是将Label和ProgressBar换成两个Button即可,这里不再贴代码。

    所有列控件构建好之后,只需要将所有列加入到表格中即可。

    private void buildTableColumn() {
        TableColumn<TableColumnModel, Boolean> selected = TableColumnBuilder.checkboxColumn("", "selected", 40);
        TableColumn<TableColumnModel, Integer> id = TableColumnBuilder.textColumn("ID", "id", 60);
        TableColumn<TableColumnModel, String> title = TableColumnBuilder.textColumn("名称", "title", 180);
        TableColumn<TableColumnModel, Double> progress = TableColumnBuilder.progressColumn("进度", "progress", 150);
        TableColumn<TableColumnModel, Integer> operator = TableColumnBuilder.operatorColumn("操作", "id", 130, this::operatorConsumer);
        table.getColumns().addAll(selected, id, title, progress, operator);
    }
    

    6. 结合业务使用

    一般来说,表格的数据是有业务模块加载出来的出来的,为了模拟真正的流程,这里采用三层架构来实现业务分层,即表示层(mvc),业务层(service),数据层(dao)。

    这里模拟实现的功能是:

    1. Controller从Service拉取数据放到Table中。
    2. 当用户点击加载的时候,模拟任务处理进度。
    3. 当用户点击删除的时候,将任务从列表中删除。

    完整是Service实现如下:

    public class TableService {
    
        private TableModel model;
        public TableService(TableModel model) {
            this.model = model;
        }
    
        public void loadTableList() {
            // 这里省略了dao层,直接随机生成模拟数据
            String[] works = new String[]{"Hi IT青年", "JavaFX MVC", "https://www.cnblogs.com/itqn/", "Wx公众号:HiIT青年"};
            for (int i = 0; i < 10; i++) {
                model.getTableList().add(TableColumnModel.fromWork(new Work(i + 1, works[(int) (Math.random() * works.length)])));
            }
        }
        
        // 处理任务加载,进度更新
        public void executeLoadWork(Integer id) {
            if (id == null) {
                return;
            }
            Optional<TableColumnModel> opt = model.getTableList().stream().filter(i -> i.getId() == id).findFirst();
            if (opt.isPresent()) {
                TableColumnModel cm = opt.get();
                new Thread(() -> {
                    while (cm.getProgress() < 1) {
                        cm.setProgress(cm.getProgress() + 0.01);
                        try {
                            TimeUnit.MILLISECONDS.sleep(100);
                        } catch (InterruptedException ignore) {
                        }
                    }
                    cm.setProgress(1);
                }).start();
            }
        }
        
        // 删除任务,将任务从模型中删除,实际可以还需要操作dao.
        public void executeDeleteWork(Integer id) {
            if (id == null) {
                return;
            }
            model.getTableList().removeIf(i -> i.getId() == id);
        }
    }
    

    最终的效果:

    =========================================================
    文章中的源码可 关注 公众号 “HiIT青年” ,发送 “javafx-mvc” 获取。

    HiIT青年
    关注公众号,阅读更多文章。