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,212 @@
|
||||
// ============================================================================
|
||||
// mainwindow.cpp —— Day3 上午版实现
|
||||
// 详见 mainwindow.h 顶部注释。本文件只做外壳组装,不含任何计算逻辑。
|
||||
// ============================================================================
|
||||
#include "mainwindow.h"
|
||||
#include "calculatordesigner.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QDebug>
|
||||
#include <QDockWidget>
|
||||
#include <QKeySequence>
|
||||
#include <QListWidget>
|
||||
#include <QListWidgetItem>
|
||||
#include <QMenu>
|
||||
#include <QMenuBar>
|
||||
#include <QMessageBox>
|
||||
#include <QStatusBar>
|
||||
|
||||
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
setWindowTitle(QStringLiteral("计算器(QMainWindow 版)"));
|
||||
|
||||
// ★ 中心部件必须显式安放(Day3 上午坑卡:直接往 QMainWindow 上 addWidget
|
||||
// 布局行为不可预期)。CalculatorDesigner 是 QWidget,原样塞进去即可。
|
||||
m_calc = new CalculatorDesigner(this);
|
||||
setCentralWidget(m_calc);
|
||||
|
||||
buildMenu();
|
||||
buildDock();
|
||||
buildStatusBar();
|
||||
|
||||
// 计算器求值成功 → 历史停靠窗追加一条(Day2 信号槽的复用:给已有部件
|
||||
// 加 evaluated 信号,外壳订阅它)。
|
||||
connect(m_calc, &CalculatorDesigner::evaluated, this, &MainWindow::onEvaluated);
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() = default;
|
||||
|
||||
// ---- 菜单栏(纯代码 + 显式 connect,对照 mainwindow_showcase 的 .ui 自动连接)----
|
||||
void MainWindow::buildMenu() {
|
||||
QMenu* fileMenu = menuBar()->addMenu(QStringLiteral("文件(&F)"));
|
||||
m_actQuit = fileMenu->addAction(QStringLiteral("退出(&Q)"));
|
||||
m_actQuit->setShortcut(QKeySequence(QStringLiteral("Ctrl+Q")));
|
||||
connect(m_actQuit, &QAction::triggered, this, &QWidget::close);
|
||||
|
||||
QMenu* editMenu = menuBar()->addMenu(QStringLiteral("编辑(&E)"));
|
||||
m_actClearHistory = editMenu->addAction(QStringLiteral("清空历史(&C)"));
|
||||
connect(m_actClearHistory, &QAction::triggered, this, &MainWindow::onClearHistory);
|
||||
|
||||
QMenu* viewMenu = menuBar()->addMenu(QStringLiteral("视图(&V)"));
|
||||
m_actToggleHistory = viewMenu->addAction(QStringLiteral("显示历史(&H)"));
|
||||
m_actToggleHistory->setCheckable(true);
|
||||
m_actToggleHistory->setChecked(true);
|
||||
connect(m_actToggleHistory, &QAction::toggled, this, &MainWindow::onToggleHistoryVisible);
|
||||
|
||||
QMenu* helpMenu = menuBar()->addMenu(QStringLiteral("帮助(&H)"));
|
||||
m_actAbout = helpMenu->addAction(QStringLiteral("关于(&A)"));
|
||||
// ★ 显式 connect:忘了这一行,菜单显示正常、点了没反应(Day3 上午坑卡)。
|
||||
connect(m_actAbout, &QAction::triggered, this, &MainWindow::onAbout);
|
||||
}
|
||||
|
||||
// ---- 历史停靠窗 ----
|
||||
void MainWindow::buildDock() {
|
||||
m_history = new QListWidget(this);
|
||||
m_history->setAlternatingRowColors(true);
|
||||
m_dock = new QDockWidget(QStringLiteral("历史"), this);
|
||||
m_dock->setObjectName(QStringLiteral("historyDock")); // 供 saveState/restoreState
|
||||
m_dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea | Qt::BottomDockWidgetArea);
|
||||
m_dock->setWidget(m_history);
|
||||
addDockWidget(Qt::RightDockWidgetArea, m_dock);
|
||||
// 双击历史条目 → 回填表达式(QListWidget 信号槽)
|
||||
connect(m_history, &QListWidget::itemDoubleClicked, this, &MainWindow::onHistoryDoubleClicked);
|
||||
}
|
||||
|
||||
void MainWindow::buildStatusBar() {
|
||||
statusBar()->showMessage(QStringLiteral("就绪"));
|
||||
}
|
||||
|
||||
void MainWindow::refreshStatus(const QString& msg) {
|
||||
statusBar()->showMessage(msg, 3000); // 临时提示,3 秒后恢复
|
||||
}
|
||||
|
||||
// ---- 槽 ----
|
||||
void MainWindow::onAbout() {
|
||||
// 标准对话框:QMessageBox::about 是模态的,exec() 内部阻塞等用户关闭。
|
||||
QMessageBox::about(this, QStringLiteral("关于"),
|
||||
QStringLiteral(
|
||||
"<b>计算器(QMainWindow 版)</b><br><br>"
|
||||
"Day3 上午教学参考实现:把 Day2 增强版计算器(表达式求值 + 括号)"
|
||||
"装进 QMainWindow,演示菜单栏 / 状态栏 / 历史停靠窗。<br><br>"
|
||||
"对应教案 OUTLINE_4DAY.md Day3 上午。"));
|
||||
}
|
||||
|
||||
void MainWindow::onClearHistory() {
|
||||
m_history->clear();
|
||||
refreshStatus(QStringLiteral("已清空历史"));
|
||||
}
|
||||
|
||||
void MainWindow::onToggleHistoryVisible(bool checked) {
|
||||
m_dock->setVisible(checked);
|
||||
}
|
||||
|
||||
void MainWindow::onEvaluated(const QString& expr, const QString& result) {
|
||||
m_history->addItem(expr + QStringLiteral(" = ") + result);
|
||||
refreshStatus(QStringLiteral("结果: ") + result);
|
||||
}
|
||||
|
||||
void MainWindow::onHistoryDoubleClicked(QListWidgetItem* item) {
|
||||
// 条目文本形如 "expr = result",取最后一个 " = " 左侧作为表达式回填。
|
||||
// 用 lastIndexOf 而非 indexOf:result 文本里一般不含 " = ",但用 lastIndexOf
|
||||
// 更稳妥(避免表达式里万一出现 " = ",实际上表达式不会含 =)。
|
||||
const QString text = item->text();
|
||||
const int idx = text.lastIndexOf(QStringLiteral(" = "));
|
||||
if (idx < 0) return;
|
||||
m_calc->loadExpression(text.left(idx));
|
||||
refreshStatus(QStringLiteral("已回填表达式"));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 自测 ----
|
||||
bool MainWindow::runSelfTest() {
|
||||
auto expect = [](const char* label, bool cond) -> bool {
|
||||
if (!cond) { qCritical() << "自测失败:" << label; return false; }
|
||||
qInfo() << "自测通过:" << label; return true;
|
||||
};
|
||||
auto expectEq = [](const char* label, const QString& actual, const QString& expected) -> bool {
|
||||
if (actual != expected) {
|
||||
qCritical() << "自测失败:" << label << "实际=" << actual << "预期=" << expected;
|
||||
return false;
|
||||
}
|
||||
qInfo() << "自测通过:" << label << "=>" << actual;
|
||||
return true;
|
||||
};
|
||||
|
||||
// —— Day2 计算 9 组用例(验证升级外壳后计算行为不变)——
|
||||
m_calc->onClear();
|
||||
m_calc->onDigit(7); m_calc->onOperator(QLatin1Char('+')); m_calc->onDigit(3); m_calc->onEquals();
|
||||
if (!expectEq("7 + 3 =", m_calc->displayText(), QStringLiteral("10"))) return false;
|
||||
if (!expect("历史追加第 1 条", m_history->count() == 1)) return false;
|
||||
if (!expectEq("历史条目文本", m_history->item(0)->text(), QStringLiteral("7+3 = 10"))) return false;
|
||||
|
||||
m_calc->onClear();
|
||||
m_calc->onDigit(6); m_calc->onOperator(QLatin1Char('/')); m_calc->onDigit(0); m_calc->onEquals();
|
||||
if (!expectEq("6 ÷ 0 =", m_calc->displayText(), QStringLiteral("Error"))) return false;
|
||||
if (!expect("除零 Error 不进历史", m_history->count() == 1)) return false;
|
||||
|
||||
m_calc->onClear();
|
||||
m_calc->onDigit(4); m_calc->onDot(); m_calc->onDigit(5);
|
||||
m_calc->onOperator(QLatin1Char('+'));
|
||||
m_calc->onDigit(0); m_calc->onDot(); m_calc->onDigit(5);
|
||||
m_calc->onEquals();
|
||||
if (!expectEq("4.5 + 0.5 =", m_calc->displayText(), QStringLiteral("5"))) return false;
|
||||
|
||||
m_calc->onClear();
|
||||
m_calc->onDigit(1); m_calc->onDigit(2); m_calc->onBackspace();
|
||||
if (!expectEq("12 退格", m_calc->displayText(), QStringLiteral("1"))) return false;
|
||||
|
||||
m_calc->onClear();
|
||||
m_calc->onDigit(2); m_calc->onOperator(QLatin1Char('+'));
|
||||
m_calc->onDigit(3); m_calc->onOperator(QLatin1Char('+'));
|
||||
m_calc->onDigit(4); m_calc->onEquals();
|
||||
if (!expectEq("2 + 3 + 4 =", m_calc->displayText(), QStringLiteral("9"))) return false;
|
||||
|
||||
m_calc->onClear();
|
||||
m_calc->onDigit(2); m_calc->onOperator(QLatin1Char('+'));
|
||||
m_calc->onDigit(3); m_calc->onOperator(QLatin1Char('*'));
|
||||
m_calc->onDigit(4); m_calc->onEquals();
|
||||
if (!expectEq("2 + 3 × 4 =", m_calc->displayText(), QStringLiteral("14"))) return false;
|
||||
|
||||
m_calc->onClear();
|
||||
m_calc->onLeftParen();
|
||||
m_calc->onDigit(2); m_calc->onOperator(QLatin1Char('+')); m_calc->onDigit(3);
|
||||
m_calc->onRightParen();
|
||||
m_calc->onOperator(QLatin1Char('*'));
|
||||
m_calc->onDigit(4); m_calc->onEquals();
|
||||
if (!expectEq("(2 + 3) × 4 =", m_calc->displayText(), QStringLiteral("20"))) return false;
|
||||
|
||||
m_calc->onClear();
|
||||
m_calc->onDigit(1); m_calc->onOperator(QLatin1Char('+'));
|
||||
m_calc->onLeftParen();
|
||||
m_calc->onDigit(2); m_calc->onOperator(QLatin1Char('*'));
|
||||
m_calc->onLeftParen();
|
||||
m_calc->onDigit(3); m_calc->onOperator(QLatin1Char('+')); m_calc->onDigit(4);
|
||||
m_calc->onRightParen();
|
||||
m_calc->onRightParen();
|
||||
m_calc->onEquals();
|
||||
if (!expectEq("1 + (2 × (3 + 4)) =", m_calc->displayText(), QStringLiteral("15"))) return false;
|
||||
|
||||
m_calc->onClear();
|
||||
m_calc->onLeftParen();
|
||||
m_calc->onDigit(1); m_calc->onOperator(QLatin1Char('+')); m_calc->onDigit(2);
|
||||
m_calc->onEquals();
|
||||
if (!expectEq("(1 + 2 = 自动补全", m_calc->displayText(), QStringLiteral("3"))) return false;
|
||||
|
||||
// 9 组用例里:第 4 组(12 退格)无 onEquals、第 2 组(6÷0)Error 不 emit,
|
||||
// 其余 7 次 onEquals 各进历史一条。
|
||||
if (!expect("历史累计 7 条", m_history->count() == 7)) return false;
|
||||
|
||||
// —— Day3 新增:历史双击回填 ——
|
||||
QListWidgetItem* last = m_history->item(m_history->count() - 1);
|
||||
if (!expectEq("最后一条历史", last->text(), QStringLiteral("(1+2) = 3"))) return false;
|
||||
onHistoryDoubleClicked(last);
|
||||
if (!expectEq("双击回填表达式区", m_calc->expressionText(), QStringLiteral("(1+2)"))) return false;
|
||||
|
||||
// —— Day3 新增:清空历史 ——
|
||||
onClearHistory();
|
||||
if (!expect("清空历史后 count==0", m_history->count() == 0)) return false;
|
||||
|
||||
// —— Day3 新增:关于 action 已创建并显式连接 ——
|
||||
// (QMessageBox::about 是模态弹窗,离屏下不真正触发,GUI 弹窗验证靠现场演示)
|
||||
if (!expect("关于 action 已创建", m_actAbout != nullptr)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user