You need to enable JavaScript to run this app. PySide2拖拽QPushButton时如何限制鼠标光标不离开QWidget区域
优惠活动
大模型
产品
解决方案
定价
更多

PySide2拖拽QPushButton时如何限制鼠标光标不离开QWidget区域

解决方案

你的问题核心有两个:

  1. 原生QDrag的事件只有光标悬浮在控件上方时才会触发,快速移动鼠标时光标滑出控件范围就收不到dragMoveEvent,自然无法修正位置
  2. 在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实现

如果不想改现有拖拽逻辑,可以做以下修改规避崩溃和出界问题:

  1. 给QApplication安装全局事件过滤器监听所有鼠标移动事件,替代dragMoveEvent做位置检测,解决光标出界收不到事件的问题
  2. 增加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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.12 16:00:05