這篇延續前一篇 Scene Builder 文章,說明如何在 FXML 中設定 Controller,讓 fx:id 自動注入元件,並透過 onAction 與 setController() 將按鈕事件交給 ABCL 建立的動態 Java 類別處理。最終會把前一篇透過 lookup() 和遍歷子元件的做法,改成由 Controller 統一管理。
可以在 FXML 的根元件設定 Controller,透過 fx:controller 屬性指定:
<GridPane fx:controller="YourController"></GridPane>
在 Scene Builder 中,也可以從左下方的 Controller 設定區指定:

設定 Controller 的好處是,FXML 載入時會直接與 Controller 類別關聯,並將具有 fx:id 的元件注入 Controller 實例。例如,指定顯示器的 fx:id="display":
<TextField fx:id="display" />
在 Scene Builder 中選取顯示器元件,再從右側的 Code 區域設定這個屬性。
不過目前是從 ABCL 使用 JavaFX,Controller 是在 ABCL 中建立的動態 Java 類別,不能直接在 FXML 中指定一個固定的 Controller 類別名稱。需要在 ABCL 程式碼中建立這個動態 Controller 類別,再由 new 建立類別實例,傳給 setController(controller):
(defun prepare-parent nil
(let* ((loader (new "javafx.fxml.FXMLLoader" (new "java.net.URL" (get-system-resource "res/CalcFX.fxml")))))
(#"setController" loader (new *calculator-controller-class*))
(#"load" loader)))
FXMLLoader 載入 FXML 後,會將 fx:id="display" 對應的 TextField 注入 Controller 中同名的 display 欄位:
(define-symbol-macro *calculator-controller-class*
(java:jnew-runtime-class
"CalculatorController"
;; 定義欄位:讓 JavaFX 能夠將 fx:id="display" 的元件注入進來
:fields '(("display" "javafx.scene.control.TextField"))))
除了注入元件之外,還可以綁定動作事件。將所有計算機按鈕的按下動作,綁定到 *calculator-controller-class* 的 handleButtonClick 方法:
<Button onAction="#handleButtonClick" text="7" GridPane.rowIndex="1" styleClass="calc-btn" />
<Button onAction="#handleButtonClick" text="8" GridPane.columnIndex="1" GridPane.rowIndex="1" styleClass="calc-btn" />
<Button onAction="#handleButtonClick" text="9" GridPane.columnIndex="2" GridPane.rowIndex="1" styleClass="calc-btn" />
<Button onAction="#handleButtonClick" text="4" GridPane.rowIndex="2" styleClass="calc-btn" />
<Button onAction="#handleButtonClick" text="5" GridPane.columnIndex="1" GridPane.rowIndex="2" styleClass="calc-btn" />
<Button onAction="#handleButtonClick" text="6" GridPane.columnIndex="2" GridPane.rowIndex="2" styleClass="calc-btn" />
<Button onAction="#handleButtonClick" text="1" GridPane.rowIndex="3" styleClass="calc-btn" />
<Button onAction="#handleButtonClick" text="2" GridPane.columnIndex="1" GridPane.rowIndex="3" styleClass="calc-btn" />
<Button onAction="#handleButtonClick" text="3" GridPane.columnIndex="2" GridPane.rowIndex="3" styleClass="calc-btn" />
<Button onAction="#handleButtonClick" text="C" GridPane.rowIndex="4" styleClass="calc-btn" />
<Button onAction="#handleButtonClick" text="0" GridPane.columnIndex="1" GridPane.rowIndex="4" styleClass="calc-btn" />
<Button onAction="#handleButtonClick" text="=" GridPane.columnIndex="2" GridPane.rowIndex="4"></Button>
<Button onAction="#handleButtonClick" text="/" GridPane.columnIndex="3" GridPane.rowIndex="1"></Button>
<Button onAction="#handleButtonClick" text="*" GridPane.columnIndex="3" GridPane.rowIndex="2"></Button>
<Button onAction="#handleButtonClick" text="-" GridPane.columnIndex="3" GridPane.rowIndex="3"></Button>
<Button onAction="#handleButtonClick" text="+" GridPane.columnIndex="3" GridPane.rowIndex="4"></Button>
設定元件事件處理的方式不只一種,這裡示範使用 Controller 方法的做法。
#前綴表示要綁定 Controller 中的方法。

如此一來,就不需要再透過 lookup() 找到顯示器,也不需要遍歷子元件後逐一綁定按鈕事件:
+ (defun prepare-parent nil
+ (let* ((loader (new "javafx.fxml.FXMLLoader" (new "java.net.URL" (get-system-resource "res/CalcFX.fxml")))))
+ (#"setController" loader (new *calculator-controller-class*))
+ (#"load" loader)))
(defun create-calculator-scene ()
"建立並回傳一個包含彈性計算機介面與事件綁定的 JavaFX Scene 物件"
+ (let* ((parent (prepare-parent))
+ (scene (new "javafx.scene.Scene" parent 320.0d0 420.0d0)))
- (scene (new "javafx.scene.Scene" parent 320.0d0 420.0d0))
- (display (#"lookup" scene "#display")))
- (let* ((nodes (jcall "getChildren" parent))
- (itr (jcall "iterator" nodes))
- (listener (make-event-handler
- (event) (let* ((target (#"getTarget" event))
- (label (#"getText" target)))
- (handle-button-press label display)))))
- (loop while (#"hasNext" itr)
- do (let ((elm (#"next" itr)))
- (when (jtypep elm 'javafx.scene.control.Button)
- (#"setOnAction" elm listener)))))
scene))
接著直接在 *calculator-controller-class* 中建立 handleButtonClick 方法:
(define-symbol-macro *calculator-controller-class*
(java:jnew-runtime-class "CalculatorController"
;; 定義欄位:讓 JavaFX 能夠將 fx:id="display" 的元件注入進來
:fields '(("display" "javafx.scene.control.TextField"))
;; 定義方法:對應 Scene Builder 的 `onAction` 設定
:methods `(("handleButtonClick" :void ("javafx.event.ActionEvent")
(lambda (this event)
(let* ((target (#"getTarget" event))
(label (#"getText" target))
(display (jfield "display" this)))
(handle-button-press label display)))))))
將元件注入與事件處理放進 Controller *calculator-controller-class* 後,建立 CalcFX Scene 的函式就變得非常簡單。以下是載入 FXML 的完整程式碼:
(defun prepare-parent nil
(let* ((loader (new "javafx.fxml.FXMLLoader" (new "java.net.URL" (get-system-resource "res/CalcFX.fxml")))))
(#"setController" loader (new *calculator-controller-class*))
(#"load" loader)))
(defun create-calculator-scene ()
"建立並回傳一個包含彈性計算機介面與事件綁定的 JavaFX Scene 物件"
(let* ((parent (prepare-parent))
(scene (new "javafx.scene.Scene" parent 320.0d0 420.0d0)))
scene))
現在的 CalcFX 使用 Scene Builder 建立 FXML 畫面,再由 ABCL 設定 Controller,完成顯示器元件注入與按鈕動作綁定。按下按鈕後,事件會交給 handleButtonClick,再呼叫前一篇建立的 handle-button-press 完成計算機邏輯。
至此,CalcFX 已經完成從 ABCL 建立畫面、改用 FXML 描述畫面,到使用 Scene Builder 設定元件與事件的學習路線。下一篇將回到 Java,重新使用 JavaFX 建立 CalcFX 應用畫面。