前一篇已經用 Java 載入同一份 FXML,並確認事件可以從按鈕一路進入 Java Controller。這一篇則把它接到真正的 Common Lisp 計算邏輯:Java 負責傳遞事件,ABCL 負責處理數字、運算子和顯示更新。
學習重點是:將真正的計算行為交給前面已完成的 handle-button-press,但畫面仍然是 FXML,事件入口仍然是 Java Controller。這樣一來,JavaFX 的啟動流程和 ABCL 的運算能力就能自然接起來。
前一篇的 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 是全域單例,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);
}
}
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();
}
}
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);
}
交互的重點:
"7"、"+"、"="
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 顯示計算結果

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