news 2026/10/15 2:26:03

Qt6计算器开发全解析:信号槽、表达式解析与工程实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt6计算器开发全解析:信号槽、表达式解析与工程实战

计算器大概是所有 Qt 初学者都绕不开的入门项目。不管去逛社区还是翻教程,第一个完整范例十有八九都是它。但说句实在话,我在很多交流群里见过一种很普遍的情况:照着示例敲完代码,界面出来了,按钮也能点了,结果一问“信号槽为什么要这么连”“布局为什么用网格不用水平加垂直”就立刻卡壳。这篇文章就围绕这个非常经典的 Qt 入门练习项目——计算器,把我自己反复写过很多遍的完整链路拆开讲一遍,包括项目选型、界面搭建、按钮布局、表达式解析、异常处理,以及最后怎么打包发布。内容基于 Qt6 + C++,没有高深技巧,适合刚学完 Qt 基础控件的读者,也适合想快速回炉找手感的老手。

1. 项目选型与整体设计思路

1.1 为什么选 QWidget 而不是 QML

很多第一次接触 Qt 的人会在 QWidget 和 QML 之间纠结。QML 现在宣传力度很大,界面写起来也漂亮,做动画和触摸交互特别顺手,但在“入门”这个阶段,我强烈建议先用 QWidget 而不是 QML。

计算器这种项目,核心任务是帮你把“界面控件”和“业务逻辑”之间的连接方式搞清楚。QWidget 体系里,按钮是按钮、文本框是文本框,信号槽的关系非常直白:你点一下按钮,clicked 信号发出来,槽函数被调用。这个流程能帮你建立对事件驱动模型最朴素也最牢固的理解。而 QML 里虽然也讲信号槽,但涉及 QML 与 C++ 交互时,会多出 context、注册类型、钦定属性等概念,对新手来说负担太重。我见过太多跳过 QWidget 直接学 QML 的人,最后连 connect 的几种写法都分不清楚,因为 QML 里的信号连接语法和 C++ 里并不完全一样。

另外还要考虑项目的复用场景。计算器做完以后,如果你想把它嵌到一个已有的桌面工具里,QWidget 是 Qt 的传统界面体系,和文件对话框、菜单栏、状态栏这类东西配合非常顺。用 QML 当然也能做桌面界面,但你在 Qt Creator 里创建一个“Qt Widgets Application”工程,项目结构会简单很多:一个 main.cpp,一个窗口类,加几个控件文件,逻辑非常清晰,不用装额外的 QML 运行时概念。对于入门,少就是多。

1.2 功能清单:先明确“不做什么”

做练习项目最容易犯的毛病就是什么都想加。看到别人做的计算器有科学计算、历史记录、皮肤切换,自己也想一步到位,结果代码越写越乱,最后连基本四则运算都出了 bug。我给自己也给别人很多次建议:入门计算器,第一版只做最核心的事。

我这版的功能清单是这样定的:

  • 数字键 0~9、小数点、00,支持连续输入多位数字
  • 四则运算 +、-、、/,支持中缀表达式连续计算,比如 1+23-4/2
  • 支持括号,能处理 (1+2)*(3+4) 这类组合
  • 清除键 C、退格键,方便改错
  • 等号直接算出结果,除零给提示
  • 对空表达式、非法表达式做统一拦截,不给程序崩溃的机会

刻意不做:负数直接输入、百分号运算、连等模式、历史记录、主题切换、键盘输入。这些不是没价值,而是放到项目跑通之后再慢慢扩展。理由很简单,计算器入门项目真正的学习目标是信号槽和布局,不是表达式解析器。表达式解析做到能处理“数字 + 四则运算法则 + 括号”就已经够练手了。如果一开始就想着支持负数、支持先按符号再输数字、支持崩溃恢复,你的注意力就会被带偏,反而不利于建立核心概念。

1.3 环境版本与工程骨架

代码基于 Qt6 编写,同时也兼容 Qt5.15。6.x 是目前的主流版本,新装环境直接选 Qt6 就行。编译器方面,Windows 上我习惯用 MinGW 64-bit,省事,不用额外配调试器路径。如果你用 MSVC,注意要装对应版本的调试器,否则 Qt Creator 里按 F5 可能提示找不到 debugger。工程创建时选择 Qt Widgets Application,基类选 QWidget,不勾选 QML。工程名就叫 Calculator,生成后你会看到一个最基本的窗口模板。

C++ 标准建议选择 C++17。Qt6 默认就会启用比较新的标准,但如果你是从 Qt5 旧工程复制过来的,一定检查一下 .pro 文件里的 CONFIG,别让编译器用 C++11 跑。lambda 表达式在信号槽里用得很频繁,C++17 在捕获、类型推导方面体验更好,也能避免一些莫名其妙的警告。

工程里主要就四个文件:calculator.h、calculator.cpp、main.cpp,还有一个 .pro 或 CMakeLists.txt。我下面的代码都用 .pro 工程说明,因为 Qt Creator 新建向导生成 .pro 最直接。

2. 界面搭建的实操细节

2.1 显示栏:从一行文本框说起

计算器的“显示器”我用一个只读的 QLineEdit 充当,它看起来像个计算器显示屏,用起来又比 QLabel 更合适:有默认边框、能设置右对齐、可以控制高度。创建代码很简单:

m_display = new QLineEdit("0", this); m_display->setReadOnly(true); m_display->setAlignment(Qt::AlignRight); m_display->setFixedHeight(64); m_display->setStyleSheet("font-size: 24px; font-weight: bold;");

初始文本设成“0”,是因为真实计算器开机时屏幕中间都有一个 0。不过这个“0”是个坑,我后面专门会讲到怎么处理输入时避免出现“09”“08”这种滑稽结果。只读模式是为了禁止用户直接从键盘往里面输内容,输入只能通过按钮,这样表达式的构建过程就是可控的。

QLineEdit 默认会有一个边框,计算器风格不需要太花哨,我在样式表里简单地设置了大号字体和右内边距。字体加粗会让数字更好认。你如果打算做成深色主题,也可以在这个样式表里直接改背景和文字颜色,不用改 C++ 代码。

2.2 按钮网格的布局规划

整个界面的核心布局是 QGridLayout。“为什么不用 QHBoxLayout 和 QVBoxLayout 拼一个键盘区出来?”这可能是我被问得最多的问题。答案很简单:网格布局天然适合计算器这种二维键盘,它能自动保证同一行的按钮高度一致、同一列的按钮宽度一致,并且调整窗口大小时所有按钮按相同的规则伸缩。你用一堆水平布局和垂直布局叠加,也能凑出样子,但按钮的尺寸关系要自己一点点调,窗口一变大小就乱作一团。

我的按钮排布是六行四列:

行第 1 列第 2 列第 3 列第 4 列
0显示屏(跨 4 列)
1C退格()
2789/
3456*
4123-
50.00+
6= (跨 4 列)

去掉了百分号按钮,换成“00”,这样既保留了十六个键位的整齐布局,又不会让表达式解析器去处理一个语义复杂、容易让人纠结的功能。括号放在第一行,方便输入复杂表达式。

把等号按钮单独放一行并且跨四列,是为了让它看起来像真实计算器上的大按键,也让主键盘区下面的空间更透气。网格布局里,跨行跨列用 addWidget 的第五、第六个参数控制:

m_grid->addWidget(equalBtn, 6, 0, 1, 4);

这行的意思是:从第 6 行第 0 列开始,占 1 行、4 列。

按钮创建我基本都用循环,而不是一个按钮一个按钮地手动 new。像数字键“7、8、9、4、5、6…”这类规律性很强的控件,写循环是最省事的方式,后期想调整顺序,只需要改那个 QStringList 里的顺序就行,不用去改十几行重复代码。

QStringList keys = {"7", "8", "9", "/", "4", "5", "6", "*", "1", "2", "3", "-", "0", ".", "00", "+"}; for (int i = 0; i < keys.size(); ++i) { QString text = keys[i]; auto *btn = new QPushButton(text, this); btn->setMinimumSize(64, 44); m_grid->addWidget(btn, 2 + i / 4, i % 4); // 数字和点走 appendText,运算符走 appendOperator bool isDigit = (text.at(0).isDigit() || text == "."); if (isDigit) { connect(btn, &QPushButton::clicked, this, [this, text]() { appendText(text); }); } else { connect(btn, &QPushButton::clicked, this, [this, text]() { appendOperator(text); }); } }

这里特别要注意,lambda 捕获的一定是当前循环里拷贝出来的局部 QString text,不能直接捕获循环变量或者 QList 的引用。循环结束之后,循环里的临时状态都不在了,你捕获到的值可能已经变成最后一个元素。我早期踩过一次这种坑,按钮全部连到同一个字符串上,点任何一个数字输入的都是最后一个。所以每个按钮单独声明一个局部变量,按值捕获到 lambda 里,这是 Qt 信号槽里最常见也最容易忽略的细节。

2.3 尺寸、字体与样式微调

QGridLayout 默认会根据控件的大小提示来排布,但按钮文字长短不一致,最后出来的按钮尺寸会千奇百怪。解决办法是给每个按钮都设置 minimumSize。我统一设成 64×44,这样在 360 宽的窗口里,四列按钮加间距刚好布满。等号按钮单独设了最小高度 48,让它比普通按钮高一点。

窗口本身的初始大小我用 resize(360, 420),同时设置 setMinimumSize(320, 400),防止用户把窗口缩到不可用的程度。如果你只设置了 resize 而不设最小尺寸,用户可能会使劲缩小窗口,按钮被压扁或者显示栏只剩一行,界面就很难看。这也是为什么“布局被压碎”会成为计算器练习里最常出现的截图求助原因之一。

给窗口起名我用 setWindowTitle("Calculator")。这部分完全可以根据使用习惯改成中文标题,没任何技术门槛。样式上,我建议新手不要在入门阶段把时间耗在 QSS 美化上,先用默认样式把功能跑通,之后再尝试改颜色、圆角、按键间距等细节。凡是影响功能调试的因素,都尽量在后面做。

3. 核心逻辑:信号槽与表达式处理

3.1 信号槽连接:按钮是消息源,不是业务入口

界面搭好之后,真正让按钮“活”起来的是信号槽。很多刚接触 Qt 的人会把按钮的槽函数当成普通函数,直接在里面写算式逻辑,这其实没抓住信号槽的核心。信号槽的本质是“消息的发布与订阅”:按钮负责发 clicked 信号,窗口类负责订阅这个信号并执行对应的槽函数。业务处理逻辑全写在窗口类里,按钮只负责通知你“我被点了”。

连接方式我统一用 Qt 新语法:

connect(btn, &QPushButton::clicked, this, &Calculator::onClear); connect(btn, &QPushButton::clicked, this, [this, text]() { appendText(text); });

第一种适合直接绑定一个现有槽函数,比如 C 按钮绑定 onClear。第二种用 lambda 表达式,适合需要传参数的场景。数字按钮需要知道自己的文本是什么,运算符需要知道自己是加还是减,lambda 里按值捕获文本字符串是最自然的方式。

我见过有些人用 QSignalMapper 处理这类按钮带参数的问题,也见过用 sender() 再类型转换取文本的写法。这两种方式不是不行,但对于计算器这个场景来说,都是绕远路。lambda 捕获变量是干净利落的选择,代码可读性也高。不过要注意,捕获列表里捕获 this 是必须的,因为你要调用成员函数;捕获 text 按值,防止引用失效。

3.2 表达式校验与防呆设计

计算器要想稳定运行,最重要的不是算术能力,而是“扛住用户瞎点”。用户会在各种不合理的时机按下各种按钮:开头按乘号,连续按两个加号,括号不闭合,末尾按除号,一个数字里点两个小数点。程序必须在这些情况下给出合理的反应,而不是崩溃。

我设计了一个 isExpressionValid 函数,做四层校验:

  • 空表达式直接拒绝
  • 用正则限制字符串只允许数字、小数点、四则运算符和括号
  • 用正则检查不能出现连续两个运算符
  • 检查括号是否配对,以及表达式结尾不能是运算符

正则这部分我写的是:

QRegularExpression re("^[0-9.\\+\\-\\*/()]+$"); if (!re.match(expr).hasMatch()) { return false; } QRegularExpression doubleOp("[\\+\\-\\*/]{2,}"); if (expr.contains(doubleOp)) { return false; }

第一段正则如果匹配不上,说明表达式里有非法字符。第二段连续运算符的限制处理了“1++2”“3*-4”这类情况。有人会问,负数的场景怎么办?这版计算器我刻意不支持直接输入负数,想表达 -5 就必须输入 0-5,想算 3×(-2) 就必须写成 3×(0-2)。这样虽然不亮眼,但能让新手聚焦在信号槽和基本解析上,不会一上来就和一元负号、运算优先级纠缠。等到项目扩展阶段,再通过“在表达式最前面和左括号后面补 0”的办法支持负数输入,这样难度曲线更平滑。

括号配对的校验也很重要:

int balance = 0; for (const QChar &ch : expr) { if (ch == '(') balance++; else if (ch == ')') balance--; if (balance < 0) return false; } if (balance != 0) return false;

这个逻辑可以类比成“借东西要还”:遇到一个左括号就记笔账,遇到右括号就还一笔,右括号比左括号先出现就直接判非法,最后账没还完也不合法。表达式校验不做这个检查的话,按下等号时括号不闭合,后面的求值器就会面对一个永远无法正确配对的表达式,非常容易出边界错误。

3.3 中缀转后缀的求值实现

表达式校验通过之后,真正计算的部分我用的是中缀转后缀再求值的思路。我们不直接递归扫描“+ 和 * 谁优先”,而是先把人类习惯的中缀表达式转换成计算机更容易计算的后缀表达式,然后用一个栈进行求值。这个算法是经典数据结构知识,代码量不大,Qt 的 QStack 直接就能用。

核心求值函数大致长这样:

#include <QStack> #include <cmath> double Calculator::evaluate(const QString &expr) { QStringList tokens; QString buffer; for (int i = 0; i < expr.size(); ++i) { QChar ch = expr.at(i); if (ch.isDigit() || ch == '.') { buffer += ch; } else { if (!buffer.isEmpty()) { tokens << buffer; buffer.clear(); } tokens << QString(ch); } } if (!buffer.isEmpty()) tokens << buffer; QStack<double> values; QStack<QString> ops; auto applyOnce = [&values, &ops]() { double rhs = values.pop(); double lhs = values.pop(); QString op = ops.pop(); if (op == "+") values.push(lhs + rhs); else if (op == "-") values.push(lhs - rhs); else if (op == "*") values.push(lhs * rhs); else if (op == "/") { if (std::abs(rhs) < 1e-9) { throw std::runtime_error("divide by zero"); } values.push(lhs / rhs); } }; for (const QString &token : tokens) { bool ok = false; double num = token.toDouble(&ok); if (ok) { values.push(num); } else if (token == "(") { ops.push(token); } else if (token == ")") { while (!ops.isEmpty() && ops.top() != "(") { applyOnce(); } if (!ops.isEmpty() && ops.top() == "(") { ops.pop(); } } else { while (!ops.isEmpty() && ops.top() != "(" && (token == "+" || token == "-" || ops.top() == "*" || ops.top() == "/")) { applyOnce(); } ops.push(token); } } while (!ops.isEmpty()) { applyOnce(); } return values.isEmpty() ? 0.0 : values.top(); }

这段代码里最核心的判断是那个 while 条件。它的意思是:当前新来的运算符如果优先级不高于栈顶运算符,就先让栈顶的运算符“算掉”。比如表达式“1+2×3”,扫到乘号时栈里是“+”,乘号压栈不弹;扫到加号时栈里是“× +”,因为新加号优先级不高于乘号,所以先把乘号弹出来算一次,再把栈里的加号也弹出来算一次,最后得到正确顺序:先乘后加。优先级处理就是靠这个条件实现的,不能凭感觉写。

除零检测放在了 applyOnce 里:右操作数绝对值接近 0 时,抛一个 std::runtime_error。这样比在字符串里查找“/0”靠谱得多,“10/0.5”这种表达式里虽然包含“/0”,但并不是除零,只有真正执行到除法这一步才知道结果是不是零。

3.4 结果格式与“0”占位问题

算完的结果是 double,直接转字符串会有两个麻烦:一是大数变成科学计数法显示,二是整数变成“8.00000000”这种丑陋的形式。我做了一个格式化函数:

QString Calculator::formatResult(double value) { if (std::isfinite(value) && std::abs(value - std::round(value)) < 1e-9) { return QString::number(static_cast<long long>(value)); } return QString::number(value, 'g', 10); }

逻辑很好理解:如果这个值在误差范围内接近整数,就直接转成 long long 输出,避免“1+2=3.00000000”;如果不是整数,用 'g' 格式保留 10 位有效数字输出。这么做不是吹毛求疵,计算器结果显示直接决定用户体感,也影响后续表达式拼接的合法性。如果显示还是 “3.00000000”,用户再按一次“+5”,表达式会变成“3.00000000+5”,虽然能算,但屏幕文本又长又丑。

“0”占位问题在前面提过。窗口初始化后表达式是空的,但显示框文本是“0”。如果不做处理,用户按数字 1 时字符串会变成“01”,再按 2 就变成“012”,计算结果也跟着乱。我在 appendText 里加了判断:

if (m_display->text() == "0" && (text.at(0).isDigit() || text == "00")) { m_expression.clear(); } m_expression.append(text); m_display->setText(m_expression);

当且仅当屏幕上显示的还是初始占位 0、并且用户按的是数字时,先把表达式清空,再追加新输入。这样“09”问题就消失了。如果你还希望小数点也能清掉占位 0,把判断条件里加上 text == ".",用户按小数点时从空表达式开始构建,输入“.5”的话,在表达式校验那里会被拦下来,所以更稳妥的做法还是要求用户先输 0 再输小数点。

4. 完整代码落地

4.1 头文件与类定义

整个计算器封装成一个 Calculator 类,继承 QWidget。头文件里声明了界面成员、核心槽函数和工具函数,完整内容如下:

#ifndef CALCULATOR_H #define CALCULATOR_H #include <QWidget> class QLineEdit; class QGridLayout; class Calculator : public QWidget { Q_OBJECT public: explicit Calculator(QWidget *parent = nullptr); private slots: void appendText(const QString &text); void appendOperator(const QString &op); void onClear(); void onBackspace(); void onEqual(); private: bool isExpressionValid(const QString &expr); double evaluate(const QString &expr); QString formatResult(double value); QLineEdit *m_display; QGridLayout *m_grid; QString m_expression; bool m_justEvaluated = false; bool m_hasError = false; }; #endif // CALCULATOR_H

我保留了 private slots 的写法,虽然在新版 Qt 里直接连 private 普通成员函数也行,但这种写法对初学者更直白,一看就知道这是处理信号的函数。类里有两个状态位:m_justEvaluated 表示“刚按过等号,当前显示的是结果”,m_hasError 表示“上次操作产生了错误提示”,它们会让后续输入更贴近真实计算器的习惯。

4.2 源文件实现

源文件里依次是构造函数、各槽函数、校验函数和求值函数。构造函数负责搭好界面和连接信号。我把数字键、运算符键的连接都写在循环里,特殊功能键单独连。

#include "calculator.h" #include <QLineEdit> #include <QGridLayout> #include <QPushButton> #include <QRegularExpression> #include <QStack> #include <cmath> #include <stdexcept> Calculator::Calculator(QWidget *parent) : QWidget(parent) { setWindowTitle("Calculator"); setMinimumSize(320, 400); resize(360, 420); m_display = new QLineEdit("0", this); m_display->setReadOnly(true); m_display->setAlignment(Qt::AlignRight); m_display->setFixedHeight(64); m_display->setStyleSheet("font-size: 24px; font-weight: bold;"); m_grid = new QGridLayout(this); m_grid->setSpacing(6); m_grid->addWidget(m_display, 0, 0, 1, 4); QStringList topButtons = {"C", "退格", "(", ")"}; for (int i = 0; i < topButtons.size(); ++i) { QString text = topButtons[i]; auto *btn = new QPushButton(text, this); btn->setMinimumHeight(44); m_grid->addWidget(btn, 1, i); if (text == "C") { connect(btn, &QPushButton::clicked, this, &Calculator::onClear); } else if (text == "退格") { connect(btn, &QPushButton::clicked, this, &Calculator::onBackspace); } else { connect(btn, &QPushButton::clicked, this, [this, text]() { appendText(text); }); } } QStringList keys = {"7", "8", "9", "/", "4", "5", "6", "*", "1", "2", "3", "-", "0", ".", "00", "+"}; for (int i = 0; i < keys.size(); ++i) { QString text = keys[i]; auto *btn = new QPushButton(text, this); btn->setMinimumSize(64, 44); m_grid->addWidget(btn, 2 + i / 4, i % 4); bool isDigit = (text.at(0).isDigit() || text == "."); if (isDigit) { connect(btn, &QPushButton::clicked, this, [this, text]() { appendText(text); }); } else { connect(btn, &QPushButton::clicked, this, [this, text]() { appendOperator(text); }); } } auto *equalBtn = new QPushButton("=", this); equalBtn->setMinimumHeight(48); m_grid->addWidget(equalBtn, 6, 0, 1, 4); connect(equalBtn, &QPushButton::clicked, this, &Calculator::onEqual); }

appendText 处理数字、小数点、括号和 00,同时在表达式过长时直接拒绝继续追加,防止一条表达式被按到上千字符导致界面和解析器都受不了:

void Calculator::appendText(const QString &text) { if (m_hasError) { m_expression.clear(); m_hasError = false; } if (m_justEvaluated) { if (text.at(0).isDigit() || text == "." || text == "00") { m_expression.clear(); m_justEvaluated = false; } } if (m_expression.length() >= 64) { return; } if (m_display->text() == "0" && (text.at(0).isDigit() || text == "00")) { m_expression.clear(); } m_expression.append(text); m_display->setText(m_expression); }

appendOperator 有两个细节:一是在表达式空了的时候不响应运算符;二是如果表达式末尾已经是运算符,后面的运算符直接把前面的替换掉。这个设计模拟了真实计算器的行为,连按“+”“-”不会出现“1+-”这种非法字符串,而是自动用后按的运算符替换先按的:

void Calculator::appendOperator(const QString &op) { if (m_hasError) { m_expression.clear(); m_hasError = false; } if (m_expression.isEmpty()) { return; } QString last = m_expression.right(1); if (last == "+" || last == "-" || last == "*" || last == "/" || last == ".") { m_expression.chop(1); } m_expression.append(op); m_display->setText(m_expression); }

退格和清除的实现很简单,都是直接操作表达式字符串:

void Calculator::onBackspace() { if (m_hasError) { m_expression.clear(); m_hasError = false; m_display->setText("0"); return; } if (!m_expression.isEmpty()) { m_expression.chop(1); } if (m_expression.isEmpty()) { m_display->setText("0"); } else { m_display->setText(m_expression); } } void Calculator::onClear() { m_expression.clear(); m_display->setText("0"); m_justEvaluated = false; m_hasError = false; }

等号槽函数是核心入口:

void Calculator::onEqual() { if (m_expression.isEmpty()) { return; } if (!isExpressionValid(m_expression)) { m_display->setText("表达式有误"); m_hasError = true; return; } try { double result = evaluate(m_expression); QString formatted = formatResult(result); m_expression = formatted; m_display->setText(formatted); m_justEvaluated = true; } catch (const std::runtime_error &) { m_display->setText("除数不能为 0"); m_hasError = true; } }

这段逻辑把“校验、计算、格式化、状态维护”分成四步。只要校验不通过,显示“表达式有误”;一旦除零异常被捕获,显示对应的提示而不是崩溃。这里异常处理是非常值得学习的点:C++ 传统代码里除零可能直接让程序崩溃,但 Qt 开发里你有很多机会用异常传递错误,只要能保证槽函数里异常不逃逸到 Qt 事件循环,就能安全处理。

4.3 main.cpp 入口

#include <QApplication> #include "calculator.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); Calculator calc; calc.show(); return app.exec(); }

.pro 工程配置:

QT += widgets CONFIG += c++17 TARGET = Calculator SOURCES += main.cpp calculator.cpp HEADERS += calculator.h

用 Qt Creator 打开 .pro 文件,编译运行就能看到一个 360×420 的计算器窗口。按下 7×8=,显示栏会正确出现 56。按下 10/0=,会出现“除数不能为 0”,而不是程序直接崩溃。

5. 常见问题与排查速查

5.1 高频率踩坑 TOP5

这个计算器项目看着简单,可实际运行的坑比想象中多。我整理了出现频率最高的五类问题,基本覆盖新手会在群里问的十有八九。

第一,按钮大小不均匀或窗口被压缩。截图里经常看到计算器按钮一行宽一行窄,或者窗口缩到只剩一条。原因多半是只设置了 resize 没设置 setMinimumSize,或者按钮没有统一的最小尺寸。检查一下两个点:QGridLayout 里的按钮是否都调用了 setMinimumSize;窗口是否设置了 setMinimumSize。布局系统在控件没有显式尺寸时会根据内容伸缩,按钮文字不同自然尺寸不同,统一 minimumSize 是治本的方法。

第二,点击任何一个数字按钮,输入的都是同一个字符。这个就是典型的信号槽 lambda 捕获问题。循环里直接捕获循环变量、再用 QList::at 或者引用形式去拿当前值,lambda 在真正执行时捕获到的已经是循环结束后的状态。修复方式我在前面代码里已经写了:循环体内先拷贝一个局部变量,lambda 按值捕获这个局部变量。

第三,按下等号程序崩溃。崩溃原因大多是表达式解析时遇到预料之外的字符串。最常见的是除零、括号不配对、连续运算符、表达式末尾是运算符这四类。任何一类都能让求值函数里的栈操作出问题。解决思路很简单:在调用 evaluate 之前一定要走 isExpressionValid,把非法情况全拦住;除零检测放在求值内部的除法分支,通过异常抛出,确保不会出现 std::abs 除数为零还硬除的情况。

第四,开机显示“0”,但按数字后出现“09”“00”之类的结果。这个我已经在前文解释过了,是占位符“0”没有在用户输入数字前被清空导致的。用 m_display->text() == "0" 判断,并且只在按数字时清空表达式即可。

第五,显示结果不是整数时出现长长的小数尾巴。比如“10/4”可能显示“2.5”,但“0.1+0.2”会显示“0.30000000000000004”。这是浮点数的经典表现,任何涉及 double 的计算器都逃不过。用格式化函数在输出前做一次取舍,整数就输出整数,非整数用 'g' 格式保留 10 位有效数字。至少用户不会看到一长串不受控的尾数。

5.2 Qt5 / Qt6 差异对照

如果你抄的代码来自老教程,或者公司旧项目用的是 Qt5,迁移时会撞上几个差异点。这里整理一张对照表:

项目Qt5 旧写法Qt6 推荐写法
正则表达式QRegExpQRegularExpression
正则匹配regexp.exactMatchre.match(...).hasMatch()
字符串编码需要 QTextCodec 处理QString 默认 UTF-8,直接写中文即可
高 DPI手动设置高分辨率缩放Qt6 默认支持
信号重载QOverload ::ofQOverload ::of 或直接函数指针

最常遇到的就是 QRegExp 被移除。Qt5 老代码里写着 QRegExp rx("\d+"),到 Qt6 直接编译不过,必须改成 QRegularExpression。我自己的经验是,正则语法基本可以照搬,只是头文件和匹配接口变化。QRegularExpression 里用 match 函数,如果要判断整个表达式合法,正则表达式必须写 ^ 和 $ 锚定符。

另一个 Qt5/Qt6 体验上的差别是中文显示。Qt5 在 Windows 上如果源码文件不是 UTF-8 编码,中文按钮文字很容易乱码。Qt6 全面转向 UTF-8,我在代码里直接用“退格”“除数不能为 0”这样的中文,编译运行都没问题。新项目建议直接用 Qt6,别在编码老问题里耗时间。

5.3 异常处理与程序稳定性

计算器看起来简单,但它是窗口程序,不像命令行程序崩溃了还能看到完整堆栈。Qt 的事件循环里,槽函数一旦抛出未捕获异常,程序往往直接终止,没有挽回余地。所以我的原则是:所有槽函数都不允许异常逃出函数体。

evaluate 内部的除零用 throw std::runtime_error 抛出,onEqual 里用 try/catch 包住整个计算过程。这样无论用户怎么瞎按,“除数不能为 0”只会出现在显示框里,程序照常运行。除捕获异常外,还要防止表达式过长导致的问题。我在 appendText 里限制了表达式最大长度 64,虽然用户按到 64 个字符时会发现后续输入无响应,但这比无限制输入把解析器和显示区都搞垮要强得多。

6. 打包发布与后续扩展

6.1 生成独立可执行文件

很多人在开发环境里程序跑得好好的,双击 exe 却发现报错找不到 Qt 的 dll。这是因为开发版链接的是 Qt 库的动态版本,发布时需要把依赖的库一起带上。Qt 官方提供了部署工具,Windows 上叫 windeployqt。

操作流程很简单:先用 Release 模式编译一次,找到生成目录下的 Calculator.exe,然后在命令行进入该目录,运行 windeployqt Calculator.exe。工具会自动复制 Qt 运行库、平台插件、样式插件等必要文件到 exe 同目录。完成后,把这个目录整个压缩打包,发给没装 Qt 的电脑也能直接运行。

实测里需要注意两点:一是必须用 Release 模式编译,Debug 模式带调试信息,windeployqt 虽然也会复制文件,但那个 exe 体积和启动速度都不适合分发;二是不要手动去把整个 Qt 安装目录拷过来,那会造成大量冗余文件。windeployqt 只挑运行必要的部分,目录清爽得多。

6.2 从计算器走向更复杂的项目

计算器做完之后,其实才是真正的开始。我建议按下面这个顺序去扩展它:

  • 支持键盘输入。重写 keyPressEvent,把数字键、运算符键、回车键映射到对应槽函数
  • 支持负数输入。修改表达式解析规则,在表达式开头或左括号后面遇到负号时自动补一个 0
  • 加历史记录。用一个 QList 存储最近算过的表达式,界面右侧或下方显示可点击的历史项
  • 做一个深色主题。把样式表集中到一个独立的 QSS 文件里,运行时动态切换
  • 用相对布局替代固定尺寸,支持窗口自由缩放时按钮跟着合理变化

负数是很多人在我这版计算器里会卡住的扩展点。举个很实际的例子:想输入“-3+5”,如果不处理,程序会判非法。扩展思路是,在构建表达式阶段,如果用户在表达式结尾输入“-”,计算结果时统一按“前一数字 + 负数”处理,或者更简单粗暴地在开头、左括号后把负号替换成“(0-”的形式等。但这个扩展需要重新设计表达式校验函数和求值器,正好可以用来复习一遍本章讲过的两个核心函数。

键盘输入其实是最能验证信号槽理解程度的功能。因为键盘事件处理不是依赖 clicked 信号,而是依赖 QWidget 的 keyPressEvent。你会意识到原先按钮和槽的连接只是一种事件传递方式,键盘事件又是另一条传递路径,两者最终都汇聚到同一个 appendText / appendOperator 槽函数,这就体现了“界面与业务分离”的思想。

我自己做这个计算器的过程中,最大的体会是“做出来”和“讲清楚为什么”完全是两回事。你可以照着代码抄一遍,十分钟界面就出来了;但如果你能手写网格布局的坐标、说清楚 lambda 捕获的语义、解释表达式校验每个正则的意图,那才算真的跨过 Qt 入门的门槛。这个项目我建议你至少做两次:第一次照着自己的理解写,第二次故意加功能、故意制造 bug,用调试器观察程序在事件循环里怎么一步步走到槽函数。带着这种“拆开再装回去”的心态去看 Qt,你会发现它远远没有传言中那么难。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 2:23:31

Superpowers实战指南:浏览器中的TypeScript多人协作游戏开发

想要安装 Superpowers 的人不少&#xff0c;这名字起得确实很有诱惑力&#xff1a;谁不想给自己的项目加持“超能力”&#xff1f;但你在搜索栏敲下这个词之后&#xff0c;大概率会看到一堆对不上号的结果&#xff0c;有讲个人成长心态的&#xff0c;有讲某个浏览器插件的&…

作者头像 李华
网站建设 2026/10/15 2:23:28

App Store Connect新手指南:从创建应用到审核上架全流程

第一次打开 App Store Connect&#xff0c;不少新手会盯着后台发呆&#xff1a;左上角一排菜单&#xff0c;中间全是待办提醒&#xff0c;右上角还藏着一个“协议、税务和银行业务”的入口。我当年第一次进来&#xff0c;第一反应是把页面上所有能点的按钮都点了一遍&#xff0…

作者头像 李华