Below is a minimal example that reproduces this problem.
from PyQt5 import QtWidgets
import pandas as pd
class PandasGUI(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# This ensures there is always a reference to this widget and it doesn't get garbage collected
self._ref = self
inner = pd.DataFrame([3, 4, 5])
# Printing the DataFrame makes some windows close/disappear leaving only 3 QMainWindow windows
# Commenting out this print statement causes 5 windows to be shown as expected
print(inner)
self.show()
# Should create 5 PandasGUI windows
app = QtWidgets.QApplication([])
for i in range(5):
outer = pd.DataFrame([1, 2, 3])
PandasGUI()
app.exec_()
The problem I have is when I run this code I get 3 or 4 windows shown instead of 5, and I cannot figure out why.
Observations
If I remove the self._ref = self line and instead store the widgets in a persistent list object I get 5 windows
If I don't create the outer DataFrame I get 5 windows
If I inherit a QWidget instead of a QMainWindow I get 5 windows
If I add any line inside __init__ that creates a Widget I get 5 windows, such as x = QtWidgets.QPushButton()
I cannot reproduce this consistently with different versions of PyQt and pandas other than those below
PyQt==5.13.0
pandas==0.24.2
I reproduced this on two different computers. I have a feeling this is a bug with one of the libraries but would like help understanding the actual root cause here since my example requires such a specific scenario... it would not be useful as a GitHub issue and I don't even know which library is responsible.
It is actually unexpected that the windows don't close. The real bug is in your own code. You are implicitly creating reference-cycles which are randomly keeping objects alive. Python's garbage-collector does not guarantee when or in what order objects will be deleted, so the unpredictable behaviour you see is "normal". If you add import gc; gc.collect() after the for-loop, the windows will probably all be deleted. So the correct solution is to keep explict references to the windows, as you already mentioned in your first bullet-point.
Related
I have a function button_max_slot that use showMaximized to maximize the window, and another function button_restore_slot to restore the window's size and place. the first time I call the button_max_slot it works great, then I use button_restore_function to restore window's size and place. But the second time I call the button_max_slot to maximize the window, it didn't work. I call self.isMaximized() and it returns true, but actually the window doesn't maximized.What should I do to fix this problem?Here is a minimal reproducible example:
from PyQt5 import QtWidgets
import sys
class MyWindow(QtWidgets.QMainWindow):
last_geometry = None
def __init__(self):
super().__init__()
self.resize(960, 540)
self.ButtonMax = QtWidgets.QPushButton(self)
self.ButtonMax.setObjectName("ButtonMax")
self.ButtonMax.setText("Max")
self.ButtonMax.move(100, 100)
self.ButtonRestore = QtWidgets.QPushButton(self)
self.ButtonRestore.setText("Restore")
self.ButtonRestore.setObjectName("ButtonRestore")
self.ButtonRestore.move(300, 100)
self.ButtonRestore.setEnabled(False)
self.ButtonMax.clicked.connect(self.button_max_slot)
self.ButtonRestore.clicked.connect(self.button_restore_slot)
def button_max_slot(self):
self.last_geometry = self.geometry()
self.showMaximized()
self.ButtonRestore.setEnabled(True)
self.ButtonMax.setEnabled(False)
def button_restore_slot(self):
self.setGeometry(self.last_geometry)
self.ButtonRestore.setEnabled(False)
self.ButtonMax.setEnabled(True)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
myshow = MyWindow()
myshow.show()
sys.exit(app.exec_())
tl;dr
Use showNormal() instead of setGeometry() to restore the window state.
Explanation
While not intuitive, it's still possible to set a geometry of a window even if its state should not allow it, and that's because setting the state of a window is not the same as setting its geometry.
Simply put, setting the window state tells the underlying platform/window manager to "choose" the geometry of the window based on the specified state, while setting the geometry asks the system to explicitly set the position and size of the window. Whether the system allows it, is another story.
An important thing to consider is that a QWidget (even a top level one, including a QDialog or a QMainWindow) is not the actual window shown on the screen. What you see is the QWindow (an abstract representation of the system's window for that widget), which is what actually contains the QWidget (or any of its inherited classes). Setting the state acts on the QWindow, while setting the geometry normally acts on the contained widget, excluding the possible window frame.
For instance, I'm able to reproduce your issue on my Linux system, but ekhumoro cannot, even though we both are using Linux (we're both using similar window managers, but they're still different: he's on OpenBox, I'm on FluxBox). Furthermore, I get inconsistent behavior after pressing the "Max" button, even if using the system features.
The fact that you got a maximized window state even if it doesn't look like it is, is exactly related to that: the state is maximized, the geometry isn't (because you changed it).
Consider it the other way around: you can manually resize a window in order to precisely occupy the whole available screen size, but that doesn't make it maximized: you can still probably see the "maximize" button in it's title bar (not the "normalize" one), and maybe even the window borders that are normally hidden when the window is actually maximized.
Note that the inconsistent behavior shown on different OS or window managers relies on two sides: the OS/wm implementation and Qt attempts to use a "standardized" behavior across all systems.
The solution is simple: just restore the state using showNormal() instead of setGeometry().
It usually works on all systems, with the exception of some very specific window managers on linux (and maybe some "non standard" behavior in recent Windows/MacOS versions), but it's the accepted approach.
For those cases, you might consider storing and restoring the geometry by overriding the top level window's changeEvent(), checking if the event.type() is a WindowStateChange and eventually decide the behavior based on the current windowState() and the oldState() of that event.
Remember that window states are flags, so they can be an OR combination of Qt.WindowState enums.
I have an application with two main windows. I want both of then to have the standard title which contains the file name and application name. But this works strange because both files show the file name but only the second window shows the application name. The first shows just "x.py" while the second "y.py - My App". Anybody has an idea why is that and how to solve it? Is this a bug or is it expected behaviour?
from qtpy.QtWidgets import QApplication, QMainWindow
app = QApplication([])
app.setApplicationDisplayName("My App")
wnd1 = QMainWindow()
wnd2 = QMainWindow()
wnd1.setWindowFilePath("x.py") # in most cases it shows only "x.py" - this is wrong
wnd2.setWindowFilePath("y.py") # correctly shows "y.py - My App"
wnd1.show()
wnd2.show()
app.exec_()
Tested on Ubuntu 16.04., PyQt 5.8.2.
UPDATE: So I also discovered it behaves non-deterministically. Sometimes both application titles appear correctly. Sometimes only one. This seems like a bug.
As a workaround for this likely bug I am going to override the setWindowFilePath() for my main window classes. This will give me another benefit such as showing the full file path instead of just file name and also indicate that the file is unnamed if it is a new file which has not yet been saved or loaded, which is what I want anyway. It also works well with changing window-modified state. I know I am sacrificing the 100 % 'native' look but... I can live with it.
def setWindowFilePath(self, filePath):
super(MainWindow, self).setWindowFilePath(filePath)
if not filePath:
filePath = "unnamed"
self.setWindowTitle("{}[*] - {}".format(filePath, qApp.applicationDisplayName()))
Maybe somebody will find a better solution.
I have a small application that I have built using PyQt4 and pyqtgraph. I want to put a few buttons in that call the exporters available with pyqtgraph (rather than, or really in addition to, using the context menu that pops up when a user right clicks on a plot).
So far, however, I have not been able to get this to work.
Here is a simplified version of the application:
from PyQt4 import QtCore, QtGui
import pyqtgraph as pg
import pyqtgraph.exporters
import numpy as np
import sys
class SimpleUI(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.resize(1500, 1000)
self.plot_widget = pg.GraphicsLayoutWidget(self)
self.layout = QtGui.QVBoxLayout(self)
data = np.arange(10)
self.plt = self.plot_widget.addPlot()
self.plt.plot(data)
self.export_btn = QtGui.QPushButton("Export")
self.export_btn.clicked.connect(self.export)
self.layout.addWidget(self.plot_widget)
self.layout.addWidget(self.export_btn)
def export(self):
img = pg.exporters.ImageExporter(self.plt)
img.export()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
ex = SimpleUi()
ex.show()
sys.exit(app.exec_())
Clicking on the "Export" button in this case causes a dialog to quickly pop up and then disappear.
If instead I put
img.export(copy=True)
And try to paste what's on the clipboard into something (Paint, whatever), python.exe crashes.
Oddly, exporting through the context menu that is available by default with pyqtgraph works just fine. Also, just working in the terminal I can copy / save plotItems just fine using the same exact lines of code as above. I.e.:
import numpy as np
import pyqtgraph as pg
import pyqtgraph.exporters
plt = pg.plot(np.arange(10))
img = pg.exporters.ImageExporter(plt.plotItem)
img.export()
Which implies to me that that exporters are working fine, but there is some weird interaction that is going on when they are called from within a pyqt4 widget in the manner that I am calling them.
I have tried both pyqtgraph 0.9.8 as well as the main branch on github. Very much at a loss as to what is the issue here
Thanks
It looks like you are not storing img anywhere, so it is collected as soon as the call to export() returns.
Explanation:
Objects in Python are kept in memory only as long as they are needed. When Python determines that an object is no longer needed, it deletes the object.
How does Python know when an object is no longer needed? By counting references. When you execute img = ImageExporter(...), a new object is created with one reference: the local variable img.
Variables that are created inside a function are considered local to the scope of that function. When the function exits, the variable img disappears, which causes the reference count of the ImageExporter object to drop to 0, which causes Python to delete the object.
By setting self.img = ImageExporter(...), you are assigning a reference to the object that is not local to the scope of the function (because the SimpleUI object referred to as self continues to exist after the function returns). This allows the object to persist as long as the SimpleUI still holds the reference.
I am confused by PyQt4. I have tried the following steps on python2.6:
In [1]: from PyQt4 import QtGui
In [2]: import sys
In [3]: app = QtGui.QApplication(sys.argv)
In [4]: pix = QtGui.QPixmap("P1010001.JPG")
In [5]: pix.width(), pix.height()
Out[5]: (0, 0)
Why does width and height show zero? The image exists and is fine. This is completely counterintuitive, which I do not expect from python.
PyQt adds a little syntactic sugar here and there to make things more Pythonic. But it is mostly a fairly thin wrapper around Qt (which is a C++ library) - and so it would be a mistake to expect PyQt to always behave in a way that is intuitive to Python programmers.
I suppose most Python programmers might expect QPixmap to raise an error when setting a path that doesn't exist. But Qt doesn't do this, and, in this case, neither does PyQt. Instead, you can check that you have a valid pixmap by using:
pix.isNull()
To actually fix the code in your example, you will obviously have to change to the appropriate directory first (or use an absolute path).
In My App, I have a QWidget which is not showing after I call show(), even though isVisible returns true.
This widget is created from an event of the main application window. But when its started on its own, i.e., as the only widget on an app, it shows up normally.
Anyone knows what may cause this behavior?
Other widgets in my app show up normally only this one is giving me troubles. It actually use to work just fine under a previous version of Qt4 (don't remember which).
the code for the widget is here
update: windows seems to appear and is immediately destroyed.
The relevant code is in hidx/GUI/main.py:
#pyqtSignature("")
def on_actionScatterplot_Matrix_activated(self):
...
spm = scatmat.ScatMat(pars, self.currentdbname)
print "==>", spm.pw.isVisible()
spm.pw.hide()
spm.pw.showMaximized()
print spm.pw.size()
print "==>", spm.pw.isVisible()
#pyqtSignature("int")
def on_rowStart_valueChanged(self, p0):
...
In on_actionScatterplot_Matrix_activated, you create an instance of ScatMat, but don't keep a reference to it. So the window will be briefly shown, and then immediately garbage-collected once the function completes.