JavaFX桌面應用-MVC模式開發,“真香” JavaFX桌面應用-MVC模式開發,“真香”

摘自:https://www.cnblogs.com/itqn/p/13462943.html

JavaFX桌面應用-MVC模式開發,“真香”

 

使用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青年
關注公衆號,閱讀更多文章。

 
標籤: JavaFX
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章