Files
scuc-qt-course/docs/teaching/examples/calculator_mainwindow_qss/mainwindow.cpp
T
张宗平 12950da129 QSS 教学示例重构:双主题样式抽离为外部 .qss 资源(QFile 运行时加载)
把原先硬编码在 mainwindow.cpp 里的 kLightQss / kDarkQss 两个 C++ 字符串,
拆成独立的 themes/light.qss、themes/dark.qss,由 themes.qrc(AUTORCC)编译进
可执行文件,applyStyle() 用 QFile 读取后 qApp->setStyleSheet 应用。

教学要点(QSS 最佳实践):
- 样式与逻辑彻底分离:设计师/非程序员可直接编辑 .qss,不必碰 C++;
- 开发期改 .qss 重跑即生效,不必改 C++ 重编译;
- 多主题物理分文件,编辑器对 .qss 有语法高亮。

README 同步更新差异表、最佳实践条目与目录结构。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 16:50:31 +08:00

219 lines
9.5 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.
// ============================================================================
// 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 <QFile>
#include <QKeySequence>
#include <QListWidget>
#include <QListWidgetItem>
#include <QMenu>
#include <QMenuBar>
#include <QMessageBox>
#include <QStatusBar>
// ---- QSS:从外部 .qss 资源文件加载(themes/light.qss / themes/dark.qss----
// 教学要点(QSS 最佳实践):把样式抽成独立 .qss 文件用 QFile 读取,而非硬编码
// C++ 字符串——① 样式与逻辑彻底分离(设计师可独立编辑 .qss);② 开发期改 .qss
// 重跑即生效,不必改 C++ 重编译;③ 多主题物理分文件,编辑器对 .qss 有语法高亮。
// 资源路径 :/themes/*.qss 由 themes.qrcAUTORCC)编译进可执行文件。
namespace {
// 从嵌入资源读取样式表。读取失败返回空串(setStyleSheet 空串即清空样式)。
QString loadQss(const QString& resourcePath) {
QFile f(resourcePath);
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning() << "无法打开样式资源" << resourcePath;
return {};
}
return QString::fromUtf8(f.readAll());
}
} // 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;
const QString path = dark ? QStringLiteral(":/themes/dark.qss")
: QStringLiteral(":/themes/light.qss");
qApp->setStyleSheet(loadQss(path));
}
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;
}