#include "datacard.h" #include #include #include #include #include #include #include "global.h" #include namespace { // 行高的上限。行按 stretch 长到这个高度就停,多出来的空间还给卡片末尾的弹性—— // 否则卡片一高(首页 2×2 那种高卡)就把两三行拉成稀稀拉拉一大片。 // 数值行的最大字号是 50px(行高约 60px),72 给足了余量,想调松紧改这个数。 const int kRowMaxH = 72; // 字号:数值行标签 32(数值字号随卡片走,见 DataCard::setValueFontPx) const int kLabelFontPx = 32; // 故障行单独一号,且**全应用统一**。放不下时不再截断,而是整条从右往左自动滑动 // (见 tickFaultScroll),所以字号可以比数值行小一点也依然读得全。 const int kFaultFontPx = 40; const int kFaultScrollMs = 30; // 滑动节拍 const int kFaultScrollStep = 3; // 每拍左移像素 -> 约 100px/s // 故障文字的裁剪容器至少要有一行高,否则没有布局时 QWidget::sizeHint() 是 (-1,-1), // 这一行会被压成 0 高 const int kFaultBoxMinH = kFaultFontPx + 8; // 给一行的所有控件设最大高度,从而把整行的高度卡住(QHBoxLayout 的高度取子项最大值) void capRow(QHBoxLayout* row, int h) { for (int i = 0; i < row->count(); ++i) if (QWidget* w = row->itemAt(i)->widget()) w->setMaximumHeight(h); } } // namespace DataCard::DataCard(const QString& title, QWidget* parent) : QWidget(parent) { setProperty("card", true); QVBoxLayout* lay = new QVBoxLayout(this); lay->setContentsMargins(24, 0, 24, 20); lay->setSpacing(16); // 标题栏(标题在卡片内居中,下沿一条分割线) m_title = new QLabel(title, this); m_title->setFixedHeight(60); m_title->setAlignment(Qt::AlignCenter); m_title->setProperty("cardTitle", true); m_title->setStyleSheet(QString("color:%1;font-size:42px;font-weight:700;border-bottom:1px solid %2;") .arg(C::TEXT, C::BORDER)); lay->addWidget(m_title); // 故障行:构造时就建好,永远占着最后一行——有无故障都不改变卡片布局。 ensureFaultRow(); // 末尾弹性:各行长到自己的上限后,剩下的高度全归它。 // 顺序因此固定为 [标题][数值行…][故障行][弹性],addField 插在倒数第二项之前。 lay->addStretch(1); } void DataCard::setTitle(const QString& t) { m_title->setText(t); } int DataCard::addField(const QString& label, const QString& value, bool big) { int index = m_values.size(); QHBoxLayout* row = new QHBoxLayout(); row->setSpacing(16); QLabel* lab = new QLabel(label + ":", this); lab->setProperty("sub", true); lab->setStyleSheet(QString("color:%1;font-size:%2px;").arg(C::TEXT_SUB).arg(kLabelFontPx)); // 数值标签只在行内样式里给字号/字重,颜色交给应用级样式表的 [cardValue] / [alert] 规则: // 控件自身的样式表优先级高于应用级样式表,行内一写 color,告警的红字与暗红闪烁就全被盖掉, // 属性照样翻转、界面上却看不出任何变化。AlarmBanner 就是这么写的,这里保持一致。 QLabel* val = new QLabel(value, this); val->setProperty("cardValue", true); val->setStyleSheet(QString("font-size:%1px;font-weight:700;").arg(big ? 50 : m_valueFontPx)); row->addWidget(lab); row->addWidget(val, 1); // 插到故障行之前(末尾两项固定是 [故障行][弹性]); // stretch = 1 让行在有富余高度时长到 kRowMaxH 为止,再高就不长了 capRow(row, kRowMaxH); QVBoxLayout* lay = qobject_cast(layout()); lay->insertLayout(lay->count() - 2, row, 1); m_values.append(val); m_labels.append(label); return index; } void DataCard::setField(int index, const QString& value) { if (index < 0 || index >= m_values.size()) return; m_values[index]->setText(value); } void DataCard::setAlarm(int index, bool on) { if (index < 0 || index >= m_values.size()) return; QLabel* val = m_values[index]; if (val->property("alert").toBool() == on) return; // 每次遥测刷新都会调进来,状态没变就别重算样式 val->setProperty("alert", on); if (on) startBlink(val, "blink"); else stopBlink(val, "blink"); // 置属性不会自动重算样式,不补这一次重算,红字要等下一次 blink 翻转(最多 500ms)才出现 val->style()->unpolish(val); val->style()->polish(val); val->update(); } // 故障行:构造时就建好,永远占着最后一行,高度与位置都不随有无故障变化。 // 不放“故障:”标签——那要吃掉一百多像素,卡片本来就窄;整行宽度都给文字。 // 文字比整行还宽时不截断,改成在裁剪容器里从右往左自动滑动。 void DataCard::ensureFaultRow() { if (m_faultRow) return; m_faultRow = new QHBoxLayout(); m_faultRow->setSpacing(0); // 裁剪容器:子控件超出它的范围会被裁掉,正好当滑动窗口用。 // 里面不放布局,标签的宽度和位置由 positionFaultLabel() 手动给。 m_faultBox = new QWidget(this); m_faultBox->setMinimumHeight(kFaultBoxMinH); m_faultBox->installEventFilter(this); // 宽度定了才知道要不要滑 // 与字段行同规矩:行内只写字号/字重,颜色交给应用级样式表的 [cardValue] / [alert] m_faultValue = new QLabel(m_faultBox); m_faultValue->setProperty("cardValue", true); m_faultValue->setStyleSheet(QString("font-size:%1px;font-weight:700;").arg(kFaultFontPx)); m_faultRow->addWidget(m_faultBox, 1); capRow(m_faultRow, kRowMaxH); // 与数值行同一个上限 // 无故障时用**空文字**占位,而不是 hide():隐藏的子控件会让这一项被判成“空”, // QBoxLayout 连它那份高度一起跳过,卡片照样会跳一下。 QVBoxLayout* lay = qobject_cast(layout()); if (lay) lay->addLayout(m_faultRow, 1); // 固定在数值行之后,高度同样受 kRowMaxH 限制 m_faultTimer = new QTimer(this); m_faultTimer->setInterval(1000); // 多条故障按 1s 轮流显示 connect(m_faultTimer, &QTimer::timeout, this, &DataCard::rotateFault); m_scrollTimer = new QTimer(this); m_scrollTimer->setInterval(kFaultScrollMs); connect(m_scrollTimer, &QTimer::timeout, this, &DataCard::tickFaultScroll); } // 设文字、按字体量出像素宽,再决定停着显示还是滑动 void DataCard::applyFaultText() { QFont f = m_faultValue->font(); f.setPixelSize(kFaultFontPx); // QSS 的字号未必已经反映到 QLabel::font() 上 const QFontMetrics fm(f); m_faultValue->setText(m_faultText); m_textW = m_faultText.isEmpty() ? 0 : fm.horizontalAdvance(m_faultText) + 2; m_faultValue->setFixedWidth(m_textW > 0 ? m_textW : 1); m_offset = 0; updateFaultScroll(); positionFaultLabel(); } // 放得下就停着显示;放不下才滑动。首次布局前 m_faultBox->width() 还是默认值, // 这里会先当成"放得下",等拿到真宽度后由 eventFilter 再判一次。 void DataCard::updateFaultScroll() { const bool need = m_textW > 0 && m_faultBox->width() > 0 && m_textW > m_faultBox->width(); if (need) { if (!m_scrollTimer->isActive()) m_scrollTimer->start(); } else { m_scrollTimer->stop(); m_offset = 0; } } void DataCard::positionFaultLabel() { m_faultValue->resize(m_textW > 0 ? m_textW : 1, m_faultBox->height()); m_faultValue->move(-m_offset, 0); // 负偏移 = 往左移出裁剪容器 } // 左移一拍;整条完全移出左边界后从头再来 void DataCard::tickFaultScroll() { if (m_textW <= 0) return; m_offset += kFaultScrollStep; if (m_offset > m_textW) m_offset = 0; positionFaultLabel(); } bool DataCard::eventFilter(QObject* obj, QEvent* ev) { if (obj == m_faultBox && ev->type() == QEvent::Resize) { // 真宽度下来了,这一刻才判得准要不要滑 updateFaultScroll(); positionFaultLabel(); } return QWidget::eventFilter(obj, ev); } void DataCard::setFaults(const QStringList& faults) { // 每次遥测刷新都会调进来。内容没变就直接返回:既省掉重复重算样式, // 也避免把 m_faultIdx 清零——否则多条故障永远停在第一条上。 if (faults == m_faults) return; m_faults = faults; m_faultIdx = 0; // 行本身一直占着位(见 ensureFaultRow),这里只切内容: // 无故障清空文字、有故障填内容并红闪——显隐规则与以前一致。 if (m_faults.isEmpty()) { m_faultTimer->stop(); stopBlink(m_faultValue, "blink"); m_faultValue->setProperty("alert", false); m_faultText.clear(); applyFaultText(); // 清空文字:停滑、宽度归 1,这一行仍然占位 return; } m_faultText = m_faults.at(0); applyFaultText(); m_faultValue->setProperty("alert", true); startBlink(m_faultValue, "blink"); m_faultValue->style()->unpolish(m_faultValue); m_faultValue->style()->polish(m_faultValue); m_faultValue->update(); if (m_faults.size() > 1) m_faultTimer->start(); else m_faultTimer->stop(); } void DataCard::rotateFault() { if (m_faults.size() < 2) return; m_faultIdx = (m_faultIdx + 1) % m_faults.size(); m_faultText = m_faults.at(m_faultIdx); applyFaultText(); } void DataCard::setHighlight(bool on) { if (on) setStyleSheet(QString("QWidget[card=\"true\"]{background-color:%1;border:3px solid %2;border-radius:12px;}") .arg(C::PANEL, C::PRIMARY)); else setStyleSheet(QString()); } // 字段行文字从左缘缩进到「中间偏左」:行首弹性 2 : 数值行尾弹性 3 void DataCard::indentRows() { QVBoxLayout* lay = qobject_cast(layout()); if (!lay) return; for (int i = 0; i < lay->count(); ++i) { QHBoxLayout* row = qobject_cast(lay->itemAt(i)->layout()); if (!row || row->count() != 2) // 字段行是 [标签][数值];标题与末尾 stretch 不是子布局 continue; if (row->itemAt(0)->spacerItem()) // 已缩进过,避免重复插入 continue; row->insertStretch(0, 2); row->setStretch(2, 3); // 插入后 [0]=行首弹性 [1]=标签 [2]=数值 } }