文章目录
- 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:三维欧式距离 √(dx² + dy² + dz²)
✅ 图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