在 QgraphicsView 内拖放不起作用(PyQt)

2022-01-11 00:00:00 python pyqt qt drag-and-drop

问题描述

我为一些按钮创建了一个自定义类.这些是可拖动"按钮,顾名思义,是可以相互拖放(取决于是否设置了 allowDrag 属性)然后执行操作的按钮.这些拖动按钮的代码已经发布在这里:拖放按钮和下拉菜单 PyQt/Qt 设计器

I created a custom class for some buttons. Those are a "draggable" buttons, which its name indicates, are buttons that you can drag and drop into each other (depending if is allowDrag property is set) and then make an action. The code of those dragbuttons is already posted here: Drag n Drop Button and Drop-down menu PyQt/Qt designer

显然,当按钮在 QWidget 中时,它们工作得很好,但是当它们被添加到 QGraphicsView 的场景中时(我也为它创建了一个自定义类),drop 事件不起作用.我收到了 QGraphicsItem::ungrabMouse: not a mouse grabber 警告.

Apparently the buttons work well when they are in a QWidget, but when they are added into a scene of a QGraphicsView (I also made a custom class of it) the drop event doesn't work. I get a QGraphicsItem::ungrabMouse: not a mouse grabber warning instead.

这是自定义 GraphicsView 的代码:

This is the code for the custom GraphicsView:

from PyQt4 import QtGui, QtCore

class WiringGraphicsView(QtGui.QGraphicsView):
    #Initializer method
    def __init__(self, parent = None,  scene=None):
        QtGui.QGraphicsView.__init__(self, scene, parent)
    #Set Accept Drops property true
        self.setAcceptDrops(True)

    #This method creates a line between two widgets
    def paintWire(self, start_widget,  end_widget):
        #Size and Position of both widgets
        _start = start_widget.geometry()
        _end = end_widget.geometry()
        #Creates a Brush object with Red color 
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0) )
        #Creates Pen object with specified brush
        pen = QtGui.QPen(brush, 2)
        #Create a Line object between two widgets
        line = QtGui.QGraphicsLineItem(_start.x() + _start.width() / 2, _start.y() + _start.height() / 2, _end.x() + _end.width() / 2, _end.y() + _end.height() / 2)
        #Set the Pen for the Line
        line.setPen(pen)
        #Add this line item to the scene.
        self.scene().addItem( line )

这里是自定义按钮和 graphicsView 所在的代码:

And here the code where the custom buttons and the graphicsView are:

from PyQt4.QtGui import *
from PyQt4.QtCore import *
from dragbutton import DragButton
from wiringgraphicsview import WiringGraphicsView
import icons_rc

app = QApplication([])

scene = QGraphicsScene()

menu = QMenu()

# put a button into the scene and move it
button1 = DragButton('Button 1')
button1.setText("")
button1.setDefault(False)
button1.setAutoDefault(True)
#button1.setMouseTracking(True)
button1.setAllowDrag(True) #Allow Drag n Drop of DragButton
button1.setGeometry(QRect(50, 50, 51, 31)) #Set dimensions of it
#Set icon of button1
icon = QIcon()
icon.addPixmap(QPixmap(":/audio-input-line.png"), QIcon.Normal, QIcon.Off)
button1.setIcon(icon)
button1.setFlat(True)
button1.setMenu(menu)
#Create a QGraphicsProxyWidget adding the widget to scene
scene_button1 = scene.addWidget(button1)
#move the button on the scene
r1 = scene_button1.geometry()
r1.moveTo(-100, -50)

# put another button into the scene
button2 = DragButton('Button 2')
button2.setText("")
#This button shoudn't be dragged, it is just for dropping.
button2.setAllowDrag(False)
button2.setAcceptDrops(True)
icon = QIcon()
icon.addPixmap(QPixmap(":/input_small.png"), QIcon.Normal, QIcon.Off)
button2.setIcon(icon)
#button2.setMouseTracking(True)
#button2.setGeometry(QRect(270, 150, 41, 31))
scene_button2 = scene.addWidget(button2)
scene_button2.setAcceptDrops(True)
r2 = scene_button2.geometry()

# Create the view using the scene
view = WiringGraphicsView(None, scene)
view.resize(300, 200)
view.show()
#and paint a wire between those buttons
view.paintWire(button1, button2)
app.exec_()

另外:如果我想先将按钮嵌入水平或垂直布局(按顺序排列)然后再嵌入 QgraphicsView,这可能吗?

Plus: what about if I want to embed buttons into a horizontal or vertical layout first (to have them in order) and then into a QgraphicsView, is that possible?

我已经发现您可以像任何其他小部件一样将带有子按钮的布局添加到图形场景中.我仍然不知道为什么在我的拖动按钮类中实现的拖放在 Qgraphicsscene/QgraphicsView 内部时不起作用.我阅读的大多数文档都谈到了在 QgraphicsItem 类中实现拖放逻辑.基于 QGraphicsItem 创建一个新类是个好主意,但此时让我做以下问题:

I already figured out that you can add layouts with their child buttons into a graphicscene as any other widget. I still don't know why my drag n drop implemented in my dragbutton class is not working when is inside of a Qgraphicsscene/QgraphicsView. Most of the documentation that I read talks about to implement drag n drop logic but in a QgraphicsItem class. It would be a good idea to create a new class based on QGraphicsItem, but at this point makes me to do the following questions:

  • 我想如何重新实现按钮行为?单击效果、属性和添加 QMenu 的可能性?当我使用 addWidget 将 QButton 或我的自定义 DragButton 添加到场景中时,这已经有效.
  • 布局怎么样?我无法将 QgraphicsItem 添加到布局中,然后将布局添加到场景中!当它们在场景/视图中时,有没有办法让这些项目按顺序排列?
  • How I suppose to re-implement the button behaviour? Click effects, properties, and the possibility to add a QMenu? This already works when I useaddWidget to add a QButton or my custom DragButton into a scene.
  • What about the layouts? I can't add QgraphicsItem into a layout and then add the layout to the scene! Is there way to have those items in order when they are in a scene/view?

编辑 2:我包含在我的另一篇文章中发布的DragButton"类的代码,因为与这个问题有关.

EDIT 2: I included the code of the "DragButton" class posted in my other post, since is relevant to this question.

from PyQt4 import QtGui, QtCore

class DragButton(QtGui.QPushButton):

def __init__(self, parent):
     super(DragButton,  self).__init__(parent)
     self.allowDrag = True

def setAllowDrag(self, allowDrag):
    if type(allowDrag) == bool:
       self.allowDrag = allowDrag
    else:
        raise TypeError("You have to set a boolean type")

def mouseMoveEvent(self, e):
    if e.buttons() != QtCore.Qt.RightButton:
        return

    if self.allowDrag == True:
        # write the relative cursor position to mime data
        mimeData = QtCore.QMimeData()
        # simple string with 'x,y'
        mimeData.setText('%d,%d' % (e.x(), e.y()))
        print mimeData.text()

        # let's make it fancy. we'll show a "ghost" of the button as we drag
        # grab the button to a pixmap
        pixmap = QtGui.QPixmap.grabWidget(self)

        # below makes the pixmap half transparent
        painter = QtGui.QPainter(pixmap)
        painter.setCompositionMode(painter.CompositionMode_DestinationIn)
        painter.fillRect(pixmap.rect(), QtGui.QColor(0, 0, 0, 127))
        painter.end()

        # make a QDrag
        drag = QtGui.QDrag(self)
        # put our MimeData
        drag.setMimeData(mimeData)
        # set its Pixmap
        drag.setPixmap(pixmap)
        # shift the Pixmap so that it coincides with the cursor position
        drag.setHotSpot(e.pos())

        # start the drag operation
        # exec_ will return the accepted action from dropEvent
        if drag.exec_(QtCore.Qt.LinkAction | QtCore.Qt.MoveAction) == QtCore.Qt.LinkAction:
            print 'linked'
        else:
            print 'moved'

def mousePressEvent(self, e):
    QtGui.QPushButton.mousePressEvent(self, e)
    if e.button() == QtCore.Qt.LeftButton:
        print 'press'
        #AQUI DEBO IMPLEMENTAR EL MENU CONTEXTUAL

def dragEnterEvent(self, e):
    e.accept()

def dropEvent(self, e):
    # get the relative position from the mime data
    mime = e.mimeData().text()
    x, y = map(int, mime.split(','))

        # move
        # so move the dragged button (i.e. event.source())
    print e.pos()
        #e.source().move(e.pos()-QtCore.QPoint(x, y))
        # set the drop action as LinkAction
    e.setDropAction(QtCore.Qt.LinkAction)
    # tell the QDrag we accepted it
    e.accept()


解决方案

该解决方案似乎要求您将 QGraphicsScene 子类化以将放置事件显式传递给 QGraphicsItem 在掉落坐标.此外,QGraphicsProxyWidget 似乎不会将放置事件传递给子小部件.同样,您需要继承 QGraphicsProxyWidget 并手动实例化该类,添加小部件,然后使用 scene.addItem() 手动将实例添加到场景中.

The solution appears to require that you subclass QGraphicsScene to explicitly pass the drop events to the QGraphicsItem at the drop coordinates. Further more, QGraphicsProxyWidget does not appear to pass drop events to the child widget. So again, you need to subclass QGraphicsProxyWidget and manually instantiate this class, add the widget and hen manually add the instance to the scene using scene.addItem().

注意:您可能知道,但除非您首先与小部件交互(例如单击它),否则不会开始拖放.据推测,这也可以通过将 mouseMoveEvent 从场景传递到代理然后传递到小部件来解决.

Note: You are probably aware, but you the drag/drop isn't started unless you have first interacted with the widget (e.g. clicked on it). Presumably this could be fixed by also passing through the mouseMoveEvent from the scene to the proxy and then to the widget.

注 2:我不知道为什么要花这么多精力才能完成这项工作.我确实觉得我可能错过了什么.文档说:

Note 2: I don't know why it takes so much effort to make this work. I do feel like I may be missing something. The documentation says:

QGraphicsProxyWidget 支持 QWidget 的所有核心功能,包括标签焦点、键盘输入、Drag &拖放和弹出窗口

QGraphicsProxyWidget supports all core features of QWidget, including tab focus, keyboard input, Drag & Drop, and popups

但如果没有子类化,我无法使其工作.

but I couldn't make it work without subclassing.

相关子类实现:

class MyScene(QGraphicsScene):
    def dragEnterEvent(self, e):
        e.acceptProposedAction()

    def dropEvent(self, e):
        # find item at these coordinates
        item = self.itemAt(e.scenePos())
        if item.setAcceptDrops == True: 
            # pass on event to item at the coordinates
            try:
               item.dropEvent(e)
            except RuntimeError: 
                pass #This will supress a Runtime Error generated when dropping into a widget with no MyProxy        

    def dragMoveEvent(self, e):
        e.acceptProposedAction()

class MyProxy(QGraphicsProxyWidget):    
    def dragEnterEvent(self, e):
        e.acceptProposedAction()

    def dropEvent(self, e):
        # pass drop event to child widget
        return self.widget().dropEvent(e)        

    def dragMoveEvent(self, e):
        e.acceptProposedAction()

修改后的应用代码:

scene = MyScene()
...
my_proxy = MyProxy()
my_proxy.setWidget(button2)
my_proxy.setAcceptDrops(True)
scene.addItem(my_proxy)
...

完整的工作(好吧,当拖放成功时它会打印出链接"...这是您之前编写的所有内容)应用程序:

Full working (well, it prints out "linked" when the drag drop succeeds...which is all you had written it to do previously) application:

from PyQt4 import QtGui, QtCore

class WiringGraphicsView(QtGui.QGraphicsView):
    #Initializer method
    def __init__(self, parent = None,  scene=None):
        QtGui.QGraphicsView.__init__(self, scene, parent)
    #Set Accept Drops property true
        self.setAcceptDrops(True)

    #This method creates a line between two widgets
    def paintWire(self, start_widget,  end_widget):
        #Size and Position of both widgets
        _start = start_widget.geometry()
        _end = end_widget.geometry()
        #Creates a Brush object with Red color 
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0) )
        #Creates Pen object with specified brush
        pen = QtGui.QPen(brush, 2)
        #Create a Line object between two widgets
        line = QtGui.QGraphicsLineItem(_start.x() + _start.width() / 2, _start.y() + _start.height() / 2, _end.x() + _end.width() / 2, _end.y() + _end.height() / 2)
        #Set the Pen for the Line
        line.setPen(pen)
        #Add this line item to the scene.
        self.scene().addItem( line )

class DragButton(QtGui.QPushButton):
    def __init__(self, parent):
         super(DragButton,  self).__init__(parent)
         self.allowDrag = True

    def setAllowDrag(self, allowDrag):
        if type(allowDrag) == bool:
           self.allowDrag = allowDrag
        else:
            raise TypeError("You have to set a boolean type")

    def mouseMoveEvent(self, e):
        if e.buttons() != QtCore.Qt.RightButton:
            return QtGui.QPushButton.mouseMoveEvent(self, e)

        if self.allowDrag == True:
            # write the relative cursor position to mime data
            mimeData = QtCore.QMimeData()
            # simple string with 'x,y'
            mimeData.setText('%d,%d' % (e.x(), e.y()))
            # print mimeData.text()

            # let's make it fancy. we'll show a "ghost" of the button as we drag
            # grab the button to a pixmap
            pixmap = QtGui.QPixmap.grabWidget(self)

            # below makes the pixmap half transparent
            painter = QtGui.QPainter(pixmap)
            painter.setCompositionMode(painter.CompositionMode_DestinationIn)
            painter.fillRect(pixmap.rect(), QtGui.QColor(0, 0, 0, 127))
            painter.end()

            # make a QDrag
            drag = QtGui.QDrag(self)
            # put our MimeData
            drag.setMimeData(mimeData)
            # set its Pixmap
            drag.setPixmap(pixmap)
            # shift the Pixmap so that it coincides with the cursor position
            drag.setHotSpot(e.pos())

            # start the drag operation
            # exec_ will return the accepted action from dropEvent
            if drag.exec_(QtCore.Qt.LinkAction | QtCore.Qt.MoveAction) == QtCore.Qt.LinkAction:
                print 'linked'
            else:
                print 'moved'

        return QtGui.QPushButton.mouseMoveEvent(self, e)

    def mousePressEvent(self, e):

        if e.button() == QtCore.Qt.LeftButton:
            print 'press'
            #AQUI DEBO IMPLEMENTAR EL MENU CONTEXTUAL
        return QtGui.QPushButton.mousePressEvent(self, e)

    def dragEnterEvent(self, e):
        e.accept()
        return QtGui.QPushButton.dragEnterEvent(self, e)

    def dropEvent(self, e):
        # get the relative position from the mime data
        mime = e.mimeData().text()
        x, y = map(int, mime.split(','))
            # move
            # so move the dragged button (i.e. event.source())
        print e.pos()
        # e.source().move(e.pos()-QtCore.QPoint(x, y))
            # set the drop action as LinkAction
        e.setDropAction(QtCore.Qt.LinkAction)
        # tell the QDrag we accepted it
        e.accept()

        return QtGui.QPushButton.dropEvent(self, QDropEvent(QPoint(e.pos().x(), e.pos().y()), e.possibleActions(), e.mimeData(), e.buttons(), e.modifiers()))



from PyQt4.QtGui import *
from PyQt4.QtCore import *

class MyScene(QGraphicsScene):
    def dragEnterEvent(self, e):
        e.acceptProposedAction()

    def dropEvent(self, e):
    # find item at these coordinates
    item = self.itemAt(e.scenePos())
    if item.setAcceptDrops == True:
        # pass on event to item at the coordinates
        try:
           item.dropEvent(e)
        except RuntimeError: 
            pass #This will supress a Runtime Error generated when dropping into a widget with no ProxyWidget      

    def dragMoveEvent(self, e):
        e.acceptProposedAction()

class MyProxy(QGraphicsProxyWidget):    
    def dragEnterEvent(self, e):
        e.acceptProposedAction()

    def dropEvent(self, e):
        # pass drop event to child widget
        return self.widget().dropEvent(e)        

    def dragMoveEvent(self, e):
        e.acceptProposedAction()


app = QApplication([])

scene = MyScene()

menu = QMenu()

# put a button into the scene and move it
button1 = DragButton('Button 1')
button1.setText("aaa")
button1.setDefault(False)
button1.setAutoDefault(True)
#button1.setMouseTracking(True)
button1.setAllowDrag(True) #Allow Drag n Drop of DragButton
button1.setGeometry(QRect(50, 50, 51, 31)) #Set dimensions of it
#Set icon of button1
icon = QIcon()
icon.addPixmap(QPixmap(":/audio-input-line.png"), QIcon.Normal, QIcon.Off)
button1.setIcon(icon)
button1.setFlat(True)
button1.setMenu(menu)
#Create a QGraphicsProxyWidget adding the widget to scene
scene_button1 = scene.addWidget(button1)
#move the button on the scene
r1 = scene_button1.geometry()
r1.moveTo(-100, -50)

# put another button into the scene
button2 = DragButton('Button 2')
button2.setText("bbb")
#This button shoudn't be dragged, it is just for dropping.
button2.setAllowDrag(False)
button2.setAcceptDrops(True)
icon = QIcon()
icon.addPixmap(QPixmap(":/input_small.png"), QIcon.Normal, QIcon.Off)
button2.setIcon(icon)
#button2.setMouseTracking(True)
#button2.setGeometry(QRect(270, 150, 41, 31))

# Instantiate our own proxy which forwars drag/drop events to the child widget
my_proxy = MyProxy()
my_proxy.setWidget(button2)
my_proxy.setAcceptDrops(True)
scene.addItem(my_proxy)

# Create the view using the scene
view = WiringGraphicsView(None, scene)
view.resize(300, 200)
view.show()
#and paint a wire between those buttons
view.paintWire(button1, button2)
app.exec_()

相关文章