【问题标题】:How to restrain mouse cursor from leaving a QWidget area in PySide2如何限制鼠标光标离开 PySide2 中的 QWidget 区域
【发布时间】:2021-09-19 04:46:18
【问题描述】:

我有一个包含两个按钮的小部件,可以使用鼠标中键进行(拖放)交换。我试图在拖放 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.避免混淆导入和模块的混合使用;因为您显然是从 PySide2 导入的(随后出现的 try/except 块是无用的),所以应该使用这些模块。 QtGui 是Qt5 的一个重要 模块,试图用QtGui 的“QtG”来“覆盖”它的名字不仅没有必要,而且只是错误。很明显,您通过从各种来源复制代码来“修补”您的方式,但这样做应该始终有意识地完成:因此,通过了解它们提供的类来使用正确的模块名称。 2. 避免不必要和分散注意力的导入->
  • ->您的示例,它们没有用,并且可能导致人们忽略您的问题,因为您的代码有太多需要注释掉的导入:回答您的人应该专注于提供答案,不是清理你的代码(那是你的工作); 3.避免不必要的代码和cmets,类和函数之间使用更一致的间距(分别为2行和1行);
  • @musicamante 感谢您的回复,代码的重点是为我的应用提供一个工作示例。这是一个完全不同的应用程序中使用的部分代码,我没有要求任何人修复我的错误或类似的东西。我只是在提供的代码的上下文中询问问题的答案。
  • 我知道这不是您的完整代码,而这正是我的观点:在提供示例时,您应该确保它只包含与问题相关的代码,而没有任何其他干扰。示例应始终为not only reproducible, but also minimal,并可能遵循良好的样式约定(空格、命名等)。回答你的人应该通过理解代码的作用来关注这个问题,而不是被代码的编写方式分散注意力。考虑创建一个 good 问题 ->
  • -> 需要很多时间,包括制作一个写得好的示例所需的时间。我提出的问题花了我一个多小时才能使它们变得更好。这是因为好的问题应该直截了当,提供相关的细节并避免任何对手头的事情不必要的东西,并且在附加的代码中也应该使用相同的概念。例如,仅您的导入就需要 17 行,而可能只有两行(sys 和 PySide),并在您的代码中造成混淆(“替换”QtGui 并将其与 QtWidgets 一起使用)不仅对我们,而且对您也是

标签: python pyqt pyside


【解决方案1】:

前提

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

  1. 拖放事件应始终由实际处理它们的小部件(在本例中为nodeBoardWidget)管理,而不是它们的父级;
  2. 获取项目的布局索引应该始终考虑项目的几何形状(不鼓励使用固定大小,因为小部件的大小取决于很多方面)以及项目可以不是是一个小部件(嵌套布局仍然是布局项,所以layout.itemAt().widget() 可以返回None);
  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)

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

【讨论】:

  • 这段代码没有提供解决方案,按钮拖得太快会导致它离开小部件的边框,就像最初问题中提供的示例代码一样。
  • @FilipSuska 请确保您使用上述功能,然后再尝试“合并”您的代码。我对其进行了测试,它可以正常工作,所以如果你的情况不是这样,那是因为你没有正确实现它。
  • 首先在 dragLeaveEvent() ---> 行“if pos not in geo:”这会创建一个 TypeError:'PySide2.QtCore.QRect' 类型的参数不可迭代。所以我不得不将代码更改为“if not geo.contains(pos):”。然后我只是在 nodeBoardWidget 内创建了两个按钮,结果还是一样.....光标离开小部件边框
  • @FilipSuska 那是因为 PySide2 没有像 PyQt 那样实现 __contains__ 运算符,但除此之外它应该可以工作(我用 PySide2 进行了测试以确保)。您是否有机会使用虚拟机?如果是这样,您必须禁用鼠标集成才能使其按预期工作。
  • 我使用的是 Windows 操作系统,当您靠近小部件的边框时,您的示例的行为完全相同,它会将光标推离它,但在快速移动后,它会离开小部件但是,它会显示“你不能放在这里”图标。
【解决方案2】:

找到了解决方法:

dragEnterEvent 导致整个事情进入递归,从而导致应用程序崩溃。 (每次我将 dragEvent 移到小部件区域之外时,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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-23
    • 1970-01-01
    • 2012-08-23
    相关资源
    最近更新 更多