Clickable elements or child widgets inside custom-painted delegate - qt

I have a QListView, where I display items using a custom delegate with custom painting. Within each item (i.e. each list row) I want to be able to show a couple of "hyperlinks" which the user could click on and which would then call on some functions.
I have already tried to check the official documentation (e.g. Model/View Programming) as well as quite a lot of googling, but haven't been able to figure out how to accomplish this.
I have two ideas, each with their own problems:
I could draw them using child widgets, like a flat QPushButton. How do I then position and display these widgets?
I could also draw them as text strings. How do I then make them clickable? Or can I capture click events on the parent QListView and somehow determine coordinates from those? I could then match coordinates to these clickable elements and act accordingly.
My initial approach was to use QListWidget with .setItemWidget(), where I had a proper widget with a layout and child widgets. Unfortunately this was too slow when my list grew to hundreds or thousands of items. That's why I changed to QListView with a delegate.

I seem to be closing in on a solution.
I can receive clicks on the elements by overriding the delegate's .editorEvent(event, model, option, index). I can then find out the event.type(), the clicked row from index.row() and the actual coordinates from event.x() and event.y() (since, if the event type is MouseButtonRelease, the event is a QMouseEvent).
From these, I think I can correlate the coordinates to my elements on screen and act accordingly.
I will update this answer once I have working code.
EDIT
A simple working example, using PySide:
class MyModel(QtGui.QStandardItemModel):
def __init__(self):
super(MyModel, self).__init__()
for i in range(10): self.appendRow(QtGui.QStandardItem("Row %d" % i))
class MyDelegate(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
super(MyDelegate, self).__init__(parent)
self.links = {}
def makeLinkFunc(self, row, text):
def linkFunc(): print("Clicked on %s in row %d" % (text, row))
return linkFunc
def paint(self, painter, option, index):
painter.save()
textHeight = QtGui.QFontMetrics(painter.font()).height()
painter.drawText(option.rect.x()+2, option.rect.y()+2+textHeight, index.data())
rowLinks = {}
for i in range(3):
text = "Link %d" % (3-i)
linkWidth = QtGui.QFontMetrics(font).width(text)
x = option.rect.right() - (i+1) * (linkWidth + 10)
painter.drawText(x, y, text)
rect = QtCore.QRect(x, y - textHeight, linkWidth, textHeight)
rowLinks[rect] = self.makeLinkFunc(index.row(), text)
self.links[index.row()] = rowLinks
painter.restore()
def sizeHint(self, option, index):
hint = super().sizeHint(option, index)
hint.setHeight(30)
return hint
def editorEvent(self, event, model, option, index):
if event.type() == QtCore.QEvent.MouseButtonRelease:
for rect, link in self.links[index.row()].items():
if rect.contains(event.pos()):
link()
return True
return False
listmodel = MyModel()
listview = QtGui.QListView()
listview.setModel(listmodel)
listview.setItemDelegate(MyDelegate(parent=listview))
listview.setSelectionMode(QtGui.QAbstractItemView.NoSelection)

Related

Implementing a delegate for wordwrap in a QTreeView (Qt/PySide/PyQt)?

I have a tree view with a custom delegate to which I am trying to add word wrap functionality. The word wrapping is working fine, but the sizeHint() seems to not work, so when the text wraps, the relevant row does not expand to include it.
I thought I was taking care of it in sizeHint() by returning document.size().height().
def sizeHint(self, option, index):
text = index.model().data(index)
document = QtGui.QTextDocument()
document.setHtml(text)
document.setTextWidth(option.rect.width())
return QtCore.QSize(document.idealWidth(), document.size().height())
However, when I print out document.size().height() it is the same for every item.
Also, even if I manually set the height (say, to 75) just to check that things will look reasonable, the tree looks like a goldfish got shot by a bazooka (that is, it's a mess):
As you can see, the text in each row is not aligned properly in the tree.
Similar posts
Similar issues have come up before, but no solutions to my problem (people usually say to reimplement sizeHint(), and that's what I am trying):
QTreeWidget set height of each row depending on content
QTreeView custom row height of individual rows
http://www.qtcentre.org/threads/1289-QT4-QTreeView-and-rows-with-multiple-lines
SSCCE
import sys
from PySide import QtGui, QtCore
class SimpleTree(QtGui.QTreeView):
def __init__(self, parent = None):
QtGui.QTreeView.__init__(self, parent)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.setGeometry(500,200, 400, 300)
self.setUniformRowHeights(False) #optimize: but for word wrap, we don't want this!
print "uniform heights in tree?", self.uniformRowHeights()
self.model = QtGui.QStandardItemModel()
self.model.setHorizontalHeaderLabels(['Task', 'Description'])
self.setModel(self.model)
self.rootItem = self.model.invisibleRootItem()
item0 = [QtGui.QStandardItem('Sneeze'), QtGui.QStandardItem('You have been blocked up')]
item00 = [QtGui.QStandardItem('Tickle nose, this is a very long entry. Row should resize.'), QtGui.QStandardItem('Key first step')]
item1 = [QtGui.QStandardItem('<b>Get a job</b>'), QtGui.QStandardItem('Do not blow it')]
self.rootItem.appendRow(item0)
item0[0].appendRow(item00)
self.rootItem.appendRow(item1)
self.setColumnWidth(0,150)
self.expandAll()
self.setWordWrap(True)
self.setItemDelegate(ItemWordWrap(self))
class ItemWordWrap(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
QtGui.QStyledItemDelegate.__init__(self, parent)
self.parent = parent
def paint(self, painter, option, index):
text = index.model().data(index)
document = QtGui.QTextDocument() # #print "dir(document)", dir(document)
document.setHtml(text)
document.setTextWidth(option.rect.width()) #keeps text from spilling over into adjacent rect
painter.save()
painter.translate(option.rect.x(), option.rect.y())
document.drawContents(painter) #draw the document with the painter
painter.restore()
def sizeHint(self, option, index):
#Size should depend on number of lines wrapped
text = index.model().data(index)
document = QtGui.QTextDocument()
document.setHtml(text)
document.setTextWidth(option.rect.width())
return QtCore.QSize(document.idealWidth() + 10, document.size().height())
def main():
app = QtGui.QApplication(sys.argv)
myTree = SimpleTree()
myTree.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
The issue seems to stem from the fact that the value for option.rect.width() passed into QStyledItemDelegate.sizeHint() is -1. This is obviously bogus!
I've solved this by storing the width in the model from within the paint() method and accessing this from sizeHint().
So in your paint() method add the line:
index.model().setData(index, option.rect.width(), QtCore.Qt.UserRole+1)
and in your sizeHint() method, replace document.setTextWidth(option.rect.width()) with:
width = index.model().data(index, QtCore.Qt.UserRole+1)
if not width:
width = 20
document.setTextWidth(width)

Changing text direction in QTableView header

I would like to have a table view in Qt, where the header text would go in vertical direction:
Either like in green square or like in red?
You can achieve that by providing your own implementation of QHeaderView. Here is an example implementation, which overrides the paintSection method to paint the text vertically.
class MyHeaderView(QtWidgets.QHeaderView):
def __init__(self, parent=None):
super().__init__(Qt.Horizontal, parent)
self._font = QtGui.QFont("helvetica", 15)
self._metrics = QtGui.QFontMetrics(self._font)
self._descent = self._metrics.descent()
self._margin = 10
def paintSection(self, painter, rect, index):
data = self._get_data(index)
painter.rotate(-90)
painter.setFont(self._font)
painter.drawText(- rect.height() + self._margin,
rect.left() + (rect.width() + self._descent) / 2, data)
def sizeHint(self):
return QtCore.QSize(0, self._get_text_width() + 2 * self._margin)
def _get_text_width(self):
return max([self._metrics.width(self._get_data(i))
for i in range(0, self.model().columnCount())])
def _get_data(self, index):
return self.model().headerData(index, self.orientation())
You can use this class in your view as follows:
headerView = MyHeaderView()
tableView.setHorizontalHeader(headerView)
And it will result in the following view:
As a side note I want to add that for regular items, you would rather provide your own item delegate. However, as noted in the Qt documentation:
Each header renders the data for each section itself, and does
not rely on a delegate. As a result, calling a header's
setItemDelegate() function will have no effect.

QTableView signal or event for "row height changed"

I' m looking for an Event or Signal like "row height changed" that is called if the user changes the height of a row in my QTableView. I want to use this signal to resize all other rows in the QTableView to the new height. I didn' t find such an Event or Signal so I reckon there must be some kind of handy workaround.
Any suggestions will be appreciated.
Row resizing is performed by the vertical QHeaderView. The object emmiting it is QTableView::verticalHeader()
the signal you are interested in is
void QHeaderView::sectionResized ( int logicalIndex, int oldSize, int newSize )
This signal is emitted when a section is resized. The section's logical number is specified by logicalIndex, the old size by oldSize, and the new size by newSize.
Use QHeaderView (You can get an instance by calling QTableView::horizontalHeader()/QTableView::verticalHeader() ) and connect to geometriesChanged() or sectionResized()
The solution with QHeaderView and sectionResized works very well for me. So for resizing all
rows to the same height as the edited row I use this code now:
class MyWidget(QWidget):
def __init__(self, parent=None):
#Do some other init things
self.myTable.verticalHeader().sectionResized.connect(
self.row_resized)
def row_resized(self, index, old_size, new_size):
for i in range(self.myTable.verticalHeader().count()):
self.myTable.setRowHeight(i, new_size)
I wanted to save data after a section was resized, but found sectionResized fired a signal continuously while adjusting, rather than once at the end.
The simplest solution I could think of was to subclass the QHeaderView.
class CustomHeader(QtWidgets.QHeaderView):
"""
Custom header class to return when a section has been resized
Only after mouse released, opposed to continuously while resizing
"""
on_section_resized = QtCore.Signal(int)
def __init__(self, *args, **kwargs):
super(CustomHeader, self).__init__(*args, **kwargs)
self._has_resized = False
self._index = 0
self.sectionResized.connect(self.section_resized)
def section_resized(self, index, old_size, new_size):
self._has_resized = True
self._index = index
return
def mouseReleaseEvent(self, event):
super(CustomHeader, self).mouseReleaseEvent(event)
if self._has_resized:
self._has_resized = False
self.on_section_resized.emit(self._index)
return
In my main class I assigned the header to the table widget:
self.table_widget.setVerticalHeader(CustomHeader(QtCore.Qt.Vertical, self.table_widget))
Then connected the custom signal to my function within the original class:
self.table_widget.verticalHeader().on_section_resized.connect(self.save_section_height)
Part of the Save function:
def save_section_height(self, row):
"""
Updates the edited row's section height
:param row: int, row that has been changed
:return: None
"""
new_section_height = self.table_widget.verticalHeader().sectionSize(row)
I expect some things could be optimised better, but this at least fires one save signal opposed to ten plus, or something!

Moving QGraphicsItem by mouse in a QGraphicsScene

I have a QGraphicsScene and its associated QGraphicsView. I let the user create some shapes in the form of derived QGraphicsItems into that scene. I also want them to be movable by mouse. Clicking one or more items select them, and moving them around while the mouse button is pressed works. I inherited QGraphicsView to do this, and overrided mousePressedEvent, mouseReleasedEvent & mouseMoveEvent to achieve this. When the user clicks, I am basically testing if an item (accessed through items() which returns the items of the associated scene) is under the mouse with contains(), and if it is then I am selecting it.
In the mouseMoveEvent, I am using setPos() on each item of the selection to move it relatively to the mouse move. It works and displays as expected.
This may not be the most efficient way, but that's what I achieved while discovering Qt. Now, the problem is : once I've moved my item or group of items with the mouse, if I want to deselect them (by clicking again on them), the contains() method supplied with the position of the input acts as if the item wasn't moved. Example : I draw a rectangle in the upper left corner, and move it around to, say, the center of the view. Clicking on it again doesn't work but clicking on where it was initially works. So I suspect it has something to do with local and global coordinates.
I've run through several problems today (most of them resolved) but I'm stuck on this one.
Here's my View class :
class CustomGraphicsView(QGraphicsView):
def __init__(self, *args):
super().__init__(*args)
self.selection = []
self.offsets = []
self.select_point = None
def mousePressEvent(self, event):
pos = self.mapFromGlobal(event.globalPos())
modifiers = event.modifiers()
if event.button() == Qt.LeftButton:
#do something else
elif event.button() == Qt.RightButton:
self.select_point = pos
for s in self.selection:
if s.contains(pos): # deselect or drag
for s in self.selection: # construct the offsets for dragging
self.offsets = [s.pos() - pos for s in self.selection]
break
def mouseReleaseEvent(self, event):
pos = self.mapFromGlobal(event.globalPos())
modifiers = event.modifiers()
if event.button() == Qt.LeftButton:
#do something else
elif event.button() == Qt.RightButton:
if self.select_point == pos: # one click selection
self.update_selection(pos)
if self.offsets:
self.offsets.clear()
def mouseMoveEvent(self, event):
pos = self.mapFromGlobal(event.globalPos())
modifiers = event.modifiers()
if event.buttons() == Qt.RightButton:
if not self.offsets:
for s in self.selection:
self.offsets = [s.pos() - pos for s in self.selection]
for s, off in zip(self.selection, self.offsets):
s.set_pos(pos + off)
def update_selection(self, pos):
for item in self.items():
if not item.contains(pos):
continue
if item.selected:
self.selection.remove(item)
else:
self.selection.append(item)
item.select()
break
The scene rect is set at (0;0) so there's no concern about moving it or whatever.

How to add QInputDialog.getText text inside a QGraphicsPolygonItem?

I'm building in PyQt4 and can't figure out how to add text to a QGraphicsPolygonItem. The idea is to have text set in the middle of a rectangular box after a user double clicks (and gets a dialog box via QInputDialog.getText).
The class is:
class DiagramItem(QtGui.QGraphicsPolygonItem):
def __init__(self, diagramType, contextMenu, parent=None, scene=None):
super(DiagramItem, self).__init__(parent, scene)
path = QtGui.QPainterPath()
rect = self.outlineRect()
path.addRoundRect(rect, self.roundness(rect.width()), self.roundness(rect.height()))
self.myPolygon = path.toFillPolygon()
My double mouse click event looks like this, but updates nothing!
def mouseDoubleClickEvent(self, event):
text, ok = QtGui.QInputDialog.getText(QtGui.QInputDialog(),'Create Region Title','Enter Region Name: ', \
QtGui.QLineEdit.Normal, 'region name')
if ok:
self.myText = str(text)
pic = QtGui.QPicture()
qp = QtGui.QPainter(pic)
qp.setFont(QtGui.QFont('Arial', 40))
qp.drawText(10,10,200,200, QtCore.Qt.AlignCenter, self.myText)
qp.end()
Well, you are not doing it correctly. You are painting to a QPicture (pic) and throwing it away.
I'm assuming you want to paint on the QGraphicsPolygonItem. paint method of QGraphicsItem (and its derivatives) is responsible for painting the item. If you want to paint extra things with the item, you should override that method and do your painting there:
class DiagramItem(QtGui.QGraphicsPolygonItem):
def __init__(self, diagramType, contextMenu, parent=None, scene=None):
super(DiagramItem, self).__init__(parent, scene)
# your `init` stuff
# ...
# just initialize an empty string for self.myText
self.myText = ''
def mouseDoubleClickEvent(self, event):
text, ok = QtGui.QInputDialog.getText(QtGui.QInputDialog(),
'Create Region Title',
'Enter Region Name: ',
QtGui.QLineEdit.Normal,
'region name')
if ok:
# you can leave it as QString
# besides in Python 2, you'll have problems with unicode text if you use str()
self.myText = text
# force an update
self.update()
def paint(self, painter, option, widget):
# paint the PolygonItem's own stuff
super(DiagramItem, self).paint(painter, option, widget)
# now paint your text
painter.setFont(QtGui.QFont('Arial', 40))
painter.drawText(10,10,200,200, QtCore.Qt.AlignCenter, self.myText)

Resources