Search code examples
pythonpyqtpyqt5pyside2

Drag & Drop Widget Implementation Error- RuntimeWarning: MetaObjectBuilder::addMethod: Invalid method signature provided for "dropped"


This is the custom widget I created for this:

from PySide2.QtCore import *
from PySide2.QtWidgets import *
from PySide2.QtGui import *
import os

class DragDropWidget(QWidget):
    def __init__(self, parent=None):
        super(DragDropWidget, self).__init__(parent)
        self.setAcceptDrops(True)

    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls:
            event.accept()
        else:
            event.ignore()

    def dragMoveEvent(self, event):
        if event.mimeData().hasUrls:
            if len(event.mimeData().urls()) != 1:
                event.ignore()
            else:
                event.setDropAction(Qt.CopyAction)
                event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        if event.mimeData().hasUrls:
            event.setDropAction(Qt.CopyAction)
            event.accept()
            if len(event.mimeData().urls()) != 1:
                event.ignore()
            else:
                url = event.mimeData().urls()[
                    0].toLocalFile()
                if os.path.exists(url):
                    self.emit(SIGNAL("dropped"), url)

        else:
            event.ignore()

I can print out the location of the file from the dropEvent function but I cannot access it from my main function using connect. My main function has the following lines for this:

self.connect(self.ui.DragDropEncode, SIGNAL("dropped"), self.add_file)
def add_file(self, file):
    print(file)

I have imported my UI from another file using:

from Main_UI import Ui_MainWindow

where I have the following code for this widget:

   self.DragDropEncode = DragDropWidget(self.AddFileEncode)
   self.DragDropEncode.setAcceptDrops(True)

When I run my main file, I'm getting the following error:

main.py:55: RuntimeWarning: MetaObjectBuilder::addMethod: Invalid method signature provided for "dropped"
  self.connect(self.ui.DragDropEncode, SIGNAL("dropped"), self.add_file)

Also, dropping a file does absolutely nothing.

I still cannot understand why this error occurs. Any help would be much appreciated. Thank You!

Here's a minimal reproducible example:

main.py

# Importing The Required Modules 
from PySide2 import QtCore, QtWidgets, QtGui
from PySide2.QtCore import *
from PySide2.QtWidgets import *
from PySide2.QtGui import *
import sys


# Importing the GUI file
from Problematic import Ui_MainWindow


class MainWindow(QMainWindow):
    def __init__(self):
        QMainWindow.__init__(self)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        # Connects to the "DragDrop" Widget defined in the GUI file
        self.connect(self.ui.DragDrop, SIGNAL("dropped"), self.add_file)

        self.show()

    def add_file(self, file):
        # Just some debugging. Not working from this end.
        print("Signal Recieved:", file)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec_())

Problematic.py

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'Problematic.ui'
#
# Created by: PyQt5 UI code generator 5.15.0
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


from PySide2 import QtCore, QtGui, QtWidgets
from DragDropWidget import DragDropWidget


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(800, 600)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget)
        self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout.setSpacing(0)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.Frame = QtWidgets.QFrame(self.centralwidget)
        self.Frame.setStyleSheet("background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 rgba(158, 7, 23, 255), stop:1 rgba(255, 130, 20, 255));")
        self.Frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
        self.Frame.setFrameShadow(QtWidgets.QFrame.Raised)
        self.Frame.setObjectName("Frame")
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.Frame)
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.DragDrop = DragDropWidget(self.Frame)
        self.DragDrop.setAcceptDrops(True)
        self.DragDrop.setStyleSheet("background-color: qlineargradient(spread:pad, x1:0.248473, y1:0.483, x2:1, y2:0, stop:0.208955 rgba(131, 62, 40, 22), stop:1 rgba(163, 13, 23, 0));\n"
"border: 2px dashed rgba(85, 85, 85, 95);")
        self.DragDrop.setObjectName("DragDrop")
        self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.DragDrop)
        self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout_3.setSpacing(0)
        self.horizontalLayout_3.setObjectName("horizontalLayout_3")
        self.Text = QtWidgets.QLabel(self.DragDrop)
        font = QtGui.QFont()
        font.setFamily("Microsoft Sans Serif")
        font.setPointSize(80)
        font.setItalic(True)
        font.setStyleStrategy(QtGui.QFont.PreferAntialias)
        self.Text.setFont(font)
        self.Text.setAlignment(QtCore.Qt.AlignCenter)
        self.Text.setObjectName("Text")
        self.horizontalLayout_3.addWidget(self.Text)
        self.horizontalLayout_2.addWidget(self.DragDrop)
        self.horizontalLayout.addWidget(self.Frame)
        MainWindow.setCentralWidget(self.centralwidget)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.Text.setText(_translate("MainWindow", "Drag & Drop\n"
"Files Here"))


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

DragDropWidget.py

from PySide2.QtCore import *
from PySide2.QtWidgets import *
from PySide2.QtGui import *
import os

class DragDropWidget(QWidget):
    def __init__(self, type, parent=None):
        super(DragDropWidget, self).__init__(parent)
        self.setAcceptDrops(True)

    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls:
            event.accept()
        else:
            event.ignore()

    def dragMoveEvent(self, event):
        if event.mimeData().hasUrls:
            if len(event.mimeData().urls()) != 1:
                event.ignore()
            else:
                event.setDropAction(Qt.CopyAction)
                event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        if event.mimeData().hasUrls:
            event.setDropAction(Qt.CopyAction)
            event.accept()
            if len(event.mimeData().urls()) != 1:
                event.ignore()
            else:
                url = event.mimeData().urls()[
                    0].toLocalFile()
                if os.path.exists(url):
                    self.emit(SIGNAL("dropped"), url)
                    # Just Some Debugging, Working from this end
                    print("Signal Emitted:", url)

        else:
            event.ignore()

Solution

  • Using the SIGNAL() macro has been considered obsolete for years, and the "new style" signal and slot syntax must always be used for new code.

    Also, the SIGNAL syntax should always have a (possibly empty) list of argument types for its signature. PyQt used to support the so called "short-circuit signals", which allowed to connect custom signals without proper signature to python callables, with the possibility to emit signals with any number and type of arguments. This was possible by using a signal without parentheses.

    As said, this syntax is obsolete, and pyside also removed the support for those short-circuit signals, as noted in the page Differrences between PySide and PyQt:

    Since this is an old and deprecated feature, and the effort to fix this is not worth it, we decided to not implement it. In PySide code you need to use something like:

    self.emit(SIGNAL ('text_changed_cb(QString)'), text)

    In your case, since you're not using arguments, it should be like this:

    self.connect(self.ui.DragDropEncode, SIGNAL("dropped()"), self.add_file)
    

    But, as said, this is an old and deprecated feature (and also too verbose and not very pythonic).

    The solution is to create signals for the class and directly emit them:

    class DragDropWidget(QWidget):
        dropped = Signal(str)
        # ...
    
        def dropEvent(self, event):
            # ...
            self.dropped.emit(url)
    

    Then connect the signal of the instance to the slots:

    self.ui.DragDropEncode.dropped.connect(self.add_file)
    

    Note that the argument signature must be respected when emitting the signal. In the case above, based on your code, I'm assuming that you're converting the QUrl of the mimeData to a string. If you need to emit a QUrl, the signal must reflect that:

    class DragDropWidget(QWidget):
        dropped = Signal(QUrl)
    

    Alternatively, there are two possibilities: you can use the generic object signature which allows to emit any kind of argument (dropped = Signal(object)), or use signal overloads. In this case, you can use a single signal that is able to emit with various arguments lengths and types. In this case, emit will use the first overload as default, while the others must be selected with square brackets:

    class DragDropWidget(QWidget):
        dropped = Signal([str], [QUrl])
    
        # ...
    
        def dropEvent(self, event):
            # ...
            url = event.mimeData().urls()[0]
            self.dropped.emit(url.toLocalFile())
            self.dropped[QUrl].emit(url)
    

    This can be useful if you need to connect to different slots according to the signature of the signal:

        self.ui.DragDropEncode.dropped.connect(self.function_that_uses_strings)
        self.ui.DragDropEncode.dropped[QUrl].connect(self.function_that_uses_urls)