2026-06-26 17:55:15 +08:00

469 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "Presenter/Inc/DroneScrewCtrlPresenter.h"
#include "Presenter/Inc/ConfigManager.h"
#include "dialogalgoarg.h"
#include "dialogcamerasetting.h"
#include "resultitem.h"
#include "Version.h"
#include <QDateTime>
#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 <QDir>
#include <QTimer>
#include "DeviceStatusWidget.h"
#include "DetectLogHelper.h"
#include "AboutDialog.h"
#include "ResultListLayoutHelper.h"
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(239, 241, 245); padding: 20px; }");
QString versionStr = QString("%1").arg(DRONESCREWCTRL_FULL_VERSION_STRING);
QLabel* buildLabel = new QLabel(versionStr);
buildLabel->setStyleSheet("color: rgb(239, 241, 245); font-size: 20px; margin-right: 16px;");
buildLabel->setCursor(Qt::PointingHandCursor);
buildLabel->installEventFilter(this);
statusBar()->addPermanentWidget(buildLabel);
m_versionLabel = buildLabel;
initUi();
// 无边框 + 最大化
setWindowFlags(Qt::FramelessWindowHint);
this->showMaximized();
// 第二相机画面初始隐藏
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("无人机螺杆控制端"));
// 主画面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();
}
void MainWindow::OnRtspFrame(const QImage& frame)
{
m_currentImage = frame;
renderDisplay();
}
void MainWindow::OnDetectionResult(const CtrlDetectionFrame& result)
{
if (!ui->detect_result_list) return;
QString head = QString(QStringLiteral("帧 #%1 目标 %2"))
.arg(result.frameId)
.arg(static_cast<int>(result.boxes.size()));
QListWidgetItem* item = new QListWidgetItem(head, ui->detect_result_list);
ResultItem* w = new ResultItem();
w->setResultData(static_cast<int>(result.frameId), result);
item->setSizeHint(w->sizeHint());
ui->detect_result_list->addItem(item);
ui->detect_result_list->setItemWidget(item, w);
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("已从服务器获取参数:曝光=%.1f, 增益=%.1f, score=%.2f")
.arg(info.leftExposure)
.arg(info.leftGain)
.arg(static_cast<double>(info.algoScore)));
}
void MainWindow::OnStatusMessage(const QString& msg)
{
appendLog(msg);
}
void MainWindow::appendLog(const QString& msg)
{
if (m_logHelper)
m_logHelper->appendLog(msg);
}
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::resizeEvent(QResizeEvent* event)
{
QMainWindow::resizeEvent(event);
renderDisplay();
}
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 =
QStringLiteral("image: url(:/common/resource/start_image.png); background-color: rgb(38, 40, 47); border: none;");
static const QString kStopAll =
QStringLiteral("image: url(:/common/resource/stop_all.png); background-color: rgb(38, 40, 47); border: none;");
static const QString kStartDetect =
QStringLiteral("image: url(:/common/resource/start_detect.png); background-color: rgb(38, 40, 47); border: none;");
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)
{
if (m_pPresenter && m_pPresenter->StartLiveStream())
{
appendLog(QStringLiteral("已请求 Server 开始实时推流"));
applyUiState(UiState::Live);
}
}
else
{
if (m_pPresenter) m_pPresenter->StopAll();
appendLog(QStringLiteral("已请求 Server 停止"));
applyUiState(UiState::Idle);
}
}
void MainWindow::on_btn_detect_start_clicked()
{
// 空闲或实时显示态均可开始检测(可不传图直接检测);检测中则忽略。
// StartDetection 内部会先停实时推流,从空闲进入也安全。
if (m_uiState == UiState::Detect) return;
if (m_pPresenter && m_pPresenter->StartDetection())
{
appendLog(QStringLiteral("已请求 Server 开始检测"));
applyUiState(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();
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();
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.modelPath = m_cachedServerInfo.algoModel;
serverParams.modelType = m_cachedServerInfo.algoModelType;
dlg.SetParams(serverParams);
}
else
{
dlg.SetParams(cfg.algo);
}
if (dlg.exec() == QDialog::Accepted)
{
cfg.algo = dlg.GetParams();
ConfigManager::Instance().SetConfig(cfg);
ConfigManager::Instance().Save();
if (m_pPresenter) m_pPresenter->PushAlgoParams(cfg.algo);
appendLog(QStringLiteral("已应用算法参数"));
}
}
void MainWindow::on_btn_test_clicked()
{
// 「测试」按钮 → 单次检测请求
if (m_pPresenter && m_pPresenter->TriggerSingleDetection())
appendLog(QStringLiteral("已发送单次检测请求"));
}
// ======================== 页面布局 ========================
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);
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);
appendLog(QStringLiteral("页面布局已调整为双相机模式"));
}
// ======================== 事件过滤器 ========================
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);
}