469 lines
16 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"
#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);
}