Drag-and-Drop in Python Qt6 (Shallow Thoughts)

Akkana's Musings on Open Source Computing and Technology, Science, and Nature.

Fri, 22 Sep 2023

Drag-and-Drop in Python Qt6

I had a need for a window to which I could drag and drop URLs.

I don't use drag-and-drop much, since I prefer using the commandline rather than a file manager and icon-studded desktop. Usually when I need some little utility and can't immediately find what I need, I whip up a little Python script.

This time, it wasn't so easy. Python has a GUI problem (as does open source in general): there are quite a few options, like TkInter, Qt, GTK, WxWidgets and assorted others, and they all have different strengths and (especially) weaknesses.

Drag-and-drop turns out to be something none of them do very well.

TkInter, my first choice these days, can't do it at all without installing extra libraries from pip (there are a couple of different choices, but none of them seem to be commonly used). There's tkinter.dnd, but the documentation says it's experimental and deprecated, and in any case makes it clear that it's only for drag-and-drop within the same application, whereas I was looking for a drop target for links I drag from a browser or other application.

I've soured on GTK lately: they keep changing APIs, and the documentation for GTK3 in Python is scanty while for GTK4, it's basically nonexistent. Because of this, GTK has lost a lot of users ... which means that web searches on how to do any given thing in GTK often fail to find any working examples.

I did get a fair number of hits on drag and drop for WxWidgets, and maybe I should have tried that route. But instead I opted for Qt6. PyQt6 was able to do what I needed, but there was a lot of frustration along the way.

For instance, there is some PyQt6 documentation, but it's pretty light on examples and it's hard to find anything. Web searches invariably lead to Pyside documentation, which is similar to PyQt6 but not exactly the same. The Pyside docs have examples, but they leave out details like from where you import things (does QClipboard import from PyQt6.QtCore, from PyQt6.QtWidgets, or what? Turns out it's in PyQt5.QtGui). And there are also outright differences, like how Pyside examples use QClipboard().supportsSelection() and mode=QClipboard.Mode.Selection for the X selection, but those don't work in PyQt6, which instead needs QApplication.clipboard().supportsSelection() and mode=QApplication.clipboard().Mode.Selection.

I had also forgotten, having not worked with PyQt for a while, its charming tendency to fail by making the Python interpreter dump core, rather than printing a Python exception and stack trace. In this case, I kept bumping up against an equally charming tendency to silently stop doing anything at all when something goes wrong ... so making a mistake in one part of the program led to a window that looked normal but didn't respond to any events, even to print debugging output.

But I did get a drop target working in the end. Here are the important parts:

from PyQt6.QtWidgets import (QPushButton, QWidget, QApplication)
from PyQt5.QtGui import QClipboard

class DropButton(QPushButton):

    def __init__(self, parent, command=None):
        # ...
        self.setAcceptDrops(True)
        # ...

    def dragEnterEvent(self, e):
        mime_data = e.mimeData()
        print("Formats:", mime_data.formats())
        if mime_data.hasFormat('text/plain'):
            e.accept()
        else:
            print("Ignoring, no text/plain")
            e.ignore()

    def dropEvent(self, e):
        text = e.mimeData().text()
        print("Dropped:", text)

While I was at it, I added middle-mouse paste of the X PRIMARY selection. Here's what I needed for that:

    def mouseReleaseEvent(self, e):
        if e.button() != Qt.MouseButton.MiddleButton:
            return
        if QApplication.clipboard().supportsSelection():
            mode = QApplication.clipboard().Mode.Selection
        else:
            print("No Selection (PRIMARY) support! Using clipboard instead")
            mode = QApplication.clipboard().Mode.Clipboard

        text = QApplication.clipboard().text(mode=mode)

And here's the working program, which adds a few niceties like changing color to indicate that something could be dropped or that something has been dropped, and the ability to run a program on the dropped or pasted text: qdroptarget.py. I'll describe how I use it in a separate article.

Tags: , ,
[ 18:45 Sep 22, 2023    More programming | permalink to this entry | ]

Comments via Disqus:

blog comments powered by Disqus