本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目基于Java Swing库开发了一个功能完整的简易计算器应用,支持加、减、乘、除、小数、负数及括号运算,涵盖用户输入处理、表达式解析与数学计算逻辑。通过JFrame、JButton等Swing组件构建图形界面,结合ActionListener实现事件响应,并引入MVC设计思想进行结构分离。项目还包含异常处理、布局管理与用户交互优化,帮助开发者掌握GUI编程核心技能,为后续开发复杂桌面应用打下坚实基础。

Java Swing计算器开发全栈指南:从界面构建到MVC架构实战

你有没有试过打开一个老旧的Java计算器,点下“1+2=”,结果弹出个 NullPointerException ?😅 那种感觉就像在厨房里切菜时突然发现刀是塑料做的——看着像那么回事,一用就崩。其实啊,90%的问题都出在 结构混乱 异常裸奔 上。

今天咱们不整那些花里胡哨的理论,直接带你手搓一个真正稳定、可维护、还能扩展成科学计算器的Swing应用。别担心,哪怕你是第一次接触GUI编程,这篇也能让你从“按钮点了没反应”进化到“我能给JVM写简历”的水平。🚀


我们先来拆解一个现实问题:为什么大多数初学者写的Swing程序总是 越改越烂

答案很简单:他们一开始就把所有东西塞进一个类里——窗口创建、按钮点击、计算逻辑全混在一起。这就好比让厨师兼做会计、服务员还管采购。短期看省事,长期必然失控。

所以,我们的目标很明确:
- ✅ 界面要美观(至少别让用户误以为中毒了)
- ✅ 代码要分层(谁干啥谁负责,互不干扰)
- ✅ 异常要兜底(就算用户输入 (((5+3) 也得优雅提示,而不是闪退)

好了,话不多说,现在就开始吧!

🛠️ 第一步:搭架子——Swing组件深度实操

JFrame不是随便new一下就完事了

你以为创建主窗口就是三行代码搞定?

JFrame frame = new JFrame("计算器");
frame.setSize(300, 400);
frame.setVisible(true);

错!这种写法迟早翻车。我见过太多项目因为忘了设置关闭行为,导致关掉窗口后进程还在后台狂吃内存。😤

正确的姿势应该是这样:

public class CalculatorFrame extends JFrame {
    public CalculatorFrame() {
        setTitle("超级计算器 Pro Max 💥");
        setSize(320, 500); // 多留点边距,别太挤
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 关闭即退出JVM
        setResizable(false); // 锁死大小,防止布局错乱
        setLocationRelativeTo(null); // 居中显示,强迫症福音
        initializeComponents(); // 初始化UI组件
    }
}

重点来了!这几行配置可不是可选项:
- setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) :这是生死线,必须设。
- setResizable(false) :小工具类应用强烈建议锁定尺寸,否则用户一拉伸,你的按钮全乱套。
- setLocationRelativeTo(null) :居中!居中!居中!重要的事情说三遍。

💡 小贴士:如果你打算做多窗口应用(比如主界面+设置面板),那可以用 DISPOSE_ON_CLOSE ,只释放当前窗口资源而不终止整个程序。

JButton:不只是能点的方块

按钮看似简单,但细节决定体验。默认样式的Swing按钮立体感强,但在现代扁平化设计面前显得有点土气。怎么让它看起来更顺眼?

关键在于这几个属性:

JButton createStyledButton(String text, Color bgColor) {
    JButton btn = new JButton(text);
    btn.setFont(new Font("微软雅黑", Font.BOLD, 18)); // 中文友好字体
    btn.setBackground(bgColor);
    btn.setForeground(Color.WHITE);
    btn.setFocusPainted(false);     // 去除点击后的虚线框
    btn.setBorderPainted(false);    // 去掉立体边框,实现扁平化
    btn.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); // 手型光标
    return btn;
}

你看,仅仅几行设置,按钮立马从“Windows XP风格”升级到了“iOS风”。特别是这句 btn.setFocusPainted(false); ,简直是拯救视觉污染的神器!

数字键 vs 运算符键 —— 色彩心理学的应用

聪明的设计会用颜色引导用户操作。比如:
- 数字键 :浅灰色背景 + 黑字 → 安全、中性
- 运算符键 :橙色/红色背景 + 白字 → 危险、重要操作

// 数字键
createStyledButton("7", new Color(220, 220, 220));

// 加减乘除
createStyledButton("+", new Color(255, 140, 0));

颜色不仅能提升颜值,更能降低误操作率。试想如果“清除”按钮也是灰色,新手很可能随手一点清空了辛苦输入的表达式……

JTextField:显示屏的灵魂所在

作为计算器的“脸面”,文本框不能只是个摆设。它需要满足几个硬性要求:

需求 实现方式
只读模式 setEditable(false)
数值右对齐 setHorizontalAlignment(JTextField.RIGHT)
字体等宽 使用 Courier New Consolas
初始值为0 setText("0")
private JTextField createDisplayField() {
    JTextField field = new JTextField("0");
    field.setEditable(false);
    field.setHorizontalAlignment(JTextField.RIGHT);
    field.setFont(new Font("Consolas", Font.BOLD, 24));
    field.setBackground(Color.WHITE);
    field.setForeground(Color.BLUE);
    field.setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY));
    return field;
}

注意到没有?我们用了 Consolas 这个等宽字体。这意味着每个数字宽度一致,当你输入 111 888 时,不会出现后者把前面内容挤出去的尴尬情况。


到这里,UI骨架已经搭好了。但别急着运行——现在的程序还是个“植物人”,有身体没灵魂。接下来我们要让它“活”起来。

⚡ 第二步:注入生命力——事件驱动机制详解

什么是真正的“响应式”?

很多人以为给按钮加个监听器就是交互了。但真正的交互系统应该具备以下特征:
- 用户点击 → 程序立刻反馈(如按钮变色)
- 输入非法内容 → 实时拦截而非事后报错
- 操作完成后 → 视觉状态同步更新

而这一切的核心,就是 事件监听机制

ActionListener 的两种流派之争

你会怎么处理十几个按钮的点击事件?是每个按钮单独写一个匿名内部类,还是统一交给一个处理器?

方案A:每人发一套房(独立监听器)
btn7.addActionListener(e -> display.append("7"));
btn8.addActionListener(e -> display.append("8"));
btnPlus.addActionListener(e -> display.append("+"));
// ...还有10个类似的

优点?简单明了。缺点?重复代码爆炸💥,后期维护噩梦。

方案B:集中供暖(统一监听器)
class CalculatorHandler implements ActionListener {
    private JTextField display;

    @Override
    public void actionPerformed(ActionEvent e) {
        String cmd = e.getActionCommand(); // 获取按钮文字
        switch (cmd) {
            case "C" -> display.setText("");
            case "←" -> deleteLastChar();
            case "=", "+", "-", "*", "/" -> appendWithSpace(cmd);
            default -> appendToDisplay(cmd);
        }
    }
}

这才是专业做法!所有按钮共享同一个监听器,通过 getActionCommand() 区分行为。既减少了类数量,又便于统一管理逻辑。

🔍 注意: getActionCommand() 默认返回按钮文本,但你可以手动修改:
java btnDivide.setActionCommand("/"); // 虽然按钮显示"÷",但命令设为"/"
这样就能在界面上保留美观符号,在后台使用标准运算符。

EDT线程——千万别在这里跑马拉松

Swing的所有事件都在 事件分发线程(Event Dispatch Thread, EDT) 上执行。这意味着如果你在 actionPerformed 里做个耗时操作:

@Override
public void actionPerformed(ActionEvent e) {
    Thread.sleep(5000); // 模拟复杂计算
    display.setText("完成!");
}

后果是什么?整个界面卡住5秒,用户疯狂点击却毫无反应,最后只能强制结束进程。

正确做法是:

SwingUtilities.invokeLater(() -> {
    // 更新UI必须回到EDT
    display.setText(result);
});

或者使用 SwingWorker 异步执行:

new SwingWorker<Double, Void>() {
    @Override
    protected Double doInBackground() throws Exception {
        return heavyCalculation();
    }

    @Override
    protected void done() {
        try {
            double result = get();
            display.setText(String.valueOf(result));
        } catch (Exception ex) {
            display.setText("错误");
        }
    }
}.execute();

记住一句话: 永远不要阻塞EDT 。把它当成医院急诊室——可以快速处理小伤口,但不能做心脏搭桥手术。


此时,你的计算器已经能响应点击并更新屏幕了。但离“工业级”还差一大截。为什么?因为还没解决最致命的问题—— 用户乱输怎么办?

🛡️ 第三步:穿上防弹衣——异常处理与健壮性设计

别再让ArithmeticException毁掉用户体验

想象一下这个场景:
用户输入 10 / 0 → 点击“=” → 程序崩溃 → 控制台打印堆栈跟踪

这不是软件,这是炸弹💣。

正确的处理流程应该是:

try {
    double result = evaluateExpression(display.getText());
    display.setText(formatResult(result));
} catch (ArithmeticException e) {
    showError("除零错误,请检查分母");
} catch (NumberFormatException e) {
    showError("包含非法字符,请重新输入");
} catch (SyntaxException e) {
    showError("表达式语法错误:" + e.getMessage());
} catch (Exception e) {
    log.error("未预期异常", e); // 记录日志
    showError("系统内部错误");
}

看到区别了吗?普通异常直接暴露给用户是极不负责任的行为。你应该:
1. 分类捕获 :不同错误给出不同提示
2. 友好文案 :别说“NumberFormatException”,要说“输入了非数字内容”
3. 记录日志 :方便开发者定位问题

输入预校验——把错误拦在门外

与其等出错了再补救,不如提前预防。我们可以设计一个前置检查器:

public class InputValidator {

    public static List<String> validate(String expr) {
        List<String> errors = new ArrayList<>();

        if (expr == null || expr.trim().isEmpty()) {
            errors.add("表达式不能为空");
        }

        if (expr.endsWith("+") || expr.endsWith("-") ||
            expr.endsWith("*") || expr.endsWith("/")) {
            errors.add("表达式不能以运算符结尾");
        }

        if (countUnmatchedParentheses(expr) != 0) {
            errors.add("括号未闭合");
        }

        if (expr.contains("++") || expr.contains("--") ||
            expr.contains("*/") || expr.contains("/*")) {
            errors.add("存在连续运算符");
        }

        return errors;
    }

    private static int countUnmatchedParentheses(String s) {
        int count = 0;
        for (char c : s.toCharArray()) {
            if (c == '(') count++;
            else if (c == ')') {
                if (--count < 0) return 1; // 右括号过多
            }
        }
        return count; // 返回左括号多余数量
    }
}

在用户点击“=”之前先调用这个验证器,如果有错误就弹窗提醒,根本不让非法表达式进入计算环节。

BigDecimal 来拯救浮点精度灾难

你还记得那个经典问题吗?

System.out.println(0.1 + 0.2); // 输出 0.30000000000000004 😱

金融级应用绝对不能容忍这种误差。解决方案就是用 BigDecimal 代替 double

private String formatResult(double value) {
    return new BigDecimal(value)
           .setScale(10, RoundingMode.HALF_UP)   // 保留10位小数
           .stripTrailingZeros()                 // 去掉末尾多余的0
           .toPlainString();                     // 不用科学计数法
}

这样 (0.1 + 0.2) 就能正确显示为 0.3 ,而不是让用户怀疑自己数学白学了。


至此,你的计算器已经有了坚实的地基。但要想让它真正“聪明”,还得引入现代软件工程的核心思想—— 分层架构

🏗️ 第四步:高阶修炼——MVC架构实战

为什么你需要MVC?

假设你现在接到需求:“新增历史记录功能”。如果你的代码全挤在一个类里,改起来会怎样?

  • 修改UI添加历史面板
  • 在主类里加List存储记录
  • 调整事件逻辑保存每次计算结果
  • ……

改完之后你会发现,原本清晰的代码变成了一团浆糊。

但如果用了MVC呢?

层级 职责 修改影响范围
Model 数据与业务逻辑 ✅ 新增history字段
View 界面展示 ✅ 添加新面板
Controller 协调两者 ✅ 增加保存逻辑

各司其职,互不影响。这就是架构的力量。

Model层:独立于UI的核心引擎

public class CalculatorModel {

    private StringBuilder expression = new StringBuilder();
    private boolean lastWasEquals = false;

    public void pressDigit(char digit) {
        if (lastWasEquals) clear();
        expression.append(digit);
        lastWasEquals = false;
    }

    public void pressOperator(char op) {
        if (expression.length() == 0) return;
        char last = expression.charAt(expression.length() - 1);
        if ("+-*/".indexOf(last) >= 0) {
            // 替换最后一个操作符
            expression.setCharAt(expression.length() - 1, op);
        } else {
            expression.append(' ').append(op).append(' ');
        }
        lastWasEquals = false;
    }

    public double calculate() {
        String expr = expression.toString().trim();
        if (expr.isEmpty()) throw new IllegalArgumentException("表达式为空");

        // 这里调用双栈算法解析中缀表达式
        return ExpressionParser.parse(expr);
    }

    public void clear() {
        expression.setLength(0);
        lastWasEquals = false;
    }

    // getter方法
    public String getExpression() { return expression.toString(); }
}

注意!这个类 完全不依赖任何Swing组件 。这意味着你可以把它移植到Web后端、Android甚至命令行工具中复用。

View层:纯粹的视觉呈现者

public class CalculatorView extends JPanel {

    private JTextField display;
    private ActionListener buttonListener;

    public CalculatorView(ActionListener listener) {
        this.buttonListener = listener;
        buildUI();
    }

    private void buildUI() {
        setLayout(new BorderLayout());

        // 显示屏
        display = new JTextField("0");
        display.setEditable(false);
        display.setHorizontalAlignment(SwingConstants.RIGHT);
        add(display, BorderLayout.NORTH);

        // 按钮网格
        JPanel grid = new JPanel(new GridLayout(5, 4, 5, 5));
        String[] keys = {/* 按钮标签数组 */};
        for (String key : keys) {
            JButton btn = new JButton(key);
            btn.addActionListener(buttonListener); // 统一转发给Controller
            styleButton(btn, key);
            grid.add(btn);
        }
        add(grid, BorderLayout.CENTER);
    }

    public void updateDisplay(String text) {
        display.setText(text);
    }

    public String getCurrentInput() {
        return display.getText();
    }
}

View只做两件事:渲染界面、转发事件。它不知道也不关心点击“+”之后会发生什么。

Controller层:真正的指挥官

public class CalculatorController implements ActionListener {

    private final CalculatorModel model;
    private final CalculatorView view;

    public CalculatorController(CalculatorModel model, CalculatorView view) {
        this.model = model;
        this.view = view;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        String cmd = e.getActionCommand();

        try {
            switch (cmd) {
                case "C" -> handleClear();
                case "=" -> handleEquals();
                case "←" -> handleBackspace();
                default -> handleInput(cmd);
            }
        } catch (Exception ex) {
            view.updateDisplay("❌ " + ex.getMessage());
        }
    }

    private void handleEquals() {
        try {
            double result = model.calculate();
            view.updateDisplay(format(result));
            model.clear(); // 下次输入开始新表达式
            model.setLastWasEquals(true);
        } catch (Exception e) {
            view.updateDisplay("错误: " + e.getMessage());
        }
    }

    private void handleInput(String input) {
        if (isDigit(input)) {
            model.pressDigit(input.charAt(0));
        } else if ("+-×÷".contains(input)) {
            char op = toInternalOp(input);
            model.pressOperator(op);
        }
        view.updateDisplay(model.getExpression());
    }

    private String format(double value) {
        return new BigDecimal(value)
               .setScale(8, RoundingMode.HALF_UP)
               .stripTrailingZeros()
               .toPlainString();
    }
}

Controller才是真正的“大脑”。它接收来自View的事件,告诉Model该做什么,然后把结果送回View刷新。

这种三层分离带来的好处是革命性的:
- 测试更容易:可以直接对Model单元测试,无需启动GUI
- 扩展更轻松:想加科学计算?只需替换Model实现
- 团队协作顺畅:前端专注UI美化,后端专注算法优化


🎨 第五步:画龙点睛——用户体验终极优化

主题系统:一键切换暗黑模式

现代应用怎能没有夜间模式?我们可以封装一个主题工具类:

public class ThemeUtil {

    public static void applyDarkTheme() {
        UIManager.put("Panel.background", new Color(45, 45, 45));
        UIManager.put("TextField.background", new Color(60, 60, 60));
        UIManager.put("TextField.foreground", Color.WHITE);
        UIManager.put("Button.background", new Color(80, 80, 80));
        UIManager.put("Button.foreground", Color.WHITE);
        UIManager.put("Button.font", new Font("微软雅黑", Font.PLAIN, 16));
    }

    public static void applyLightTheme() {
        UIManager.put("Panel.background", Color.WHITE);
        UIManager.put("TextField.background", Color.WHITE);
        UIManager.put("TextField.foreground", Color.BLACK);
        UIManager.put("Button.background", Color.LIGHT_GRAY);
        UIManager.put("Button.foreground", Color.BLACK);
    }
}

启动时检测系统偏好自动切换,或者让用户手动选择:

JMenu themeMenu = new JMenu("主题");
themeMenu.add(new JMenuItem("亮色")).addActionListener(e -> ThemeUtil.applyLightTheme());
themeMenu.add(new JMenuItem("暗色")).addActionListener(e -> ThemeUtil.applyDarkTheme());

键盘支持:效率党的刚需

鼠标点按太慢?那就加上键盘快捷键!

// 注册键盘动作
getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
    .put(KeyStroke.getKeyStroke(KeyEvent.VK_0, 0), "digit0");
getRootPane().getActionMap().put("digit0", new AbstractAction() {
    @Override
    public void actionPerformed(ActionEvent e) {
        simulateButtonClick("0");
    }
});

// 更简单的全局监听
addKeyListener(new KeyAdapter() {
    @Override
    public void keyPressed(KeyEvent e) {
        switch (e.getKeyCode()) {
            case KeyEvent.VK_ESCAPE -> controller.actionPerformed(new ActionEvent(this, 0, "C"));
            case KeyEvent.VK_ENTER, KeyEvent.VK_EQUALS -> controller.actionPerformed(new ActionEvent(this, 0, "="));
            case KeyEvent.VK_BACK_SPACE -> controller.actionPerformed(new ActionEvent(this, 0, "←"));
        }
    }
});
requestFocusInWindow(); // 确保能接收到键盘事件

从此用户可以用键盘完成全部操作,效率翻倍。

动态布局:适配不同屏幕

虽然我们锁定了不可缩放,但初始尺寸仍需考虑分辨率差异:

Toolkit tk = Toolkit.getDefaultToolkit();
Dimension screenSize = tk.getScreenSize();
int width = (int)(screenSize.width * 0.3);
int height = (int)(screenSize.height * 0.5);
setSize(width, height);

或者根据DPI动态调整字体大小,确保在高清屏上也不会显得太小。


🔮 最终展望:从小计算器到大系统的跃迁

你现在拥有的不再是一个玩具程序,而是一个具备完整工程结构的模板。它可以轻松演进为:

科学计算器

增加三角函数、对数、幂运算等功能:

ExpressionBuilder("sin(pi/2) + log(100)").build().evaluate();

表达式求值器

集成 exp4j 库,支持变量和自定义函数:

<dependency>
    <groupId>net.objecthunter</groupId>
    <artifactId>exp4j</artifactId>
    <version>0.4.8</version>
</dependency>

跨平台桌面应用

将来迁移到JavaFX甚至Electron都变得可行,因为核心逻辑已经解耦。


最后送大家一句忠告: 不要为了炫技而写复杂代码,但一定要为未来留好扩展接口

就像这个计算器,今天我们花了80%的时间在“看不见的地方”——架构、异常、分层。但它换来的是未来新增功能时的游刃有余。

下次当你打开IDE准备敲代码时,先问自己三个问题:
1. 这个功能将来会不会变?
2. 别人能不能看懂我的代码?
3. 如果用户乱操作,程序会不会当场去世?

答好了这三个问题,你写的就不再是“能跑就行”的脚本,而是真正的工业级软件。💪

🎉 完整源码已整理成GitHub仓库: github.com/javacalc/swing-calculator
欢迎star & fork,一起打造最强Java计算器!

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目基于Java Swing库开发了一个功能完整的简易计算器应用,支持加、减、乘、除、小数、负数及括号运算,涵盖用户输入处理、表达式解析与数学计算逻辑。通过JFrame、JButton等Swing组件构建图形界面,结合ActionListener实现事件响应,并引入MVC设计思想进行结构分离。项目还包含异常处理、布局管理与用户交互优化,帮助开发者掌握GUI编程核心技能,为后续开发复杂桌面应用打下坚实基础。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐