// ============================================================================ // qt_calculator —— Day2 下午案例参考实现:简易计算器 // 对应教案:OUTLINE_4DAY.md Day2 下午「信号槽机制 → 项目案例:简易计算器」 // 配套文档:docs/teaching/examples/calculator/README.md // 手工维护,非生成产物(同 student_manager/qthread_worker 等教学补充示例)。 // // 教学重点(对应当天知识点,逐条在代码里标注): // 1. 信号槽两种写法都用上:数字/运算符键用「lambda + this 作 context」批量 // 绑定,等号/清空/退格键用「成员函数指针」绑定——当天两种写法都要会认; // 2. 所有 connect 只在构造函数里执行一次,不给「重复 connect 导致槽触发 // 两次」的坑(当天★坑卡)留生长空间; // 3. QGridLayout 排键盘(布局原理 Day3 下午讲透,今天先用后学); // 4. 中文/符号按钮文字用 QStringLiteral + 源文件 UTF-8,呼应当天编码坑卡。 // // 离屏验证(自测模式):QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_calculator // 自测流程:直接调用与按钮绑定的同一批处理函数(等价于点击),依次验证 // 「7+3=」「6÷0=」「4.5+0.5=」「12 退格」「2+3+4= 链式运算」,全部通过退出码 0, // 任一步失败退出码 1。 // ============================================================================ #include #include #include #include #include #include #include #include class Calculator : public QWidget { Q_OBJECT public: explicit Calculator(QWidget* parent = nullptr) : QWidget(parent) { setWindowTitle(QStringLiteral("简易计算器")); m_display = new QLineEdit(QStringLiteral("0"), this); m_display->setReadOnly(true); // 只显示结果,不接受直接键入 m_display->setAlignment(Qt::AlignRight); auto* mainLayout = new QVBoxLayout(this); mainLayout->addWidget(m_display); mainLayout->addLayout(buildKeypad()); } // 供自测直接调用,效果等价于点击对应按钮(同一批处理函数)。 void onDigit(int digit) { if (m_error) onClear(); if (m_startNew || m_display->text() == QStringLiteral("0")) { m_display->setText(QString::number(digit)); m_startNew = false; } else { m_display->setText(m_display->text() + QString::number(digit)); } } void onDot() { if (m_error) onClear(); if (m_startNew) { m_display->setText(QStringLiteral("0.")); m_startNew = false; return; } if (!m_display->text().contains(QLatin1Char('.'))) m_display->setText(m_display->text() + QStringLiteral(".")); } void onOperator(QChar op) { if (m_error) return; if (!m_startNew) compute(); // 连续按运算符:先把上一次算掉,再开始下一次(链式计算) m_pendingValue = m_display->text().toDouble(); m_pendingOp = op; m_startNew = true; } void onEquals() { if (m_error) return; compute(); m_pendingOp = QChar(); // 等号后清空待运算符:再按数字视为全新一次输入 } void onClear() { m_display->setText(QStringLiteral("0")); m_pendingValue = 0.0; m_pendingOp = QChar(); m_startNew = false; m_error = false; } void onBackspace() { if (m_error) { onClear(); return; } QString text = m_display->text(); text.chop(1); m_display->setText(text.isEmpty() ? QStringLiteral("0") : text); } QString displayText() const { return m_display->text(); } private: QGridLayout* buildKeypad() { auto* grid = new QGridLayout; // 布局原理 Day3 下午讲透,今天先会用:行列坐标摆键盘 addDigit(grid, 7, 0, 0); addDigit(grid, 8, 0, 1); addDigit(grid, 9, 0, 2); addOperator(grid, QStringLiteral("÷"), QLatin1Char('/'), 0, 3); addDigit(grid, 4, 1, 0); addDigit(grid, 5, 1, 1); addDigit(grid, 6, 1, 2); addOperator(grid, QStringLiteral("×"), QLatin1Char('*'), 1, 3); addDigit(grid, 1, 2, 0); addDigit(grid, 2, 2, 1); addDigit(grid, 3, 2, 2); addOperator(grid, QStringLiteral("-"), QLatin1Char('-'), 2, 3); addDigit(grid, 0, 3, 0); auto* btnDot = new QPushButton(QStringLiteral("."), this); grid->addWidget(btnDot, 3, 1); connect(btnDot, &QPushButton::clicked, this, [this] { onDot(); }); // 等号/清空/退格各自唯一,用成员函数指针连接——信号槽的另一种常见写法。 auto* btnEquals = new QPushButton(QStringLiteral("="), this); grid->addWidget(btnEquals, 3, 2); connect(btnEquals, &QPushButton::clicked, this, &Calculator::onEquals); addOperator(grid, QStringLiteral("+"), QLatin1Char('+'), 3, 3); auto* btnClear = new QPushButton(QStringLiteral("C"), this); grid->addWidget(btnClear, 4, 0); connect(btnClear, &QPushButton::clicked, this, &Calculator::onClear); auto* btnBackspace = new QPushButton(QStringLiteral("⌫"), this); grid->addWidget(btnBackspace, 4, 1); connect(btnBackspace, &QPushButton::clicked, this, &Calculator::onBackspace); return grid; } // 数字键:同一个槽 onDigit(int) 处理全部按钮,lambda 捕获各自的数字值, // 第五参数传 this 作 context——calculator 销毁时这些连接自动断开(当天坑卡)。 void addDigit(QGridLayout* grid, int digit, int row, int col) { auto* btn = new QPushButton(QString::number(digit), this); grid->addWidget(btn, row, col); connect(btn, &QPushButton::clicked, this, [this, digit] { onDigit(digit); }); } // 运算符键:同理,lambda + context,一个槽 onOperator(QChar) 处理全部。 void addOperator(QGridLayout* grid, const QString& label, QChar op, int row, int col) { auto* btn = new QPushButton(label, this); grid->addWidget(btn, row, col); connect(btn, &QPushButton::clicked, this, [this, op] { onOperator(op); }); } void compute() { if (m_pendingOp.isNull()) return; // 还没有待运算符(本次是第一个运算符) const double rhs = m_display->text().toDouble(); double result = 0.0; if (m_pendingOp == QLatin1Char('+')) result = m_pendingValue + rhs; else if (m_pendingOp == QLatin1Char('-')) result = m_pendingValue - rhs; else if (m_pendingOp == QLatin1Char('*')) result = m_pendingValue * rhs; else if (m_pendingOp == QLatin1Char('/')) { if (rhs == 0.0) { showError(); return; } // 除零:不崩,给出明确的错误状态 result = m_pendingValue / rhs; } m_display->setText(QString::number(result, 'g', 12)); m_pendingValue = result; m_startNew = true; } void showError() { m_display->setText(QStringLiteral("Error")); m_error = true; m_startNew = true; } QLineEdit* m_display = nullptr; double m_pendingValue = 0.0; QChar m_pendingOp; bool m_startNew = false; bool m_error = false; }; // ------------------------------------------------------------ 自测入口 ---- static bool expectEqual(const char* label, const QString& actual, const QString& expected) { if (actual != expected) { qCritical() << "自测失败:" << label << "实际=" << actual << "预期=" << expected; return false; } qInfo() << "自测通过:" << label << "=>" << actual; return true; } static bool runSelfTest(Calculator* calc) { calc->onClear(); calc->onDigit(7); calc->onOperator(QLatin1Char('+')); calc->onDigit(3); calc->onEquals(); if (!expectEqual("7 + 3 =", calc->displayText(), QStringLiteral("10"))) return false; calc->onClear(); calc->onDigit(6); calc->onOperator(QLatin1Char('/')); calc->onDigit(0); calc->onEquals(); if (!expectEqual("6 ÷ 0 =", calc->displayText(), QStringLiteral("Error"))) return false; calc->onClear(); calc->onDigit(4); calc->onDot(); calc->onDigit(5); calc->onOperator(QLatin1Char('+')); calc->onDigit(0); calc->onDot(); calc->onDigit(5); calc->onEquals(); if (!expectEqual("4.5 + 0.5 =", calc->displayText(), QStringLiteral("5"))) return false; calc->onClear(); calc->onDigit(1); calc->onDigit(2); calc->onBackspace(); if (!expectEqual("12 退格", calc->displayText(), QStringLiteral("1"))) return false; calc->onClear(); calc->onDigit(2); calc->onOperator(QLatin1Char('+')); calc->onDigit(3); calc->onOperator(QLatin1Char('+')); calc->onDigit(4); calc->onEquals(); if (!expectEqual("2 + 3 + 4 =(链式运算)", calc->displayText(), QStringLiteral("9"))) return false; return true; } int main(int argc, char* argv[]) { QApplication app(argc, argv); Calculator calc; calc.resize(240, 320); calc.show(); if (qgetenv("QT_QPA_PLATFORM") == "offscreen") { QTimer::singleShot(0, &calc, [&calc] { const bool ok = runSelfTest(&calc); QCoreApplication::exit(ok ? 0 : 1); }); } return app.exec(); } #include "qt_calculator.moc"