如何限制鼠标光标离开 PySide2 中的 QWidget 区域

How to restrain mouse cursor from leaving a QWidget area in PySide2

我有一个小部件,其中包含两个可以使用鼠标中键进行(拖放)交换的按钮。我试图在拖放 Qpushbutton 时限制鼠标光标离开 QWidget 区域......我正在使用 dragMoveEvent() 每次它穿过小部件的边界时它都会偏移光标。当您缓慢移动鼠标时它会起作用,但快速移动会使光标离开该区域。实现这一目标的最佳方式是什么?谢谢

PS: 去拖放区参考


import os
import random
import sys
import time
from PySide2 import QtOpenGL
from PySide2 import QtWidgets
from PySide2.QtCore import QEvent, QMimeData, QPoint, QRect
from PySide2.QtGui import QCursor, QDrag, QWindow
# import nuke
# import nukescripts
from collapse import Collapse
try:
    from PySide import QtGui, QtCore
except ImportError:
    from PySide2 import QtCore
    from PySide2 import QtWidgets as QtGui
    from PySide2 import QtGui as QtG

class CreateNodeBoard(QtGui.QWidget):
    def __init__(self, parent = None):
        QtGui.QWidget.__init__(self, parent)
        
        self.nukePathSeparator = "/"
        #self.toolPath = self.getFullPathWithExt()
        self.currentDir = os.path.dirname(os.path.realpath(__file__))

    ################################################################################
    # GUI
    ################################################################################
        self.setMinimumWidth(350)        
        self.mainLayout = QtGui.QVBoxLayout()
        self.mainLayout.setSpacing(0)
        self.mainLayout.setAlignment(QtCore.Qt.AlignTop)
        self.setLayout(self.mainLayout)
        self.target = None
        self.setAcceptDrops(True)
        
        self.nodeBoardWidget = QtGui.QWidget()
        self.nodeBoardWidget.setAcceptDrops(True)
        nodeBoardVLayout = QtWidgets.QVBoxLayout()
        self.nodeBoardWidget.setLayout(nodeBoardVLayout)

        self.userButtonLayout = QtGui.QGridLayout()
        nodeBoardVLayout.addLayout(self.userButtonLayout)
        button1 = QtWidgets.QPushButton("a")
        button2 = QtWidgets.QPushButton("b")
        self.userButtonLayout.addWidget(button1)
        self.userButtonLayout.addWidget(button2)
        self.userButtonLayout.setAlignment(QtCore.Qt.AlignLeft)

        self.mainLayout.addWidget(self.nodeBoardWidget)
    def get_index(self, pos):
        for i in range(self.userButtonLayout.count()):
            buttonGlob = self.userButtonLayout.itemAt(i).widget().mapToGlobal(QPoint(0,0)) 
            if QtCore.QRect(buttonGlob.x(), buttonGlob.y(), 80, 23).contains(pos) and i != self.target:
                return i
    
    def mousePressEvent(self, event):
        if event.button() == QtCore.Qt.MiddleButton:           
            self.target = self.get_index(QCursor.pos())
        else:
            self.target = None
    
    def mouseMoveEvent(self, event):        
        if event.buttons() & QtCore.Qt.MiddleButton and self.target is not None:
            print("moving")
            drag = QDrag(self.userButtonLayout.itemAt(self.target).widget())
            pix = self.userButtonLayout.itemAt(self.target).widget().grab()
            mimedata = QMimeData()
            mimedata.setImageData(pix)
            drag.setMimeData(mimedata)
            drag.setPixmap(pix)
            drag.setHotSpot(QPoint(40,10))
            drag.exec_()
            
    def dragMoveEvent(self, event):
        cursorPos = QCursor.pos()
        widgetPos = self.nodeBoardWidget.mapToGlobal(QPoint(0,0))
        if cursorPos.x() < widgetPos.x() or cursorPos.y() < widgetPos.y():
           QCursor.setPos(QCursor.pos().x() + 1 , QCursor.pos().y() + 1 )
        event.accept()
    

    def dragEnterEvent(self, event):
        print("drag enter event")
        if event.mimeData().hasImage():
              event.accept()
        else:
            event.ignore()
    

    def dropEvent(self, event):
        print("drop")
        buttonGlob = self.userButtonLayout.itemAt(self.target).widget().mapToGlobal(self.pos())
        if not QtCore.QRect(buttonGlob.x(), buttonGlob.y(), 80, 23).contains(QCursor.pos()):
            source = self.get_index(QCursor.pos())
            if source is None:
                return

            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)
            self.target = None

app = QtWidgets.QApplication(sys.argv)

# Create a Qt widget, which will be our window.
window = CreateNodeBoard()
window.show()  # IMPORTANT!!!!! Windows are hidden by default.

# Start the event loop.
app.exec_()

编辑


所以在进一步调查和测试两者的代码后 LINUX/WINDOWS 我得出的结论是,这两种行为都是由程序超过最大递归限制引起的。任何时候鼠标光标在拖动事件期间离开指定的小部件时,都会导致事件相互调用,这会导致我的应用程序崩溃。将其作为独立应用程序不会造成任何问题,我不知道为什么?另外,我不知道这个程序是如何进入递归的。

我之前尝试为鼠标创建“安全区”的解决方案并没有解决问题,因为某些鼠标移动会导致相同的错误。

这是工作代码的更好版本。正如我已经提到的,它可以作为独立的 GUI 使用,但会导致程序在另一个软件环境中崩溃。

from __future__ import print_function

import sys

try:
    from PySide import QtWidgets, QtCore
except ImportError:
    from PySide2 import QtCore
    from PySide2 import QtWidgets
    from PySide2 import QtGui
    from PySide2 import QtOpenGL


class CreateNodeBoard(QtWidgets.QWidget):
    def __init__(self, parent=None):
        QtWidgets.QWidget.__init__(self, parent)

    ################################################################################
    # GUI
    ################################################################################

        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.targetWidget = None
        self.setAcceptDrops(True)

    ################################################################################
    # GUI - NODE BOARD
    ################################################################################

        # Create a Layout to hold all widgets
        self.nodeBoardWidget = QtWidgets.QWidget()
        self.nodeBoardWidget.setAcceptDrops(True)
        nodeBoardVLayout = QtWidgets.QVBoxLayout()
        self.nodeBoardWidget.setLayout(nodeBoardVLayout)

        # create a grid layout inside nodeBoaardVLayout and load buttons from JSON
        self.userButtonLayout = QtWidgets.QGridLayout()
        nodeBoardVLayout.addLayout(self.userButtonLayout)

        button1 = QtWidgets.QPushButton('button1')
        self.userButtonLayout.addWidget(button1)

        button2 = QtWidgets.QPushButton('button2')
        self.userButtonLayout.addWidget(button2)

        button3 = QtWidgets.QPushButton('test button')
        button3.clicked.connect(self._test)
        self.userButtonLayout.addWidget(button3)

        self.userButtonLayout.setAlignment(QtCore.Qt.AlignLeft)
        self.mainLayout.addWidget(self.nodeBoardWidget)
        nodeBoardVLayout.addStretch(1)

    ############################################################################
    # test
    ############################################################################

    def _test(self):
        print(self.topLevelWidget())

    def dragLeaveEvent(self, event):
        print("dragLeaveEvent :", event)

        # XXX: does not work on macOS
        # self.drag.cancel()

        # parent = self.parent().mapToGlobal(self.drag.hotSpot())
        # QtGui.QCursor.setPos(parent.x() + 50, parent.y() + 50)

        # XXX: could still causes a crash
        # q = QMessageBox()
        # q.setText('no can do')
        # q.exec_()

    def leaveEvent(self, event):
        pass

    def enterEvent(self, event):
        pass

    ################################################################################
    # DRAG AND DROP
    ################################################################################

    def get_index(self, pos):
        for i in range(self.userButtonLayout.count()):
            buttonGlob = self.userButtonLayout.itemAt(
                i).widget().mapToGlobal(QtCore.QPoint(0, 0))
            if QtCore.QRect(buttonGlob.x(), buttonGlob.y(), 80, 23).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())

        else:
            self.target = None

    def mouseMoveEvent(self, event):

        if event.buttons() and QtCore.Qt.MiddleButton and self.target is not None:
            print("mouseClickEvent :", event)

            self.drag = QtGui.QDrag(
                self.userButtonLayout.itemAt(self.target).widget())
            pix = self.userButtonLayout.itemAt(self.target).widget().grab()
            mimedata = QtCore.QMimeData()
            mimedata.setImageData(pix)
            self.drag.setMimeData(mimedata)
            self.drag.setPixmap(pix)
            self.drag.setHotSpot(QtCore.QPoint(40, 10))
            self.drag.exec_()

    def dragMoveEvent(self, event):
        # print("dragMoveEvent :", event)
        cursorPos = QtGui.QCursor.pos()
        widgetPos = self.nodeBoardWidget.mapToGlobal(QtCore.QPoint(0, 0))
        if cursorPos.x() <= widgetPos.x() or cursorPos.y() <= widgetPos.y():
            QtGui.QCursor.setPos(QtGui.QCursor.pos().x() +
                                 10, QtGui.QCursor.pos().y() + 10)

    def dragEnterEvent(self, event):
        print("dragEnterEvent :", event)
        # XXX: if ignored, will not crash but will not propagate events
        event.accept()

    def dropEvent(self, event):
        # print("dropEvent :", event)
        buttonGlob = self.userButtonLayout.itemAt(
            self.target).widget().mapToGlobal(self.pos())
        if not QtCore.QRect(buttonGlob.x(), buttonGlob.y(), 80, 23).contains(QtGui.QCursor.pos()):
            source = self.get_index(QtGui.QCursor.pos())
            if source is None:
                return

            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)
            self.target = None


class TestWidget(QtWidgets.QWidget):
    def __init__(self):
        QtWidgets.QWidget.__init__(self)

        self.test_widget = QtWidgets.QWidget()
        self.set_test()

        _layout = QtWidgets.QHBoxLayout()
        _layout.addWidget(CreateNodeBoard())
        _layout.addWidget(self.test_widget)

        self.setLayout(_layout)

    def set_test(self):
        """Adjacent test widget"""
        self.test_widget.setAutoFillBackground(True)
        self.test_widget.setPalette(QtGui.QColor(255, 0, 0))

        _test_layout = QtWidgets.QVBoxLayout()
        _test_layout.addWidget(QtWidgets.QLabel('TEST WIDGET'))

        self.test_widget.setLayout(_test_layout)


try:
    import nukescripts
except ImportError as error:
    APP = QtWidgets.QApplication(sys.argv)
    WINDOW = TestWidget()
    WINDOW.show()
    APP.exec_()
else:
    nukescripts.panels.registerWidgetAsPanel(
        'TestWidget', 'DragDrop',
        'DragDrop.MainWindow')

前提

这个答案非常局限于特定问题(防止用户将鼠标移到给定小部件的边界之外)。不幸的是,由于给定代码中的许多概念性问题,它不是一个完整的解决方案:

  1. 拖动 放置事件应始终由实际处理它们的小部件(在本例中为 nodeBoardWidget)管理,而不是它们的父级;
  2. 获取项目的布局索引应该始终考虑项目的几何形状(不鼓励使用固定大小,因为小部件大小取决于很多方面)以及一个事实item 可以 是一个小部件(嵌套布局仍然是布局项,所以 layout.itemAt().widget() 可以 return None);
  3. 基于项目索引的“交换”项目并不总是保留项目索引,因为生成的索引可能不可靠(尤其是对于网格布局);

部分解决方案

要牢记的重要方面是,尝试将鼠标移动一个小而固定的量以“固定”其位置是错误的,因为鼠标事件不是连续的:如果鼠标从 x=0x=100 您不会得到 0 到 100 之间的所有值,而只会得到中间位置的一小部分。
出于同样的原因,试图通过固定数量的像素“固定”位置是错误的,因为偏移量可以根据鼠标速度而变化。

如果鼠标在父边界之外移动得太快,上述结果会导致 dragMoveEvent 不被调用。虽然在您的特定情况下它“有效”,但这只是因为您在父级中实现了该功能(如前所述,这不是建议的方法,这是该原因的一个明显示例)。如果必须“包含”鼠标位置,则必须改为实现 dragLeaveEvent

class CreateNodeBoard(QtWidgets.QWidget):
    def __init__(self, parent = None):
        # ...
        self.targetWidget = None

    def mousePressEvent(self, event):
        if event.button() == QtCore.Qt.MiddleButton:
            widget = QtWidgets.QApplication.widgetAt(event.globalPos())
            if (widget != self.nodeBoardWidget and 
                self.nodeBoardWidget.isAncestorOf(widget)):
                    self.targetWidget = widget

    def mouseMoveEvent(self, event):
        if self.targetWidget:
            drag = QDrag(self.targetWidget)
            pix = self.targetWidget.grab()
            mimedata = QMimeData()
            mimedata.setImageData(pix)
            drag.setMimeData(mimedata)
            drag.setPixmap(pix)
            drag.setHotSpot(QPoint(40,10))
            drag.exec_()

    def dragEnterEvent(self, event):
        if self.nodeBoardWidget.isAncestorOf(event.source()):
            event.accept()

    def dragLeaveEvent(self, event):
        geo = self.nodeBoardWidget.rect().translated(
            self.nodeBoardWidget.mapToGlobal(QtCore.QPoint()))
        pos = QtGui.QCursor.pos()
        if pos not in geo:
            if pos.x() < geo.x():
                pos.setX(geo.x())
            elif pos.x() > geo.right():
                pos.setX(geo.right())
            if pos.y() < geo.y():
                pos.setY(geo.y())
            elif pos.y() > geo.bottom():
                pos.setY(geo.bottom())
            QtGui.QCursor.setPos(pos)

我强烈建议您研究上面的示例和注意事项,因为您的代码有很多概念性问题,如果不从头开始创建一个全新的示例,我的答案将无法解决。此外,由于很明显您是从 Web 上的各种来源获取代码,因此我还建议您使用 awareness 来做到这一点。模仿是一种很好的学习方式,但并非不了解正在做的事情。对 所有 函数和使用的 类 进行研究,并研究所有相关文档,从 layout managers, drag and drop and not forgetting about official code styling practices.

开始

找到修复:

dragEnterEvent 导致整个事件进入递归状态,从而导致应用程序崩溃。 (Linux 每次我将 dragEvent 移出小部件区域时,终端一直显示超出最大递归限制)

因此,为了解决这个问题,我在 dragEnterEvent 中创建了一个条件,如果鼠标光标移动到小部件之外,它应该忽略该事件。


    ################################################################################
    # DRAG AND DROP
    ################################################################################

    def get_index(self, pos):
        for i in range(self.userButtonLayout.count()):
            buttonGlob = self.userButtonLayout.itemAt(i).widget().mapToGlobal(QtCore.QPoint(0,0)) 
            if QtCore.QRect(buttonGlob.x(), buttonGlob.y(), 80, 23).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 event.buttons() & QtCore.Qt.MiddleButton and self.target is not None:
                drag = QtGui.QDrag(self.userButtonLayout.itemAt(self.target).widget())
                pix = self.userButtonLayout.itemAt(self.target).widget().grab()
                mimedata = QtCore.QMimeData()
                mimedata.setImageData(pix)
                drag.setMimeData(mimedata)
                drag.setPixmap(pix)
                drag.setHotSpot(QtCore.QPoint(40,10))
                drag.exec_()
        else:
            self.target = None

    def dragLeaveEvent(self, event):
        if self.cursorInWidget():
            drag = QtGui.QDrag(self.userButtonLayout.itemAt(self.target).widget())
            drag.cancel()

    def cursorInWidget(self):
        cursorPos = QtGui.QCursor.pos()
        widgetWidth = self.nodeBoardWidget.geometry().width()
        widgetHeight = self.nodeBoardWidget.geometry().height()        
        widgetPos = self.nodeBoardWidget.mapToGlobal(QtCore.QPoint(0,0))
        if cursorPos.x() <= widgetPos.x() or cursorPos.y() <= widgetPos.y() or cursorPos.x() >= (widgetPos.x() + widgetWidth) or cursorPos.y() >= (widgetPos.y() + widgetHeight):
            return False
        else:
            return True

    def dragEnterEvent(self, event):
        if self.cursorInWidget():
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        buttonGlob = self.userButtonLayout.itemAt(self.target).widget().mapToGlobal(self.pos())
        if not QtCore.QRect(buttonGlob.x(), buttonGlob.y(), 80, 23).contains(QtGui.QCursor.pos()):
            source = self.get_index(QtGui.QCursor.pos())
            if source is None:
                return
            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)
            self.target = None