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:
张宗平
2026-07-07 22:59:05 +08:00
parent ea6f12e7ac
commit 05a685c952
45 changed files with 4599 additions and 6 deletions
@@ -0,0 +1,277 @@
// ============================================================================
// mainwindow.cpp —— Day3 下午版实现(上午版 + QSS 双主题)
// 与 ../calculator_mainwindow/mainwindow.cpp 的差异全部集中在本文件的 QSS 部分
// (见 kLightQss / kDarkQss / applyStyle / onToggleTheme / 视图菜单多一项),
// 菜单/停靠窗/状态栏/历史/自测的计算用例部分一字未改。
// ============================================================================
#include "mainwindow.h"
#include "calculatordesigner.h"
#include <QAction>
#include <QApplication>
#include <QDebug>
#include <QDockWidget>
#include <QKeySequence>
#include <QListWidget>
#include <QListWidgetItem>
#include <QMenu>
#include <QMenuBar>
#include <QMessageBox>
#include <QStatusBar>
// ---- QSS:按按键功能分色,ID 选择器精确定位(widget.ui 里已设好 objectName----
// 教学要点:QSS 选择器认的是 objectName——setObjectName("btnEquals") 之后才能用
// #btnEquals{...}。本例的 objectName 在 widget.ui 里已定义,无需再 setObjectName。
namespace {
// 浅色主题:数字=浅蓝、运算=橙、等号=绿、括号=灰、清空/退格=红、显示区=米黄
const char* kLightQss = R"(
QMainWindow { background: #f5f5f5; }
QLineEdit#display, QTextEdit#expressionEdit {
background: #fffbe6; color: #222;
border: 1px solid #d0c8a0; border-radius: 4px;
font-size: 18px; padding: 4px;
}
QPushButton {
border: 1px solid #bbb; border-radius: 4px;
padding: 8px; font-size: 15px; min-height: 28px;
}
QPushButton#btn0, QPushButton#btn1, QPushButton#btn2, QPushButton#btn3,
QPushButton#btn4, QPushButton#btn5, QPushButton#btn6, QPushButton#btn7,
QPushButton#btn8, QPushButton#btn9, QPushButton#btnDot {
background: #e3f2fd; border-color: #90caf9; color: #0d47a1;
}
QPushButton#btnPlus, QPushButton#btnMinus, QPushButton#btnMul, QPushButton#btnDiv {
background: #ffe0b2; border-color: #ffb74d; color: #e65100;
}
QPushButton#btnEquals {
background: #c8e6c9; border-color: #81c784; color: #1b5e20; font-weight: bold;
}
QPushButton#btnLeftParen, QPushButton#btnRightParen {
background: #eceff1; border-color: #b0bec5; color: #37474f;
}
QPushButton#btnClear, QPushButton#btnBackspace {
background: #ffcdd2; border-color: #e57373; color: #b71c1c;
}
QPushButton:hover { background: #ddd; }
QPushButton:pressed { background: #bbb; }
QListWidget { background: #fafafa; alternate-background-color: #eeeeee; }
QStatusBar { background: #e0e0e0; }
QMenuBar { background: #eceff1; }
QDockWidget { color: #333; }
)";
// 深色主题:整体深底浅字,功能分色保留但调暗,便于和浅色对照验证「切换生效」
const char* kDarkQss = R"(
QMainWindow { background: #2b2b2b; color: #eee; }
QLineEdit#display, QTextEdit#expressionEdit {
background: #3a3a2a; color: #fffbe6;
border: 1px solid #6b6b4a; border-radius: 4px;
font-size: 18px; padding: 4px;
}
QPushButton {
border: 1px solid #555; border-radius: 4px;
padding: 8px; font-size: 15px; min-height: 28px; color: #eee;
}
QPushButton#btn0, QPushButton#btn1, QPushButton#btn2, QPushButton#btn3,
QPushButton#btn4, QPushButton#btn5, QPushButton#btn6, QPushButton#btn7,
QPushButton#btn8, QPushButton#btn9, QPushButton#btnDot {
background: #1565c0; border-color: #42a5f5; color: #e3f2fd;
}
QPushButton#btnPlus, QPushButton#btnMinus, QPushButton#btnMul, QPushButton#btnDiv {
background: #d84315; border-color: #ff7043; color: #ffe0b2;
}
QPushButton#btnEquals {
background: #2e7d32; border-color: #66bb6a; color: #c8e6c9; font-weight: bold;
}
QPushButton#btnLeftParen, QPushButton#btnRightParen {
background: #455a64; border-color: #78909c; color: #eceff1;
}
QPushButton#btnClear, QPushButton#btnBackspace {
background: #c62828; border-color: #ef5350; color: #ffcdd2;
}
QPushButton:hover { background: #666; }
QPushButton:pressed { background: #888; }
QListWidget { background: #333; alternate-background-color: #3d3d3d; color: #eee; }
QStatusBar { background: #1f1f1f; color: #eee; }
QMenuBar { background: #37474f; color: #eee; }
QDockWidget { color: #eee; }
)";
} // namespace
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
setWindowTitle(QStringLiteral("计算器(QMainWindow + QSS 版)"));
m_calc = new CalculatorDesigner(this);
setCentralWidget(m_calc);
buildMenu();
buildDock();
buildStatusBar();
connect(m_calc, &CalculatorDesigner::evaluated, this, &MainWindow::onEvaluated);
applyStyle(false); // 默认浅色主题
}
MainWindow::~MainWindow() = default;
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);
// 【下午版新增】深色主题切换:checkable,勾上=深色
m_actToggleTheme = viewMenu->addAction(QStringLiteral("深色主题(&D)"));
m_actToggleTheme->setCheckable(true);
m_actToggleTheme->setChecked(false);
connect(m_actToggleTheme, &QAction::toggled, this, &MainWindow::onToggleTheme);
QMenu* helpMenu = menuBar()->addMenu(QStringLiteral("帮助(&H)"));
m_actAbout = helpMenu->addAction(QStringLiteral("关于(&A)"));
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"));
m_dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea | Qt::BottomDockWidgetArea);
m_dock->setWidget(m_history);
addDockWidget(Qt::RightDockWidgetArea, m_dock);
connect(m_history, &QListWidget::itemDoubleClicked, this, &MainWindow::onHistoryDoubleClicked);
}
void MainWindow::buildStatusBar() {
statusBar()->showMessage(QStringLiteral("就绪"));
}
void MainWindow::refreshStatus(const QString& msg) {
statusBar()->showMessage(msg, 3000);
}
// 【下午版新增】主题切换:用 qApp->setStyleSheet 全局生效(影响所有窗口及其子部件)。
// 注意 QSS 级联——qApp 级样式会被 widget 级 setStyleSheet 覆盖;本例只用 qApp 级,
// 避免和未来某控件局部样式打架(Day3 下午坑卡:setStyleSheet 级联失控)。
void MainWindow::applyStyle(bool dark) {
m_dark = dark;
qApp->setStyleSheet(QString::fromUtf8(dark ? kDarkQss : kLightQss));
}
void MainWindow::onToggleTheme(bool dark) {
applyStyle(dark);
refreshStatus(dark ? QStringLiteral("已切换深色主题") : QStringLiteral("已切换浅色主题"));
}
void MainWindow::onAbout() {
QMessageBox::about(this, QStringLiteral("关于"),
QStringLiteral(
"<b>计算器(QMainWindow + QSS 版)</b><br><br>"
"Day3 下午教学参考实现:在上午版基础上加 QSS 双主题美化——"
"数字/运算/等号/括号/清空退格按键分色,:hover/:pressed 视觉反馈,"
"深/浅主题切换。<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) {
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;
};
// —— 计算用例与上午版一致(验证加 QSS 后行为不变)——
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;
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(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;
// —— 【下午版新增】主题切换确实改变全局 styleSheet ——
applyStyle(true);
if (!expect("深色主题 styleSheet 非空", !qApp->styleSheet().isEmpty())) return false;
if (!expect("深色主题 styleSheet 含深色标识",
qApp->styleSheet().contains(QStringLiteral("#2b2b2b")))) return false;
applyStyle(false);
if (!expect("浅色主题 styleSheet 含浅色标识",
qApp->styleSheet().contains(QStringLiteral("#f5f5f5")))) return false;
if (!expect("主题状态 m_dark 已回浅", !m_dark)) return false;
// —— 历史双击回填 + 清空(与上午版一致)——
m_calc->onClear();
m_calc->onLeftParen();
m_calc->onDigit(1); m_calc->onOperator(QLatin1Char('+')); m_calc->onDigit(2);
m_calc->onEquals();
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;
onClearHistory();
if (!expect("清空历史后 count==0", m_history->count() == 0)) return false;
if (!expect("关于 action 已创建", m_actAbout != nullptr)) return false;
if (!expect("主题 action 已创建", m_actToggleTheme != nullptr)) return false;
return true;
}