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

2024-06-28 20:28:17 发布

您现在位置:Python中文网/ 问答频道 /正文

我有一个包含两个按钮的小部件,可以使用鼠标中键(拖放)交换。我试图在拖放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')

Tags: frompostestimportselfeventtargetif
2条回答

找到修复程序:

dragEnterEvent导致整个事件进入递归,从而导致应用程序崩溃。(每次我将DrageEvent移到widgets区域之外时,Linux终端都会显示超过了最大递归限制)

因此,为了解决这个问题,我在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

前提

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

  1. 拖放事件应始终由实际处理它们的小部件(在本例中为nodeBoardWidget)管理,而不是它们的父事件
  2. 获取一个条目的布局索引应该是<>强>始终/强>考虑项目几何(使用固定大小是不鼓励的,因为小工具大小取决于很多方面)和一个项目可以<强>不/强>的事实是一个小部件(嵌套布局仍然是布局项目,所以^ { }可以返回^ {< CD3> });<李>
  3. 基于项目索引的“交换”项目并不总是保留项目索引,因为生成的索引可能不可靠(特别是对于网格布局)

部分解

需要记住的一个重要方面是,尝试移动鼠标一小部分并固定其位置是错误的,因为鼠标事件不是连续的:如果鼠标从x=0快速移动到x=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上的各种来源,因此我也建议您使用感知来实现这一点。模仿是一种很好的学习方式,但不是不了解正在做什么。从layout managersdrag and drop开始,不要忘记official code styling practices,研究所有使用的函数和类,并研究所有相关文档。

相关问题 更多 >