Files
scuc-qt-course/docs/teaching/examples/calculator/qt_calculator.cpp
T
张宗平 05a685c952 Day3 教学示例:增强版计算器接入 + QMainWindow/QSS 两版参考实现
- calculator_designer_ext(Day2 增强版:表达式求值 + 括号,递归下降解析器)接入 cmake
- calculator_mainwindow(Day3 上午):增强版计算器装进 QMainWindow,
  菜单/状态栏/历史停靠窗,纯代码 + 显式 connect,与 mainwindow_showcase 形成坑卡对照
- calculator_mainwindow_qss(Day3 下午):上午版 + QSS 双主题美化(按键分色/伪状态/深浅切换)
- 验证 mainwindow_showcase(QMainWindow 六大组件综合演示,.ui + .qrc)
- OUTLINE_4DAY.md / ASSIGNMENTS.md Day3 衔接增强版起点 + 参考实现指引
- 补提交 Day2 calculator/ 与 calculator_designer/ 参考实现(CMakeLists 已引用)
- 全量构建 28/28,4 目标离屏自测退出码 0

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 22:59:05 +08:00

235 lines
9.3 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================================
// 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 <QApplication>
#include <QDebug>
#include <QGridLayout>
#include <QLineEdit>
#include <QPushButton>
#include <QTimer>
#include <QVBoxLayout>
#include <QWidget>
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"