计算器大概是所有 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支持连续输入多位数字四则运算 、-、、/支持中缀表达式连续计算比如 123-4/2支持括号能处理 (12)*(34) 这类组合清除键 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 标准建议选择 C17。Qt6 默认就会启用比较新的标准但如果你是从 Qt5 旧工程复制过来的一定检查一下 .pro 文件里的 CONFIG别让编译器用 C11 跑。lambda 表达式在信号槽里用得很频繁C17 在捕获、类型推导方面体验更好也能避免一些莫名其妙的警告。工程里主要就四个文件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.006 跨 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; }第一段正则如果匹配不上说明表达式里有非法字符。第二段连续运算符的限制处理了“12”“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; QStackdouble values; QStackQString 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 条件。它的意思是当前新来的运算符如果优先级不高于栈顶运算符就先让栈顶的运算符“算掉”。比如表达式“12×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_castlong long(value)); } return QString::number(value, g, 10); }逻辑很好理解如果这个值在误差范围内接近整数就直接转成 long long 输出避免“123.00000000”如果不是整数用 g 格式保留 10 位有效数字输出。这么做不是吹毛求疵计算器结果显示直接决定用户体感也影响后续表达式拼接的合法性。如果显示还是 “3.00000000”用户再按一次“5”表达式会变成“3.000000005”虽然能算但屏幕文本又长又丑。“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 c17 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.10.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 文件里运行时动态切换用相对布局替代固定尺寸支持窗口自由缩放时按钮跟着合理变化负数是很多人在我这版计算器里会卡住的扩展点。举个很实际的例子想输入“-35”如果不处理程序会判非法。扩展思路是在构建表达式阶段如果用户在表达式结尾输入“-”计算结果时统一按“前一数字 负数”处理或者更简单粗暴地在开头、左括号后把负号替换成“(0-”的形式等。但这个扩展需要重新设计表达式校验函数和求值器正好可以用来复习一遍本章讲过的两个核心函数。键盘输入其实是最能验证信号槽理解程度的功能。因为键盘事件处理不是依赖 clicked 信号而是依赖 QWidget 的 keyPressEvent。你会意识到原先按钮和槽的连接只是一种事件传递方式键盘事件又是另一条传递路径两者最终都汇聚到同一个 appendText / appendOperator 槽函数这就体现了“界面与业务分离”的思想。我自己做这个计算器的过程中最大的体会是“做出来”和“讲清楚为什么”完全是两回事。你可以照着代码抄一遍十分钟界面就出来了但如果你能手写网格布局的坐标、说清楚 lambda 捕获的语义、解释表达式校验每个正则的意图那才算真的跨过 Qt 入门的门槛。这个项目我建议你至少做两次第一次照着自己的理解写第二次故意加功能、故意制造 bug用调试器观察程序在事件循环里怎么一步步走到槽函数。带着这种“拆开再装回去”的心态去看 Qt你会发现它远远没有传言中那么难。