新版布局

This commit is contained in:
xu
2026-10-16 09:06:03 +08:00
parent e5efdea06e
commit 1ec93e42cb
37 changed files with 1785 additions and 977 deletions
+194 -12
View File
@@ -2,9 +2,40 @@
#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)
{
@@ -23,7 +54,11 @@ DataCard::DataCard(const QString& title, QWidget* parent)
.arg(C::TEXT, C::BORDER));
lay->addWidget(m_title);
// 字段行容器
// 故障行:构造时就建好,永远占着最后一行——有无故障都不改变卡片布局。
ensureFaultRow();
// 末尾弹性:各行长到自己的上限后,剩下的高度全归它。
// 顺序因此固定为 [标题][数值行…][故障行][弹性],addField 插在倒数第二项之前。
lay->addStretch(1);
}
@@ -41,18 +76,23 @@ int DataCard::addField(const QString& label, const QString& value, bool big)
QLabel* lab = new QLabel(label + ":", this);
lab->setProperty("sub", true);
lab->setStyleSheet(QString("color:%1;font-size:32px;").arg(C::TEXT_SUB));
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->setStyleSheet(QString("color:%1;%2").arg(C::TEXT,
big ? "font-size:50px;font-weight:700;" : "font-size:42px;font-weight:700;"));
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 之前)
// 插到故障行之前(末尾两项固定是 [故障行][弹性]);
// stretch = 1 让行在有富余高度时长到 kRowMaxH 为止,再高就不长了
capRow(row, kRowMaxH);
QVBoxLayout* lay = qobject_cast<QVBoxLayout*>(layout());
lay->insertLayout(lay->count() - 1, row);
lay->insertLayout(lay->count() - 2, row, 1);
m_values.append(val);
m_labels.append(label);
@@ -71,17 +111,159 @@ void DataCard::setAlarm(int index, bool on)
if (index < 0 || index >= m_values.size())
return;
QLabel* val = m_values[index];
if (on) {
val->setProperty("alert", true);
if (val->property("alert").toBool() == on)
return; // 每次遥测刷新都会调进来,状态没变就别重算样式
val->setProperty("alert", on);
if (on)
startBlink(val, "blink");
} else {
else
stopBlink(val, "blink");
val->setProperty("alert", false);
val->style()->unpolish(val);
val->style()->polish(val);
// 置属性不会自动重算样式,不补这一次重算,红字要等下一次 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)