Painted image with rounded rect in QTableView seems to be fixed on - python

I'm using a QDelegate to display an image in QTableView with rounded corners. The basic implementation is working, but as soon as I resize the whole QMainWindow where the table is located the image stays fixed at it's original position and the grid lines are moved over the image.
How to mount the image to the cell instead of the table?
I've tried QLabel and QPixmap / QPaint with no result. The working example is enclosed.
import pathlib
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
class TableModel(QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
def data(self, index, role):
if role == Qt.DisplayRole:
return self._data[index.row()][index.column()]
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._data[0])
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.resize(800,600)
self.table = QTableView()
data = [
['Line 1', 'c:/daten/dummy/image1.png', 19],
['Line 2', 'c:/daten/dummy/image2.png', 29],
['Line 3', 'c:/daten/dummy/image3.png', 39],
['Line 4', 'c:/daten/dummy/image4.png', 49]
]
self.model = TableModel(data)
self.table.setModel(self.model)
self.setCentralWidget(self.table)
self.table.setRowHeight(0, 200)
self.table.setRowHeight(1, 200)
self.table.setRowHeight(2, 200)
self.table.setRowHeight(3, 200)
imageDelegate = ImageDelegate (self.table)
self.table.setItemDelegateForColumn(1, imageDelegate)
header = self.table.horizontalHeader ()
header.setSectionResizeMode (1, QHeaderView.Stretch)
class ImageDelegate(QStyledItemDelegate):
def __init__(self, parent ):
QStyledItemDelegate.__init__ (self, parent)
def paint(self, painter, option, index):
image = str(pathlib.Path(index.data(Qt.DisplayRole)))
imagetoshow = QImage(image)
pixmap = QPixmap.fromImage(imagetoshow)
pixmap = pixmap.scaled(400, 400, Qt.KeepAspectRatio)
#pixmap.set
option.widget.setRowHeight(index.row(), pixmap.height())
option.widget.setColumnWidth(index.row(), 400)
##
brush = QBrush(pixmap)
painter.setRenderHint (QPainter.Antialiasing, True)
painter.setBrush (brush)
painter.drawRoundedRect (option.rect.x(), option.rect.y(), pixmap.width(), pixmap.height(), 20, 20)
app=QApplication(sys.argv)
window=MainWindow()
window.show()
app.exec_()

The main problem is that you are using the pixmap as a brush, and in that case it's treated as a texture, which normally has an origin at (0, 0).
To avoid that you can use setBrushOrigin() with the top left point of the option.rect.
Note that you should always prefer to save() and restore() the state of the painter when changing its properties (font, pen, brush, brush origin, background, render hints, clipping and transformations), and the state must be always restored (every save() has to be matched by a corresponding restore()). This is very important for functions that receive an already constructed QPainter, like in this case with the paint() function.
brush = QBrush(pixmap)
painter.save()
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setBrush(brush)
painter.setBrushOrigin(option.rect.topLeft())
painter.drawRoundedRect(option.rect.x(), option.rect.y(),
pixmap.width(), pixmap.height(), 20, 20)
painter.restore()
On the other hand, you can also use clipping and directly draw the pixmap:
path = QPainterPath()
path.addRoundedRect(option.rect.x(), option.rect.y(),
pixmap.width(), pixmap.height(), 20, 20)
painter.save()
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setClipPath(path)
painter.drawPixmap(option.rect.topLeft(), pixmap)
painter.restore()
Finally, you should never change view properties within the paint event, most importantly because it could cause recursion (and in the best case it will at least cause unnecessary redraws, like in your case), but also because it's not the good way to do so.
Also, it doesn't make much sense that you use setColumnWidth with the index row.
Since you're already using the 400 pixel limit, you can set the second column width Fixed and resize the column for the header, then use ResizeToContents for the vertical header and implement the sizeHint() for the delegate. Alternatively, you can use the ResizeToContents mode for the column too.
class MainWindow(QMainWindow):
def __init__(self):
# ...
header = self.table.horizontalHeader()
header.setSectionResizeMode(1, QHeaderView.Fixed)
header.resizeSection(1, 400)
self.table.verticalHeader().setSectionResizeMode(
QHeaderView.ResizeToContents)
class ImageDelegate(QStyledItemDelegate):
# ...
def sizeHint(self, opt, index):
image = str(pathlib.Path(index.data(Qt.DisplayRole)))
imagetoshow = QImage(image)
pixmap = QPixmap.fromImage(imagetoshow)
pixmap = pixmap.scaled(400, 400, Qt.KeepAspectRatio)
return pixmap.size()

Related

Create padding when fixed aspect ratio QWidget cannot fill entire window

A custom widget (class name MyLabel, inherits QLabel) has a fixed aspect ratio 16:9.
When I resize my window, the label is top-left aligned unless the window happens to be 16:9, in which case it fills the window perfectly.
How do I get the label to be centered? I have looked at size policies, alignments, using spaceitems and stretch, but I cannot seem to get it working as desired.
Here is a minimal reproducible example:
import sys
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow
from PyQt5.QtCore import QSize, Qt
from PyQt5.Qt import QVBoxLayout, QWidget
class MyLabel(QLabel):
def __init__(self, text, parent=None):
super().__init__(text, parent)
self.setStyleSheet("background-color: lightgreen") # Just for visibility
def resizeEvent(self, event):
# Size of 16:9 and scale it to the new size maintaining aspect ratio.
new_size = QSize(16, 9)
new_size.scale(event.size(), Qt.KeepAspectRatio)
self.resize(new_size)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__(None)
# Main widget and layout, and set as centralWidget
self.main_layout = QVBoxLayout()
self.main_widget = QWidget()
self.main_widget.setLayout(self.main_layout)
self.setCentralWidget(self.main_widget)
# Add button to main_layout
label = MyLabel("Hello World")
self.main_layout.addWidget(label)
self.show()
app = QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())
Examples of desired outcome:
Examples of actual outcome:
Qt unfortunately doesn't provide a straight forward solution for widgets that require a fixed aspect ratio.
There are some traces in old documentation, but the main problem is that:
all functions related to aspect ratio (hasHeightForWidth() etc) for widgets, layouts and size policies are only considered for the size hint, so no constraint is available if the widget is manually resized by the layout;
as the documentation reports changing the geometry of a widget within the moveEvent() or resizeEvent() might lead to infinite recursion;
it's not possible to (correctly) control the size growth or shrinking while keeping aspect ratio;
For the sake of completeness, here's a partial solution to this issue, but be aware that QLabel is a very peculiar widget that has some constraints related to its text representation (most importantly, with rich text and/or word wrap).
class MyLabel(QLabel):
lastRect = None
isResizing = False
def __init__(self, text, parent=None):
super().__init__(text, parent)
self.setStyleSheet("background-color: lightgreen")
self.setScaledContents(True)
def restoreRatio(self, lastRect=None):
if self.isResizing:
return
rect = QRect(QPoint(),
QSize(16, 9).scaled(self.size(), Qt.KeepAspectRatio))
if not lastRect:
lastRect = self.geometry()
rect.moveCenter(lastRect.center())
if rect != lastRect:
self.isResizing = True
self.setGeometry(rect)
self.isResizing = False
self.lastRect = None
def hasHeightForWidth(self):
return True
def heightForWidth(self, width):
if self.pixmap():
return width * self.pixmap().height() / self.pixmap().width()
return width * 9 / 16
def sizeHint(self):
if self.pixmap():
return self.pixmap().size()
return QSize(160, 90)
def moveEvent(self, event):
self.lastRect = self.geometry()
def resizeEvent(self, event):
self.restoreRatio(self.lastRect)
Since the purpose is to display an image, another possibility is to manually paint everything on your own, for which you don't need a QLabel at all, and you can just override the paintEvent of a QWidget, but for performance purposes it could be slightly better to use a container widget with a child QLabel: this would theoretically make things a bit faster, as all the computation is completely done in Qt:
class ParentedLabel(QWidget):
def __init__(self, pixmap=None):
super().__init__()
self.child = QLabel(self, scaledContents=True)
if pixmap:
self.child.setPixmap(pixmap)
def setPixmap(self, pixmap):
self.child.setPixmap(pixmap)
self.updateGeometry()
def updateChild(self):
if self.child.pixmap():
r = self.child.pixmap().rect()
size = self.child.pixmap().size().scaled(
self.size(), Qt.KeepAspectRatio)
r = QRect(QPoint(), size)
r.moveCenter(self.rect().center())
self.child.setGeometry(r)
def hasHeightForWidth(self):
return bool(self.child.pixmap())
def heightForWidth(self, width):
return width * self.child.pixmap().height() / self.child.pixmap().width()
def sizeHint(self):
if self.child.pixmap():
return self.child.pixmap().size()
return QSize(160, 90)
def moveEvent(self, event):
self.updateChild()
def resizeEvent(self, event):
self.updateChild()
Finally, another possibility is to use a QGraphicsView, which is probably the faster approach of all, with a small drawback: the image shown based on the given size hint will probably be slightly smaller (a couple of pixels) than the original, with the result that it will seem a bit "out of focus" due to the resizing.
class ViewLabel(QGraphicsView):
def __init__(self, pixmap=None):
super().__init__()
self.setStyleSheet('ViewLabel { border: 0px solid none; }')
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scene = QGraphicsScene()
self.setScene(scene)
self.pixmapItem = QGraphicsPixmapItem(pixmap)
self.pixmapItem.setTransformationMode(Qt.SmoothTransformation)
scene.addItem(self.pixmapItem)
def setPixmap(self, pixmap):
self.pixmapItem.setPixmap(pixmap)
self.updateGeometry()
self.updateScene()
def updateScene(self):
self.fitInView(self.pixmapItem, Qt.KeepAspectRatio)
def hasHeightForWidth(self):
return not bool(self.pixmapItem.pixmap().isNull())
def heightForWidth(self, width):
return width * self.pixmapItem.pixmap().height() / self.pixmapItem.pixmap().width()
def sizeHint(self):
if not self.pixmapItem.pixmap().isNull():
return self.pixmapItem.pixmap().size()
return QSize(160, 90)
def resizeEvent(self, event):
self.updateScene()

how to make an overriden QGraphicsTextItem editable & movable?

I am using PyQt and I'm trying to re-implement a QGraphicsTextItem, but it seems I'm missing something.
I would like to make the NodeTag item's text editable. I have tried setting flags such as Qt.TextEditorInteraction and QGraphicsItem.ItemIsMovable , but those seem to be ignored...
Here is a Minimal Reproducible Example :
import sys
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QMainWindow, QApplication, QGraphicsItem, QGraphicsTextItem
from PyQt5.QtCore import *
from PyQt5.QtGui import QPen
class NodeTag(QGraphicsTextItem):
def __init__(self,text):
QGraphicsTextItem.__init__(self,text)
self.text = text
self.setPos(0,0)
self.setTextInteractionFlags(Qt.TextEditorInteraction)
# self.setFlag(QGraphicsItem.ItemIsFocusable, True) # All these flags are ignored...
# self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
def boundingRect(self):
return QRectF(0,0,80,25)
def paint(self,painter,option,widget):
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
painter.drawText(self.boundingRect(),self.text)
def mousePressEvent(self, event):
print("CLICK!")
# self.setTextInteractionFlags(Qt.TextEditorInteraction) # make text editable on click
# self.setFocus()
class GView(QGraphicsView):
def __init__(self, parent, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parent = parent
self.setGeometry(100, 100, 700, 450)
self.show()
class Scene(QGraphicsScene):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
tagItem = NodeTag("myText") # create a NodeTag item
self.addItem(tagItem)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__() # create default constructor for QWidget
self.setGeometry(900, 70, 1000, 800)
self.createGraphicView()
self.show()
def createGraphicView(self):
self.scene = Scene(self)
gView = GView(self)
scene = Scene(gView)
gView.setScene(scene)
# Set the main window's central widget
self.setCentralWidget(gView)
# Run program
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec_())
As you can see I have tried overriding the mousePressEvent and setting flags there too, but no luck so far.
Any help appreciated!
All QGraphicsItem subclasses have a paint method, and all items that paint some contents have that method overridden so that they can actually paint themselves.
The mechanism is the same as standard QWidgets, for which there is a paintEvent (the difference is that paint of QGraphicsItem receives an already instanciated QPainter), so if you want to do further painting other than what the class already provides, the base implementation must be called.
Consider that painting always happen from bottom to top, so everything that needs to be drawn behind the base painting has to be done before calling super().paint(), and everything that is going to be drawn in front of the default painting has to be placed after.
Depending on the situation, overriding might require that the default base implementation is called anyway, and that's important in your case for boundingRect too. QGraphicsTextItem automatically resizes itself when its contents change, so you should not always return a fixed QRect. If you need to have a minimum size, the solution is to merge a minimum rectangle with those provided by the default boundingRect() function.
Then, editing on a QGraphicsTextItem happens when the item gets focused, but since you also want to be able to move the item, things get trickier as both actions are based on mouse clicks. If you want to be able to edit the text with a single click, the solution is to make the item editable only when the mouse button has been released and has not been moved by some amount of pixels (the startDragDistance() property), otherwise the item is moved with the mouse. This obviously makes the ItemIsMovable flag useless, as we're going to take care of the movement internally.
Finally, since a minimum size is provided, we also need to override the shape() method in order to ensure that collision and clicks are correctly mapped, and return a QPainterPath that includes the whole bounding rect (for normal QGraphicsItem that should be the default behavior, but that doesn't happen with QGraphicsRectItem).
Here's a full implementation of what described above:
class NodeTag(QGraphicsTextItem):
def __init__(self, text):
QGraphicsTextItem.__init__(self, text)
self.startPos = None
self.isMoving = False
# the following is useless, not only because we are leaving the text
# painting to the base implementation, but also because the text is
# already accessible using toPlainText() or toHtml()
#self.text = text
# this is unnecessary too as all new items always have a (0, 0) position
#self.setPos(0, 0)
def boundingRect(self):
return super().boundingRect() | QRectF(0, 0, 80, 25)
def paint(self, painter, option, widget):
# draw the border *before* (as in "behind") the text
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
super().paint(painter, option, widget)
def shape(self):
shape = QPainterPath()
shape.addRect(self.boundingRect())
return shape
def focusOutEvent(self, event):
# this is required in order to allow movement using the mouse
self.setTextInteractionFlags(Qt.NoTextInteraction)
def mousePressEvent(self, event):
if (event.button() == Qt.LeftButton and
self.textInteractionFlags() != Qt.TextEditorInteraction):
self.startPos = event.pos()
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.startPos:
delta = event.pos() - self.startPos
if (self.isMoving or
delta.manhattanLength() >= QApplication.startDragDistance()):
self.setPos(self.pos() + delta)
self.isMoving = True
return
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if (not self.isMoving and
self.textInteractionFlags() != Qt.TextEditorInteraction):
self.setTextInteractionFlags(Qt.TextEditorInteraction)
self.setFocus()
# the following lines are used to correctly place the text
# cursor at the mouse cursor position
cursorPos = self.document().documentLayout().hitTest(
event.pos(), Qt.FuzzyHit)
textCursor = self.textCursor()
textCursor.setPosition(cursorPos)
self.setTextCursor(textCursor)
super().mouseReleaseEvent(event)
self.startPos = None
self.isMoving = False
As a side note, remember that QGraphicsTextItem supports rich text formatting, so even if you want more control on the text painting process you should not use QPainter.drawText(), because you'd only draw the plain text. In fact, QGraphicsTextItem draws its contents using the drawContents() function of the underlying text document.
Try it:
...
class NodeTag(QGraphicsTextItem):
def __init__(self, text, parent=None):
super(NodeTag, self).__init__(parent)
self.text = text
self.setPlainText(text)
self.setFlag(QGraphicsItem.ItemIsMovable)
self.setFlag(QGraphicsItem.ItemIsSelectable)
def focusOutEvent(self, event):
self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
super(NodeTag, self).focusOutEvent(event)
def mouseDoubleClickEvent(self, event):
if self.textInteractionFlags() == QtCore.Qt.NoTextInteraction:
self.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
super(NodeTag, self).mouseDoubleClickEvent(event)
def paint(self,painter,option,widget):
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
# painter.drawText(self.boundingRect(),self.text)
super().paint(painter, option, widget)
...

Initial paint of QStyledItemDelegate has the wrong height

I am attempting to use a QStyledItemDelegate with my QListView to display rich text in the items.
The first time the item is painted its height is too small. If I then mouse over the item it gets repainted with the correct height. Below are screenshots of the initial paint and repaint.
How can I get the initial paint to be the right height?
Example code that demonstrates the issue:
from PySide2 import QtCore, QtGui, QtWidgets
class RichTextItemDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, parent=None):
super(RichTextItemDelegate, self).__init__(parent)
self.doc = QtGui.QTextDocument(self)
def paint(self, painter, option, index):
painter.save()
self.initStyleOption(option, index)
self.doc.setHtml(option.text)
option_no_text = QtWidgets.QStyleOptionViewItem(option)
option_no_text.text = ''
style = QtWidgets.QApplication.style() if option_no_text.widget is None else option_no_text.widget.style()
style.drawControl(QtWidgets.QStyle.CE_ItemViewItem, option_no_text, painter)
margin_top = (option.rect.height() - self.doc.size().height()) // 2
text_rect = style.subElementRect(QtWidgets.QStyle.SE_ItemViewItemText, option_no_text, None)
text_rect.setTop(text_rect.top() + margin_top)
painter.translate(text_rect.topLeft())
painter.setClipRect(text_rect.translated(-text_rect.topLeft()))
context = QtGui.QAbstractTextDocumentLayout.PaintContext()
self.doc.documentLayout().draw(painter, context)
painter.restore()
def sizeHint(self, option, index):
other = super().sizeHint(option, index)
w = min(self.doc.idealWidth(), other.width())
h = max(self.doc.size().height(), other.height())
return QtCore.QSize(w, h)
class ExampleWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
item = QtGui.QStandardItem()
item.setText('Example<br><span style="font-size: 14pt; font-weight: bold;">Example<br>Example<br>Example</span>', )
model = QtGui.QStandardItemModel()
model.appendRow(item)
self.listview = QtWidgets.QListView(parent=self)
self.listview.setModel(model)
delegate = RichTextItemDelegate(self.listview)
self.listview.setItemDelegate(delegate)
app = QtWidgets.QApplication([])
example = ExampleWidget()
example.resize(320, 240)
example.show()
app.exec_()
So I believe the initial paint was sized wrong because the sizeHint() function can't return good values until it knows what the item text is. In my original code the text wasn't set until paint() was called.
I used the following for sizeHint() and it seems to work:
def sizeHint(self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize:
self.doc.setHtml(index.data())
doc_size = self.doc.size()
doc_width = int(math.ceil(doc_size.width()))
doc_height = int(math.ceil(doc_size.height()))
item_size = super().sizeHint(option, index)
w = min(doc_width, item_size.width())
h = max(doc_height, item_size.height())
return QtCore.QSize(w, h)
Be warned... I do not know what the implications of calling self.doc.setHtml() inside of sizeHint() are. Also, for some reason super().sizeHint() returns a very large width value for me, and I'm not sure why, which is why I call min() instead of max().

How to create circular image using pyqt4?

Here I wrote this code but did not work:
import sys
from PyQt4 import QtGui, QtCore
class CricleImage(QtCore.QObject):
def __init__(self):
super(CricleImage, self).__init__()
self.pix = QtGui.QGraphicsPixmapItem(QtGui.QPixmap("bird(01).jpg"))
#drawRoundCircle
rect = self.pix.boundingRect()
self.gri = QtGui.QGraphicsRectItem(rect)
self.gri.setPen(QtGui.QColor('red'))
if __name__ == '__main__':
myQApplication = QtGui.QApplication(sys.argv)
IMG = CricleImage()
#scene
scene = QtGui.QGraphicsScene(0, 0, 400, 300)
scene.addItem(IMG.pix)
#view
view = QtGui.QGraphicsView(scene)
view.show()
sys.exit(myQApplication.exec_())
One possible solution is to overwrite the paint() method of the QGraphicsPixmapItem and use setClipPath to restrict the painting region:
from PyQt4 import QtCore, QtGui
class CirclePixmapItem(QtGui.QGraphicsPixmapItem):
#property
def radius(self):
if not hasattr(self, "_radius"):
self._radius = 0
return self._radius
#radius.setter
def radius(self, value):
if value >= 0:
self._radius = value
self.update()
def paint(self, painter, option, widget=None):
painter.save()
rect = QtCore.QRectF(QtCore.QPointF(), 2 * self.radius * QtCore.QSizeF(1, 1))
rect.moveCenter(self.boundingRect().center())
path = QtGui.QPainterPath()
path.addEllipse(rect)
painter.setClipPath(path)
super().paint(painter, option, widget)
painter.restore()
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
pixmap = QtGui.QPixmap("logo.jpg")
scene = QtGui.QGraphicsScene()
view = QtGui.QGraphicsView(scene)
view.setRenderHints(
QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform
)
it = CirclePixmapItem(pixmap)
scene.addItem(it)
it.radius = pixmap.width() / 2
view.show()
sys.exit(app.exec_())
Update:
# ...
view = QtGui.QGraphicsView(
scene, alignment=QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft
)
# ...
view.show()
it.setPos(80, 80)
sys.exit(app.exec_())
Second possible solution:
import sys
#from PyQt4 import QtCore, QtGui
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class Label(QLabel):
def __init__(self, *args, antialiasing=True, **kwargs):
super(Label, self).__init__(*args, **kwargs)
self.Antialiasing = antialiasing
self.setMaximumSize(200, 200)
self.setMinimumSize(200, 200)
self.radius = 100
self.target = QPixmap(self.size())
self.target.fill(Qt.transparent) # Fill the background with transparent
# Upload image and zoom to control level
p = QPixmap("head2.jpg").scaled(
200, 200, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
painter = QPainter(self.target)
if self.Antialiasing:
# antialiasing
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setRenderHint(QPainter.HighQualityAntialiasing, True)
painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
path = QPainterPath()
path.addRoundedRect(
0, 0, self.width(), self.height(), self.radius, self.radius)
# pruning
painter.setClipPath(path)
painter.drawPixmap(0, 0, p)
self.setPixmap(self.target)
class Window(QWidget):
def __init__(self, *args, **kwargs):
super(Window, self).__init__(*args, **kwargs)
layout = QHBoxLayout(self)
layout.addWidget(Label(self))
self.setStyleSheet("background: green;")
if __name__ == "__main__":
app = QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())
Another approach, slightly different from the one provided by eyllanesc. While this might seem much more complicated than that, I believe that it offers a better implementation and interface, with the addition of better performance.
In this case, instead of overriding the paint method (that is run everytime the item is painted, which happens very often), I'm using the shape() function along with the QGraphicsItem.ItemClipsToShape flag, that allows to limit the painting only within the boundaries of the path shape.
What shape() does is to return a QPainterPath that includes only the "opaque" portions of an item that will react to mouse events and collision detection (with the scene boundaries and its other items). In the case of a QGraphicsPixmapItem this also considers the possible mask (for example, a PNG based pixmap with transparent areas, or an SVG image). By setting the ItemClipsToShape we can ensure that the painting will only cover the parts of the image that are within that shape.
The main advantage of this approach is that mouse interaction and collision detection with other items honors the actual circle shape of the item.
This means that if you click outside the circle (but still within the rectangle area of the full image), the item will not receive the event. Also, if the image supports masking (a PNG with transparent areas) which by default would not be part of the shape, this method will take that into account.
Also, by "caching" the shape we are also speeding up the painting process a bit (since Qt will take care of it, without any processing done using python).
class CircleClipPixmapItem(QtGui.QGraphicsPixmapItem):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFlag(self.ItemClipsToShape)
self.updateRect()
def updateRect(self):
baseRect = super().boundingRect()
minSize = min(baseRect.width(), baseRect.height())
self._boundingRect = QtCore.QRectF(0, 0, minSize, minSize)
self._boundingRect.moveCenter(baseRect.center())
self._shape = QtGui.QPainterPath()
self._shape.addEllipse(self._boundingRect)
# the shape might include transparent areas, using the & operator
# I'm ensuring that _shape only includes the areas that intersect
# the shape provided by the base implementation
self._shape &= super().shape()
def setPixmap(self, pm):
super().setPixmap(pm)
# update the shape to reflect the new image size
self.updateRect()
def setShapeMode(self, mode):
super().setShapeMode(mode)
# update the shape with the new mode
self.updateRect()
def boundingRect(self):
return self._boundingRect
def shape(self):
return self._shape
Keep in mind that there's a catch about both methods: if the aspect ratio of the image differs very much from 1:1, you'll always end up with some positioning issues. With my image, for example, it will always be shown 60 pixel right from the actual item position. If you want to avoid that, the updateRect function will be slightly different and, unfortunately, you'll have to override the paint() function (while still keeping it a bit faster than other options):
def updateRect(self):
baseRect = super().boundingRect()
minSize = min(baseRect.width(), baseRect.height())
self._boundingRect = QtCore.QRectF(0, 0, minSize, minSize)
# the _boundingRect is *not* centered anymore, but a new rect is created
# as a reference for both shape intersection and painting
refRect= QtCore.QRectF(self._boundingRect)
refRect.moveCenter(baseRect.center())
# note the minus sign!
self._reference = -refRect.topLeft()
self._shape = QtGui.QPainterPath()
self._shape.addEllipse(self._boundingRect)
self._shape &= super().shape().translated(self._reference)
# ...
def paint(self, painter, option, widget):
# we are going to translate the painter to the "reference" position,
# let's save its state before that
painter.save()
painter.translate(self._reference)
super().paint(painter, option, widget)
painter.restore()
This will make the boundingRect (and resulting internal shape) position the whole item at the top-left of the item position.
The following image shows the differences between the two approaches; I've used a PNG with transparent areas to better explain the whole concept.
On the top there is the source image, in the middle the paint() override approach, and finally the shape() implementation at the bottom.
While there seems to be no difference between the two methods, as shown on the examples on the left, on the right I've highlighted the actual boundaries of each item, by showing their boundingRect (in blue), shape (in red), which will be used for mouse events, collision detection and paint clipping; the green circle shows the overall circle used for both shape and painting.
The examples in the middle show the positioning based on the original image size, while on the right you can see the absolute positioning based on the effective circle size as explained above.
Drawing a circle around the image
Unfortunately, the ItemClipsToShape flag doesn't support antialiasing for clipping: if we just draw a circle after painting the image the result will be ugly. On the left you can see that the circle is very pixellated and does not overlap perfectly on the image. On the right the correct painting.
To support that, the flag must not be set, and the paint function will be a bit different.
class CircleClipPixmapItem(QtGui.QGraphicsPixmapItem):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# we don't need this anymore:
# self.setFlag(self.ItemClipsToShape)
# always set the shapeMode to the bounding rect without any masking:
# if the image has transparent areas they will be clickable anyway
self.setShapeMode(self.BoundingRectShape)
self.updateRect()
self.pen = QtGui.QPen(QtCore.Qt.red, 2)
# ...
def setPen(self, pen):
self.pen = pen
self.update()
def paint(self, painter, option, widget):
# we are going to translate the painter to the "reference" position,
# and we are also changing the pen, let's save the state before that
painter.save()
painter.translate(.5, .5)
painter.setRenderHints(painter.Antialiasing)
# another painter save "level"
painter.save()
# apply the clipping to the painter
painter.setClipPath(self._shape)
painter.translate(self._reference)
super().paint(painter, option, widget)
painter.restore()
painter.setPen(self.pen)
# adjust the rectangle to precisely match the circle to the image
painter.drawEllipse(self._boundingRect.adjusted(.5, .5, -.5, -.5))
painter.restore()
# restore the state of the painter

Using QStyledItemDelegates as custom items in QListView

I want to design a custom ListView widget which has custom items similar to this:
https://i.stack.imgur.com/iTNbN.png
However, the qt documentation and some stackoverflow posts state that one should ideally use a QStyleItemDelegate. I never worked with 'delegates' before but as far as I understood from my research they are called by the ListView for drawing / rendering each item.
I found a delegate example in another project (https://github.com/pyblish/pyblish-lite/blob/master/pyblish_lite/delegate.py) and they draw everything by hand / are essentially rebuilding entire widgets by painting rectangles.
This seems a bit impractical for me as most of the time custom item widgets can be compounds of existing widgets. Take a look at the screenshot above. It essentially contains a Qlabel, QPixmap, and four DoubleSpinBoxes.
Question: How would you use the painting / rendering methods that already exist in them instead of manually painting everything on your own?
That way you can profit from existing member methods and can use layouts for structuring your widget.
For example the first ListViewItem should pass the model data to the delegate so that the text of the self.lightGroupName QLabel can be set to "Light1".
Any help is greatly appreciated, since I have no idea how to go on from here:
from PySide2 import QtCore, QtGui, QtWidgets
class LightDelagate(QtWidgets.QStyledItemDelegate): #custom item view
def __init__(self, parent=None):
super(LightDelagate, self).__init__(parent)
self.setupUI()
def setupUI(self):
self.masterWidget = QtWidgets.QWidget()
#Light Group Header
self.hlayLightHeader = QtWidgets.QHBoxLayout()
self.lightGroupName = QtWidgets.QLabel("Checker")
self.hlayLightHeader.addWidget(self.lightGroupName)
#Light AOV Preview
self.lightPreview = QtWidgets.QLabel()
#set size
self.aovThumbnail = QtGui.QPixmap(180, 101)
#self.lightPreview.setPixmap(self.aovThumbnail.scaled(self.lightPreview.width(), self.lightPreview.height(), QtCore.Qt.KeepAspectRatio))
# #Color Dials
# self.hlayColorDials = QtWidgets.QHBoxLayout()
# self.rgbDials = QtWidgets.QHBoxLayout()
# self.rDial = QtWidgets.QDoubleSpinBox()
# self.rDial.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons)
# self.gDial = QtWidgets.QDoubleSpinBox()
# self.gDial.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons)
# self.bDial = QtWidgets.QDoubleSpinBox()
# self.bDial.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons)
# self.rgbDials.addWidget(self.rDial)
# self.rgbDials.addWidget(self.gDial)
# self.rgbDials.addWidget(self.bDial)
# #Exposure
# self.hlayExposureDials = QtWidgets.QHBoxLayout()
# self.exposureDial = QtWidgets.QDoubleSpinBox()
# self.exposureDial.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons)
# self.hlayExposureDials.addWidget(self.exposureDial)
# self.hlayColorDials.addLayout(self.rgbDials)
# self.hlayColorDials.addLayout(self.hlayExposureDials)
#entire layout
self.vlayWidget = QtWidgets.QVBoxLayout()
self.vlayWidget.addLayout(self.hlayLightHeader)
self.vlayWidget.addWidget(self.lightPreview)
# self.vlayWidget.addLayout(self.hlayColorDials)
self.vlayWidget.setContentsMargins(2,2,2,2)
self.vlayWidget.setSpacing(2)
self.masterWidget.setLayout(self.vlayWidget)
def paint(self, painter, option, index):
rowData = index.model().data(index, QtCore.Qt.DisplayRole)
self.lightGroupName.setText(rowData[0])
print (option.rect)
painter.drawRect(option.rect)
painter.drawText()
def sizeHint(self, option, index):
return QtCore.QSize(200, 150)
class LightListModel(QtCore.QAbstractListModel): #data container for list view
def __init__(self, lightList= None):
super(LightListModel, self).__init__()
self.lightList = lightList or []
#reimplement
def rowCount(self, index):
return len(self.lightList)
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
lightGroupData = self.lightList[index.row()]
return lightGroupData
class LightListView(QtWidgets.QListView): #
def __init__(self):
super(LightListView, self).__init__()
self.setFlow(QtWidgets.QListView.LeftToRight)
self.setItemDelegate(LightDelagate(self))
self.setMinimumWidth(1880)
lightListTest = [
('Light1' , {'lightList' : [], 'lightColor': (0,0,0), 'mod_exposure': 1, 'mod_color' : (0,0,0)}),
('Light2' , {'lightList' : [], 'lightColor': (0,0,0), 'mod_exposure': 1, 'mod_color' : (0,0,0)}),
('Light3' , {'lightList' : [], 'lightColor': (0,0,0), 'mod_exposure': 1, 'mod_color' : (0,0,0)}),
('Light4' , {'lightList' : [], 'lightColor': (0,0,0), 'mod_exposure': 1, 'mod_color' : (0,0,0)})
]
app = QtWidgets.QApplication([])
LLV = LightListView()
model = LightListModel(lightList=lightListTest)
LLV.setModel(model)
LLV.show()
LLV.setSe
app.exec_()
Instead of QListView, could you use QListWidget and override itemWidget? The idea would be that this lets you return a QWidget (with children as per your screenshot) instead of having to implement a QStyledItemDelegate that calls each child widget's paint method.

Categories