You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PyQt5+VTK重复点选出现异常行为:无需移动光标即按顺序选点的问题排查

PyQt5+VTK重复点选出现异常行为:无需移动光标即按顺序选点的问题排查

我仔细看了你的问题和代码,这个异常的核心原因大概率是vtkPointPicker的状态没有被正确重置,导致连续pick时复用了之前的遍历状态,从而按顺序返回Cell内的点(比如从18跳到0)。另外你的多选逻辑也有一些可以优化的地方,下面一步步说:

问题根源分析

  1. Picker状态残留:你使用的vtkPointPicker在开启UseCellsOn()后,会遍历Cell内的点来寻找最近点,但如果没有在每次pick前重置picker状态,它可能会从上次遍历的位置继续,导致不移动光标也返回不同的点ID。
  2. 多选逻辑不完整:当前代码只处理了"按住CTRL添加新点",但没有处理"按住CTRL点击已选点取消选中"的情况,而且当重复点击同一点时会直接跳过处理,这也可能引发状态异常。

解决方案

1. 每次Pick前重置Picker状态

在调用Pick()方法前,调用self.point_picker.Initialize(),强制重置picker的内部状态,确保每次pick都是基于当前鼠标位置重新计算的。

2. 完善CTRL多选逻辑

添加对已选点的判断:按住CTRL点击已选点时移除它,点击未选点时添加它,这样更符合标准的多选交互习惯。

3. 代码优化细节

  • 封装了检查点是否已选中的工具方法,让代码更整洁
  • 移除了冗余的GetPickPosition()调用(原代码中未使用该值,还可能导致状态混淆)
  • 修正了selection_node的FieldType初始化(原代码中先设为CELL后又改为POINT,统一设置为POINT更合理)

修改后的完整代码

import sys
import numpy as np
import vtk
from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
from PyQt5 import QtCore, QtGui, QtWidgets

# Subclass QVTKRenderWindowInteractor. This uses PyQT event handling
#   rather than vtk's to avoid clashes.
class MyRenderWindowInteractor(QVTKRenderWindowInteractor):

    def __init__(self, parent, mygui):
        super().__init__(parent)
        self.setFocusPolicy(QtCore.Qt.StrongFocus)
        self.mygui = mygui

        # Initialize the renderer and interactor
        self.renderer = vtk.vtkRenderer()
        self.interactor = self.GetRenderWindow().GetInteractor()

        # Booleans for mouse click events
        self.left_mouse_pressed = False
        self.middle_mouse_pressed = False
        self.right_mouse_pressed = False
        self.mouse_position = None
        self.mouse_pressed_position = None
        self.mouse_released_position = None

        # Initialize objects for selection
        self.selection_ids = vtk.vtkIdTypeArray()
        self.selection_ids.SetNumberOfComponents(1)
        self.selection_grid = vtk.vtkUnstructuredGrid()
        self.selection_node = vtk.vtkSelectionNode()
        self.selection_node.SetSelectionList(self.selection_ids)
        # 统一设置为POINT类型,避免前后类型切换的混淆
        self.selection_node.SetFieldType(vtk.vtkSelectionNode.POINT)
        self.selection_node.SetContentType(vtk.vtkSelectionNode.INDICES)
        self.selection = vtk.vtkSelection()
        self.selection.AddNode(self.selection_node)
        self.selection_extractor = vtk.vtkExtractSelection()
        self.selection_data_mapper = vtk.vtkDataSetMapper()
        self.selection_data_mapper.SetInputData(self.selection_grid)

        # Initialize objects for point selection
        self.picked_point_id = -1
        self.point_picker = vtk.vtkPointPicker()
        self.point_picker.UseCellsOn()
        self.selection_node_actor = vtk.vtkActor()
        self.selection_node_actor.SetMapper(self.selection_data_mapper)
        self.selection_node_actor.GetProperty().SetPointSize(5)
        self.selection_node_actor.GetProperty().SetColor(vtk.vtkNamedColors().GetColor3d("Red"))
        self.renderer.AddActor(self.selection_node_actor)
        
    def _is_point_selected(self, point_id):
        """工具方法:检查点是否已在选中列表中"""
        for idx in range(self.selection_ids.GetNumberOfTuples()):
            if self.selection_ids.GetValue(idx) == point_id:
                return True
        return False

    # Override
    def mousePressEvent(self, event):

        # Overall Try/Except
        try:

            # Update mouse pressed position
            self.mouse_pressed_position = event.localPos()
            self.mouse_position = self.mouse_pressed_position

            # If the left button is pressed 
            if event.button() == QtCore.Qt.LeftButton:

                # Update pressed boolean
                self.left_mouse_pressed = True

                # 重置Picker状态,彻底避免残留导致的异常选点
                self.point_picker.Initialize()
                # 执行Pick操作,注意VTK的Y坐标是从下到上,需要转换QT的Y坐标
                self.point_picker.Pick(event.localPos().x(),
                                       self.height() - event.localPos().y(), 0, self.renderer)
                picked_point_id = self.point_picker.GetPointId()
                print(picked_point_id)

                # 如果选中了有效点
                if picked_point_id != -1:
                    if event.modifiers() == QtCore.Qt.ControlModifier:
                        if self._is_point_selected(picked_point_id):
                            # 按住CTRL点击已选点:移除该点
                            new_ids = vtk.vtkIdTypeArray()
                            new_ids.SetNumberOfComponents(1)
                            for idx in range(self.selection_ids.GetNumberOfTuples()):
                                current_id = self.selection_ids.GetValue(idx)
                                if current_id != picked_point_id:
                                    new_ids.InsertNextValue(current_id)
                            self.selection_ids = new_ids
                        else:
                            # 按住CTRL点击未选点:添加该点
                            self.selection_ids.InsertNextValue(picked_point_id)
                    else:
                        # 未按CTRL:重置选中列表并添加当前点
                        self.selection_ids.Initialize()
                        self.selection_ids.InsertNextValue(picked_point_id)
                    
                    self.picked_point_id = picked_point_id
                    # 更新选择集与渲染
                    self.selection.Modified()
                    self.selection_extractor.SetInputData(0, self.mygui.mesh_grid)
                    self.selection_extractor.SetInputData(1, self.selection)
                    self.selection_extractor.Update()
                    self.selection_grid.ShallowCopy(self.selection_extractor.GetOutput())
                    self.selection_grid.Modified()
                    self.selection_data_mapper.Update()
                    self.interactor.Render()

            # If the middle button is pressed 
            elif event.button() == QtCore.Qt.MiddleButton:

                # Update pressed boolean
                self.middle_mouse_pressed = True

            # If the right button is pressed 
            elif event.button() == QtCore.Qt.RightButton:

                # Update pressed boolean
                self.right_mouse_pressed = True

        # Print Error
        except Exception as error:
            print('MOUSE PRESSED EVENT ERROR: '+str(error)+'.')

    # Override
    def mouseReleaseEvent(self, event):

        # Overall Try/Except
        try:

            # Update mouse released position
            self.mouse_released_position = event.localPos()
            self.mouse_position = self.mouse_released_position

            # If the left button is released
            if event.button() == QtCore.Qt.LeftButton:

                # Update pressed boolean
                self.left_mouse_pressed = False

            # If the middle button is released 
            elif event.button() == QtCore.Qt.MiddleButton:

                # Update pressed boolean
                self.middle_mouse_pressed = False

            # If the right button is released 
            elif event.button() == QtCore.Qt.RightButton:

                # Update pressed boolean
                self.right_mouse_pressed = False
                        
        # Print Error
        except Exception as error:
            print('MOUSE RELEASE EVENT ERROR: '+str(error)+'.')

    # Override
    def mouseMoveEvent(self, event):
            
        # If the middle mouse button is currently pressed, rotate
        if self.middle_mouse_pressed:

            # Get the old and new mouse positions on the display in vtk coordinates
            display_pos_old = QtCore.QPoint(int(self.mouse_position.x()),
                                            int(self.height() - self.mouse_position.y()))
            display_pos_new = QtCore.QPoint(int(event.localPos().x()),
                                            int(self.height() - event.localPos().y()))
            display_motion = display_pos_new - display_pos_old
            
            # Compute the new display focal position, and new world positions
            display_focal = np.array([0., 0., 0.])
            world_pos_old = np.array([0., 0., 0., 0.])
            world_pos_new = np.array([0., 0., 0., 0.])
            world_pos = np.array(self.renderer.GetActiveCamera().GetPosition())
            world_focal = np.array(self.renderer.GetActiveCamera().GetFocalPoint())
            self.interactor.GetInteractorStyle().ComputeWorldToDisplay(self.renderer,
                                                                 world_focal[0],
                                                                 world_focal[1],
                                                                 world_focal[2],
                                                                 display_focal)
            self.interactor.GetInteractorStyle().ComputeDisplayToWorld(self.renderer,
                                                                  display_pos_old.x(),
                                                                  display_pos_old.y(),
                                                                  display_focal[2],
                                                                  world_pos_old)
            self.interactor.GetInteractorStyle().ComputeDisplayToWorld(self.renderer,
                                                                  display_pos_new.x(),
                                                                  display_pos_new.y(),
                                                                  display_focal[2],
                                                                  world_pos_new)
            world_motion = world_pos_old - world_pos_new

            # Rotate as appropriate
            if np.abs(display_motion.x()) > np.abs(display_motion.y()):
                self.renderer.GetActiveCamera().Azimuth(-2.0 * display_motion.x())
            else:
                self.renderer.GetActiveCamera().Elevation(-1.0 * display_motion.y())
            self.renderer.GetActiveCamera().OrthogonalizeViewUp()
            self.renderer.ResetCameraClippingRange()
            self.interactor.Render()

            # Reset mouse position to the latest event
            self.mouse_position = event.localPos()

        # If the right mouse button is currently pressed, pan
        if self.right_mouse_pressed:

            # Get the old and new mouse positions on the display
            display_pos_old = QtCore.QPoint(int(self.mouse_position.x()),
                                            int(self.height() - self.mouse_position.y()))
            display_pos_new = QtCore.QPoint(int(event.localPos().x()),
                                            int(self.height() - event.localPos().y()))
            display_motion = display_pos_new - display_pos_old

            # Compute pan motion in world coordinates
            display_focal = np.array([0., 0., 0.])
            world_pos_old = np.array([0., 0., 0., 0.])
            world_pos_new = np.array([0., 0., 0., 0.])
            world_focal = np.array(self.renderer.GetActiveCamera().GetFocalPoint())
            self.interactor.GetInteractorStyle().ComputeWorldToDisplay(self.renderer,
                                                                 world_focal[0],
                                                                 world_focal[1],
                                                                 world_focal[2],
                                                                 display_focal)
            self.interactor.GetInteractorStyle().ComputeDisplayToWorld(self.renderer,
                                                                  display_pos_old.x(),
                                                                  display_pos_old.y(),
                                                                  display_focal[2],
                                                                  world_pos_old)
            self.interactor.GetInteractorStyle().ComputeDisplayToWorld(self.renderer,
                                                                  display_pos_new.x(),
                                                                  display_pos_new.y(),
                                                                  display_focal[2],
                                                                  world_pos_new)
            world_motion = world_pos_old - world_pos_new

            # Pan the camera
            self.renderer.GetActiveCamera().SetPosition(
                self.renderer.GetActiveCamera().GetPosition() + world_motion[:3])
            self.renderer.GetActiveCamera().SetFocalPoint(
                world_focal + world_motion[:3])
            self.renderer.ResetCameraClippingRange()
            self.interactor.Render()

            # Reset mouse position to the latest event
            self.mouse_position = event.localPos()

备注:内容来源于stack exchange,提问作者Adam

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.13 19:35:31