Java Swing科学计算器开发实战:从界面设计到中缀表达式解析(附完整代码)

Java Swing科学计算器开发实战:从界面设计到中缀表达式解析

记得我刚开始学Java GUI编程那会儿,第一个想做的项目就是计算器。看起来简单,不就是几个按钮加个显示屏吗?但真正动手之后才发现,这里面藏着不少门道。从按钮布局的像素级对齐,到用户输入的逻辑校验,再到复杂表达式的解析计算,每一步都可能让你踩坑。今天我就把自己这些年做Java Swing计算器的经验整理出来,特别是那些教科书上不会讲的实战细节。

如果你正在做Java课程设计,或者想通过一个完整的项目来巩固Swing和算法知识,这个科学计算器项目绝对是绝佳的选择。它涵盖了GUI设计、事件处理、数据结构(栈的应用)、算法实现(中缀转后缀)等多个核心知识点,而且最终能做出一个真正可用的工具,成就感满满。

1. 项目架构设计与技术选型

在动手写代码之前,我得先跟你聊聊架构设计。很多初学者一上来就埋头写界面,结果写到一半发现计算逻辑没法优雅地集成进去,只能推倒重来。我吃过这个亏,所以现在做任何GUI项目,都会先想清楚**模型-视图-控制器(MVC)**的分离。

1.1 为什么选择MVC模式?

对于计算器这种有明确数据流和用户交互的项目,MVC能让你的代码结构清晰,后期维护也方便。看看下面这个简单的类图关系:

CalculatorModel (模型)
├── 存储当前表达式字符串
├── 存储计算结果
├── 提供表达式验证方法
└── 封装计算核心算法

CalculatorView (视图)
├── JFrame主窗口
├── JTextField显示区域
├── JButton按钮面板
└── 界面布局管理器

CalculatorController (控制器)
├── 监听按钮点击事件
├── 调用模型方法处理输入
├── 更新视图显示
└── 处理用户操作流程

模型层是计算器的“大脑”,它不关心界面长什么样,只负责处理计算逻辑。视图层就是用户看到的界面,控制器层则是两者的桥梁。这种分离有个明显的好处:哪天你想把Swing界面换成JavaFX,只需要重写视图层,模型和控制器基本不用动。

1.2 技术栈深度解析

你可能觉得用Swing做界面有点“过时”,但我要说,对于学习GUI编程和桌面应用开发,Swing依然是绝佳的起点。它足够轻量,不需要额外依赖,而且Java标准库自带,学习成本相对较低。

不过Swing也有它的局限性,比如默认的界面风格比较老旧。这里有个小技巧:通过设置LookAndFeel,你可以让计算器看起来更现代化。我常用的几个风格:

风格名称设置代码视觉效果适用场景
NimbusUIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel")现代扁平化大多数情况
WindowsUIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel")Win10风格Windows平台
MetalUIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel")Java经典跨平台兼容

注意:设置LookAndFeel必须在创建任何Swing组件之前,否则可能不生效。我一般放在main方法的最开始。

除了界面,计算器的核心是表达式解析。这里涉及到计算机科学中一个经典算法:中缀表达式转后缀表达式(逆波兰表达式)。为什么要转?因为计算机直接计算中缀表达式(如3+4*5)很麻烦,需要处理运算符优先级和括号,而后缀表达式(如3 4 5 * +)可以直接用栈顺序计算,简单高效。

2. 界面设计与布局实战

界面设计是用户的第一印象,好的界面不仅要美观,更要符合操作直觉。Windows计算器、macOS计算器都是很好的参考,但我们要做的不是简单模仿,而是理解背后的设计逻辑。

2.1 布局管理器的选择与陷阱

新手最容易犯的错误就是直接用setBounds()硬编码每个组件的位置。看起来简单直接,但维护起来是噩梦。试试调整一个按钮的大小,所有相关坐标都要重新计算。我推荐使用组合布局管理器

这是我的计算器界面布局方案:

public class CalculatorView extends JFrame {
    private JTextField displayField;
    private JPanel buttonPanel;
    
    public CalculatorView() {
        setTitle("科学计算器");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        // 主容器使用BorderLayout
        setLayout(new BorderLayout(5, 5));
        
        // 顶部显示区域
        displayField = new JTextField("0", 20);
        displayField.setFont(new Font("Monospaced", Font.BOLD, 24));
        displayField.setHorizontalAlignment(JTextField.RIGHT);
        displayField.setEditable(false);
        displayField.setBackground(Color.WHITE);
        add(displayField, BorderLayout.NORTH);
        
        // 中部按钮区域
        buttonPanel = new JPanel();
        // 使用GridLayout,6行7列,间距2像素
        buttonPanel.setLayout(new GridLayout(6, 7, 2, 2));
        add(buttonPanel, BorderLayout.CENTER);
        
        // 创建按钮并添加到面板
        initButtons();
        
        pack();
        setLocationRelativeTo(null); // 居中显示
    }
}

这里有个关键点:GridLayout会把所有单元格强制设为相同大小。如果你想要某些按钮(比如等号按钮)跨越多行或多列,就需要更复杂的布局。我的做法是嵌套使用不同的布局管理器

// 创建一个专门处理特殊按钮布局的面板
private JPanel createSpecialButtonPanel() {
    JPanel panel = new JPanel(new BorderLayout());
    
    // 等号按钮放在右侧,占据整个高度
    JButton equalsButton = new JButton("=");
    equalsButton.setPreferredSize(new Dimension(80, 200));
    panel.add(equalsButton, BorderLayout.EAST);
    
    // 其他按钮用GridLayout放在左侧
    JPanel numPanel = new JPanel(new GridLayout(4, 3, 2, 2));
    // ... 添加数字按钮
    panel.add(numPanel, BorderLayout.CENTER);
    
    return panel;
}

2.2 按钮设计与用户体验

按钮不仅仅是点击的控件,它还是用户与计算器交互的主要方式。好的按钮设计要考虑:

  1. 视觉反馈:鼠标悬停、点击时的颜色变化
  2. 状态反馈:按钮是否可用、是否被选中
  3. 操作反馈:点击后的即时响应

我通常会给按钮添加鼠标监听器来增强交互体验:

private void enhanceButtonStyle(JButton button) {
    // 保存原始背景色
    Color originalBg = button.getBackground();
    Color originalFg = button.getForeground();
    
    button.addMouseListener(new MouseAdapter() {
        @Override
        public void mouseEntered(MouseEvent e) {
            button.setBackground(originalBg.brighter());
            button.setCursor(new Cursor(Cursor.HAND_CURSOR));
        }
        
        @Override
        public void mouseExited(MouseEvent e) {
            button.setBackground(originalBg);
            button.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
        }
        
        @Override
        public void mousePressed(MouseEvent e) {
            button.setBackground(originalBg.darker());
        }
        
        @Override
        public void mouseReleased(MouseEvent e) {
            button.setBackground(originalBg.brighter());
        }
    });
}

对于科学计算器,按钮数量多,功能分类很重要。我通常按功能分区:

区域包含按钮布局特点
数字区0-9, 小数点, 正负号3×4网格,符合电话键盘布局
基本运算区+, -, ×, ÷, =右侧一列,等号按钮较大
科学函数区sin, cos, tan, log, ln, √, ^上方两行,按功能分组
高级功能区π, e, (, ), %, 1/x, x!, x², x³分散布局,常用功能突出
控制区C, CE, ←, M+, M-, MR, MC顶部一行,操作相关

3. 核心算法:中缀表达式解析与计算

这是计算器最核心、也最有趣的部分。用户输入的是中缀表达式,比如3+4*5/(2-1),计算机需要正确理解运算顺序。直接计算中缀表达式很复杂,所以我们要把它转换成后缀表达式。

3.1 中缀转后缀算法详解

中缀转后缀的算法基于栈数据结构,遵循以下规则:

  1. 遇到操作数,直接输出到后缀表达式
  2. 遇到运算符,与栈顶运算符比较优先级:
    • 如果栈为空或栈顶是左括号,直接入栈
    • 如果优先级高于栈顶,入栈
    • 如果优先级不高于栈顶,弹出栈顶并输出,然后继续比较
  3. 遇到左括号,直接入栈
  4. 遇到右括号,弹出栈顶运算符并输出,直到遇到左括号(左括号弹出但不输出)
  5. 表达式结束后,弹出栈中所有运算符

看看我的实现代码:

public class ExpressionParser {
    // 运算符优先级映射
    private static final Map<Character, Integer> PRECEDENCE = new HashMap<>();
    static {
        PRECEDENCE.put('+', 1);
        PRECEDENCE.put('-', 1);
        PRECEDENCE.put('*', 2);
        PRECEDENCE.put('/', 2);
        PRECEDENCE.put('^', 3);
        PRECEDENCE.put('s', 4); // sin
        PRECEDENCE.put('c', 4); // cos
        PRECEDENCE.put('t', 4); // tan
        PRECEDENCE.put('l', 4); // log
        PRECEDENCE.put('g', 4); // sqrt
        PRECEDENCE.put('!', 4); // 阶乘
    }
    
    public List<String> infixToPostfix(String expression) {
        List<String> output = new ArrayList<>();
        Stack<Character> operatorStack = new Stack<>();
        
        // 第一步:分词,把表达式拆分成操作数和运算符
        List<String> tokens = tokenize(expression);
        
        for (String token : tokens) {
            if (isNumber(token)) {
                // 操作数直接输出
                output.add(token);
            } else if (token.equals("(")) {
                // 左括号入栈
                operatorStack.push('(');
            } else if (token.equals(")")) {
                // 右括号:弹出直到左括号
                while (!operatorStack.isEmpty() && operatorStack.peek() != '(') {
                    output.add(String.valueOf(operatorStack.pop()));
                }
                operatorStack.pop(); // 弹出左括号
            } else if (isOperator(token)) {
                // 运算符:处理优先级
                char op = token.charAt(0);
                while (!operatorStack.isEmpty() && 
                       operatorStack.peek() != '(' && 
                       PRECEDENCE.getOrDefault(operatorStack.peek(), 0) >= PRECEDENCE.get(op)) {
                    output.add(String.valueOf(operatorStack.pop()));
                }
                operatorStack.push(op);
            }
        }
        
        // 弹出栈中剩余运算符
        while (!operatorStack.isEmpty()) {
            output.add(String.valueOf(operatorStack.pop()));
        }
        
        return output;
    }
    
    private List<String> tokenize(String expression) {
        List<String> tokens = new ArrayList<>();
        int i = 0;
        
        while (i < expression.length()) {
            char ch = expression.charAt(i);
            
            if (Character.isDigit(ch) || ch == '.') {
                // 处理数字(包括小数)
                StringBuilder num = new StringBuilder();
                while (i < expression.length() && 
                      (Character.isDigit(expression.charAt(i)) || expression.charAt(i) == '.')) {
                    num.append(expression.charAt(i));
                    i++;
                }
                tokens.add(num.toString());
            } else if (isOperatorChar(ch) || ch == '(' || ch == ')') {
                // 运算符或括号
                tokens.add(String.valueOf(ch));
                i++;
            } else if (Character.isLetter(ch)) {
                // 处理函数名(sin, cos, log等)
                StringBuilder func = new StringBuilder();
                while (i < expression.length() && Character.isLetter(expression.charAt(i))) {
                    func.append(expression.charAt(i));
                    i++;
                }
                // 转换为单字符标识符以便处理
                char funcSymbol = mapFunctionToSymbol(func.toString());
                tokens.add(String.valueOf(funcSymbol));
            } else {
                // 跳过空格等
                i++;
            }
        }
        
        return tokens;
    }
    
    private char mapFunctionToSymbol(String func) {
        // 将函数名映射为单字符,简化处理
        switch (func.toLowerCase()) {
            case "sin": return 's';
            case "cos": return 'c';
            case "tan": return 't';
            case "log": return 'l';
            case "ln": return 'n';
            case "sqrt": return 'g';
            default: return '?';
        }
    }
}

提示:这里我把函数名映射为单字符是为了简化处理。实际项目中,你可能需要更复杂的数据结构来保存函数信息。

3.2 后缀表达式计算

得到后缀表达式后,计算就简单多了。算法如下:

  1. 从左到右扫描后缀表达式
  2. 遇到操作数,入栈
  3. 遇到运算符,弹出栈顶两个操作数(注意顺序),计算结果,结果入栈
  4. 扫描结束,栈顶就是最终结果

对于一元运算符(如sin、cos、阶乘),只需要弹出一个操作数。

public class PostfixCalculator {
    public double evaluate(List<String> postfix) {
        Stack<Double> stack = new Stack<>();
        
        for (String token : postfix) {
            if (isNumber(token)) {
                stack.push(Double.parseDouble(token));
            } else if (isOperator(token.charAt(0))) {
                // 二元运算符
                if (token.length() == 1) {
                    char op = token.charAt(0);
                    if (op == '!') {
                        // 阶乘是一元运算符
                        double num = stack.pop();
                        stack.push(factorial(num));
                    } else if (isUnaryOperator(op)) {
                        // 其他一元运算符
                        double num = stack.pop();
                        stack.push(applyUnaryOperator(op, num));
                    } else {
                        // 二元运算符
                        double b = stack.pop();
                        double a = stack.pop();
                        stack.push(applyBinaryOperator(op, a, b));
                    }
                }
            }
        }
        
        return stack.isEmpty() ? 0 : stack.pop();
    }
    
    private double applyBinaryOperator(char op, double a, double b) {
        switch (op) {
            case '+': return a + b;
            case '-': return a - b;
            case '*': return a * b;
            case '/': 
                if (b == 0) throw new ArithmeticException("除数不能为零");
                return a / b;
            case '^': return Math.pow(a, b);
            default: throw new IllegalArgumentException("未知运算符: " + op);
        }
    }
    
    private double applyUnaryOperator(char op, double num) {
        switch (op) {
            case 's': return Math.sin(num); // sin
            case 'c': return Math.cos(num); // cos
            case 't': return Math.tan(num); // tan
            case 'l': return Math.log10(num); // log
            case 'n': return Math.log(num); // ln
            case 'g': return Math.sqrt(num); // sqrt
            default: throw new IllegalArgumentException("未知一元运算符: " + op);
        }
    }
    
    private double factorial(double n) {
        if (n < 0 || n != Math.floor(n)) {
            throw new IllegalArgumentException("阶乘要求非负整数");
        }
        long result = 1;
        for (int i = 2; i <= n; i++) {
            result *= i;
        }
        return result;
    }
}

4. 输入验证与错误处理

用户输入什么都有可能,好的计算器必须能优雅地处理各种错误输入。我见过太多计算器在遇到非法输入时直接崩溃,这体验太糟糕了。

4.1 实时输入验证

与其在计算时报错,不如在输入时就阻止非法输入。我的做法是给每个按钮添加智能验证:

public class InputValidator {
    private String currentExpression = "";
    
    public boolean canAppendDigit(String digit) {
        if (currentExpression.isEmpty()) {
            return true;
        }
        
        char lastChar = currentExpression.charAt(currentExpression.length() - 1);
        
        // 检查数字后能否追加数字
        if (Character.isDigit(lastChar) || lastChar == '.') {
            return true;
        }
        
        // 检查运算符后能否追加数字
        if (isOperator(lastChar) || lastChar == '(') {
            return true;
        }
        
        // 检查函数后能否追加数字
        if (lastChar == ')' || isFunctionChar(lastChar)) {
            return false; // 函数后不能直接跟数字,需要运算符
        }
        
        return false;
    }
    
    public boolean canAppendOperator(char op) {
        if (currentExpression.isEmpty()) {
            // 表达式为空时,只有负号可以
            return op == '-';
        }
        
        char lastChar = currentExpression.charAt(currentExpression.length() - 1);
        
        // 数字或右括号后可以跟运算符
        if (Character.isDigit(lastChar) || lastChar == ')' || lastChar == '!') {
            return true;
        }
        
        // 检查连续运算符的情况(如3+-5是允许的)
        if (isOperator(lastChar) && op == '-') {
            // 允许在运算符后跟负号(表示负数)
            return true;
        }
        
        return false;
    }
    
    public boolean canAppendDecimalPoint() {
        if (currentExpression.isEmpty()) {
            return true; // 可以输入".5"表示0.5
        }
        
        // 找到当前数字的开始位置
        int i = currentExpression.length() - 1;
        while (i >= 0 && Character.isDigit(currentExpression.charAt(i))) {
            i--;
        }
        
        // 检查当前数字是否已经有小数点
        String currentNumber = currentExpression.substring(i + 1);
        return !currentNumber.contains(".");
    }
}

4.2 表达式完整性检查

在用户点击等号前,我们还需要检查表达式是否完整合法:

public class ExpressionValidator {
    public ValidationResult validate(String expression) {
        ValidationResult result = new ValidationResult();
        
        // 1. 括号匹配检查
        if (!checkParentheses(expression)) {
            result.setValid(false);
            result.setErrorMessage("括号不匹配");
            return result;
        }
        
        // 2. 运算符位置检查
        if (!checkOperatorPositions(expression)) {
            result.setValid(false);
            result.setErrorMessage("运算符位置错误");
            return result;
        }
        
        // 3. 小数点检查
        if (!checkDecimalPoints(expression)) {
            result.setValid(false);
            result.setErrorMessage("小数点使用错误");
            return result;
        }
        
        // 4. 函数参数检查
        if (!checkFunctionArguments(expression)) {
            result.setValid(false);
            result.setErrorMessage("函数参数错误");
            return result;
        }
        
        result.setValid(true);
        return result;
    }
    
    private boolean checkParentheses(String expr) {
        int balance = 0;
        for (char ch : expr.toCharArray()) {
            if (ch == '(') balance++;
            else if (ch == ')') balance--;
            
            if (balance < 0) return false; // 右括号出现在左括号前
        }
        return balance == 0;
    }
    
    private boolean checkOperatorPositions(String expr) {
        if (expr.isEmpty()) return true;
        
        // 检查开头和结尾
        char first = expr.charAt(0);
        char last = expr.charAt(expr.length() - 1);
        
        // 开头只能是数字、左括号、负号或函数
        if (!(Character.isDigit(first) || first == '(' || first == '-' || 
              first == '.' || Character.isLetter(first))) {
            return false;
        }
        
        // 结尾只能是数字、右括号或阶乘
        if (!(Character.isDigit(last) || last == ')' || last == '!')) {
            return false;
        }
        
        // 检查连续的运算符
        for (int i = 0; i < expr.length() - 1; i++) {
            char current = expr.charAt(i);
            char next = expr.charAt(i + 1);
            
            // 不允许的连续运算符组合
            if (isBinaryOperator(current) && isBinaryOperator(next) && next != '-') {
                return false;
            }
            
            // 运算符后不能直接跟右括号(除了阶乘)
            if (isBinaryOperator(current) && next == ')' && current != '!') {
                return false;
            }
            
            // 左括号后不能直接跟二元运算符(除了负号)
            if (current == '(' && isBinaryOperator(next) && next != '-') {
                return false;
            }
        }
        
        return true;
    }
}

4.3 运行时错误处理

即使输入验证通过了,计算时仍可能出错(比如除零、负数开平方等)。我们需要优雅地处理这些错误:

public class SafeCalculator {
    public CalculationResult calculate(String expression) {
        CalculationResult result = new CalculationResult();
        
        try {
            // 1. 验证表达式
            ValidationResult validation = validator.validate(expression);
            if (!validation.isValid()) {
                result.setSuccess(false);
                result.setError(validation.getErrorMessage());
                return result;
            }
            
            // 2. 转换为后缀表达式
            List<String> postfix = parser.infixToPostfix(expression);
            
            // 3. 计算后缀表达式
            double value = calculator.evaluate(postfix);
            
            result.setSuccess(true);
            result.setValue(value);
            
        } catch (ArithmeticException e) {
            result.setSuccess(false);
            result.setError("数学错误: " + e.getMessage());
        } catch (IllegalArgumentException e) {
            result.setSuccess(false);
            result.setError("输入错误: " + e.getMessage());
        } catch (Exception e) {
            result.setSuccess(false);
            result.setError("计算错误: " + e.getClass().getSimpleName());
        }
        
        return result;
    }
}

在界面上显示错误时,我建议用不同的颜色区分错误类型:

private void displayResult(CalculationResult result) {
    if (result.isSuccess()) {
        displayField.setForeground(Color.BLACK);
        displayField.setText(formatNumber(result.getValue()));
    } else {
        displayField.setForeground(Color.RED);
        displayField.setText("错误: " + result.getError());
        
        // 3秒后恢复黑色
        Timer timer = new Timer(3000, e -> {
            displayField.setForeground(Color.BLACK);
            displayField.setText("0");
        });
        timer.setRepeats(false);
        timer.start();
    }
}

5. 高级功能与扩展

基础功能完成后,我们可以考虑添加一些高级功能,让计算器更实用。

5.1 内存功能实现

内存功能是科学计算器的标配。我实现了一个简单的内存管理器:

public class MemoryManager {
    private double memoryValue = 0;
    private boolean memoryActive = false;
    
    public enum MemoryOperation {
        STORE, RECALL, CLEAR, ADD, SUBTRACT
    }
    
    public double operate(MemoryOperation operation, Double currentValue) {
        switch (operation) {
            case STORE:
                if (currentValue != null) {
                    memoryValue = currentValue;
                    memoryActive = true;
                }
                return currentValue != null ? currentValue : 0;
                
            case RECALL:
                return memoryActive ? memoryValue : 0;
                
            case CLEAR:
                memoryValue = 0;
                memoryActive = false;
                return 0;
                
            case ADD:
                if (currentValue != null && memoryActive) {
                    memoryValue += currentValue;
                } else if (currentValue != null) {
                    memoryValue = currentValue;
                    memoryActive = true;
                }
                return memoryValue;
                
            case SUBTRACT:
                if (currentValue != null && memoryActive) {
                    memoryValue -= currentValue;
                } else if (currentValue != null) {
                    memoryValue = -currentValue;
                    memoryActive = true;
                }
                return memoryValue;
                
            default:
                return 0;
        }
    }
    
    public boolean isMemoryActive() {
        return memoryActive;
    }
    
    public double getMemoryValue() {
        return memoryValue;
    }
}

在界面上,我通常用一个小的指示灯显示内存状态:

private void updateMemoryIndicator() {
    if (memoryManager.isMemoryActive()) {
        memoryIndicator.setText("M");
        memoryIndicator.setForeground(Color.RED);
    } else {
        memoryIndicator.setText("");
    }
}

5.2 历史记录功能

历史记录对于检查计算过程非常有用。我的实现方案:

public class HistoryManager {
    private List<HistoryEntry> history = new ArrayList<>();
    private int maxEntries = 50; // 最多保存50条记录
    
    public void addEntry(String expression, double result) {
        HistoryEntry entry = new HistoryEntry(expression, result, new Date());
        history.add(0, entry); // 新的记录放在前面
        
        // 限制历史记录数量
        if (history.size() > maxEntries) {
            history = history.subList(0, maxEntries);
        }
        
        // 可以在这里添加持久化逻辑,保存到文件
        saveToFile();
    }
    
    public List<HistoryEntry> getHistory() {
        return new ArrayList<>(history);
    }
    
    public void clearHistory() {
        history.clear();
    }
    
    public static class HistoryEntry {
        private final String expression;
        private final double result;
        private final Date timestamp;
        
        public HistoryEntry(String expression, double result, Date timestamp) {
            this.expression = expression;
            this.result = result;
            this.timestamp = timestamp;
        }
        
        // getters...
    }
}

历史记录可以显示在一个单独的对话框中:

private void showHistoryDialog() {
    JDialog historyDialog = new JDialog(this, "计算历史", false);
    historyDialog.setLayout(new BorderLayout());
    
    DefaultListModel<String> listModel = new DefaultListModel<>();
    for (HistoryManager.HistoryEntry entry : historyManager.getHistory()) {
        String item = String.format("%tH:%tM:%tS  %s = %.6f", 
            entry.getTimestamp(), entry.getTimestamp(), entry.getTimestamp(),
            entry.getExpression(), entry.getResult());
        listModel.addElement(item);
    }
    
    JList<String> historyList = new JList<>(listModel);
    historyList.setFont(new Font("Monospaced", Font.PLAIN, 12));
    
    JScrollPane scrollPane = new JScrollPane(historyList);
    historyDialog.add(scrollPane, BorderLayout.CENTER);
    
    JPanel buttonPanel = new JPanel();
    JButton clearButton = new JButton("清空历史");
    clearButton.addActionListener(e -> {
        historyManager.clearHistory();
        listModel.clear();
    });
    
    JButton closeButton = new JButton("关闭");
    closeButton.addActionListener(e -> historyDialog.dispose());
    
    buttonPanel.add(clearButton);
    buttonPanel.add(closeButton);
    historyDialog.add(buttonPanel, BorderLayout.SOUTH);
    
    historyDialog.setSize(400, 300);
    historyDialog.setLocationRelativeTo(this);
    historyDialog.setVisible(true);
}

5.3 角度/弧度切换

科学计算器需要支持角度和弧度两种模式。我的实现:

public class TrigonometricCalculator {
    public enum AngleMode {
        DEGREES, RADIANS
    }
    
    private AngleMode currentMode = AngleMode.DEGREES;
    
    public double sin(double value) {
        return Math.sin(convertToRadians(value));
    }
    
    public double cos(double value) {
        return Math.cos(convertToRadians(value));
    }
    
    public double tan(double value) {
        double rad = convertToRadians(value);
        // 检查tan(90°)或tan(π/2)的情况
        if (Math.abs(Math.cos(rad)) < 1e-10) {
            throw new ArithmeticException("正切值不存在");
        }
        return Math.tan(rad);
    }
    
    public double asin(double value) {
        double result = Math.asin(value);
        return convertFromRadians(result);
    }
    
    public double acos(double value) {
        double result = Math.acos(value);
        return convertFromRadians(result);
    }
    
    public double atan(double value) {
        double result = Math.atan(value);
        return convertFromRadians(result);
    }
    
    private double convertToRadians(double value) {
        if (currentMode == AngleMode.DEGREES) {
            return Math.toRadians(value);
        }
        return value;
    }
    
    private double convertFromRadians(double value) {
        if (currentMode == AngleMode.DEGREES) {
            return Math.toDegrees(value);
        }
        return value;
    }
    
    public void setAngleMode(AngleMode mode) {
        this.currentMode = mode;
    }
    
    public AngleMode getAngleMode() {
        return currentMode;
    }
}

在界面上,我用一个切换按钮来改变模式:

private JToggleButton angleModeButton;
private TrigonometricCalculator trigCalculator;

private void initAngleModeButton() {
    angleModeButton = new JToggleButton("Deg");
    angleModeButton.setSelected(true); // 默认角度制
    
    angleModeButton.addActionListener(e -> {
        if (angleModeButton.isSelected()) {
            angleModeButton.setText("Rad");
            trigCalculator.setAngleMode(TrigonometricCalculator.AngleMode.RADIANS);
        } else {
            angleModeButton.setText("Deg");
            trigCalculator.setAngleMode(TrigonometricCalculator.AngleMode.DEGREES);
        }
    });
}

5.4 数值格式化与精度控制

计算器显示数字时需要考虑格式化和精度。我的格式化工具:

public class NumberFormatter {
    private static final int MAX_DISPLAY_LENGTH = 16;
    private static final double EPSILON = 1e-10;
    
    public String format(double value) {
        // 处理特殊值
        if (Double.isNaN(value)) {
            return "NaN";
        }
        if (Double.isInfinite(value)) {
            return value > 0 ? "∞" : "-∞";
        }
        
        // 检查是否为整数(在误差范围内)
        if (Math.abs(value - Math.round(value)) < EPSILON) {
            long longValue = Math.round(value);
            return formatLong(longValue);
        }
        
        // 使用科学计数法处理很大或很小的数
        if (Math.abs(value) > 1e10 || (Math.abs(value) < 1e-5 && value != 0)) {
            return formatScientific(value);
        }
        
        // 普通小数格式化
        return formatDecimal(value);
    }
    
    private String formatLong(long value) {
        String str = Long.toString(value);
        if (str.length() <= MAX_DISPLAY_LENGTH) {
            return str;
        }
        return formatScientific(value);
    }
    
    private String formatDecimal(double value) {
        // 尝试不同的小数位数
        for (int decimals = 10; decimals >= 0; decimals--) {
            String format = "%." + decimals + "f";
            String result = String.format(Locale.US, format, value).replaceAll("0+$", "").replaceAll("\\.$", "");
            if (result.length() <= MAX_DISPLAY_LENGTH) {
                return result;
            }
        }
        return formatScientific(value);
    }
    
    private String formatScientific(double value) {
        // 使用科学计数法
        String scientific = String.format(Locale.US, "%.6E", value);
        
        // 简化显示:去掉不必要的0
        scientific = scientific.replaceAll("0*E", "E").replaceAll("E\\+", "E").replaceAll("E0", "E");
        
        if (scientific.length() > MAX_DISPLAY_LENGTH) {
            scientific = scientific.substring(0, MAX_DISPLAY_LENGTH);
        }
        
        return scientific;
    }
}

6. 性能优化与代码质量

一个优秀的计算器不仅要功能正确,还要性能良好、代码可维护。

6.1 事件处理的优化

计算器有几十个按钮,如果每个按钮都单独创建监听器,代码会非常冗长。我使用命令模式来优化:

public class ButtonCommandFactory {
    private final CalculatorController controller;
    
    public ButtonCommandFactory(CalculatorController controller) {
        this.controller = controller;
    }
    
    public ActionListener createCommand(String buttonText) {
        switch (buttonText) {
            case "0": case "1": case "2": case "3": case "4":
            case "5": case "6": case "7": case "8": case "9":
                return e -> controller.appendDigit(buttonText);
                
            case "+": case "-": case "*": case "/": case "^":
                return e -> controller.appendOperator(buttonText);
                
            case "=":
                return e -> controller.calculate();
                
            case "C":
                return e -> controller.clear();
                
            case "CE":
                return e -> controller.clearEntry();
                
            case "←":
                return e -> controller.backspace();
                
            case "sin": case "cos": case "tan":
                return e -> controller.appendFunction(buttonText);
                
            case "(": case ")":
                return e -> controller.appendParenthesis(buttonText);
                
            case ".":
                return e -> controller.appendDecimalPoint();
                
            default:
                return e -> controller.handleSpecialFunction(buttonText);
        }
    }
}

这样初始化按钮时就很简洁:

private void initButtons() {
    for (JButton button : allButtons) {
        ActionListener command = commandFactory.createCommand(button.getText());
        button.addActionListener(command);
    }
}

6.2 键盘支持

除了鼠标点击,还应该支持键盘操作:

private void setupKeyboardSupport() {
    // 数字键
    for (int i = 0; i <= 9; i++) {
        setupKeyBinding(String.valueOf(i), String.valueOf(i));
    }
    
    // 运算符
    setupKeyBinding("ADD", "+");
    setupKeyBinding("SUBTRACT", "-");
    setupKeyBinding("MULTIPLY", "*");
    setupKeyBinding("DIVIDE", "/");
    setupKeyBinding("ENTER", "=");
    setupKeyBinding("DECIMAL", ".");
    setupKeyBinding("ESCAPE", "C");
    setupKeyBinding("BACK_SPACE", "←");
    setupKeyBinding("PARENTHESIS_LEFT", "(");
    setupKeyBinding("PARENTHESIS_RIGHT", ")");
}

private void setupKeyBinding(String keyStroke, String buttonText) {
    InputMap inputMap = getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
    ActionMap actionMap = getRootPane().getActionMap();
    
    KeyStroke ks = KeyStroke.getKeyStroke(keyStroke);
    inputMap.put(ks, buttonText);
    actionMap.put(buttonText, new AbstractAction() {
        @Override
        public void actionPerformed(ActionEvent e) {
            // 模拟按钮点击
            for (JButton button : allButtons) {
                if (button.getText().equals(buttonText)) {
                    button.doClick();
                    break;
                }
            }
        }
    });
}

6.3 单元测试

计算器的核心算法必须经过充分测试:

public class ExpressionParserTest {
    private ExpressionParser parser;
    
    @Before
    public void setUp() {
        parser = new ExpressionParser();
    }
    
    @Test
    public void testSimpleAddition() {
        List<String> postfix = parser.infixToPostfix("3+4");
        assertEquals(Arrays.asList("3", "4", "+"), postfix);
    }
    
    @Test
    public void testOperatorPrecedence() {
        List<String> postfix = parser.infixToPostfix("3+4*5");
        assertEquals(Arrays.asList("3", "4", "5", "*", "+"), postfix);
    }
    
    @Test
    public void testParentheses() {
        List<String> postfix = parser.infixToPostfix("(3+4)*5");
        assertEquals(Arrays.asList("3", "4", "+", "5", "*"), postfix);
    }
    
    @Test
    public void testFunction() {
        List<String> postfix = parser.infixToPostfix("sin(30)+cos(45)");
        // 假设函数被映射为单字符
        assertEquals(Arrays.asList("30", "s", "45", "c", "+"), postfix);
    }
    
    @Test(expected = IllegalArgumentException.class)
    public void testInvalidExpression() {
        parser.infixToPostfix("3++4");
    }
}

public class PostfixCalculatorTest {
    private PostfixCalculator calculator;
    
    @Before
    public void setUp() {
        calculator = new PostfixCalculator();
    }
    
    @Test
    public void testAddition() {
        double result = calculator.evaluate(Arrays.asList("3", "4", "+"));
        assertEquals(7, result, 1e-10);
    }
    
    @Test
    public void testComplexExpression() {
        // 3 + 4 * 5 / 2
        double result = calculator.evaluate(Arrays.asList("3", "4", "5", "*", "2", "/", "+"));
        assertEquals(13, result, 1e-10);
    }
    
    @Test
    public void testSinFunction() {
        // sin(30°) 在弧度模式下
        double result = calculator.evaluate(Arrays.asList("0.5235987756", "s"));
        assertEquals(0.5, result, 1e-5);
    }
    
    @Test(expected = ArithmeticException.class)
    public void testDivisionByZero() {
        calculator.evaluate(Arrays.asList("5", "0", "/"));
    }
}

6.4 内存管理优化

计算器虽然不大,但也要注意内存使用:

public class CalculatorModel {
    // 使用StringBuilder而不是String拼接
    private StringBuilder currentExpression = new StringBuilder();
    
    // 使用弱引用缓存常用计算结果
    private Map<String, SoftReference<Double>> calculationCache = new WeakHashMap<>();
    
    public void appendToExpression(String text) {
        currentExpression.append(text);
    }
    
    public double calculate() {
        String expr = currentExpression.toString();
        
        // 检查缓存
        SoftReference<Double> cachedRef = calculationCache.get(expr);
        if (cachedRef != null) {
            Double cached = cachedRef.get();
            if (cached != null) {
                return cached;
            }
        }
        
        // 计算并缓存
        double result = doCalculation(expr);
        calculationCache.put(expr, new SoftReference<>(result));
        
        return result;
    }
    
    // 定期清理过期缓存
    public void cleanupCache() {
        calculationCache.entrySet().removeIf(entry -> 
            entry.getValue() != null && entry.getValue().get() == null);
    }
}

7. 打包与部署

项目完成后,我们需要把它打包成可执行的程序。

7.1 创建可执行JAR

使用Maven或Gradle可以方便地打包,这里展示手动创建可执行JAR的方法:

  1. 首先编译所有Java文件:
javac -d bin src/*.java src/*/*.java
  1. 创建manifest文件MANIFEST.MF
Manifest-Version: 1.0
Main-Class: com.example.calculator.CalculatorApp
Class-Path: .
  1. 打包JAR:
jar cvfm Calculator.jar MANIFEST.MF -C bin .

7.2 使用Launch4j创建Windows EXE

对于Windows用户,可以创建EXE文件:

  1. 下载Launch4j
  2. 配置:
    • Output file: Calculator.exe
    • Jar: Calculator.jar
    • Icon: calculator.ico (可选)
    • JRE: 指定最小Java版本

7.3 制作安装程序

使用Inno Setup或NSIS创建安装程序:

[Setup]
AppName=科学计算器
AppVersion=1.0
DefaultDirName={pf}\ScientificCalculator
DefaultGroupName=科学计算器
OutputDir=installer
OutputBaseFilename=ScientificCalculatorSetup

[Files]
Source: "Calculator.exe"; DestDir: "{app}"
Source: "*.dll"; DestDir: "{app}"
Source: "README.txt"; DestDir: "{app}"

[Icons]
Name: "{group}\科学计算器"; Filename: "{app}\Calculator.exe"
Name: "{commondesktop}\科学计算器"; Filename: "{app}\Calculator.exe"

7.4 跨平台考虑

如果你的计算器要跨平台运行,需要注意:

  1. 文件路径分隔符:使用File.separator而不是硬编码/\
  2. 换行符:使用System.lineSeparator()
  3. 字体可用性:指定回退字体
  4. LookAndFeel兼容性:测试不同系统的外观
public class PlatformUtils {
    public static String getConfigPath() {
        String os = System.getProperty("os.name").toLowerCase();
        String userHome = System.getProperty("user.home");
        
        if (os.contains("win")) {
            return userHome + "\\AppData\\Local\\ScientificCalculator\\";
        } else if (os.contains("mac")) {
            return userHome + "/Library/Application Support/ScientificCalculator/";
        } else {
            return userHome + "/.scientific-calculator/";
        }
    }
    
    public static Font getDefaultFont() {
        String os = System.getProperty("os.name").toLowerCase();
        
        if (os.contains("win")) {
            return new Font("Microsoft YaHei", Font.PLAIN, 12);
        } else if (os.contains("mac")) {
            return new Font("PingFang SC", Font.PLAIN, 12);
        } else {
            return new Font("DejaVu Sans", Font.PLAIN, 12);
        }
    }
}

8. 项目总结与进一步优化方向

通过这个科学计算器项目,我们不仅实现了一个实用的工具,还实践了多个重要的编程概念:

  1. GUI设计:Swing组件、布局管理器、事件处理
  2. 算法实现:中缀转后缀、栈的应用、表达式求值
  3. 软件工程:MVC模式、单元测试、错误处理
  4. 用户体验:输入验证、实时反馈、键盘支持

如果你还想进一步扩展这个项目,这里有一些方向:

8.1 图形计算功能

添加函数绘图功能,让用户可视化数学函数:

public class FunctionPlotter {
    public BufferedImage plot(String function, double xMin, double xMax, 
                              double yMin, double yMax, int width, int height) {
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2d = image.createGraphics();
        
        // 绘制坐标轴
        drawAxes(g2d, width, height, xMin, xMax, yMin, yMax);
        
        // 计算并绘制函数
        List<Point2D> points = evaluateFunction(function, xMin, xMax, 1000);
        drawFunction(g2d, points, width, height, xMin, xMax, yMin, yMax);
        
        return image;
    }
}

8.2 矩阵计算

实现矩阵运算功能:

public class MatrixCalculator {
    public double[][] add(double[][] a, double[][] b) {
        // 矩阵加法
    }
    
    public double[][] multiply(double[][] a, double[][] b) {
        // 矩阵乘法
    }
    
    public double determinant(double[][] matrix) {
        // 行列式计算
    }
    
    public double[][] inverse(double[][] matrix) {
        // 矩阵求逆
    }
}

8.3 单位换算

添加常用单位换算功能:

public class UnitConverter {
    public enum UnitType {
        LENGTH, AREA, VOLUME, TEMPERATURE, WEIGHT
    }
    
    public double convert(double value, String fromUnit, String toUnit, UnitType type) {
        switch (type) {
            case LENGTH:
                return convertLength(value, fromUnit, toUnit);
            case TEMPERATURE:
                return convertTemperature(value, fromUnit, toUnit);
            // ... 其他类型
        }
    }
    
    private double convertLength(double value, String from, String to) {
        // 长度单位换算表
        Map<String, Double> toMeter = new HashMap<>();
        toMeter.put("m", 1.0);
        toMeter.put("cm", 0.01);
        toMeter.put("km", 1000.0);
        toMeter.put("in", 0.0254);
        toMeter.put("ft", 0.3048);
        toMeter.put("mi", 1609.344);
        
        double inMeters = value * toMeter.get(from);
        return inMeters / toMeter.get(to);
    }
}

8.4 插件系统

设计插件架构,允许用户扩展功能:

public interface CalculatorPlugin {
    String getName();
    String getDescription();
    JComponent createUI(CalculatorContext context);
    void onEnable();
    void onDisable();
}

public class PluginManager {
    private List<CalculatorPlugin> plugins = new ArrayList<>();
    
    public void loadPlugin(File pluginJar) {
        // 使用URLClassLoader动态加载插件
    }
    
    public void registerPlugin(CalculatorPlugin plugin) {
        plugins.add(plugin);
        plugin.onEnable();
    }
}

这个科学计算器项目虽然基础,但涵盖的知识点非常全面。从界面设计到算法实现,从错误处理到用户体验,每一个环节都能学到实用的编程技能。最重要的是,通过完成这样一个完整的项目,你能建立起解决复杂问题的信心和能力。

我在实际开发中发现,最难的不是实现某个具体功能,而是如何让各个模块优雅地协作,如何让代码既灵活又健壮。这个计算器项目就像是一个微型的软件工程实践,你能在其中体验到从需求分析、设计、编码、测试到部署的完整流程。

如果你在实现过程中遇到问题,或者有新的想法想要添加,随时可以基于这个框架进行扩展。编程最有意思的地方就在于,你永远可以把它做得更好。

源码直接下载地址: https://pan.quark.cn/s/a4b39357ea24 平台简介 License Spring Boot JDK-17 本项目基于ruoyi-vue-fast实现 项目代码、文档 均开源免费可商用 遵循开源协议在项目中保留开源协议文件即可 活到老写到老 为兴趣而开源 为学习而开源 为让大家真正可以学到技术而开源 若依wms是一套基于若依的wms仓库管理系统,支持lodop和网页打印入库单、出库单。 毫无保留给个人及企业免费使用。 前端采用Vue、Element UI。 后端采用Spring Boot、Spring Security、Redis & Jwt。 权限认证使用Jwt,支持多终端认证系统。 支持加载动态权限菜单,多方式轻松权限控制。 高效率开发,使用代码生成器可以一键生成前后端代码。 前端项目地址 gitee https://gitee.com/zccbbg/ruo-yi-wms-vue https://.com/zccbbg/RuoYi-WMS-VUE 若依实战技术专栏 关注文末公众号回复:星球 在线体验 https://wms.ichengle.top/ 不同分支介绍 jdk17+vue3,支持多仓库,没有库区概念,操作简单,覆盖大部分库存应用场景。 jdk17+vue3,支持多仓库、多库区,记录生产日期、过期日期、sn,可适配一物一码,操作相对复杂。 jdk8+vue2 同步lite分支 若依wms功能 首页:库存预警与到期提醒、基础数据报表展示 仓库/库区:管理维护仓库基础数据 物料:管理维护物料基础数据 客户/供应商:管理维护联系人基础数据 入库:创建入库单后包括如下几个状态:暂存、作废、完成入库,入库类型包括:采购...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值