12950da129
把原先硬编码在 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>
219 lines
9.5 KiB
C++
219 lines
9.5 KiB
C++
// ============================================================================
|
||
// 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.qrc(AUTORCC)编译进可执行文件。
|
||
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;
|
||
}
|