#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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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(result.boxes.size())); QListWidgetItem* item = new QListWidgetItem(head, ui->detect_result_list); ResultItem* w = new ResultItem(); w->setResultData(static_cast(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(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); }