三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Java Swing高级计算器实战:从GUI布局到事件驱动编程

Java Swing高级计算器实战:从GUI布局到事件驱动编程

1. 项目概述:从命令行到图形界面的计算器进化

很多Java初学者在掌握了基础语法和面向对象概念后,都会尝试用控制台写一个简单的计算器。加减乘除,处理几个数字,逻辑并不复杂。但当你把这个计算器从黑乎乎的命令行窗口,搬到一个有按钮、有显示框、能响应鼠标点击的图形窗口里时,你会发现这完全是另一个维度的挑战。这个“JavaSwing实现高级计算器”的项目,就是这样一个经典的、能让你把Java核心知识和GUI编程实战结合起来的练手项目。它远不止是“1+1=2”那么简单,它涉及到事件驱动编程、组件布局管理、状态维护、异常处理以及一个完整桌面应用的基本架构。

所谓“高级”,在这里并不是指要实现科学计算器里那些复杂的三角函数或积分运算——虽然你也可以往这个方向扩展。更核心的“高级”体现在:一个健壮的、用户友好的、具备完整交互逻辑的图形化计算器。它要能清晰显示输入和结果,能处理连续运算(比如 3 + 4 * 5),要有退格、清空等编辑功能,要能妥善处理除零错误、输入格式错误等异常情况,并且整个界面布局要合理、美观、符合用户操作直觉。通过亲手实现它,你会深刻理解Swing的事件监听机制(ActionListener),掌握BorderLayoutGridLayout等布局管理器的灵活运用,学会如何组织代码使得逻辑清晰(比如将界面构建、事件处理、计算逻辑分离),这些都是开发更复杂桌面应用的基石。

2. 核心需求与功能设计拆解

在动手写代码之前,我们必须先把计算器应该做什么、怎么做规划清楚。一个高级计算器,其功能可以模块化分解。

2.1 核心功能模块定义

首先,我们需要明确计算器的核心功能集合,这决定了我们的界面需要哪些按钮,以及后端逻辑需要处理哪些操作。

  1. 基本算术运算:这是基石,包括加法(+)、减法(-)、乘法(*)、除法(/)。这里需要注意,在计算机中,除法要特别处理整数除法和浮点数除法的区别,以及至关重要的除零错误。
  2. 连续运算与表达式求值:这是体现“高级”的关键。用户输入“3 + 4”后点击“=”得到7,这是简单的。但如果用户输入“3 + 4 * 5”,计算器是应该立即根据运算符优先级计算(结果为23),还是先计算“3+4”再乘以5(结果为35)?通常,我们模拟真实计算器的行为:在按下“=”之前,不进行真正的运算,只是记录表达式。或者,我们可以实现一个简单的实时求值逻辑。更复杂的,需要引入表达式解析器(如调度场算法),但这属于进阶内容。我们至少要实现不带优先级的连续运算,例如:输入“3 + 4 =”得7,接着输入“* 5 =”得35。
  3. 数据输入与显示:需要一个JTextFieldJLabel来显示当前输入的数字、运算符以及最终结果。它应该能处理长数字的显示,可能需要科学计数法。
  4. 编辑功能:包括清空当前输入(CE)、清空全部(C)、退格(←)。CE通常清除当前输入的数字,而C重置整个计算状态。退格则逐位删除当前输入。
  5. 辅助功能:正负号切换(±)、小数点(.)、百分号(%)。百分号的处理需要定义,通常是除以100。
  6. 错误处理:当发生除零、无效输入(如连续两个小数点)、表达式不完整时,应在显示区域给出明确的错误提示(如“Error”或“除数不能为零”),并且需要提供一种方式(如按C键)从错误状态恢复。

2.2 界面布局与组件规划

基于以上功能,我们可以设计界面。一个经典的计算器布局如下:

  • 顶部显示区域:使用一个JTextField,设置为右对齐,不可编辑(setEditable(false)),但允许复制结果。字体可以大一些,方便查看。
  • 中部按钮区域:使用GridLayout网格布局管理器来排列按钮。一个常见的布局是:
    • 第一行:C(清空),CE(清空输入),(退格),/(除)
    • 第二行:7,8,9,*(乘)
    • 第三行:4,5,6,-(减)
    • 第四行:1,2,3,+(加)
    • 第五行:±(正负),0,.(小数点),=(等于)
  • 整体窗口:使用JFrame作为主窗口。将显示区域放在BorderLayout.NORTH,将按钮面板放在BorderLayout.CENTER

注意:按钮的排列和配色可以自定义,但务必遵循用户习惯。例如,等号(=)按钮通常用醒目的颜色(如蓝色或橙色),数字键和功能键在颜色上有所区分。

2.3 状态维护与计算逻辑设计

这是项目的核心难点。图形界面是“面子”,计算逻辑和状态维护是“里子”。我们需要设计一些核心变量来跟踪计算器的状态:

  • operand1:存储第一个操作数(或上一次运算的结果)。
  • operand2:存储当前输入的操作数。
  • operator:存储当前选定的运算符(+, -, *, /)。
  • waitingForOperand:一个布尔标志,表示当前是否在等待用户输入一个新的操作数。例如,当用户按下“+”号后,这个标志应设为true,表示接下来输入的数字属于第二个操作数。
  • displayValue:一个字符串或BigDecimal,与显示组件的内容同步。

计算流程的逻辑

  1. 用户点击数字键:将数字追加到当前操作数的末尾,并更新显示。
  2. 用户点击运算符(+, -, *, /):
    • 如果waitingForOperandfalse(即当前已有一个操作数在输入),则先执行上一次挂起的运算(如果有),然后将结果存入operand1,并设置新的operator
    • waitingForOperand设为true,准备接收第二个操作数。
  3. 用户点击等号(=):
    • 如果有一个挂起的运算符和两个操作数,则执行运算。
    • 将结果显示出来,同时这个结果成为新的operand1(便于连续运算)。
    • 重置operatornull,并将waitingForOperand设为true

这种设计模式通常被称为“状态模式”或“简单命令模式”的雏形,它能有效管理计算器复杂的交互状态。

3. 关键技术实现与代码解析

接下来,我们深入到代码层面,看看如何将上述设计落地。我会分模块讲解关键代码,并解释为什么这么做。

3.1 图形用户界面构建

首先,我们构建计算器的“脸面”。这里采用JFrameJPanel的组合。

import javax.swing.*; import java.awt.*; public class CalculatorFrame extends JFrame { private JTextField displayField; private JPanel buttonPanel; public CalculatorFrame() { // 1. 基础窗口设置 setTitle("高级计算器 - Java Swing实现"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(400, 500); setLayout(new BorderLayout(5, 5)); // 设置边框布局,并添加组件间距 // 2. 创建并配置显示区域 displayField = new JTextField(); displayField.setHorizontalAlignment(JTextField.RIGHT); // 文本右对齐 displayField.setEditable(false); // 禁止直接编辑,但可通过按钮输入 displayField.setFont(new Font("Arial", Font.BOLD, 24)); // 设置大字体 displayField.setBackground(Color.WHITE); // 设置一个初始值,避免显示为空 displayField.setText("0"); add(displayField, BorderLayout.NORTH); // 3. 创建按钮面板 buttonPanel = new JPanel(); // 使用4行5列的网格布局,组件间有5像素的间距 buttonPanel.setLayout(new GridLayout(5, 4, 5, 5)); // 4. 定义按钮标签 String[] buttonLabels = { "C", "CE", "←", "/", "7", "8", "9", "*", "4", "5", "6", "-", "1", "2", "3", "+", "±", "0", ".", "=" }; // 5. 创建按钮并添加到面板 for (String label : buttonLabels) { JButton button = new JButton(label); button.setFont(new Font("Arial", Font.PLAIN, 18)); // 为等号按钮设置特殊样式 if ("=".equals(label)) { button.setBackground(new Color(66, 133, 244)); // 谷歌蓝 button.setForeground(Color.WHITE); } else if ("/".equals(label) || "*".equals(label) || "-".equals(label) || "+".equals(label)) { // 运算符按钮用另一种颜色 button.setBackground(new Color(218, 220, 224)); // 浅灰色 } buttonPanel.add(button); // 事件监听器将在下一节添加 } add(buttonPanel, BorderLayout.CENTER); // 6. 窗口居中显示 setLocationRelativeTo(null); } public JTextField getDisplayField() { return displayField; } public JPanel getButtonPanel() { return buttonPanel; } }

实操心得:在设置布局时,为BorderLayoutGridLayout添加间距参数(如new BorderLayout(5,5)new GridLayout(5,4,5,5))能让界面看起来不那么拥挤,更美观。字体大小的选择也很关键,显示区域字体要大且清晰,按钮字体要适中。

3.2 事件处理与业务逻辑绑定

界面建好了,但按钮点了没反应。现在我们需要给按钮注入“灵魂”——事件监听器。我们将采用ActionListener,并为不同类型的按钮分配不同的行为。

我们创建一个CalculatorLogic类来处理核心计算逻辑和状态,并在主类中绑定事件。

import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.math.BigDecimal; import java.math.RoundingMode; public class CalculatorLogic { // 状态变量 private BigDecimal currentOperand; // 当前显示的值/正在输入的操作数 private BigDecimal previousOperand; // 前一个操作数(或上一次结果) private String pendingOperator; // 等待执行的运算符 private boolean waitingForNewOperand; // 标志:是否在输入新操作数 public CalculatorLogic() { clearAll(); } // 处理数字按钮输入 public void inputDigit(String digit) { if (waitingForNewOperand) { currentOperand = BigDecimal.ZERO; waitingForNewOperand = false; } // 防止输入过长的数字(这里简单处理,超过15位不再追加) String currentText = (currentOperand.compareTo(BigDecimal.ZERO) == 0) ? "" : currentOperand.stripTrailingZeros().toPlainString(); if (currentText.replace(".", "").length() >= 15) { return; } // 拼接数字 currentOperand = new BigDecimal(currentOperand.toString() + digit); } // 处理小数点输入 public void inputDecimalPoint() { if (waitingForNewOperand) { currentOperand = BigDecimal.ZERO; waitingForNewOperand = false; } String currentText = currentOperand.toString(); // 防止输入多个小数点 if (!currentText.contains(".")) { currentOperand = new BigDecimal(currentText + "."); } } // 处理运算符按钮 public void performOperation(String op) { if (!waitingForNewOperand) { // 如果已经有挂起的运算,先计算它 if (pendingOperator != null && !pendingOperator.isEmpty()) { calculate(); } previousOperand = currentOperand; waitingForNewOperand = true; } pendingOperator = op; } // 执行计算 public void calculate() { if (pendingOperator == null || waitingForNewOperand) { return; // 没有待执行的运算符,或第二个操作数还没输入 } BigDecimal result = BigDecimal.ZERO; try { switch (pendingOperator) { case "+": result = previousOperand.add(currentOperand); break; case "-": result = previousOperand.subtract(currentOperand); break; case "*": result = previousOperand.multiply(currentOperand); break; case "/": if (currentOperand.compareTo(BigDecimal.ZERO) == 0) { throw new ArithmeticException("除数不能为零"); } // 除法保留10位小数,并采用四舍五入 result = previousOperand.divide(currentOperand, 10, RoundingMode.HALF_UP); break; } // 计算结果赋值给当前操作数,用于显示和后续计算 currentOperand = result.stripTrailingZeros(); // 去除末尾无意义的零 pendingOperator = null; previousOperand = BigDecimal.ZERO; waitingForNewOperand = true; // 计算完成后,等待新的输入 } catch (ArithmeticException e) { // 处理除零等算术异常 clearAll(); currentOperand = null; // 用null表示错误状态 } } // 清空所有(C) public void clearAll() { currentOperand = BigDecimal.ZERO; previousOperand = BigDecimal.ZERO; pendingOperator = null; waitingForNewOperand = true; } // 清空当前输入(CE) public void clearEntry() { currentOperand = BigDecimal.ZERO; waitingForNewOperand = true; } // 退格 public void backspace() { if (waitingForNewOperand) { return; } String text = currentOperand.toPlainString(); if (text.length() > 1) { text = text.substring(0, text.length() - 1); currentOperand = new BigDecimal(text); } else { currentOperand = BigDecimal.ZERO; waitingForNewOperand = true; } } // 正负号切换 public void negate() { currentOperand = currentOperand.negate(); } // 获取当前显示字符串 public String getDisplayValue() { if (currentOperand == null) { return "Error"; } // 格式化输出,避免科学计数法,并处理整数情况 String plainString = currentOperand.stripTrailingZeros().toPlainString(); // 如果过长,可以截断或使用科学计数法(这里简单返回) return plainString; } // ... 其他getter方法 }

现在,在主启动类中,我们将界面和逻辑连接起来:

public class AdvancedCalculator { public static void main(String[] args) { SwingUtilities.invokeLater(() -> { CalculatorFrame frame = new CalculatorFrame(); CalculatorLogic logic = new CalculatorLogic(); // 为所有按钮添加事件监听器 for (Component comp : frame.getButtonPanel().getComponents()) { if (comp instanceof JButton) { JButton button = (JButton) comp; button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String command = e.getActionCommand(); handleButtonClick(command, logic, frame.getDisplayField()); } }); } } frame.setVisible(true); }); } private static void handleButtonClick(String cmd, CalculatorLogic logic, JTextField display) { try { switch (cmd) { case "0": case "1": case "2": case "3": case "4": case "5": case "6": case "7": case "8": case "9": logic.inputDigit(cmd); break; case ".": logic.inputDecimalPoint(); break; case "+": case "-": case "*": case "/": logic.performOperation(cmd); break; case "=": logic.calculate(); break; case "C": logic.clearAll(); break; case "CE": logic.clearEntry(); break; case "←": logic.backspace(); break; case "±": logic.negate(); break; } // 每次操作后更新显示 display.setText(logic.getDisplayValue()); } catch (Exception ex) { display.setText("Error"); logic.clearAll(); // 出错后重置状态 } } }

核心技巧:使用BigDecimal而不是double进行金融或精确计算。double类型有精度损失,例如0.1 + 0.2的结果并非精确的0.3BigDecimal可以精确表示和计算小数,通过setScaleRoundingMode可以控制精度和舍入方式,这对于计算器来说是至关重要的。

3.3 高级功能实现:连续运算与优先级

上面实现的逻辑已经支持了“3 + 4 = 7, * 5 = 35”这种不带优先级的连续运算。但它无法正确处理“3 + 4 * 5”的优先级问题。要实现标准的算术优先级,我们需要引入表达式求值。

一个相对简单且经典的方法是调度场算法。我们可以在用户按下“=”时,将之前输入的数字和运算符序列(如["3", "+", "4", "*", "5"])转换成一个后缀表达式(逆波兰表达式),然后求值。

这里给出一个简化的思路和部分代码:

  1. 维护两个栈:一个操作数栈(BigDecimal),一个运算符栈(String)。
  2. 定义优先级*/的优先级高于+-
  3. 中缀转后缀算法
    • 遇到数字,直接输出(或压入操作数栈的最终顺序)。
    • 遇到运算符,与运算符栈顶元素比较优先级。如果栈顶优先级更高或相等,则弹出栈顶运算符并输出,然后当前运算符入栈。
    • 左括号入栈(我们这里没括号,但扩展时有用)。
    • 遇到等号或表达式结束,将运算符栈中所有元素依次弹出并输出。
  4. 后缀表达式求值
    • 从左到右扫描后缀表达式。
    • 遇到数字,入操作数栈。
    • 遇到运算符,弹出栈顶两个操作数进行运算,结果入栈。
    • 最后栈中剩下的一个数就是结果。

由于实现完整的表达式解析器代码量较大,在初级项目中,可以先实现无优先级的连续运算作为核心,将表达式求值作为可选的高级扩展。这能让你更专注于Swing和事件处理本身。

4. 界面美化与用户体验优化

一个看起来专业、用起来顺手的计算器,离不开细节的打磨。

4.1 键盘事件支持

除了鼠标点击,很多用户习惯使用键盘输入。我们可以为JFrame添加键盘监听器。

frame.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { char keyChar = e.getKeyChar(); int keyCode = e.getKeyCode(); String cmd = null; if (keyChar >= '0' && keyChar <= '9') { cmd = String.valueOf(keyChar); } else { switch (keyCode) { case KeyEvent.VK_ADD: cmd = "+"; break; case KeyEvent.VK_SUBTRACT: cmd = "-"; break; case KeyEvent.VK_MULTIPLY: cmd = "*"; break; case KeyEvent.VK_DIVIDE: cmd = "/"; break; case KeyEvent.VK_DECIMAL: cmd = "."; break; case KeyEvent.VK_ENTER: case KeyEvent.VK_EQUALS: cmd = "="; break; case KeyEvent.VK_BACK_SPACE: cmd = "←"; break; case KeyEvent.VK_ESCAPE: cmd = "C"; break; case KeyEvent.VK_DELETE: cmd = "CE"; break; } } if (cmd != null) { // 模拟按钮点击事件 handleButtonClick(cmd, logic, display); } } }); frame.setFocusable(true); // 确保Frame能接收键盘事件

注意事项:添加键盘监听后,要注意按键冲突。例如,小键盘的“/”和主键盘的“/”可能不同。同时,要确保在按钮点击事件处理函数handleButtonClick中能正确处理这些来自键盘的命令字符串。

4.2 显示区域的优化

  • 文本溢出处理:当数字过长时,JTextField可能显示不全。我们可以监听显示文本的变化,动态调整字体大小。
    // 在更新display文本后,可以添加一个自动调整字体大小的逻辑 private static void autoScaleFont(JTextField field) { Font originalFont = field.getFont(); String text = field.getText(); FontMetrics fm = field.getFontMetrics(originalFont); int textWidth = fm.stringWidth(text); int fieldWidth = field.getWidth() - 10; // 留一些边距 if (textWidth > fieldWidth && fieldWidth > 0) { // 计算缩放比例 float scale = (float)fieldWidth / (float)textWidth; int newSize = Math.max(12, (int)(originalFont.getSize() * scale * 0.9)); // 最小12号字 field.setFont(originalFont.deriveFont((float)newSize)); } else { // 恢复原始字体大小 field.setFont(originalFont); } } // 在handleButtonClick中更新显示后调用:autoScaleFont(display);
  • 错误状态高亮:当显示“Error”时,可以将背景色变为浅红色,提醒用户。
    if ("Error".equals(display.getText())) { display.setBackground(new Color(255, 200, 200)); // 浅红 } else { display.setBackground(Color.WHITE); }

4.3 按钮反馈与动画

为了提升交互感,可以为按钮添加简单的鼠标悬停和按下效果。这可以通过改变按钮的背景色和边框来实现。

// 在创建按钮的循环中,为每个按钮添加MouseListener button.addMouseListener(new MouseAdapter() { @Override public void mouseEntered(MouseEvent e) { if (!button.getBackground().equals(Color.BLUE)) { // 避免改变等号按钮的特殊颜色 button.setBackground(button.getBackground().brighter()); } } @Override public void mouseExited(MouseEvent e) { // 恢复原始颜色,这里需要记录或判断按钮的原始类别颜色 if ("=".equals(cmd)) { button.setBackground(new Color(66, 133, 244)); } else if ("/".equals(cmd) || "*".equals(cmd) || "-".equals(cmd) || "+".equals(cmd)) { button.setBackground(new Color(218, 220, 224)); } else { button.setBackground(UIManager.getColor("Button.background")); // 系统默认色 } } });

5. 项目扩展与进阶思考

完成基础版本后,你可以从这个项目出发,进行多方向的扩展,这能极大提升你的编程能力。

5.1 功能扩展方向

  1. 科学计算功能:添加sin,cos,tan,log,ln,(开方),(平方),x^y(幂运算),π,e等按钮。这需要引入Math类中的函数,并处理好角度/弧度模式切换。
  2. 历史记录:在界面侧边或底部增加一个JListJTextArea,记录每一次完整的计算表达式和结果。可以添加“清除历史”功能。
  3. 内存功能:模仿真实计算器的M+(加存)、M-(减存)、MR(读存)、MC(清存)按钮。这需要维护一个独立的内存变量。
  4. 多主题切换:提供“浅色模式”和“深色模式”,让用户可以选择不同的界面配色方案。这可以通过定义不同的颜色常量,并在切换时重新设置所有组件的颜色来实现。
  5. 表达式显示:在显示当前结果的上方,用较小的字体显示正在进行的完整表达式(如“3 + 4 *”),提供更好的上下文。

5.2 架构优化方向

  1. MVC模式重构:将项目严格分为模型(Model,即CalculatorLogic)、视图(View,即CalculatorFrame)和控制器(Controller,即事件处理逻辑)。这使代码更清晰,易于测试和维护。例如,模型层完全独立,可以单独进行单元测试。
  2. 使用观察者模式:让显示组件(View)作为观察者,监听模型(Model)状态的变化。当模型中的计算结果改变时,自动通知所有观察者更新显示,而不是在控制器里手动调用display.setText()。这进一步解耦了视图和模型。
  3. 命令模式:将每一个计算操作(如数字输入、运算符执行、清空)封装成一个独立的命令对象。这样可以轻松实现“撤销/重做”功能,这也是一个非常经典的练习。

5.3 打包与部署

最后,你可以将项目打包成一个可执行的JAR文件,方便分享或使用。

  1. 在IDE(如Eclipse或IntelliJ IDEA)中,选择“Export” -> “Runnable JAR file”。
  2. 指定主类(AdvancedCalculator)。
  3. 选择将依赖库打包进JAR或提取到外部文件夹。
  4. 生成JAR文件后,在命令行中可以通过java -jar YourCalculator.jar来运行它。

你还可以尝试使用Launch4jJPackage(JDK 14+)等工具,将JAR文件打包成Windows的.exe或macOS的.app原生应用,用户体验会更上一层楼。

从构建一个简单的图形窗口,到实现复杂的交互逻辑和状态管理,再到考虑用户体验和代码架构,这个“高级计算器”项目就像一把钥匙,帮你打开了Java桌面应用开发的大门。过程中遇到的每一个问题,比如事件队列阻塞、线程安全(Swing要求所有UI更新在事件分发线程EDT中进行)、数值精度处理,都是宝贵的实战经验。当你看到自己亲手打造的计算器能够稳定流畅地运行时,那种成就感就是学习编程最好的动力。

← 返回列表