1062 lines
38 KiB
C++
1062 lines
38 KiB
C++
#include "mainwindow.h"
|
||
#include "ui_mainwindow.h"
|
||
|
||
#include "Presenter/Inc/DroneScrewCtrlPresenter.h"
|
||
#include "Presenter/Inc/ConfigManager.h"
|
||
#include "dialogalgoarg.h"
|
||
#include "dialogcamerasetting.h"
|
||
#include "dialoghistoryresult.h"
|
||
#include "resultitem.h"
|
||
#include "Version.h"
|
||
|
||
#include <QDateTime>
|
||
#include <QFile>
|
||
#include <QMessageBox>
|
||
#include <QResizeEvent>
|
||
#include <QPixmap>
|
||
#include <QListWidgetItem>
|
||
#include <QGraphicsView>
|
||
#include <QScreen>
|
||
#include <QApplication>
|
||
#include <QMenu>
|
||
#include <QAction>
|
||
#include <QFileDialog>
|
||
#include <QStandardPaths>
|
||
#include <QThread>
|
||
#include <QProgressDialog>
|
||
#include <QDir>
|
||
#include <QTimer>
|
||
#include <QBrush>
|
||
#include <QFrame>
|
||
#include <QListView>
|
||
#include <QColor>
|
||
#include <QPalette>
|
||
#include <QProgressBar>
|
||
#include <QPushButton>
|
||
#include <QStringList>
|
||
#include <QTextStream>
|
||
#include <cmath>
|
||
|
||
#include "DeviceStatusWidget.h"
|
||
#include "DetectLogHelper.h"
|
||
#include "AboutDialog.h"
|
||
#include "ResultListLayoutHelper.h"
|
||
#include "IVrUtils.h"
|
||
|
||
namespace
|
||
{
|
||
constexpr int kThemeWindowR = 25;
|
||
constexpr int kThemeWindowG = 26;
|
||
constexpr int kThemeWindowB = 28;
|
||
constexpr int kThemeTextR = 221;
|
||
constexpr int kThemeTextG = 225;
|
||
constexpr int kThemeTextB = 233;
|
||
|
||
const QString& panelStyle()
|
||
{
|
||
static const QString kStyle = QStringLiteral(
|
||
"background-color: rgb(37, 38, 42);"
|
||
"border: 1px solid rgb(51, 53, 60);"
|
||
"color: rgb(239, 241, 245);");
|
||
return kStyle;
|
||
}
|
||
|
||
QString iconButtonStyle(const QString& imagePath)
|
||
{
|
||
return QStringLiteral(
|
||
"QPushButton { image: url(%1); background-color: rgb(38, 40, 47); border: none; }"
|
||
"QPushButton:hover { background-color: rgb(47, 49, 56); }"
|
||
"QPushButton:pressed { background-color: rgb(31, 33, 38); }"
|
||
"QPushButton:disabled { background-color: rgb(38, 40, 47); }")
|
||
.arg(imagePath);
|
||
}
|
||
|
||
QString windowButtonStyle(const QString& imagePath)
|
||
{
|
||
return QStringLiteral(
|
||
"QPushButton { image: url(%1); background-color: transparent; border: none; }"
|
||
"QPushButton:hover { background-color: rgb(47, 49, 56); }"
|
||
"QPushButton:pressed { background-color: rgb(31, 33, 38); }")
|
||
.arg(imagePath);
|
||
}
|
||
|
||
bool isDistanceDisplayable(const CtrlDetectionDistance& distance)
|
||
{
|
||
return distance.fromId >= 0;
|
||
}
|
||
|
||
int displayableDistanceCount(const std::vector<CtrlDetectionDistance>& distances)
|
||
{
|
||
int count = 0;
|
||
for (const auto& distance : distances)
|
||
{
|
||
if (isDistanceDisplayable(distance))
|
||
++count;
|
||
}
|
||
return count;
|
||
}
|
||
|
||
bool hasValidHistoryPhysicalHeight(const CtrlDetectionBox& box)
|
||
{
|
||
return box.hasPhysicalHeight &&
|
||
std::isfinite(static_cast<double>(box.physicalHeightMm)) &&
|
||
box.physicalHeightMm > 0.0f;
|
||
}
|
||
|
||
bool isHistoryDistanceValid(const CtrlDetectionDistance& distance)
|
||
{
|
||
return distance.fromId >= 0 &&
|
||
std::isfinite(static_cast<double>(distance.distanceMm));
|
||
}
|
||
|
||
QString boolText(bool value)
|
||
{
|
||
return value ? QStringLiteral("true") : QStringLiteral("false");
|
||
}
|
||
|
||
QString finalFailureText(const CtrlDetectionFrame& frame)
|
||
{
|
||
return frame.message.isEmpty() ? QStringLiteral("检测失败") : frame.message;
|
||
}
|
||
|
||
QString physicalHeightHistoryErrorText(const CtrlDetectionFrame& frame,
|
||
const CtrlDetectionBox& box)
|
||
{
|
||
if (!frame.success)
|
||
return finalFailureText(frame);
|
||
|
||
QStringList parts;
|
||
parts << QStringLiteral("未测到物理高度");
|
||
|
||
const QString status = box.matchStatus.trimmed();
|
||
if (!status.isEmpty() && status != QStringLiteral("ok"))
|
||
parts << QStringLiteral("状态:%1").arg(status);
|
||
|
||
if (!box.hasStereoMatch)
|
||
parts << QStringLiteral("双目未匹配");
|
||
else if (!box.trusted)
|
||
parts << QStringLiteral("结果不可信");
|
||
|
||
if (box.hasPhysicalHeight)
|
||
parts << QStringLiteral("高度无效:%1 mm").arg(box.physicalHeightMm, 0, 'f', 2);
|
||
|
||
return parts.join(QStringLiteral(" "));
|
||
}
|
||
|
||
void applyPullingDialogTheme(QProgressDialog& dialog)
|
||
{
|
||
const QColor windowColor(kThemeWindowR, kThemeWindowG, kThemeWindowB);
|
||
const QColor textColor(kThemeTextR, kThemeTextG, kThemeTextB);
|
||
|
||
QPalette pal = dialog.palette();
|
||
pal.setColor(QPalette::Window, windowColor);
|
||
pal.setColor(QPalette::WindowText, textColor);
|
||
pal.setColor(QPalette::Text, textColor);
|
||
pal.setColor(QPalette::ButtonText, textColor);
|
||
dialog.setPalette(pal);
|
||
dialog.setAutoFillBackground(true);
|
||
|
||
dialog.setStyleSheet(QStringLiteral(
|
||
"QProgressDialog { background-color: rgb(25, 26, 28); color: rgb(221, 225, 233); }"
|
||
"QProgressDialog * { color: rgb(221, 225, 233); }"
|
||
"QProgressDialog QLabel { color: rgb(221, 225, 233); background-color: transparent; font-size: 18px; }"
|
||
"QProgressDialog QPushButton { background-color: rgb(47, 48, 52); color: rgb(221, 225, 233); border: 1px solid rgb(75, 78, 86); padding: 5px 16px; }"
|
||
"QProgressDialog QPushButton:hover { background-color: rgb(58, 60, 66); }"
|
||
"QProgressDialog QProgressBar { background-color: rgb(47, 48, 52); border: 1px solid rgb(75, 78, 86); color: rgb(221, 225, 233); text-align: center; }"
|
||
"QProgressDialog QProgressBar::chunk { background-color: rgb(72, 116, 184); }"));
|
||
|
||
for (QLabel* label : dialog.findChildren<QLabel*>())
|
||
{
|
||
label->setPalette(pal);
|
||
label->setStyleSheet(QStringLiteral("color: rgb(221, 225, 233); background-color: transparent; font-size: 18px;"));
|
||
}
|
||
for (QPushButton* button : dialog.findChildren<QPushButton*>())
|
||
{
|
||
button->setPalette(pal);
|
||
button->setStyleSheet(QStringLiteral(
|
||
"QPushButton { background-color: rgb(47, 48, 52); color: rgb(221, 225, 233); border: 1px solid rgb(75, 78, 86); padding: 5px 16px; }"
|
||
"QPushButton:hover { background-color: rgb(58, 60, 66); }"));
|
||
}
|
||
for (QProgressBar* bar : dialog.findChildren<QProgressBar*>())
|
||
{
|
||
bar->setPalette(pal);
|
||
bar->setStyleSheet(QStringLiteral(
|
||
"QProgressBar { background-color: rgb(47, 48, 52); border: 1px solid rgb(75, 78, 86); color: rgb(221, 225, 233); text-align: center; }"
|
||
"QProgressBar::chunk { background-color: rgb(72, 116, 184); }"));
|
||
}
|
||
}
|
||
}
|
||
|
||
MainWindow::MainWindow(QWidget* parent)
|
||
: QMainWindow(parent)
|
||
, ui(new Ui::MainWindow)
|
||
{
|
||
ui->setupUi(this);
|
||
|
||
// 设置窗口图标
|
||
#ifdef _WIN32
|
||
this->setWindowIcon(QIcon(":/dronescrewctrl/resource/logo.ico"));
|
||
#else
|
||
this->setWindowIcon(QIcon(":/dronescrewctrl/resource/logo.png"));
|
||
#endif
|
||
|
||
// 状态栏:右侧显示版本信息
|
||
QFont statusFont = statusBar()->font();
|
||
statusFont.setPointSize(12);
|
||
statusBar()->setFont(statusFont);
|
||
statusBar()->setStyleSheet("QStatusBar { color: rgb(221, 225, 233); padding: 20px; }"
|
||
"QStatusBar QLabel { color: rgb(221, 225, 233); }");
|
||
|
||
QString versionWithBuildTime = QString("%1_%2%3%4%5%6%7")
|
||
.arg(GetDroneScrewCtrlFullVersion())
|
||
.arg(YEAR)
|
||
.arg(MONTH, 2, 10, QChar('0'))
|
||
.arg(DAY, 2, 10, QChar('0'))
|
||
.arg(HOUR, 2, 10, QChar('0'))
|
||
.arg(MINUTE, 2, 10, QChar('0'))
|
||
.arg(SECOND, 2, 10, QChar('0'));
|
||
|
||
QLabel* buildLabel = new QLabel(versionWithBuildTime, statusBar());
|
||
buildLabel->setStyleSheet("QLabel { color: rgb(221, 225, 233); font-size: 20px; background-color: transparent; }");
|
||
buildLabel->setCursor(Qt::PointingHandCursor);
|
||
buildLabel->installEventFilter(this);
|
||
buildLabel->adjustSize();
|
||
buildLabel->show();
|
||
statusBar()->setMinimumHeight(buildLabel->sizeHint().height() + 10);
|
||
m_versionLabel = buildLabel;
|
||
|
||
initUi();
|
||
|
||
// 无边框 + 最大化
|
||
setWindowFlags(Qt::FramelessWindowHint);
|
||
this->showMaximized();
|
||
updateVersionLabelAlignment();
|
||
|
||
// 第二相机画面初始隐藏
|
||
ui->detect_image_2->setVisible(false);
|
||
|
||
// 初始化 detect_image_2 的 GraphicsScene
|
||
QGraphicsScene* scene2 = new QGraphicsScene(this);
|
||
ui->detect_image_2->setScene(scene2);
|
||
|
||
// 设备状态组件放入 frame_dev
|
||
m_deviceStatusWidget = new DeviceStatusWidget();
|
||
QVBoxLayout* frameDevLayout = new QVBoxLayout(ui->frame_dev);
|
||
frameDevLayout->setContentsMargins(0, 0, 0, 0);
|
||
frameDevLayout->addWidget(m_deviceStatusWidget);
|
||
|
||
// 本应用只有一个设备:隐藏机械臂、单设备占满、关闭相机切换点击
|
||
m_deviceStatusWidget->setRobotVisible(false);
|
||
m_deviceStatusWidget->setCameraCount(1);
|
||
m_deviceStatusWidget->setCameraClickable(false);
|
||
m_deviceStatusWidget->setCamera1Name(QStringLiteral("设备"));
|
||
m_deviceStatusWidget->updateCamera1Status(false);
|
||
|
||
// 日志辅助类
|
||
m_logHelper = new DetectLogHelper(ui->detect_log, this);
|
||
|
||
auto cfg = ConfigManager::Instance().GetConfig();
|
||
m_maxResultItems = cfg.display.maxResultListItems;
|
||
|
||
m_pPresenter = new DroneScrewCtrlPresenter(this);
|
||
m_pPresenter->SetStatusReceiver(this);
|
||
|
||
// 信号连接要在 InitApp 之前,避免漏掉启动时的首次连接/状态事件
|
||
connect(m_pPresenter, &DroneScrewCtrlPresenter::statusMessage,
|
||
this, [this](const QString& s) { appendLog(s); });
|
||
connect(m_pPresenter, &DroneScrewCtrlPresenter::serverConnectionChanged,
|
||
this, &MainWindow::OnServerConnectionChanged);
|
||
connect(m_pPresenter, &DroneScrewCtrlPresenter::serverInfoReceived,
|
||
this, &MainWindow::onServerInfoReceived);
|
||
|
||
m_pPresenter->ApplyConfig(cfg);
|
||
|
||
// 软件启动默认进入空闲态;不在主界面构造阶段向 Server 下发控制命令。
|
||
applyUiState(UiState::Idle);
|
||
|
||
// 网络发现放到窗口完成构造后执行,避免主界面显示前被 UDP/ZMQ 超时阻塞。
|
||
QTimer::singleShot(100, this, [this]() {
|
||
if (m_pPresenter)
|
||
m_pPresenter->InitApp();
|
||
});
|
||
}
|
||
|
||
MainWindow::~MainWindow()
|
||
{
|
||
if (m_pPresenter)
|
||
{
|
||
m_pPresenter->DeinitApp();
|
||
delete m_pPresenter;
|
||
m_pPresenter = nullptr;
|
||
}
|
||
delete ui;
|
||
}
|
||
|
||
void MainWindow::initUi()
|
||
{
|
||
setWindowTitle(QStringLiteral("无人机螺杆控制端"));
|
||
|
||
if (ui->centralwidget)
|
||
ui->centralwidget->setStyleSheet(QStringLiteral("background-color: rgb(25, 26, 28);"));
|
||
if (ui->groupBox)
|
||
ui->groupBox->setStyleSheet(QStringLiteral("background-color: rgb(38, 40, 47); border: none;"));
|
||
if (ui->label)
|
||
ui->label->setStyleSheet(QStringLiteral("color: rgb(239, 241, 245); background-color: transparent;"));
|
||
if (ui->detect_log)
|
||
ui->detect_log->setStyleSheet(panelStyle());
|
||
if (ui->detect_image)
|
||
ui->detect_image->setStyleSheet(panelStyle());
|
||
if (ui->detect_image_2)
|
||
ui->detect_image_2->setStyleSheet(panelStyle());
|
||
if (ui->frame_dev)
|
||
ui->frame_dev->setStyleSheet(QStringLiteral("background-color: transparent; border: none;"));
|
||
if (ui->btn_camera)
|
||
ui->btn_camera->setStyleSheet(iconButtonStyle(QStringLiteral(":/common/resource/config_camera.png")));
|
||
if (ui->btn_algo_config)
|
||
ui->btn_algo_config->setStyleSheet(iconButtonStyle(QStringLiteral(":/common/resource/config_algo.png")));
|
||
if (ui->btn_test)
|
||
{
|
||
ui->btn_test->setStyleSheet(iconButtonStyle(QStringLiteral(":/common/resource/config_data_test.png")));
|
||
ui->btn_test->setToolTip(QStringLiteral("历史数据"));
|
||
}
|
||
if (ui->btn_close)
|
||
ui->btn_close->setStyleSheet(windowButtonStyle(QStringLiteral(":/common/resource/close.png")));
|
||
if (ui->btn_hide)
|
||
ui->btn_hide->setStyleSheet(windowButtonStyle(QStringLiteral(":/common/resource/hide.png")));
|
||
|
||
// 主画面:QGraphicsScene + PixmapItem
|
||
if (ui->detect_image)
|
||
{
|
||
m_pScene = new QGraphicsScene(this);
|
||
m_pPixmapItem = new QGraphicsPixmapItem();
|
||
m_pScene->addItem(m_pPixmapItem);
|
||
ui->detect_image->setScene(m_pScene);
|
||
ui->detect_image->setRenderHint(QPainter::SmoothPixmapTransform);
|
||
ui->detect_image->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||
ui->detect_image->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||
}
|
||
|
||
// 隐藏工作状态标签
|
||
if (ui->label_work) ui->label_work->hide();
|
||
|
||
if (ui->detect_result_list)
|
||
{
|
||
ui->detect_result_list->setStyleSheet(QStringLiteral(
|
||
"QListWidget { background-color: rgb(37, 38, 42); border: 1px solid rgb(51, 53, 60); color: rgb(239, 241, 245); }"
|
||
"QListWidget::item { background-color: rgb(37, 38, 42); border: none; }"
|
||
"QListWidget::item:selected { background-color: rgb(37, 38, 42); }"));
|
||
ui->detect_result_list->setFrameShape(QFrame::NoFrame);
|
||
ui->detect_result_list->setAutoFillBackground(true);
|
||
ui->detect_result_list->setSpacing(0);
|
||
ui->detect_result_list->setViewMode(QListView::ListMode);
|
||
ui->detect_result_list->setResizeMode(QListView::Adjust);
|
||
ui->detect_result_list->setFlow(QListView::TopToBottom);
|
||
ui->detect_result_list->setWrapping(false);
|
||
ui->detect_result_list->setGridSize(QSize());
|
||
ui->detect_result_list->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||
ui->detect_result_list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||
}
|
||
}
|
||
|
||
void MainWindow::OnRtspFrame(const QImage& frame)
|
||
{
|
||
m_currentImage = frame;
|
||
renderDisplay();
|
||
}
|
||
|
||
void MainWindow::OnDetectionResult(const CtrlDetectionFrame& result)
|
||
{
|
||
if (result.isFinalResult)
|
||
saveFinalResultHistory(result);
|
||
|
||
if (!ui->detect_result_list) return;
|
||
|
||
auto upsertResultItem = [this, &result](int row,
|
||
const QString& head,
|
||
int targetIndex,
|
||
ResultItem::DisplayMode mode) {
|
||
QListWidgetItem* item = nullptr;
|
||
ResultItem* w = nullptr;
|
||
if (row >= 0 && row < ui->detect_result_list->count())
|
||
{
|
||
item = ui->detect_result_list->item(row);
|
||
w = qobject_cast<ResultItem*>(ui->detect_result_list->itemWidget(item));
|
||
}
|
||
if (!item)
|
||
{
|
||
item = new QListWidgetItem();
|
||
ui->detect_result_list->addItem(item);
|
||
}
|
||
item->setForeground(QBrush(Qt::white));
|
||
item->setBackground(QBrush(QColor(37, 38, 42)));
|
||
item->setData(Qt::UserRole, head);
|
||
if (!w)
|
||
{
|
||
w = new ResultItem();
|
||
ui->detect_result_list->setItemWidget(item, w);
|
||
}
|
||
w->setResultData(targetIndex, result, mode);
|
||
QSize hint = w->sizeHint();
|
||
if (hint.height() < w->minimumHeight())
|
||
hint.setHeight(w->minimumHeight());
|
||
const int listWidth = ui->detect_result_list->viewport()
|
||
? ui->detect_result_list->viewport()->width()
|
||
: ui->detect_result_list->width();
|
||
if (listWidth > 0)
|
||
hint.setWidth(listWidth);
|
||
w->setMinimumHeight(hint.height());
|
||
item->setSizeHint(hint);
|
||
};
|
||
|
||
auto trimResultItems = [this](int keepCount) {
|
||
while (ui->detect_result_list->count() > keepCount)
|
||
{
|
||
QListWidgetItem* item = ui->detect_result_list->takeItem(keepCount);
|
||
delete item;
|
||
}
|
||
};
|
||
|
||
int row = 0;
|
||
if (m_resultDisplayMode == ResultDisplayMode::Distance)
|
||
{
|
||
const int distanceCount = displayableDistanceCount(result.distances);
|
||
const QString head = QStringLiteral("帧#%1 测距 %2")
|
||
.arg(result.frameId)
|
||
.arg(distanceCount);
|
||
if (distanceCount <= 0)
|
||
{
|
||
upsertResultItem(row++, head, -1, ResultItem::DisplayMode::Distance);
|
||
}
|
||
else
|
||
{
|
||
for (size_t i = 0; i < result.distances.size(); ++i)
|
||
{
|
||
if (!isDistanceDisplayable(result.distances[i]))
|
||
continue;
|
||
upsertResultItem(row++, head, static_cast<int>(i), ResultItem::DisplayMode::Distance);
|
||
}
|
||
}
|
||
}
|
||
else if (result.boxes.empty() && result.distances.empty())
|
||
{
|
||
const QString head = QStringLiteral("帧#%1 精准目标 0")
|
||
.arg(result.frameId);
|
||
upsertResultItem(row++, head, -1, ResultItem::DisplayMode::Precision);
|
||
}
|
||
else
|
||
{
|
||
for (size_t i = 0; i < result.boxes.size(); ++i)
|
||
{
|
||
const QString head = QStringLiteral("帧#%1 目标 %2/%3")
|
||
.arg(result.frameId)
|
||
.arg(static_cast<int>(i + 1))
|
||
.arg(static_cast<int>(result.boxes.size()));
|
||
upsertResultItem(row++, head, static_cast<int>(i), ResultItem::DisplayMode::Precision);
|
||
}
|
||
for (size_t i = 0; i < result.distances.size(); ++i)
|
||
{
|
||
const QString head = QStringLiteral("帧#%1 杆间距 %2/%3")
|
||
.arg(result.frameId)
|
||
.arg(static_cast<int>(i + 1))
|
||
.arg(static_cast<int>(result.distances.size()));
|
||
upsertResultItem(row++, head, static_cast<int>(i), ResultItem::DisplayMode::PrecisionDistance);
|
||
}
|
||
}
|
||
|
||
trimResultItems(row);
|
||
ui->detect_result_list->scrollToBottom();
|
||
clampResultList();
|
||
}
|
||
|
||
void MainWindow::OnWorkStatusChanged(CtrlWorkStatus status)
|
||
{
|
||
if (!ui->label_work) return;
|
||
switch (status)
|
||
{
|
||
case CtrlWorkStatus::Idle: ui->label_work->setText(QStringLiteral("空闲")); break;
|
||
case CtrlWorkStatus::Connecting: ui->label_work->setText(QStringLiteral("连接中")); break;
|
||
case CtrlWorkStatus::Streaming: ui->label_work->setText(QStringLiteral("拉流中")); break;
|
||
case CtrlWorkStatus::Detecting: ui->label_work->setText(QStringLiteral("检测中")); break;
|
||
case CtrlWorkStatus::Error: ui->label_work->setText(QStringLiteral("错误")); break;
|
||
}
|
||
}
|
||
|
||
void MainWindow::OnServerConnectionChanged(bool connected)
|
||
{
|
||
if (ui->label_work)
|
||
ui->label_work->setText(connected ? QStringLiteral("已连接")
|
||
: QStringLiteral("未连接"));
|
||
// 单设备状态联动
|
||
if (m_deviceStatusWidget)
|
||
m_deviceStatusWidget->updateCamera1Status(connected);
|
||
}
|
||
|
||
void MainWindow::onServerInfoReceived(const WDRemoteServerInfo& info)
|
||
{
|
||
// 更新相机参数到 UI(如果有相机设置对话框,这里可以刷新)
|
||
// 更新算法参数到 dialogalgoarg(延迟到对话框打开时读取,或缓存到成员变量)
|
||
m_cachedServerInfo = info;
|
||
appendLog(QStringLiteral("已从服务器获取参数:曝光=%1, 增益=%2, score=%3")
|
||
.arg(info.leftExposure, 0, 'f', 1)
|
||
.arg(info.leftGain, 0, 'f', 1)
|
||
.arg(static_cast<double>(info.algoScore), 0, 'f', 2));
|
||
}
|
||
|
||
bool MainWindow::refreshServerInfoFromServer()
|
||
{
|
||
if (!m_pPresenter)
|
||
return false;
|
||
|
||
WDRemoteServerInfo info;
|
||
if (!m_pPresenter->QueryServerInfo(info))
|
||
{
|
||
appendLog(QStringLiteral("读取服务器参数失败,使用当前缓存/本地配置"));
|
||
return false;
|
||
}
|
||
|
||
onServerInfoReceived(info);
|
||
return true;
|
||
}
|
||
|
||
void MainWindow::OnStatusMessage(const QString& msg)
|
||
{
|
||
appendLog(msg);
|
||
}
|
||
|
||
void MainWindow::appendLog(const QString& msg)
|
||
{
|
||
if (m_logHelper)
|
||
m_logHelper->appendLog(msg);
|
||
statusBar()->showMessage(msg, 5000);
|
||
}
|
||
|
||
void MainWindow::clampResultList()
|
||
{
|
||
if (!ui->detect_result_list) return;
|
||
while (ui->detect_result_list->count() > m_maxResultItems)
|
||
{
|
||
QListWidgetItem* it = ui->detect_result_list->takeItem(0);
|
||
delete it;
|
||
}
|
||
}
|
||
|
||
void MainWindow::clearResultList()
|
||
{
|
||
if (ui->detect_result_list)
|
||
ui->detect_result_list->clear();
|
||
}
|
||
|
||
QString MainWindow::historyRootDir() const
|
||
{
|
||
QString homePath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
|
||
if (homePath.isEmpty())
|
||
homePath = QDir::homePath();
|
||
|
||
return QDir(homePath).filePath(QStringLiteral("DroneScrewCtrlApp/history"));
|
||
}
|
||
|
||
QString MainWindow::finalResultHistoryKey(const CtrlDetectionFrame& result) const
|
||
{
|
||
return QStringLiteral("%1:%2:%3")
|
||
.arg(result.saveIndex)
|
||
.arg(result.frameId)
|
||
.arg(result.timestampUs);
|
||
}
|
||
|
||
QString MainWindow::makeFinalResultHistoryText(const CtrlDetectionFrame& result,
|
||
const QDateTime& savedAt) const
|
||
{
|
||
QString text;
|
||
QTextStream out(&text);
|
||
|
||
int validHeightCount = 0;
|
||
for (const CtrlDetectionBox& box : result.boxes)
|
||
{
|
||
if (hasValidHistoryPhysicalHeight(box))
|
||
++validHeightCount;
|
||
}
|
||
|
||
int validDistanceCount = 0;
|
||
for (const CtrlDetectionDistance& distance : result.distances)
|
||
{
|
||
if (isHistoryDistanceValid(distance))
|
||
++validDistanceCount;
|
||
}
|
||
|
||
out << QStringLiteral("保存时间:") << savedAt.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz")) << "\n";
|
||
out << QStringLiteral("结果类型:final") << "\n";
|
||
out << QStringLiteral("frameId:") << result.frameId << "\n";
|
||
out << QStringLiteral("saveIndex:") << result.saveIndex << "\n";
|
||
out << QStringLiteral("timestampUs:") << result.timestampUs << "\n";
|
||
out << QStringLiteral("success:") << boolText(result.success) << "\n";
|
||
out << QStringLiteral("errorCode:") << result.errorCode << "\n";
|
||
if (!result.message.isEmpty())
|
||
out << QStringLiteral("message:") << result.message << "\n";
|
||
out << QStringLiteral("rankScore:") << QString::number(result.rankScore, 'f', 6) << "\n";
|
||
out << QStringLiteral("rankSampleCount:") << result.rankSampleCount << "\n";
|
||
out << QStringLiteral("target_count:") << static_cast<int>(result.boxes.size()) << "\n";
|
||
out << QStringLiteral("height_mm_count:") << validHeightCount << "\n";
|
||
|
||
if (result.boxes.empty())
|
||
out << QStringLiteral("height_mm_error:未测到物理高度") << "\n";
|
||
|
||
for (size_t i = 0; i < result.boxes.size(); ++i)
|
||
{
|
||
const CtrlDetectionBox& box = result.boxes[i];
|
||
const int targetIndex = static_cast<int>(i + 1);
|
||
const QString prefix = QStringLiteral("height_mm_%1").arg(targetIndex);
|
||
const QString status = box.matchStatus.trimmed().isEmpty()
|
||
? QStringLiteral("-")
|
||
: box.matchStatus.trimmed();
|
||
|
||
if (hasValidHistoryPhysicalHeight(box))
|
||
out << prefix << QStringLiteral(":") << QString::number(box.physicalHeightMm, 'f', 2) << "\n";
|
||
else
|
||
{
|
||
out << prefix << QStringLiteral(":ERROR") << "\n";
|
||
out << prefix << QStringLiteral("_error:") << physicalHeightHistoryErrorText(result, box) << "\n";
|
||
}
|
||
out << prefix << QStringLiteral("_status:") << status << "\n";
|
||
out << prefix << QStringLiteral("_trusted:") << boolText(box.trusted) << "\n";
|
||
out << prefix << QStringLiteral("_stereo_match:") << boolText(box.hasStereoMatch) << "\n";
|
||
out << prefix << QStringLiteral("_score:") << QString::number(box.score, 'f', 6) << "\n";
|
||
}
|
||
|
||
out << QStringLiteral("rod_spacing_mm_count:") << validDistanceCount << "\n";
|
||
if (validDistanceCount <= 0)
|
||
out << QStringLiteral("rod_spacing_mm_error:未测到杆间距离") << "\n";
|
||
|
||
int distanceIndex = 1;
|
||
for (const CtrlDetectionDistance& distance : result.distances)
|
||
{
|
||
if (!isHistoryDistanceValid(distance))
|
||
continue;
|
||
|
||
const QString prefix = QStringLiteral("rod_spacing_mm_%1").arg(distanceIndex);
|
||
const QString fromText = QString::number(distance.fromId + 1);
|
||
const QString toText = distance.toId >= 0
|
||
? QString::number(distance.toId + 1)
|
||
: QStringLiteral("-");
|
||
|
||
out << prefix << QStringLiteral(":") << QString::number(distance.distanceMm, 'f', 2) << "\n";
|
||
out << prefix << QStringLiteral("_from:") << fromText << "\n";
|
||
out << prefix << QStringLiteral("_to:") << toText << "\n";
|
||
out << prefix << QStringLiteral("_pair:") << fromText << QStringLiteral("-") << toText << "\n";
|
||
++distanceIndex;
|
||
}
|
||
|
||
return text;
|
||
}
|
||
|
||
bool MainWindow::saveFinalResultHistory(const CtrlDetectionFrame& result)
|
||
{
|
||
if (!result.isFinalResult)
|
||
return false;
|
||
|
||
const bool hasStableKey = (result.saveIndex > 0 || result.frameId > 0 || result.timestampUs > 0);
|
||
const QString historyKey = finalResultHistoryKey(result);
|
||
if (hasStableKey && historyKey == m_lastStoredFinalResultKey)
|
||
return true;
|
||
|
||
const QString dirPath = historyRootDir();
|
||
if (!QDir().mkpath(dirPath))
|
||
{
|
||
appendLog(QStringLiteral("创建历史结果目录失败:%1").arg(dirPath));
|
||
return false;
|
||
}
|
||
|
||
const QDateTime savedAt = QDateTime::currentDateTime();
|
||
QString fileName = savedAt.toString(QStringLiteral("yyyyMMdd_HHmmss_zzz"));
|
||
if (result.saveIndex > 0)
|
||
fileName += QStringLiteral("_save_%1").arg(result.saveIndex);
|
||
if (result.frameId > 0)
|
||
fileName += QStringLiteral("_frame_%1").arg(result.frameId);
|
||
fileName += QStringLiteral(".txt");
|
||
|
||
QFile file(QDir(dirPath).filePath(fileName));
|
||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate))
|
||
{
|
||
appendLog(QStringLiteral("保存历史结果失败:%1").arg(file.errorString()));
|
||
return false;
|
||
}
|
||
|
||
QTextStream out(&file);
|
||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||
out.setCodec("UTF-8");
|
||
#endif
|
||
out << makeFinalResultHistoryText(result, savedAt);
|
||
out.flush();
|
||
file.close();
|
||
|
||
if (hasStableKey)
|
||
m_lastStoredFinalResultKey = historyKey;
|
||
|
||
appendLog(QStringLiteral("已保存最终汇总结果:%1").arg(QDir(dirPath).filePath(fileName)));
|
||
return true;
|
||
}
|
||
|
||
void MainWindow::resizeEvent(QResizeEvent* event)
|
||
{
|
||
QMainWindow::resizeEvent(event);
|
||
renderDisplay();
|
||
updateVersionLabelAlignment();
|
||
}
|
||
|
||
void MainWindow::renderDisplay()
|
||
{
|
||
if (!ui->detect_image || !m_pPixmapItem || !m_pScene) return;
|
||
if (m_currentImage.isNull()) return;
|
||
|
||
m_pPixmapItem->setPixmap(QPixmap::fromImage(m_currentImage));
|
||
|
||
const QSize imageSize = m_currentImage.size();
|
||
const QSize viewSize = ui->detect_image->viewport()
|
||
? ui->detect_image->viewport()->size()
|
||
: ui->detect_image->size();
|
||
if (imageSize != m_lastRenderImageSize || viewSize != m_lastRenderViewSize)
|
||
{
|
||
m_pScene->setSceneRect(m_pPixmapItem->boundingRect());
|
||
ui->detect_image->fitInView(m_pPixmapItem, Qt::KeepAspectRatio);
|
||
m_lastRenderImageSize = imageSize;
|
||
m_lastRenderViewSize = viewSize;
|
||
}
|
||
}
|
||
|
||
void MainWindow::applyUiState(UiState s)
|
||
{
|
||
m_uiState = s;
|
||
|
||
// 用 image 而非 background-image:图片会缩放填满按钮内容区,
|
||
// 背景/边框与顶部工具按钮(btn_camera 等)保持一致
|
||
static const QString kStartImg = iconButtonStyle(QStringLiteral(":/common/resource/start_image.png"));
|
||
static const QString kStopAll = iconButtonStyle(QStringLiteral(":/common/resource/stop_all.png"));
|
||
static const QString kStartDetect = iconButtonStyle(QStringLiteral(":/common/resource/start_detect.png"));
|
||
|
||
switch (s)
|
||
{
|
||
case UiState::Idle:
|
||
// 空闲:主按钮=开始实时显示;检测按钮可用(支持不传图直接检测)
|
||
ui->btn_start->setStyleSheet(kStartImg);
|
||
ui->btn_start->setToolTip(QStringLiteral("开始实时显示"));
|
||
ui->btn_detect_start->setStyleSheet(kStartDetect);
|
||
ui->btn_detect_start->setToolTip(QStringLiteral("开始检测"));
|
||
ui->btn_detect_start->setVisible(true);
|
||
break;
|
||
case UiState::Live:
|
||
// 实时显示中:主按钮=停止;检测按钮可用
|
||
ui->btn_start->setStyleSheet(kStopAll);
|
||
ui->btn_start->setToolTip(QStringLiteral("停止"));
|
||
ui->btn_detect_start->setStyleSheet(kStartDetect);
|
||
ui->btn_detect_start->setToolTip(QStringLiteral("开始检测"));
|
||
ui->btn_detect_start->setVisible(true);
|
||
break;
|
||
case UiState::Detect:
|
||
// 检测中:主按钮=停止;检测按钮隐藏(避免重复触发)
|
||
ui->btn_start->setStyleSheet(kStopAll);
|
||
ui->btn_start->setToolTip(QStringLiteral("停止"));
|
||
ui->btn_detect_start->setVisible(false);
|
||
break;
|
||
}
|
||
}
|
||
|
||
void MainWindow::on_btn_start_clicked()
|
||
{
|
||
// 主开关:空闲 → 开始实时推流并拉流显示;运行中(实时/检测) → 全部停止
|
||
if (m_uiState == UiState::Idle)
|
||
{
|
||
m_resultDisplayMode = ResultDisplayMode::Distance;
|
||
clearResultList();
|
||
appendLog(QStringLiteral("正在启动距离检测并打开拉流..."));
|
||
ui->btn_start->setEnabled(false);
|
||
ui->btn_detect_start->setEnabled(false);
|
||
|
||
QProgressDialog pullingDialog(QStringLiteral("正在拉流,请稍候..."),
|
||
QString(),
|
||
0,
|
||
0,
|
||
this);
|
||
pullingDialog.setWindowTitle(QStringLiteral("提示"));
|
||
pullingDialog.setCancelButton(nullptr);
|
||
pullingDialog.setWindowModality(Qt::ApplicationModal);
|
||
pullingDialog.setMinimumDuration(0);
|
||
pullingDialog.setAutoClose(false);
|
||
pullingDialog.setAutoReset(false);
|
||
pullingDialog.setStyleSheet(QStringLiteral(
|
||
"QProgressDialog { background-color: rgb(25, 26, 28); color: rgb(221, 225, 233); }"
|
||
"QProgressDialog QLabel { color: rgb(221, 225, 233); background-color: transparent; font-size: 18px; }"
|
||
"QProgressDialog QProgressBar { background-color: rgb(47, 48, 52); border: 1px solid rgb(75, 78, 86); color: rgb(221, 225, 233); }"));
|
||
applyPullingDialogTheme(pullingDialog);
|
||
pullingDialog.show();
|
||
applyPullingDialogTheme(pullingDialog);
|
||
QApplication::processEvents();
|
||
|
||
const bool ok = m_pPresenter && m_pPresenter->StartLiveStream();
|
||
pullingDialog.close();
|
||
QApplication::processEvents();
|
||
if (ok)
|
||
{
|
||
appendLog(QStringLiteral("已请求 Server 开始实时推流"));
|
||
applyUiState(UiState::Live);
|
||
}
|
||
else
|
||
{
|
||
appendLog(QStringLiteral("启动距离检测/拉流失败"));
|
||
applyUiState(UiState::Idle);
|
||
}
|
||
ui->btn_start->setEnabled(true);
|
||
ui->btn_detect_start->setEnabled(true);
|
||
}
|
||
else
|
||
{
|
||
appendLog(QStringLiteral("正在请求 Server 停止..."));
|
||
ui->btn_start->setEnabled(false);
|
||
ui->btn_detect_start->setEnabled(false);
|
||
|
||
QProgressDialog* finalizingDialog = nullptr;
|
||
if (m_uiState == UiState::Detect)
|
||
{
|
||
finalizingDialog = new QProgressDialog(QStringLiteral("正在汇总计算..."),
|
||
QString(),
|
||
0,
|
||
0,
|
||
this);
|
||
finalizingDialog->setWindowTitle(QStringLiteral("提示"));
|
||
finalizingDialog->setCancelButton(nullptr);
|
||
finalizingDialog->setWindowModality(Qt::ApplicationModal);
|
||
finalizingDialog->setMinimumDuration(0);
|
||
finalizingDialog->setAutoClose(false);
|
||
finalizingDialog->setAutoReset(false);
|
||
applyPullingDialogTheme(*finalizingDialog);
|
||
finalizingDialog->show();
|
||
applyPullingDialogTheme(*finalizingDialog);
|
||
}
|
||
QApplication::processEvents();
|
||
const bool ok = m_pPresenter && m_pPresenter->StopAll();
|
||
if (finalizingDialog)
|
||
{
|
||
finalizingDialog->close();
|
||
delete finalizingDialog;
|
||
finalizingDialog = nullptr;
|
||
}
|
||
QApplication::processEvents();
|
||
if (ok)
|
||
{
|
||
appendLog(QStringLiteral("已请求 Server 停止"));
|
||
applyUiState(UiState::Idle);
|
||
}
|
||
else
|
||
{
|
||
appendLog(QStringLiteral("Server 停止失败,状态未切换"));
|
||
}
|
||
ui->btn_start->setEnabled(true);
|
||
ui->btn_detect_start->setEnabled(true);
|
||
}
|
||
}
|
||
|
||
void MainWindow::on_btn_detect_start_clicked()
|
||
{
|
||
// 空闲或实时显示态均可开始检测(可不传图直接检测);检测中则忽略。
|
||
// StartDetection 内部会先停实时推流,从空闲进入也安全。
|
||
if (m_uiState == UiState::Detect) return;
|
||
m_resultDisplayMode = ResultDisplayMode::Precision;
|
||
clearResultList();
|
||
appendLog(QStringLiteral("正在启动精准检测..."));
|
||
ui->btn_start->setEnabled(false);
|
||
ui->btn_detect_start->setEnabled(false);
|
||
QApplication::processEvents();
|
||
|
||
const UiState previousState = m_uiState;
|
||
const bool ok = m_pPresenter && m_pPresenter->StartDetection();
|
||
if (ok)
|
||
{
|
||
appendLog(QStringLiteral("已请求 Server 开始检测"));
|
||
applyUiState(UiState::Detect);
|
||
}
|
||
else
|
||
{
|
||
appendLog(QStringLiteral("启动精准检测失败"));
|
||
applyUiState(previousState);
|
||
}
|
||
ui->btn_start->setEnabled(true);
|
||
ui->btn_detect_start->setEnabled(m_uiState != UiState::Detect);
|
||
}
|
||
|
||
void MainWindow::on_btn_close_clicked()
|
||
{
|
||
close();
|
||
}
|
||
|
||
void MainWindow::on_btn_hide_clicked()
|
||
{
|
||
showMinimized();
|
||
}
|
||
|
||
void MainWindow::on_btn_camera_clicked()
|
||
{
|
||
// 「相机」按钮 → 相机设置:Server IP + 曝光时间(μs) + 增益
|
||
auto cfg0 = ConfigManager::Instance().GetConfig();
|
||
refreshServerInfoFromServer();
|
||
|
||
DialogCameraSetting dlg(this);
|
||
|
||
// 优先使用从 Server 读取的参数
|
||
DroneScrewCameraParams camParams = cfg0.camera;
|
||
if (m_cachedServerInfo.ok)
|
||
{
|
||
camParams.exposure = m_cachedServerInfo.leftExposure;
|
||
camParams.gain = m_cachedServerInfo.leftGain;
|
||
}
|
||
dlg.SetParams(QString::fromStdString(cfg0.server.serverIp), camParams);
|
||
|
||
// 存盘 + 下发:IP 变化才重连,曝光/增益始终实时下发
|
||
auto applyFn = [this](const QString& ip, const DroneScrewCameraParams& cam) {
|
||
auto cfg = ConfigManager::Instance().GetConfig();
|
||
const bool ipChanged = (ip.toStdString() != cfg.server.serverIp);
|
||
cfg.server.serverIp = ip.toStdString();
|
||
cfg.camera = cam;
|
||
ConfigManager::Instance().SetConfig(cfg);
|
||
ConfigManager::Instance().Save();
|
||
if (m_pPresenter)
|
||
{
|
||
if (ipChanged) m_pPresenter->ApplyConfig(cfg); // IP 改变才重建连接
|
||
m_pPresenter->SetServerExposure(cam.exposure);
|
||
m_pPresenter->SetServerGain(cam.gain);
|
||
}
|
||
appendLog(QStringLiteral("已应用相机设置: IP=%1 曝光=%2μs 增益=%3")
|
||
.arg(ip).arg(cam.exposure).arg(cam.gain));
|
||
};
|
||
|
||
// 「应用」:实时下发但不关闭对话框,便于一边看实时图一边调曝光
|
||
connect(&dlg, &DialogCameraSetting::applyRequested, this, applyFn);
|
||
|
||
if (dlg.exec() == QDialog::Accepted)
|
||
applyFn(dlg.GetServerIp(), dlg.GetCameraParams());
|
||
}
|
||
|
||
void MainWindow::on_btn_algo_config_clicked()
|
||
{
|
||
auto cfg = ConfigManager::Instance().GetConfig();
|
||
refreshServerInfoFromServer();
|
||
DialogAlgoArg dlg(this);
|
||
|
||
// 优先使用从 Server 读取的参数,没有则用本地配置
|
||
if (m_cachedServerInfo.ok)
|
||
{
|
||
DroneScrewAlgoUiParams serverParams;
|
||
serverParams.scoreThreshold = m_cachedServerInfo.algoScore;
|
||
serverParams.nmsThreshold = m_cachedServerInfo.algoNms;
|
||
serverParams.inputWidth = m_cachedServerInfo.algoWidth;
|
||
serverParams.inputHeight = m_cachedServerInfo.algoHeight;
|
||
serverParams.modelType = m_cachedServerInfo.algoModelType;
|
||
serverParams.expectedBoltCount = m_cachedServerInfo.algoExpectedBoltCount;
|
||
dlg.SetParams(serverParams);
|
||
}
|
||
else
|
||
{
|
||
dlg.SetParams(cfg.algo);
|
||
}
|
||
dlg.SetDisplayOptions(cfg.display);
|
||
|
||
if (dlg.exec() == QDialog::Accepted)
|
||
{
|
||
cfg.algo = dlg.GetParams();
|
||
cfg.display = dlg.GetDisplayOptions();
|
||
ConfigManager::Instance().SetConfig(cfg);
|
||
ConfigManager::Instance().Save();
|
||
m_maxResultItems = cfg.display.maxResultListItems;
|
||
if (m_pPresenter) m_pPresenter->UpdateDisplayOptions(cfg.display);
|
||
if (m_pPresenter) m_pPresenter->PushAlgoParams(cfg.algo);
|
||
appendLog(QStringLiteral("已应用算法参数"));
|
||
}
|
||
}
|
||
|
||
void MainWindow::on_btn_test_clicked()
|
||
{
|
||
DialogHistoryResult dlg(historyRootDir(), this);
|
||
dlg.exec();
|
||
}
|
||
|
||
// ======================== 页面布局 ========================
|
||
|
||
int MainWindow::getAvailableHeight()
|
||
{
|
||
QScreen* screen = QApplication::primaryScreen();
|
||
if (screen)
|
||
{
|
||
QRect availableGeometry = screen->availableGeometry();
|
||
return availableGeometry.height() - 30;
|
||
}
|
||
return 1050;
|
||
}
|
||
|
||
void MainWindow::adjustLayoutForCameraCount(int cameraCount)
|
||
{
|
||
if (cameraCount <= 1)
|
||
setupSingleCameraLayout();
|
||
else
|
||
setupMultiCameraLayout();
|
||
}
|
||
|
||
void MainWindow::setupSingleCameraLayout()
|
||
{
|
||
int availableHeight = getAvailableHeight();
|
||
ResultListLayoutHelper::setupSingleCameraMode(
|
||
ui->detect_result_list,
|
||
ui->detect_image, ui->detect_image_2,
|
||
ui->frame_dev, ui->detect_log,
|
||
availableHeight);
|
||
m_deviceStatusWidget->setCameraCount(1);
|
||
updateVersionLabelAlignment();
|
||
appendLog(QStringLiteral("页面布局已调整为单相机模式"));
|
||
}
|
||
|
||
void MainWindow::setupMultiCameraLayout()
|
||
{
|
||
int availableHeight = getAvailableHeight();
|
||
ResultListLayoutHelper::setupMultiCameraMode(
|
||
ui->detect_result_list,
|
||
ui->detect_image, ui->detect_image_2,
|
||
ui->frame_dev, ui->detect_log,
|
||
availableHeight);
|
||
m_deviceStatusWidget->setCameraCount(2);
|
||
updateVersionLabelAlignment();
|
||
appendLog(QStringLiteral("页面布局已调整为双相机模式"));
|
||
}
|
||
|
||
void MainWindow::updateVersionLabelAlignment()
|
||
{
|
||
if (!m_versionLabel || !ui || !ui->detect_log || !statusBar())
|
||
return;
|
||
|
||
const int targetRight = ui->detect_log->mapTo(this, QPoint(ui->detect_log->width(), 0)).x();
|
||
const int statusRight = statusBar()->mapTo(this, QPoint(statusBar()->width(), 0)).x();
|
||
int rightMargin = statusRight - targetRight;
|
||
if (rightMargin < 0)
|
||
rightMargin = 0;
|
||
|
||
const QSize hint = m_versionLabel->sizeHint();
|
||
m_versionLabel->resize(hint);
|
||
int x = statusBar()->width() - rightMargin - hint.width();
|
||
if (x < 0)
|
||
x = 0;
|
||
int y = (statusBar()->height() - hint.height()) / 2;
|
||
if (y < 0)
|
||
y = 0;
|
||
m_versionLabel->move(x, y);
|
||
m_versionLabel->raise();
|
||
}
|
||
|
||
// ======================== 事件过滤器 ========================
|
||
|
||
bool MainWindow::eventFilter(QObject* obj, QEvent* event)
|
||
{
|
||
if (obj == m_versionLabel && event->type() == QEvent::MouseButtonPress)
|
||
{
|
||
QString versionStr = m_versionLabel->text();
|
||
AboutDialog dlg(DRONESCREWCTRL_APP_NAME, versionStr, QString(), this);
|
||
dlg.exec();
|
||
return true;
|
||
}
|
||
return QMainWindow::eventFilter(obj, event);
|
||
}
|