QGraphicsItem rendering on Apple Retina Display - qt

I'm adding support for the Apple Retina Display to my PyQt5 application. While I successfully managed to render high resolution icons (by adding the #2x suffix to all my .png files and setting the Qt.AA_UseHighDpiPixmaps in my QApplication), I'm having some troubles in rendering high resolutions QGraphicsItem in a QGraphicsScene + QGraphicsView.
In my application, other than loading .png files, I also generate several QPixmap my self (embedding them into a Icon), to build the palette of symbols the user can use to add new shapes to the diagram rendered in the QGraphicsView, i.e:
def icon(cls, width, height, **kwargs):
"""
Returns an icon of this item suitable for the palette.
:type width: int
:type height: int
:rtype: QIcon
"""
icon = QIcon()
for i in (1.0, 2.0):
# CREATE THE PIXMAP
pixmap = QPixmap(width * i, height * i)
pixmap.setDevicePixelRatio(i)
pixmap.fill(Qt.transparent)
# PAINT THE SHAPE
polygon = cls.createPolygon(46, 34)
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.Antialiasing)
painter.setPen(QPen(QColor(0, 0, 0), 1.1, Qt.SolidLine))
painter.setBrush(QColor(252, 252, 252))
painter.translate(width / 2, height / 2)
painter.drawPolygon(polygon)
# PAINT THE TEXT INSIDE THE SHAPE
painter.setFont(Font('Arial', 11, Font.Light))
painter.drawText(polygon.boundingRect(), Qt.AlignCenter, 'role')
painter.end()
# ADD THE PIXMAP TO THE ICON
icon.addPixmap(pixmap)
return icon
Which generate one of the symbols in my Palette (the diamond one).
However when I add elements to my QGraphicsScene, displayed in a QGraphicsView they are rendered in low resolution:
def paint(self, painter, option, widget=None):
"""
Paint the node in the diagram.
:type painter: QPainter
:type option: QStyleOptionGraphicsItem
:type widget: QWidget
"""
painter.setPen(self.pen)
painter.setBrush(self.brush)
painter.drawPolygon(self.polygon)
The text within the shape is rendered correctly, and I'm not painting it myself since it's a QGraphicsTextItem having my QGraphicsItem as parent.
The problem is that while for QPixmap I can set the device pixel ratio, for QGraphicsItem i cannot. Am I missing something?
I'm using PyQt 5.5.1 built against Qt 5.5.1 and SIP 4.18 (not using 5.6 since I'm experiencing several crashes on application startup which I already reported to PyQt devs).

Probably not what you wanted to hear, but Qt added retina support in 5.6.

I'm also struggling with the similar issue in PyQt 5.7.
If you are writing your sub class of QGraphicsItem, try to set render hint to antialias in the paint() method:
def paint(self, painter, option, widget=None):
"""
Paint the node in the diagram.
:type painter: QPainter
:type option: QStyleOptionGraphicsItem
:type widget: QWidget
"""
painter.setPen(self.pen)
painter.setBrush(self.brush)
painter.setRenderHint(QPainter.Antialiasing) # <-- add this line
painter.drawPolygon(self.polygon)
Note, this may not be the best or correct answer.

Related

How to avoid over-packing non-srolling Qt layouts?

A Qt packing layout, such as QVBoxLayout, can pack widgets inside it, such as buttons. In this case, they will be packed vertically as shown in image below:
When we pack too many widgets inside such a layout, and since scrolling is not added by default, the buttons will eventually get squeezed onto each other up to a point that they will overlap, as shown below:
My questions are:
How to tell Qt to not show/pack widgets beyond the available viewing space in the non-scrolling layout?
How to handle the case when the window is resized? I.e. Qt should add/remove widgets accordingly. E.g. if there is extra space available, then perhaps Qt should add some extra widgets that it couldn't add previously.
To be specific: "too many packed widgets" is when the widgets start invading spaces of other widgets, including their inter-widget spacings or margins.
Appendix
Images above are generated by this code below as run in a tile in i3, which is a modified version of this.
from PyQt5 import QtCore, QtWidgets
app = QtWidgets.QApplication([])
widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout(widget)
for i in range(40):
layout.addWidget(QtWidgets.QPushButton(str(i + 1)))
widget.show()
app.exec_()
When too many widgets are packed:
If the window is tiled, you see them overcrowded as in in the image.
If the window is floating, the window will keep growing until it is no longer fully visible in the monitor.
None of these outcomes are acceptable in my case. My goal is to have Qt only pack as much as will be visible, and add/remove/hide/show dynamically as the window gets resized.
Try this code. It does not rely on QVBoxLayout but it basically does the same as this layout. It hides the child widgets which are outside of the area. There are no partially visible widgets.
from PyQt5 import QtWidgets
class Container(QtWidgets.QWidget):
_spacing = 5
def __init__(self, parent=None):
super().__init__(parent)
y = self._spacing
for i in range(40):
button = QtWidgets.QPushButton("Button" + str(i + 1), self)
button.move(self._spacing, y)
y += button.sizeHint().height() + self._spacing
def resizeEvent(self, event):
super().resizeEvent(event)
for child in self.children():
if isinstance(child, QtWidgets.QWidget):
child.resize(self.width() - 2 * self._spacing, child.height())
child.setVisible(child.geometry().bottom() < self.height())
app = QtWidgets.QApplication([])
w = Container()
w.resize(500, 500)
w.show()
app.exec_()
Note that is in fact does not add nor remove widgets dynamically, this would be much more code and it would probably be very depending on your specific use case. Moreover it feels as a premature optimization. Unless you really need it, do not do it.
UPDATE:
I experimented with the code above and proposed some improvements. I especially wanted to make it responsive to changes in child widgets. The problem is that if the child widget changes it size, the parent container must be re-layouted. The code above does not react in any way. To make it responsive, we need to react to LayoutRequest event. Note that in the code below, I have created three types of buttons - one add a line to itself, other increases font size, and yet another decreases font size.
from PyQt5 import QtCore, QtWidgets
def changeFontSize(increment):
font = QtWidgets.QApplication.font()
font.setPointSize(font.pointSize() + increment)
QtWidgets.QApplication.setFont(font)
class Container(QtWidgets.QWidget):
_spacing = 5
_children = [] # maintains the order of creation unlike children()
def __init__(self, parent=None):
super().__init__(parent)
for i in range(100):
child = QtWidgets.QPushButton(self)
child.installEventFilter(self)
# these are just to test various changes in child widget itself to force relayout
r = i % 3
if r == 0:
text = "New line"
onClicked = lambda state, w=child: w.setText(w.text() + "\nclicked")
elif r == 1:
text = "Bigger font"
onClicked = lambda: changeFontSize(1)
elif r == 2:
text = "Smaller font"
onClicked = lambda: changeFontSize(-1)
child.setText(text)
child.clicked.connect(onClicked)
self._children.append(child)
def resizeEvent(self, event):
super().resizeEvent(event)
self._relayout()
def event(self, event):
if event.type() == QtCore.QEvent.LayoutRequest:
self._relayout()
return super().event(event)
def _relayout(self):
y = self._spacing
for child in self._children:
h = child.sizeHint().height()
child.move(self._spacing, y)
child.resize(self.width() - 2 * self._spacing, h)
y += h + self._spacing
child.setVisible(y < self.height())
app = QtWidgets.QApplication([])
w = Container()
w.resize(500, 500)
w.show()
app.exec_()
This code is satisfactory, however it is not perfect. I have observed that when the container is being re-layouted and some of the child widgets will change its visibility state, re-layouting is called again. This is not needed but I have not discovered how to prevent it.
Maybe there is some better way...

controlling size of QItemDelegate dynamically

I have a couple of questions regarding the sizes of QItemDelegates in a QListView:
I have a QListView using a QItemDelegate which renders a widget in the delegate's custom paint() method like so:
self.thumbnail = MyCustomWidget()
self.thumbnail.render(painter, QtCore.QPoint(option.rect.x(), option.rect.y()))
This, however, shows the item with a 250x260 image in a QListView, even though the MyCustomWidget().sizeHint() is 250x250 and it's maximumSize() returns 250x250 as well.
I found that the culprit is the QListView's spacing, which I had set to 10. If I set the spacing to 100, I still get the QItemDelegates size of 250x260, but if I just don't use setSpacing() at all it renders as expected at 250x250.
The spacing seems to alter the option.rect that is passed into the paint method, causing the incorrect size.
I do need that spacing, so I'm a bit confused why the QListView's spacing alters the QItemDelegates's size? Is this a bug?
I can work around this by rendering a QPixmap first, then have the painter draw the QPixmap instead of rendering to the painter directly:
self.thumbnail = MyCustomWidget()
pixmap = QtGui.QPixmap(self.thumbnail.size())
self.thumbnail.render(pixmap)
painter.drawPixmap(option.rect.topLeft(), pixmap)
This yields 250x250 images which is what I need, but I don't understand why the first method doesn't render the correct size when I use setSpacing?!
Now, the bigger challenge is how to dynamically scale the size of the QItemDelegate's via a QSlider:
I have a QSlider in the QListView that is supposed to scale the items so the user can chose to see smaller but more items in the current view. I tested the resizing of a standalone instance of MyCustomWidget() and it works just fine.
However, the delegates won't scale as expected. This is my delegate code:
class Delegate(QtGui.QItemDelegate):
def __init__(self, parent = None):
super(Delegate, self).__init__(parent)
self.scaleValue = 100 # size in percent (as returned by QSlider)
def paint(self, painter, option, index):
proxyModel = index.model()
item = proxyModel.sourceModel().itemFromIndex(proxyModel.mapToSource(index))
self.thumbnail = ElementThumbnail(item)
self.thumbnail.scale(self.scaleValue)
pixmap = QtGui.QPixmap(self.thumbnail.size())
self.thumbnail.render(pixmap)
painter.drawPixmap(option.rect.topLeft() * self.scaleValue / 100.0, pixmap)
super(Delegate, self).paint(painter, option, index)
def setScaleValue(self, value):
self.scaleValue = value
def sizeHint(self, option, index):
return ElementThumbnail.thumbSize * self.scaleValue / 100.0
and in the QListView I am using this slot connected to the slider's valueChanges signal:
def scaleThumbnails(self, value):
self.itemDelegate().setScaleValue(value)
self.update()
The result is that the QSlider will crop the QItemDelegates but not scale them, because the QItemDelegate's sizeHint() is only called when the QListView is first shown.
Additionally, I need to make sure that when the widgets are (eventually) scaled down, the layout of QListView is recalculated and more items are fit inside the visible area.
So in a nutshell my questions are:
How can I scale QItemDelegates dynamically inside a QListView?
How can I force the QListView to recalculate it's layout after the delegate size has been changed?
edit: as for issue 2: QAbstractItemView.doItemsLayout seems to do the trick. Still wondering about issue 1 though
Thanks,
frank
Turns out it was the context of my code that was the issue, not the delegate.
I was scaling the widget before rendering it to a pixmap, which of course made the editor scale properly, but not the item when it wasn't in edit state.
So the solution is simply to scale the pixmap after rendering it from the widget, e.g.:
scaledPixmap = pixmap.scaled(pixmap.size() * self.scaleValue / 100.0)
painter.drawPixmap(option.rect.topLeft(), scaledPixmap)

How to support high-res pixmap in qgraphicsproxywidget when zooming

I have a qgraphicsview with a scene that contains a qgraphicsproxywidget. The widget currently shows some hi-res images in a square about 25x25 via QPixmap. I'm looking for the proper approach to support zooming in on the image without deteriorating its resolution so much. I've found hints that it might be possible by overriding the paint method (ie derive from QPixmap then override paint method), or by using QImage, or some configuration options (I have tried setSmoothRendering on the QGraphicsView but this only helps a little), but it's not clear if these techniques apply when the image is in a widget in a graphics view.
I wrote the following program (actually, my colleague Colin did, I simplified it for posting) that shows the technique I use. Once you save the attached image and run the program, position the mouse over the pixmap, and press + several times to zoom in: notice how the pixmap is pixelated, whereas the text is perfect.
from PyQt5.QtCore import Qt
from PyQt5.Qt import QPixmap
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import (QApplication, QGraphicsScene, QWidget,
QGraphicsView, QGraphicsProxyWidget, QLabel, QVBoxLayout, QHBoxLayout)
class TestGraphicsWidget(QWidget):
def __init__(self):
super().__init__()
self.setupUi(self)
def setupUi(self, TEST):
TEST.resize(400, 300)
self.verticalLayout = QVBoxLayout(TEST)
self.widget = QWidget(TEST)
self.horizontalLayout = QHBoxLayout(self.widget)
self.image_label = QLabel(self.widget)
self.image_label.setStyleSheet("border: 2px solid red;")
self.horizontalLayout.addWidget(self.image_label)
self.text_label1 = QLabel(self.widget)
self.text_label1.setStyleSheet("border: 2px solid red;")
self.horizontalLayout.addWidget(self.text_label1)
self.verticalLayout.addWidget(self.widget)
self.text_label2 = QLabel(TEST)
self.text_label2.setStyleSheet("border: 2px solid red;")
self.verticalLayout.addWidget(self.text_label2)
TEST.setWindowTitle("Form")
self.text_label1.setText("TEXT LABEL 1")
self.text_label2.setText("TEXT LABEL 2")
class TestView(QGraphicsView):
def __init__(self):
super().__init__()
scene = QGraphicsScene(self)
scene.setItemIndexMethod(QGraphicsScene.NoIndex)
scene.setSceneRect(-400, -400, 800, 800)
self.setScene(scene)
self.setCacheMode(QGraphicsView.CacheBackground)
self.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate)
self.setRenderHint(QPainter.Antialiasing)
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QGraphicsView.AnchorViewCenter)
test_widget = TestGraphicsWidget()
test_widget.image_label.setFixedSize(25, 25)
test_widget.image_label.setScaledContents(True)
test_widget.image_label.setPixmap(QPixmap(r"chicken.jpg"))
proxy = QGraphicsProxyWidget()
proxy.setWidget(test_widget)
proxy.setPos(-100, -100)
scene.addItem(proxy)
self.scale(0.8, 0.8)
self.setMinimumSize(400, 400)
def keyPressEvent(self, event):
key = event.key()
if key == Qt.Key_Plus:
self.scaleView(1.2)
elif key == Qt.Key_Minus:
self.scaleView(1 / 1.2)
else:
super().keyPressEvent(event)
def scaleView(self, scaleFactor):
self.scale(scaleFactor, scaleFactor)
if __name__ == '__main__':
import sys
app = QApplication([])
widget = TestView()
widget.show()
sys.exit(app.exec_())
Although this is not all that surprising because the pixmap is downsampled into a 25x25 square of pixels then scaled up by the view, I wouldn't be surprised if there is a qt-specific technique I'm missing, like perhaps something that can be done by overriding the paint of QPixmap to take into account the current scale of the view. I have no choice about using QGraphicsView or embedding an image in a QGraphicsProxyWidget, but I have freedom on image format, the configuration of view or the class to use to load the image into qt, etc.
Any help would be really appreciated.
You appear to be asking: how do I downscale a 600x600 jpg to 25x25 without pixelation? The answer to which is obviously: you can't.
If you put a crappy little 25x25 jpg image in a graphics-view and wind the scale in and out, it's just like standing closer or further away from it. The image doesn't change at all: only your view of it does. And the closer you are to it, the more its intrinsic crappiness is revealed.
So one solution would appear to be: start with a much bigger subject. Resize the widget to, say, four times its original size (and likewise the image label), and then scale down the graphics view to get back to the widget's original starting size.

How to draw over QLabel in Qt

I have to create one screen in Qt in which I have to show a remote having lots of buttons in it and when user clicks some button on actual remote, corresponding button in the image get highlighted. So what I have done is, I have used QLabel and set the remote image as background image and then I have put small rectangular label for each button and filled them with semi transparent color and when user click button in actual remote label color changes, but by using this method lot of labels are getting used making code looking inefficient, so I was thinking of drawing on QLabel (which has a remote as background image) over buttons.
Can anybody suggest me, which API of Qt should I use, and how to follow up on this?
I believe QGraphics is the correct route for a completely custom graphical interface, but if you want to try something that doesn't require you to change too much of your existing approach, you can do a widget with a custom paint event:
This is written in PyQt but you can easily translate to Qt
from PyQt4 import QtCore, QtGui
class LabelButton(QtGui.QWidget):
clicked = QtCore.pyqtSignal()
def __init__(self, labelStr, pixStr, parent=None):
super(LabelButton, self).__init__(parent)
self.label = labelStr
self.pix = QtGui.QPixmap(pixStr)
def paintEvent(self, event):
super(LabelButton, self).paintEvent(event)
rect = event.rect()
painter = QtGui.QPainter(self)
painter.drawPixmap(rect, self.pix)
pos = (rect.bottomLeft()+rect.bottomRight()) / 2
pos.setY(pos.y()-10)
painter.drawText(pos, self.label)
painter.end()
def mousePressEvent(self, event):
event.accept()
self.clicked.emit()
def handleClick():
print "CLICK"
if __name__ == "__main__":
app = QtGui.QApplication([])
widget = LabelButton("A Text Label", "myImage.png")
widget.resize(600,400)
widget.show()
widget.raise_()
widget.clicked.connect(handleClick)
app.exec_()
This is a rough example. You can get more fine tuned with the drawing of the text. This widget takes a label string, and a picture path, and will paint the picture as the background, and the text as a label. You can do any number of things with this custom widget in both the paint event, and with custom signals and events.
I have used this code to Draw over Image in Label:
Image is loaded in Ui and the Code is as follows In paintevent
void ColorTab::paintEvent(QPaintEvent *e){
ui->lbl_capture_img->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
ui->Lbl_color_tab_WG->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
Cap_Image = QImage(ui->lbl_capture_img->pixmap()->toImage());
Lbl_Image = QImage(ui->Lbl_color_tab_WG->pixmap()->toImage());
QPainter painter_Lbl(&Lbl_Image);
QPainter painter_Cap(&Cap_Image);
QPen pen(Qt::white, 5, Qt::DotLine, Qt::RoundCap, Qt::RoundJoin);
painter_Lbl.setPen(pen);
painter_Cap.setPen(pen);
painter_Lbl.drawPolygon(blinkRect_Lbl);
painter_Cap.drawPolygon(blinkRect_Cap);
ui->lbl_capture_img->setPixmap(QPixmap::fromImage(Cap_Image));
ui->Lbl_color_tab_WG->setPixmap(QPixmap::fromImage(Lbl_Image));
painter_Cap.end();
painter_Lbl.end();
}

Why isn't the "rectangle" that I want to draw on my Qt widget showing up?

I basically want to display a rectangle on a dialog window widget. Using another question as reference, I tried to adapt the framework of using a QLabel and painting to it (the process overall seems overly complicated).
I started by making a member in the dialog box's class:
QLabel* label;
In the constructor of the dialog box:
label = new QLabel(this);
label->setGeometry(20, 50, 50, 100);
Just to try and make it work, I gave the dialog box a button to make the "rectangle" created with the label appear on the widget. I connected the "pressed" signal of this button to a slot which does the following:
QPixmap pixmap(50, 100);
pixmap.fill(QColor("transparent"));
QPainter painter(&pixmap);
painter.setBrush(QBrush(Qt::black));
painter.drawRect(20, 50, 50, 100);
label->setPixmap(pixmap);
update();
Unfortunately, nothing appears in the widget when I press the button. What am I missing here?
I tried this with PyQt and it generally works, but I'm not 100% sure about the procedure. Maybe you should try calling painter.end() the painter before calling setPixmap(). Also, I'm not sure if one is supposed to draw onto a QPixmap outside of QWidget:paintEvent, it might be safer to draw a QImage and create a QPixmap from it.
from PyQt4 import QtGui
app = QtGui.QApplication([])
class Test(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.bn = QtGui.QPushButton("Paint")
self.lb = QtGui.QLabel()
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.bn)
layout.addWidget(self.lb)
self.bn.clicked.connect(self.handleClick)
def handleClick(self):
pixmap = QtGui.QPixmap(50, 100)
pixmap.fill(QtGui.QColor("transparent"))
p = QtGui.QPainter(pixmap)
p.drawRect(0,0,50-1,100-1)
p.end()
self.lb.setPixmap(pixmap)
t = Test()
t.show()
app.exec_()
For simply drawing a rectangle this is certainly very complicated. I don't know what you are planning, be aware that there is QGraphicsView for drawing figures.

Resources