How would I apply a border radius, or otherwise achieve a rounded-corner effect, with a QMovie in PyQt5? It doesn't seem to react to QSS. Although I do not believe it to be relevant, here is my current code anyways, to give an idea what I have tried:
image = QLabel()
image.setObjectName("rant-image")
movie = QMovie("image_cache/" + img_name)
image.setMovie(movie)
movie.start()
with QSS:
QLabel#rant-image{
border-radius: 5px;
}
I have also tried painting the current pixmap each paintEvent by subclassing a QWidget, but nothing appears, and the pixmap has dimensions of 0:
invisible_pen = QPen()
invisible_pen.setWidth(0)
invisible_pen.setStyle(Qt.NoPen)
class RoundedMovie(QWidget):
def __init__(self, movie, parent=None):
QWidget.__init__(self, parent)
self.movie = movie
def setMovie(self, movie):
self.movie = movie
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing, True)
pixmap = self.movie.currentPixmap()
brush = QBrush(pixmap)
rect = QRect(0, 0, pixmap.width() - 10, pixmap.height() - 10)
painter.setPen(invisible_pen)
painter.setBrush(brush)
painter.drawRoundedRect(rect, 5, 5)
I also know the above implementation would not work because a paintEvent will not occur often enough to play the movie as expected
A possible solution is to implement a QProxyStyle:
from PyQt5 import QtCore, QtGui, QtWidgets
class RoundPixmapStyle(QtWidgets.QProxyStyle):
def __init__(self, radius=10, *args, **kwargs):
super(RoundPixmapStyle, self).__init__(*args, **kwargs)
self._radius = radius
def drawItemPixmap(self, painter, rectangle, alignment, pixmap):
painter.save()
pix = QtGui.QPixmap(pixmap.size())
pix.fill(QtCore.Qt.transparent)
p = QtGui.QPainter(pix)
p.setBrush(QtGui.QBrush(pixmap))
p.setPen(QtCore.Qt.NoPen)
p.drawRoundedRect(pixmap.rect(), self._radius, self._radius)
p.end()
super(RoundPixmapStyle, self).drawItemPixmap(painter, rectangle, alignment, pix)
painter.restore()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QLabel(alignment=QtCore.Qt.AlignCenter)
proxy_style = RoundPixmapStyle(radius=20, style=w.style())
w.setStyle(proxy_style)
movie = QtGui.QMovie("foo.gif")
w.setMovie(movie)
movie.start()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
Related
I've a problem with pyqt5. I've create a windows with a scene that has a background image, re-implementing drawBackground. I've also a button that allow me to add a line in a position on the scene. The problem is that if i click the button to draw the line, then this line is drawn in a separate scene with it's own background, instead of into the scene i have. Seems like it create a new scene to draw the line on. Here is my code:
import sys
from PyQt5 import QtGui
from PyQt5.QtGui import QImage
from PyQt5.QtWidgets import (QMainWindow, QGraphicsView, QPushButton,
QHBoxLayout, QVBoxLayout, QWidget, QApplication, QGraphicsScene)
class GraphicsScene(QGraphicsScene):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._image = QImage()
#property
def image(self):
return self._image
#image.setter
def image(self, img):
self._image = img
self.update()
def drawBackground(self, painter, rect):
if self.image.isNull():
super().drawBackground(painter, rect)
else:
painter.drawImage(rect, self._image)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.title = "parcelDeliveryIta";
self.top = 100
self.left = 100
self.width = 1500
self.height = 900
self.initUI()
def initUI(self):
self.scene = GraphicsScene(self)
self.scene._image = QImage('Italy.png')
view = QGraphicsView(self.scene, self)
self.scene.setSceneRect(0, 0, view.width(), view.height())
addLine = QPushButton('AddLine')
addLine.clicked.connect(self.addLine)
hbox = QHBoxLayout(self)
hbox.addWidget(view)
vbox = QVBoxLayout(self)
vbox.addWidget(addLine)
hbox.addLayout(vbox)
self.setWindowTitle(self.title)
self.setGeometry(self.top, self.left, self.width, self.height)
self.setFixedSize(self.width, self.height)
self.setLayout(hbox)
self.show()
def addLine(self):
self.scene.addLine(0, 0, 100, 100)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec_())
and this is the result when clicking the button:
As it is possible to see, the line is drawn with its own backgroung, that is image I've setted as background to the scene (the image above is cropped to show better the line)
Thanks for helping.
Explanation:
It seems that my previous solution is not suitable for your case since as the docs points out:
void QGraphicsScene::drawBackground(QPainter *painter, const
QRectF &rect) Draws the background of the scene using painter,
before any items and the foreground are drawn. Reimplement this
function to provide a custom background for the scene.
All painting is done in scene coordinates. The rect parameter is the
exposed rectangle.
If all you want is to define a color, texture, or gradient for the
background, you can call setBackgroundBrush() instead.
See also drawForeground() and drawItems().
(emphasis mine)
That paint will also be used to paint the base of the items and therefore caused that behavior.
Solution:
So you will have to resort to another solution, for example to use a QGraphicsPixmapItem as a base and readjust the size of the window with that information:
import sys
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import (
QGraphicsView,
QPushButton,
QHBoxLayout,
QVBoxLayout,
QWidget,
QApplication,
QGraphicsScene,
)
class GraphicsView(QGraphicsView):
def __init__(self, parent=None):
super().__init__(parent)
scene = QGraphicsScene(self)
self.setScene(scene)
self._pixmap_item = self.scene().addPixmap(QPixmap())
self._pixmap_item.setZValue(-1)
#property
def pixmap(self):
return self._pixmap_item.pixmap()
#pixmap.setter
def pixmap(self, pixmap):
self._pixmap_item.setPixmap(pixmap)
self.scene().setSceneRect(self._pixmap_item.boundingRect())
def resizeEvent(self, event):
if not self._pixmap_item.pixmap().isNull():
self.fitInView(self._pixmap_item)
super().resizeEvent(event)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.title = "parcelDeliveryIta"
self.top = 100
self.left = 100
self.width = 1500
self.height = 900
self.initUI()
def initUI(self):
self.view = GraphicsView(self)
self.view.pixmap = QPixmap("Italy.png")
addLine = QPushButton("AddLine")
addLine.clicked.connect(self.addLine)
hbox = QHBoxLayout(self)
hbox.addWidget(self.view)
vbox = QVBoxLayout()
vbox.addWidget(addLine)
hbox.addLayout(vbox)
self.setWindowTitle(self.title)
self.setGeometry(self.top, self.left, self.width, self.height)
self.setFixedSize(self.width, self.height)
self.show()
def addLine(self):
self.view.scene().addLine(0, 0, 100, 100)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec_())
Librecad uses a widget which can be infinitely resized, you can zoom in and out as much as you can. Which widget does it uses?
When I paint into a common widget, the painting is done at certain coordinates of the widget. However, I would like to draw at floating coordinates of the widget and use a line width which is fixed to certain pixels of the viewport.
Before resizing:
After resizing:
Which widget provides this functionality?
You have to use QGraphicsView and QGraphicsScene(see Graphics View Framework):
from PyQt5 import QtCore, QtGui, QtWidgets
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super(GraphicsView, self).__init__(parent)
self.factor = 1.2
self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
self.setRenderHints(
QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform
)
self.setMouseTracking(True)
self.setScene(
QtWidgets.QGraphicsScene(QtCore.QRectF(-400, -400, 800, 800), self)
)
QtWidgets.QShortcut(QtGui.QKeySequence.ZoomIn, self, activated=self.zoomIn) # Ctrl + +
QtWidgets.QShortcut(QtGui.QKeySequence.ZoomOut, self, activated=self.zoomOut) # Ctrl + -
#QtCore.pyqtSlot()
def zoomIn(self):
self.zoom(self.factor)
#QtCore.pyqtSlot()
def zoomOut(self):
self.zoom(1 / self.factor)
def zoom(self, f):
self.scale(f, f)
def drawForeground(self, painter, rect):
super(GraphicsView, self).drawForeground(painter, rect)
if not hasattr(self, "cursor_position"):
return
painter.save()
painter.setTransform(QtGui.QTransform())
pen = QtGui.QPen(QtGui.QColor("yellow"))
pen.setWidth(4)
painter.setPen(pen)
r = self.mapFromScene(rect).boundingRect()
linex = QtCore.QLine(
r.left(), self.cursor_position.y(), r.right(), self.cursor_position.y(),
)
liney = QtCore.QLine(
self.cursor_position.x(), r.top(), self.cursor_position.x(), r.bottom(),
)
for line in (linex, liney):
painter.drawLine(line)
painter.restore()
def mouseMoveEvent(self, event):
self.cursor_position = event.pos()
self.scene().update()
super(GraphicsView, self).mouseMoveEvent(event)
def wheelEvent(self, event):
angle = event.angleDelta().y()
if angle < 0:
self.zoomIn()
else:
self.zoomOut()
super(GraphicsView, self).wheelEvent(event)
if __name__ == "__main__":
import sys
import random
app = QtWidgets.QApplication(sys.argv)
w = GraphicsView()
for _ in range(4):
r = QtCore.QLineF(
*random.sample(range(-200, 200), 2), *random.sample(range(50, 150), 2)
)
it = w.scene().addLine(r)
pen = QtGui.QPen(QtGui.QColor(*random.sample(range(255), 3)))
pen.setWidthF(5.0)
pen.setCosmetic(True)
it.setPen(pen)
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
I
I have one QMainWindow which contains a QWidget with a QPixmap in that. When I try to get the location of mouse click, it shows based on the QMainWindow and not based on the QPixmap (even on the gray area out of the image). Can anyone help me how can I transfer the x,y position from QMainWindow to QPixmap (Image) coordinates only?
The following code returns the x,y coordinates of the whole QMainWindow and not the QPixmap (only on the image) specifically.
class ImageViewer(QtGui.QMainWindow):
def __init__(self, input, name):
super(ImageViewer, self).__init__()
self.image_series = input
self.starter()
def starter(self):
self.setWindowTitle(self.title)
self.main_Viewer_frame = QtGui.QFrame()
self.setCentralWidget(self.main_Viewer_frame)
self.main_Viewer_frame_layout = QtGui.QGridLayout()
self.show()
self.main_Viewer_frame.mousePressEvent = self.getpos
def getpos(self, event):
x = event.pos().x()
y = event.pos().y()
print(x,y)
class SliceViewer(QtGui.QGraphicsView):
def __init__(self, input, *args, **kwargs):
self.scene = QtGui.QGraphicsScene()
super(SliceViewer, self).__init__(self.scene, *args, **kwargs)
self.setScene(self.scene)
self.img_overlay = self.scene.addPixmap(self.getPixmap(input))
viewer = SliceViewer(img.png)
window = ImageViewer(img.png, "Super Mario")
window.layout.addwidget(viewer)
You have the following errors:
Do not assign a function to a method that inherits from the class as mousePressEvent because it already has a predefined behavior but when you do X you are deleting it.
You have to use the mouseClicked method of the QGraphicsView to get the click on the scene, and then make the necessary conversions to obtain the position with respect to the QGraphicsPixmapItem (which is equivalent to a position with respect to the QPixmap).
Considering the above, the solution is:
from PySide import QtCore, QtGui
class SliceViewer(QtGui.QGraphicsView):
pixmapClicked = QtCore.Signal(QtCore.QPoint)
def __init__(self, filename, *args, **kwargs):
self._scene = QtGui.QGraphicsScene()
super(SliceViewer, self).__init__(self._scene, *args, **kwargs)
self.img_overlay = self._scene.addPixmap(QtGui.QPixmap(filename))
def mousePressEvent(self, event):
vp = event.pos()
if self.itemAt(vp) == self.img_overlay:
sp = self.mapToScene(vp)
lp = self.img_overlay.mapFromScene(sp).toPoint()
self.pixmapClicked.emit(lp)
super(SliceViewer, self).mousePressEvent(event)
class ImageViewer(QtGui.QMainWindow):
def __init__(self, parent=None):
super(ImageViewer, self).__init__(parent)
viewer = SliceViewer("/path/of/Super Mario")
central_widget = QtGui.QFrame()
self.setCentralWidget(central_widget)
lay = QtGui.QGridLayout(central_widget)
lay.addWidget(viewer)
self.setGeometry(200, 200, 512, 512)
#QtCore.Slot(QtCore.QPoint)
def on_pixmapClicked(self, point):
print(point)
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
w = ImageViewer()
w.show()
sys.exit(app.exec_())
I have a QWidget with a QLayout on which there is a QLabel.
I set a QPixmap on the label. Wherever the user clicks on the image, I want to draw a point. I defined mouseReleaseEvent (which works) and paintEvent (but no points are drawn). I've read all similar questions and none of the solutions worked for me. Any help? My relevant code:
class ImageScroller(QtWidgets.QWidget):
def __init__(self, img):
QtWidgets.QWidget.__init__(self)
main_layout = QtWidgets.QVBoxLayout()
self._image_label = QtWidgets.QLabel()
self._set_image(img)
main_layout.addWidget(self._image_label)
main_layout.addStretch()
self.setLayout(main_layout)
def _set_image(self, img):
img = qimage2ndarray.array2qimage(img)
qimg = QtGui.QPixmap.fromImage(img)
self._img_pixmap = QtGui.QPixmap(qimg)
self._image_label.show()
def paintEvent(self, paint_event):
painter = QtGui.QPainter(self)
painter.begin(self)
painter.setPen(QtGui.QPen(QtCore.Qt.red))
pen = QtGui.QPen()
pen.setWidth(20)
painter.setPen(pen)
painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
painter.drawPoint(300,300)
painter.drawLine(100, 100, 400, 400)
for pos in self.chosen_points:
painter.drawPoint(pos)
painter.end()
def mouseReleaseEvent(self, cursor_event):
self.chosen_points.append(QtGui.QCursor().pos())
self.update()
When you use QtGui.QCursor.pos() is getting the coordinates of the cursor with respect to the screen, but when you want to paint a widget you must be in the coordinates of the widget, for it the widget has the mapToGlobal() method:
self.mapFromGlobal(QtGui.QCursor.pos())
But in this case there is another solution, you must use the event that returns mouseReleaseEvent that has the information in the pos() method:
cursor_event.pos()
Another problem is that the label you created is above the widget so you do not see the points, the easiest thing is to draw the QPixmap directly with the drawPixmap() method.
Complete code:
from PyQt5 import QtWidgets, QtGui, QtCore
class ImageScroller(QtWidgets.QWidget):
def __init__(self):
self.chosen_points = []
QtWidgets.QWidget.__init__(self)
self._image = QtGui.QPixmap("image.png")
def paintEvent(self, paint_event):
painter = QtGui.QPainter(self)
painter.drawPixmap(self.rect(), self._image)
pen = QtGui.QPen()
pen.setWidth(20)
painter.setPen(pen)
painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
painter.drawPoint(300, 300)
painter.drawLine(100, 100, 400, 400)
for pos in self.chosen_points:
painter.drawPoint(pos)
def mouseReleaseEvent(self, cursor_event):
self.chosen_points.append(cursor_event.pos())
# self.chosen_points.append(self.mapFromGlobal(QtGui.QCursor.pos()))
self.update()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = ImageScroller()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
I'm working on a touch screen app where gui space is very tight. I'd like to rotate a QLabel a bit so that it's either vertical, or offset at a diagonal a bit. Any suggestions? I couldn't find anything relevant on the QLabel interface.
Thanks so much!
QLabel does not do this. But you can easily create your own widget containing a bit of text:
class MyLabel(QtGui.QWidget):
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setPen(QtCore.Qt.black)
painter.translate(20, 100)
painter.rotate(-90)
painter.drawText(0, 0, "hellos")
painter.end()
Another option is to draw a QGraphicsView, you then have the liberty to map real widgets (i.e. a QLabel) through any coordinate transformation.
I used this post to make another solution that I think is maybe better. Here it is:
class VerticalLabel(QLabel):
def __init__(self, *args):
QLabel.__init__(self, *args)
def paintEvent(self, event):
QLabel.paintEvent(self, event)
painter = QPainter (self)
painter.translate(0, self.height()-1)
painter.rotate(-90)
self.setGeometry(self.x(), self.y(), self.height(), self.width())
QLabel.render(self, painter)
def minimumSizeHint(self):
size = QLabel.minimumSizeHint(self)
return QSize(size.height(), size.width())
def sizeHint(self):
size = QLabel.sizeHint(self)
return QSize(size.height(), size.width())
Try this
class myStyle(QCommonStyle):
def __init__(self, angl=0, point=QPoint(0, 0)):
super(myStyle, self).__init__()
self.angl = angl
self.point = point
def drawItemText(self, painter, rect, flags, pal, enabled, text, textRole):
if not text:
return
savedPen = painter.pen()
if textRole != QPalette.NoRole:
painter.setPen(QPen(pal.brush(textRole), savedPen.widthF()))
if not enabled:
pen = painter.pen()
painter.setPen(pen)
painter.translate(self.point)
painter.rotate(self.angl)
painter.drawText(rect, flags, text)
painter.setPen(savedPen)
and
label = QLabel()
label.setStyle(myStyle(-45, QPoint(0, 100)))
Answer from #Controlix is a generic implementation, however, I was getting a "recursive painting call" warning. I was able to address that by combining approaches from #Controlix and #Ivo. Here is my implementation:
from PyQt5.Qt import QLabel
from PyQt5 import QtGui
class VerticalLabel(QLabel):
def __init__(self, *args):
QLabel.__init__(self, *args)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.translate(0, self.height())
painter.rotate(-90)
painter.drawText(0, self.width()/2, self.text())
painter.end()
I took all of the above posts, and tested each one.
I believe this is the best combination of them all.
This centers the text horizontally and vertically, and sets size hints correctly.
from PyQt5 import QtGui
from PyQt5 import QtWidgets
from PyQt5 import QtCore
class VerticalLabel(QtWidgets.QLabel):
def __init__(self, *args):
QtWidgets.QLabel.__init__(self, *args)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.translate(0, self.height())
painter.rotate(-90)
# calculate the size of the font
fm = QtGui.QFontMetrics(painter.font())
xoffset = int(fm.boundingRect(self.text()).width()/2)
yoffset = int(fm.boundingRect(self.text()).height()/2)
x = int(self.width()/2) + yoffset
y = int(self.height()/2) - xoffset
# because we rotated the label, x affects the vertical placement, and y affects the horizontal
painter.drawText(y, x, self.text())
painter.end()
def minimumSizeHint(self):
size = QtWidgets.QLabel.minimumSizeHint(self)
return QtCore.QSize(size.height(), size.width())
def sizeHint(self):
size = QtWidgets.QLabel.sizeHint(self)
return QtCore.QSize(size.height(), size.width())
class Example(QtWidgets.QWidget):
def __init__(self):
super(Example, self).__init__()
self.initUI()
def initUI(self):
lbl1 = VerticalLabel('ABSOLUTE')
lbl1.setFont(QtGui.QFont('Arial', 20))
lbl1.setStyleSheet("QLabel { background-color : black; color : orange; }");
lbl2 = VerticalLabel('lbl 2')
lbl3 = VerticalLabel('lbl 3')
hBoxLayout = QtWidgets.QHBoxLayout()
hBoxLayout.addWidget(lbl1)
hBoxLayout.addWidget(lbl2)
hBoxLayout.addWidget(lbl3)
self.setLayout(hBoxLayout)
self.setGeometry(300, 300, 250, 150)
self.show()
def main():
app = QtWidgets.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()