I have a PyQt5 application that shows a small list. It allows the user to copy list items. When the user copies a list item, it uses delayed rendering to place a reference to the item onto the clipboard. When the item is pasted from the clipboard, it attempts to toggle the selection and place the next item into the clipboard automatically.
Delayed rendering works the first time. However, when I attempt to clear or re-use the clipboard, I get an internal Qt error which prints a message but does not propagate into Python. This is happening on Windows 10. While I am looking for a cross-platform solution (hence Qt), I am currently only interested in solving this on Windows.
Here is an overview of what the app looks like:
When I hit Ctrl+C, the selected item is copied correctly. I then hit Ctrl+V in a Notepad window. The selected text pastes just fine. Then, the lines QApplication.clipboard().clear()
and QApplication.clipboard().setMimeData(data)
in self.copy
both "silently" fail with the following printouts:
OleSetClipboard: Failed to set mime data (NULL) on clipboard: COM error 0xffffffff800401f0 CO_E_NOTINITIALIZED (Unknown error 0x0800401f0) (The parameter is incorrect.)
OleSetClipboard: Failed to set mime data (text/plain) on clipboard: COM error 0xffffffff800401f0 CO_E_NOTINITIALIZED (Unknown error 0x0800401f0) (The parameter is incorrect.)
I believe that this has something to do with the lifetimes of the objects that Qt creates under the hood to support the PyQt interface, but I don't know how to fix it.
The code is below. I've implemented a custom QMimeData
class that can only handle text, and calls a callback in response to retreiveData
. I put the callback on a Timer
so that the object can be returned and pasted before we repurpose the clipboard. This does not seem to make a difference: even if I update the selection, the paste happens correctly and it's a little more obvious as to why I can't access the clipboard for another copy.
from PyQt5.QtCore import Qt, QMimeData, QStringListModel, QVariant
from PyQt5.QtGui import QClipboard
from PyQt5.QtWidgets import QAbstractItemView, QApplication, QListView
from threading import Timer
class MyMimeData(QMimeData):
FORMATS = {'text/plain'}
def __init__(self, item, hook=None):
super().__init__()
self.item = item
self.hook = hook
def hasFormat(self, fmt):
return fmt in self.FORMATS
def formats(self):
return list(self.FORMATS)
def retrieveData(self, mime, type):
if self.hasFormat(mime):
if self.hook:
self.hook()
return QVariant(self.item)
return QVariant()
class MyListView(QListView):
def keyPressEvent(self, event):
if event.key() == Qt.Key_C and event.modifiers() & Qt.ControlModifier:
self.copy()
else:
super().keyPressEvent(event)
def toggleRow(self):
current = self.selectedIndexes()[0]
self.setCurrentIndex(self.model().index((1 - current.row()) % 2, current.column()))
Timer(0.5, self.copy).start()
def copy(self):
item = self.selectedIndexes()[0].data()
data = MyMimeData(item, self.toggleRow)
# These are the lines that fail on the second round
QApplication.clipboard().clear()
QApplication.clipboard().setMimeData(data)
# Boilerplate to run the app
app = QApplication([])
model = QStringListModel(["First", "Second"])
view = MyListView()
view.setSelectionMode(QAbstractItemView.SingleSelection)
view.setModel(model)
view.show()
app.exec_()
I've tried extending the duration of the timer, but that does not change anything (besides delaying the error message of course). This is not surprising, as I expect there are some scoping issues occurring under the hood that I am not aware of.
I have also tried using a single instance of MyMimeData
and just updating the content it retreives based on the current row. Only the first row gets pasted over and over in that case since apparently the clipboard caches the value for a particular format once it is retrieved.
Platform specs:
- OS: Windows 10
- Conda Version: conda 4.8.3
- Python Version: Python 3.7.6
PyQt5.QtCore.QT_VERSION_STR
: 5.12.5PyQt5.Qt.PYQT_VERSION_STR
: 5.12.3
The inspiration for this is my attempt to answer Detecting paste in python
Timer(0.5, self.copy).start()
toQtCore.QTimer.singleShot(500, self.copy)
– Outhouse