iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Software Development

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

lesson 8: 資料雙向綁定:Hello {Name}!發揮 ABCL 的威力

  • 分享至 

  • xImage
  •  

TL;DR

這篇從 TextField 開始,先完成一個可以詢問使用者名字的 Hello {Name} 範例,再透過 SimpleStringProperty 與 bindBidirectional() 了解 JavaFX 的資料雙向綁定。最後將相同的做法延伸成可以動態評估 Common Lisp 程式碼的 ABCL Eval 場景。

Hello, {Name}:詢問使用者的名字

單一按鈕(Button)只能完成有限的情境處理,所以除了按鈕,也需要其他互動元件。

先以簡單的「Hello {Name}」範例來說:允許使用者輸入自己的名字,程式會向使用者打招呼。這是一個簡單的 Hello World 變形。

在這個需求中,需要 javafx.scene.control.TextField 物件讓使用者可以輸入自己的名字。我們先定義 +TextField+:

(defconstant +TextField+ "javafx.scene.control.TextField")

容器 box 只保留 text-field 和 btn-hello:

          (box (new +HBox+ 50.5 (jnew-array-from-list +Secne.Node+
                                 `(,text-field ,btn-hello))))

VBox佈局同樣可以,不過HBox在使用上更直覺些。兩者在程式使用上基本沒有差異,可以直接替換。

按下按鈕時,透過 TextField.getText() 取得使用者在 TextField 輸入的內容,並顯示在訊息對話框中。

(#"setOnAction" btn-hello
                    (make-event-handler (event)
                    (declare (ignore event))
                    (prog* ((data-msg (format nil "Hello, ~A!~%" (#"getText" text-field)))
                         (alert (make-messagebox data-msg)))
                    (#"show" alert))))

也可以在動作處理最後使用 TextField.setText("") 清空輸入內容,便於下一次輸入:

(#"setText" text-field "")

資料雙向綁定:綁定字串屬性

TextField 是很簡單的互動元件,主要透過文字屬性(Text Property)與使用者互動。因此由 TextField 進一步了解 JavaFX 雙向綁定的做法再適合不過。

所謂雙向綁定,就是在程式中存在一個屬性:當程式設定屬性值時,畫面會同步變化;使用者透過介面輸入時,屬性值也會同步更新。SimpleStringProperty 是符合 TextField 綁定需求的「簡單文字屬性」物件。

(setq name (new "javafx.beans.property.SimpleStringProperty" ""))

再來透過TextField.textProperty.bindBidirectional(property)綁定資料屬性:

(chain text-field "textProperty" ("bindBidirectional" name))

SimpleStringProperty的用法很簡單,主要就是get()和set(string),可以用這些替換getText()和setText(string):

(#"setOnAction" btn-hello
                    (make-event-handler (event)
                    (declare (ignore event))
                    (prog* ((data-msg (format nil "Hello, ~A!~%" (#"get" name)))
                                   (alert (make-messagebox data-msg)))
                         (#"set" name "")
                         (#"show" alert))))

雖然這只是一個很簡單的例子,幾乎不需要替換成雙向綁定的做法,不過透過 TextField 進行雙向綁定,可以先理解這種機制的效果。面對更複雜的資料結構與互動元件時,雙向綁定能夠簡化程式寫法與設計邏輯。

完整的 Hello 場景建立如下:

注意:不再是JAVA Info的場景,而是Hello場景。
這並不是這一篇的重點,就簡單提醒下。

(defun create-hello-scene ()
  (prog* ((text-field  (new +TextField+))
          (name (new "javafx.beans.property.SimpleStringProperty" ""))
          (btn-hello (new +Button+ "Hello"))
          (box (new +HBox+ 50.5 (jnew-array-from-list +Secne.Node+
                                 `(,text-field ,btn-hello))))
          (scene-style (get-system-resource "res/style.css"))
          (result (new +scene+ box 640 480)))
     (chain (#"getStyleClass" box) ("add" "info-box"))
     (chain (#"getStylesheets" result)
            ("add" scene-style))
     (#"setPromptText" text-field "請出入你的名字:")
     (chain text-field "textProperty" ("bindBidirectional" name))
     (#"setOnAction" btn-hello
                     (make-event-handler
                      (event)
                      (declare (ignore event))
                      (prog* ((data-name (format nil "Hello, ~A!~%" (or (#"get" name)
                                                                    "No Name")))
                              (alert (make-messagebox data-name)))
                         (#"set" name "")
                         (#"show" alert))))
     (return result)))

發揮 ABCL 的動態開發能力:在 JavaFX 中評估 Common Lisp,靈活開發應用程式

到目前為止,開發學習是使用 ABCL 的 REPL 環境,還是使用 ABCL CLI 讀取並執行檔案?

最開始我是使用 REPL 環境,或者說透過 Emacs 裡的 Slime(後來看到也有人推薦改用 Sly,不過我還沒有用過)。但因為一個 JVM 實例只能執行一次 JavaFX Application,因此後來改成讀取執行檔案的方式,無法很好地發揮 Lisp 動態開發的優勢。(每次都要啓動一次JRE環境)

但現在已經湊齊了所有需要的 JavaFX 元件。我們可以利用 JavaFX 建立一個 Lisp 評估環境(eval)。這個環境有 JavaFX,也有一個正在運行的 JavaFX Application,並且能夠接受新的評估內容(form)。

那麼就來搭建一個 ABCL Eval 場景:

(defun create-abcl-eval-scene ()
  (prog* ((text-area  (new +TextArea+))
                  (forms (new "javafx.beans.property.SimpleStringProperty" ""))
                  (eval-btn (new +Button+ "Eval"))
                  (box (new +VBox+ 50.5 (jnew-array-from-list +Secne.Node+
                                        `(,text-area ,eval-btn))))
                  (scene-style (get-system-resource "res/style.css"))
                  (result (new +scene+ box 640 480)))
     (chain (#"getStyleClass" box) ("add" "info-box"))
     (chain (#"getStylesheets" result)
            ("add" scene-style))
     (#"setPromptText" text-area "Eval Forms")
     (chain text-area "textProperty" ("bindBidirectional" forms))
     (#"setOnAction" eval-btn
                     (make-event-handler
                      (event)
                      (declare (ignore event))
                      (prog* ((_forms (#"get" forms)))
                         (#"show" (make-messagebox (format nil "Eval Value: ~A"  (eval (read-from-string _forms)))))
                         (#"set" forms ""))))
     (return result)))

就這麼簡單!

這段程式碼幾乎和「Hello {Name}」沒有差異。TextField 改成 TextArea,用來支援多行文字;name 改成 forms 只是變更變數名稱;box 則從 HBox 調整回 VBox。真正的變化在於按鈕動作的處理:

(#"setOnAction" eval-btn
            (make-event-handler  (event)
              (#"show" (make-messagebox (format nil "Eval Value: ~A"  
                (eval                                           ;; 評估
                  (read-from-string (#"get" forms))))))  ;; 將字串轉換成評估形式
                (#"set" forms "")))

最重要的地方在於 #'eval 和 #'read-from-string。這會將使用者輸入的文字視作 Lisp 程式碼進行評估執行。接著再避免使用者完全沒有輸入時發生錯誤:

(#"setOnAction" eval-btn
           (make-event-handler
             (event)
             (declare (ignore event))
             (prog* ((_forms (#"get" forms)))
               (if (string/= "" _forms)        ;; 檢查使用者是否有輸入
                 (#"show" (make-messagebox (format nil "Eval Value: ~A"  (eval (read-from-string _forms)))))   ;; 如果有輸入,進行評估
                 (#"show" (make-messagebox "No Forms")))  ;; 提示使用者,沒有評估的內容
               (#"set" forms ""))))

在主程式將主場景換成「ABCL Eval」:

(defun +MY-APP.start+ (this stage)
  (prog ((scene (create-abcl-eval-scene))    ;; 換成ABCL Eval場景
         (icon-uri (get-system-resource "res/icon32x32.png")))
     (#"setUserAgentStylesheet" this "file:mystyle.css" )
     (#"setScene" stage scene)
     (#"setTitle" stage "Hello JavaFX")
     (chain (#"getIcons" stage)
            ("add" (new "javafx.scene.image.Image" icon-uri)))
     (#"show" stage)))

執行結果

執行主程式後,可以在輸入框輸入:

(+ 1 2)

會跳出「Eval Value: 3」的訊息。可以執行任意合法的LISP程式,更強大的是可以運用JavaFX的元件,像是輸入:

(let* ((stage (jnew "javafx.stage.Stage"))
       (scene (abcl-javafx::create-hello-scene)))
  (#"setScene" stage scene)
  (#"setTitle" stage "Hello {Name}")
  (#"show" stage))

按下評估(Eval)以後,會跳出「Hello {Name}」的視窗。

https://ithelp.ithome.com.tw/upload/images/20260930/20112470oAgJTwMi4C.png

至此,JavaFX 與 ABCL 的基礎準備已經完成。接下來將建立一個計算機應用程式 CalcFX,將前面學到的場景、元件、事件與資料處理組合起來。


上一篇
lesson 7: 第一個互動元件,按鈕:Hello Button & MessageBox & Action
下一篇
lesson 9: CalcFX 畫面:用 ABCL + JavaFX 實現計算機畫面
系列文
玩轉ABCL Lisp!用奇怪偏門的方式學習JavaFX 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言