292 lines
11 KiB
C++
292 lines
11 KiB
C++
#include "datacard.h"
|
||
#include <QVBoxLayout>
|
||
#include <QHBoxLayout>
|
||
#include <QLabel>
|
||
#include <QTimer>
|
||
#include <QFontMetrics>
|
||
#include <QEvent>
|
||
#include "global.h"
|
||
#include <QStyle>
|
||
|
||
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<QVBoxLayout*>(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<QVBoxLayout*>(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<QVBoxLayout*>(layout());
|
||
if (!lay)
|
||
return;
|
||
for (int i = 0; i < lay->count(); ++i) {
|
||
QHBoxLayout* row = qobject_cast<QHBoxLayout*>(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]=数值
|
||
}
|
||
}
|