如何限制鼠标光标离开 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')
前提
这个答案非常局限于特定问题(防止用户将鼠标移到给定小部件的边界之外)。不幸的是,由于给定代码中的许多概念性问题,它不是一个完整的解决方案:
- 拖动 和 放置事件应始终由实际处理它们的小部件(在本例中为
nodeBoardWidget
)管理,而不是它们的父级;
- 获取项目的布局索引应该始终考虑项目的几何形状(不鼓励使用固定大小,因为小部件大小取决于很多方面)以及一个事实item 可以 不 是一个小部件(嵌套布局仍然是布局项,所以
layout.itemAt().widget()
可以 return None
);
- 基于项目索引的“交换”项目并不总是保留项目索引,因为生成的索引可能不可靠(尤其是对于网格布局);
部分解决方案
要牢记的重要方面是,尝试将鼠标移动一个小而固定的量以“固定”其位置是错误的,因为鼠标事件不是连续的:如果鼠标从 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 上的各种来源获取代码,因此我还建议您使用 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
我有一个小部件,其中包含两个可以使用鼠标中键进行(拖放)交换的按钮。我试图在拖放 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')
前提
这个答案非常局限于特定问题(防止用户将鼠标移到给定小部件的边界之外)。不幸的是,由于给定代码中的许多概念性问题,它不是一个完整的解决方案:
- 拖动 和 放置事件应始终由实际处理它们的小部件(在本例中为
nodeBoardWidget
)管理,而不是它们的父级; - 获取项目的布局索引应该始终考虑项目的几何形状(不鼓励使用固定大小,因为小部件大小取决于很多方面)以及一个事实item 可以 不 是一个小部件(嵌套布局仍然是布局项,所以
layout.itemAt().widget()
可以 returnNone
); - 基于项目索引的“交换”项目并不总是保留项目索引,因为生成的索引可能不可靠(尤其是对于网格布局);
部分解决方案
要牢记的重要方面是,尝试将鼠标移动一个小而固定的量以“固定”其位置是错误的,因为鼠标事件不是连续的:如果鼠标从 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 上的各种来源获取代码,因此我还建议您使用 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