#include "global.h" #include #include #include #include #include #include #include #include // ===================== 全局闪烁节拍 ===================== BlinkBeat::BlinkBeat(QObject* parent) : QObject(parent) { m_timer = new QTimer(this); m_timer->setInterval(500); // 闪烁半周期:亮 500ms、灭 500ms connect(m_timer, &QTimer::timeout, this, [this]() { m_on = !m_on; emit toggled(m_on); }); m_timer->start(); } BlinkBeat* BlinkBeat::instance() { // 首次用到才建(此时 QApplication 已就绪),进程结束自然销毁。 // 不挂在某个控件下:它要跨页面、跨控件地给所有闪烁源提供同一个相位。 static BlinkBeat beat; return &beat; } namespace { // 订阅句柄挂在被闪的控件下、名字固定,stopBlink 靠它找回并删掉。 // QObject 析构时会自动断开它身上所有的连接,所以“删除句柄”就等于“退订”。 QString blinkKey(const char* prop) { return QString("blinkconn_") + prop; } } // 挂到全局节拍上:每拍翻转一次动态属性,QSS 据此在 40%↔100% 透明度间切换。 // 用公共节拍而不是各起一个定时器——各自起的话相位各异,同屏几处会各闪各的。 void startBlink(QWidget* w, const char* prop) { if (w->findChild(blinkKey(prop))) return; // 已订阅 QObject* holder = new QObject(w); holder->setObjectName(blinkKey(prop)); // 以 holder 作接收者:它随控件一起销毁,连接也就一并断开 QObject::connect(BlinkBeat::instance(), &BlinkBeat::toggled, holder, [w, prop](bool on) { w->setProperty(prop, on); w->style()->unpolish(w); w->style()->polish(w); w->update(); }); // 立刻按当前相位显示,不然要等到下一拍 w->setProperty(prop, BlinkBeat::instance()->on()); w->style()->unpolish(w); w->style()->polish(w); w->update(); } void stopBlink(QWidget* w, const char* prop) { if (QObject* holder = w->findChild(blinkKey(prop))) delete holder; // 删除句柄 = 退订 w->setProperty(prop, false); w->style()->unpolish(w); w->style()->polish(w); w->update(); } QString cancelButtonStyle(int fontPx) { return QString( "QPushButton{font-size:%1px;}" "QPushButton:hover,QPushButton:pressed,QPushButton:focus" "{background-color:%2;color:#FFFFFF;border:2px solid #FFA6A9;}") .arg(fontPx).arg(C::DANGER); } // ===================== 按钮焦点(选中)光圈 ===================== ButtonFocusGlow::ButtonFocusGlow(const QColor& glow, QObject* parent) : QObject(parent), m_glow(glow) { } // 触摸屏上手指抬起后指针并不会跟着移开,Qt 会把刚点过的按钮一直留在悬停(:hover)态。 // 本项目的悬停样式与选中样式是同一套(蓝底/亮边),残留的悬停看起来就是"还选中着", // 此时再按 Tab 选中另一个按钮,屏幕上就出现了两个"选中"。选中态一律以焦点为准, // 故每次产生新的选中(点击/触摸按下、焦点移入)时,把其它按钮残留的悬停态清掉。 static void clearStaleHover(QWidget* keep) { // 用 allWidgets 而非 qApp->findChildren:顶层窗口并非 QApplication 的子对象 const QWidgetList all = QApplication::allWidgets(); for (QWidget* w : all) { if (w == keep || !w->underMouse() || !qobject_cast(w)) continue; w->setAttribute(Qt::WA_UnderMouse, false); w->update(); } } bool ButtonFocusGlow::eventFilter(QObject* obj, QEvent* ev) { QWidget* w = qobject_cast(obj); if (!w) return QObject::eventFilter(obj, ev); switch (ev->type()) { case QEvent::FocusIn: clearStaleHover(w); // 新的选中出现 → 旧按钮的悬停残影一并清掉 if (qobject_cast(w) && !w->graphicsEffect()) { QGraphicsDropShadowEffect* g = new QGraphicsDropShadowEffect(w); // 挂一圈发光(偏移 0 → 四周光晕) g->setBlurRadius(40); // 光晕大小(越大越强) g->setOffset(0, 0); g->setColor(m_glow); w->setGraphicsEffect(g); } break; case QEvent::FocusOut: if (qobject_cast(w) && w->graphicsEffect()) { // 光晕带模糊、偏移为 0,会溢出按钮矩形画到父控件上。 // 只重绘按钮自身清不掉溢出的那圈,Tab 移走焦点后仍会留下蓝色残影, // 所以把父控件整块标脏,连同按钮一起重绘。 QWidget* parent = w->parentWidget(); w->setGraphicsEffect(nullptr); // Qt 会删除旧效果 w->update(); if (parent) parent->update(parent->rect()); } break; case QEvent::EnabledChange: // 光标一直停在按钮上时,禁用期间 Qt 仍给它留着 WA_UnderMouse;重新启用的一瞬间 // :hover 规则就生效了(:disabled 排在 :hover 之后才压得住它),看着像"被选中"。 // 这里把这层残影清掉,和 FocusIn 那条是同一件事:保证同时只有一个按钮呈选中样式。 if (w->isEnabled() && w->underMouse()) clearStaleHover(QApplication::focusWidget()); break; case QEvent::MouseButtonPress: // 触摸/点击与 Tab 归一到同一个焦点:谁最后操作,谁就是唯一的选中 if (static_cast(ev)->button() == Qt::LeftButton && (w->focusPolicy() & Qt::ClickFocus)) { w->setFocus(Qt::MouseFocusReason); clearStaleHover(w); } break; default: break; } return QObject::eventFilter(obj, ev); } void installButtonFocusGlow(QApplication* app) { if (app) app->installEventFilter(new ButtonFocusGlow(QColor("#7FBBFF"), app)); // 亮蓝,深色底更醒目 } // ===================== 方向键(旋钮)隔离 ===================== ArrowKeyGuard::ArrowKeyGuard(QObject* parent) : QObject(parent) { } bool ArrowKeyGuard::eventFilter(QObject* obj, QEvent* ev) { if (ev->type() == QEvent::KeyPress) { // 只在焦点落在按钮上时拦截:否则 QAbstractButton 会拿方向键去换焦 QWidget* fw = QApplication::focusWidget(); if (fw && qobject_cast(fw)) { const int k = static_cast(ev)->key(); if (Keys::up(k) || Keys::down(k)) return true; } } return QObject::eventFilter(obj, ev); } void installArrowKeyGuard(QApplication* app) { if (app) app->installEventFilter(new ArrowKeyGuard(app)); } QString appStyleSheet() { const QString s = QString(R"QSS( * { font-family: "Microsoft YaHei", "PingFang SC", "Source Han Sans CN"; outline: none; } QWidget { background-color: BG; color: TXT; } QWidget#pageRoot { background-color: BG; } QLabel { background: transparent; color: TXT; } QLabel[sub="true"] { color: SUB; } QLabel[title="true"] { color: TXT; font-weight: 700; } /* ===== 按钮 ===== */ QPushButton { background-color: PHI; color: TXT; border: 1px solid BD; border-radius: 12px; font-size: 40px; } QPushButton:hover { background-color: PRI; color: TXT; border-color: PRI; } QPushButton:pressed { background-color: PRI; color: #FFFFFF; border-color: PRI; } QPushButton:checked { background-color: PRI; color: #FFFFFF; border: none; } QPushButton:disabled { background-color: DIS; color: DIST; border-color: DIS; } QPushButton[variant="primary"] { background-color: PRI; color: #FFFFFF; border: none; } QPushButton[variant="primary"]:disabled { background-color: DIS; color: DIST; } QPushButton[variant="danger"] { background-color: DAN; color: #FFFFFF; border: 1px solid DAN; border-radius: 6px; } QPushButton[variant="danger"]:disabled { background-color: DIS; color: DIST; border-color: DIS; } QPushButton[variant="link"] { background: transparent; color: PRI; border: none; font-size: 22px; } QPushButton[variant="estop"] { background-color: DAN; color: #FFFFFF; border: 1px solid DAN; border-radius: 6px; font-size: 24px; font-weight: 700; } /* ===== 输入框 ===== */ QLineEdit { background-color: BG; border: 2px solid BD; border-radius: 8px; color: TXT; font-size: 50px; padding: 0 12px; } QLineEdit:focus { border: 3px solid PRI; } /* ===== 进度条 ===== */ QProgressBar { background-color: PHI; border: none; border-radius: 4px; text-align: center; color: #FFFFFF; font-size: 40px; } QProgressBar::chunk { background-color: PRI; border-radius: 4px; } /* ===== 表格 ===== */ QTableWidget { background-color: PAN; color: TXT; border: 1px solid BD; gridline-color: BD; font-size: 40px; alternate-background-color: #2A3442; } QHeaderView::section { background-color: PHI; color: TXT; border: none; border-right: 1px solid BD; border-bottom: 1px solid BD; padding: 8px; font-size: 40px; } QTableWidget::item { padding: 6px; } QTableWidget::item:selected { background-color: PRI; color: #FFFFFF; } /* ===== 数字键盘 ===== */ QPushButton[numpad="true"] { background-color: PHI; color: TXT; border: 1px solid BD; border-radius: 12px; font-size: 50px; } QPushButton[numpad="true"]:pressed { background-color: PRI; color: #FFFFFF; } /* ===== 焦点样式(Tab / Shift+Tab / 光标选中:高亮按钮显色 + 边框发亮) ===== */ QPushButton:focus { background-color: PRI; color: #FFFFFF; border: 4px solid #8ABFFF; } QPushButton:focus:disabled { background-color: DIS; color: DIST; border: 1px solid DIS; } QPushButton[variant="danger"]:focus, QPushButton[variant="estop"]:focus { background-color: DAN; border: 2px solid #FFA6A9; } QPushButton[variant="link"]:focus { background: transparent; color: PRI; border: none; } QPushButton[numpad="true"]:focus { background-color: PRI; color: #FFFFFF; border: 2px solid #8ABFFF; } /* ===== 数据卡片数值 ===== */ /* 数值标签的颜色必须由这里给:卡片的行内样式只写字号/字重,不写 color。 行内样式优先级高于应用级样式表,写上 color 就会把下面的告警红 / 暗红闪一起盖掉。 本规则与 [alert] 同为单属性选择器,靠位置分胜负,故必须排在告警规则之前。 */ QLabel[cardValue="true"] { color: TXT; } /* ===== 告警文字(红色闪烁由 blink 属性驱动) ===== */ QLabel[alert="true"] { color: DAN; font-weight: 700; } QLabel[alert="true"][blink="false"] { color: rgba(229,72,77,102); } QLabel[amber="true"] { color: WAR; font-weight: 700; } /* ===== 数据卡片 ===== */ QWidget[card="true"] { background-color: PAN; border: 2px solid BD; border-radius: 12px; } QLabel[cardTitle="true"] { color: TXT; font-weight: 700; } )QSS"); // 用占位替换(注意:DIST 须在 DIS 之前替换,避免前缀误匹配) QString out = s; out.replace("DIST", C::DISABLED_T); out.replace("DIS", C::DISABLED); out.replace("BG", C::BG); out.replace("PAN", C::PANEL); out.replace("PHI", C::PANEL_HI); out.replace("BD", C::BORDER); out.replace("TXT", C::TEXT); out.replace("SUB", C::TEXT_SUB); out.replace("PRI", C::PRIMARY); out.replace("SUC", C::SUCCESS); out.replace("WAR", C::WARNING); out.replace("DAN", C::DANGER); return out; }