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>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# 简易计算器 —— Day2 下午案例参考实现(代码布局版)
|
||||
|
||||
> 对应教案 [`OUTLINE_4DAY.md`](../../OUTLINE_4DAY.md) Day2 下午「信号槽机制 →
|
||||
> 项目案例:简易计算器」。教师侧参考答案,手工维护,非生成产物。
|
||||
> Designer 表单对照版见 [`../calculator_designer/`](../calculator_designer/README.md)。
|
||||
|
||||
## 覆盖的知识点(讲解时逐条回指)
|
||||
|
||||
| 环节 | 知识点 | 首次出现 |
|
||||
|---|---|---|
|
||||
| 界面骨架 | `QGridLayout` 排数字/运算符键盘 | Day2 下午(脚手架先用后学,Day3 下午讲透原理) |
|
||||
| 信号槽写法 1 | lambda + 第五参数 context(数字/运算符键批量绑定) | Day2 下午 |
|
||||
| 信号槽写法 2 | 成员函数指针连接(等号/清空/退格键) | Day2 下午 |
|
||||
| 坑点规避 | 所有 `connect` 只在构造函数执行一次,不留「重复 connect」隐患 | Day2 ★坑卡 |
|
||||
| 编码 | 中文/符号按钮文字 `QStringLiteral` + 源文件 UTF-8 | Day2 ★坑卡 |
|
||||
| 状态机 | 数字/小数点/运算符/等号/清空/退格 六类输入的最小状态机;连续按运算符走「链式计算」;除零给出 `Error` 而不崩溃 | 案例本身 |
|
||||
|
||||
## 验证方式(自测模式)
|
||||
|
||||
```bash
|
||||
QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_calculator
|
||||
# 自测流程:直接调用与按钮绑定的同一批处理函数(等价于点击),依次验证
|
||||
# 「7+3=10」「6÷0=Error」「4.5+0.5=5」「12 退格→1」「2+3+4=9(链式运算)」
|
||||
# 全部通过退出码 0,任一步失败退出码 1
|
||||
tools/run_qt.sh qt_calculator # 有 X 环境:完整交互
|
||||
```
|
||||
|
||||
## 课堂讲解建议
|
||||
|
||||
1. 先跑成品:现场敲一次「7 + 3 =」,再回头拆代码——项目 = 已学知识点的组装;
|
||||
2. 重点讲两种 connect 写法的取舍:数字/运算符键数量多、逻辑相同,用
|
||||
lambda 捕获参数 + 统一槽函数(`onDigit(int)`/`onOperator(QChar)`);
|
||||
等号/清空/退格各自唯一,直接用成员函数指针更直观;
|
||||
3. 现场移除某个按钮的 `connect`,演示"按钮显示正常、点击没反应"——呼应
|
||||
QAction 忘 connect 的坑(Day3 会再见到同一类问题);
|
||||
4. 除零处理是刻意设计的教学点:不用 `assert`/崩溃,而是给出可恢复的错误状态
|
||||
(`Error` + 下一次输入自动清空),这是「健壮性」在中初级学员能理解层面的
|
||||
最小示范。
|
||||
|
||||
## 练习方向(对应任务卡 2 · 课后作业)
|
||||
|
||||
见 [`OUTLINE_4DAY.md`](../../OUTLINE_4DAY.md) Day2 下午「16:00–17:00 作业」与
|
||||
`slides/day2.html` 任务卡 2;本参考实现即该作业的教师侧标准答案,**不要在课上
|
||||
直接展示源码给学生抄**,讲解思路后学生自己实现。
|
||||
|
||||
## 参考
|
||||
|
||||
- 布局脚手架来源:`p03/ch08/custom_widget.cpp`(`QHBoxLayout`/`QSlider` 联动)
|
||||
- 信号槽写法来源:`p03/ch04/`(`builtin_signal_slot.cpp`、`custom_signal_slot.cpp`、
|
||||
`lambda_signal_slot.cpp`)
|
||||
- Designer 表单对照版:[`../calculator_designer/`](../calculator_designer/README.md)
|
||||
@@ -0,0 +1,234 @@
|
||||
// ============================================================================
|
||||
// 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"
|
||||
Reference in New Issue
Block a user