QDataWidgetMapper and multiple delegates - qt

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

Related

Is there a way how to automatically expose QAbstractItemModel's row functions to QML?

Since Qt 5.6 we can finally write code like this:
ListView {
id: list
model: MyModel
delegate: TextInput {
text: display
onEditingFinished: {
model.edit = displayText
}
}
i.e. model.edit will call MyModel's setData() with Qt::EditRole and display value from the TextInput. Great, was headache for a long time.
However even if using QAbstractItemModel is the recommended practice for more complex C++ based models I still have the feeling that all of it is meant only for read only models, i.e. that a qml view can read the number of rows, columns etc but it was never meant as a way for adding or removing rows (for clean implementation of QAbstractItemModel::setData the row must be already present).
It feels really dirty to reimplement all the insert/remove functions with Q_INVOKABLE and qml's ListModel is far too simple for anything serious.
What would you recommend for a qml based widget which should add/remove rows, edit items and yet have a C++ model?
Reimplementing the insertRows() and removeRows() for your new subclass of QAbstractListModel or QAbstractItemModel is not dirty, it's normal!
Notice that insertRows() and removeRows() are marked virtual in the base class, indicating just that.
The base class does not know how to manipulate your data structure, as it could be a QList, or it could be something much more complicated like a SQL database or a 3rd party library.
In your class definition you can either mark the methods as Q_INVOKABLE or as public slots. Note also that several functions in Qt models classes - both virtual and non - are already marked as invokable, see e.g. here.
I recommend you to override insertRows() and removeRows() methods which are Q_INVOKABLE and takes index as integer and invokes actual insertRows() and removeRows() methods.
So you can use both widget and qml UI approaches.
I think it is not dirty because you just make that ready for different usage and also override is normal.

How to add extra data to qfilesystemmodel items?

I'm trying to make an pyqt app, which examines directory structure and the files in it.
What I want to do is to add some extra info to the items in the qfilesystemmodel, for example, tagging files as 'checked' or 'unchecked'.
I have found that each item (file or folder) in the model has four columns, name, size, timestamp and type.
Is it possible to make the item have fifth column, which will contain custom information (tags) ?
Or, Is there any way to annotate extra info on the items in the model ?
If not, I think I have to have another model, such as qstanarditemmodel, to keep the tags. But I don't want it to be in a saperated model.
Thanks!
The QFileSystemModel class delegates to a live file-system. So it doesn't really contain any items that you can add information to. The items are actually the files and directories inside the file-system.
Given this, it follows that a second data structure will be required to hold the additional information. Otherwise, you'd need to somehow store the information in the files and directories themselves. This might be possible for certain file-types (e.g. images). But it is obviously unfeasible to do this for arbitrary file-types.
It should be possible to sub-class QFileSystemModel and reimplement the usual methods so that extra columns can be added. As a bare minimum, you would need to reimplement columnCount() and data() - but obviously the exact details of how you go about this will depend on the data structure you choose to hold the additional information. Very roughly, it might look something like this:
class FileSystemModel(QFileSystemModel):
def __init__(self, parent=None):
super(FileSystemModel, self).__init__(parent)
self._data = DataStructure()
def columnCount(self, parent):
return super(FileSystemModel, self).columnCount(parent) + 1
def data(self, index, role):
if index.isValid() and index.column() == self.columnCount() - 1:
if role == QtCore.Qt.DisplayRole:
# return the relevant additional data
elif role == QtCore.Qt.CheckStateRole:
# etc, etc
else:
return super(FileSystemModel, self).data(index,role)
In short, no, not easily. The Qt developers stopped working on QFileSystemModel for complexity reasons. Basically, the backend file system engine is way too complex and fragile, and QFileSystemModel relies entirely upon it.
You could reimplement QFileSystemModel, but it would require a lot of painful work and accessing the private headers.

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

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

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.

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