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,59 @@
|
||||
# 简易计算器 —— Qt Designer 表单对照版
|
||||
|
||||
> 对应教案 [`OUTLINE_4DAY.md`](../../OUTLINE_4DAY.md) Day2 下午「项目案例:简易
|
||||
> 计算器」。与 [`../calculator/`](../calculator/README.md)(纯代码 `QGridLayout`
|
||||
> 版)对照阅读:**界面搭建方式换了,业务逻辑一字不差**——`onDigit`/`onOperator`/
|
||||
> `onEquals`/`onClear`/`onBackspace`/`compute` 全部照搬。手工维护,非生成产物。
|
||||
|
||||
## 与代码版的唯一区别:界面从哪来
|
||||
|
||||
| | 代码版 `qt_calculator.cpp` | Designer 版 `qt_calculator_designer.cpp` |
|
||||
|---|---|---|
|
||||
| 控件树+布局 | `buildKeypad()` 手写 `new QPushButton` + `QGridLayout::addWidget` | `calculator_designer.ui`(Designer 表单 XML)+ `uic` 编译期生成 `ui_calculator_designer.h` + `setupUi(this)` 一次建好 |
|
||||
| 找控件 | 局部变量/成员指针 | `ui->btn7`、`ui->display` ……(objectName 对应成员名) |
|
||||
| 构建开关 | `AUTOMOC ON` | `AUTOMOC ON` **+ `AUTOUIC ON`**(CMake 自动跑 `uic`) |
|
||||
| 业务逻辑 | 状态机 | 完全相同 |
|
||||
|
||||
## ★ 新增知识点:Designer 表单的「自动连接」
|
||||
|
||||
`ui->setupUi(this)` 内部会调用 `QMetaObject::connectSlotsByName(this)`:只要
|
||||
类里有名为 `on_<objectName>_<signal>()` 的槽(比如 `on_btnClear_clicked()`),
|
||||
Qt 会**自动**把表单里 `objectName` 为 `btnClear` 的控件的 `clicked()` 信号连上去,
|
||||
不需要手写 `connect()`。这是信号槽的第三种写法(前两种见 `../calculator/`)。
|
||||
|
||||
**本示例故意不用这个自动连接**,全部改成显式 `connect()`——原因是当天的坑卡:
|
||||
|
||||
- 坑:表单里把按钮 `objectName` 从 `btnClear` 改成 `btnReset`,`on_btnClear_clicked()`
|
||||
这个槽**不报编译错误、不报运行时错误**,只是从此再也不会被调用——`connectSlotsByName`
|
||||
找不到匹配的 `objectName` 时只是静默跳过。这是「静默失败」坑点家族的第三个成员
|
||||
(前两个是旧 `SIGNAL()/SLOT()` 字符串语法、重复 `connect`)。
|
||||
- 结论:显式 `connect()` 虽然多写几行,但改名/删控件时编译器/IDE 能帮你追踪引用,
|
||||
排查成本远低于依赖命名约定的隐式连接。项目里**推荐显式 connect**,`on_xxx_yyy()`
|
||||
自动连接更适合 Qt Designer 快速原型阶段。
|
||||
|
||||
## 验证方式(自测模式)
|
||||
|
||||
```bash
|
||||
QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_calculator_designer
|
||||
# 自测用例与 ../calculator/ 完全一致:「7+3=10」「6÷0=Error」「4.5+0.5=5」
|
||||
# 「12 退格→1」「2+3+4=9(链式运算)」,全部通过退出码 0
|
||||
tools/run_qt.sh qt_calculator_designer # 有 X 环境:完整交互
|
||||
```
|
||||
|
||||
## 课堂讲解建议
|
||||
|
||||
1. 先展示 `.ui` 文件本身是纯 XML(`<widget class="QPushButton" name="btn7">…`)——
|
||||
Qt Designer 只是这份 XML 的图形化编辑器,不是什么黑魔法;
|
||||
2. 对照 `../calculator/qt_calculator.cpp` 的 `buildKeypad()`,逐行说明
|
||||
`uic` 把这份 XML 翻译成了等价的 C++ 建控件代码(`build/` 里能翻出真实生成的
|
||||
`ui_calculator_designer.h` 验证);
|
||||
3. 演示自动连接的坑:把 `.ui` 里某个按钮的 `name` 改掉,重新构建,点击按钮
|
||||
没反应——现场复现「改名静默失效」;
|
||||
4. 讨论:两种方式怎么选?—— 快速原型/美术同学调界面用 Designer 更直观;
|
||||
逻辑复杂、控件数量随数据动态变化(比如 Day5 的表格行)就必须手写代码搭建,
|
||||
`.ui` 表单描述不了「运行时循环创建 N 个控件」。
|
||||
|
||||
## 参考
|
||||
|
||||
- 代码布局对照版:[`../calculator/`](../calculator/README.md)
|
||||
- Qt 官方 Widgets 表单机制:`uic`(User Interface Compiler)+ `QMetaObject::connectSlotsByName`
|
||||
@@ -0,0 +1,164 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>CalculatorForm</class>
|
||||
<widget class="QWidget" name="CalculatorForm">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>358</width>
|
||||
<height>320</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>简易计算器(Designer 版)</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="rootLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="display">
|
||||
<property name="text">
|
||||
<string>0</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="keypadLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QPushButton" name="btn7">
|
||||
<property name="text">
|
||||
<string>7</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="btn8">
|
||||
<property name="text">
|
||||
<string>8</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QPushButton" name="btn9">
|
||||
<property name="text">
|
||||
<string>9</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="QPushButton" name="btnDiv">
|
||||
<property name="text">
|
||||
<string>÷</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QPushButton" name="btn4">
|
||||
<property name="text">
|
||||
<string>4</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QPushButton" name="btn5">
|
||||
<property name="text">
|
||||
<string>5</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="btn6">
|
||||
<property name="text">
|
||||
<string>6</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="3">
|
||||
<widget class="QPushButton" name="btnMul">
|
||||
<property name="text">
|
||||
<string>×</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QPushButton" name="btn1">
|
||||
<property name="text">
|
||||
<string>1</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QPushButton" name="btn2">
|
||||
<property name="text">
|
||||
<string>2</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QPushButton" name="btn3">
|
||||
<property name="text">
|
||||
<string>3</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="3">
|
||||
<widget class="QPushButton" name="btnMinus">
|
||||
<property name="text">
|
||||
<string>-</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QPushButton" name="btn0">
|
||||
<property name="text">
|
||||
<string>0</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QPushButton" name="btnDot">
|
||||
<property name="text">
|
||||
<string>.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QPushButton" name="btnEquals">
|
||||
<property name="text">
|
||||
<string>=</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="3">
|
||||
<widget class="QPushButton" name="btnPlus">
|
||||
<property name="text">
|
||||
<string>+</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QPushButton" name="btnClear">
|
||||
<property name="text">
|
||||
<string>C</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QPushButton" name="btnBackspace">
|
||||
<property name="text">
|
||||
<string>⌫</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,202 @@
|
||||
// ============================================================================
|
||||
// qt_calculator_designer —— Day2 案例参考实现(Qt Designer 表单版)
|
||||
// 对应教案:OUTLINE_4DAY.md Day2 下午「项目案例:简易计算器」
|
||||
// 配套文档:docs/teaching/examples/calculator_designer/README.md
|
||||
// 手工维护,非生成产物。
|
||||
//
|
||||
// 与 ../calculator/qt_calculator.cpp(纯代码 QGridLayout 版)对照阅读:
|
||||
// 界面结构完全一致,唯一区别是键盘布局用 Qt Designer 表单
|
||||
// (calculator_designer.ui)设计——编译期由 uic 生成 ui_calculator_designer.h,
|
||||
// setupUi(this) 一次性把控件树 + 布局全部建好;业务逻辑(onDigit/onOperator/
|
||||
// onEquals/...)与代码版完全相同、原样照搬,刻意保持一致以突出「界面搭建方式
|
||||
// 变了,逻辑代码不用变」。
|
||||
//
|
||||
// 教学重点(对照代码版新增的一条):
|
||||
// Designer 表单里控件的 objectName 决定「自动连接」——setupUi() 内部会调用
|
||||
// QMetaObject::connectSlotsByName(this),自动把名为 on_<objectName>_<signal>()
|
||||
// 的槽连到对应控件的信号。本例改用显式 connect()(更贴近代码版、也更利于
|
||||
// 团队协作排查),但这个自动连接特性是 Designer 工作流特有的第三种信号槽写法,
|
||||
// 坑点见 README:控件改名后自动连接悄悄失效,不报编译错误也不报运行时错误。
|
||||
//
|
||||
// 离屏验证(自测模式):QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_calculator_designer
|
||||
// 自测用例与 ../calculator/ 完全一致,用于验证「界面来源不同、行为一致」。
|
||||
// ============================================================================
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
#include "ui_calculator_designer.h"
|
||||
|
||||
class CalculatorDesigner : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CalculatorDesigner(QWidget* parent = nullptr)
|
||||
: QWidget(parent), ui(new Ui::CalculatorForm) {
|
||||
ui->setupUi(this); // 表单里的控件树 + QGridLayout 全部由 Designer XML 建好
|
||||
|
||||
connect(ui->btn0, &QPushButton::clicked, this, [this] { onDigit(0); });
|
||||
connect(ui->btn1, &QPushButton::clicked, this, [this] { onDigit(1); });
|
||||
connect(ui->btn2, &QPushButton::clicked, this, [this] { onDigit(2); });
|
||||
connect(ui->btn3, &QPushButton::clicked, this, [this] { onDigit(3); });
|
||||
connect(ui->btn4, &QPushButton::clicked, this, [this] { onDigit(4); });
|
||||
connect(ui->btn5, &QPushButton::clicked, this, [this] { onDigit(5); });
|
||||
connect(ui->btn6, &QPushButton::clicked, this, [this] { onDigit(6); });
|
||||
connect(ui->btn7, &QPushButton::clicked, this, [this] { onDigit(7); });
|
||||
connect(ui->btn8, &QPushButton::clicked, this, [this] { onDigit(8); });
|
||||
connect(ui->btn9, &QPushButton::clicked, this, [this] { onDigit(9); });
|
||||
connect(ui->btnDot, &QPushButton::clicked, this, [this] { onDot(); });
|
||||
|
||||
connect(ui->btnPlus, &QPushButton::clicked, this, [this] { onOperator(QLatin1Char('+')); });
|
||||
connect(ui->btnMinus, &QPushButton::clicked, this, [this] { onOperator(QLatin1Char('-')); });
|
||||
connect(ui->btnMul, &QPushButton::clicked, this, [this] { onOperator(QLatin1Char('*')); });
|
||||
connect(ui->btnDiv, &QPushButton::clicked, this, [this] { onOperator(QLatin1Char('/')); });
|
||||
|
||||
connect(ui->btnEquals, &QPushButton::clicked, this, &CalculatorDesigner::onEquals);
|
||||
connect(ui->btnClear, &QPushButton::clicked, this, &CalculatorDesigner::onClear);
|
||||
connect(ui->btnBackspace, &QPushButton::clicked, this, &CalculatorDesigner::onBackspace);
|
||||
}
|
||||
|
||||
~CalculatorDesigner() override { delete ui; }
|
||||
|
||||
// 供自测直接调用,效果等价于点击对应按钮;逻辑与 ../calculator/ 版一字不差。
|
||||
void onDigit(int digit) {
|
||||
if (m_error) onClear();
|
||||
if (m_startNew || ui->display->text() == QStringLiteral("0")) {
|
||||
ui->display->setText(QString::number(digit));
|
||||
m_startNew = false;
|
||||
} else {
|
||||
ui->display->setText(ui->display->text() + QString::number(digit));
|
||||
}
|
||||
}
|
||||
|
||||
void onDot() {
|
||||
if (m_error) onClear();
|
||||
if (m_startNew) {
|
||||
ui->display->setText(QStringLiteral("0."));
|
||||
m_startNew = false;
|
||||
return;
|
||||
}
|
||||
if (!ui->display->text().contains(QLatin1Char('.')))
|
||||
ui->display->setText(ui->display->text() + QStringLiteral("."));
|
||||
}
|
||||
|
||||
void onOperator(QChar op) {
|
||||
if (m_error) return;
|
||||
if (!m_startNew) compute();
|
||||
m_pendingValue = ui->display->text().toDouble();
|
||||
m_pendingOp = op;
|
||||
m_startNew = true;
|
||||
}
|
||||
|
||||
void onEquals() {
|
||||
if (m_error) return;
|
||||
compute();
|
||||
m_pendingOp = QChar();
|
||||
}
|
||||
|
||||
void onClear() {
|
||||
ui->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 = ui->display->text();
|
||||
text.chop(1);
|
||||
ui->display->setText(text.isEmpty() ? QStringLiteral("0") : text);
|
||||
}
|
||||
|
||||
QString displayText() const { return ui->display->text(); }
|
||||
|
||||
private:
|
||||
void compute() {
|
||||
if (m_pendingOp.isNull()) return;
|
||||
const double rhs = ui->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;
|
||||
}
|
||||
ui->display->setText(QString::number(result, 'g', 12));
|
||||
m_pendingValue = result;
|
||||
m_startNew = true;
|
||||
}
|
||||
|
||||
void showError() {
|
||||
ui->display->setText(QStringLiteral("Error"));
|
||||
m_error = true;
|
||||
m_startNew = true;
|
||||
}
|
||||
|
||||
Ui::CalculatorForm* ui;
|
||||
double m_pendingValue = 0.0;
|
||||
QChar m_pendingOp;
|
||||
bool m_startNew = false;
|
||||
bool m_error = false;
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------ 自测入口 ----
|
||||
// 与 ../calculator/qt_calculator.cpp 完全相同的 5 组用例:验证「界面搭建方式
|
||||
// 换了,行为不变」。
|
||||
|
||||
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(CalculatorDesigner* 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);
|
||||
CalculatorDesigner calc;
|
||||
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_designer.moc"
|
||||
Reference in New Issue
Block a user