iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
Software Development

玩轉ABCL Lisp!用奇怪偏門的方式學習JavaFX系列 第 21 篇

lesson 12: 怎麼取得FXML定義的物件?重新讓CalcFX活動起來!

  • 分享至 

  • xImage
  •  

TL;DR

這篇處理使用 FXML 分離畫面之後的下一個問題:如何從 FXML 載入的場景中取得元件,並重新將 CalcFX 的計算機邏輯綁定到按鈕上。做法分成三步:透過 ID 查詢顯示器、遍歷根節點的子元件找出 Button,再為每個按鈕設定相同的事件處理器。

從 FXML 取得顯示器

透過 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 的元件。

遍歷 FXML 建立的按鈕

顯示器有明確的 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,原本的計算機邏輯就能繼續使用。

重新組合 CalcFX

完整的 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。


上一篇
lesson 11: Hello FXML:分離畫面設計與程式邏輯
下一篇
lesson 13: CalcFX with SceneBuilder:使用畫面設計工具建立計算機畫面
系列文
玩轉ABCL Lisp!用奇怪偏門的方式學習JavaFX 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言