1. 为什么选择Java Swing开发计算器?
十年前我刚入行Java开发时,第一个独立完成的项目就是Swing计算器。当时导师说:"能把计算器做明白,GUI编程就入门了。"如今各种现代化框架层出不穷,但Swing作为Java原生GUI工具包,依然是学习桌面开发的绝佳起点。
用Swing开发计算器至少有三大优势:首先,它内置于JDK,无需额外依赖,一个简单的javac命令就能编译运行;其次,MVC架构清晰,事件监听机制非常适合计算器这种交互密集型的应用;最重要的是,通过这个项目你能掌握GUI编程的核心思想,这些思想在Android、JavaFX等现代框架中依然适用。
我见过不少开发者跳过Swing直接学JavaFX,结果遇到布局问题还是一头雾水。计算器虽小,但包含了文本框、按钮、事件处理、业务逻辑等完整要素,是检验GUI编程能力的试金石。
2. 计算器核心设计思路
2.1 功能规划
一个完整的计算器应该包含:
- 基础运算(加减乘除)
- 连续运算(3+5×2)
- 正负号切换
- 百分号计算
- 清空功能(CE/C)
- 小数点处理
- 错误处理(如除零错误)
进阶功能可以考虑:
- 历史记录
- 内存存储(M+/M-/MR)
- 科学计算(三角函数、指数等)
- 皮肤切换
2.2 界面布局分析
采用经典计算器布局,从上到下分为:
- 结果显示区(JTextField)
- 功能按钮区(退格、清空等)
- 数字输入区(0-9)
- 运算符区(+-×÷)
- 等号按钮
使用BorderLayout作为根布局:
- NORTH放结果显示框
- CENTER用GridLayout管理按钮
java复制JPanel buttonPanel = new JPanel(new GridLayout(5, 4, 3, 3)); // 5行4列,间距3px
2.3 数据模型设计
需要维护三个核心变量:
currentInput:当前输入值previousValue:前一个操作数currentOperator:当前运算符
运算流程示例:
- 用户输入"5" → currentInput="5"
- 点击"+" → previousValue=5, currentOperator="+", currentInput清零
- 输入"3" → currentInput="3"
- 点击"=" → 执行5+3,显示结果8
3. 关键实现细节
3.1 按钮事件处理
为所有按钮添加统一的ActionListener:
java复制private class ButtonClickListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
String command = e.getActionCommand();
switch (command) {
case "0": case "1": case "2": /* 数字处理 */ break;
case "+": case "-": /* 运算符处理 */ break;
case "=": /* 计算结果 */ break;
case "C": /* 清空 */ break;
}
}
}
注意:不要为每个按钮单独创建监听器,这会导致代码臃肿且难以维护
3.2 连续运算实现
处理运算符时的核心逻辑:
java复制private void handleOperator(String newOperator) {
if (currentInput.isEmpty()) {
currentOperator = newOperator;
return;
}
if (!previousValue.isEmpty()) {
calculateResult();
}
previousValue = currentInput;
currentInput = "";
currentOperator = newOperator;
}
3.3 小数点处理要点
必须防止重复输入小数点:
java复制private void appendDecimalPoint() {
if (currentInput.contains(".")) {
return; // 已有小数点则忽略
}
if (currentInput.isEmpty()) {
currentInput = "0."; // 开头的小数点补零
} else {
currentInput += ".";
}
}
4. 完整实现代码解析
4.1 界面构建
java复制public class Calculator extends JFrame {
private JTextField display;
private String currentInput = "";
private String previousValue = "";
private String currentOperator = "";
public Calculator() {
setTitle("Java计算器");
setSize(300, 400);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new BorderLayout(5, 5));
// 结果显示框
display = new JTextField();
display.setEditable(false);
display.setFont(new Font("Arial", Font.PLAIN, 24));
add(display, BorderLayout.NORTH);
// 按钮面板
JPanel buttonPanel = new JPanel(new GridLayout(5, 4, 3, 3));
String[] buttons = {
"7", "8", "9", "/",
"4", "5", "6", "*",
"1", "2", "3", "-",
"0", ".", "=", "+",
"C", "±", "%", "⌫"
};
ButtonClickListener listener = new ButtonClickListener();
for (String text : buttons) {
JButton button = new JButton(text);
button.addActionListener(listener);
buttonPanel.add(button);
}
add(buttonPanel, BorderLayout.CENTER);
}
}
4.2 计算逻辑实现
java复制private void calculateResult() {
if (previousValue.isEmpty() || currentInput.isEmpty() || currentOperator.isEmpty()) {
return;
}
double result = 0;
double num1 = Double.parseDouble(previousValue);
double num2 = Double.parseDouble(currentInput);
switch (currentOperator) {
case "+": result = num1 + num2; break;
case "-": result = num1 - num2; break;
case "*": result = num1 * num2; break;
case "/":
if (num2 == 0) {
display.setText("Error");
resetCalculator();
return;
}
result = num1 / num2;
break;
}
display.setText(String.valueOf(result));
currentInput = String.valueOf(result);
previousValue = "";
currentOperator = "";
}
5. 进阶功能实现
5.1 历史记录功能
添加LinkedList保存历史记录:
java复制private LinkedList<String> history = new LinkedList<>();
private void addToHistory(String expression, String result) {
history.addFirst(expression + " = " + result);
if (history.size() > 10) {
history.removeLast();
}
}
可以添加一个侧边栏展示历史:
java复制JList<String> historyList = new JList<>(historyModel);
JScrollPane historyScroll = new JScrollPane(historyList);
add(historyScroll, BorderLayout.EAST);
5.2 皮肤切换功能
定义不同颜色方案:
java复制private static final Color[] THEMES = {
new Color(240, 240, 240), // 浅色
new Color(45, 45, 45), // 深色
new Color(200, 230, 255) // 蓝色
};
private void changeTheme(int themeIndex) {
getContentPane().setBackground(THEMES[themeIndex]);
// 遍历所有组件设置颜色
for (Component comp : getComponents()) {
if (comp instanceof JPanel) {
comp.setBackground(THEMES[themeIndex]);
}
}
}
6. 常见问题与调试技巧
6.1 数字格式化问题
直接使用Double会出现精度问题:
java复制// 错误做法
double d = 0.1 + 0.2; // 得到0.30000000000000004
// 正确做法
BigDecimal bd1 = new BigDecimal("0.1");
BigDecimal bd2 = new BigDecimal("0.2");
BigDecimal result = bd1.add(bd2); // 精确得到0.3
6.2 按钮响应延迟
如果界面卡顿,检查是否在EDT(事件分发线程)中执行耗时操作:
java复制// 错误做法
button.addActionListener(e -> {
performLongCalculation(); // 会阻塞界面
});
// 正确做法
button.addActionListener(e -> {
new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() {
performLongCalculation();
return null;
}
}.execute();
});
6.3 内存泄漏预防
当添加多个监听器时,记得移除旧的:
java复制// 错误做法
component.addMouseListener(listener1);
component.addMouseListener(listener2); // 两个监听器都会触发
// 正确做法
component.removeMouseListener(listener1);
component.addMouseListener(listener2);
7. 项目打包与分发
7.1 生成可执行JAR
在pom.xml中添加maven-assembly-plugin:
xml复制<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>com.example.Calculator</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
运行mvn package生成包含所有依赖的jar。
7.2 制作原生安装包
使用jpackage(JDK14+):
bash复制jpackage --name MyCalculator --input target --main-jar calculator-1.0.jar --main-class com.example.Calculator --type dmg
支持生成Windows的exe、Mac的dmg和Linux的deb/rpm。
8. 项目扩展方向
-
表达式解析:实现复杂表达式计算,如"(3+5)×2"
- 使用Dijkstra的Shunting-yard算法
- 或引入ANTLR等解析器生成器
-
单位换算:增加长度、重量等单位换算功能
java复制enum LengthUnit { MM(0.001), CM(0.01), M(1), KM(1000); private final double meterRatio; // ... } -
图表绘制:扩展为科学计算器,绘制函数图像
- 使用JFreeChart库
- 实现坐标轴缩放和平移
-
多语言支持:通过ResourceBundle实现国际化
properties复制# messages_zh.properties button.equals=等于 button.clear=清除 -
插件系统:设计SPI接口,支持动态加载功能模块
java复制public interface CalculatorPlugin { String getButtonText(); void onClick(CalculatorContext context); }
这个项目我前后重构过三次:第一次只实现了基本功能,代码全部写在main方法里;第二次引入了MVC模式;第三次加入了插件化设计。每次重构都让我对面向对象和GUI编程有新的认识。建议你在完成基础版本后,尝试用不同的设计模式重构,这才是提升编程能力的捷径。