【问题标题】:Drag n Drop inside QgraphicsView doesn't work (PyQt)在 QgraphicsView 内拖放不起作用(PyQt)
【发布时间】:2015-05-01 02:24:34
【问题描述】:

我为一些按钮创建了一个自定义类。这些是“可拖动”按钮,顾名思义,是可以相互拖放(取决于是否设置了 allowDrag 属性)然后执行操作的按钮。 这些拖动按钮的代码已经发布在这里: Drag n Drop Button and Drop-down menu PyQt/Qt designer

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

这是自定义 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 所在的代码:

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,这可能吗?

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

  • 我想如何重新实现按钮行为?单击效果、属性和添加 QMenu 的可能性?当我使用addWidget 将 QButton 或我的自定义 DragButton 添加到场景中时,这已经有效。
  • 布局怎么样?我无法将 QgraphicsItem 添加到布局中,然后将布局添加到场景中!当它们在场景/视图中时,有没有办法让这些项目按顺序排列?

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

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 中的按钮做任何事情......
  • @three_pineapples 至少应该打印“已链接”,并且光标应该在即将下降时发生变化。当他们在场景中时不会发生。你看到上面按钮代码的链接了吗?又来了:stackoverflow.com/questions/28258050/…
  • @three_pineapples 我添加了按钮的代码。正如我所说,它们在布局或小部件中时按预期工作,但一旦将它们添加到图形场景中显然就不起作用
  • 您能否发布/链接到在 qwidget 模式下使用 DragButton 而没有 qgraphicsview/scene 的可运行示例?这样我可以更好地理解它应该如何工作。
  • @three_pineapples 这里是:tny.cz/8e8727b0

标签: python qt drag-and-drop pyqt


【解决方案1】:

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

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

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

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

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

相关子类实现:

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)
...

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

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_()

【讨论】:

  • 我还在某处读到了您的 QGraphicsProxyWidget 报价,这就是为什么我如此困惑,因为我认为只需使用 addWidget() 将按钮添加到场景中就足以获得所有拖放行为.至少您的解决方案按预期工作,谢谢。
  • 是的,我同意。我会想到同样的事情。也许其他人会知道为什么......
  • 我在玩你的一些代码,我发现我可以像往常一样将带有布局和子按钮的 QWidgets 添加到带有 addWidget() 的场景中,这很好,因为布局允许我拥有我的按钮对齐,但这只会将它们带入场景。正如我们所看到的,拖放将不起作用,至少我为每个将接收拖放的按钮实例化一个代理。所以我做了并且工作了。
  • 现在唯一困扰我的是一个异常Exception "unhandled RuntimeError" no access to protected functions or signals for objects not created from Python,每次我放入包含按钮的 QWidget 时都会引发该异常(但不会放在按钮上)。
  • 哦,好吧,我发现每次我放入一个没有自己代理的对象时都会引发这个问题。
猜你喜欢
  • 2020-12-29
  • 2012-02-02
  • 1970-01-01
  • 2013-10-21
  • 1970-01-01
  • 1970-01-01
  • 2017-02-16
  • 2016-07-10
  • 2015-08-10
相关资源
最近更新 更多