Short context:
I have a QMainwindow with an mpv player inside. I play videos with mpv, create overlay images with PIL and run it all in the pyqt window. The overlay image is updated more or less every frame of the video.
Here is my issue:
If the mpv picture is large, then updating the overlay images is far too slow (I have optimized a lot to improve performance, using separate processes and threads, only using one overlay etc.). If the picture is small, however, it all works flawlessly (thus indicating that it is not far from satisfactory in performance).
I wouldn't mind losing resolution to gain performance, so I want to have a large window with lower resolution content. Is this possible?
The bottleneck here is mpv's overlay.update function
My main idea is to zoom the QMainwindow, but I cannot seem to find a way to do this. Any other solution is of course sufficient.
Example code (note that test.mp4 is the hardcoded video, provide anything you have)
#!/usr/bin/env python3
import mpv
import sys
from PIL import Image, ImageDraw
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class Test(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.container = QWidget(self)
self.setCentralWidget(self.container)
self.container.setAttribute(Qt.WA_DontCreateNativeAncestors)
self.container.setAttribute(Qt.WA_NativeWindow)
self.w = 1800
self.h = int(self.w / 16 * 9)
self.setFixedSize(self.w, self.h)
self.player = mpv.MPV(wid=str(int(self.container.winId())),
log_handler=print)
self.player.play('test.mp4')
self.overlay = self.player.create_image_overlay()
self.coords = [20, 20, 50, 50]
def play(self):
#self.player.property_observer("time-pos")
def _time_observer(_name: str, value: float) -> None:
for i in range(len(self.coords)):
self.coords[i] = self.coords[i]*2 % self.h
img = Image.new("RGBA", (self.w, self.h), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
draw.rectangle(self.coords, outline=(255,255,255,255), width=4)
self.overlay.update(img)
app = QApplication(sys.argv)
# This is necessary since PyQT stomps over the locale settings needed by libmpv.
# This needs to happen after importing PyQT before creating the first mpv.MPV instance.
import locale
locale.setlocale(locale.LC_NUMERIC, 'C')
win = Test()
win.show()
win.play()
sys.exit(app.exec_())
Short Summary
Having a large window causes mpv's overlay.update method to consume too much time/computation. It is acceptable to decrease the dpi (resolution) of the overlay pictures, and even the video, to make it run faster.
Related
I need to play a .mov video (ProRes4444) with alpha channel in a scene. The scene has a background image and I need to use the alpha channel of the video so it overlays on the background.
If I open the video normally with QMediaPlayer, the alpha channel appears in black.
screen with background pic & video with black alpha:
How can I make the output of the QMediaPlayer (QGraphicsVideoItem) respect the alpha and make the overlay effect possible?
The closest I got to the answer based on online research is code in cpp that I've found that shows the necessity to create a subclass of a QAbstractVideoSurface that receives videoframes converts to ARGB, then forwards those to a QLabel that displays them.
Displaying a video with an alpha channel using qt
I've also tried that unsuccessfully. Is this the right course or I'm just missing something simple on my current code?
EDIT:
Link to files (background image and video .mov)
https://drive.google.com/drive/folders/1LIZzTg1E8wkaD0YSvkkcfSATdlDTggyh?usp=sharing
import sys
from PyQt5.QtMultimedia import *
from PyQt5.QtMultimediaWidgets import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class VideoWindow(QMainWindow):
def __init__(self):
super(VideoWindow, self).__init__()
self.setWindowTitle('QMediaPlayer TEST')
self.resize(1920, 1080)
self.vista = QGraphicsView(self)
self.vista.setGeometry(QRect(0, 0, 1920, 1080))
self.scene = QGraphicsScene(self.vista)
self.scene.setSceneRect(0, 0, 1920, 1080)
self.vista.setScene(self.scene)
self.graphvitem1 = QGraphicsVideoItem()
#SET BACKGROUND IMAGE ON SCENE
self.tempImg = QPixmap("/Users/elemental/Desktop/pyvids/fons.jpeg")
self.tempImg = self.tempImg.scaled(self.scene.width(), self.scene.height())
self.graphicsPixmapItem = QGraphicsPixmapItem(self.tempImg)
self.scene.addItem(self.graphicsPixmapItem)
#SET VIDEO 1 WITH LOOP
self.mediaPlayer1 = QMediaPlayer(None, QMediaPlayer.VideoSurface)
self.mediaPlayer1.setVideoOutput(self.graphvitem1)
self.playlist1 = QMediaPlaylist(self)
self.playlist1.addMedia(QMediaContent(QUrl.fromLocalFile("/Users/elemental/Desktop/pyvids/vida1.mov")))
self.playlist1.setCurrentIndex(1)
self.playlist1.setPlaybackMode(QMediaPlaylist.CurrentItemInLoop)
self.mediaPlayer1.setPlaylist(self.playlist1)
self.graphvitem1.setPos(500, 100)
self.graphvitem1.setSize(QSizeF(1000, 500))
self.scene.addItem(self.graphvitem1)
self.mediaPlayer1.play()
self.vista.show()
if __name__ == '__main__':
app = QApplication([])
window = VideoWindow()
window.show()
sys.exit(app.exec_())
From what I can see, QVideoWidget doesn't support alpha channels by default, so it falls back to the "basic" black background.
But, implementation seems possible, by properly subclassing QAbstractVideoSurface.
Consider that the following code is experimental, my knowledge of QMediaPlayer and the Qt video surface isn't that deep (the former is an abstract for multiple platforms and multiple libraries that can behave very differently on different configurations), and I could only test it on two Linux platforms, so I don't know how it behaves under Windows nor MacOS.
The assumption is that the video surface provides a default dedicated QWidget subclass (VideoWidget) unless another class with a suitable setImage is provided, and updates its image whenever the media player requires it.
Note that I only tested it with a couple of videos (including the provided one), and further testing might be required.
from PyQt5.QtMultimedia import *
from PyQt5.QtMultimediaWidgets import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class VideoWidget(QWidget):
image = QImage()
def __init__(self, **kwargs):
super().__init__(**kwargs)
def setImage(self, image):
self.image = image
self.update()
def sizeHint(self):
return QSize(640, 480)
def paintEvent(self, event):
qp = QPainter(self)
# ensure that smooth transformation is used while scaling pixmaps
qp.setRenderHints(qp.SmoothPixmapTransform)
# provide compliancy with background set using stylesheets, see:
# https://doc.qt.io/qt-5/stylesheet-reference.html#qwidget-widget
opt = QStyleOption()
opt.initFrom(self)
self.style().drawPrimitive(QStyle.PE_Widget, opt, qp, self)
# draw the image, scaled to the widget size; if you need fixed sizes
# or keep aspect ratio, implement this (or the widget) accordingly
qp.drawImage(self.rect(), self.image, self.image.rect())
class AlphaVideoDrawer(QAbstractVideoSurface):
def __init__(self, videoWidget=None, widgetOptions=None):
super().__init__()
if videoWidget:
if not hasattr(videoWidget, 'setImage'):
raise NotImplementedError(
'setImage() must be implemented for videoWidget!')
else:
if not isinstance(widgetOptions, dict):
widgetOptions = {}
elif not 'styleSheet' in widgetOptions:
# just a default background for testing purposes
widgetOptions = {'styleSheet': 'background: darkGray;'}
videoWidget = VideoWidget(**widgetOptions)
self.videoWidget = videoWidget
# QVideoFrame.image() has been introduced since Qt 5.15
version, majVersion, minVersion = map(int, QT_VERSION_STR.split('.'))
if version < 6 and majVersion < 15:
self.imageFromFrame = self._imageFromFrameFix
else:
self.imageFromFrame = lambda frame: frame.image()
def _imageFromFrameFix(self, frame):
clone_frame = QVideoFrame(frame)
clone_frame.map(QAbstractVideoBuffer.ReadOnly)
image = QImage(
clone_frame.bits(), frame.width(), frame.height(), frame.bytesPerLine(),
QVideoFrame.imageFormatFromPixelFormat(frame.pixelFormat()))
clone_frame.unmap()
return image
def supportedPixelFormats(self, type):
return [QVideoFrame.Format_ARGB32]
def present(self, frame: QVideoFrame):
if frame.isValid():
self.videoWidget.setImage(self.imageFromFrame(frame))
if self.surfaceFormat().pixelFormat() != frame.pixelFormat() or \
self.surfaceFormat().frameSize() != frame.size():
self.setError(QAbstractVideoSurface.IncorrectFormatError)
self.stop()
return False
else:
return True
class AlphaVideoTest(QMainWindow):
def __init__(self):
super().__init__()
self.setStyleSheet('''
QFrame#mainFrame {
background: green;
}
''')
mainFrame = QFrame(objectName='mainFrame')
self.setCentralWidget(mainFrame)
layout = QVBoxLayout(mainFrame)
self.playButton = QPushButton('Play', checkable=True)
layout.addWidget(self.playButton)
self.drawer = AlphaVideoDrawer()
layout.addWidget(self.drawer.videoWidget)
self.mediaPlayer1 = QMediaPlayer(self, QMediaPlayer.VideoSurface)
self.playlist = QMediaPlaylist(self)
path = QDir.current().absoluteFilePath('vida1.mov')
self.playlist.addMedia(QMediaContent(QUrl.fromLocalFile(path)))
self.playlist.setCurrentIndex(1)
self.playlist.setPlaybackMode(QMediaPlaylist.CurrentItemInLoop)
self.mediaPlayer1.setPlaylist(self.playlist)
self.mediaPlayer1.setVideoOutput(self.drawer)
self.playButton.toggled.connect(self.togglePlay)
def togglePlay(self, play):
if play:
self.mediaPlayer1.play()
self.playButton.setText('Pause')
else:
self.mediaPlayer1.pause()
self.playButton.setText('Play')
import sys
app = QApplication(sys.argv)
test = AlphaVideoTest()
test.show()
sys.exit(app.exec_())
I based the above code on the following sources:
wrong video frame of present() of qabstractvideosurface in pyqt5
Displaying a video with an alpha channel using qt
Note that I limited the supportedPixelFormats output, as using the full list of formats provided in the related question didn't work; this doesn't mean that this would work anyway, but that further testing is probably required, possibly on different machines and different OS/System configurations and video formats: remember that QMediaPlayer completely relies on the underlying OS and default media backend.
Finally, if you only need this for "limited" and predefined animations, consider implementing your own subclass of QWidget that uses a list of loaded PNG images and shows them by implementing paintEvent() that would be called by updates based on a QVariantAnimation. While this kind of implementation might result less performant or ideal, it has the major benefit of providing cross platform compatibility.
When two rectangles overlap, I noticed that the intersection between the two become more opaque, which, as I understand, is due to the addition of the QBrush alphas.
Visual example of the overlapping intersection becoming more opaque:
I would like to prevent the overlapping intersection from becoming more opaque, the area should have the same color as the rest of the rectangles. (Which is what happens when using fully opaque QColors, but I want to use semitransparent ones).
I have read several topics on the subject (including this very similar question: Qt: Overlapping semitransparent QgraphicsItem), every answer is stating that the problem can be solved by changing the Composition Mode of the painter, however I just can't find a composition mode that provide the expected result, I have tried all of the 34 Composition Modes listed in the Qpainter documentation (https://doc.qt.io/qt-5/qpainter.html#setCompositionMode). Some of the modes just turn the QColor fully opaque, and most are irrelevant to this problem, none of them allow me to have the intersection of the two rectangles keep the same semitransparent color as the rest.
I would be thankful if anyone has an idea.
You can find below a short code reproducing the problem:
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter, QColor
from PyQt5.QtCore import QRect
import sys
class Drawing(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(300, 300, 350, 300)
self.show()
def paintEvent(self, event):
painter = QPainter(self)
painter.setCompositionMode(QPainter.CompositionMode_SourceOver)
painter.setPen(QColor(128, 128, 255, 128))
painter.setBrush(QColor(128, 128, 255, 128))
rect1 = QRect(10,10,100,100)
rect2 = QRect(50,50,100,100)
painter.drawRect(rect1)
painter.drawRect(rect2)
def main():
app = QApplication(sys.argv)
ex = Drawing()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
The simple solution is to use a QPainterPath, ensuring that it both uses setFillRule(Qt.WindingFill) and use the simplified() version of the path, that merges all subpaths:
path = QPainterPath()
path.setFillRule(Qt.WindingFill)
path.addRect(10, 10, 100, 100)
path.addRect(50, 50, 100, 100)
painter.drawPath(path.simplified())
I've been working on a chess GUI for a short while. Am new to PyQt5. Already coded quite a bit though, so for the sake of generality (and to keep the lines of code here to a minimum), I'll just treat it as any board game that has a grid, and has pieces navigating the grid. I managed to get drag and drop behaviour in different ways, but they all had some (for me significant) downside. In any case, the particular interaction I am trying to implement is 'grabbing' a piece from a certain square (on click) and then having it follow the mouse, until it is dropped onto a different square (on release).
Version info
PyQt5 version: 5.15.0
Windows version: Windows 10 Pro 64 bits (10.0)
First Approach
Initially I was hoping simply moving a QLabel with a given pixmap might do the trick but when moving the mouse quite fast, it seems the drawing is not fast enough to keep up with the movement of the mouse. As a result, it looks like the image phases in and out of existence, or only half of it renders before it gets redrawn. Tried something similar using QGraphicsView and QGraphicsScene, and some QGraphicsItem objects and making them movable. But the results were the same, image cutting off while trying to keep up with the cursor. Example code attempting to use QGraphicsView:
from PyQt5.QtWidgets import (QMainWindow, QGraphicsView, QApplication, QGraphicsPixmapItem,
QGraphicsScene, QGraphicsItem)
from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import Qt
import sys
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.pm = QPixmap(70,70)
self.pm.fill(Qt.black)
self.item = QGraphicsPixmapItem(self.pm)
self.setGeometry(100,100,900,900)
self.scene = QGraphicsScene()
self.view = QGraphicsView(self.scene, self)
self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.viewWidth = 800
self.viewHeight = self.viewWidth
self.view.setGeometry(0,0,self.viewWidth, self.viewHeight)
self.bk = self.scene.addPixmap(self.pm)
self.bk.setFlag(QGraphicsItem.ItemIsMovable)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
With this code, if you grab the 'piece' and go back and forth with the mouse at a high speed, I mainly see half/a quarter of the image. In the middle of the area I move the object, I usually don't see the object at all. Video of this in action: https://vimeo.com/user41790219
Second Approach
I found the QDrag class, and with it, a reasonable way of creating draggable pieces. It looks fantastic, absolutely no problems once the drag actually started, the image moves very smoothly. However, I wanted the drag to start on a mousePressEvent, and have it start immediately. With 'start immediately', I rather mean render the pixmap at the mouse location on click. This does not occur on Windows. Now, as stated in https://doc.qt.io/qt-5/qdrag.html#exec, the QDrag.exec_() is blocking on Windows. It says that once entering exec, it will call processEvents() frequently to make sure the GUI stays responsive. This is all fine, but on Windows it does not render the image assigned to QDrag, until the mouse has moved some amount. This is a shame, because on Linux it actually looks kinda snappy (on click it moves to the mouse instantly). Maybe it's a bit petty, but I really want to have the piece instantly snap to the mouse. This is some sample code I kinda pieced together/came up using QDrag and QPushbuttons that shows this behaviour:
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtSvg import *
import sys
BOARDSIZE = 500
SQUARESIZE = 100
PIECESIZE = 70
FILES = 5
RANKS = 5
class Square(QPushButton):
def __init__(self, pixmap=None, size=100, dark=False):
super(Square,self).__init__()
self.pixmap = pixmap
self.size = size
if dark:
self.bg = '#8CA2AD'
else:
self.bg = '#DEE3E6'
self.setFixedWidth(size)
self.setFixedHeight(size)
if self.pixmap is not None:
self.setIcon(QIcon(self.pixmap))
self.setIconSize(self.pixmap.rect().size())
self.setStyle()
self.setAcceptDrops(True)
def setStyle(self):
self.setStyleSheet(f"""
QPushButton {{
background-color:{self.bg};
color:#ffffff;
border-width:0px;
border-radius: 0px;
padding:0px 0px 0px 0px;
}}
""")
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
if self.pixmap is not None:
s = self.pixmap.width()
drag = QDrag(self)
drag.setPixmap(self.pixmap)
mime = QMimeData()
mime.setText('piece')
drag.setMimeData(mime)
drag.setHotSpot(QPoint(int(s/2) , int(s/2)))
self.setIcon(QIcon())
self.pixmap = None
drag.exec_(Qt.MoveAction | Qt.CopyAction)
def dragEnterEvent(self, event):
event.accept()
def dropEvent(self, event):
pixmap = QPixmap(PIECESIZE, PIECESIZE)
pixmap.fill(Qt.black)
self.pixmap = pixmap
self.setIcon(QIcon(pixmap))
self.setIconSize(pixmap.rect().size())
class Board(QWidget):
def __init__(self, parent=None, width=BOARDSIZE, height=BOARDSIZE):
super(Board,self).__init__(parent)
self.width = width
self.height = height
self.setFixedWidth(self.width)
self.setFixedHeight(self.height)
grid = QGridLayout(self)
grid.setSpacing(0)
self.setAcceptDrops(False)
size = int(self.width/FILES)
pixmap = QPixmap(PIECESIZE, PIECESIZE)
pixmap.fill(Qt.black)
dark = True
for row in range(0,FILES):
for col in range(0,RANKS):
if row == 0 or row == 4:
pm = pixmap
else:
pm = None
square = Square(pixmap=pm,size=size,dark=dark)
grid.addWidget(square, row, col)
dark = not dark
self.setLayout(grid)
self.setAcceptDrops(False)
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow,self).__init__()
self.board = Board(parent=self)
self.setGeometry(100,100,BOARDSIZE+50,BOARDSIZE+50)
self.setCentralWidget(self.board)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
But in this example, when you click, the drag only visually initiates after the mouse has been moved, I would want it to visually initiate immediately after the mouse is clicked. This is Windows-specific.
Mention-worthy stuff
I found that on a QApplication, you can set the startDragTime and startDragDistance, but after setting those to some values (including 0 and -1), nothing happened, so I assumed they are just symbolic variables to be used as globals.
Question
What I'm looking for is the QDrag behaviour after the mouse moves slightly, i.e. the smooth movement during the drag + that it shows up immediately after the mousePressEvent. I'm also fine with any anything involving the use of QObjects with a pixmap and simply moving them, as long as the movement under the cursor looks smooth (doesnt clip or cut off). Any hints/ideas, or resources I may have missed are greatly appreciated!
The Problem :
I have an application, from which I open a modless child window by clicking on a button. This child window contains an embedded matplotlib figure. I would like this child window to be destroyed, along with the matplotlib figure, after is has been closed.
The problem is that, even though the child window seems to be properly deleted on the Qt side, there seems to be no memory deallocated from the process. The problem seems to be cumulative, i.e. the memory taken up by my application increases if I open multiple child windows and then close them manually with 'X'.
My system : Ubuntu 15.04 + Matplotlib 1.4.2 + python 2.7.9 or 3.4.3 + PySide 1.2.2
Note : I currently circumvent the "memory leak" problem by not destroying the child window and reusing the same artists to update the figure with new data. I would like however to be able to release completely the memory taken up by this child window when it is not needed.
What I've tried so far :
I've tried all the combination I could think of with gc.collect(), setParent(None), deleteLater(), del, set_attributes(QtCore.Qt.WA_DeleteOnClose), I've tried to be extra careful with the namespace and to use only weak links to matplotlib artists, I've also tried with and without using pyplot, but with no real success... I've been able to release some of the memory taken by the matplotlib child window(s) by reimplementing the closeEvent method of the child window class and cleaning stuff manually, but still, it's not working perfect and it's not pretty.
Below is a MCVE that illustrates the problem with a basic implementation of the best "solution" I have so far. I've tried the same code by replacing the FigureCanvasQTAgg widget with a "pure" qt widget (a QLabel hosting a large QPixmap) and all the memory taken up by the child window(s) was released on close.
import sys
import numpy as np
from PySide import QtGui, QtCore
import matplotlib as mpl
mpl.rcParams['backend.qt4']='PySide'
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg
import gc
class MyApp(QtGui.QWidget):
def __init__(self):
super(MyApp, self).__init__()
btn_open = QtGui.QPushButton('Show Figure')
btn_open.clicked.connect(self.show_a_figure)
layout = QtGui.QGridLayout()
layout.addWidget(btn_open, 0, 0)
self.setLayout(layout)
def show_a_figure(self):
MyFigureManager(self).show()
class MyFigureManager(QtGui.QWidget):
def __init__(self, parent=None):
super(MyFigureManager, self).__init__(parent)
self.setWindowFlags(QtCore.Qt.Window)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
layout = QtGui.QGridLayout()
layout.addWidget(MyFigureCanvas(), 0, 0)
self.setLayout(layout)
def closeEvent(self, event):
fig_canvas = self.findChild(MyFigureCanvas)
fig_canvas.figure.clear()
del fig_canvas.figure
fig_canvas.renderer.clear()
del fig_canvas.renderer
fig_canvas.mpl_disconnect(fig_canvas.scroll_pick_id)
fig_canvas.mpl_disconnect(fig_canvas.button_pick_id)
fig_canvas.close()
del fig_canvas
gc.collect()
super(MyFigureManager, self).closeEvent(event)
class MyFigureCanvas(FigureCanvasQTAgg):
def __init__(self, parent=None):
super(MyFigureCanvas, self).__init__(figure=mpl.figure.Figure())
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
#-- plot some data --
ax = self.figure.add_axes([0.1, 0.1, 0.85, 0.85])
ax.axis([0, 1, 0, 1])
N = 100000
x = np.random.rand(N)
y = np.random.rand(N)
colors = np.random.rand(N)
area = np.pi * (5 * np.random.rand(N)) ** 2
ax.scatter(x, y, s=area, c=colors, alpha=0.5)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
w = MyApp()
w.show()
sys.exit(app.exec_())
Update 2015-09-01
Example using a QLabel and only FigureCanvasAgg :
This is a simple application I've produced while trying to isolate the problem. When clicking on the "Show Figure" button:
if the QSpinBox number equals 1, the child window will contain a QLabel displaying a QPixmap produced from an external image.
if the QSpinBox number equals 2, the child window will contain a QLabel displaying a QPixmap produced from a matplotlib figure, using only the non-GUI backend FigureCanvasAgg.
I've also added a button to explicitly call a gc.collect(). In the first case (mode=1), all the memory taken up by the child window(s) are recovered upon closing. In the "matplotlib mode" (mode=2), some memory can be recovered when explicitly forcing a gc.collect(), but some is not. The amount of memory not being released seems to scale with the amount of points plotted in mpl, and somewhat related to the amount of child windows opened at the same time.
I am not very knowledgeable about memory management. I'm currently monitoring the memory used by the app through the System Load Indicator in Ubuntu. Please comment if I'm doing it wrong.
import sys
import numpy as np
from PySide import QtGui, QtCore
import matplotlib as mpl
import gc
from matplotlib.backends.backend_agg import FigureCanvasAgg
class MyApp(QtGui.QWidget):
def __init__(self):
super(MyApp, self).__init__()
btn_open = QtGui.QPushButton('Show Figure')
btn_open.clicked.connect(self.show_a_figure)
btn_call4gc = QtGui.QPushButton('Garbage Collect')
btn_call4gc.clicked.connect(self.collect)
self.mode = QtGui.QSpinBox()
self.mode.setRange (1, 2)
layout = QtGui.QGridLayout()
layout.addWidget(btn_open, 1, 1)
layout.addWidget(self.mode, 1, 2)
layout.addWidget(btn_call4gc, 2, 1)
self.setLayout(layout)
def show_a_figure(self):
MyFigureManager(mode=self.mode.value(), parent=self).show()
def collect(self):
num = gc.collect()
print(num)
class MyFigureManager(QtGui.QWidget):
def __init__(self, mode, parent=None):
super(MyFigureManager, self).__init__(parent)
self.setWindowFlags(QtCore.Qt.Window)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
layout = QtGui.QGridLayout()
layout.addWidget(MyFigureCanvas(mode=mode, parent=self), 0, 0)
self.setLayout(layout)
class MyFigureCanvas(QtGui.QLabel):
def __init__(self, mode, parent=None):
super(MyFigureCanvas, self).__init__()
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.setMaximumSize(1000, 650)
if mode == 1:
#-- import a large image from hardisk --
qpix = QtGui.QPixmap('aLargeImg.jpg')
elif mode == 2:
#-- plot some data with mpl --
canvas = FigureCanvasAgg(mpl.figure.Figure())
renderer = canvas.get_renderer()
ax = canvas.figure.add_axes([0.1, 0.1, 0.85, 0.85])
ax.axis([0, 1, 0, 1])
N = 50000
x = np.random.rand(N)
y = np.random.rand(N)
colors = np.random.rand(N)
area = np.pi * (5 * np.random.rand(N)) ** 2
ax.scatter(x, y, s=area, c=colors, alpha=0.5)
#-- convert mpl imag to pixmap --
canvas.draw()
imgbuf = canvas.figure.canvas.buffer_rgba()
imgwidth = int(renderer.width)
imgheight =int(renderer.height)
qimg = QtGui.QImage(imgbuf, imgwidth, imgheight,
QtGui.QImage.Format_ARGB32)
qimg = QtGui.QImage.rgbSwapped(qimg)
qpix = QtGui.QPixmap(qimg)
self.setPixmap(qpix)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
w = MyApp()
w.show()
sys.exit(app.exec_())
Related Documentation :
SO posts :
matplotlib and PyQt: Dynamic figure runs slow after several loads or looks messy
PyQt4 / matplotlib - how to release memory?
Closing and deleteLater-ing PyQt tabs with Matplotlib figures on them doesn't free up memory
PyQt4 / matplotlib - how to release memory?
Is deleteLater() necessary in PyQt/PySide?
Matplotlib Documentation :
http://matplotlib.org/examples/user_interfaces/embedding_in_qt4.html
I am making a kind of imageviewer in which we can see the histogram and hsv of the loaded image. Code is running as expected except whenever I am maximizing the window, all QPushButtons get misplaced.
from PyQt4 import QtGui, QtCore
import cv2
import numpy as np
from matplotlib import pyplot as plt
class Example(QtGui.QWidget):
def __init__(self):
super(Example, self).__init__()
hbox = QtGui.QHBoxLayout(self)
top = QtGui.QFrame(self)
top.setFrameShape(QtGui.QFrame.StyledPanel)
bottomleft = QtGui.QFrame(self)
bottomleft.setFrameShape(QtGui.QFrame.StyledPanel)
bottomright = QtGui.QFrame(self)
bottomright.setFrameShape(QtGui.QFrame.StyledPanel)
splitter1 = QtGui.QSplitter(QtCore.Qt.Vertical)
splitter1.addWidget(top)
splitter2 = QtGui.QSplitter(QtCore.Qt.Horizontal)
splitter2.addWidget(bottomleft)
splitter2.addWidget(bottomright)
splitter1.addWidget(splitter2)
hbox.addWidget(splitter1)
splitter1.setSizes([190,220])
splitter2.setSizes([400,360])
self.setLayout(hbox)
QtGui.QApplication.setStyle(QtGui.QStyleFactory.create('Cleanlooks'))
self.setGeometry(600, 120, 990, 850)
self.setWindowTitle('PIMAD')
self.setWindowIcon(QtGui.QIcon('imag.jpg'))
self.show()
browseButton = QtGui.QPushButton("Browse")
browseButton.clicked.connect(self.loadFromFile)
browseButton.setToolTip('click to <b>Browse</b>')
histButton = QtGui.QPushButton("")
histButton.setToolTip('For image <b>Histogram</b> click here')
histButton.setIcon(QtGui.QIcon('download.jpg'))
histButton.setIconSize(QtCore.QSize(55,35))
histButton.clicked.connect(self.loadFromHist)
hsvButton = QtGui.QPushButton("")
hsvButton.clicked.connect(self.loadFromHsv)
hsvButton.setToolTip('For <b>Image HSV </b> click here')
hsvButton.setIcon(QtGui.QIcon('hsv.jpg'))
hsvButton.setIconSize(QtCore.QSize(50,35))
self.lbl= QtGui.QLabel()
self.lbl.setScaledContents(True)
bottomleftLayout = QtGui.QHBoxLayout()
self.lbl.setFixedSize(470, 480)
self.lbl2 = QtGui.QLabel()
self.lbl2.setScaledContents(True)
bottomrightLayout = QtGui.QHBoxLayout()
self.lbl3 = QtGui.QLabel()
self.lbl3.setScaledContents(True)
self.lbl3.setFixedSize(300,250)
self.lbl3.move(650,05)
self.lbl3.setParent(top)
self.image = "C:\New folder (2)\logo.jpeg"
self.pix = QtGui.QPixmap(self.image)
self.lbl3.setPixmap(self.pix)
self.lbl3.show()
topLayout = QtGui.QVBoxLayout()
self.fileName = "\Users\Public\Pictures\Sample Pictures\lord.jpg"
self.pixmap = QtGui.QPixmap(self.fileName)
self.lbl.setPixmap(self.pixmap)
bottomleftLayout.addWidget(self.lbl)
bottomleft.setLayout(bottomleftLayout)
bottomrightLayout.addWidget(self.lbl2)
bottomright.setLayout(bottomrightLayout)
topLayout.addWidget(self.lbl3)
topLayout.addStretch(1)
top.setLayout(topLayout)
topLayout.addStretch(1)
browseButton.setParent(top)
histButton.setParent(top)
hsvButton.setParent(top)
browseButton.move(720,260)
histButton.move(790,260)
hsvButton.move(860,260)
browseButton.resize(60,40)
histButton.resize(60,40)
hsvButton.resize(60,40)
browseButton.show()
histButton.show()
hsvButton.show()
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
app.setApplicationName('Example')
main = Example()
main.show()
app.exec_()
You are using both layouts and absolute positioning (with method move), which is a bit weird. You also don't need to use show() that much. It's should only be called on the main window.
Absolute positioning usually don't work well if you want to resize your windows. It is meant to be fixed. The widgets will stay at the same position relatively to the top left corner of the window. If the windows becomes smaller, some widgets will be hidden, and if the windows becomes larger, the new space won't be used.
Layouts are meant to be flexible. You can arrange your widgets on vertical or horizontal lines (QHBoxLayout and QVBoxLayout) or on a grid (QGridLayout). Layouts will filled all the space available: they work on any window size.
I'd suggest you re-start from scratch by following a tutorial (a good one is ZetCode Layout management in PyQt4). Try to start simple and progressively add more elements in your layout. Test at every steps to resize the window to see if it works as intended.