QMimeData: setting and getting the right MIME types for arbitrary widgets - qt

Using PySide, I construct a draggable label that works exactly how I want:
class DraggableLabel(QtGui.QLabel):
def __init__(self, txt, parent):
QtGui.QLabel.__init__(self, txt, parent)
self.setStyleSheet("QLabel { background-color: rgb(255, 255, 0)}")
def mouseMoveEvent(self, event):
drag=QtGui.QDrag(self)
dragMimeData=QtCore.QMimeData()
drag.setMimeData(dragMimeData)
drag.exec_(QtCore.Qt.MoveAction)
(Note a full example that uses DraggableLabel is pasted below). Unfortunately, I do not understand what is going on with QMimeData, and fear I am going to run into big problems when I use similar code in real-world examples.
In particular, I am worried that my reimplementation of mouseMoveEvent creates an instance of QMimeData without any argument passed: QtCore.QMimeData(). Is this normal? Within more complex widgets will I be OK if I keep doing that within the relevant event handler: will the program automatically create the right type of MIME data for dragging and dropping?
The reason I fear I am missing something is because at the Qt Drag and Drop documentation, it has lines of code like:
mimeData -> setText(commentEdit->toPlainText());
which seems decidedly not like just letting the program take care of things within a reimplementation of an event handler.
Also, the QMimeData Documentation discusses convenience functions to test, get, and set data, but those are for standard data types (e.g., text, urls). I have found no clear way to define such convenience functions for widgets like my draggable QLabel. Am I missing it? Is there a simple way to find out if I am dragging around a widget of type X?
Edit: I've tried the same code above with much more complicated widgets than QLabels, and it does not work.
Potentially relevant posts:
Dragging a QWidget in QT 5
How to Drag and Drop Custom Widgets?
https://stackoverflow.com/questions/18272650/fill-the-system-clipboard-with-data-of-custom-mime-type
Python object in QMimeData
Important Caveat: if you just want to move a widget in a window, you do not need to invoke esoteric drag-drop mechanisms, but more vanilla event handling. See this: Dragging/Moving a QPushButton in PyQt.
Full working self-contained code example that incorporates the above:
# -*- coding: utf-8 -*-
from PySide import QtGui, QtCore
class LabelDrag(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.initUI()
def initUI(self):
self.lbl=DraggableLabel("Drag me", self)
self.setAcceptDrops(True)
self.setGeometry(40,50,200,200)
self.show()
def dragEnterEvent(self,event):
event.accept()
def dropEvent(self, event):
self.lbl.move(event.pos()) #moves label to position once the movement finishes (dropped)
event.accept()
class DraggableLabel(QtGui.QLabel):
def __init__(self, txt, parent):
QtGui.QLabel.__init__(self, txt, parent)
self.setStyleSheet("QLabel { background-color: rgb(255, 255, 0)}")
def mouseMoveEvent(self, event):
drag=QtGui.QDrag(self)
dragMimeData=QtCore.QMimeData()
drag.setMimeData(dragMimeData)
drag.exec_(QtCore.Qt.MoveAction)
def main():
import sys
qt_app=QtGui.QApplication(sys.argv)
myMover=LabelDrag()
sys.exit(qt_app.exec_())
if __name__=="__main__":
main()
Note I'm posting this at QtCentre as well, will post anything useful from there.

In my experience, MimeData is used to filter drag/drop operations so that the action actually makes sense. For instance, you shouldn't be able to drag your QLabel into the middle of a QTextEdit or your browsers address bar, or the desktop of your computer.
From the docs:
QMimeData is used to describe information that can be stored in the clipboard, and transferred via the drag and drop mechanism. QMimeData objects associate the data that they hold with the corresponding MIME types to ensure that information can be safely transferred between applications, and copied around within the same application.
If you were doing something standard, like dragging/dropping text from one place to another, you would use one of the standard MIME types (like setting the MIME data of your drag using dragMimeData.setText('your text') or equivalently dragMimeData.setData('text/plain', 'your text')). However, since you are doing something completely custom, you should probably specify a custom MIME type so that you can't accidentally do things that don't make sense.
So I would set the MIME data to something like dragMimeData.setData('MoveQLabel', QByteArray('any string you want')) which stores an arbitrary string for the MIME type MoveQLabel. This arbitrary string could be used to look up which widget you want to move at the end of the drag (maybe by storing it's position?).
You should modify your LabelDrag class above to check the MIME type of the event (use event.mimeData() to get the QMimeData object you set when you started the drag), and accept or reject the event depending on whether the MIME type matches MoveQLabel (or whatever you call your custom MIME type). This should be done in both the dragEnterEvent and dropEvent methods.
You code would look something like:
def dragEnterEvent(self, event):
# you may also need to check that event.mimeData() returns a valid QMimeData object
if event.mimeData().hasFormat('MoveQLabel'):
event.accept()
else:
event.reject()
If you also store some useful MIME data with the MIME type (aka something else instead of 'any string you want' above), you could use it to dynamically select, within LabelDrag.dropEvent, which widget it is that you want to move. You can access it through event.mimeData().data('MoveQLabel'). This means that your QWidget can handle moving multiple labels as it will always move the one that is being dropped.

Related

PyQt5 one signal and multiple slots

I have come up with a simple signal/slot mechanism below. The signal is called by QSlider when the value is changed via QSlider::valueChanged(). And the slot is called via the QLCDNumber::display() method.
What is confusing to me is why PyQt5 has so little documentation and most documentation leads to links for Qt5. The specific issue I have with the code is:
1) If QSlider::valueChanged() (signal) expects an integer as a parameter why do we only pass in QLCDNumber::display() (slot) which is a void function. Therefore nothing is going to be passed in.
2) In the commented-out code below, I am not able to call a second slot. Is there a limit to the number of slots you can call for 1 signal? If there is how do I go about calling multiple slots in PyQt5.
Edit: I believe because printLabel() is not a defined slot, this is why I am getting issues. Is it possible for me to include any amount of slots in the ::connect() parameters? Or am I approaching this in a hacky way.
import sys
from PyQt5.QtCore import (Qt)
from PyQt5.QtWidgets import (QWidget, QLCDNumber, QSlider,
QVBoxLayout, QApplication)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def printLabel(self, str):
print(str)
def initUI(self):
lcd = QLCDNumber(self)
sld = QSlider(Qt.Horizontal, self)
vbox = QVBoxLayout()
vbox.addWidget(lcd)
vbox.addWidget(sld)
self.setLayout(vbox)
#This line works
sld.valueChanged.connect(lcd.display)
#This line does not work
#sld.valueChanged.connect(lcd.display, self.printLabel("hi"))
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Signal & slot')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
Taking your points in order:
The Qt5 documentation contains at least 90% of what you need to know, so there's little point in repeating it all in the PyQt5 documentation, which instead mainly concentrates on the features that are specific to PyQt (see the Signals and Slots article, for instance). Qt5 is a C++ library. You don't need to know C++ in order to use PyQt (or to read Qt's excellent documentation), but you can hardly expect to avoid it altogether.
The QSlider.valueChanged signal sends an int value to all the slots that are connected to it. The QLCDNUmber.display slot receives either a string, int or float object (i.e. it has three overloads). For built-in Qt slots, there must be an overload that matches what the signal sends. But user-defined slots in PyQt can be any python callable object, and it doesn't matter if the signature doesn't match (any extra parameters will be thrown away). Having said that, an explicit signature can be defined for a user-defined slot by using the pyqtSlot decorator (which therefore allows multiple overloads to be specified in PyQt).
As explained in the first section of the PyQt article mentioned above, both signals and slots can have multiple connections. The article also details the PyQt-specific APIs (including pyqtSlot), and gives examples of how to use them.
Here's how to connect to multiple slots your example:
class Example(QWidget):
...
def initUI(self):
...
#connect to a built-in slot
sld.valueChanged.connect(lcd.display)
#connect to a user-defined lot
sld.valueChanged.connect(self.printLabel)
#any python callable will do
sld.valueChanged.connect(lambda x: print('lambda:', x))
...
def printLabel(self, value):
print('printLabel:', value)

Refresh view when model data has not changed (Qt/PySide/PyQt)?

I have a tree view of a standard item model in which I can use a spinbox to change the row height in the view (see SSCCE below). This doesn't change the content of the view, only its appearance, sort of like resizing the main window except I have to do it myself:
I change the row height from within the delegate's sizeHint method. It is within sizeHint that I get the value from the spinbox and set the row height to that value. To make sure the size hint is actually called, I refresh the view when the spinbox value is changed.
My question is this: in such cases of purely cosmetic changes, what is the recommended way to tell the view to refresh? Is there some method built specifically for such cases?
Obviously, this question assumes my general strategy for adjusting row height is sound, which I am also open to correction on.
There are a few methods for telling the view that it is time to refetch the data and redraw things: layoutChanged, reset, setModel, dataChanged. Hell, I found that even just calling expandAll on the tree was enough to update my view to show the new row height.
In practice, I found using layoutChanged works:
QtGui.QStandardItemModel.layoutChanged.emit()
It is sort of uncommon usage, as that is more for when you have rearranged your data (e.g., by sorting). This is what I include in the SSCCE below, because it works. I also tried following the more commonly suggested practice of emitting dataChanged:
QtGui.QStandardItemModel.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())
This does not work for me. Even if it did, it would also be something of a hack, because it is telling the view that the data has changed in the model. When it hasn't.
At any rate, there is a lot of discussion online about what to do when you change the data in your model (see Relevant Posts), but none I have found about what to do when you just want to simply refresh the view for purely cosmetic reasons.
Cross post
I posted the same question at Qt Centre:
http://www.qtcentre.org/threads/63982-Best-way-to-refresh-view-for-cosmetic-%28non-model%29-changes
I got an answer there that have incorporated into the accepted answer below.
Relevant posts
Qt Model-View update view?
PyQt - Automatically refresh a custom view when the model is updated?
http://www.qtcentre.org/threads/48230-QTreeView-How-to-refresh-the-view
http://www.qtcentre.org/threads/3145-QTableView-How-to-refresh-the-view-after-an-update-of-the-model
SSCCE
import sys
from PySide import QtGui, QtCore
class MainTree(QtGui.QMainWindow):
def __init__(self, parent = None):
QtGui.QMainWindow.__init__(self)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.createRowHeightSpinbox() #create first otherwise get errors
self.tree = SimpleTree(self)
self.setCentralWidget(self.tree)
#Add spinbox to toolbar
self.rowHeightAction = QtGui.QAction("Change row height", self)
self.toolbar = self.addToolBar("rowHeight")
self.toolbar.addWidget(QtGui.QLabel("Row height "))
self.toolbar.addWidget(self.rowHeightSpinBox)
#Expand and resize tree
self.tree.expandAll()
self.tree.resizeColumnToContents(0)
self.tree.resizeColumnToContents(1)
def createRowHeightSpinbox(self):
self.rowHeightSpinBox = QtGui.QSpinBox()
self.rowHeightSpinBox.setRange(10, 50)
self.rowHeightSpinBox.setValue(18)
self.rowHeightSpinBox.valueChanged.connect(self.refreshView) #showimage uses the spinbox attribute to scale image
def refreshView(self):
self.tree.model.layoutChanged.emit()
class SimpleTree(QtGui.QTreeView):
def __init__(self, parent = None):
QtGui.QTreeView.__init__(self, parent)
self.setUniformRowHeights(True) #optimize
self.model = QtGui.QStandardItemModel()
self.rootItem = self.model.invisibleRootItem()
item0 = [QtGui.QStandardItem('Sneeze'), QtGui.QStandardItem('You have been blocked up')]
item00 = [QtGui.QStandardItem('Tickle nose'), QtGui.QStandardItem('Key first step')]
item1 = [QtGui.QStandardItem('Get a job'), QtGui.QStandardItem('Do not blow it')]
self.rootItem.appendRow(item0)
item0[0].appendRow(item00)
self.rootItem.appendRow(item1)
self.setModel(self.model)
self.setItemDelegate(ExpandableRows(self))
class ExpandableRows(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
QtGui.QStyledItemDelegate.__init__(self, parent)
self.parent = parent
def sizeHint(self, option, index):
rowHeight = self.parent.window().rowHeightSpinBox.value()
text = index.model().data(index)
document = QtGui.QTextDocument()
document.setDefaultFont(option.font)
document.setPlainText(text) #for html use setHtml
return QtCore.QSize(document.idealWidth() + 5, rowHeight)
def main():
app = QtGui.QApplication(sys.argv)
#myTree = SimpleTree()
myMainTree = MainTree()
myMainTree.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Solution
The principled solution is to emit QAbstractItemDelegate.sizeHintChanged when the spinbox value changes. This is because you only want to call sizeHint of your delegate, and that's exactly what this method does.
In the example in the OP, the size hint is intended to change when the value in the spinbox is changed. You can connect the valueChanged signal from the spinbox to the delegate's sizeHintChanged signal as follows:
class ExpandableRows(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
QtGui.QStyledItemDelegate.__init__(self, parent)
self.parent = parent
self.parent.window().rowHeightSpinBox.valueChanged.connect(self.emitSizeChange)
def emitSizeChange(self):
self.sizeHintChanged.emit(QtCore.QModelIndex())
Analysis
As indicated in the OP, you don't want to call dataChanged because it doesn't actually work, and because your data hasn't actually changed. Further, while calling layoutChanged works, it is less principled because it is technically meant to be used to tell the view that the model's items have been rearranged, which they have not.
Caveat: I believe that sizeHintChanged expects to use a valid index, but my solution is working with the invalid index. Because it works, I'm leaving it with the invalid QtCore.QModelIndex(). Perhaps someone can find an improvement on that and edit this answer.
Is principled better than fast?
Note when you do it the principled way, it actually is a little bit slower than using the layoutChanged trick. Specifically running layoutChanged takes about 70 microseconds, while emitting sizeHintChanged takes about 100 microseconds. This didn't depend on the size of the models I tested (up to 1000 rows). This difference of 30 microseconds is so small as to be negligible in most applications, but if someone really wants to fully optimize for speed, they might go with the layoutChanged trick.
The layoutChanged trick also has the benefit of being simpler: it doesn't involve messing around with the delegate, but uses intuitively simple methods on the main window class. Also because it doesn't depend on any methods being implemented in the delegate, the trick (arguably) seems to be more modular. The "proper" way depends on creating a more brittle dependence between the delegate and the main window, which means it will be easier to break the application when developers modify one or the other.
In sum, a case could be made that in just about every measurable way, the hack from the OP is better than the "principled" solution here.
Acknowledgment
I got turned on to the existence of sizeHintChanged from answers at the same question that I cross-posted at QtCentre:
http://www.qtcentre.org/threads/63982-Best-way-to-refresh-view-for-cosmetic-%28non-model%29-changes

Detecting enter on a QLineEdit or QPushButton

I've built an app for a game, simple to start. It's a game in which the system randomly chooses a number and a gamer (player) tries to find out the number. Everything is almost done. The app consists of a QLineEdit, a label and three buttons. Once the app tells the player the range of the wanted number, he/she types a bet_number and clicks on the play button. And according to this number he/she gets a message about how close or far the wanted number is away from the bet_number.
But I find it a little disgusting to click a button. Instead I want to use Enter key to play. So to achieve this, it comes down to specifically two questions:
How could one change to using Enter to play (I mean I need know when QLineEdit detects enter key is pressed)? In this way I'll code properly to point the play method.
If the play button's got the focus, how do you use enter key on this button? (make Button accept Enter key)
For the QLineEdit connect to the returnPressed signal.
Alternatively, if you use the setAutoDefault method on your QPushButtons you emit the clicked signal when Enter is pressed on a focused QPushButton:
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import sip
sip.setapi('QString', 2)
sip.setapi('QVariant', 2)
from PyQt4 import QtGui, QtCore
class MyWindow(QtGui.QWidget):
def __init__(self, parent=None):
super(MyWindow, self).__init__(parent)
self.pushButtonOK = QtGui.QPushButton(self)
self.pushButtonOK.setText("OK")
self.pushButtonOK.clicked.connect(self.on_pushButtonOK_clicked)
self.pushButtonOK.setAutoDefault(True)
self.lineEditNumber = QtGui.QLineEdit(self)
self.lineEditNumber.returnPressed.connect(self.pushButtonOK.click)
self.layoutHorizontal = QtGui.QHBoxLayout(self)
self.layoutHorizontal.addWidget(self.pushButtonOK)
self.layoutHorizontal.addWidget(self.lineEditNumber)
#QtCore.pyqtSlot()
def on_pushButtonOK_clicked(self):
inputNumber = self.lineEditNumber.text()
if inputNumber.isdigit():
info = "You selected `{0}`"
else:
info = "Please select a number, `{0}` isn't valid!"
print info.format(inputNumber)
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
app.setApplicationName('MyWindow')
main = MyWindow()
main.show()
sys.exit(app.exec_())
QLineEdit will emit the signal returnPressed() whenever the user presses the enter key while in it: http://qt-project.org/doc/qt-4.8/qlineedit.html#signals. You can either connect this signal to your button's click() slot or directly call whatever your button's clicked() signal was connected to.
A slight C++ variation on other answers, it's not significantly different, but I thought i would include it anyway because how you lay things in QT code can be very different from codebase to codebase and I wanted to strip out the extraneous stuff to give the shortest and easiest to understand excerpt of code.
QLineEdit *TextSend = new QLineEdit("");
QPushButton *SendPB = new QPushButton("Send!");
connect(TextSend, &QLineEdit::returnPressed, this, &CLITab::SendCommand);
connect(SendPB, &QPushButton::released, this, &CLITab::SendCommand);
So what this is doing is we create a QLineEdit textbox and a QPushbutton.
We do cosmetic things like set the string label for them and add them to our layout.
Then we setup a callback handler, which will be triggered when the QLineEdit returns "returnPressed", which then calls automatically into a function which i wrote called "CLITab::SendCommand()", and then it's upto this function to extract the data out of QLineEdit and do whatever needs to be done. In practice the TextSend and SendPB pointers would live in the parent class, so that SendCommand() has visibility over these objects.
Just putting this here, along side an example pushbutton, because essentially they work in precisely the same way, all that's different is the signal name emitted.

QDataWidgetMapper and multiple delegates

mapper = QtGui.QDataWidgetMapper()
mapper.setModel(my_table_model)
mapper.addMapping(widgetA, 0) #mapping widget to a column
mapper.addMapping(widgetB, 1) #mapping widget to a column
mapper.setItemDelegate(MyDelegateA(widgetA)) #Hmm. Where is the 'column' parameter?
mapper.setItemDelegate(MyDelegateB(widgetB)) #now itemDelegate is rewritten, MyDelegateB will be used
So... How do I set up mutiple delegates for a single QDataWidgetMapper? As far as I understand there is no QDataWidgetMapper.setItemDelegateForColumn() Or do I have to create some delegate factory, which will use appropriate delegates?
Thanks!
You have to use one single delegate and handle the way behavior of the different widgets in the setEditorData and setModelData functions of the delegate. For an example (C++ but straight forward) check this article from Qt Quarterly.
Ok, I got it. (At least, it works for me). So, the main idea is this class (a simplified version), which keeps a list of real delegate instances and routes data to\from them:
class DelegateProxy(QtGui.QStyledItemDelegate):
def __init__(self, delegates, parent=None):
QtGui.QStyledItemDelegate.__init__(self, parent)
self.delegates = delegates
def setEditorData(self, editor, index):
delegate = self.delegates[index.column()]
delegate.setEditorData(editor, index)
def setModelData(self, editor, model, index):
delegate = self.delegates[index.column()]
delegate.setModelData(editor, model, index)
Fully working example is in the pastebin
I found this problem too, and it really sucks. I'm right now trying to subclass QtGui.QDataWidgetMapper in order to workaround this, the subclass having its own addMapping() with a delegate argument, a dict to store the delegate for each widget, and a matching meta-delegate that calls the appropiate delegate for each case.
The weirdest thing about this is the problem also existed in earlier versions of Qt 4 in QAbstractItemView (i.e tables and trees) and later was fixed adding the setItemDelegateForColumn() method, but QDataWidgetMapper didn't get the fix.
An alternative could be using more than a mapper, and connect them to keep them in sync if necessary, but it is a bit messy, specially if you need lots of different special delegates:
mainMapper = QtGui.QDataWidgetMapper()
mainMapper.setModel(my_table_model)
auxMapper1 = QtGui.QDataWidgetMapper()
auxMapper1.setModel(my_table_model)
# If you move the index in the main mapper, the auxiliary will follow
mainMapper.currentIndexChanged.connect(auxMapper1.setCurrentIndex)
mainMapper.addMapping(widgetA, 0) #mapping widget to a column
auxMapper1.addMapping(widgetB, 1) #mapping widget to a column
mainMapper.setItemDelegate(MyDelegateA(widgetA))
auxMapper1.setItemDelegate(MyDelegateB(widgetB))

Set bold rows in a QTreeView

I have a custom subclass of a QTreeView in a pyqt application. I'm trying to give the user the ability to highlight and "lowlight" (for lack of a better term) rows. Highlighted rows should have bold text and (optionally) a different background color. Any ideas?
I'm considering StyleSheets as an option, but have so far been unable to get that to work. If I set the QTreeView's stylesheet:
self.setStyleSheet("QTreeView::item:selected {border: 1px solid #567dbc;}")
I can't figure out how to manually enable 'states' that would keep only the desired rows at a particular state. If I try setting an individual item's stylesheet:
#modelIndex is a valid QModelIndex
modelIndex.internalPointer().setStyleSheet()
I get a segfault.
I'm not convinced stylesheets are the way to go, I'm open to all ideas. Thanks!
From what you've said it looks like the easiest solution would be to define a custom item delegate for your treeview and set items font weight to bold whenever it's needed. Pls, check if an example below would work for you, it should create a treeview with custom item delegate which would change item's font style.
import sys
from PyQt4 import QtGui, QtCore
class BoldDelegate(QtGui.QStyledItemDelegate):
def paint(self, painter, option, index):
# decide here if item should be bold and set font weight to bold if needed
option.font.setWeight(QtGui.QFont.Bold)
QtGui.QStyledItemDelegate.paint(self, painter, option, index)
class MainForm(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainForm, self).__init__(parent)
model = QtGui.QStandardItemModel()
for k in range(0, 4):
parentItem = model.invisibleRootItem()
for i in range(0, 4):
item = QtGui.QStandardItem(QtCore.QString("item %0 %1").arg(k).arg(i))
parentItem.appendRow(item)
parentItem = item
self.view = QtGui.QTreeView()
self.view.setModel(model)
self.view.setItemDelegate(BoldDelegate(self))
self.setCentralWidget(self.view)
def main():
app = QtGui.QApplication(sys.argv)
form = MainForm()
form.show()
app.exec_()
if __name__ == '__main__':
main()
hope this helps, regards
I can think of a few ways to do this. The easiest, if you have access to the model, is to add some state tracking of the indexes in the model, and return the proper options for the roles requested in the data() function. The drawback to this is if you are using the same model in different views, and want to keep the highlights local to one view.
The second-easiest is probably to make a proxy model, which keeps track of the data itself, and gets all other data from the original model. In this situation (where you aren't changing the rows or columns of the original model) it would probably be pretty easy.
The hardest would be to make a custom delegate that keeps track of which rows/columns should be highlighted, and draws itself differently based on the row/column of the model index it is drawing. You would have to keep access to the delegate so that you could tell it which rows and columns need to be set. You would also need to deal with what happens when the model changes.

Resources