PDA

View Full Version : Custom QItemDelegate: mouse hover, margins, colors?



fede
26th May 2010, 02:15
Hello,

I am playing around with a custom delegate for a QTableWidget, which just shows a combobox as an editor, stores the value as a number (which is a foreign key to a list), and displays the text corresponding to the item at that value's position in a list. For example: it stores 1 and displays 'one' if the list is ['someitem', 'one'], and stores 4 and displays 'cow' if the list is ['bird', 'horse', 'pig', 'snake', 'cow'].

My problem is I cannot get the appearance of the cells which use the custom delegate 'right' - which in this case would be the same as the default delegate. In particular:

Hovering normal cells with the mouse produces a shadow over the hovered cell. How can I reproduce this effect in my custom delegate? I have tried to find a custom delegate with a working "hover" effect in the examples, without luck. Both the star delegate and the spin box delegate examples have the same problem in the columns affected.
My delegate's painter does not draw using the same cell margins as the default delegate's painter. How should I set these margins in my custom delegate?
Which is the correct way of looking up colors and effects to be used by the custom delegate's paint method so that it mimics the default delegate's style? In particular, the selected item under the default delegate has a sublte "glow" texture in its background,which is not reproduced by my delegate's paint method, which simply uses a solid color as background. I couldn't find this effect in the examples, either, they too seem to use a solid color as background when using custom delegates. (Edited to clarify)

Thanks!


#! /usr/bin/env python
# -*- coding: utf-8 -*-
import sys
from PyQt4 import QtCore
from PyQt4 import QtGui

class QRelationalDelegate(QtGui.QStyledItemDelegate):
VALUES = ['zero', 'one', 'two', 'three', 'four']

def paint(self, painter, option, index):
value = index.data(QtCore.Qt.DisplayRole).toInt()[0] # integer stored in tablewidget model
text = self.VALUES[value] # text to be displayed
painter.save()
if option.state & QtGui.QStyle.State_Selected: # highligh background if selected
painter.fillRect(option.rect, option.palette.highlight())
painter.drawText(option.rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, text)
painter.restore()

def createEditor(self, parent, option, index):
combobox = QtGui.QComboBox(parent)
combobox.addItems(self.VALUES)
combobox.setEditable(True)
return combobox

def setEditorData(self, editor, index):
text = self.VALUES[index.data(QtCore.Qt.DisplayRole).toInt()[0]]
pos = editor.findText(text)
if pos == -1: #text not found, set cell value to first item in VALUES
pos = 0
editor.setCurrentIndex(pos)

def setModelData(self, editor, model, index):
model.setData(index, QtCore.QVariant(editor.currentIndex()))


class myWindow(QtGui.QDialog):
def __init__(self):
QtGui.QDialog.__init__(self)
DATA = [['First row', 1, 1], ['Second Row', 2, 2]]
self.table = QtGui.QTableWidget(self)
self.table.setSortingEnabled(False)
self.table.setRowCount(len(DATA))
self.table.setColumnCount(len(DATA[0]))
self.table.setItemDelegateForColumn(1, QRelationalDelegate(self))
for row in range(len(DATA)):
for col in range(len(DATA[row])):
item = QtGui.QTableWidgetItem(str(DATA[row][col]))
self.table.setItem(row, col, item)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.table)
self.setLayout(layout)


if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
window = myWindow()
window.show()
app.exec_()

Attached is a picture the hover effect which works for columns 0 and 2 in my example but not for column 1 which uses the custom delegate.4687

yetien
30th May 2010, 02:24
Hi!

I had the same question but I think I finally found a nice solution and thought to share it.

I can't answer all your questions, but as I understand it you were looking for a way to display text in a cell with a custom delegate of a tableview. Furthermore this cell should look in case of selection and mouse hovering exactly like the cells which are rendered by the default delegate.

The following should work:


class myDelegate(QStyledItemDelegate):
def __init__(self, parent = None):
super(myDelegate, self).__init__(parent)
def paint(self, painter, option, index):
styleOption = QStyleOptionViewItemV4(option)

# define the text to be shown
styleOption.text = "mytext"
# paint the cell
self.parent().style().drawControl(QStyle.CE_ItemVi ewItem, styleOption, painter)

Now hovering and selecting looks like it should. For self.parent().style() to work I made the tableview parent of the delegate. You could also use QApplication.style() or any other Style you like.

It is also possible to let Qt render the item with drawControl first. Afterwards, you can use the painter to paint sth over the nicely rendered background.

However, I havn't figured out how to determine the colors/painting effects (esp. the hovering color) and the margins.

Have fun!

fede
31st May 2010, 13:55
Thanks a lot, that is exactly what I needed!

I have no need to determine myself the colors and painting effects for the time being, your suggestion works perfectly.

self.parent().style() seems to work fine without making the tableview parent of the delegate, in my example self.parent().style().drawControl(...) is calling the main window's qstyle control (myWindow.style().drawControl(...)). Is there a reason for your suggestion to make the tableview parent of the delegate that I'm missing?

It is curious that the working examples in Qt (and in the two books I have on Qt) do not resort to this method and fail to reproduce the default style's behaviour. Which makes your reply awesome. Thanks a lot, again!

Here is the working code using your suggestion, which I post for reference: an example of a tablewidget with a custom delegate which is a simple combobox



#! /usr/bin/env python
# -*- coding: utf-8 -*-
import sys
from PyQt4 import QtCore
from PyQt4 import QtGui

class QRelationalDelegate(QtGui.QStyledItemDelegate):
VALUES = ['zero', 'one', 'two', 'three', 'four']

def __init__(self, parent = None):
super(QRelationalDelegate, self).__init__(parent)

def paint(self, painter, option, index):
styleOption = QtGui.QStyleOptionViewItemV4(option)
# define the text to be shown
value = index.data(QtCore.Qt.DisplayRole).toInt()[0] # integer stored in tablewidget model
styleOption.text = self.VALUES[value]
# paint the cell
self.parent().style().drawControl(QtGui.QStyle.CE_ ItemViewItem, styleOption, painter)

def createEditor(self, parent, option, index):
combobox = QtGui.QComboBox(parent)
combobox.addItems(self.VALUES)
combobox.setEditable(True)
return combobox

def setEditorData(self, editor, index):
text = self.VALUES[index.data(QtCore.Qt.DisplayRole).toInt()[0]]
pos = editor.findText(text)
if pos == -1: #text not found, set cell value to first item in VALUES
pos = 0
editor.setCurrentIndex(pos)

def setModelData(self, editor, model, index):
model.setData(index, QtCore.QVariant(editor.currentIndex()))


class myWindow(QtGui.QDialog):
def __init__(self):
QtGui.QDialog.__init__(self)
DATA = [['First row', 1, 1], ['Second Row', 2, 2]]
self.table = QtGui.QTableWidget(self)
self.table.setSortingEnabled(False)
self.table.setRowCount(len(DATA))
self.table.setColumnCount(len(DATA[0]))
self.table.setItemDelegateForColumn(1, QRelationalDelegate(self))
for row in range(len(DATA)):
for col in range(len(DATA[row])):
item = QtGui.QTableWidgetItem(str(DATA[row][col]))
self.table.setItem(row, col, item)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.table)
self.setLayout(layout)


if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
window = myWindow()
window.show()
app.exec_()

yetien
15th July 2010, 20:50
Fine that I could help you. :-)


self.parent().style() seems to work fine without making the tableview parent of the delegate, in my example self.parent().style().drawControl(...) is calling the main window's qstyle control (myWindow.style().drawControl(...)). Is there a reason for your suggestion to make the tableview parent of the delegate that I'm missing?

I thought if the tableview has a different stylesheet than the main application (for whatever reason) it would be a little more clean to refer to the "closest" style; no other thoughts beyond that.

witsch
20th October 2010, 17:30
Just what I needed. Although a programmer of 20 years, I struggle very hard with the PyQt stuff. Models are difficult! Anyway many thanks for this wonderful example.

HW

fede
22nd October 2010, 15:34
You're welcome, I'm glad it's useful. I am myself struggling with the Qt models and I do agree they are difficult.

I also wanted to post an update: a slight change to the code above so that the delegate will work with (key, display text) pairs, where key is a meaningful value stored by the model (in my actual program from which this example is taken it is a column in an sql table which is the foreign key to the table where the display text is stored). In any case, the code above just stores the combobox's index values in the model, whereas the code below is slightly more sophisticated and allows for storing arbitrary values in the model, using the UserRole property of the combobox. Hope it's useful to someone out there!


#! /usr/bin/env python
# -*- coding: utf-8 -*-
import sys
from PyQt4 import QtCore
from PyQt4 import QtGui

class MyRelationalDelegate(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
self.choices = {1: 'One', 2: 'Two', 3:'Three', 7: 'Seven'}
super(MyRelationalDelegate, self).__init__(parent)

def paint(self, painter, option, index):
styleOption = QtGui.QStyleOptionViewItemV4(option)
# define the text to be shown
text = self.choices[index.data(QtCore.Qt.DisplayRole).toInt()[0]] # value in choices dict corresponding to integer stored in tablewidget model
styleOption.text = text
# paint the cell
self.parent().style().drawControl(QtGui.QStyle.CE_ ItemViewItem, styleOption, painter)

def createEditor(self, parent, option, index):
combobox = QtGui.QComboBox(parent)
combobox.setEditable(True)
for c in sorted(self.choices.items()):
combobox.addItem(c[1], c[0]) # Add items to combobox, storing display text in DisplayRole and underlying value in UserRole
return combobox

def select_line_editor(self):
self.line_editor.setText("")

def setEditorData(self, editor, index):
pos = editor.findData(index.data().toInt()[0])
if pos == -1: #text not found, set cell value to first item in VALUES
pos = 0
editor.setCurrentIndex(pos)

def setModelData(self, editor, model, index):
model.setData(index, QtCore.QVariant(editor.itemData(editor.currentInde x()))) # editor.currentIndex es la posición actual en el combobox; itemData recupera (por default) la data UserRole almacenada allÃ*, que en este caso es el valor del foreign key


class myWindow(QtGui.QDialog):
def __init__(self):
QtGui.QDialog.__init__(self)
DATA = [['First row', 1, 1], ['Second Row', 2, 2]]
self.table = QtGui.QTableWidget(self)
self.table.setSortingEnabled(False)
self.table.setRowCount(len(DATA))
self.table.setColumnCount(len(DATA[0]))
self.table.setItemDelegateForColumn(1, MyRelationalDelegate(self))
for row in range(len(DATA)):
for col in range(len(DATA[row])):
item = QtGui.QTableWidgetItem(str(DATA[row][col]))
self.table.setItem(row, col, item)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.table)
self.setLayout(layout)


if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
window = myWindow()
window.show()
app.exec_()