YangSn 15ba37d83c feat:WorkpieceRotor
-增加了logo

  -修改了软件名称
2026-10-08 11:10:11 +08:00

1223 lines
41 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 "XyzRpyItem.h"
#include "ResultListLayoutHelper.h"
#include <QStandardItemModel>
#include <QDebug>
#include <QPainter>
#include <QBrush>
#include <QMessageBox>
#include <QFileDialog>
#include <QListWidgetItem>
#include <QtMath>
#include <QThread>
#include <QDateTime>
#include <QTextCursor>
#include <QStringListModel>
#include <QScreen>
#include <QApplication>
#include <QStatusBar>
#include "VrLog.h"
#include "VrDateUtils.h"
#include <QIcon>
#include <QMetaType>
#include <QLabel>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QMenu>
#include <QAction>
#include <QPushButton>
#include <QAbstractButton>
#include <QStandardPaths>
#include <QDir>
#include <QGraphicsView>
#include "StatorWorkpiecePositionPresenter.h"
#include <QProcess>
#include <QSettings>
#include <QFile>
#include <QFileInfo>
#include <QTextStream>
#include <QEvent>
#include <exception>
#include "AboutDialog.h"
#include "StyledMessageBox.h"
#include "Version.h"
#include "IVrUtils.h"
namespace
{
// 将算法返回的位置编号转换为结果卡片上的中文名称。
QString workpiecePositionText(int workpieceType)
{
switch (workpieceType)
{
case 0:
return QStringLiteral("中央");
case 1:
return QStringLiteral("上边");
case 2:
return QStringLiteral("下边");
case 3:
return QStringLiteral("左边");
case 4:
return QStringLiteral("右边");
case 5:
return QStringLiteral("左上角");
case 6:
return QStringLiteral("右上角");
case 7:
return QStringLiteral("左下角");
case 8:
return QStringLiteral("右下角");
case 9:
return QStringLiteral("隔板");
default:
return QStringLiteral("未知");
}
}
} // namespace
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
, m_selectedButton(nullptr)
, m_contextMenu(nullptr)
, m_saveDataAction(nullptr)
{
ui->setupUi(this);
// 设置窗口图标(Windows使用.ico格式)
#ifdef _WIN32
this->setWindowIcon(QIcon(":/common/resource/logo.ico"));
#else
this->setWindowIcon(QIcon(":/common/resource/logo.png"));
#endif
// 设置状态栏字体
QFont statusFont = statusBar()->font();
statusFont.setPointSize(12);
statusBar()->setFont(statusFont);
// 设置状态栏颜色和padding
statusBar()->setStyleSheet("QStatusBar { color: rgb(239, 241, 245); padding: 20px; }");
// 设置CPU序列号显示(左下角)
setupCPUSerialDisplay();
// 在状态栏右侧添加版本信息(包含编译时间)
// 点击版本号时通过 AboutDialog 显示软件、算法和机器信息。
m_softwareVersionText = QString("%1_%2%3%4%5%6%7")
.arg(GetStatorWorkpiecePositionFullVersion())
.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'));
m_versionLabel = new QLabel(m_softwareVersionText);
m_versionLabel->setStyleSheet("color: rgb(239, 241, 245); font-size: 20px; margin-right: 16px;");
m_versionLabel->setCursor(Qt::PointingHandCursor);
m_versionLabel->installEventFilter(this);
statusBar()->addPermanentWidget(m_versionLabel);
// 隐藏标题栏
setWindowFlags(Qt::FramelessWindowHint);
// 启动后自动最大化显示
this->showMaximized();
// 初始化时隐藏label_work
ui->label_work->setVisible(false);
ui->detect_image_2->setVisible(false);
// 初始化日志辅助类
m_logHelper = new DetectLogHelper(ui->detect_log, this);
// 注册自定义类型,使其能够在信号槽中跨线程传递
qRegisterMetaType<StatorWorkpiecePositionDetectionResult>("StatorWorkpiecePositionDetectionResult");
qRegisterMetaType<WorkStatus>("WorkStatus");
qRegisterMetaType<WorkpiecePoint3D>("WorkpiecePoint3D");
qRegisterMetaType<WorkpiecePosition>("WorkpiecePosition");
// 连接工作状态更新信号槽
connect(this, &MainWindow::workStatusUpdateRequested, this, &MainWindow::updateWorkStatusLabel);
// 连接检测结果更新信号槽
connect(this, &MainWindow::detectionResultUpdateRequested, this, &MainWindow::updateDetectionResultDisplay);
// 初始化右键菜单
setupContextMenu();
// 初始化模块
Init();
}
MainWindow::~MainWindow()
{
// 启动线程持有 Presenter,必须在销毁配置、设备和界面之前回收。
if (m_startDetectionThread.joinable())
{
m_startDetectionThread.join();
}
if (m_debugDetectionThread.joinable()) {
m_debugDetectionThread.join();
}
// Dialogs retain Presenter/device pointers. Destroy them before Presenter.
delete ui_dialogCameraLevel;
ui_dialogCameraLevel = nullptr;
delete ui_dialogCamera;
ui_dialogCamera = nullptr;
delete ui_dialogConfig;
ui_dialogConfig = nullptr;
// 释放业务逻辑处理类
if (m_presenter) {
StatorWorkpiecePositionPresenter* presenter = m_presenter;
m_presenter = nullptr;
delete presenter;
}
delete ui;
}
bool MainWindow::eventFilter(QObject* obj, QEvent* event)
{
if (obj == m_versionLabel && event->type() == QEvent::MouseButtonPress) {
const QString algoVersion = m_presenter ? m_presenter->GetAlgoVersion() : QString();
AboutDialog dlg(STATORWORKPIECEPOSITION_APP_NAME,
m_softwareVersionText, algoVersion, this);
dlg.exec();
return true;
}
return QMainWindow::eventFilter(obj, event);
}
void MainWindow::updateStatusLog(const QString& message)
{
// 使用日志辅助类更新日志
if (m_logHelper) {
m_logHelper->appendLog(message);
}
}
void MainWindow::clearDetectionLog()
{
// 使用日志辅助类清空日志
if (m_logHelper) {
m_logHelper->clearLog();
}
}
void MainWindow::Init()
{
// 创建业务逻辑处理类
m_presenter = new StatorWorkpiecePositionPresenter();
// 设置状态回调接口(使用BasePresenter的模板方法)
m_presenter->SetStatusCallback<IYStatorWorkpiecePositionStatus>(this);
m_deviceStatusWidget = new DeviceStatusWidget(); //因为初始化回调的数据要存储,所以要在init前创建好
// 连接DeviceStatusWidget的相机点击信号到MainWindow的槽函数
connect(m_deviceStatusWidget, SIGNAL(cameraClicked(int)), this, SLOT(onCameraClicked(int)));
// 将设备状态widget添加到frame_dev中
QVBoxLayout* frameDevLayout = new QVBoxLayout(ui->frame_dev);
frameDevLayout->setContentsMargins(0, 0, 0, 0);
frameDevLayout->addWidget(m_deviceStatusWidget);
// 设置列表视图模式
ui->detect_result_list->setViewMode(QListView::IconMode);
ui->detect_result_list->setResizeMode(QListView::Adjust);
ui->detect_result_list->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
ui->detect_result_list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
// 初始化期间禁用所有功能按钮
setButtonsEnabled(false);
// BasePresenter::Init() already owns and joins its initialization thread.
// Calling it directly avoids a detached wrapper accessing a deleted window.
updateStatusLog(tr("正在初始化系统..."));
const int result = m_presenter->Init();
if (result != 0) {
updateStatusLog(tr("初始化失败,错误码:%1").arg(result));
}
}
void MainWindow::displayImage(const QImage& image)
{
if (image.isNull()) {
updateStatusLog(tr("图片无效"));
return;
}
ui->detect_image->setImage(image);
}
// 在第二个图像视图中显示图像
void MainWindow::displayImageInSecondView(const QImage& image)
{
if (image.isNull()) {
updateStatusLog(tr("第二相机图片无效"));
return;
}
ui->detect_image_2->setImage(image);
}
// 添加扩展版本的检测结果函数
void MainWindow::addDetectionResult(const StatorWorkpiecePositionDetectionResult& result)
{
// 清空之前的所有检测结果数据
ui->detect_result_list->clear();
// 设置item间距和流向(gridSize现在在布局调整函数中动态设置)
int itemSpacing = 0;
ui->detect_result_list->setSpacing(itemSpacing); // 设置item间距为0px
// 获取当前的gridSize(由布局调整函数设置)
QSize currentGridSize = ui->detect_result_list->gridSize();
int gridWidth = currentGridSize.width();
int gridHeight = currentGridSize.height();
// 显示每个工件的中心点坐标
for (size_t i = 0; i < result.positions.size(); i++) {
XyzRpyItem* resultWidget = new XyzRpyItem();
resultWidget->setAutoFillBackground(true);
// 设置中心点数据
const WorkpiecePosition& workpiece = result.positions[i];
resultWidget->setResultData(
workpiece.workpieceId,
QStringLiteral("工件"),
workpiecePositionText(workpiece.workpieceType),
static_cast<const PositionData<double>&>(workpiece),
i == 0);
// 创建QListWidgetItem
QListWidgetItem* item = new QListWidgetItem();
item->setBackground(QBrush(Qt::transparent));
item->setSizeHint(QSize(gridWidth, gridHeight));
item->setFlags(item->flags() & ~Qt::ItemIsDragEnabled & ~Qt::ItemIsDropEnabled);
ui->detect_result_list->addItem(item);
ui->detect_result_list->setItemWidget(item, resultWidget);
}
}
// 状态更新槽函数
void MainWindow::OnStatusUpdate(const std::string& statusMessage)
{
LOG_DEBUG("[UI Display] Status update: %s\n", statusMessage.c_str());
const QString message = QString::fromStdString(statusMessage);
QMetaObject::invokeMethod(this, [this, message]() {
updateStatusLog(message);
}, Qt::QueuedConnection);
}
void MainWindow::OnDetectionResult(const StatorWorkpiecePositionDetectionResult& result)
{
// 通过信号槽机制更新UI(确保在主线程中执行)
emit detectionResultUpdateRequested(result);
}
void MainWindow::OnCamera1StatusChanged(bool isConnected)
{
QMetaObject::invokeMethod(this, [this, isConnected]() {
if (m_deviceStatusWidget) {
m_deviceStatusWidget->updateCamera1Status(isConnected);
}
}, Qt::QueuedConnection);
}
// 相机2状态更新槽函数
void MainWindow::OnCamera2StatusChanged(bool isConnected)
{
QMetaObject::invokeMethod(this, [this, isConnected]() {
if (m_deviceStatusWidget) {
m_deviceStatusWidget->updateCamera2Status(isConnected);
}
}, Qt::QueuedConnection);
}
// 相机个数更新槽函数
void MainWindow::OnCameraCountChanged(int cameraCount)
{
QMetaObject::invokeMethod(this, [this, cameraCount]() {
// 设置设备状态widget中的相机数量
if (m_deviceStatusWidget) {
m_deviceStatusWidget->setCameraCount(cameraCount);
// 设置相机名称(从配置文件中读取)
if (m_presenter) {
const auto& cameraList = m_presenter->GetCameraList();
if (cameraList.size() >= 1) {
QString camera1Name = QString::fromStdString(cameraList[0].first);
m_deviceStatusWidget->setCamera1Name(camera1Name);
}
if (cameraList.size() >= 2) {
QString camera2Name = QString::fromStdString(cameraList[1].first);
m_deviceStatusWidget->setCamera2Name(camera2Name);
}
}
}
// 根据相机个数调整页面布局
adjustLayoutForCameraCount(cameraCount);
updateStatusLog(cameraCount < 2 ? tr("单相机模式") : tr("双相机模式"));
}, Qt::QueuedConnection);
}
// 机械臂状态更新槽函数
void MainWindow::OnRobotConnectionChanged(bool isConnected)
{
QMetaObject::invokeMethod(this, [this, isConnected]() {
if (m_deviceStatusWidget) {
m_deviceStatusWidget->updateRobotStatus(isConnected);
}
}, Qt::QueuedConnection);
}
// 串口连接状态更新槽函数
void MainWindow::OnSerialConnectionChanged(bool isConnected)
{
QMetaObject::invokeMethod(this, [this, isConnected]() {
if (m_deviceStatusWidget) {
m_deviceStatusWidget->updateSerialStatus(isConnected);
}
}, Qt::QueuedConnection);
}
// 工作状态更新槽函数
void MainWindow::OnWorkStatusChanged(WorkStatus status)
{
// 通过信号槽机制更新UI(确保在主线程中执行)
emit workStatusUpdateRequested(status);
}
void MainWindow::updateWorkStatusLabel(WorkStatus status)
{
const bool statusChanged = m_workStatus != status;
// 如果状态变为Working,清空检测日志(表示开始新的检测)
if (status == WorkStatus::Working && m_workStatus != WorkStatus::Working)
{
clearDetectionLog();
}
m_workStatus = status;
// 获取状态对应的显示文本
QString statusText = QString::fromStdString(WorkStatusToString(status));
// 在label_work中显示状态
if (!ui->label_work) return;
ui->label_work->setText(statusText);
statusText = "【工作状态】" + statusText;
if (statusChanged)
{
updateStatusLog(statusText);
}
// 根据不同状态设置不同的样式和按钮启用状态
switch (status) {
case WorkStatus::Ready:
ui->label_work->setStyleSheet("color: green;");
setButtonsEnabled(true); // 就绪状态下启用所有按钮
break;
case WorkStatus::InitIng:
ui->label_work->setStyleSheet("color: blue;");
setButtonsEnabled(false); // 初始化期间禁用按钮
break;
case WorkStatus::Working:
ui->label_work->setStyleSheet("color: blue;");
setButtonsEnabled(false); // 工作期间禁用按钮
break;
case WorkStatus::Completed:
ui->label_work->setStyleSheet("color: green; font-weight: bold;");
setButtonsEnabled(true); // 完成后启用按钮
break;
case WorkStatus::Error:
ui->label_work->setStyleSheet("color: red; font-weight: bold;");
setButtonsEnabled(true); // 错误状态下仍可操作
break;
default:
ui->label_work->setStyleSheet("");
setButtonsEnabled(false); // 未知状态禁用按钮
break;
}
}
void MainWindow::updateDetectionResultDisplay(const StatorWorkpiecePositionDetectionResult& result)
{
if (result.hasBinInfo) {
updateStatusLog(tr("料框定位完成 L=%1 W=%2 TopZ=%3")
.arg(result.binInfo.length, 0, 'f', 1)
.arg(result.binInfo.width, 0, 'f', 1)
.arg(result.binInfo.binTopZ, 0, 'f', 1));
}
// 根据相机索引决定显示在哪个图像视图上
if (!result.image.isNull()) {
if (result.cameraIndex == 2) {
// 第二个相机的结果显示在detect_image_2
displayImageInSecondView(result.image);
} else {
// 第一个相机或默认显示在detect_image
displayImage(result.image);
}
}
// 2D模型检测完成后会先推送图像结果,此时没有3D位姿,不刷新结果列表
if (!result.positions.empty()) {
addDetectionResult(result);
} else if (result.errorCode != 0) {
ui->detect_result_list->clear();
}
}
void MainWindow::on_btn_start_clicked()
{
if (!m_presenter)
{
updateStatusLog(tr("系统未初始化,请等待初始化完成"));
return;
}
if (m_startDetectionPending || m_presenter->IsDetectionCycleActive())
{
updateStatusLog(tr("检测正在进行,请勿重复启动"));
return;
}
const WorkStatus previousStatus = m_workStatus;
m_startDetectionPending = true;
updateWorkStatusLabel(WorkStatus::Working);
updateStatusLog(tr("开始工件检测,正在准备二维采集..."));
// 取图、二维推理、保存调试图和启动扫描均在后台执行,UI 只处理排队的通知
try
{
m_startDetectionThread = std::thread([this]()
{
QString errorMessage;
try
{
const int result = m_presenter->StartWorkpieceDetection(-1, false);
if (result != 0)
{
errorMessage = tr("启动检测失败,错误码:%1").arg(result);
}
}
catch (const std::exception& exception)
{
errorMessage = tr("启动检测异常:%1").arg(QString::fromUtf8(exception.what()));
m_presenter->StopDetection();
}
catch (...)
{
errorMessage = tr("启动检测发生未知异常");
m_presenter->StopDetection();
}
QMetaObject::invokeMethod(this, [this, errorMessage]()
{
// 耗时操作已经结束,回收后才允许下一次按钮启动复用线程对象
m_startDetectionThread.join();
m_startDetectionPending = false;
if (!errorMessage.isEmpty())
{
updateStatusLog(errorMessage);
if (!m_presenter->IsDetectionCycleActive())
{
updateWorkStatusLabel(WorkStatus::Error);
}
}
setButtonsEnabled(m_workStatus == WorkStatus::Ready ||
m_workStatus == WorkStatus::Completed || m_workStatus == WorkStatus::Error);
}, Qt::QueuedConnection);
});
}
catch (const std::exception& exception)
{
m_startDetectionPending = false;
updateWorkStatusLabel(previousStatus);
updateStatusLog(tr("无法创建检测线程:%1").arg(QString::fromUtf8(exception.what())));
}
}
void MainWindow::on_btn_stop_clicked()
{
if (m_startDetectionPending)
{
updateStatusLog(tr("正在准备检测,请等待启动完成。"));
return;
}
// 检查Presenter是否已初始化
if (!m_presenter) {
updateStatusLog(tr("系统未初始化,请等待初始化完成。"));
return;
}
m_presenter->StopDetection();
}
void MainWindow::on_btn_camera_clicked()
{
// 检查是否有其他按钮已被选中
if (m_selectedButton != nullptr && m_selectedButton != ui->btn_camera) {
updateStatusLog(tr("请先关闭当前打开的配置窗口。"));
return;
}
// 检查Presenter是否已初始化
if (!m_presenter) {
updateStatusLog(tr("系统未初始化,请等待初始化完成。"));
return;
}
// 设置当前按钮为选中状态
setButtonSelectedState(ui->btn_camera, true);
// 使用新的相机列表获取功能
// 如果对话框不存在,创建新的CommonDialogCamera
if (nullptr == ui_dialogCamera) {
ui_dialogCamera = new CommonDialogCamera(m_presenter->GetCameraList(), this);
// 连接对话框关闭信号
connect(ui_dialogCamera, &QDialog::finished, this, [this]() {
setButtonSelectedState(ui->btn_camera, false);
});
}
ui_dialogCamera->show();
}
void MainWindow::on_btn_algo_config_clicked()
{
// 检查是否有其他按钮已被选中
if (m_selectedButton != nullptr && m_selectedButton != ui->btn_algo_config) {
updateStatusLog(tr("请先关闭当前打开的配置窗口。"));
return;
}
// 检查Presenter是否已初始化
if (!m_presenter) {
updateStatusLog(tr("系统未初始化,请等待初始化完成。"));
return;
}
// 设置当前按钮为选中状态
setButtonSelectedState(ui->btn_algo_config, true);
// 获取配置对象
ConfigManager* config = m_presenter->GetConfigManager();
if (!config) {
// 恢复按钮状态
setButtonSelectedState(ui->btn_algo_config, false);
StyledMessageBox::warning(this, tr("错误"), tr("配置模块未正确初始化!"));
return;
}
if(nullptr == ui_dialogConfig){
try {
ui_dialogConfig = new DialogAlgoarg(config, this);
// 连接对话框关闭信号
connect(ui_dialogConfig, &QDialog::finished, this, [this]() {
setButtonSelectedState(ui->btn_algo_config, false);
});
} catch (const std::exception& e) {
// 恢复按钮状态
setButtonSelectedState(ui->btn_algo_config, false);
// 删除可能已创建的对话框
if (ui_dialogConfig) {
delete ui_dialogConfig;
ui_dialogConfig = nullptr;
}
StyledMessageBox::critical(this, tr("错误"),
QString("创建配置对话框失败: %1").arg(e.what()));
return;
} catch (...) {
// 恢复按钮状态
setButtonSelectedState(ui->btn_algo_config, false);
// 删除可能已创建的对话框
if (ui_dialogConfig) {
delete ui_dialogConfig;
ui_dialogConfig = nullptr;
}
StyledMessageBox::critical(this, tr("错误"), tr("创建配置对话框失败: 未知错误"));
return;
}
}
ui_dialogConfig->show();
}
void MainWindow::on_btn_camera_levelling_clicked()
{
// 检查是否有其他按钮已被选中
if (m_selectedButton != nullptr && m_selectedButton != ui->btn_camera_levelling) {
updateStatusLog(tr("请先关闭当前打开的配置窗口。"));
return;
}
// 检查Presenter是否已初始化
if (!m_presenter) {
updateStatusLog(tr("系统未初始化,请等待初始化完成。"));
return;
}
// 设置当前按钮为选中状态
setButtonSelectedState(ui->btn_camera_levelling, true);
if(nullptr == ui_dialogCameraLevel){
ui_dialogCameraLevel = new CommonDialogCameraLevel(this);
// 连接对话框关闭信号
connect(ui_dialogCameraLevel, &QDialog::finished, this, [this]() {
setButtonSelectedState(ui->btn_camera_levelling, false);
});
}
// 设置相机列表、presenter、计算器和结果保存器到对话框
// m_presenter 同时实现了 ICameraLevelCalculator 和 ICameraLevelResultSaver 接口
ui_dialogCameraLevel->SetCameraList(m_presenter->GetCameraList(), m_presenter, m_presenter, m_presenter);
ui_dialogCameraLevel->show();
}
void MainWindow::on_btn_hide_clicked()
{
// 最小化窗口
this->showMinimized();
}
void MainWindow::on_btn_close_clicked()
{
// 关闭应用程序
this->close();
}
void MainWindow::on_btn_test_clicked()
{
if (m_startDetectionPending || (m_presenter && m_presenter->IsDetectionCycleActive()))
{
updateStatusLog(tr("检测正在进行,暂不能复检"));
return;
}
// 检查是否有其他按钮已被选中
if (m_selectedButton != nullptr && m_selectedButton != ui->btn_test) {
updateStatusLog(tr("请先关闭当前打开的配置窗口。"));
return;
}
// 设置当前按钮为选中状态
setButtonSelectedState(ui->btn_test, true);
clearDetectionLog();
// 数据复检固定为工件复检:直接选择二维图像和对应的三维数据。
if (m_presenter) {
const QString imageFileName = QFileDialog::getOpenFileName(
this, tr("选择复检工件二维图像"), QString(),
tr("图像文件 (*.png *.jpg *.jpeg *.bmp);;所有文件 (*.*)"));
if (imageFileName.isEmpty()) {
setButtonSelectedState(ui->btn_test, false);
updateStatusLog(tr("已取消复检图像选择"));
return;
}
const QString dataFileName = QFileDialog::getOpenFileName(
this, tr("选择复检工件三维数据"), QFileInfo(imageFileName).absolutePath(),
tr("激光数据文件 (*.txt *.dat);;所有文件 (*.*)"));
if (dataFileName.isEmpty()) {
setButtonSelectedState(ui->btn_test, false);
updateStatusLog(tr("已取消复检三维数据选择"));
return;
}
updateStatusLog(tr("开始工件数据复检:%1 / %2")
.arg(QFileInfo(imageFileName).fileName())
.arg(QFileInfo(dataFileName).fileName()));
if (m_debugDetectionThread.joinable()) {
m_debugDetectionThread.join();
}
m_debugDetectionThread = std::thread([this, imageFileName, dataFileName]() {
const int result = m_presenter->LoadDebugImageAndDataAndDetect(
imageFileName.toStdString(), dataFileName.toStdString(), false);
const QString message = result == 0
? tr("工件数据复检成功")
: tr("复检失败: %1").arg(result);
QMetaObject::invokeMethod(this, [this, message]() {
updateStatusLog(message);
setButtonSelectedState(ui->btn_test, false);
}, Qt::QueuedConnection);
});
return;
}
QMessageBox taskBox(this);
taskBox.setWindowTitle(tr("选择复检类型"));
taskBox.setText(tr("请选择本次复检类型"));
taskBox.setStyleSheet(
"QMessageBox { background-color: #25262A; color: rgb(221, 225, 233); font-size: 16pt; }"
"QMessageBox QLabel { color: rgb(221, 225, 233); background-color: transparent; font-size: 16pt; }"
"QMessageBox QPushButton { background-color: #404040; color: rgb(221, 225, 233); border: 1px solid #666666; padding: 8px 14px; font-size: 14pt; min-width: 120px; }"
"QMessageBox QPushButton:hover { background-color: #505050; }");
QPushButton* workpieceButton = taskBox.addButton(tr("工件复检"), QMessageBox::AcceptRole);
QPushButton* binButton = taskBox.addButton(tr("料框复检"), QMessageBox::ActionRole);
taskBox.addButton(tr("取消"), QMessageBox::RejectRole);
for (QAbstractButton* button : taskBox.buttons()) {
button->setStyleSheet(
"QPushButton { background-color: #404040; color: rgb(221, 225, 233); border: 1px solid #666666; padding: 8px 14px; font-size: 14pt; }"
"QPushButton:hover { background-color: #505050; }");
}
taskBox.setDefaultButton(workpieceButton);
taskBox.exec();
QAbstractButton* clickedButton = taskBox.clickedButton();
if (clickedButton != workpieceButton && clickedButton != binButton) {
setButtonSelectedState(ui->btn_test, false);
return;
}
const bool detectBin = (clickedButton == binButton);
QString imageFileName;
if (!detectBin) {
imageFileName = QFileDialog::getOpenFileName(
this,
tr("复检步骤 1/2:选择左目图像"),
QString(),
tr("图像文件 (*.png *.jpg *.jpeg *.bmp);;所有文件 (*.*)"));
if (imageFileName.isEmpty()) {
setButtonSelectedState(ui->btn_test, false);
updateStatusLog(tr("已取消复检图像选择"));
return;
}
updateStatusLog(tr("复检步骤 1/2:已选择左目图像 %1")
.arg(QFileInfo(imageFileName).fileName()));
}
const QString dataFileName = QFileDialog::getOpenFileName(
this,
detectBin ? tr("复检步骤 1/1:选择 3D 数据") : tr("复检步骤 2/2:选择对应的 3D 数据"),
detectBin ? QString() : QFileInfo(imageFileName).absolutePath(),
tr("激光数据文件 (*.txt *.dat);;所有文件 (*.*)"));
if (dataFileName.isEmpty()) {
setButtonSelectedState(ui->btn_test, false);
updateStatusLog(detectBin ? tr("已取消料框复检 3D 数据选择") : tr("已取消复检 3D 数据选择"));
return;
}
updateStatusLog(detectBin
? tr("复检步骤 1/1:已选择 3D 数据 %1").arg(QFileInfo(dataFileName).fileName())
: tr("复检步骤 2/2:已选择 3D 数据 %1").arg(QFileInfo(dataFileName).fileName()));
// 检查Presenter是否已初始化
if (!m_presenter) {
// 恢复按钮状态
setButtonSelectedState(ui->btn_test, false);
StyledMessageBox::warning(this, tr("错误"), tr("系统未正确初始化!"));
return;
}
if (m_debugDetectionThread.joinable()) {
m_debugDetectionThread.join();
}
m_debugDetectionThread = std::thread([this, imageFileName, dataFileName, detectBin]() {
const int result = m_presenter->LoadDebugImageAndDataAndDetect(
imageFileName.toStdString(), dataFileName.toStdString(), detectBin);
const QString message = result == 0
? (detectBin ? tr("料框复检成功") : tr("图像和 3D 数据完整复检成功"))
: tr("复检失败: %1").arg(result);
QMetaObject::invokeMethod(this, [this, message]() {
updateStatusLog(message);
}, Qt::QueuedConnection);
// 测试完成后恢复按钮状态
QMetaObject::invokeMethod(this, [this]() {
setButtonSelectedState(ui->btn_test, false);
}, Qt::QueuedConnection);
});
}
// 设置按钮启用/禁用状态
void MainWindow::setButtonsEnabled(bool enabled)
{
enabled = enabled && !m_startDetectionPending;
// 功能按钮
if (ui->btn_start) ui->btn_start->setEnabled(enabled);
if (ui->btn_stop) ui->btn_stop->setEnabled(enabled);
if (ui->btn_camera) ui->btn_camera->setEnabled(enabled);
if (ui->btn_algo_config) ui->btn_algo_config->setEnabled(enabled);
if (ui->btn_camera_levelling) ui->btn_camera_levelling->setEnabled(enabled);
if (ui->btn_test)
{
ui->btn_test->setEnabled(enabled);
}
// 窗口控制按钮(最小化和关闭)始终可用
// if (ui->btn_hide) ui->btn_hide->setEnabled(true);
// if (ui->btn_close) ui->btn_close->setEnabled(true);
}
// 设置按钮图像
void MainWindow::setButtonImage(QPushButton* button, const QString& imagePath)
{
if (button) {
QString styleSheet = QString("image: url(%1);background-color: rgb(38, 40, 47);border: none;").arg(imagePath);
button->setStyleSheet(styleSheet);
}
}
// 设置按钮选中状态
void MainWindow::setButtonSelectedState(QPushButton* button, bool selected)
{
if (!button) return;
QString normalImage, selectedImage;
// 根据按钮确定对应的图像路径
if (button == ui->btn_camera) {
normalImage = ":/common/resource/config_camera.png";
selectedImage = ":/common/resource/config_camera_s.png";
} else if (button == ui->btn_algo_config) {
normalImage = ":/common/resource/config_algo.png";
selectedImage = ":/common/resource/config_algo_s.png";
} else if (button == ui->btn_camera_levelling) {
normalImage = ":/common/resource/config_camera_level.png";
selectedImage = ":/common/resource/config_camera_level_s.png";
} else if (button == ui->btn_test) {
normalImage = ":/common/resource/config_data_test.png";
selectedImage = ":/common/resource/config_data_test_s.png";
}
// 设置对应的图像
if (selected) {
setButtonImage(button, selectedImage);
m_selectedButton = button;
// 禁用其他按钮
setOtherButtonsEnabled(button, false);
} else {
setButtonImage(button, normalImage);
if (m_selectedButton == button) {
m_selectedButton = nullptr;
// 启用所有按钮
setOtherButtonsEnabled(nullptr, true);
}
}
}
// 恢复所有按钮状态
void MainWindow::restoreAllButtonStates()
{
setButtonSelectedState(ui->btn_camera, false);
setButtonSelectedState(ui->btn_algo_config, false);
setButtonSelectedState(ui->btn_camera_levelling, false);
setButtonSelectedState(ui->btn_test, false);
}
// 设置其他按钮的启用状态(用于互斥控制)
void MainWindow::setOtherButtonsEnabled(QPushButton* exceptButton, bool enabled)
{
enabled = enabled && !m_startDetectionPending && m_workStatus != WorkStatus::Working;
QList<QPushButton*> configButtons = {
ui->btn_camera,
ui->btn_algo_config,
ui->btn_camera_levelling,
ui->btn_test
};
for (QPushButton* button : configButtons) {
if (button && button != exceptButton) {
button->setEnabled(enabled);
}
}
}
// 根据相机个数调整页面布局
void MainWindow::adjustLayoutForCameraCount(int cameraCount)
{
if (cameraCount <= 1) {
// 单相机模式布局
setupSingleCameraLayout();
} else {
// 多相机模式布局
setupMultiCameraLayout();
}
}
// 获取可用高度(屏幕高度 - 任务栏 - 边距)
int MainWindow::getAvailableHeight()
{
// 获取主屏幕的可用几何区域(自动排除任务栏)
QScreen* screen = QApplication::primaryScreen();
if (screen) {
QRect availableGeometry = screen->availableGeometry();
// 额外预留20px底部边距
return availableGeometry.height() - 30;
}
// 如果无法获取屏幕信息,返回默认值
return 1050; // 默认值(1080 - 任务栏约30px - 20px边距)
}
// 设置单相机模式布局
void MainWindow::setupSingleCameraLayout()
{
// 获取可用高度(已排除任务栏和20px边距)
int availableHeight = getAvailableHeight();
ResultListLayoutHelper::setupSingleCameraMode(
ui->detect_result_list,
ui->detect_image, ui->detect_image_2,
ui->frame_dev, ui->detect_log,
availableHeight);
// 恢复设备状态widget的原始布局
m_deviceStatusWidget->setCameraCount(1);
}
// 设置多相机模式布局
void MainWindow::setupMultiCameraLayout()
{
// 获取可用高度(已排除任务栏和20px边距)
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);
}
// 处理相机点击事件
void MainWindow::onCameraClicked(int cameraIndex)
{
if (m_startDetectionPending || (m_presenter && m_presenter->IsDetectionCycleActive()))
{
updateStatusLog(tr("检测进行中,暂不能切换相机"));
return;
}
if (m_presenter) {
if (!m_presenter->SelectCamera(cameraIndex)) {
updateStatusLog(tr("检测进行中,暂不能切换相机"));
}
}
}
// 设置右键菜单
void MainWindow::setupContextMenu()
{
// 创建右键菜单
m_contextMenu = new QMenu(this);
// 创建保存数据动作
m_saveDataAction = new QAction(tr("保存检测数据"), this);
m_saveDataAction->setIcon(QIcon(":/common/resource/save.png")); // 如果有保存图标的话
// 设置右键菜单的样式表,使菜单项默认显示为灰色
m_contextMenu->setStyleSheet(
"QMenu::item { color: gray; }"
"QMenu::item:enabled { color: gray; }"
"QMenu::item:disabled { color: gray; }"
);
// 添加动作到菜单
m_contextMenu->addAction(m_saveDataAction);
// 连接信号槽
connect(m_saveDataAction, &QAction::triggered, this, &MainWindow::onSaveDetectionData);
// 为两个图像视图设置右键菜单事件
ui->detect_image->setContextMenuPolicy(Qt::CustomContextMenu);
ui->detect_image_2->setContextMenuPolicy(Qt::CustomContextMenu);
// 连接右键菜单信号
connect(ui->detect_image, &QGraphicsView::customContextMenuRequested,
this, [this](const QPoint& pos) { showContextMenu(pos, ui->detect_image); });
connect(ui->detect_image_2, &QGraphicsView::customContextMenuRequested,
this, [this](const QPoint& pos) { showContextMenu(pos, ui->detect_image_2); });
}
// 显示右键菜单
void MainWindow::showContextMenu(const QPoint& pos, QGraphicsView* view)
{
// 检查是否有检测数据可以保存
if (!m_presenter) {
updateStatusLog(tr("系统未初始化,无法保存数据。"));
return;
}
// 根据视图确定相机索引
int cameraIndex = (view == ui->detect_image_2) ? 2 : 1;
// 检查是否是上次检测的相机
if (cameraIndex != m_presenter->GetDetectIndex()) {
updateStatusLog(tr("当前视图不是上次检测的相机,无法保存数据。"));
return;
}
if (m_presenter->GetDetectionDataCacheSize() == 0) {
updateStatusLog(tr("没有检测数据可以保存。"));
return;
}
// 显示右键菜单
m_contextMenu->exec(view->mapToGlobal(pos));
}
// 保存检测数据槽函数
void MainWindow::onSaveDetectionData()
{
if (!m_presenter) {
updateStatusLog(tr("系统未初始化,无法保存数据。"));
return;
}
ConfigManager* config = m_presenter->GetConfigManager();
if (!config)
{
updateStatusLog(tr("配置模块未初始化,无法保存数据。"));
return;
}
// 让用户选择保存目录
QString dirPath = QFileDialog::getExistingDirectory(this,
tr("选择保存目录"),
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
if (dirPath.isEmpty()) {
updateStatusLog(tr("用户取消了保存操作"));
return;
}
// 生成文件名(包含时间戳和相机信息)
std::string timeStamp = CVrDateUtils::GetNowTime();
const bool binary = config->GetConfigResult().debugParam.storageType == DebugDataStorageType::Binary;
std::string fileName = "Laserline_" + std::to_string(m_presenter->GetDetectIndex()) + "_" + timeStamp + (binary ? ".dat" : ".txt");
QString fullPath = QDir(dirPath).filePath(QString::fromStdString(fileName));
// 保存数据
if (saveDetectionDataToFile(fullPath, m_presenter->GetDetectIndex())) {
updateStatusLog(tr("检测数据和2D图像已保存,基础文件:%1").arg(fileName.c_str()));
}
}
// 保存检测数据到文件
bool MainWindow::saveDetectionDataToFile(const QString& filePath, int cameraIndex)
{
try {
// 直接调用Presenter的保存方法
int result = m_presenter->SaveDetectionDataToFile(filePath.toStdString());
if (result == 0) {
return true;
} else {
updateStatusLog(tr("保存数据失败,错误码:%1").arg(result));
return false;
}
} catch (const std::exception& e) {
updateStatusLog(tr("保存数据时发生异常:%1").arg(e.what()));
return false;
}
}
// 获取CPU序列号(跨平台实现)
QString MainWindow::getCPUSerialNumber()
{
QString serialNumber = "UNKNOWN";
#ifdef _WIN32
// Windows平台:使用WMI获取CPU序列号
QProcess process;
process.start("wmic", QStringList() << "cpu" << "get" << "ProcessorId");
process.waitForFinished();
QString output = QString::fromLocal8Bit(process.readAllStandardOutput());
QStringList lines = output.split('\n', Qt::SkipEmptyParts);
// 第一行是标题"ProcessorId",第二行是实际的序列号
if (lines.size() >= 2) {
serialNumber = lines[1].trimmed();
}
// 如果WMIC失败,尝试读取注册表(备用方案)
if (serialNumber.isEmpty() || serialNumber == "UNKNOWN") {
QSettings settings("HKEY_LOCAL_MACHINE\\HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", QSettings::NativeFormat);
serialNumber = settings.value("ProcessorNameString", "UNKNOWN").toString();
// 从处理器名称中提取唯一标识
if (serialNumber != "UNKNOWN") {
// 使用处理器名称的哈希作为替代
serialNumber = QString("WIN_%1").arg(QString::number(qHash(serialNumber), 16).toUpper());
}
}
#else
// Linux/ARM Ubuntu平台:从/proc/cpuinfo读取
QFile file("/proc/cpuinfo");
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QTextStream in(&file);
QString line;
// 首先尝试查找Serial字段
while (!in.atEnd()) {
line = in.readLine();
if (line.contains("Serial", Qt::CaseInsensitive)) {
QStringList parts = line.split(':');
if (parts.size() >= 2) {
serialNumber = parts[1].trimmed();
break;
}
}
}
file.close();
}
#endif
// 转换为大写
return serialNumber.toUpper();
}
// 设置CPU序列号显示
void MainWindow::setupCPUSerialDisplay()
{
return;
QString cpuSerial = getCPUSerialNumber();
// 创建CPU序列号标签
m_cpuSerialLabel = new QLabel(QString("编号: %1").arg(cpuSerial));
m_cpuSerialLabel->setStyleSheet("color: rgb(239, 241, 245); font-size: 16px; margin-left: 16px;");
// 添加到状态栏左侧
statusBar()->addWidget(m_cpuSerialLabel);
LOG_INFO("CPU Serial Number: %s\n", cpuSerial.toStdString().c_str());
}