iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
Software Development

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

lesson 10: CalcFX 邏輯:用 ABCL 綁定動作處理,實現計算機邏輯

  • 分享至 

  • xImage
  •  

TL;DR

這篇在前一篇 CalcFX 畫面的基礎上,加入計算機的狀態管理、按鈕事件與四則運算邏輯。畫面與邏輯目前仍放在同一個程式中,之後再將兩者拆分。

建立計算機狀態

在計算機的邏輯處理上,需要先建立幾個全域變數:

(defvar *current-input* "0")  ;; 當前顯示幕上的字串
(defvar *stored-value* nil)   ;; 暫存的左操作數 (數字)
(defvar *active-op* nil)      ;; 當前準備執行的運算子 (+, -, *, /)
(defvar *clear-on-next* nil)  ;; 是否在下次按下數字時清空顯示幕

綁定按鈕事件

建立按鈕物件(btn)時,同時設定按下按鈕的處理函式 #'handle-button-press:

(let ((listener (make-event-handler (event)
                                  (declare (ignore event))
                                  (handle-button-press label display))))
(#"setOnAction" btn listener))

#'handle-button-press 接收一個字串(label)和一個 TextField 物件(display),依據 label 分別處理不同邏輯,並更新 display 顯示的文字內容:

(defun handle-button-press (label display)
  "核心邏輯:處理按鈕點擊事件"
  (cond
    ;; 1. 數字鍵處理
    ((find label '("0" "1" "2" "3" "4" "5" "6" "7" "8" "9") :test #'string=) ......)

    ;; 2. 清除鍵處理
    ((string= label "C") ......)

    ;; 3. 運算子處理 (+, -, *, /)
    ((member label '("+" "-" "*" "/") :test #'string=) ......)

    ;; 4. 等號處理 (=)
    ((string= label "=") ......)))

可以分成數字按鈕、清除按鈕、等號按鈕和運算子按鈕四種情況。以運算子按鈕為例,會依照不同運算子執行對應的處理邏輯:

(defun calculate (op num1 num2)
  "執行四則運算,回傳數字"
  (handler-case
      (case op
        (:add (+ num1 num2))
        (:sub (- num1 num2))
        (:mul (* num1 num2))
        (:div (if (= num2 0) "Error" (/ num1 num2))))
    (error () "Error")))

這裡不詳細展開每一個條件分支,完整處理邏輯會在後文列出。目前最重要的是透過 (#"setOnAction" btn listener) 將按鈕與事件處理函式綁定起來。

到目前為止,CalcFX 已經是一個可以實際操作的計算機應用程式。不過雖然 CSS 已經獨立出來,畫面設計與運算邏輯仍然寫在同一個程式中;後續再將這兩部分分開。

完整處理邏輯程式碼

(defvar *current-input* "0")  ;; 當前顯示幕上的字串
(defvar *stored-value* nil)   ;; 暫存的左操作數 (數字)
(defvar *active-op* nil)      ;; 當前準備執行的運算子 (+, -, *, /)
(defvar *clear-on-next* nil)  ;; 是否在下次按下數字時清空顯示幕

(defun update-display (display value)
  "統一更新狀態與顯示幕。value 可以是字串或數字,會自動轉為字串儲存。"
  (let ((str-val (if (stringp value)
                     value
                     (format nil "~G" value)))) ; 確保轉為字串,避免存入純數字
    (setf *current-input* str-val)
    (jcall "setText" display str-val)))

(defun calculate (op num1 num2)
  "執行四則運算,回傳數字"
  (handler-case
      (case op
        (:add (+ num1 num2))
        (:sub (- num1 num2))
        (:mul (* num1 num2))
        (:div (if (= num2 0) "Error" (/ num1 num2))))
    (error () "Error")))

(defun handle-button-press (label display)
  "核心邏輯:處理按鈕點擊事件"
  (cond
    ;; 1. 數字鍵處理
    ((find label '("0" "1" "2" "3" "4" "5" "6" "7" "8" "9") :test #'string=)
     (if (or (string= *current-input* "0") *clear-on-next* (string= *current-input* "Error"))
         (progn
           (update-display display label)
           (setf *clear-on-next* nil))
         (update-display display (concatenate 'string *current-input* label))))

    ;; 2. 清除鍵處理
    ((string= label "C")
     (setf *stored-value* nil
           *active-op* nil
           *clear-on-next* nil)
     (update-display display "0"))

    ;; 3. 運算子處理 (+, -, *, /)
    ((member label '("+" "-" "*" "/") :test #'string=)
     (let ((current-num (read-from-string *current-input* nil)))
       (when (numberp current-num)
         (if (and *stored-value* *active-op*)
             ;; 連續計算:先前已有暫存值,直接運算
             (let ((res (calculate *active-op* *stored-value* current-num)))
               (if (equal res "Error")
                   (progn
                     (update-display display "Error")
                     (setf *stored-value* nil
                           *active-op* nil))
                   (progn
                     (update-display display res)
                     (setf *stored-value* res))))
             ;; 第一次按下運算子
             (setf *stored-value* current-num)))
       (setf *active-op* (cond ((string= label "+") :add)
                               ((string= label "-") :sub)
                               ((string= label "*") :mul)
                               ((string= label "/") :div)))
       (setf *clear-on-next* t)))

    ;; 4. 等號處理 (=)
    ((string= label "=")
     (let ((current-num (read-from-string *current-input* nil)))
       (when (and *stored-value* *active-op* (numberp current-num))
         (let ((res (calculate *active-op* *stored-value* current-num)))
           (if (equal res "Error")
               (progn
                 (update-display display "Error")
                 (setf *stored-value* nil
                       *active-op* nil))
               (update-display display (if (stringp res)
                                           res
                                           (format nil "~G" res)))))
         ;; 重設計算機狀態
         (setf *stored-value* nil
               *active-op* nil
               *clear-on-next* t))))))

執行結果

加入這段邏輯後,CalcFX 可以處理數字輸入、清除目前內容、選擇加減乘除運算子,以及按下等號顯示計算結果;除以零或其他計算錯誤時則顯示 Error。

目前事件處理、狀態管理與計算函式仍然和畫面建立程式放在一起。下一步可以將畫面設計與計算機邏輯拆成不同部分,讓程式更容易維護與延伸。


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

尚未有邦友留言

立即登入留言