從零構建一款擁有標準/科學模式、鍵盤支援、表達式解析、歷史記錄的 Swing 計算器。本教程涵蓋 MVC 架構、事件處理、表達式引擎 (Shunting Yard 算法)、UI 佈局、打包發布全流程。
Model: CalculatorEngine - evaluate(expression: String): Double // 核心計算邏輯 - history: List// 歷史記錄 View: CalculatorFrame / CalculatorPanel - JTextField display // 顯示區 - JButton[][] buttons // 按鈕網格 - KeyboardListener // 鍵盤綁定 Controller: CalculatorController - 綁定 View 事件 → 調用 Model → 更新 View - 狀態管理:輸入中、運算符等待、結果顯示、錯誤狀態
public class CalculatorEngine {
private static final Map PRECEDENCE = Map.of(
"+", 1, "-", 1, "*", 2, "/", 2, "^", 3
);
private static final Set RIGHT_ASSOC = Set.of("^");
public double evaluate(String infix) {
List postfix = toPostfix(tokenize(infix));
return evalPostfix(postfix);
}
// 1. 詞法分析:字符串 → Token 列表 (數字、運算符、括號、函數)
private List tokenize(String expr) {
List tokens = new ArrayList<>();
StringBuilder num = new StringBuilder();
for (int i = 0; i < expr.length(); i++) {
char c = expr.charAt(i);
if (Character.isDigit(c) || c == '.') {
num.append(c);
} else {
if (num.length() > 0) { tokens.add(num.toString()); num.setLength(0); }
if (c == ' ') continue;
if ("+-*/^()".indexOf(c) >= 0) tokens.add(String.valueOf(c));
else if (Character.isLetter(c)) { // 函數名 sin, cos, sqrt...
StringBuilder fn = new StringBuilder();
while (i < expr.length() && Character.isLetter(expr.charAt(i))) {
fn.append(expr.charAt(i++));
}
i--; tokens.add(fn.toString());
}
}
}
if (num.length() > 0) tokens.add(num.toString());
return tokens;
}
// 2. 中綴轉後綴 (Shunting Yard)
private List toPostfix(List tokens) {
List output = new ArrayList<>();
Deque opStack = new ArrayDeque<>();
for (String token : tokens) {
if (isNumber(token)) output.add(token);
else if (isFunction(token)) opStack.push(token);
else if (token.equals("(")) opStack.push(token);
else if (token.equals(")")) {
while (!opStack.isEmpty() && !opStack.peek().equals("("))
output.add(opStack.pop());
opStack.pop(); // 彈出 (
if (!opStack.isEmpty() && isFunction(opStack.peek()))
output.add(opStack.pop()); // 函數入隊
} else if (isOperator(token)) {
while (!opStack.isEmpty() && isOperator(opStack.peek()) &&
(PRECEDENCE.get(opStack.peek()) > PRECEDENCE.get(token) ||
(PRECEDENCE.get(opStack.peek()).equals(PRECEDENCE.get(token)) && !RIGHT_ASSOC.contains(token)))) {
output.add(opStack.pop());
}
opStack.push(token);
}
}
while (!opStack.isEmpty()) output.add(opStack.pop());
return output;
}
// 3. 後綴表達式求值
private double evalPostfix(List postfix) {
Deque stack = new ArrayDeque<>();
for (String token : postfix) {
if (isNumber(token)) stack.push(Double.parseDouble(token));
else if (isFunction(token)) {
double a = stack.pop();
stack.push(applyFunction(token, a));
} else { // 運算符
double b = stack.pop(), a = stack.pop();
stack.push(applyOperator(token, a, b));
}
}
return stack.pop();
}
private boolean isNumber(String s) {
try { Double.parseDouble(s); return true; } catch (NumberFormatException e) { return false; }
}
private boolean isOperator(String s) { return PRECEDENCE.containsKey(s); }
private boolean isFunction(String s) { return Set.of("sin","cos","tan","sqrt","log","ln").contains(s); }
private double applyOperator(String op, double a, double b) {
return switch (op) {
case "+" -> a + b; case "-" -> a - b; case "*" -> a * b;
case "/" -> { if (b == 0) throw new ArithmeticException("Divide by zero"); yield a / b; }
case "^" -> Math.pow(a, b); default -> throw new IllegalArgumentException(op);
};
}
private double applyFunction(String fn, double a) {
return switch (fn) {
case "sin" -> Math.sin(Math.toRadians(a)); // 角度制
case "cos" -> Math.cos(Math.toRadians(a));
case "tan" -> Math.tan(Math.toRadians(a));
case "sqrt" -> { if (a < 0) throw new ArithmeticException("sqrt negative"); yield Math.sqrt(a); }
case "log" -> Math.log10(a); case "ln" -> Math.log(a);
default -> throw new IllegalArgumentException(fn);
};
}
}
public class CalculatorFrame extends JFrame {
private final CalculatorEngine engine = new CalculatorEngine();
private final CalculatorController controller;
private final JTextField display = new JTextField();
private StringBuilder currentInput = new StringBuilder();
private boolean resultDisplayed = false; // 標記剛顯示結果,下一輸入應清屏
public CalculatorFrame() {
setTitle("Java 計算器");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(340, 420);
setLocationRelativeTo(null);
setLayout(new BorderLayout());
// 顯示區
display.setFont(new Font("Monospaced", Font.PLAIN, 28));
display.setHorizontalAlignment(JTextField.RIGHT);
display.setEditable(false);
display.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
add(display, BorderLayout.NORTH);
// 按鈕面板
JPanel btnPanel = new JPanel(new GridLayout(6, 4, 5, 5));
btnPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
String[][] buttons = {
{"MC", "MR", "MS", "M+"},
{"C", "⌫", "%", "/"},
{"7", "8", "9", "*"},
{"4", "5", "6", "-"},
{"1", "2", "3", "+"},
{"±", "0", ".", "="}
};
for (String[] row : buttons) {
for (String text : row) {
JButton btn = new JButton(text);
btn.setFont(new Font("SansSerif", Font.BOLD, 18));
btn.setFocusPainted(false);
if ("+-*/=".contains(text)) btn.setBackground(new Color(0xE0E0E0));
if ("C⌫".contains(text)) btn.setForeground(Color.RED);
btn.addActionListener(e -> controller.onButtonClick(text));
btnPanel.add(btn);
}
}
add(btnPanel, BorderLayout.CENTER);
// 鍵盤支援
addKeyListener(new KeyAdapter() {
@Override public void keyPressed(KeyEvent e) {
controller.onKeyPress(e.getKeyCode(), e.getKeyChar());
}
});
setFocusable(true);
controller = new CalculatorController(this, engine);
}
// View 接口方法
public void setDisplayText(String text) { display.setText(text); }
public String getDisplayText() { return display.getText(); }
public void showError(String msg) {
display.setText("Error: " + msg);
display.setForeground(Color.RED);
}
}
public class CalculatorController {
private final CalculatorFrame view;
private final CalculatorEngine engine;
private enum State { START, INPUT_NUMBER, INPUT_OPERATOR, RESULT, ERROR }
private State state = State.START;
public CalculatorController(CalculatorFrame view, CalculatorEngine engine) {
this.view = view; this.engine = engine;
}
public void onButtonClick(String text) {
if (state == State.ERROR) { clear(); return; }
switch (text) {
case "0","1","2","3","4","5","6","7","8","9" -> inputDigit(text);
case "." -> inputDecimal();
case "+","-","*","/","^" -> inputOperator(text);
case "=" -> calculateResult();
case "C" -> clear();
case "⌫" -> backspace();
case "±" -> toggleSign();
case "%" -> percent();
case "MC","MR","MS","M+" -> memoryOp(text); // 記憶體功能
}
}
private void inputDigit(String digit) {
if (state == State.RESULT || state == State.START) { view.setDisplayText(""); state = State.INPUT_NUMBER; }
if (state == State.INPUT_NUMBER) {
String cur = view.getDisplayText();
if (cur.equals("0")) view.setDisplayText(digit);
else view.setDisplayText(cur + digit);
}
}
private void inputOperator(String op) {
if (state == State.INPUT_NUMBER || state == State.RESULT) {
// 允許連續運算:5 + 3 = 8,再按 * 視為 8 * ...
state = State.INPUT_OPERATOR;
} else if (state == State.INPUT_OPERATOR) {
// 替換運算符
String cur = view.getDisplayText();
view.setDisplayText(cur.substring(0, cur.length()-1) + op);
}
}
private void calculateResult() {
if (state == State.INPUT_NUMBER || state == State.RESULT) {
try {
double result = engine.evaluate(view.getDisplayText());
view.setDisplayText(formatResult(result));
state = State.RESULT;
} catch (Exception e) {
view.showError(e.getMessage());
state = State.ERROR;
}
}
}
private String formatResult(double v) {
if (v == (long) v) return String.format("%,d", (long) v); // 整數顯示千分位
return String.valueOf(v);
}
// 其他方法:clear, backspace, toggleSign, percent, memoryOp, onKeyPress...
}
| 功能 | 實現要點 |
|---|---|
| 科學模式 | 切換面板顯示 sin/cos/tan/sqrt/log/ln/π/e/x^y/10^x 等按鈕,擴展 Engine 函數表 |
| 歷史記錄 | Model 維護 `List |
| 鍵盤完整支援 | 數字鍵、小鍵盤、Enter(=)、Esc(C)、Backspace(⌫)、方向鍵編輯、Ctrl+C/V 複製貼上 |
| 主題切換 | UIManager LookAndFeel (FlatLaf/Darcula/系統原生),或自定義 Color Scheme 持久化 |
| 單位轉換/程序員模式 | 擴展 Engine 支援進制轉換 (HEX/DEC/OCT/BIN)、位運算 (&、|、^、~、<<、>>) |
| 打包發布 | `jpackage` 生成原生安裝包 (exe/msi/dmg/deb),或 `jlink` 定制 JRE + 啟動腳本 |
calculator/ ├── src/main/java/com/example/calculator/ │ ├── engine/CalculatorEngine.java │ ├── ui/CalculatorFrame.java, CalculatorController.java │ ├── model/HistoryManager.java, Memory.java │ └── Main.java ├── src/main/resources/icons/ ├── pom.xml (maven-shade-plugin / jpackage 配置) └── README.md
Swing 計算器是練習 GUI 開發、表達式解析、狀態機設計、MVC 架構的經典項目。核心亮點:Shunting Yard 算法解耦解析與計算、狀態機驅動輸入邏輯、MVC 分層便於測試與擴展。從這裡出發,可擴展為科學計算器、程序員計算器、單位轉換器,甚至接入脚本引擎 (Nashorn/GraalJS) 支持用戶自定義函數。