這篇處理使用 FXML 分離畫面之後的下一個問題:如何從 FXML 載入的場景中取得元件,並重新將 CalcFX 的計算機邏輯綁定到按鈕上。做法分成三步:透過 ID 查詢顯示器、遍歷根節點的子元件找出 Button,再為每個按鈕設定相同的事件處理器。
透過 FXML 建立場景畫面後,畫面物件不再由 Lisp 程式碼逐一建立。這時首先要解決的是:如何取得 FXML 建立的物件?
在前一篇的 CalcFX.fxml 中,顯示器設定了 id="display"。它可以像 CSS 選擇器一樣被查詢:
<children>
<TextField id="display"
text="0"
editable="false"
styleClass="display"
GridPane.columnSpan="4"
maxWidth="1.7976931348623157E308" />
</children>
JavaFX 的 lookup() 可以用 "#display" 找到這個 TextField:
(let* ((parent (jstatic "load"
"javafx.fxml.FXMLLoader"
(new "java.net.URL"
(get-system-resource "res/CalcFX.fxml"))))
(scene (new "javafx.scene.Scene" parent 320.0d0 420.0d0))
(display (#"lookup" scene "#display")))
......)
這裡的 parent 是 FXML 載入後的根節點,scene 則是放入根節點後建立的 JavaFX 場景。因為 Scene 也提供 lookup(),所以可以直接從場景中查詢指定 ID 的元件。
顯示器有明確的 ID,可以直接查詢;按鈕則有許多個,而且目前沒有為每一個按鈕指定獨立 ID。因此可以取得根節點的 children,再逐一檢查其中的元件類型:
(let* ((nodes (jcall "getChildren" parent))
(itr (jcall "iterator" nodes)))
(loop while (#"hasNext" itr)
do (let ((element (#"next" itr)))
(when (jtypep element 'javafx.scene.control.Button)
......))))
GridPane 的直接子節點包含顯示器與所有按鈕。透過 jtypep 判斷元件是否為 Button,就能排除顯示器,只處理按鈕。
找到按鈕之後,就可以使用前面建立的 #'make-event-handler 與 #'handle-button-press 綁定動作事件。事件處理器可以共用同一個實例,按下按鈕時再從事件取得實際按鈕,讀取它的文字作為按鍵標籤:
(let ((listener
(make-event-handler
(event)
(let* ((target (#"getTarget" event))
(label (#"getText" target)))
(handle-button-press label display)))))
......)
這樣就不需要為每一個按鈕分別撰寫事件處理。每個按鈕只要把自己的文字交給同一個 handle-button-press,原本的計算機邏輯就能繼續使用。
完整的 create-calculator-scene 如下:
(defun create-calculator-scene ()
"建立並回傳一個包含 FXML 畫面與事件綁定的 JavaFX Scene 物件"
(let* ((parent (jstatic "load"
"javafx.fxml.FXMLLoader"
(new "java.net.URL"
(get-system-resource "res/CalcFX.fxml"))))
(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 ((element (#"next" itr)))
(when (jtypep element 'javafx.scene.control.Button)
(#"setOnAction" element listener)))))
scene))
這次修改只改變畫面的取得方式,沒有改寫前一篇的計算機邏輯。FXML 負責描述 GridPane、顯示器與按鈕,ABCL 負責載入場景、找到元件並連接事件,兩者的分工就清楚了。
重新執行 CalcFX 後,FXML 建立的按鈕已經可以觸發原本的 handle-button-press,顯示器也會隨著數字輸入與運算結果更新。至此,CalcFX 已完成從 Lisp 程式建立畫面,到使用 FXML 描述畫面並由 ABCL 重新接回計算邏輯的轉換。
下一篇將使用 SceneBuilder 建立 CalcFX 畫面,讓 FXML 的編輯不必完全依靠手寫 XML。