pyqt5: second video does not play: concurrent QMediaPlayer issue? - python

I am basically building a GUI with pyqt5 supposed to incorporate two videos. To do this, I use QMediaPlayer in combination with QVideoWidget, one for each class. The point is: while the first video plays as expected, the second one refuses to play. It uses exactly the same framework as the first one (one pushbuton for play/pause and one slidebar), and the same structure of code, but the screen remains desperately black when trying to play.
Worse, if I comment the code for the first video, the second now plays normally. Could that mean there is some conflict between the two QMedialPlayers? I can't make sense of that.
Any help would be greatly appreciated.
Here is my code (the GUI looks weird because I have removed most of it for clarity):
from PyQt5 import QtWidgets, QtGui, QtCore
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QLabel, QPushButton, QLineEdit, QFrame, QHBoxLayout, QCheckBox, QRadioButton, QButtonGroup, QStyle, QSlider, QStackedLayout
import sys
from tkinter import Tk
from PyQt5.QtCore import pyqtSlot, QRect, Qt, QRunnable, QThreadPool, QThread, QObject, QUrl, QSize
import time
from PyQt5 import QtMultimedia
from PyQt5.QtMultimedia import QMediaContent, QMediaPlayer
from PyQt5.QtMultimediaWidgets import QVideoWidget
from PyQt5.QtGui import QFont
from PyQt5.QtGui import QImage, QPalette, QBrush, QIcon, QPixmap
class DNN_Viewer(QWidget):
def __init__(self, n_filters=2):
super(DNN_Viewer, self).__init__()
# initialise GUI
self.init_gui()
# initialise videos to display images
self.mp1.play()
self.mp1.pause()
self.mp2.play()
self.mp2.pause()
def init_gui(self):
# main window
root = Tk()
screen_width = root.winfo_screenwidth() # screen width
screen_height = root.winfo_screenheight() # screen heigth
self.width = 1900 # interface width
self.heigth = 1000 # interface height
self.left = (screen_width - self.width) / 2 # left-center interface
self.top = (screen_height - self.heigth) / 2 # top-center interface
self.setFixedSize(self.width, self.heigth)
self.move(self.left, self.top)
self.setStyleSheet("background: white"); # interface background color
# bottom left frame
self.fm2 = QFrame(self) # creation
self.fm2.setGeometry(30, 550, 850, 430) # left, top, width, height
self.fm2.setFrameShape(QFrame.Panel); # use panel style for frame
self.fm2.setLineWidth(1) # frame line width
# video for weights and gradients
self.vw1 = QVideoWidget(self) # declare video widget
self.vw1.move(50,555) # left, top
self.vw1.resize(542,380) # width, height
self.vw1.setStyleSheet("background-color:black;"); # set black background
# wrapper for the video
self.mp1 = QMediaPlayer(self) # declare QMediaPlayer
self.mp1.setVideoOutput(self.vw1) # use video widget vw1 as output
fileName = "path_to_video_1" # local path to video
self.mp1.setMedia(QMediaContent(QUrl.fromLocalFile(fileName))) # path to video
self.mp1.stateChanged.connect(self.cb_mp1_1) # callback on change state (play, pause, stop)
self.mp1.positionChanged.connect(self.cb_mp1_2) # callback to move slider cursor
self.mp1.durationChanged.connect(self.cb_mp1_3) # callback to update slider range
# play button for video
self.pb2 = QPushButton(self) # creation
self.pb2.move(50,940) # left, top
self.pb2.resize(40,30) # width, height
self.pb2.setIconSize(QSize(18,18)) # button text
self.pb2.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) # standard triangle icon for play
self.pb2.clicked.connect(self.cb_pb2) # callback on click (play/pause)
# position slider for video
self.sld1 = QSlider(Qt.Horizontal,self) # creation
self.sld1.setGeometry( 110, 940, 482, 30) # left, top, width, height
self.sld1.sliderMoved.connect(self.cb_sld1) # callback on move
# title label
self.lb23 = QLabel(self) # creation
self.lb23.setText("Loss and accuracy") # label text
self.lb23.move(980,10) # left, top
self.lb23.setStyleSheet("font-size: 30px; font-family: \
FreeSans; font-weight: bold") # set font and size
# top right frame
self.fm3 = QFrame(self) # creation
self.fm3.setGeometry(980, 50, 850, 430) # left, top, width, height
self.fm3.setFrameShape(QFrame.Panel); # use panel style for frame
self.fm3.setLineWidth(1) # frame line width
# video for loss and accuracy
self.vw2 = QVideoWidget(self) # declare video widget
self.vw2.move(1000,55) # left, top
self.vw2.resize(542,380) # width, height
self.vw2.setStyleSheet("background-color:black;"); # set black background
# wrapper for the video
self.mp2 = QMediaPlayer(self) # declare QMediaPlayer
self.mp2.setVideoOutput(self.vw2) # use video widget vw1 as output
fileName2 = "path_to_video_2" # local path to video
self.mp2.setMedia(QMediaContent(QUrl.fromLocalFile(fileName2))) # path to video
self.mp2.stateChanged.connect(self.cb_mp2_1) # callback on change state (play, pause, stop)
self.mp2.positionChanged.connect(self.cb_mp2_2) # callback to move slider cursor
self.mp2.durationChanged.connect(self.cb_mp2_3) # callback to update slider range
# play button for video
self.pb3 = QPushButton(self) # creation
self.pb3.move(1000,440) # left, top
self.pb3.resize(40,30) # width, height
self.pb3.setIconSize(QSize(18,18)) # button text
self.pb3.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) # standard triangle icon for play
self.pb3.clicked.connect(self.cb_pb3) # callback on click (play/pause)
# position slider for video
self.sld2 = QSlider(Qt.Horizontal,self) # creation
self.sld2.setGeometry(1060, 440, 482, 30) # left, top, width, height
self.sld2.sliderMoved.connect(self.cb_sld2) # callback on move
def cb_mp1_1(self, state):
if self.mp1.state() == QMediaPlayer.PlayingState: # if playing, switch button icon to pause
self.pb2.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
elif self.mp1.state() == QMediaPlayer.StoppedState: # if stopped, rewind to first image
self.mp1.play()
self.mp1.pause()
else:
self.pb2.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) # if paused, switch button icon to play
def cb_mp1_2(self, position):
self.sld1.setValue(position) # set slider position to video position
def cb_mp1_3(self, duration):
self.sld1.setRange(0, duration) # set slider range to video position
def cb_pb2(self):
if self.mp1.state() == QMediaPlayer.PlayingState: # set to pause if playing
self.mp1.pause()
else:
self.mp1.play() # set to play if in pause
def cb_sld1(self, position):
self.mp1.setPosition(position) # set video position to slider position
def cb_mp2_1(self, state):
if self.mp2.state() == QMediaPlayer.PlayingState: # if playing, switch button icon to pause
self.pb3.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
elif self.mp2.state() == QMediaPlayer.StoppedState: # if stopped, rewind to first image
self.mp2.play()
self.mp2.pause()
else:
self.pb3.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) # if paused, switch button icon to play
def cb_mp2_2(self, position):
self.sld2.setValue(position) # set slider position to video position
def cb_mp2_3(self, duration):
self.sld2.setRange(0, duration) # set slider range to video position
def cb_pb3(self):
if self.mp2.state() == QMediaPlayer.PlayingState: # set to pause if playing
self.mp2.pause()
else:
self.mp2.play() # set to play if in pause
def cb_sld2(self, position):
self.mp2.setPosition(position) # set video position to slider position
# run GUI
def dnn_viewer():
app = QApplication(sys.argv) # initiate app; sys.argv argument is only for OS-specific settings
viewer = DNN_Viewer() # create instance of Fil_Rouge_Dashboard class
viewer.show() # display dashboard
sys.exit(app.exec_()) # allow exit of the figure by clicking on the top right cross
# call window function
dnn_viewer()

tl:dr;
Use layout managers.
Explanation
Well, it seems you accidentally found a (possible) bug by doing something really wrong.
QVideoWidget is a widget that is more complex than it seems, since it interfaces itself with the underlying graphics system of the OS, and, in order to correctly show its contents (the video), it has to be actively notified of its geometry.
Simply speaking, QVideoWidget does not directly show the "pictures" of the video QMediaPlayer shows, but tells the Operating System to do so (well, not exactly, but we won't discuss it here). This is because video displaying might take advantage of some hardware acceleration, or require some processing (for example, for HDR videos), similarly to what 3D/OpenGL graphics does.
When a program is going to display some (system managed) video, it has to tell the OS about the available geometry for that video, so that the OS is able to show it at the correct coordinates, and possibly apply resizing, some form of "clipping" (if another window is overlayed, for example) or any other level of [post]processing.
The "something really wrong" I was talking about before is based on the fact that you are using fixed geometries (sizes and positions) for both video widgets, and I think that Qt is not able to notify the system about those geometries for more than a widget at once if that happens before the video window is actually mapped (as in "shown").
Why is it really wrong, besides the issue at hand?
Each one of our devices is mostly unique: what you see on your device will be shown in a (possibly radically) different way on other's.
There are many reasons for that, including:
Operating System (and versions) and its behavior;
screen size and DPI (for example, I wasn't able to view the complete window of your code, since I've a smaller screen);
default/customized system font size; most importantly, if the default font is very big, the widgets might overlap;
further customization (for example, default margins and spacing);
if the interface is "adaptive", the user should be able to resize the interface:
if the user has a smaller screen, the ui should be resizable, so that everything is visible instead of having the need to move the window beyond the screen margins (something that is sometimes impossible: for example on Windows you can't move a window above the top margin of the screen);
if the user has a bigger screen (or uses a very high DPI setting), the interface would be too small and some elements might be hard to read or interact with;
That's the reason for which almost any nowadays website uses "responsive" layouts, which adapt the contents according to the screen of the device they're going to be displayed into.
The solution is very simple, and will also solve the big issue about your GUI: avoid any fixed geometry for your GUI and use layout managers instead.
Note that you can still use fixed sizes (not positions, sizes!): that's not that big of an issue, but using layout managers will help you a lot with it, by repositioning all elements according to the available space.
The reason for it is that layout managers ensure that any resizing operation (something that also happens many times as soon as a window is shown the first time) is also notified to the system, whenever it's required (like, for instance, adapting the QVideoWidget output).
If you want to keep the "bottom-right/top-left" layout, you can still do that:
set a main QGridLayout for the widget (DNN_Viewer), create another grid layout for each player and add those layout to the main one.
The structure will be something like this:
+------------------------- DNN_Viewer -------------------------+
| | +------ player2Layout ------+ |
| | | | |
| | | vw2 | |
| | | | |
| | +-------+-------------------+ |
| | | pb2 | sld1 | |
| | +-------+-------------------+ |
+------------------------------+-------------------------------+
| +------ player1Layout------+ | |
| | | | |
| | vw1 | | |
| | | | |
| +-------+------------------+ | |
| | pb1 | sld2 | | |
| +-------+------------------+ | |
+------------------------------+-------------------------------+
class DNN_Viewer(QWidget):
# ...
def init_gui(self):
# create a grid layout for the widget and automatically set it for it
layout = QtWidgets.QGridLayout(self)
player1Layout = QtWidgets.QGridLayout()
# add the layout to the second row, at the first column
layout.addLayout(player1Layout, 1, 0)
# video for weights and gradients
self.vw1 = QVideoWidget(self)
# add the video widget at the first row and column, but set its column
# span to 2: we'll need to add two widgets in the second row, the play
# button and the slider
player1Layout.addWidget(self.vw1, 0, 0, 1, 2)
# ...
self.pb2 = QPushButton(self)
# add the button to the layout; if you don't specify rows and columns it
# normally means that the widget is added to a new grid row
player1Layout.addWidget(self.pb2)
# ...
self.sld1 = QSlider(Qt.Horizontal,self)
# add the slider to the second row, besides the button
player1Layout.addWidget(self.sld1, 1, 1)
# ...
player2Layout = QtWidgets.QGridLayout()
# add the second player layout to the first row, second column
layout.addLayout(player2Layout, 0, 1)
self.vw2 = QVideoWidget(self)
# same column span as before
player2Layout.addWidget(self.vw2, 0, 0, 1, 2)
# ...
self.pb3 = QPushButton(self)
player2Layout.addWidget(self.pb3, 1, 0)
# ...
self.sld2 = QSlider(Qt.Horizontal,self)
player2Layout.addWidget(self.sld2, 1, 1)
This will solve your main issue (and lots of others you didn't consider).
Some further suggestions:
use more descriptive variable names; things like pb2 or lb23 seem easier to use and you might be led to think that short variables equals less time spent typing. Actually, there's no final benefit in that: while it may be true that shorter variable names might improve compiling speed (especially for interpreted languages like Python), at the end there's almost no advantage; on the contrary, you'll have to remember what "sld2" means, while something like "player2Slider" is way more descriptive and easier to read (which means you'll read and debug faster, and people reading your code will understand it and help you much more easily)
for the same reason above, use more descriptive function names: names like cb_mp1_3 mean literally nothing; naming is really important, and the startup speed improvement reported above is almost dismissible with todays computers; it also helps you to get help from others: it took more time to understand what was your actual issue, than to understand what your code does, since all those names were almost meaningless to me; read more on the official Style Guide for Python Code (aka, PEP 8);
use comments wisely:
avoid over-commenting, it makes comments distracting while losing much of their purpose (that said, while "Let the code be the documentation" is a good concept, don't overextimate it)
avoid "fancy" formatted comments: they might seem cool, but at the end they are just annoying to deal with; if you want to comment a function to better describe what it does, use the triple quotes feature Python already provides; also consider that many code sharing services have column limits (and StackOverflow is amongst them): people would need to scroll each line to read the corresponding comment;
if you need a description for a single line function, it's possible that the function is not descriptive as it could or should be, as explained above;
be more consistent with blank lines separations between functions or classes: Python was created with readability in mind, and it's a good thing to follow that principle;
don't overwrite existing attribute names: self.width() and self.height() are base properties of all QWidgets, and you might need to access them often;
be more consistent with the imports you're using, especially with complex modules like Qt: you should either import the submodules (from PyQt5 import QtWidgets, ...) or the single classes (from PyQt5.QtWidgets import QApplication, ...); note that, while the latter could be considered more "pythonic", it's usually tricky with Qt, since it has hundreds of classes (and you might need tens of them in each script), then you always have to remember to add every class each time you need it, and you might end up importing unnecessary classes you're not using anymore; there's not much performance improvement using this approach, at least with Qt, especially if you forget to remove unnecessary imports (in your case, the possible benefit of importing single classes is completely canceled by the fact that there are at least 10 imported classes that are never actually used);
avoid unnecessary imports from other frameworks if they are not absolutely necessary: if you need to know the screen geometry, use QApplication.screens(), don't import Tk just for that;

So, I started again trying to solve the issue with musicamant's very exhaustive answer. It indeed solved the issue, but I was not happy with having a solution that would work only with adaptative GUIs. So I investigated again the issue, starting with a minimal GUI where only the two videos would be present. And, to my greatest amazement, the two videos played fine, even with a fixed size GUI.
So I started inflating the GUI again, adding all the elements till I recovered my initial GUI. And at some point, I experienced the bug again, which made it possible to identify the actual cause.
So the culprit is called... QFrame. Yes, for real. The Qframe caused all that mess. At first I was using a QFrame with setFrameShape(QFrame.Panel), so that a rectangular frame is created at once. Then I installed the video widget inside the frame. It turns out that with certain videos, the QFrame adopts a strange behaviours and kind of "covers" the video output, making the video viewer screen vanish. The sound remains unaffected. It only happens for certain videos and not for others, which does not make any real sense. Still, removing the frame instantaneously solves the issue, so that really is a bug.
It seems that with musicamante's solution, the frame does not adopt this strange behaviour, hence a working solution. Another possible solution with fixed size GUIs is to use frames that don't cover the video. Concretely, rather than using a single QFrame with setFrameShape(QFrame.Panel) which creates a rectangle in one frame, a set of four frames must be used, two of them being QFrame with setFrameShape(QFrame.Hline), and the other two being QFrame with setFrameShape(QFrame.Vline), organised to form a rectangle. I tested it and it works. The frames only cover the horizontal/vertical surfaces they go through, and so the "inside" of the rectangle is not part of any frame, which avoids the bug.

Related

How to add an invisible line in PyQt5

This attached image is the screenshot of an application developed using PyQt5.
The image clearly has an invisible line running in the middle of the boxes enclosing the contents.
What code should I add in my program to draw an invisible line overlaying all other objects created earlier. I couldn't find any documentation regarding this but as the image suggests, it has somehow been implemented.
A code snippet is not needed to be provided by me since this is a question about adding/developing a feature rather than debugging or changing any existing code.
Premise: what you provided as an example doesn't seem a very good thing to do. It also seems more a glich than a "feature", and adding "invisible" lines like that might result in an annoying GUI for the user. The only scenario in which I'd use it would be a purely graphical/fancy one, for which you actually want to create a "glitch" for some reason. Also, note that the following solutions are not easy, and their usage requires you an advanced skill level and experience with Qt, because if you don't really understand what's happening, you'll most certainly encounter bugs or unexpected results that will be very difficult to fix.
Now. You can't actually "paint an invisible line", but there are certain work arounds that can get you a similar result, depending on the situation.
The main problem is that painting (at least on Qt) happens from the "bottom" of each widget, and each child widget is painted over the previous painting process, in reverse stacking order: if you have widgets that overlap, the topmost one will paint over the other. This is more clear if you have a container widget (such as a QFrame or a QGroupBox) with a background color and its children use another one: the background of the children will be painted over the parent's.
The (theoretically) most simple solution is to have a child widget that is not added to the main widget layout manager.
Two important notes:
The following will only work if applied to the topmost widget on which the "invisible line" must be applied.
If the widget on which you apply this is not the top level window, the line will probably not be really invisible.
class TestWithChildLine(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
for row in range(3):
for col in range(6):
layout.addWidget(QtWidgets.QDial(), row, col)
# create a widget child of this one, but *do not add* it to the layout
self.invisibleWidget = QtWidgets.QWidget(self)
# ensure that the widget background is painted
self.invisibleWidget.setAutoFillBackground(True)
# and that it doesn't receive mouse events
self.invisibleWidget.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
def resizeEvent(self, event):
super().resizeEvent(event)
# create a rectangle that will be used for the "invisible" line, wide
# as the main widget but with 10 pixel height, then center it
rect = QtCore.QRect(0, 0, self.width(), 10)
rect.moveCenter(self.rect().center())
# set the geometry of the "invisible" widget to that rectangle
self.invisibleWidget.setGeometry(rect)
Unfortunately, this approach has a big issue: if the background color has an alpha component or uses a pixmap (like many styles do, and you have NO control nor access to it), the result will not be an invisible line.
Here is a screenshot taken using the "Oxygen" style (I set a 20 pixel spacing for the layout); as you can see, the Oxygen style draws a custom gradient for window backgrounds, which will result in a "not invisible line":
The only easy workaround for that is to set the background using stylesheets (changing the palette is not enough, as the style will still use its own way of painting using a gradient derived from the QPalette.Window role):
self.invisibleWidget = QtWidgets.QWidget(self)
self.invisibleWidget.setObjectName('InvisibleLine')
self.invisibleWidget.setAutoFillBackground(True)
self.invisibleWidget.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
self.setStyleSheet('''
TestWithChildFull, #InvisibleLine {
background: lightGray;
}
''')
The selectors are required to avoid stylesheet propagation to child widgets; I used the '#' selector to identify the object name of the "invisible" widget.
As you can see, now we've lost the gradient, but the result works as expected:
Now. There's another, more complicated solution, but that should work with any situation, assuming that you're still using it on a top level window.
This approach still uses the child widget technique, but uses QWidget.render() to paint the current background of the top level window on a QPixmap, and then set that pixmap to the child widget (which now is a QLabel).
The trick is to use the DrawWindowBackground render flag, which allows us to paint the widget without any children. Note that in this case I used a black background, which shows a "lighter" gradient on the borders that better demonstrate the effect:
class TestWithChildLabel(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
layout.setSpacing(40)
for row in range(3):
for col in range(6):
layout.addWidget(QtWidgets.QDial(), row, col)
self.invisibleWidget = QtWidgets.QLabel(self)
self.invisibleWidget.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
palette = self.palette()
palette.setColor(palette.Window, QtGui.QColor('black'))
self.setPalette(palette)
def resizeEvent(self, event):
super().resizeEvent(event)
pm = QtGui.QPixmap(self.size())
pm.fill(QtCore.Qt.transparent)
qp = QtGui.QPainter(pm)
maskRect = QtCore.QRect(0, 0, self.width(), 50)
maskRect.moveTop(50)
region = QtGui.QRegion(maskRect)
self.render(qp, maskRect.topLeft(), flags=self.DrawWindowBackground,
sourceRegion=region)
qp.end()
self.invisibleWidget.setPixmap(pm)
self.invisibleWidget.setGeometry(self.rect())
And here is the result:
Finally, an further alternative would be to manually apply a mask to each child widget, according to their position. But that could become really difficult (and possibly hard to manage/debug) if you have complex layouts or a high child count, since you'd need to set (or unset) the mask for all direct children each time a resize event occurs. I won't demonstrate this scenario, as I believe it's too complex and unnecessary.

Pyqt5 image coordinates

I display images with Qlabel.I need image coordinates/pixel coordinates but, I use mouseclickevent its show me only Qlabel coordinates.
for examples my image is 800*753 and my Qlabel geometry is (701,451).I reads coordinates in (701,451) but I need image coordinates in (800*753)
def resimac(self):
filename= QtWidgets.QFileDialog.getOpenFileName(None, 'Resim Yükle', '.', 'Image Files (*.png *.jpg *.jpeg *.bmp *.tif)')
self.image=QtGui.QImage(filename[0])
self.pixmap=QtGui.QPixmap.fromImage(self.image)
self.resim1.setPixmap(self.pixmap)
self.resim1.mousePressEvent=self.getPixel
def getPixel(self, event):
x = event.pos().x()
y = event.pos().y()
print("X=",x," y= ",y)
Since you didn't provide a minimal, reproducible example, I'm going to assume that you're probably setting the scaledContents property, but that could also be not true (in case you set a maximum or fixed size for the label).
There are some other serious issues about your answer, I'll address them at the end of this answer.
The point has to be mapped to the pixmap coordinates
When setting a pixmap to a QLabel, Qt automatically resizes the label to its contents.
Well, it does it unless the label has some size constrains: a maximum/fixed size that is smaller than the pixmap, and/or the QLabel has the scaledContents property set to True as written above. Note that this also happens if any of its ancestors has some size constraints (for example, the main window has a maximum size, or it's maximized to a screen smaller than the space the window needs).
In any of those cases, the mousePressEvent will obviously give you the coordinates based on the widget, not on the pixmap.
First of all, even if it doesn't seem to be that important, you'll have to consider that every widget can have some contents margins: the widget will still receive events that happen inside the area of those margins, even if they are outside its actual contents, so you'll have to consider that aspect, and ensure that the event happens within the real geometry of the widget contents (in this case, the pixmap). If that's true, you'll have to translate the event position to that rectangle to get its position according to the pixmap.
Then, if the scaledContents property is true, the image will be scaled to the current available size of the label (which also means that its aspect ratio will not be maintained), so you'll need to scale the position.
This is just a matter of math: compute the proportion between the image size and the (contents of the) label, then multiply the value using that proportion.
# click on the horizontal center of the widget
mouseX = 100
pixmapWidth = 400
widgetWidth = 200
xRatio = pixmapWidth / widgetWidth
# xRatio = 2.0
pixmapX = mouseX * xRatio
# the resulting "x" is the horizontal center of the pixmap
# pixmapX = 200
On the other hand, if the contents are not scaled you'll have to consider the QLabel alignment property; it is usually aligned on the left and vertically centered, but that depends on the OS, the style currently in use and the localization (consider right-to-left writing languages). This means that if the image is smaller than the available size, there will be some empty space within its margins, and you'll have to be aware of that.
In the following example I'm trying to take care about all of that (I'd have to be honest, I'm not 100% sure, as there might be some 1-pixel tolerance due to various reasons, most regarding integer-based coordinates and DPI awareness).
Note that instead of overwriting mousePressEvent as you did, I'm using an event filter, I'll explain the reason for it afterwards.
from PyQt5 import QtCore, QtGui, QtWidgets
class Window(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QGridLayout(self)
self.getImageButton = QtWidgets.QPushButton('Select')
layout.addWidget(self.getImageButton)
self.getImageButton.clicked.connect(self.resimac)
self.resim1 = QtWidgets.QLabel()
layout.addWidget(self.resim1)
self.resim1.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter)
# I'm assuming the following...
self.resim1.setScaledContents(True)
self.resim1.setFixedSize(701,451)
# install an event filter to "capture" mouse events (amongst others)
self.resim1.installEventFilter(self)
def resimac(self):
filename, filter = QtWidgets.QFileDialog.getOpenFileName(None, 'Resim Yükle', '.', 'Image Files (*.png *.jpg *.jpeg *.bmp *.tif)')
if not filename:
return
self.resim1.setPixmap(QtGui.QPixmap(filename))
def eventFilter(self, source, event):
# if the source is our QLabel, it has a valid pixmap, and the event is
# a left click, proceed in trying to get the event position
if (source == self.resim1 and source.pixmap() and not source.pixmap().isNull() and
event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton):
self.getClickedPosition(event.pos())
return super().eventFilter(source, event)
def getClickedPosition(self, pos):
# consider the widget contents margins
contentsRect = QtCore.QRectF(self.resim1.contentsRect())
if pos not in contentsRect:
# outside widget margins, ignore!
return
# adjust the position to the contents margins
pos -= contentsRect.topLeft()
pixmapRect = self.resim1.pixmap().rect()
if self.resim1.hasScaledContents():
x = pos.x() * pixmapRect.width() / contentsRect.width()
y = pos.y() * pixmapRect.height() / contentsRect.height()
pos = QtCore.QPoint(x, y)
else:
align = self.resim1.alignment()
# for historical reasons, QRect (which is based on integer values),
# returns right() as (left+width-1) and bottom as (top+height-1),
# and so their opposite functions set/moveRight and set/moveBottom
# take that into consideration; using a QRectF can prevent that; see:
# https://doc.qt.io/qt-5/qrect.html#right
# https://doc.qt.io/qt-5/qrect.html#bottom
pixmapRect = QtCore.QRectF(pixmapRect)
# the pixmap is not left aligned, align it correctly
if align & QtCore.Qt.AlignRight:
pixmapRect.moveRight(contentsRect.x() + contentsRect.width())
elif align & QtCore.Qt.AlignHCenter:
pixmapRect.moveLeft(contentsRect.center().x() - pixmapRect.width() / 2)
# the pixmap is not top aligned (note that the default for QLabel is
# Qt.AlignVCenter, the vertical center)
if align & QtCore.Qt.AlignBottom:
pixmapRect.moveBottom(contentsRect.y() + contentsRect.height())
elif align & QtCore.Qt.AlignVCenter:
pixmapRect.moveTop(contentsRect.center().y() - pixmapRect.height() / 2)
if not pos in pixmapRect:
# outside image margins, ignore!
return
# translate coordinates to the image position and convert it back to
# a QPoint, which is integer based
pos = (pos - pixmapRect.topLeft()).toPoint()
print('X={}, Y={}'.format(pos.x(), pos.y()))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())
Now. A couple of suggestions.
Don't overwrite existing child object methods with [other] object's instance attributes
There are various reasons for which this is not a good idea, and, while dealing with Qt, the most important of them is that Qt uses function caching for virtual functions; this means that as soon as a virtual is called the first time, that function will always be called in the future. While your approach could work in simple cases (especially if the overwriting happens within the parent's __init__), it's usually prone to unexpected behavior that's difficult to debug if you're not very careful.
And that's exactly your case: I suppose that resimac is not called upon parent instantiation and until after some other event (possibly a clicked button) happens. But if the user, for some reason, clicks on the label before a new pixmap is loaded, your supposedly overwritten method will never get called: at that time, you've not overwritten it yet, so the user clicks the label, Qt calls the QLabel's base class mousePressEvent implementation, and then that method will always be called from that point on, no matter if you try to overwrite it.
To work around that, you have at least 3 options:
use an event filter (as the example above); an event filter is something that "captures" events of a widgets and allows you to observe (and interact) with it; you can also decide to propagate that event to the widget's parent or not (that's mostly the case of key/mouse events: if a widget isn't "interested" about one of those events, it "tells" its parent to care about it); this is the simplest method, but it can become hard to implement and debug for complex cases;
subclass the widget and manually add it to your GUI within your code;
subclass it and "promote" the widget if you're using Qt's Designer;
You don't need to use a QImage for a QLabel.
This is not that an issue, it's just a suggestion: QPixmap already uses (sort of) fromImage within its C++ code when constructing it with a path as an argument, so there's no need for that.
Always, always provide usable, Minimal Reproducible Example code.
See:
https://stackoverflow.com/help/how-to-ask
https://stackoverflow.com/help/minimal-reproducible-example
It could take time, even hours to get an "MRE", but it's worth it: there'll always somebody that could answer you, but doesn't want to or couldn't dig into your code for various reasons (mostly because it's incomplete, vague, inusable, lacking context, or even too expanded). If, for any reason, there'll be just that one user, you'll be losing your occasion to solve your problem. Be patient, carefully prepare your questions, and you'll probably get plenty of interactions and useful insight from it.

Move pyqt button out of list of buttons

I have a list of pyqt4 push button and want to move the position. Since it is troublesome it make lots of buttons variable I create them through a list. The code below
import sys
from PyQt5.QtWidgets import QApplication, QPushButton, QWidget, QVBoxLayout
class Window(QWidget):
def __init__(self):
QWidget.__init__(self)
layout = QVBoxLayout(self)
self.buttons = []
for i in range(3):
self.buttons.append(QPushButton('',self))
self.buttons[-1].setFixedWidth(50)
self.buttons[-1].setFixedHeight(50)
self.buttons[-1].move(70*i+50,300)
layout.addWidget(self.buttons[-1])
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = Window()
window.resize(500,500)
window.show()
sys.exit(app.exec_())
don't work for specify the position
but
class Window(QWidget):
def __init__(self):
QWidget.__init__(self)
layout = QVBoxLayout(self)
self.button1 = QPushButton('',self)
self.button1.setFixedWidth(50)
self.button1.setFixedHeight(50)
self.button1.move(50,300)
self.button2 = QPushButton('',self)
self.button2.setFixedWidth(50)
self.button2.setFixedHeight(50)
self.button2.move(120,300)
self.button3 = QPushButton('',self)
self.button3.setFixedWidth(50)
self.button3.setFixedHeight(50)
self.button3.move(190,300)
layout = QVBoxLayout(self)
layout.addWidget(self.button1)
layout.addWidget(self.button2)
layout.addWidget(self.button3)
works fine.
What's the reason behind?
If you want to manually specify the geometry (position and size) of widgets, you should not add them to a layout.
Your second example "works" just because you already created and set a layout to the top-level widget (the Window class), and since a layout already exists the second one is not "installed". Running it from the console will shows this error:
StdErr: QLayout: Attempting to add QLayout "" to Window "", which already has a layout
When a widget is added to a layout, the ownership of the "layout item" (an abstract item used by layouts to manage its widgets) is transferred to the layout, and the widget is reparented to the widget that uses that layout.
Since the second layout cannot be set, it will not manage the geometries of the items you tried to add, and the result is that they will keep the geometries you set before.
The same result can be obtained if you remove all the last lines referencing the other layout, which is exactly what you need.
Also, note that in order to add a widget that is not managed by a layout, a parent is required. Your last example also works because you specified the window as the parent while instantiating them; if you don't do that the buttons will not be shown, but if you do show them (with show() or setVisible(True)) they will each appear in separate windows, as they will become their own top level windows.
If you don't have other widgets that should be managed by a layout, you can also avoid creating the first layout at all (but, still, the parent is required).
Finally, let me tell you that using manual geometries is generally discouraged, and there are very few and specific cases for which it's a good idea to go with.
The main reason behind this is that widgets tend to show very differently from device to device, and that depends on various aspects:
different OS and OS versions draw widgets differently (sometimes dramatically), and this involves varying the widget size and behavior; while this might not be a major issue for simple widgets, it can be a problem for things like item views, spinboxes, etc;
different OS and systems use different styles, which also includes things like internal widget content margins and minimum size;
specific system settings (most importantly, the default font) could make widgets mostly unreadable, specifically with text being clipped within the widget margins if the font size is too big;
depending on the OS, you might face issues if the system uses High DPI displays, and you could end up with very tiny widgets that are almost impossible to interact with;
fixed geometries force you (and the user) to have a fixed widget/window size, and (along with the DPI issue above) this can be a problem: the same widget could look too big or too small;

PyQt4 - QWidget save as image

I'm trying to use Python to programmatically save a QWidget in PyQt4 as an image (any format would be fine - PNG, PDF, JPEF, GIF...etc)
I thought this would be very straightforward, but I actually wasn't able to find anything on the web about it. Can someone point me in the right direction?
To be clear, I'm trying to do this
gui = <SOME QMainWindow>
gui.show() # this displays the gui. it's useful, but what i need is to save the image
gui.save("image.png") ## How do I do this?
You can do this using the QPixmap.grabWindow() method.
From the docs:
Grabs the contents of the window window and makes a pixmap out of it.
Returns the pixmap.
The arguments (x, y) specify the offset in the window, whereas (w, h)
specify the width and height of the area to be copied.
If w is negative, the function copies everything to the right border
of the window. If h is negative, the function copies everything to the
bottom of the window.
Note that grabWindow() grabs pixels from the screen, not from the
window. If there is another window partially or entirely over the one
you grab, you get pixels from the overlying window, too.
Note also that the mouse cursor is generally not grabbed.
The reason we use a window identifier and not a QWidget is to enable
grabbing of windows that are not part of the application, window
system frames, and so on.
Warning: Grabbing an area outside the screen is not safe in general.
This depends on the underlying window system.
Warning: X11 only: If window is not the same depth as the root window
and another window partially or entirely obscures the one you grab,
you will not get pixels from the overlying window. The contests of the
obscured areas in the pixmap are undefined and uninitialized.
Sample code:
import sys
from PyQt4.QtGui import *
filename = 'Screenshot.jpg'
app = QApplication(sys.argv)
widget = QWidget()
widget.setLayout(QVBoxLayout())
label = QLabel()
widget.layout().addWidget(label)
def take_screenshot():
p = QPixmap.grabWindow(widget.winId())
p.save(filename, 'jpg')
widget.layout().addWidget(QPushButton('take screenshot', clicked=take_screenshot))
widget.show()
app.exec_()
This will produce a window that looks like this:
When you click the button, it will create a file named Screenshot.jpg in the current directory. Said image will look like this (notice the window frame is missing):

QApplication' is not defined

I downloaded the script below from http://www.pythoncentral.io/pyside-pyqt-tutorial-interactive-widgets-and-layout-containers/
I get the following error message: NameError: name 'QApplication' is not defined
I added the first two lines of the script.
That did not help.
I thought maybe I must not have qt installed. But when I tried to run PyQt4-4.10.3-gpl-Py2.7-Qt4.8.5-x32.exe, the program told me it was already installed.
Does anyone have any suggestions?
marc
# copied from http://www.pythoncentral.io/pyside-pyqt-tutorial-interactive-widgets-and-layout-containers/
# Every Qt application must have one and only one QApplication object;
# it receives the command line arguments passed to the script, as they
# can be used to customize the application's appearance and behavior
import sys
from PyQt4 import QtGui, QtCore
#import PyQt4.QtGui, PyQt4.QtCore
qt_app = QApplication(sys.argv)
class AbsolutePositioningExample(QWidget):
''' An example of PySide absolute positioning; the main window
inherits from QWidget, a convenient widget for an empty window. '''
def __init__(self):
# Initialize the object as a QWidget
QWidget.__init__(self)
# We have to set the size of the main window
# ourselves, since we control the entire layout
self.setMinimumSize(400, 185)
self.setWindowTitle('Dynamic Greeter')
# Create the controls with this object as their parent and set
# their position individually; each row is a label followed by
# another control
# Label for the salutation chooser
self.salutation_lbl = QLabel('Salutation:', self)
self.salutation_lbl.move(5, 5) # offset the first control 5px
# from top and left
self.salutations = ['Ahoy',
'Good day',
'Hello',
'Heyo',
'Hi',
'Salutations',
'Wassup',
'Yo']
# Create and fill the combo box to choose the salutation
self.salutation = QComboBox(self)
self.salutation.addItems(self.salutations)
# Allow 100px for the label and 5px each for borders at the
# far left, between the label and the combobox, and at the far
# right
self.salutation.setMinimumWidth(285)
# Place it five pixels to the right of the end of the label
self.salutation.move(110, 5)
# The label for the recipient control
self.recipient_lbl = QLabel('Recipient:', self)
# 5 pixel indent, 25 pixels lower than last pair of widgets
self.recipient_lbl.move(5, 30)
# The recipient control is an entry textbox
self.recipient = QLineEdit(self)
# Add some ghost text to indicate what sort of thing to enter
self.recipient.setPlaceholderText(""e.g. 'world' or 'Matey'"")
# Same width as the salutation
self.recipient.setMinimumWidth(285)
# Same indent as salutation but 25 pixels lower
self.recipient.move(110, 30)
# The label for the greeting widget
self.greeting_lbl = QLabel('Greeting:', self)
# Same indent as the others, but 45 pixels lower so it has
# physical separation, indicating difference of function
self.greeting_lbl.move(5, 75)
# The greeting widget is also a label
self.greeting = QLabel('', self)
# Same indent as the other controls
self.greeting.move(110, 75)
# The build button is a push button
self.build_button = QPushButton('&Build Greeting', self)
# Place it at the bottom right, narrower than
# the other interactive widgets
self.build_button.setMinimumWidth(145)
self.build_button.move(250, 150)
def run(self):
# Show the form
self.show()
# Run the Qt application
qt_app.exec_()
# Create an instance of the application window and run it
app = AbsolutePositioningExample()
app.run()
If you read through the tutorial in order, you'd see that the previous article in the series showed the stuff you need to part at the start of each fragment to make it a runnable program. The author apparently did this so that the same code could be used with both PyQt and PySide.
So, if you're using PyQt4, you'll need to add this:
# Allow access to command-line arguments
import sys
# SIP allows us to select the API we wish to use
import sip
# use the more modern PyQt API (not enabled by default in Python 2.x);
# must precede importing any module that provides the API specified
sip.setapi('QDate', 2)
sip.setapi('QDateTime', 2)
sip.setapi('QString', 2)
sip.setapi('QTextStream', 2)
sip.setapi('QTime', 2)
sip.setapi('QUrl', 2)
sip.setapi('QVariant', 2)
# Import all of Qt
from PyQt4.Qt import *
If PySide:
# Allow access to command-line arguments
import sys
# Import the core and GUI elements of Qt
from PySide.QtCore import *
from PySide.QtGui import *
Below the box showing your this boilerplate, there's a nice, readable explanation of what it all means and why you need to do it.
However, I'd suggest that if you're trying to learn from a tutorial, you start at the start and work forward, instead of starting in the middle and trying to figure out what you missed along the way.
If you just do from PyQt4 import QtGui, QtCore instead of importing * from them, the names in those modules are available, but only as qualified names. That is, instead of QApplication, you have to write QtCore.QApplication.
If you don't understand the difference, read Modules in the official Python tutorial, or something equivalent, to learn how imports work.
I guess that import is wrong, it should be:
from PyQt4.QtCore import *
from PyQt4.QtGui import *
For anyone experiencing this issue in PyQt5, try using QCoreApplication instead and it should work.

Categories