Files
QT_wuhan_YT/app/global.cpp
T
2026-10-16 09:06:03 +08:00

339 lines
12 KiB
C++

#include "global.h"
#include <QStyle>
#include <QApplication>
#include <QAbstractButton>
#include <QPushButton>
#include <QEvent>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QGraphicsDropShadowEffect>
// ===================== 全局闪烁节拍 =====================
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<QObject*>(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<QObject*>(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<QAbstractButton*>(w))
continue;
w->setAttribute(Qt::WA_UnderMouse, false);
w->update();
}
}
bool ButtonFocusGlow::eventFilter(QObject* obj, QEvent* ev)
{
QWidget* w = qobject_cast<QWidget*>(obj);
if (!w)
return QObject::eventFilter(obj, ev);
switch (ev->type()) {
case QEvent::FocusIn:
clearStaleHover(w); // 新的选中出现 → 旧按钮的悬停残影一并清掉
if (qobject_cast<QPushButton*>(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<QPushButton*>(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<QMouseEvent*>(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<QAbstractButton*>(fw)) {
const int k = static_cast<QKeyEvent*>(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;
}