文章目錄
- CloundCompare在點、線、面三種模式下的顯示內容
- ? 圖1:點模式
- ? 圖2:線模式
- ? 圖3:面模式
- 增加控制菜單欄
- 實現測量功能類
- 如何調用
- 項目git鏈接
CloundCompare在點、線、面三種模式下的顯示內容
點
線
面
三張圖展示了 CloudCompare 式測量工具浮窗在點、線、面三種模式下的顯示內容,它們都包括:
? 圖1:點模式
Point@Tri#15242
X1 532.893433 XE 258532.893433 R 254
Y1 -126.423424 YE 3356873.576576 G 0
Z1 0.000000 ZE 0.000000 B 0
X1/Y1/Z1
:局部坐標(例如:模型內部坐標)XE/YE/ZE
:全局坐標(世界/地理參考系坐標)R/G/B
:該點的顏色信息(RGB)@Tri#15242
:三角面片索引號中該點所屬的 triangle ID
? 圖2:線模式
Distance: 159.394958
△X 158.730469 △XY 159.394958
△Y 14.539291 △XZ 158.730469
△Z 0.000000 △ZY 14.539291
- △X/△Y/△Z:兩點之間的坐標差
- △XY/△XZ/△ZY:平面投影差(XY平面距離等)
- Distance:三維歐式距離 √(dx2 + dy2 + dz2)
? 圖3:面模式
Area: 6427.653320
index.A 15242 AB 159.394958
index.B 14731 BC 126.297385
index.C 12000 CA 101.850845angle.A 52.358784 Nx 0.000000
angle.B 39.685806 Ny 0.000000
angle.C 87.955391 Nz 1.000000
index.A/B/C
:三個點的 IDAB/BC/CA
:邊長angle.A/B/C
:夾角(角A 是 ∠BAC)Nx/Ny/Nz
:面法向量的分量
增加控制菜單欄
頭文件:
/*** @file MeasurementMenuWidget.h* @brief 該頭文件定義了 MeasurementMenuWidget 類,用于創建測量功能的菜單界面。* @author qtree* @date 2025年5月29日*/
#pragma once#include <QWidget>
#include <QPushButton>
#include <QVBoxLayout>
#include <QEvent>
#include <QMoveEvent>/*** @class MeasurementMenuWidget* @brief 繼承自 QWidget,用于創建并管理測量功能的菜單界面。* 該菜單包含點測量、線測量、三角形測量和關閉測量等功能按鈕。*/
class MeasurementMenuWidget : public QWidget
{Q_OBJECTpublic:/*** @brief 構造函數,初始化測量菜單窗口。* @param parent 父窗口指針,默認為 nullptr。*/explicit MeasurementMenuWidget(QWidget *parent = nullptr);/*** @brief 在指定位置顯示測量菜單。* @param position 菜單顯示的位置。*/void showMenu(const QPoint &position); // 顯示菜單/*** @brief 隱藏測量菜單。*/void hideMenu(); // 隱藏菜單protected:/*** @brief 事件過濾器,用于處理特定對象的事件。* @param watched 被監視的對象。* @param event 發生的事件。* @return 如果事件已被處理則返回 true,否則返回 false。*/bool eventFilter(QObject *watched, QEvent *event);signals:/*** @brief 發出點測量請求信號。*/void pointMeasureRequested();/*** @brief 發出線測量請求信號。*/void lineMeasureRequested();/*** @brief 發出三角形測量請求信號。*/void triangleMeasureRequested();/*** @brief 發出關閉測量請求信號。*/void closeMeasureRequested();private:/*** @brief 點測量功能按鈕。*/QPushButton *pointBtn_;/*** @brief 線測量功能按鈕。*/QPushButton *lineBtn_;/*** @brief 三角形測量功能按鈕。*/QPushButton *triangleBtn_;/*** @brief 關閉測量功能按鈕。*/QPushButton *closeBtn_;/*** @brief 菜單相對于父窗口的位置偏移。*/QPoint anchorOffset_; // 相對于父窗口的位置偏移/*** @brief 更新按鈕的高亮狀態。* @param activeBtn 當前激活的按鈕。*/void updateHighlight(QPushButton *activeBtn);
};
源文件:
#include "MeasurementMenuWidget.h"MeasurementMenuWidget::MeasurementMenuWidget(QWidget *parent): QWidget(parent)
{if (parent)parent->installEventFilter(this);setWindowFlags(Qt::FramelessWindowHint | Qt::Tool); // 無邊框懸浮窗setAttribute(Qt::WA_ShowWithoutActivating); // 不搶焦點setStyleSheet("QPushButton { min-width: 80px; }");QVBoxLayout *layout = new QVBoxLayout(this);layout->setContentsMargins(5, 5, 5, 5);layout->setSpacing(5);pointBtn_ = new QPushButton("Point");lineBtn_ = new QPushButton("Line");triangleBtn_ = new QPushButton("Triangle");closeBtn_ = new QPushButton("Close");layout->addWidget(pointBtn_);layout->addWidget(lineBtn_);layout->addWidget(triangleBtn_);layout->addWidget(closeBtn_);connect(pointBtn_, &QPushButton::clicked, this, [=](){emit pointMeasureRequested();updateHighlight(pointBtn_); });connect(lineBtn_, &QPushButton::clicked, this, [=](){emit lineMeasureRequested();updateHighlight(lineBtn_); });connect(triangleBtn_, &QPushButton::clicked, this, [=](){emit triangleMeasureRequested();updateHighlight(triangleBtn_); });connect(closeBtn_, &QPushButton::clicked, this, [=](){emit closeMeasureRequested();hideMenu(); });
}void MeasurementMenuWidget::showMenu(const QPoint &position)
{if (parentWidget())anchorOffset_ = position - parentWidget()->mapToGlobal(QPoint(0, 0));move(position);show();
}void MeasurementMenuWidget::hideMenu()
{hide();
}bool MeasurementMenuWidget::eventFilter(QObject *watched, QEvent *event)
{if (watched == parentWidget() && event->type() == QEvent::Move){if (isVisible()){QPoint newGlobalPos = parentWidget()->mapToGlobal(QPoint(0, 0)) + anchorOffset_;move(newGlobalPos);}}return QWidget::eventFilter(watched, event);
}void MeasurementMenuWidget::updateHighlight(QPushButton *activeBtn)
{QList<QPushButton *> buttons = {pointBtn_, lineBtn_, triangleBtn_};for (auto btn : buttons)btn->setStyleSheet(btn == activeBtn ? "background-color: lightblue;" : "");
}
實現測量功能類
頭文件:
#pragma once#include <QObject>
#include <vtkSmartPointer.h>
#include <vtkRenderer.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkTextActor.h>
#include <vtkActor.h>
#include <vtkLineSource.h>
#include <vtkPolyDataMapper.h>
#include <vtkSphereSource.h>
#include <vtkCaptionActor2D.h>
#include <vector>
#include <array>/*** @enum MeasurementMode* @brief 定義測量模式的枚舉類型,用于指定當前的測量操作類型。*/
enum class MeasurementMode
{None, ///< 無測量模式,不進行任何測量操作。Point, ///< 點測量模式,用于選擇和測量單個點。Line, ///< 線測量模式,用于選擇和測量兩點之間的距離。Triangle ///< 三角形測量模式,用于選擇和測量三角形的面積和角度。
};/*** @class MeasurementController* @brief 測量控制器類,繼承自 QObject,負責處理點、線、面的選擇和測量顯示。** 該類提供了設置測量模式、清除測量數據、處理鼠標點擊事件等功能,* 并能根據用戶選擇的測量模式在渲染窗口中顯示相應的測量結果。*/
class MeasurementController : public QObject
{Q_OBJECTpublic:/*** @brief 構造函數,初始化測量控制器。** @param renderer 指向 vtkRenderer 的指針,用于在渲染窗口中顯示測量結果。* @param interactor 指向 vtkRenderWindowInteractor 的指針,用于處理用戶交互事件。*/MeasurementController(vtkRenderer *renderer, vtkRenderWindowInteractor *interactor);/*** @brief 設置當前的測量模式。** @param mode 要設置的測量模式,為 MeasurementMode 枚舉類型。*/void setMode(MeasurementMode mode);/*** @brief 清除當前所有的測量數據和顯示的圖形。*/void clearMeasurements();/*** @brief 處理鼠標左鍵點擊事件。** 該函數需要在外部與鼠標左鍵點擊事件連接,用于響應鼠標點擊操作。*/void onLeftButtonPressed();/*** @brief 重新將文本框和其他圖形添加到渲染場景中。*/void ReAddActorsToRenderer();private:/*** @brief 在指定位置添加一個球體標記點。** @param pos 標記點的三維坐標數組。*/void addPointMarker(const double pos[3]);/*** @brief 根據已選的測量點更新測量圖形和文本顯示。*/void updateMeasurementDisplay();/*** @brief 在渲染窗口中繪制一條直線。** @param p1 直線起點的三維坐標數組。* @param p2 直線終點的三維坐標數組。*/void renderLine(const double p1[3], const double p2[3]);/*** @brief 在渲染窗口中繪制一個三角形。** @param p1 三角形第一個頂點的三維坐標數組。* @param p2 三角形第二個頂點的三維坐標數組。* @param p3 三角形第三個頂點的三維坐標數組。*/void renderTriangle(const double p1[3], const double p2[3], const double p3[3]);/*** @brief 請求刷新渲染窗口。*/void render();/*** @brief 更新文本信息框的顯示內容。*/void updateTextActor();/*** @brief 清除所有的標記點和測量圖形。*/void clearAllMarkers();vtkRenderer *renderer_; ///< 指向 vtkRenderer 的指針,用于渲染測量結果。vtkRenderWindowInteractor *interactor_; ///< 指向 vtkRenderWindowInteractor 的指針,用于處理用戶交互。MeasurementMode mode_ = MeasurementMode::None; ///< 當前的測量模式。std::vector<std::array<double, 3>> pickedPoints_; ///< 已選的測量點的列表。std::vector<vtkSmartPointer<vtkActor>> pointMarkers_; ///< 所有繪制的 actor(點、線)的列表。vtkSmartPointer<vtkTextActor> textActor_; ///< 用于顯示測量信息的文本 actor。
};
源文件:
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif#include "MeasurementController.h"
#include <vtkPointPicker.h>
#include <vtkTextProperty.h>
#include <vtkProperty.h>
#include <vtkMath.h>
#include <cmath>
#include <vtkRenderWindow.h>
#include <vtkSphereSource.h>
#include <vtkProperty2D.h>
#include <QDebug>MeasurementController::MeasurementController(vtkRenderer *renderer, vtkRenderWindowInteractor *interactor): renderer_(renderer), interactor_(interactor)
{qDebug() << "[MeasurementController] Entering constructor";textActor_ = vtkSmartPointer<vtkTextActor>::New();textActor_->SetDisplayPosition(20, 20);textActor_->GetTextProperty()->SetFontSize(16);textActor_->GetTextProperty()->SetColor(0.0, 0.0, 0.0); // 黑字textActor_->GetTextProperty()->SetBackgroundColor(1.0, 1.0, 1.0); // 白底textActor_->GetTextProperty()->SetBackgroundOpacity(0.8); // 半透明背景textActor_->GetTextProperty()->SetFrame(1); // 開啟邊框textActor_->GetTextProperty()->SetFrameColor(1.0, 0.0, 0.0); // 紅框textActor_->SetVisibility(0); // 初始隱藏renderer_->AddActor2D(textActor_);
}void MeasurementController::setMode(MeasurementMode mode)
{// clearMeasurements();mode_ = mode;if (mode_ == MeasurementMode::None){clearMeasurements();}
}void MeasurementController::clearMeasurements()
{pickedPoints_.clear();for (auto &actor : pointMarkers_){renderer_->RemoveActor(actor);}pointMarkers_.clear();textActor_->SetInput("");render();
}void MeasurementController::onLeftButtonPressed()
{if (mode_ == MeasurementMode::None){qDebug() << "[MeasurementController] Current mode is None. Click ignored.";return;}int x, y;interactor_->GetEventPosition(x, y);qDebug() << "[MeasurementController] Mouse clicked at: (" << x << "," << y << ")";auto picker = vtkSmartPointer<vtkPointPicker>::New();if (!picker->Pick(x, y, 0, renderer_)){qDebug() << "[MeasurementController] Point picking failed. No valid geometry hit.";return;}double pos[3];picker->GetPickPosition(pos);qDebug() << "[MeasurementController] Point picked at: ("<< pos[0] << "," << pos[1] << "," << pos[2] << ")";// 清除邏輯根據當前點的數量判斷switch (mode_){case MeasurementMode::Point:pickedPoints_.clear();clearAllMarkers(); // 清除之前的可視化標記break;case MeasurementMode::Line:if (pickedPoints_.size() >= 2){pickedPoints_.clear();clearAllMarkers();qDebug() << "[MeasurementController] Line mode - previous 2 points cleared.";}break;case MeasurementMode::Triangle:if (pickedPoints_.size() >= 3){pickedPoints_.clear();clearAllMarkers();qDebug() << "[MeasurementController] Triangle mode - previous 3 points cleared.";}break;default:break;}// 添加當前點擊的點pickedPoints_.emplace_back(std::array<double, 3>{pos[0], pos[1], pos[2]});qDebug() << "[MeasurementController] Picked point count:" << pickedPoints_.size();// 添加可視化標記addPointMarker(pos);// 當達到點數要求時,執行測量邏輯if ((mode_ == MeasurementMode::Line && pickedPoints_.size() == 2) ||(mode_ == MeasurementMode::Triangle && pickedPoints_.size() == 3)){qDebug() << "[MeasurementController] Required number of points reached. Updating measurement display.";updateMeasurementDisplay();}updateTextActor();
}void MeasurementController::addPointMarker(const double pos[3])
{auto sphere = vtkSmartPointer<vtkSphereSource>::New();double center[3] = {pos[0], pos[1], pos[2]};sphere->SetCenter(center);sphere->SetRadius(1.0);auto mapper = vtkSmartPointer<vtkPolyDataMapper>::New();mapper->SetInputConnection(sphere->GetOutputPort());auto actor = vtkSmartPointer<vtkActor>::New();actor->SetMapper(mapper);actor->GetProperty()->SetColor(1, 0, 0); // 紅色球體pointMarkers_.push_back(actor);renderer_->AddActor(actor);render();
}void MeasurementController::updateMeasurementDisplay()
{if (mode_ == MeasurementMode::Line && pickedPoints_.size() >= 2){renderLine(pickedPoints_[0].data(), pickedPoints_[1].data());}else if (mode_ == MeasurementMode::Triangle && pickedPoints_.size() >= 3){renderTriangle(pickedPoints_[0].data(), pickedPoints_[1].data(), pickedPoints_[2].data());}render();
}void MeasurementController::renderLine(const double p1[3], const double p2[3])
{qDebug() << "[MeasurementController] Entering renderLine function";auto line = vtkSmartPointer<vtkLineSource>::New();double pt1[3] = {p1[0], p1[1], p1[2]}; // 非 const 拷貝double pt2[3] = {p2[0], p2[1], p2[2]}; // 非 const 拷貝line->SetPoint1(pt1);line->SetPoint2(pt2);auto mapper = vtkSmartPointer<vtkPolyDataMapper>::New();mapper->SetInputConnection(line->GetOutputPort());// ?關鍵:啟用拓撲偏移,讓線繪制時偏移一點點,避免被遮擋(VTK 8.2 的做法)vtkMapper::SetResolveCoincidentTopologyToPolygonOffset();mapper->SetResolveCoincidentTopology(true);mapper->SetResolveCoincidentTopologyPolygonOffsetParameters(1.0, 1.0); // 偏移強度auto actor = vtkSmartPointer<vtkActor>::New();actor->SetMapper(mapper);actor->GetProperty()->SetColor(1, 0, 0);actor->GetProperty()->SetLineWidth(2.0);actor->GetProperty()->SetColor(1, 0, 0); // 紅色actor->GetProperty()->SetLineWidth(2.0); // 線寬actor->GetProperty()->SetLighting(false); // 可選,關閉光照影響// 可選:強制不透明,避免透明影響排序actor->GetProperty()->SetOpacity(1.0);pointMarkers_.push_back(actor);renderer_->AddActor(actor);
}void MeasurementController::renderTriangle(const double p1[3], const double p2[3], const double p3[3])
{renderLine(p1, p2);renderLine(p2, p3);renderLine(p3, p1);
}void MeasurementController::render()
{if (renderer_ && renderer_->GetRenderWindow()){renderer_->GetRenderWindow()->Render();}
}void MeasurementController::updateTextActor()
{if (mode_ == MeasurementMode::None){textActor_->SetVisibility(0);return;}QString text;if (pickedPoints_.size() == 1){const auto &p = pickedPoints_[0];text = QString("Point@Local\nX1:%1 Y1:%2 Z1:%3").arg(p[0], 0, 'f', 6).arg(p[1], 0, 'f', 6).arg(p[2], 0, 'f', 6);}else if (pickedPoints_.size() == 2){const auto &p1 = pickedPoints_[0];const auto &p2 = pickedPoints_[1];double dx = p2[0] - p1[0];double dy = p2[1] - p1[1];double dz = p2[2] - p1[2];double dxy = std::sqrt(dx * dx + dy * dy);double dxz = std::sqrt(dx * dx + dz * dz);double dyz = std::sqrt(dy * dy + dz * dz);double dist = std::sqrt(dx * dx + dy * dy + dz * dz);text = QString("Distance: %1\n""△X:%2 △Y:%3 △Z:%4\n""△XY:%5 △XZ:%6 △YZ:%7").arg(QString::number(dist, 'f', 6).rightJustified(12, ' ')).arg(QString::number(dx, 'f', 6).rightJustified(12, ' ')).arg(QString::number(dy, 'f', 6).rightJustified(12, ' ')).arg(QString::number(dz, 'f', 6).rightJustified(12, ' ')).arg(QString::number(dxy, 'f', 6).rightJustified(12, ' ')).arg(QString::number(dxz, 'f', 6).rightJustified(12, ' ')).arg(QString::number(dyz, 'f', 6).rightJustified(12, ' '));}else if (pickedPoints_.size() == 3){const auto &A = pickedPoints_[0];const auto &B = pickedPoints_[1];const auto &C = pickedPoints_[2];// 向量 AB, BC, CAdouble AB[3] = {B[0] - A[0], B[1] - A[1], B[2] - A[2]};double BC[3] = {C[0] - B[0], C[1] - B[1], C[2] - B[2]};double CA[3] = {A[0] - C[0], A[1] - C[1], A[2] - C[2]};// 邊長double lenAB = std::sqrt(AB[0] * AB[0] + AB[1] * AB[1] + AB[2] * AB[2]);double lenBC = std::sqrt(BC[0] * BC[0] + BC[1] * BC[1] + BC[2] * BC[2]);double lenCA = std::sqrt(CA[0] * CA[0] + CA[1] * CA[1] + CA[2] * CA[2]);// 向量 AC(用于法線)double AC[3] = {C[0] - A[0], C[1] - A[1], C[2] - A[2]};double N[3] = {AB[1] * AC[2] - AB[2] * AC[1],AB[2] * AC[0] - AB[0] * AC[2],AB[0] * AC[1] - AB[1] * AC[0]};double normN = std::sqrt(N[0] * N[0] + N[1] * N[1] + N[2] * N[2]);double area = 0.5 * normN;// 單位法向量if (normN > 1e-6){N[0] /= normN;N[1] /= normN;N[2] /= normN;}// 角度(夾角):使用余弦定理auto angle = [](const double *u, const double *v) -> double{double dot = u[0] * v[0] + u[1] * v[1] + u[2] * v[2];double lenU = std::sqrt(u[0] * u[0] + u[1] * u[1] + u[2] * u[2]);double lenV = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);double cosTheta = dot / (lenU * lenV);cosTheta = std::clamp(cosTheta, -1.0, 1.0);return std::acos(cosTheta) * 180.0 / M_PI;};double angleA = angle(CA, AB); // ∠CABdouble angleB = angle(AB, BC); // ∠ABCdouble angleC = angle(BC, CA); // ∠BCAtext = QString("Area:%1\n").arg(QString::number(area, 'f', 6).rightJustified(12, ' '));text += QString("AB:%1 BC:%2 CA:%3\n").arg(QString::number(lenAB, 'f', 6).rightJustified(12, ' ')).arg(QString::number(lenBC, 'f', 6).rightJustified(12, ' ')).arg(QString::number(lenCA, 'f', 6).rightJustified(12, ' '));text += QString("angle.A:%1° angle.B:%2° angle.C:%3°\n").arg(QString::number(angleA, 'f', 3).rightJustified(8, ' ')).arg(QString::number(angleB, 'f', 3).rightJustified(8, ' ')).arg(QString::number(angleC, 'f', 3).rightJustified(8, ' '));text += QString("Nx:%1 Ny:%2 Nz:%3").arg(QString::number(N[0], 'f', 6).rightJustified(12, ' ')).arg(QString::number(N[1], 'f', 6).rightJustified(12, ' ')).arg(QString::number(N[2], 'f', 6).rightJustified(12, ' '));}else{text = ""; // 超過3個點暫不支持}textActor_->SetInput(text.toUtf8().data());textActor_->SetDisplayPosition(20, 20);textActor_->SetVisibility(!text.isEmpty());
}void MeasurementController::clearAllMarkers()
{for (auto actor : pointMarkers_){renderer_->RemoveActor(actor);}pointMarkers_.clear();if (textActor_){textActor_->SetVisibility(0); // 不刪除,只隱藏}interactor_->GetRenderWindow()->Render();
}void MeasurementController::ReAddActorsToRenderer()
{if (textActor_ && renderer_){renderer_->AddActor2D(textActor_);}
}
如何調用
定義調用類內全局變量
// 初始化測量菜單
void initMeasurementMenu();protected:bool eventFilter(QObject *obj, QEvent *event);// 測量功能
MeasurementMenuWidget *measurementMenuWidget_;
std::unique_ptr<MeasurementController> measurementController_;
QPushButton *measurement_btn_;
實現類
// 初始化測量控制器
measurementController_ = std::make_unique<MeasurementController>(renderer_, interactor_);
initMeasurementMenu();// 測量按鈕
measurement_btn_ = new QPushButton("measurement");
control_btn_layout_2->addWidget(measurement_btn_);
// 測量按鈕點擊后顯示菜單(放在合適位置,如右上角)
connect(measurement_btn_, &QPushButton::clicked, this, [=](){QPoint globalPos = mapToGlobal(QPoint(width() - 150, 50)); // 控制右上角偏移measurementMenuWidget_->showMenu(globalPos); });void ThreeDimensionalDisplayPage::initMeasurementMenu()
{measurementMenuWidget_ = new MeasurementMenuWidget(this);// 連接槽函數(你已有的 measurementController_)connect(measurementMenuWidget_, &MeasurementMenuWidget::pointMeasureRequested, this, [=](){ measurementController_->setMode(MeasurementMode::Point); });connect(measurementMenuWidget_, &MeasurementMenuWidget::lineMeasureRequested, this, [=](){ measurementController_->setMode(MeasurementMode::Line); });connect(measurementMenuWidget_, &MeasurementMenuWidget::triangleMeasureRequested, this, [=](){ measurementController_->setMode(MeasurementMode::Triangle); });connect(measurementMenuWidget_, &MeasurementMenuWidget::closeMeasureRequested, this, [=](){ measurementController_->setMode(MeasurementMode::None); });// 測量按鈕點擊后顯示菜單(放在合適位置,如右上角)connect(measurement_btn_, &QPushButton::clicked, this, [=](){QPoint globalPos = mapToGlobal(QPoint(width() - 150, 50)); // 控制右上角偏移measurementMenuWidget_->showMenu(globalPos); });
}// 重新添加測量控件的 2D actor
if (measurementController_)
{measurementController_->ReAddActorsToRenderer();
}bool ThreeDimensionalDisplayPage::eventFilter(QObject *obj, QEvent *event)
{if (obj == m_pScene && event->type() == QEvent::MouseButtonPress){QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);if (mouseEvent->button() == Qt::LeftButton){if (measurementController_)measurementController_->onLeftButtonPressed();return true; // 攔截事件}}return QWidget::eventFilter(obj, event); // 交給默認處理
}
項目git鏈接
gitee:https://gitee.com/strange-tree-qian/vtktest
github:https://github.com/qishuqian666/project-vtk-test