之前实现了,直接使用pcl的PCLVisualizer进行的点选,这是pcl对vtk进行了封装,能够实现点选功能。
但是现在有个问题就是,我把上面的代码嵌入qt中,就实现不了点选功能,我尝试了好久,还是一直卡住了。
分析原因:
- PCLVisualizer 内部管理了自己的交互器
- 在 Qt VTK 集成环境中,交互器被重新配置
- 回调函数的调用链被破坏
可能由于笔者笨拙,就是实现不了。
所以改用 VTK 原生的 vtkPointPicker,直接与底层的渲染器和交互器交互,不依赖 PCLVisualizer 的封装。
此外,再加上我的版本是VTK9.3.0,网上的更多的是低版本的,内容不太一样。
点选功能实现详解
1. 核心组件
在 VTK 中实现点选功能主要依赖以下几个组件:
┌─────────────────────────────────────────────────────────────┐
│ VTK 点选机制 │
├─────────────────────────────────────────────────────────────┤
│ │
│ vtkRenderWindowInteractor ← 鼠标事件的处理者 │
│ ↓ │
│ vtkPointPicker ← 负责在屏幕上点击位置拾取点 │
│ ↓ │
│ vtkRenderer ← 渲染器,包含所有可拾取对象 │
│ │
└─────────────────────────────────────────────────────────────┘
2. 实现步骤
第一步:创建点选器 (setupPointPicking)
cpp
void pointpicking::setupPointPicking() {
// 1. 创建 vtkPointPicker 实例
pointPicker = vtkSmartPointer<vtkPointPicker>::New();
// 2. 获取交互器
vtkRenderWindowInteractor* interactor = ui.openGLWidget->interactor();
// 3. 将点选器设置到交互器
interactor->SetPicker(pointPicker);
// 4. 添加鼠标左键事件观察者
// 当左键按下时,调用 onPointPicked 回调
interactor->AddObserver(vtkCommand::LeftButtonPressEvent,
this,
&pointpicking::onPointPicked);
}
关键点解释:
vtkPointPicker是专门用于拾取点的选择器AddObserver是 VTK 的观察者模式,监听鼠标事件vtkCommand::LeftButtonPressEvent是左键按下的事件类型
第二步:处理点击事件 (onPointPicked)
cpp
void pointpicking::onPointPicked(vtkObject* caller,
unsigned long eventId,
void* callData) {
// 1. 获取触发事件的交互器
vtkRenderWindowInteractor* interactor =
reinterpret_cast<vtkRenderWindowInteractor*>(caller);
// 2. 获取鼠标点击的屏幕坐标
int* pos = interactor->GetEventPosition();
// 3. 执行拾取操作
// 在 (pos[0], pos[1]) 位置,拾取 renderer 中的对象
pointPicker->Pick(pos[0], pos[1], 0, renderer);
// 4. 获取拾取结果
if (pointPicker->GetPointId() != -1) {
// 拾取成功,获取世界坐标
double* pickedPos = pointPicker->GetPickPosition();
// 显示结果
QString info = QString("Picked point - Coords: (%1, %2, %3)")
.arg(pickedPos[0])
.arg(pickedPos[1])
.arg(pickedPos[2]);
logMessage(info);
}
}
关键点解释:
GetEventPosition()返回鼠标在窗口中的像素坐标 (x, y)Pick(x, y, 0, renderer)在指定位置拾取对象,第三个参数是 z 平面(设为 0)GetPointId()返回拾取到的点 ID,-1 表示没有拾取到任何点GetPickPosition()返回拾取点的世界坐标 (x, y, z)
3. 工作流程图
用户点击屏幕
↓
VTK 捕获鼠标事件 (LeftButtonPressEvent)
↓
触发 onPointPicked 回调
↓
获取鼠标屏幕坐标 (pos[0], pos[1])
↓
调用 pointPicker->Pick() 执行拾取
↓
遍历 renderer 中所有 actor
↓
找到点击位置最近的几何图元
↓
返回拾取结果 (PointId 和 坐标)
↓
在日志面板显示结果
运行结果

完整代码
pointpicking.h
cpp
#pragma once
#include <QtWidgets/QMainWindow>
#include "ui_pointpicking.h"
#include <pcl/io/pcd_io.h>
#include <pcl/visualization/pcl_visualizer.h>
#include <pcl/visualization/point_picking_event.h>
#include <vtkGenericOpenGLRenderWindow.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkCommand.h>
#include <vtkCellPicker.h>
#include <vtkPointPicker.h>
#include <vtkActor.h>
#include <vtkRenderer.h>
#include <QVTKRenderWidget.h>
#include <QVTKOpenGLNativeWidget.h>
class pointpicking : public QMainWindow
{
Q_OBJECT
public:
pointpicking(QWidget* parent = nullptr);
~pointpicking();
private:
Ui::pointpicking ui;
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud;
boost::shared_ptr<pcl::visualization::PCLVisualizer> view;
vtkSmartPointer<vtkRenderer> renderer;
vtkSmartPointer<vtkPointPicker> pointPicker;
void initialVtkWidget();
void logMessage(const QString& message);
void setupPointPicking();
void onPointPicked(vtkObject* caller, unsigned long eventId, void* callData);
private slots:
void onOpen();
};
pointpicking.cpp
cpp
#include "pointpicking.h"
#include <QFileDialog>
#include <QDebug>
#include <QDateTime>
pointpicking::pointpicking(QWidget* parent) : QMainWindow(parent) {
ui.setupUi(this);
initialVtkWidget();
connect(ui.actionopen, SIGNAL(triggered()), this, SLOT(onOpen()));
logMessage("Application started");
logMessage("Tip: Hold Shift + Left click to pick points");
}
pointpicking::~pointpicking() {}
void pointpicking::setupPointPicking() {
if (!view || !ui.openGLWidget->interactor())
return;
pointPicker = vtkSmartPointer<vtkPointPicker>::New();
vtkRenderWindowInteractor* interactor = ui.openGLWidget->interactor();
interactor->SetPicker(pointPicker);
interactor->AddObserver(vtkCommand::LeftButtonPressEvent, this, &pointpicking::onPointPicked);
logMessage("Point picking enabled");
}
void pointpicking::onPointPicked(vtkObject* caller, unsigned long eventId, void* callData) {
vtkRenderWindowInteractor* interactor = reinterpret_cast<vtkRenderWindowInteractor*>(caller);
if (!interactor)
return;
int* pos = interactor->GetEventPosition();
pointPicker->Pick(pos[0], pos[1], 0, renderer);
if (pointPicker->GetPointId() != -1) {
double* pickedPos = pointPicker->GetPickPosition();
QString info = QString("Picked point - PointId: %1, Coords: (%2, %3, %4)")
.arg(pointPicker->GetPointId())
.arg(pickedPos[0], 0, 'f', 4)
.arg(pickedPos[1], 0, 'f', 4)
.arg(pickedPos[2], 0, 'f', 4);
logMessage(info);
}
}
void pointpicking::onOpen() {
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(
new pcl::PointCloud<pcl::PointXYZ>());
QString fileName = QFileDialog::getOpenFileName(this, "Open PointCloud", ".",
"Open PCD files(*.pcd)");
if (fileName == "") return;
logMessage("Loading: " + fileName);
if (pcl::io::loadPCDFile(fileName.toStdString(), *cloud) == -1) {
logMessage("Error: Failed to load PCD file");
return;
}
QString info = QString("Loaded %1 points").arg(cloud->size());
logMessage(info);
this->cloud = cloud;
view->removeAllPointClouds();
view->addPointCloud(cloud, "cloud");
view->resetCamera();
ui.openGLWidget->update();
setupPointPicking();
logMessage("Point cloud displayed");
}
void pointpicking::initialVtkWidget() {
renderer = vtkSmartPointer<vtkRenderer>::New();
vtkSmartPointer<vtkGenericOpenGLRenderWindow> renderWindow =
vtkSmartPointer<vtkGenericOpenGLRenderWindow>::New();
renderWindow->AddRenderer(renderer);
view.reset(new pcl::visualization::PCLVisualizer(renderer, renderWindow,
"viewer", false));
view->setBackgroundColor(0.2, 0.2, 0.2);
view->setupInteractor(ui.openGLWidget->interactor(),
ui.openGLWidget->renderWindow());
ui.openGLWidget->setRenderWindow(view->getRenderWindow());
logMessage("VTK widget initialized");
}
void pointpicking::logMessage(const QString& message) {
QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
ui.logTextEdit->append(QString("[%1] %2").arg(timestamp, message));
}
main.cpp
cpp
#include "pointpicking.h"
#include <QtWidgets/QApplication>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
pointpicking window;
window.show();
return app.exec();
}
pointpicking.ui
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>pointpicking</class>
<widget class="QMainWindow" name="pointpicking">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>pointpicking</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QVTKOpenGLNativeWidget" name="openGLWidget">
<property name="minimumSize">
<size>
<width>0</width>
<height>300</height>
</size>
</property>
</widget>
<widget class="QTextEdit" name="logTextEdit">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>150</height>
</size>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>25</height>
</rect>
</property>
<widget class="QMenu" name="menufile">
<property name="title">
<string>file</string>
</property>
<addaction name="actionopen"/>
</widget>
<addaction name="menufile"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<action name="actionopen">
<property name="text">
<string>open</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>QVTKOpenGLNativeWidget</class>
<extends>QOpenGLWidget</extends>
<header>qvtkopenglnativewidget.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>