首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java Swing 實戰:構建 Windows 風格計算器完整教程

     編輯:Java 相關知識

從零構建一款擁有標準/科學模式、鍵盤支援、表達式解析、歷史記錄的 Swing 計算器。本教程涵蓋 MVC 架構、事件處理、表達式引擎 (Shunting Yard 算法)、UI 佈局、打包發布全流程。

架構設計:MVC 分層

Model: CalculatorEngine
  - evaluate(expression: String): Double  // 核心計算邏輯
  - history: List                 // 歷史記錄
  
View: CalculatorFrame / CalculatorPanel
  - JTextField display                    // 顯示區
  - JButton[][] buttons                   // 按鈕網格
  - KeyboardListener                      // 鍵盤綁定
  
Controller: CalculatorController
  - 綁定 View 事件 → 調用 Model → 更新 View
  - 狀態管理:輸入中、運算符等待、結果顯示、錯誤狀態

核心計算引擎:Shunting Yard 算法 (中綴轉後綴 + 棧計算)


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);
        };
    }
}

Swing UI 構建 (關鍵代碼片段)


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 history`,計算成功後 `history.add(expr + " = " + result)`,View 顯示側邊欄或下拉列表
鍵盤完整支援數字鍵、小鍵盤、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 + 啟動腳本

常見坑與調試技巧

  • EDT 違規:所有 UI 更新必須在 Event Dispatch Thread,用 `SwingUtilities.invokeLater()`。
  • 浮點數精度:金融計算用 `BigDecimal`,科學計算 `double` 足夠,顯示時格式化避免 `0.1+0.2=0.30000000000000004`。
  • 除零/非法運算:引擎層拋出異常,控制器捕獲轉為友好錯誤提示,狀態機進入 ERROR 狀態需用戶主動清除。
  • 焦點管理:`setFocusable(true)`、`requestFocusInWindow()` 確保鍵盤事件生效。
  • 高 DPI 支援:JDK 9+ 自動縮放,或 `-Dsun.java2d.uiScale=1.5`,圖標用 SVG/矢量圖。

項目結構建議

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) 支持用戶自定義函數。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved