新版布局

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
+13 -3
View File
@@ -79,13 +79,15 @@ bool ConfirmDialog::confirm(QWidget* parent, const QString& title, const QString
return dlg.exec() == QDialog::Accepted;
}
void ConfirmDialog::message(QWidget* parent, const QString& title, const QString& text)
void ConfirmDialog::message(QWidget* parent, const QString& title, const QString& text,
int width, int fontPx)
{
ConfirmDialog dlg(parent);
dlg.m_panel->setFixedSize(900, 600); // 正文行数少 → 面板收窄
dlg.m_cancel->hide(); // 只保留“确认”
dlg.m_title->setStyleSheet(QString("background:transparent;color:%1;font-size:50px;font-weight:700;").arg(C::TEXT));
dlg.m_body->setStyleSheet(QString("background:transparent;color:%1;font-size:50px;").arg(C::TEXT));
// 正文单独给字号:文案长的调用点可以调小它,让固定的字数在更窄的面板里排成预期行数
dlg.m_body->setStyleSheet(QString("background:transparent;color:%1;font-size:%2px;")
.arg(C::TEXT).arg(fontPx));
dlg.m_title->setWordWrap(true);
dlg.m_title->setContentsMargins(0, 20, 0, 0); // 标题文字距该行顶部 10px
dlg.m_body->setContentsMargins(0, 0, 0, 0);
@@ -97,6 +99,14 @@ void ConfirmDialog::message(QWidget* parent, const QString& title, const QString
}
dlg.setTitle(title);
dlg.setText(text);
// 面板宽度由调用点给定,高度按内容算:标题/正文都开了 wordWrap,宽度先定下来,
// layout 才能用 heightForWidth 量出准确高度(QLabel 的 sizeHint 对换行文本不准,
// 所以不能靠 adjustSize)。宽度以前是写死的常量、三个调用点共用,一处的长文案
// 会把别处的弹窗一起撑宽,故改成逐调用点传入。
const int kW = width;
dlg.m_panel->ensurePolished(); // 先让 QSS 的 50px 字号落到控件上,量出来的高度才对
dlg.m_panel->setFixedSize(kW, dlg.m_panel->heightForWidth(kW));
dlg.exec();
}
+6 -2
View File
@@ -20,8 +20,12 @@ public:
// 模态确认,返回是否“确认”
static bool confirm(QWidget* parent, const QString& title, const QString& text);
// 独立信息弹窗(仅“确认”按钮,替代系统 QMessageBox;正文 50px)
static void message(QWidget* parent, const QString& title, const QString& text);
// 独立信息弹窗(仅“确认”按钮,替代系统 QMessageBox)。
// width = 面板宽度,fontPx = **正文字号**(标题固定 50px);正文按宽度换行、高度按内容算。
// 两个参数都是**各调用点各给各的**——以前写死共用,一处的长文案会把别处的弹窗一起撑宽,
// 想让它窄一点就只能靠截断,现在可以靠缩字号。
static void message(QWidget* parent, const QString& title, const QString& text,
int width = 1200, int fontPx = 50);
// 轻提示:显示 text,ms 毫秒后自动消失(用于“修改成功”等)
static void toast(QWidget* parent, const QString& text, int ms = 2000);
+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)
+38 -2
View File
@@ -1,13 +1,21 @@
#ifndef DATACARD_H
#define DATACARD_H
#include <QWidget>
#include <QString>
#include <QStringList>
#include <QVector>
#include <QWidget>
class QLabel;
class QHBoxLayout;
class QTimer;
class QEvent;
// ===================== 数据卡片 =====================
// 标题栏 + 若干“标签:值”行;支持指定字段红色闪烁告警
// 标题栏 + 若干“标签:值”行 + 一条常驻的“故障”行。
// 每行有最大高度、长到上限就停,多出来的高度归末尾弹性,所以有无故障都不改变布局;
// 字段行支持红色闪烁告警;故障行没有标签、字号 kFaultFontPx,有故障时红闪、
// 多条按 1s 轮流显示,文字比整行宽时自动滑动,无故障时内容为空(行仍占位)。
class DataCard : public QWidget
{
Q_OBJECT
@@ -18,13 +26,41 @@ public:
int addField(const QString& label, const QString& value = "--", bool big = false);
void setField(int index, const QString& value);
void setAlarm(int index, bool on);
void setFaults(const QStringList& faults); // 故障行内容(空 = 清空文字,行仍占位)
// 数值行字号。要在 addField 之前调——字号是建标签时就写进行内样式表的,改不了已建的。
// big 字段不走这个值(固定 50)。
void setValueFontPx(int px) { m_valueFontPx = px; }
void setHighlight(bool on); // 主色描边突出
void indentRows(); // 字段行整体缩进到「中间偏左」(行首 : 行尾 = 2 : 3),只需调用一次
protected:
bool eventFilter(QObject* obj, QEvent* ev) override; // 盯故障数值标签的宽度变化
private slots:
void rotateFault();
void tickFaultScroll(); // 故障文字滑动一拍
private:
void ensureFaultRow();
void applyFaultText(); // 设文字、量宽度、决定要不要滑动
void updateFaultScroll(); // 放得下就停着显示,放不下才开滑动
void positionFaultLabel(); // 按当前偏移摆放故障标签
QLabel* m_title = nullptr;
QHBoxLayout* m_faultRow = nullptr; // 构造函数里就建好,永远占着最后一行
QWidget* m_faultBox = nullptr; // 裁剪容器:文字超宽时标签在这里面左移
QLabel* m_faultValue = nullptr; // 无“故障:”标签;宽度按文字自然宽度,位置手动给
QTimer* m_faultTimer = nullptr; // 多条故障 1s 轮换
QTimer* m_scrollTimer = nullptr; // 滑动节拍
QStringList m_faults; // 当前故障列表,空 = 无故障
QString m_faultText; // 当前该显示的完整文字
int m_faultIdx = 0;
int m_textW = 0; // 当前故障文字的像素宽
int m_offset = 0; // 已左移的像素
QVector<QLabel*> m_values;
QVector<QString> m_labels;
int m_valueFontPx = 42; // 数值行字号;big 字段固定 50,不受它影响
};
#endif // DATACARD_H
+16 -16
View File
@@ -26,21 +26,17 @@ LedIndicator::LedIndicator(const QString& text, QWidget* parent)
void LedIndicator::setState(State s)
{
m_state = s;
bool needBlink = (s == RedBlink || s == YellowBlink);
if (needBlink && !m_blink) {
m_blink = new QTimer(this);
m_blink->setInterval(500);
connect(m_blink, &QTimer::timeout, this, [this]() {
m_on = !m_on;
update();
});
m_blink->start();
m_on = true;
} else if (!needBlink && m_blink) {
m_blink->stop();
delete m_blink;
m_blink = nullptr;
m_on = true;
const bool needBlink = (s == RedBlink || s == YellowBlink);
if (needBlink && !m_blinkConn) {
// 挂全局节拍,而不是本控件自己起定时器:各自起的话相位互不相同,
// 同一屏上的几个指示灯(顶部通信状态、底栏、自检结果…)会各闪各的。
m_blinkConn = connect(BlinkBeat::instance(), &BlinkBeat::toggled, this,
[this](bool on) { m_on = on; update(); });
m_on = BlinkBeat::instance()->on(); // 立刻对齐当前相位,别等下一拍
} else if (!needBlink && m_blinkConn) {
disconnect(m_blinkConn);
m_blinkConn = QMetaObject::Connection();
m_on = true; // 停闪=常亮
}
update();
}
@@ -103,7 +99,11 @@ void LedIndicator::paintEvent(QPaintEvent*)
p.drawEllipse(c, m_radius, m_radius);
// 文字(圆点在文字后时,文字从控件左边缘开始)
p.setPen(QColor(C::TEXT));
QColor textColor = m_textColor.isValid() ? m_textColor : QColor(C::TEXT);
// 顶部状态条这类要“整条闪烁”的场合:文字与圆点同步降到 40% 透明度
if (m_blinkText && (m_state == RedBlink || m_state == YellowBlink) && !m_on)
textColor.setAlpha(102);
p.setPen(textColor);
const QRectF textRect(m_dotAfter ? 0 : textLeft(), 0,
m_dotAfter ? textW : width() - textLeft() - 10, height());
p.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, m_text);
+8 -2
View File
@@ -1,8 +1,9 @@
#ifndef LEDINDICATOR_H
#define LEDINDICATOR_H
#include <QColor>
#include <QMetaObject>
#include <QWidget>
#include <QTimer>
// ===================== 指示灯(自绘圆形渐变 + 外发光) =====================
// 状态:灰=未运行/关闭/正常,绿=运行/开启/到位,红=故障/液位低,
@@ -20,6 +21,9 @@ public:
void setTextPixelSize(int px) { m_textPx = px; updateGeometry(); update(); } // 文字字号
void setRadius(int r) { m_radius = r; updateGeometry(); update(); } // 圆形指示点半径
void setDotAfterText(bool after) { m_dotAfter = after; updateGeometry(); update(); } // 圆点画在文字后面
void setTextColor(const QColor& c) { m_textColor = c; update(); } // 文字颜色,默认 C::TEXT
// 文字是否随闪烁状态一起明暗(默认只闪圆点,文字常亮)
void setBlinkText(bool on) { m_blinkText = on; update(); }
State state() const { return m_state; }
QSize sizeHint() const override; // 宽度按文字实际宽度计算,避免文字被裁
@@ -32,11 +36,13 @@ private:
State m_state = Off;
QString m_text;
QTimer* m_blink = nullptr;
QMetaObject::Connection m_blinkConn; // 订阅全局闪烁节拍(见 BlinkBeat)
bool m_on = true;
int m_radius = 16; // 圆形指示点 32×32
int m_textPx = 24; // 文字字号
bool m_dotAfter = false; // false=圆点在文字前,true=圆点在文字后
QColor m_textColor; // 无效色 = 用默认 C::TEXT
bool m_blinkText = false; // true=文字随闪烁明暗,false=文字常亮
};
#endif // LEDINDICATOR_H
-39
View File
@@ -18,32 +18,8 @@ StatusStrip::StatusStrip(AppContext* ctx, QWidget* parent)
lay->setContentsMargins(36, 0, 24, 0);
lay->setSpacing(40);
// 底部不再显示底盘/液位数据(改由各页面卡片展示),只保留模式徽标 + 返回按钮
//auto makeVal = [this](const QString& label) {
// QLabel* l = new QLabel(this);
// l->setStyleSheet(QString("color:%1;font-size:40px;").arg(C::TEXT_SUB));
// l->setProperty("label", label);
// return l;
//};
//m_battery = makeVal("底盘电量");
//m_voltage = makeVal("底盘电压");
//m_levelA = makeVal("清水液位");
//m_levelFoam = makeVal("泡沫液位");
//lay->addWidget(m_battery);
//lay->addWidget(m_voltage);
//lay->addWidget(m_levelA);
//lay->addWidget(m_levelFoam);
lay->addStretch(1);
// 模式徽标(仅显示:行车/机械臂由遥控器拨钮经串口回传,界面不参与切换)
m_mode = new QLabel("行车模式", this);
m_mode->setFixedSize(260, 80);
m_mode->setAlignment(Qt::AlignCenter);
m_mode->setStyleSheet(QString("background-color:%1;color:#FFFFFF;border-radius:8px;font-size:40px;font-weight:700;")
.arg(C::SUCCESS));
lay->addWidget(m_mode);
// 返回按钮
m_back = new QPushButton("返回", this);
m_back->setFixedSize(200, 80);
@@ -52,11 +28,6 @@ StatusStrip::StatusStrip(AppContext* ctx, QWidget* parent)
lay->addWidget(m_back);
connect(m_ctx->comm(), &CarCommunication::telemetryUpdated, this, &StatusStrip::refresh);
connect(m_ctx, &AppContext::modeChanged, this, [this](Mode m) {
m_mode->setText(m == Mode::Drive ? "行车模式" : "机械臂模式");
m_mode->setStyleSheet(QString("background-color:%1;color:#FFFFFF;border-radius:8px;font-size:40px;font-weight:700;")
.arg(m == Mode::Drive ? C::SUCCESS : C::WARNING));
});
refresh();
}
@@ -70,17 +41,7 @@ void StatusStrip::setBackEnabled(bool enabled)
m_back->setEnabled(enabled);
}
void StatusStrip::setModeVisible(bool visible)
{
m_mode->setVisible(visible);
}
void StatusStrip::refresh()
{
// 底部已不显示遥测数据,保留空实现(仍连着 telemetryUpdated,方便以后加显示项)
//const Telemetry& t = m_ctx->telemetry;
//m_battery->setText(QString("底盘电量:%1%").arg(t.soc));
//m_voltage->setText(QString("底盘电压:%1V").arg(t.volt, 0, 'f', 0));
//m_levelA->setText(QString("清水液位:%1L").arg(t.levelA, 0, 'f', 0));
//m_levelFoam->setText(QString("泡沫液位:%1L").arg(t.levelFoam, 0, 'f', 0));
}
+1 -7
View File
@@ -8,7 +8,7 @@ class QPushButton;
class AppContext;
// ===================== 底部状态条 =====================
// 只保留:模式徽标(仅显示)+ 返回按钮。行车/机械臂由遥控器拨钮决定,界面不参与切换。
// 只保留「返回」按钮。行车/机械臂模式徽标已撤——底部不再展示模式。
class StatusStrip : public QWidget
{
Q_OBJECT
@@ -17,7 +17,6 @@ public:
void setBackVisible(bool visible);
void setBackEnabled(bool enabled);
void setModeVisible(bool visible); // 固定行车模式的页面(如底盘页)可隐藏模式徽标
signals:
void backClicked();
@@ -27,11 +26,6 @@ private slots:
private:
AppContext* m_ctx;
//QLabel* m_battery = nullptr; // 底部不再显示数据
//QLabel* m_voltage = nullptr;
//QLabel* m_levelA = nullptr;
//QLabel* m_levelFoam = nullptr;
QLabel* m_mode = nullptr; // 模式徽标(只显示,不参与切换)
QPushButton* m_back = nullptr;
};
+68 -26
View File
@@ -2,10 +2,22 @@
#include "ledindicator.h"
#include "appcontext.h"
#include "car.h"
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include "global.h"
namespace {
// 首页/作业选择“中央卡片区”的左边缘:主区左内边距 24 + 左列 300 + 间距 24
const int kCardLeft = 300;
// LedIndicator 的文字起点(圆点区宽度):radius(16)×2 + 左侧留白 18,见 LedIndicator::textLeft()
const int kCommTextLeft = 50;
// 顶栏左右区宽度。左区要容下最长的一条异常“气体检测仪通信异常”
// (9 个全角字 × 43px = 387):274(左侧留白) + 50 + 387 + 10 = 721;
// 上限 726——居中标题的左边缘在 750,再宽就压上去了。
const int kSideW = 726;
} // namespace
TitleBar::TitleBar(AppContext* ctx, QWidget* parent)
: QWidget(parent), m_ctx(ctx)
{
@@ -14,16 +26,18 @@ TitleBar::TitleBar(AppContext* ctx, QWidget* parent)
setStyleSheet(QString("QWidget#TitleBar{background-color:%1;border-bottom:1px solid %2;}")
.arg(C::PANEL, C::BORDER));
// 左侧区(车载端通信状态)
// 左侧区(车载端通信状态,固定一行,不轮播)
QWidget* leftBox = new QWidget(this);
QHBoxLayout* leftLay = new QHBoxLayout(leftBox);
leftLay->setContentsMargins(260, 0, 0, 0);
leftLay->setSpacing(20);
m_comm = new LedIndicator("通信正常", leftBox);
m_comm->setFixedHeight(80); // 容纳放大后的文字,避免上下裁切
m_comm->setTextPixelSize(45);
leftLay->setSpacing(0); // 只有指示器 + 弹簧,留间距只会白白吃掉文字宽度
m_comm = new LedIndicator("系统通信正常", leftBox);
// 高度沿用 LedIndicator 默认的 52:43px 字与 48px 直径的圆点外发光刚好容得下
m_comm->setTextPixelSize(45); // 与右侧“遥控器正常”同字号
m_comm->setBlinkText(true); // 异常时文字跟着圆点一起闪
m_comm->setState(LedIndicator::Green);
leftLay->addWidget(m_comm);
leftLay->setContentsMargins(kCardLeft - 24 - kCommTextLeft, 0, 0, 0);
leftLay->addWidget(m_comm, 0, Qt::AlignLeft | Qt::AlignVCenter);
leftLay->addStretch(1);
// 右侧区(遥控器状态:链路 + 电量)
@@ -37,35 +51,37 @@ TitleBar::TitleBar(AppContext* ctx, QWidget* parent)
m_rc->setTextPixelSize(45);
m_rc->setState(LedIndicator::RedBlink);
rightLay->addWidget(m_rc);
m_rcBat = new QLabel("--%", rightBox);
m_rcBat->setStyleSheet(QString("color:%1;font-size:60px;").arg(C::TEXT_SUB));
m_rcBat = new QLabel(rightBox);
m_rcBat->setAlignment(Qt::AlignCenter);
rightLay->addWidget(m_rcBat);
setRcBattery(-1);
// 左右两侧等宽 → 中间标题恰好居中于屏幕
// 左区需 260(左边距)+ 240(圆点区与留白 60 + 文字 4×45)= 500
// 右区需 285(圆点区与留白 60 + 文字 5×45)+ 30 + 130(电量)+ 20 = 465
const int sideW = 520;
leftBox->setFixedWidth(sideW);
rightBox->setFixedWidth(sideW);
// 左右两侧区等宽(宽度见 kSideW:左区要放得下最长的一条异常)
leftBox->setFixedWidth(kSideW);
rightBox->setFixedWidth(kSideW);
// 标题(居中)
m_title = new QLabel("", this);
m_title->setAlignment(Qt::AlignCenter);
m_title->setStyleSheet(QString("color:%1;font-size:60px;font-weight:700;").arg(C::TEXT));
QHBoxLayout* lay = new QHBoxLayout(this);
// 用 QGridLayout 让标题横跨整条顶栏居中,而不是夹在左右两区之间拉伸:
// 后者下标题的可用宽度 = 1920 - 24×2 - 两侧区宽×2,左区为了放下通信文字一加宽,
// 标题就被挤窄甚至裁字;横跨三列后标题始终居中于屏幕,两侧区多宽都不影响它。
QGridLayout* lay = new QGridLayout(this);
lay->setContentsMargins(24, 6, 24, 6);
lay->setSpacing(0);
lay->addWidget(leftBox);
lay->addWidget(m_title, 1);
lay->addWidget(rightBox);
lay->addWidget(leftBox, 0, 0, Qt::AlignLeft | Qt::AlignVCenter);
lay->addWidget(rightBox, 0, 2, Qt::AlignRight | Qt::AlignVCenter);
lay->addWidget(m_title, 0, 0, 1, 3, Qt::AlignCenter);
// 车载端通信状态刷新
connect(m_ctx->comm(), &CarCommunication::commOkChanged, this, [this](bool ok) {
if (ok) { m_comm->setText("通信正常"); m_comm->setState(LedIndicator::Green); }
else { m_comm->setText("通信异常"); m_comm->setState(LedIndicator::RedBlink); }
});
// 车载端通信状态:只在文案变化时由通信层通知,这里不做任何轮播。
connect(m_ctx->comm(), &CarCommunication::commTextChanged,
this, &TitleBar::onCommTextChanged);
// 每个页面各有自己的顶条实例,构造时通信层可能早就报过状态了;而 commTextChanged
// 只在内容变化时才发,所以这里必须先取一次当前值,否则新页面的顶条会一直停在构造时的初值。
updateComm();
// 遥控器链路与电量刷新
connect(m_ctx->rc(), &RcCommunication::linkChanged, this, &TitleBar::onRcLink);
@@ -73,6 +89,22 @@ TitleBar::TitleBar(AppContext* ctx, QWidget* parent)
onRcLink(m_ctx->rc()->linkOk());
}
void TitleBar::onCommTextChanged()
{
updateComm();
}
// 左区固定显示通信状态:链路断或带 All_Comm_FaultCode 就红色闪烁,其余绿灯。
// 文案由通信层渲染(Telemetry::commText),这里只决定配色。
void TitleBar::updateComm()
{
const Telemetry& t = m_ctx->telemetry;
const bool warn = !t.commOk || t.commCode != 0;
m_comm->setText(t.commText);
m_comm->setState(warn ? LedIndicator::RedBlink : LedIndicator::Green);
m_comm->setTextColor(QColor(warn ? C::DANGER : C::TEXT)); // 只有异常才用红色警示
}
void TitleBar::setTitle(const QString& title)
{
m_title->setText(title);
@@ -86,11 +118,21 @@ void TitleBar::onRcLink(bool ok)
} else {
m_rc->setText("遥控器断开");
m_rc->setState(LedIndicator::RedBlink);
m_rcBat->setText("--%");
setRcBattery(-1);
}
}
void TitleBar::setRcBattery(int battery)
{
// battery < 0 表示链路断开,无电量可显示
const bool valid = battery >= 0;
const QString color = !valid ? C::TEXT_SUB
: (battery <= 20 ? C::DANGER : C::SUCCESS);
m_rcBat->setText(valid ? QString("%1%").arg(battery) : QString("--%"));
m_rcBat->setStyleSheet(QString("color:%1;font-size:50px;").arg(color));
}
void TitleBar::onRcFrame(const RcFrame& f)
{
m_rcBat->setText(QString("%1%").arg(f.battery));
setRcBattery(f.battery);
}
+10 -1
View File
@@ -9,7 +9,9 @@ class AppContext;
class LedIndicator;
// ===================== 顶条组件(高 98px) =====================
// 左侧“车载端通信:●正常/异常”,中央页面标题,右侧“遥控器:●正常/断开 + 电量”
// 左侧“车载端通信:●系统通信正常/xx通信异常”,中央页面标题,右侧“遥控器:●正常/断开 + 电量”。
// 左区**固定**显示通信状态一行,不再轮播故障明细——各项子系统的故障明细分别进对应卡片的
// “故障”行(见 DataCard::setFaults)。文案由 CarCommunication 渲染进 Telemetry::commText。
class TitleBar : public QWidget
{
Q_OBJECT
@@ -21,8 +23,15 @@ public:
private slots:
void onRcLink(bool ok);
void onRcFrame(const RcFrame& f);
void onCommTextChanged();
private:
// 按 Telemetry 的通信状态刷新左区指示灯
void updateComm();
// 电量文字与配色:>20% 绿色,≤20% 红色,无数据时用次文字色
void setRcBattery(int battery);
AppContext* m_ctx;
LedIndicator* m_comm = nullptr;
LedIndicator* m_rc = nullptr;