1062 lines
38 KiB
C++
Raw Normal View History

2026-06-26 17:55:15 +08:00
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "Presenter/Inc/DroneScrewCtrlPresenter.h"
#include "Presenter/Inc/ConfigManager.h"
#include "dialogalgoarg.h"
#include "dialogcamerasetting.h"
2026-07-03 14:19:06 +08:00
#include "dialoghistoryresult.h"
2026-06-26 17:55:15 +08:00
#include "resultitem.h"
#include "Version.h"
#include <QDateTime>
2026-07-03 14:19:06 +08:00
#include <QFile>
2026-06-26 17:55:15 +08:00
#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>
2026-06-26 17:55:15 +08:00
#include <QDir>
#include <QTimer>
#include <QBrush>
#include <QFrame>
#include <QListView>
#include <QColor>
#include <QPalette>
#include <QProgressBar>
#include <QPushButton>
2026-07-03 14:19:06 +08:00
#include <QStringList>
#include <QTextStream>
#include <cmath>
2026-06-26 17:55:15 +08:00
#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);
}
2026-07-03 14:19:06 +08:00
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); }"));
}
}
}
2026-06-26 17:55:15 +08:00
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'));
2026-07-11 15:28:36 +08:00
QLabel* buildLabel = new QLabel(versionWithBuildTime, statusBar());
buildLabel->setStyleSheet("QLabel { color: rgb(221, 225, 233); font-size: 20px; background-color: transparent; }");
2026-06-26 17:55:15 +08:00
buildLabel->setCursor(Qt::PointingHandCursor);
buildLabel->installEventFilter(this);
2026-07-11 15:28:36 +08:00
buildLabel->adjustSize();
buildLabel->show();
statusBar()->setMinimumHeight(buildLabel->sizeHint().height() + 10);
2026-06-26 17:55:15 +08:00
m_versionLabel = buildLabel;
initUi();
// 无边框 + 最大化
setWindowFlags(Qt::FramelessWindowHint);
this->showMaximized();
2026-07-11 15:28:36 +08:00
updateVersionLabelAlignment();
2026-06-26 17:55:15 +08:00
// 第二相机画面初始隐藏
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)
2026-07-03 14:19:06 +08:00
{
ui->btn_test->setStyleSheet(iconButtonStyle(QStringLiteral(":/common/resource/config_data_test.png")));
2026-07-03 14:19:06 +08:00
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")));
2026-06-26 17:55:15 +08:00
// 主画面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);
}
2026-06-26 17:55:15 +08:00
}
void MainWindow::OnRtspFrame(const QImage& frame)
{
m_currentImage = frame;
renderDisplay();
}
void MainWindow::OnDetectionResult(const CtrlDetectionFrame& result)
{
2026-07-03 14:19:06 +08:00
if (result.isFinalResult)
saveFinalResultHistory(result);
2026-06-26 17:55:15 +08:00
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)
{
2026-07-03 14:19:06 +08:00
const int distanceCount = displayableDistanceCount(result.distances);
const QString head = QStringLiteral("帧#%1 测距 %2")
.arg(result.frameId)
2026-07-03 14:19:06 +08:00
.arg(distanceCount);
if (distanceCount <= 0)
{
upsertResultItem(row++, head, -1, ResultItem::DisplayMode::Distance);
}
else
{
for (size_t i = 0; i < result.distances.size(); ++i)
2026-07-03 14:19:06 +08:00
{
if (!isDistanceDisplayable(result.distances[i]))
continue;
upsertResultItem(row++, head, static_cast<int>(i), ResultItem::DisplayMode::Distance);
2026-07-03 14:19:06 +08:00
}
}
}
2026-07-02 10:57:32 +08:00
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);
}
2026-07-02 10:57:32 +08:00
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);
}
}
2026-06-26 17:55:15 +08:00
trimResultItems(row);
ui->detect_result_list->scrollToBottom();
2026-06-26 17:55:15 +08:00
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));
2026-06-26 17:55:15 +08:00
}
2026-07-03 14:19:06 +08:00
bool MainWindow::refreshServerInfoFromServer()
{
if (!m_pPresenter)
return false;
WDRemoteServerInfo info;
if (!m_pPresenter->QueryServerInfo(info))
{
appendLog(QStringLiteral("读取服务器参数失败,使用当前缓存/本地配置"));
return false;
}
onServerInfoReceived(info);
return true;
}
2026-06-26 17:55:15 +08:00
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);
2026-06-26 17:55:15 +08:00
}
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();
}
2026-07-03 14:19:06 +08:00
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;
}
2026-06-26 17:55:15 +08:00
void MainWindow::resizeEvent(QResizeEvent* event)
{
QMainWindow::resizeEvent(event);
renderDisplay();
2026-07-11 15:28:36 +08:00
updateVersionLabelAlignment();
2026-06-26 17:55:15 +08:00
}
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"));
2026-06-26 17:55:15 +08:00
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)
2026-06-26 17:55:15 +08:00
{
appendLog(QStringLiteral("已请求 Server 开始实时推流"));
applyUiState(UiState::Live);
}
else
{
appendLog(QStringLiteral("启动距离检测/拉流失败"));
applyUiState(UiState::Idle);
}
ui->btn_start->setEnabled(true);
ui->btn_detect_start->setEnabled(true);
2026-06-26 17:55:15 +08:00
}
else
{
appendLog(QStringLiteral("正在请求 Server 停止..."));
ui->btn_start->setEnabled(false);
ui->btn_detect_start->setEnabled(false);
2026-07-03 14:19:06 +08:00
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();
2026-07-03 14:19:06 +08:00
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);
2026-06-26 17:55:15 +08:00
}
}
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)
2026-06-26 17:55:15 +08:00
{
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);
2026-06-26 17:55:15 +08:00
}
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();
2026-07-03 14:19:06 +08:00
refreshServerInfoFromServer();
2026-06-26 17:55:15 +08:00
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();
2026-07-03 14:19:06 +08:00
refreshServerInfoFromServer();
2026-06-26 17:55:15 +08:00
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;
2026-07-03 14:19:06 +08:00
serverParams.expectedBoltCount = m_cachedServerInfo.algoExpectedBoltCount;
2026-06-26 17:55:15 +08:00
dlg.SetParams(serverParams);
}
else
{
dlg.SetParams(cfg.algo);
}
2026-07-01 18:16:18 +08:00
dlg.SetDisplayOptions(cfg.display);
2026-06-26 17:55:15 +08:00
if (dlg.exec() == QDialog::Accepted)
{
cfg.algo = dlg.GetParams();
2026-07-01 18:16:18 +08:00
cfg.display = dlg.GetDisplayOptions();
2026-06-26 17:55:15 +08:00
ConfigManager::Instance().SetConfig(cfg);
ConfigManager::Instance().Save();
2026-07-01 18:16:18 +08:00
m_maxResultItems = cfg.display.maxResultListItems;
if (m_pPresenter) m_pPresenter->UpdateDisplayOptions(cfg.display);
2026-06-26 17:55:15 +08:00
if (m_pPresenter) m_pPresenter->PushAlgoParams(cfg.algo);
appendLog(QStringLiteral("已应用算法参数"));
}
}
void MainWindow::on_btn_test_clicked()
{
2026-07-03 14:19:06 +08:00
DialogHistoryResult dlg(historyRootDir(), this);
dlg.exec();
2026-06-26 17:55:15 +08:00
}
// ======================== 页面布局 ========================
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);
2026-07-11 15:28:36 +08:00
updateVersionLabelAlignment();
2026-06-26 17:55:15 +08:00
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);
2026-07-11 15:28:36 +08:00
updateVersionLabelAlignment();
2026-06-26 17:55:15 +08:00
appendLog(QStringLiteral("页面布局已调整为双相机模式"));
}
2026-07-11 15:28:36 +08:00
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();
}
2026-06-26 17:55:15 +08:00
// ======================== 事件过滤器 ========================
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);
}