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