469 lines
16 KiB
C++
469 lines
16 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 "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);
|
||
}
|