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,你可以让计算器看起来更现代化。我常用的几个风格:
| 风格名称 | 设置代码 | 视觉效果 | 适用场景 |
|---|---|---|---|
| Nimbus | UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel") | 现代扁平化 | 大多数情况 |
| Windows | UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel") | Win10风格 | Windows平台 |
| Metal | UIManager.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 按钮设计与用户体验
按钮不仅仅是点击的控件,它还是用户与计算器交互的主要方式。好的按钮设计要考虑:
- 视觉反馈:鼠标悬停、点击时的颜色变化
- 状态反馈:按钮是否可用、是否被选中
- 操作反馈:点击后的即时响应
我通常会给按钮添加鼠标监听器来增强交互体验:
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 中缀转后缀算法详解
中缀转后缀的算法基于栈数据结构,遵循以下规则:
- 遇到操作数,直接输出到后缀表达式
- 遇到运算符,与栈顶运算符比较优先级:
- 如果栈为空或栈顶是左括号,直接入栈
- 如果优先级高于栈顶,入栈
- 如果优先级不高于栈顶,弹出栈顶并输出,然后继续比较
- 遇到左括号,直接入栈
- 遇到右括号,弹出栈顶运算符并输出,直到遇到左括号(左括号弹出但不输出)
- 表达式结束后,弹出栈中所有运算符
看看我的实现代码:
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 后缀表达式计算
得到后缀表达式后,计算就简单多了。算法如下:
- 从左到右扫描后缀表达式
- 遇到操作数,入栈
- 遇到运算符,弹出栈顶两个操作数(注意顺序),计算结果,结果入栈
- 扫描结束,栈顶就是最终结果
对于一元运算符(如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的方法:
- 首先编译所有Java文件:
javac -d bin src/*.java src/*/*.java
- 创建manifest文件
MANIFEST.MF:
Manifest-Version: 1.0
Main-Class: com.example.calculator.CalculatorApp
Class-Path: .
- 打包JAR:
jar cvfm Calculator.jar MANIFEST.MF -C bin .
7.2 使用Launch4j创建Windows EXE
对于Windows用户,可以创建EXE文件:
- 下载Launch4j
- 配置:
- Output file:
Calculator.exe - Jar:
Calculator.jar - Icon:
calculator.ico(可选) - JRE: 指定最小Java版本
- Output file:
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 跨平台考虑
如果你的计算器要跨平台运行,需要注意:
- 文件路径分隔符:使用
File.separator而不是硬编码/或\ - 换行符:使用
System.lineSeparator() - 字体可用性:指定回退字体
- 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. 项目总结与进一步优化方向
通过这个科学计算器项目,我们不仅实现了一个实用的工具,还实践了多个重要的编程概念:
- GUI设计:Swing组件、布局管理器、事件处理
- 算法实现:中缀转后缀、栈的应用、表达式求值
- 软件工程:MVC模式、单元测试、错误处理
- 用户体验:输入验证、实时反馈、键盘支持
如果你还想进一步扩展这个项目,这里有一些方向:
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();
}
}
这个科学计算器项目虽然基础,但涵盖的知识点非常全面。从界面设计到算法实现,从错误处理到用户体验,每一个环节都能学到实用的编程技能。最重要的是,通过完成这样一个完整的项目,你能建立起解决复杂问题的信心和能力。
我在实际开发中发现,最难的不是实现某个具体功能,而是如何让各个模块优雅地协作,如何让代码既灵活又健壮。这个计算器项目就像是一个微型的软件工程实践,你能在其中体验到从需求分析、设计、编码、测试到部署的完整流程。
如果你在实现过程中遇到问题,或者有新的想法想要添加,随时可以基于这个框架进行扩展。编程最有意思的地方就在于,你永远可以把它做得更好。
&spm=1001.2101.3001.5002&articleId=154828122&d=1&t=3&u=19c35b86c3e14187bc1dc27d4332a3a8)
941

被折叠的 条评论
为什么被折叠?



