#include "CloudViewMainWindow.h" #include #include #include #include #include #include #include "VrLog.h" CloudViewMainWindow::CloudViewMainWindow(QWidget* parent) : QMainWindow(parent) , m_glWidget(nullptr) , m_converter(std::make_unique()) , m_cloudCount(0) , m_currentLineNum(0) , m_currentLinePtNum(0) , m_linePointsDialog(nullptr) , m_linePointsTable(nullptr) { setupUI(); LOG_INFO("CloudViewMainWindow initialized\n"); } CloudViewMainWindow::~CloudViewMainWindow() { } void CloudViewMainWindow::setupUI() { // 创建中央控件 QWidget* centralWidget = new QWidget(this); setCentralWidget(centralWidget); // 创建主布局 QHBoxLayout* mainLayout = new QHBoxLayout(centralWidget); mainLayout->setContentsMargins(5, 5, 5, 5); mainLayout->setSpacing(5); // 创建分割器 QSplitter* splitter = new QSplitter(Qt::Horizontal, centralWidget); // 左侧:点云显示区域 QWidget* viewerArea = createViewerArea(); splitter->addWidget(viewerArea); // 右侧:控制面板 QWidget* controlPanel = createControlPanel(); splitter->addWidget(controlPanel); // 设置分割器初始大小(左侧 70%,右侧 30%) splitter->setSizes({700, 300}); mainLayout->addWidget(splitter); // 状态栏 statusBar()->showMessage("就绪"); } QWidget* CloudViewMainWindow::createViewerArea() { QWidget* widget = new QWidget(this); QVBoxLayout* layout = new QVBoxLayout(widget); layout->setContentsMargins(0, 0, 0, 0); m_glWidget = new PointCloudGLWidget(widget); m_glWidget->setMinimumSize(400, 300); // 设置最小尺寸 m_glWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); layout->addWidget(m_glWidget); // 连接信号 connect(m_glWidget, &PointCloudGLWidget::pointSelected, this, &CloudViewMainWindow::onPointSelected); connect(m_glWidget, &PointCloudGLWidget::twoPointsSelected, this, &CloudViewMainWindow::onTwoPointsSelected); connect(m_glWidget, &PointCloudGLWidget::lineSelected, this, &CloudViewMainWindow::onLineSelected); return widget; } QWidget* CloudViewMainWindow::createControlPanel() { QWidget* widget = new QWidget(this); widget->setMaximumWidth(300); QVBoxLayout* layout = new QVBoxLayout(widget); layout->setContentsMargins(5, 5, 5, 5); layout->setSpacing(10); // 文件操作组 layout->addWidget(createFileGroup()); // 选点测距组 layout->addWidget(createMeasureGroup()); // 选线拟合组 layout->addWidget(createLineGroup()); // 点云列表组 layout->addWidget(createCloudListGroup()); // 添加弹性空间 layout->addStretch(); return widget; } QGroupBox* CloudViewMainWindow::createFileGroup() { QGroupBox* group = new QGroupBox("文件操作", this); group->setMaximumWidth(300); QVBoxLayout* layout = new QVBoxLayout(group); m_btnOpenFile = new QPushButton("打开文件", group); m_btnOpenFile->setMinimumHeight(30); connect(m_btnOpenFile, &QPushButton::clicked, this, &CloudViewMainWindow::onOpenFile); layout->addWidget(m_btnOpenFile); m_btnClearAll = new QPushButton("清除所有", group); connect(m_btnClearAll, &QPushButton::clicked, this, &CloudViewMainWindow::onClearAll); layout->addWidget(m_btnClearAll); m_btnResetView = new QPushButton("重置视图", group); connect(m_btnResetView, &QPushButton::clicked, this, &CloudViewMainWindow::onResetView); layout->addWidget(m_btnResetView); return group; } QGroupBox* CloudViewMainWindow::createMeasureGroup() { QGroupBox* group = new QGroupBox("选点测距", this); group->setMaximumWidth(300); QVBoxLayout* layout = new QVBoxLayout(group); // 操作说明 QLabel* lblTip = new QLabel("Ctrl+左键点击点云选择点", group); lblTip->setWordWrap(true); lblTip->setStyleSheet("color: gray; font-size: 10px;"); layout->addWidget(lblTip); // 测距复选框 m_cbMeasureDistance = new QCheckBox("启用测距", group); m_cbMeasureDistance->setChecked(false); connect(m_cbMeasureDistance, &QCheckBox::toggled, this, [this](bool checked) { m_glWidget->setMeasureDistanceEnabled(checked); // 切换模式时清除已选点 m_glWidget->clearSelectedPoints(); m_lblPoint1->setText("--"); m_lblPoint2->setText("--"); m_lblDistance->setText("--"); }); layout->addWidget(m_cbMeasureDistance); // 清除选点按钮 m_btnClearPoints = new QPushButton("清除选点", group); connect(m_btnClearPoints, &QPushButton::clicked, this, &CloudViewMainWindow::onClearSelectedPoints); layout->addWidget(m_btnClearPoints); // 点1信息 QHBoxLayout* point1Layout = new QHBoxLayout(); QLabel* lblPoint1Title = new QLabel("点1:", group); m_lblPoint1 = new QLabel("--", group); m_lblPoint1->setWordWrap(true); point1Layout->addWidget(lblPoint1Title); point1Layout->addWidget(m_lblPoint1, 1); layout->addLayout(point1Layout); // 点2信息 QHBoxLayout* point2Layout = new QHBoxLayout(); QLabel* lblPoint2Title = new QLabel("点2:", group); m_lblPoint2 = new QLabel("--", group); m_lblPoint2->setWordWrap(true); point2Layout->addWidget(lblPoint2Title); point2Layout->addWidget(m_lblPoint2, 1); layout->addLayout(point2Layout); // 距离信息 QHBoxLayout* distLayout = new QHBoxLayout(); QLabel* lblDistTitle = new QLabel("距离:", group); m_lblDistance = new QLabel("--", group); m_lblDistance->setStyleSheet("font-weight: bold; color: green;"); distLayout->addWidget(lblDistTitle); distLayout->addWidget(m_lblDistance, 1); layout->addLayout(distLayout); return group; } QGroupBox* CloudViewMainWindow::createLineGroup() { QGroupBox* group = new QGroupBox("选线", this); group->setMaximumWidth(300); QVBoxLayout* layout = new QVBoxLayout(group); // 操作说明 QLabel* lblTip = new QLabel("Shift+左键点击点云选择线", group); lblTip->setWordWrap(true); lblTip->setStyleSheet("color: gray; font-size: 10px;"); layout->addWidget(lblTip); // 选线模式选择 QHBoxLayout* modeLayout = new QHBoxLayout(); m_rbVertical = new QRadioButton("纵向", group); m_rbHorizontal = new QRadioButton("横向", group); m_rbVertical->setChecked(true); connect(m_rbVertical, &QRadioButton::toggled, this, &CloudViewMainWindow::onLineSelectModeChanged); modeLayout->addWidget(m_rbVertical); modeLayout->addWidget(m_rbHorizontal); layout->addLayout(modeLayout); // 输入索引选择 QHBoxLayout* inputLayout = new QHBoxLayout(); m_lineNumberInput = new QLineEdit(group); m_lineNumberInput->setPlaceholderText("输入索引"); m_btnSelectByNumber = new QPushButton("选择", group); connect(m_btnSelectByNumber, &QPushButton::clicked, this, &CloudViewMainWindow::onSelectLineByNumber); inputLayout->addWidget(m_lineNumberInput, 1); inputLayout->addWidget(m_btnSelectByNumber); layout->addLayout(inputLayout); // 清除选线按钮 m_btnClearLine = new QPushButton("清除选线", group); connect(m_btnClearLine, &QPushButton::clicked, this, &CloudViewMainWindow::onClearLinePoints); layout->addWidget(m_btnClearLine); // 显示线上点按钮 m_btnShowLinePoints = new QPushButton("显示线上点", group); connect(m_btnShowLinePoints, &QPushButton::clicked, this, &CloudViewMainWindow::onShowLinePoints); layout->addWidget(m_btnShowLinePoints); // 线索引信息 QHBoxLayout* indexLayout = new QHBoxLayout(); QLabel* lblIndexTitle = new QLabel("索引:", group); m_lblLineIndex = new QLabel("--", group); indexLayout->addWidget(lblIndexTitle); indexLayout->addWidget(m_lblLineIndex, 1); layout->addLayout(indexLayout); // 点数信息 QHBoxLayout* countLayout = new QHBoxLayout(); QLabel* lblCountTitle = new QLabel("点数:", group); m_lblLinePointCount = new QLabel("--", group); countLayout->addWidget(lblCountTitle); countLayout->addWidget(m_lblLinePointCount, 1); layout->addLayout(countLayout); return group; } QGroupBox* CloudViewMainWindow::createCloudListGroup() { QGroupBox* group = new QGroupBox("点云列表", this); group->setMaximumWidth(300); QVBoxLayout* layout = new QVBoxLayout(group); m_cloudList = new QListWidget(group); m_cloudList->setMinimumHeight(100); layout->addWidget(m_cloudList); return group; } void CloudViewMainWindow::onOpenFile() { QString fileName = QFileDialog::getOpenFileName( this, "打开点云文件", QString(), "点云文件 (*.pcd *.txt);;PCD 文件 (*.pcd);;TXT 文件 (*.txt);;所有文件 (*.*)" ); if (fileName.isEmpty()) { return; } statusBar()->showMessage("正在加载点云..."); // 使用 PointCloudConverter 加载点云 PointCloudXYZ cloud; int result = m_converter->loadFromFile(fileName.toStdString(), cloud); if (result != 0) { QMessageBox::critical(this, "错误", QString("加载点云失败: %1").arg(QString::fromStdString(m_converter->getLastError()))); statusBar()->showMessage("加载失败"); return; } // 保存原始完整点云(包含0,0,0点,用于旋转) m_originalCloud = cloud; // 添加到显示控件(显示时会自动过滤0,0,0点) QFileInfo fileInfo(fileName); QString cloudName = QString("Cloud_%1 (%2)").arg(++m_cloudCount).arg(fileInfo.fileName()); m_glWidget->addPointCloud(cloud, cloudName); // 保存线信息(用于旋转功能) int lineCount = m_converter->getLoadedLineCount(); if (lineCount > 0) { m_currentLineNum = lineCount; // 计算每线点数(假设均匀分布) m_currentLinePtNum = static_cast(m_converter->getLoadedPointCount()) / lineCount; } else { m_currentLineNum = 0; m_currentLinePtNum = 0; } // 添加到列表 QString itemText; if (lineCount > 0) { itemText = QString("%1 - %2 点, %3 线").arg(cloudName).arg(m_converter->getLoadedPointCount()).arg(lineCount); } else { itemText = QString("%1 - %2 点").arg(cloudName).arg(m_converter->getLoadedPointCount()); } m_cloudList->addItem(itemText); statusBar()->showMessage(QString("已加载 %1 个点").arg(m_converter->getLoadedPointCount())); } void CloudViewMainWindow::onClearAll() { m_glWidget->clearPointClouds(); m_cloudList->clear(); m_cloudCount = 0; m_currentLineNum = 0; m_currentLinePtNum = 0; m_originalCloud.clear(); // 清除选点信息 m_lblPoint1->setText("--"); m_lblPoint2->setText("--"); m_lblDistance->setText("--"); // 清除选线信息 m_lblLineIndex->setText("--"); m_lblLinePointCount->setText("--"); statusBar()->showMessage("已清除所有点云"); } void CloudViewMainWindow::onResetView() { m_glWidget->resetView(); statusBar()->showMessage("视图已重置"); } void CloudViewMainWindow::onClearSelectedPoints() { m_glWidget->clearSelectedPoints(); m_lblPoint1->setText("--"); m_lblPoint2->setText("--"); m_lblDistance->setText("--"); statusBar()->showMessage("已清除选中的点"); } void CloudViewMainWindow::onPointSelected(const SelectedPointInfo& point) { if (!point.valid) { return; } updateSelectedPointsDisplay(); // 状态栏显示:坐标、线号、索引号 QString statusMsg = QString("选中点: (%1, %2, %3)") .arg(point.x, 0, 'f', 3) .arg(point.y, 0, 'f', 3) .arg(point.z, 0, 'f', 3); if (point.lineIndex >= 0) { statusMsg += QString(" | 线号: %1").arg(point.lineIndex); if (point.pointIndexInLine >= 0) { statusMsg += QString(" | 索引号: %1").arg(point.pointIndexInLine); } } statusBar()->showMessage(statusMsg); } void CloudViewMainWindow::onTwoPointsSelected(const SelectedPointInfo& p1, const SelectedPointInfo& p2, float distance) { updateSelectedPointsDisplay(); m_lblDistance->setText(QString("%1 mm").arg(distance, 0, 'f', 3)); statusBar()->showMessage(QString("测量距离: %1 mm").arg(distance, 0, 'f', 3)); } void CloudViewMainWindow::updateSelectedPointsDisplay() { auto selectedPoints = m_glWidget->getSelectedPoints(); if (selectedPoints.size() >= 1 && selectedPoints[0].valid) { QString text; if (selectedPoints[0].lineIndex >= 0) { text = QString("线索引:%1 点索引:%2\nx: %3 y: %4 z: %5") .arg(selectedPoints[0].lineIndex) .arg(selectedPoints[0].pointIndexInLine) .arg(selectedPoints[0].x, 0, 'f', 3) .arg(selectedPoints[0].y, 0, 'f', 3) .arg(selectedPoints[0].z, 0, 'f', 3); } else { text = QString("x: %1 y: %2 z: %3") .arg(selectedPoints[0].x, 0, 'f', 3) .arg(selectedPoints[0].y, 0, 'f', 3) .arg(selectedPoints[0].z, 0, 'f', 3); } m_lblPoint1->setText(text); } else { m_lblPoint1->setText("--"); } if (selectedPoints.size() >= 2 && selectedPoints[1].valid) { QString text; if (selectedPoints[1].lineIndex >= 0) { text = QString("线索引:%1 点索引:%2\nx: %3 y: %4 z: %5") .arg(selectedPoints[1].lineIndex) .arg(selectedPoints[1].pointIndexInLine) .arg(selectedPoints[1].x, 0, 'f', 3) .arg(selectedPoints[1].y, 0, 'f', 3) .arg(selectedPoints[1].z, 0, 'f', 3); } else { text = QString("x: %1 y: %2 z: %3") .arg(selectedPoints[1].x, 0, 'f', 3) .arg(selectedPoints[1].y, 0, 'f', 3) .arg(selectedPoints[1].z, 0, 'f', 3); } m_lblPoint2->setText(text); } else { m_lblPoint2->setText("--"); } } void CloudViewMainWindow::onLineSelectModeChanged(bool checked) { if (checked) { // 纵向模式 m_glWidget->setLineSelectMode(LineSelectMode::Vertical); } else { // 横向模式 m_glWidget->setLineSelectMode(LineSelectMode::Horizontal); } m_lineNumberInput->setPlaceholderText("输入索引"); } void CloudViewMainWindow::onClearLinePoints() { m_glWidget->clearSelectedLine(); m_glWidget->clearListHighlightPoint(); // 清除列表选中的高亮点 m_lblLineIndex->setText("--"); m_lblLinePointCount->setText("--"); statusBar()->showMessage("已清除选线"); } void CloudViewMainWindow::onLineSelected(const SelectedLineInfo& line) { // 重新选线时清除列表高亮点 m_glWidget->clearListHighlightPoint(); if (!line.valid) { m_lblLineIndex->setText("--"); m_lblLinePointCount->setText("--"); return; } // 状态栏显示:线号/索引号、线点数 if (line.mode == LineSelectMode::Vertical) { m_lblLineIndex->setText(QString::number(line.lineIndex)); statusBar()->showMessage(QString("选中线 | 线号: %1 | 线点数: %2") .arg(line.lineIndex) .arg(line.pointCount)); } else { // 横向选线:显示索引号 m_lblLineIndex->setText(QString::number(line.pointIndex)); statusBar()->showMessage(QString("选中横向线 | 索引号: %1 | 线点数: %2") .arg(line.pointIndex) .arg(line.pointCount)); } m_lblLinePointCount->setText(QString::number(line.pointCount)); // 如果线上点对话框已打开,刷新内容 updateLinePointsDialog(); } void CloudViewMainWindow::onSelectLineByNumber() { if (m_glWidget->getCloudCount() == 0) { QMessageBox::warning(this, "提示", "请先加载点云"); return; } QString text = m_lineNumberInput->text().trimmed(); if (text.isEmpty()) { QMessageBox::warning(this, "提示", "请输入索引"); return; } bool ok; int index = text.toInt(&ok); if (!ok || index < 0) { QMessageBox::warning(this, "提示", "请输入有效的索引(非负整数)"); return; } bool success = false; if (m_rbVertical->isChecked()) { // 纵向选线:直接使用索引 success = m_glWidget->selectLineByIndex(index); if (!success) { QMessageBox::warning(this, "提示", QString("索引 %1 不存在").arg(index)); } } else { // 横向选线:直接使用索引 success = m_glWidget->selectHorizontalLineByIndex(index); if (!success) { QMessageBox::warning(this, "提示", QString("索引 %1 不存在").arg(index)); } } } QVector CloudViewMainWindow::getOriginalLinePoints(const SelectedLineInfo& lineInfo) { QVector points; if (!lineInfo.valid || m_originalCloud.empty()) { return points; } if (lineInfo.mode == LineSelectMode::Vertical) { // 纵向选线:获取同一条扫描线上的所有点 for (size_t i = 0; i < m_originalCloud.points.size(); ++i) { if (i < m_originalCloud.lineIndices.size() && m_originalCloud.lineIndices[i] == lineInfo.lineIndex) { const auto& pt = m_originalCloud.points[i]; points.append(QVector3D(pt.x, pt.y, pt.z)); } } } else { // 横向选线:获取所有线的相同索引点 if (m_currentLinePtNum > 0 && lineInfo.pointIndex >= 0) { for (size_t i = 0; i < m_originalCloud.points.size(); ++i) { int originalIdx = static_cast(i); if (originalIdx % m_currentLinePtNum == lineInfo.pointIndex) { const auto& pt = m_originalCloud.points[i]; points.append(QVector3D(pt.x, pt.y, pt.z)); } } } } return points; } void CloudViewMainWindow::updateLinePointsDialog() { if (!m_linePointsDialog || !m_linePointsTable) { return; } SelectedLineInfo lineInfo = m_glWidget->getSelectedLine(); if (!lineInfo.valid) { m_linePointsTable->setRowCount(0); m_linePointsDialog->setWindowTitle("线上点坐标"); m_currentLinePoints.clear(); return; } // 从原始数据获取线上点(包含0,0,0) m_currentLinePoints = getOriginalLinePoints(lineInfo); // 更新标题 QString title; if (lineInfo.mode == LineSelectMode::Vertical) { title = QString("线上点坐标 - 线号: %1 (共 %2 个点)") .arg(lineInfo.lineIndex) .arg(m_currentLinePoints.size()); } else { title = QString("线上点坐标 - 索引号: %1 (共 %2 个点)") .arg(lineInfo.pointIndex) .arg(m_currentLinePoints.size()); } m_linePointsDialog->setWindowTitle(title); // 更新表格 m_linePointsTable->setRowCount(m_currentLinePoints.size()); // 斑马线颜色 QColor evenColor(245, 245, 245); // 浅灰色 QColor oddColor(255, 255, 255); // 白色 for (int i = 0; i < m_currentLinePoints.size(); ++i) { const QVector3D& pt = m_currentLinePoints[i]; QColor rowColor = (i % 2 == 0) ? evenColor : oddColor; // 序号 QTableWidgetItem* indexItem = new QTableWidgetItem(QString::number(i)); indexItem->setTextAlignment(Qt::AlignCenter); indexItem->setBackground(rowColor); indexItem->setFlags(indexItem->flags() & ~Qt::ItemIsEditable); m_linePointsTable->setItem(i, 0, indexItem); // X QTableWidgetItem* xItem = new QTableWidgetItem(QString::number(pt.x(), 'f', 3)); xItem->setTextAlignment(Qt::AlignCenter); xItem->setBackground(rowColor); xItem->setFlags(xItem->flags() & ~Qt::ItemIsEditable); m_linePointsTable->setItem(i, 1, xItem); // Y QTableWidgetItem* yItem = new QTableWidgetItem(QString::number(pt.y(), 'f', 3)); yItem->setTextAlignment(Qt::AlignCenter); yItem->setBackground(rowColor); yItem->setFlags(yItem->flags() & ~Qt::ItemIsEditable); m_linePointsTable->setItem(i, 2, yItem); // Z QTableWidgetItem* zItem = new QTableWidgetItem(QString::number(pt.z(), 'f', 3)); zItem->setTextAlignment(Qt::AlignCenter); zItem->setBackground(rowColor); zItem->setFlags(zItem->flags() & ~Qt::ItemIsEditable); m_linePointsTable->setItem(i, 3, zItem); } } void CloudViewMainWindow::onShowLinePoints() { SelectedLineInfo lineInfo = m_glWidget->getSelectedLine(); if (!lineInfo.valid) { QMessageBox::warning(this, "提示", "请先选择一条线"); return; } // 如果对话框已存在,刷新内容并显示 if (m_linePointsDialog) { updateLinePointsDialog(); m_linePointsDialog->raise(); m_linePointsDialog->activateWindow(); return; } // 创建对话框 m_linePointsDialog = new QDialog(this); m_linePointsDialog->resize(450, 500); m_linePointsDialog->setAttribute(Qt::WA_DeleteOnClose); // 对话框关闭时清理指针 connect(m_linePointsDialog, &QDialog::destroyed, this, [this]() { m_linePointsDialog = nullptr; m_linePointsTable = nullptr; m_currentLinePoints.clear(); m_glWidget->clearListHighlightPoint(); }); QVBoxLayout* layout = new QVBoxLayout(m_linePointsDialog); // 提示标签 QLabel* lblTip = new QLabel("点击行在3D视图中高亮显示", m_linePointsDialog); lblTip->setStyleSheet("color: gray; font-size: 10px;"); layout->addWidget(lblTip); // 创建表格控件 m_linePointsTable = new QTableWidget(m_linePointsDialog); m_linePointsTable->setColumnCount(4); m_linePointsTable->setHorizontalHeaderLabels({"序号", "X", "Y", "Z"}); m_linePointsTable->setFont(QFont("Consolas", 9)); m_linePointsTable->setSelectionBehavior(QAbstractItemView::SelectRows); m_linePointsTable->setSelectionMode(QAbstractItemView::SingleSelection); m_linePointsTable->verticalHeader()->setVisible(false); // 设置列宽 m_linePointsTable->setColumnWidth(0, 60); // 序号 m_linePointsTable->setColumnWidth(1, 110); // X m_linePointsTable->setColumnWidth(2, 110); // Y m_linePointsTable->setColumnWidth(3, 110); // Z // 表头样式 m_linePointsTable->horizontalHeader()->setStretchLastSection(true); m_linePointsTable->horizontalHeader()->setDefaultAlignment(Qt::AlignCenter); connect(m_linePointsTable, &QTableWidget::cellClicked, this, &CloudViewMainWindow::onLinePointTableClicked); layout->addWidget(m_linePointsTable); // 关闭按钮 QPushButton* btnClose = new QPushButton("关闭", m_linePointsDialog); connect(btnClose, &QPushButton::clicked, m_linePointsDialog, &QDialog::close); layout->addWidget(btnClose); // 填充数据 updateLinePointsDialog(); m_linePointsDialog->show(); } void CloudViewMainWindow::onLinePointTableClicked(int row, int column) { Q_UNUSED(column); if (row >= 0 && row < m_currentLinePoints.size()) { const QVector3D& pt = m_currentLinePoints[row]; m_glWidget->setListHighlightPoint(pt); // 在状态栏显示选中点信息 statusBar()->showMessage(QString("列表选中点 %1: (%2, %3, %4)") .arg(row) .arg(pt.x(), 0, 'f', 3) .arg(pt.y(), 0, 'f', 3) .arg(pt.z(), 0, 'f', 3)); } }