2014年10月17日 星期五

JavaFX Cell Factory (3)

表格 (Table) 為視窗程式中重要的物件之一,常運用於試算表與資料庫,表格以直行 (Column) 與橫列 (Row) 的方式呈現資料,每一直行與橫列交會之處稱為表格單元 (Table Cell)。

表格物件由Table View、Table Column、Table Row與Table Cell所組成,TableView類別用以置放直行、橫列與表格單元,並顯示表格物件,為表格物件的最上層,如同TreeViewComboBoxListView類別一般,可修改各表格單元的形式,不同的是,JavaFX的表格分別依據直行或橫列設定其Cell Factory。

表格單元的類別為javafx.scene.control.TableCell,繼承自TableCell的類別包括:

  • 核取方塊表格單元:CheckBoxTableCell.
  • 選項方塊表格單元:ChoiceBoxTableCell.
  • 複合方塊表格單元:ComboBoxTableCell.
  • 進度列表格單元:ProgressBarTableCell.
  • 文字欄位表格單元:TextFieldTableCell.
因此可使用核取方塊、選項方塊、複合方塊、進度列與文字欄位等取代原表格單元,較樹狀單元多了進度列物件。

以下範例以繼承TableCell類別之自定TextFieldCellCallback類別做為setCellFactory()方法的回呼函式。在TextFieldCellCallback類別的建構函式中,以setAlignment()方法設定欄位內容為置中對齊,並建立快顯選單,當在表格上按下滑鼠右鍵時,將顯示快顯選單以新增一橫列:

private final class TextFieldCellCallback extends TableCell<> {
  TextField textfield;
  ContextMenu contextmenu = new ContextMenu();

  // 建構函式
  public TextFieldCellCallback() {
    // 設定欄位內容為置中對齊
    setAlignment(Pos.CENTER);
    // 建立快顯選單
    MenuItem menuitem = new MenuItem("Add New Row");
    menuitem.setGraphic(new ImageView(new Image(
      getClass().getResourceAsStream("images/row.gif"))));

    menuitem.setOnAction((ActionEvent e) -> {
      // 新增一表格橫列
      data.add(new Product("", "", "", "", ""));
    });
    contextmenu.getItems().add(menuitem);
  }
  ...
}
...
TextFieldCellCallback類別中,分別覆寫TableCell類別的startEdit()cancelEdit()updateItem()方法以處理開始編輯、取消編輯與更新表格單元等。

以開始編輯為例,在覆寫startEdit()方法中,當以滑鼠點選表格單元時,則以setGraphic()方法將表格單元設定為文字欄位,以此編輯表格單元的內容。此外,並以setOnKeyReleased()方法處理當在表格單元文字欄位釋放鍵盤按鍵時之事件,若按下Enter鍵,代表完成輸入,則以commitEdit()方法完成編輯表格單元;若按下ESC鍵,代表取消輸入,則以cancelEdit()方法取消編輯表格單元:

// 覆寫TableCell類別的startEdit()方法
@Override public void startEdit() {
  super.startEdit();

  if (textfield == null) {
    textfield = new TextField(getItem() == null ? "" : getItem());
    textfield.setMinWidth(this.getWidth()-this.getGraphicTextGap()*2);
    
    textfield.setOnKeyReleased((KeyEvent e) -> {
      if (e.getCode() == KeyCode.ENTER) {
        // 完成編輯表格單元
        commitEdit(textfield.getText());
      } 
      else if (e.getCode() == KeyCode.ESCAPE) {
        // 取消編輯表格單元
        cancelEdit();
      }
    });
  }

  setText(null);
  // 將表格單元設定為文字欄位
  setGraphic(textfield);
  textfield.selectAll();
}
...
須注意的是,由於需要編輯表格單元,因此須以TableView類別的setEditable()方法設定Table View為可編輯狀態,並以TableView類別的setCellFactory()方法以下列方式設定Cell Factory的回呼函式:
TableView<Product> tableView = new TableView<>();
// 設定Table View為可編輯狀態
tableView.setEditable(true);

TableColumn column[] = new TableColumn[title.length];

column[i].setCellFactory(new Callback<TableColumn<Product, String>, 
  TableCell<Product, String>>() {
  @Override public TableCell<Product, String> call(
    TableColumn<Product, String> value) {
      return new TextFieldCellCallback();
  }
});
...
【執行結果】 
藉由繼承TableCell類別之自定類別做為setCellFactory()方法的回呼函式,可使用各種物件取代原表格單元,以便編輯表格單元內容

此方式雖然可行,但必須覆寫TableCell類別的startEdit()cancelEdit()commitEdit()updateItem()方法,因此有一定難度。 此外,JavaFX提供以下類別,可分別以核取方塊、選項方塊、複合方塊、進度列與文字欄位等取代原表格單元類別彼此之間所提供的方法十分類似,使用上亦很容易:
  • 核取方塊表格單元:CheckBoxTableCell.
  • 選項方塊表格單元:ChoiceBoxTableCell.
  • 複合方塊表格單元:ComboBoxTableCell.
  • 進度列表格單元:ProgressBarTableCell.
  • 文字欄位表格單元:TextFieldTableCell.
CheckBoxTableCell類別類似於CheckBox類別,以建立核取方塊樣式的表格單元,功能與核取方塊十分類似,其選取狀態分為未勾選與已勾選。以下範例以TableColumn類別的setCellFactory()方法設定Cell Factory為CheckBoxTableCell,並以其forTableColumn()方法以核取方塊取代原表格單元,相較於之前的範例,範例更為精簡:
TableColumn column[] = new TableColumn[title.length];

for (int i=0; i<title.length; i++) {
  column[i] = new TableColumn(title[i]);
  column[i].setMinWidth(width[i]);
  // 設定直行標題的圖像
  column[i].setGraphic(new ImageView(new Image(
    getClass().getResourceAsStream("images/icon" + (i+1) + ".gif"))));

  if (i!=4) {
    // 設定直行對應於資料陣列的順序
    column[i].setCellValueFactory(
      new PropertyValueFactory<>(cellValue[i]));
  }
  else { // CheckBoxTableCell
    // 設定直行對應於資料陣列的順序
    column[i].setCellValueFactory(
      new PropertyValueFactory<>(cellValue[i]));

    // 設定Cell Factory
    column[i].setCellFactory(
      CheckBoxTableCell.forTableColumn(column[i]));
    column[i].setEditable(true);
  }
}
...
【執行結果】 
ChoiceBoxTableCell類別類似於ChoiceBox類別,以建立選項方塊樣式的表格單元,功能與選項方塊十分類似,當點選表格單元時,則以「下拉式選單」的方式列出選項項目,由於包含一個以上的選項項目,因此ChoiceBoxTableCell類別必須以陣列定義一組選項項目,以做為「下拉式選單」的選項內容。以下範例以TableColumn類別的setCellFactory()方法設定Cell Factory為ChoiceBoxTableCell,並以其forTableColumn()方法以選項方塊取代原表格單元,當點選表格單元時,則以「下拉式選單」的方式列出選項項目,其中以ObservableList設定選項方塊的選項項目:
TableColumn column[] = new TableColumn[title.length];
...

// 設定選項方塊的選項項目
ObservableList<String> items = FXCollections.observableArrayList(
  "JX001", "JX002", "JX003", "JX004", "JX005", 
  "JX006", "JX007", "JX008", "JX009", "JX010");

column[i] = new TableColumn(title[i]);
// 設定直行對應於資料陣列的順序
column[i].setCellValueFactory(
  new PropertyValueFactory<>(cellValue[i]));
// 設定Cell Factory
column[i].setCellFactory(
  ChoiceBoxTableCell.forTableColumn(items));
column[i].setEditable(true);
...
【執行結果】 
ComboBoxTableCell類別類似於ComboBox類別,以建立複合方塊樣式的表格單元,功能與複合方塊十分類似。以下範例以TableColumn類別的setCellFactory()方法設定Cell Factory為ComboBoxTableCell,並以其forTableColumn()方法以複合方塊取代原表格單元,當點選表格單元時,則以「下拉式選單」的方式列出選項項目,其中以ObservableList設定複合方塊的選項項目:
TableColumn column[] = new TableColumn[title.length];
...

// 設定選項方塊的選項項目
ObservableList<String> items = FXCollections.observableArrayList(
  "JX001", "JX002", "JX003", "JX004", "JX005", 
  "JX006", "JX007", "JX008", "JX009", "JX010");

column[i] = new TableColumn(title[i]);
// 設定直行對應於資料陣列的順序
column[i].setCellValueFactory(
  new PropertyValueFactory<>(cellValue[i]));
// 設定Cell Factory
column[i].setCellFactory(
  ComboBoxTableCell.forTableColumn(items));
column[i].setEditable(true);
...
【執行結果】 
ProgressBarTableCell類別類似於ProgressBar類別,以建立進度列樣式的表格單元,顯示進度之用。為處理進度列顯示進度,範例以繼承Task抽象類別之自定ProgressBarTask類別做為處理進度之用:
private final class ProgressBarTask extends Task<Void> {
  int waiting; 
  int pausing;

  ProgressBarTask(int _waiting, int _pausing) {
    this.waiting = _waiting;
    this.pausing = _pausing;
  }

  @Override protected Void call() throws Exception {
    this.updateProgress(ProgressIndicator.INDETERMINATE_PROGRESS, 1);
    this.updateMessage("Waiting...");
    Thread.sleep(waiting);
    this.updateMessage("Running...");
    
    for (int i=0; i<500; i++) {
      updateProgress((1.0*i)/500, 1);
      Thread.sleep(pausing);
    }
    this.updateMessage("Done");
    this.updateProgress(1, 1);
    return null;
  }
}
...
接著以TableColumn類別的setCellFactory()方法設定Cell Factory為ProgressBarTableCell,並以其forTableColumn()方法以進度列取代原表格單元:
Random randomv = new Random();

for (int i=0; i<10; i++) {
  tableView.getItems().add(new ProgressBarTask(
    randomv.nextInt(5000)+1000, randomv.nextInt(50)+10));
}

TableColumn<ProgressBarTask, Double> column1 = 
  new TableColumn("Progress");
column1.setGraphic(new ImageView(new Image(
  getClass().getResourceAsStream("images/icon1.gif"))));
// 設定直行對應於資料陣列的順序
column1.setCellValueFactory(new PropertyValueFactory<>("progress"));
// 設定Cell Factory
column1.setCellFactory(
  ProgressBarTableCell.<ProgressBarTask>forTableColumn());

TableColumn<ProgressBarTask, String> column2 = 
  new TableColumn("Status");
column2.setGraphic(new ImageView(new Image(
  getClass().getResourceAsStream("images/icon2.gif"))));
// 設定直行對應於資料陣列的順序
column2.setCellValueFactory(new PropertyValueFactory<>("message"));
column2.setPrefWidth(80);
...
【執行結果】 
TextFieldTableCell類別類似於TextField類別,以建立文字欄位樣式的表格單元,當以滑鼠點選表格單元時,則將原表格單元轉換為文字欄位,以此編輯表格單元內容。以下範例以TableColumn類別的setCellFactory()方法設定Cell Factory為TextFieldTableCell,並以其forTableColumn()方法以文字欄位取代原表格單元,當以滑鼠點選表格單元時,則將原表格單元轉換為文字欄位,以此編輯表格單元內容:
TableColumn column[] = new TableColumn[title.length];
...

column[i] = new TableColumn(title[i]);
column[i].setMinWidth(width[i]);
// 設定直行對應於資料陣列的順序
column[i].setCellValueFactory(
  new PropertyValueFactory<>(cellValue[i]));
// 設定Cell Factory
column[i].setCellFactory(TextFieldTableCell.forTableColumn());
column[i].setEditable(true);
...
【執行結果】 
【參考資料】

[1] Java Official Web Site:http://www.oracle.com/technetwork/java/index.html
[2] JavaFX:http://www.oracle.com/technetwork/java/javafx
[3] JavaFX 8.0 API Specification.
[4] Java Platform, Standard Edition 8 API Specification.

© Chia-Hui Huang

1 則留言:

  1. 發現一個好東西…應該是用JavaFX做的吧

    http://fineexcel.com/

    回覆刪除