PySide2拖拽QPushButton时如何限制鼠标光标不离开QWidget区域
解决方案
你的问题核心有两个:
- 原生
QDrag的事件只有光标悬浮在控件上方时才会触发,快速移动鼠标时光标滑出控件范围就收不到dragMoveEvent,自然无法修正位置 - 在
dragMoveEvent中直接调用QCursor.setPos会触发新的鼠标移动事件,进而再次触发dragMoveEvent形成无限递归,独立运行时递归深度低未触发栈溢出,嵌入宿主环境(如Nuke)后事件传递链路变长,很容易超过最大递归深度导致崩溃。
最优实现方案
推荐放弃原生QDrag机制,改用自定义鼠标捕获+位置钳制的方式实现拖拽,完全规避系统拖拽事件和宿主环境的冲突,同时保证光标不会出界:
核心逻辑
- 拖拽开始时调用
grabMouse()接管全局鼠标事件,不管光标是否在控件范围内都能收到移动回调 - 提前记录控件的全局边界矩形,每次鼠标移动时直接将光标位置钳制在边界内
- 自定义拖拽预览控件,替代原生
QDrag的pixmap显示,完全控制拖拽行为 - 避免在事件回调中触发同类型事件,加标志位防止递归
关键代码修改
from __future__ import print_function import sys try: from PySide import QtWidgets, QtCore, QtGui except ImportError: from PySide2 import QtCore, QtWidgets, QtGui class CreateNodeBoard(QtWidgets.QWidget): def __init__(self, parent=None): QtWidgets.QWidget.__init__(self, parent) self.setMinimumWidth(350) self.mainLayout = QtWidgets.QVBoxLayout() self.mainLayout.setSpacing(0) self.mainLayout.setAlignment(QtCore.Qt.AlignTop) self.setLayout(self.mainLayout) self.target = None # 拖拽相关新增成员变量 self.is_dragging = False self.drag_preview = None self.drag_bounds = None self.is_adjusting_cursor = False self.nodeBoardWidget = QtWidgets.QWidget() nodeBoardVLayout = QtWidgets.QVBoxLayout() self.nodeBoardWidget.setLayout(nodeBoardVLayout) self.userButtonLayout = QtWidgets.QGridLayout() nodeBoardVLayout.addLayout(self.userButtonLayout) # 测试按钮 for btn_text in ["button1", "button2", "test button"]: btn = QtWidgets.QPushButton(btn_text) self.userButtonLayout.addWidget(btn) self.userButtonLayout.setAlignment(QtCore.Qt.AlignLeft) self.mainLayout.addWidget(self.nodeBoardWidget) nodeBoardVLayout.addStretch(1) def get_index(self, pos): for i in range(self.userButtonLayout.count()): btn_geo = self.userButtonLayout.itemAt(i).widget().geometry() btn_geo.moveTopLeft(self.userButtonLayout.itemAt(i).widget().mapToGlobal(QtCore.QPoint(0,0))) if btn_geo.contains(pos) and i != self.target: return i def mousePressEvent(self, event): if event.button() == QtCore.Qt.MiddleButton: self.target = self.get_index(QtGui.QCursor.pos()) if self.target is not None: self.is_dragging = True # 记录拖拽允许的边界范围(全局坐标) self.drag_bounds = self.nodeBoardWidget.geometry() self.drag_bounds.moveTopLeft(self.nodeBoardWidget.mapToGlobal(QtCore.QPoint(0,0))) # 接管全局鼠标事件 self.grabMouse() # 创建拖拽预览 target_btn = self.userButtonLayout.itemAt(self.target).widget() self.drag_preview = QtWidgets.QLabel() self.drag_preview.setPixmap(target_btn.grab()) self.drag_preview.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.ToolTip) self.drag_preview.setAttribute(QtCore.Qt.WA_TranslucentBackground) self.drag_preview.show() target_btn.hide() super().mousePressEvent(event) def mouseMoveEvent(self, event): if not self.is_dragging or self.target is None or self.is_adjusting_cursor: return super().mouseMoveEvent(event) cursor_pos = QtGui.QCursor.pos() # 钳制光标位置到允许的边界内 clamped_x = max(self.drag_bounds.left(), min(cursor_pos.x(), self.drag_bounds.right() - 1)) clamped_y = max(self.drag_bounds.top(), min(cursor_pos.y(), self.drag_bounds.bottom() - 1)) # 修正光标位置 if clamped_x != cursor_pos.x() or clamped_y != cursor_pos.y(): self.is_adjusting_cursor = True QtGui.QCursor.setPos(clamped_x, clamped_y) self.is_adjusting_cursor = False cursor_pos = QtCore.QPoint(clamped_x, clamped_y) # 更新预览位置 self.drag_preview.move(cursor_pos - self.drag_preview.rect().center()) def mouseReleaseEvent(self, event): if event.button() == QtCore.Qt.MiddleButton and self.is_dragging and self.target is not None: self.releaseMouse() self.is_dragging = False # 处理按钮交换逻辑 source = self.get_index(QtGui.QCursor.pos()) if source is not None: i, j = max(self.target, source), min(self.target, source) p1, p2 = self.userButtonLayout.getItemPosition(i), self.userButtonLayout.getItemPosition(j) self.userButtonLayout.addItem(self.userButtonLayout.takeAt(i), *p2) self.userButtonLayout.addItem(self.userButtonLayout.takeAt(j), *p1) # 恢复UI状态 self.userButtonLayout.itemAt(self.target).widget().show() self.drag_preview.deleteLater() self.drag_preview = None self.target = None super().mouseReleaseEvent(event)
如果你一定要保留原生QDrag实现
如果不想改现有拖拽逻辑,可以做以下修改规避崩溃和出界问题:
- 给
QApplication安装全局事件过滤器监听所有鼠标移动事件,替代dragMoveEvent做位置检测,解决光标出界收不到事件的问题 - 增加
is_adjusting_cursor标志位,调整光标位置时屏蔽事件处理,避免递归
def __init__(self, parent=None): # ... 原有初始化逻辑 ... self.is_adjusting_cursor = False QtWidgets.QApplication.instance().installEventFilter(self) def eventFilter(self, obj, event): if event.type() == QtCore.QEvent.MouseMove and self.target is not None and not self.is_adjusting_cursor: cursor_pos = QtGui.QCursor.pos() bounds = self.nodeBoardWidget.geometry() bounds.moveTopLeft(self.nodeBoardWidget.mapToGlobal(QtCore.QPoint(0,0))) if not bounds.contains(cursor_pos): clamped_x = max(bounds.left(), min(cursor_pos.x(), bounds.right() -1)) clamped_y = max(bounds.top(), min(cursor_pos.y(), bounds.bottom() -1)) self.is_adjusting_cursor = True QtGui.QCursor.setPos(clamped_x, clamped_y) self.is_adjusting_cursor = False return super().eventFilter(obj, event)
内容的提问来源于stack exchange,提问作者Filip Suska
相关产品推荐
相关产品推荐

