iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0
Software Development

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

lesson 16 (S2): 透過 JAVA 使用 ABCL,實現 CalcFX 邏輯

  • 分享至 

  • xImage
  •  

TL;DR

前一篇已經用 Java 載入同一份 FXML,並確認事件可以從按鈕一路進入 Java Controller。這一篇則把它接到真正的 Common Lisp 計算邏輯:Java 負責傳遞事件,ABCL 負責處理數字、運算子和顯示更新。

學習重點是:將真正的計算行為交給前面已完成的 handle-button-press,但畫面仍然是 FXML,事件入口仍然是 Java Controller。這樣一來,JavaFX 的啟動流程和 ABCL 的運算能力就能自然接起來。

先把 LISP 邏輯載入 Java

前一篇的 Java Controller 只是驗證事件真能跑到 Controller;現在需要改成使用calculator.lisp 的 handle-button-press函式,處理按鈕事件邏輯。

專案資源目錄多出一個 calculator.lisp,內容基本就是ABCL版本的CalcFX實現的邏輯內容:

CalcFX/
├── src/
│   └── CalcFX/
│       ├── CalcFX.java
│       └── CalcFXController.java
└── res/
    ├── CalcFX.fxml
    ├── calc.css
    └── calculator.lisp

同樣先用 getResource() 找到 Lisp 資源,再把 URL 轉成一個可供 ABCL load 的字串,執行載入:

URL calcLisp = CalcFX.class.getResource("/res/calculator.lisp");
if (calcLisp == null) {
    throw new IllegalStateException("找不到 res/calculator.lisp");
}

String loadCommand = "(load \"" + calcLisp.toExternalForm() + "\")";
interpreter.eval(loadCommand);

把 Lisp 原始碼放在資源目錄,並在執行時再載入它。這也和 FXML、CSS 的讀取方式保持一致:最後都會被複製到 class/ 下面:

cp -r res class/

建立並取得 ABCL Interpreter

ABCL 的 Interpreter 是全域單例,createInstance() 會建立並初始化解譯器;如果已經存在,它就會回傳 null。getInstance() 則只是取回已建立的實例。

也就是說,啟動順序不能亂:在 JavaFX 建立 Controller 並使用到 Interpreter 之前,必須先建立 Interpreter,並先載入 calculator.lisp。之後 Controller 才能用 Interpreter.getInstance() 取得同一個執行環境。

這個初始化步驟放在 main 裡面最自然:

package CalcFX;

import java.net.URL;

import org.armedbear.lisp.Interpreter;

public class CalcFX extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        // 省略:FXML 載入與場景建立
    }

    public static void main(String[] args) {
        Interpreter interpreter = Interpreter.createInstance();
        URL calcLisp = CalcFX.class.getResource("/res/calculator.lisp");
        if (calcLisp == null) {
            throw new IllegalStateException("找不到 res/calculator.lisp");
        }

        String loadCommand = "(load \"" + calcLisp.toExternalForm() + "\")";
        interpreter.eval(loadCommand);
        launch(args);
    }
}

從 CL-USER 取出 HANDLE-BUTTON-PRESS

簡化calculator.lisp ,函式不需要再放在自訂 package 之中,直接用 defun 定義:

- (in-package :abcl-javafx)
(defun handle-button-press (label display)
  ....)

依 Common Lisp 慣例,函式會落在 CL-USER Package 裡面。在 Java 使用,要先定位 CL-USER,再找出它的 symbol,最後從 symbol 取出函式物件:

Package clUser = Packages.findPackage("CL-USER");
Symbol buttonPressSymbol = clUser.findAccessibleSymbol("HANDLE-BUTTON-PRESS");
Function buttonPressFunction = (Function) buttonPressSymbol.getSymbolFunction();

這段準備工作可以放進 CalcFXController 的建構子:

import org.armedbear.lisp.Interpreter;
import org.armedbear.lisp.Packages;
import org.armedbear.lisp.Package;
import org.armedbear.lisp.Symbol;
import org.armedbear.lisp.Function;

public class CalcFXController {

    public TextField display;

    private final Interpreter interpreter;
    private final Function buttonPressFunction;

    public CalcFXController() {
        interpreter = Interpreter.getInstance();
        if (interpreter == null) {
            throw new IllegalStateException("ABCL Interpreter 尚未初始化");
        }

        Package clUser = Packages.findPackage("CL-USER");
        Symbol buttonPressSymbol = clUser.findAccessibleSymbol("HANDLE-BUTTON-PRESS");
        if (buttonPressSymbol == null || buttonPressSymbol.getSymbolFunction() == null) {
            throw new IllegalStateException("找不到 CL-USER:HANDLE-BUTTON-PRESS");
        }
        buttonPressFunction = (Function) buttonPressSymbol.getSymbolFunction();
    }
}

從按鈕事件呼叫 Lisp

FXML 的按鈕仍然使用 onAction="#handleButtonClick",所以 Java Controller 會收到 ActionEvent。處理方法從事件取得按鈕文字,再把 Java String 轉成 ABCL 的 SimpleString;顯示器則用 JavaObject 包裝,作為第二個參數傳給 Lisp 函式:

import javafx.scene.control.Button;
import org.armedbear.lisp.JavaObject;
import org.armedbear.lisp.SimpleString;

public void handleButtonClick(ActionEvent event) {
    Button target = (Button) event.getTarget();
    String label = target.getText();

    SimpleString lispLabel = new SimpleString(label);
    JavaObject lispDisplay = new JavaObject(display);
    buttonPressFunction.execute(lispLabel, lispDisplay);
}

交互的重點:

  • 第一個參數是 Lisp 字串:"7"、"+"、"="
  • 第二個參數是包裝好的 JavaFX TextField,Lisp 仍然可以透過 Java 互操作直接呼叫 setText,更新顯示器內容。

至此,數字輸入、清除、四則運算、等號計算與顯示更新,都已經重新使用ABCL的實現,不需要用 Java 寫成。


CalcFXController 的完整版本如下:

package CalcFX;

import javafx.event.ActionEvent;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import org.armedbear.lisp.Function;
import org.armedbear.lisp.Interpreter;
import org.armedbear.lisp.JavaObject;
import org.armedbear.lisp.Package;
import org.armedbear.lisp.Packages;
import org.armedbear.lisp.SimpleString;
import org.armedbear.lisp.Symbol;

public class CalcFXController {

    public TextField display;

    private final Interpreter interpreter;
    private final Function buttonPressFunction;

    public CalcFXController() {
        interpreter = Interpreter.getInstance();
        if (interpreter == null) {
            throw new IllegalStateException("ABCL Interpreter 尚未初始化");
        }

        Package clUser = Packages.findPackage("CL-USER");
        Symbol buttonPressSymbol = clUser.findAccessibleSymbol("HANDLE-BUTTON-PRESS");
        if (buttonPressSymbol == null || buttonPressSymbol.getSymbolFunction() == null) {
            throw new IllegalStateException("找不到 CL-USER:HANDLE-BUTTON-PRESS");
        }
        buttonPressFunction = (Function) buttonPressSymbol.getSymbolFunction();
    }

    public void handleButtonClick(ActionEvent event) {
        Button target = (Button) event.getTarget();
        String label = target.getText();

        SimpleString lispLabel = new SimpleString(label);
        JavaObject lispDisplay = new JavaObject(display);
        buttonPressFunction.execute(lispLabel, lispDisplay);
    }
}

編譯與執行

Java 還需要知道 ABCL JAR 在哪裡,編譯時要把它加進 CLASSPATH:

CLASSPATH="${CLASSPATH}:<abcl.jar檔案位置>:<abcl-contrib.jar檔案位置>"

mkdir -p class

javac \
  --module-path "${SDK_PATH}" \
  --add-modules javafx.controls,javafx.fxml \
  --class-path "${CLASSPATH}" \
  -d class \
  src/CalcFX/*.java

cp -r res class/

執行時也要讓 JVM 找得到 ABCL:

java \
  --module-path "${SDK_PATH}" \
  --add-modules javafx.controls,javafx.fxml \
  -cp "class:${CLASSPATH}" \
  CalcFX.CalcFX

在 Windows 上,classpath 多個路徑的分隔符號是分號 ;;Linux 和 macOS 則使用冒號 :。實際設定 CLASSPATH 時要依作業系統調整。

執行結果

畫面由 FXML 描述、Java 負責載入視窗並接收事件、計算邏輯則仍留在 Lisp。資料流可以整理成:

FXML Button
    ↓ onAction
CalcFXController.handleButtonClick()
    ↓ label + display
ABCL HANDLE-BUTTON-PRESS
    ↓ 更新 TextField
JavaFX 顯示計算結果

https://ithelp.ithome.com.tw/upload/images/20261008/20112470q7ROMHkSsh.png

結論

到這裡,CalcFX 已經從「ABCL 直接寫 Controller」變成「Java 進入點 + ABCL 邏輯」的混合架構。

  • JavaFX 的事件與資源管理由 Java 做
  • 計算邏輯仍然保留在 Lisp,方便重用與驗證。也方便調整與邏輯修改,保留足夠彈性。
  • 更重要的是,這有利於分發,馬上就會提到。

上一篇
lesson 15 (S2): 另一條從JAVA來的路,改用 JAVA 建置 Hello CalcFX 畫面
系列文
玩轉ABCL Lisp!用奇怪偏門的方式學習JavaFX 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言