QtWidget.grab() consumes gradually memory when called in the loop - python

I have PyQt5 GUI, where I load some data, which I consequently plot into graphs
to do not upload whole application I created just example where is used what crashes ...
once, I need to save "GUI-visible" graphs as pictures (for later usage), so I call:
grabbed = some_graphically_visible_widget.grab()
and
grabbed.save("My_name.png")
these two methods are called in the loop up to 350 times and during the
loop, python saves the grabbed "object" somewhere because as memory_profiler
showed and I found out, each .grab() cycle memory consumption increases ~ 1.5MB
also, I tried multiple variations of using:
del grabbed
in the end of loop, or playing with
gc.collect()
But nothing helped and calling this cycle always eats "it's part".
Below is full example application, which is fully working once
the PyQt5 and pyqtgraph modules are provided to be "imported":
import sys
import os
from random import randint
from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5.QtWidgets import QShortcut, QMessageBox
from PyQt5.QtGui import QKeySequence
import pyqtgraph
app = QtWidgets.QApplication(sys.argv)
class Ui_MainWindow(object):
def __init__(self):
self.graph_list = []
def setupUi(self, MainWindow):
MainWindow.setObjectName("Example")
MainWindow.resize(750, 750)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
MainWindow.setCentralWidget(self.centralwidget)
self.tabWidget = QtWidgets.QTabWidget(self.centralwidget)
self.tabWidget.setGeometry(QtCore.QRect(5, 5, 740, 740))
self.tabWidget.setObjectName("tabWidget")
self.shortcut_CtrlL = QShortcut(QKeySequence('Ctrl+E'),self.centralwidget)
self.shortcut_CtrlL.activated.connect(self.doExport)
progress = QtWidgets.QProgressDialog("Creating enough graphs to simulate my case ... (350) ", None, 0, 350, self.centralwidget)
progress.setWindowTitle("...")
progress.show()
"Typical amount of graphs in application"
for tab_idx in range(350):
font = QtGui.QFont()
font.setPixelSize(15)
tab = QtWidgets.QWidget()
graph = pyqtgraph.PlotWidget(tab)
self.graph_list.append(graph)
graph.setGeometry(QtCore.QRect(5, 5, 740, 740))
graph.addLegend(size=None, offset=(370, 35))
x = []
y = []
min = []
max = []
for num in range(10):
x.append(num)
y.append(randint(0, 10))
min.append(0)
max.append(10)
graph.plot(x, y, symbol='o', symbolPen='b', symbolBrush='b', name = "List of randomized values")
graph.plot(x, min, pen=pyqtgraph.mkPen('r', width=3, style=QtCore.Qt.DashLine))
graph.plot(x, max, pen=pyqtgraph.mkPen('r', width=3, style=QtCore.Qt.DashLine))
graph.showGrid(x=True)
graph.showGrid(y=True)
graph.setTitle(str(graph))
self.tabWidget.addTab(tab, str(tab_idx))
progress.setValue(tab_idx)
app.processEvents()
msgBox = QMessageBox()
msgBox.setIcon(QMessageBox.Information)
str_to_show = "Once you see GUI, press CTRL+E and watch memory consumption in task manager"
msgBox.setText(str_to_show)
msgBox.setWindowTitle("Information")
msgBox.setStandardButtons(QMessageBox.Ok)
msgBox.exec()
progress.close()
def doExport(self):
iterations = 0
progress = QtWidgets.QProgressDialog("Doing .grab() and .save() iterations \nnow you may watch increase RAM consumption - you must open taskmgr", None, 0, 350, self.centralwidget)
progress.setWindowTitle("...")
progress.show()
for graph in self.graph_list:
iterations += 1
grabbed = graph.grab()
grabbed.save("Dont_worry_I_will_be_multiple_times_rewritten.png")
progress.setValue(iterations)
app.processEvents()
progress.close()
msgBox = QMessageBox()
msgBox.setIcon(QMessageBox.Information)
str_to_show = str(iterations) + ' graphs was grabbed and converted into .png and \n python\'s RAM consumption had to increase ...'
msgBox.setText(str_to_show)
msgBox.setWindowTitle("Information")
msgBox.setStandardButtons(QMessageBox.Ok)
msgBox.exec()
if __name__ == "__main__":
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())

This has nothing to do with grab (at least, not directly), but with QGraphicsView caching (pyqtgraph PlotWidgets are actually QGraphicsView subclasses).
In fact, if you comment the whole grabbing and use self.tabWidget.setCurrentIndex(iterations) instead, you'll see the memory spiking anyway, and that's because grab() obviously causes the widget to be painted and, therefore, create the graphics view cache.
The solution to your issue is to disable caching for each graph:
def setupUi(self, MainWindow):
# ...
for tab_idx in range(350):
# ...
graph = pyqtgraph.PlotWidget(tab)
self.graph_list.append(graph)
graph.setCacheMode(graph.CacheNone)
The real question is: do you really need to add so many graphs? If you only need to grab each graph, use a single plot widget, and set each plot/grab in a for cycle. Honestly, I don't understand what is the benefit of showing so many graphs all at once: 350 QGraphicsViews are a lot, and I sincerely doubt you really need the user to access them all at once, especially considering that using QTabWidget would make them hard to access.
Also:
You're creating a tab QWidget for each tab, but you're just adding a single graphics view and without any layout manager; this results in a problem when resizing the main window (the plot widgets don't adjust their size) and is anyway unnecessary: just add the plot widget to the QTabWidget: self.tabWidget.addTab(graph, str(tab_idx))
NEVER modify the files generated by pyuic, nor try to mimic their behavior; if you are building the UI from code, just subclass the widget that will act as a container/window (QMainWindow, in your case), add child widgets to it (using a central widget for main windows) and implement all methods from that class, otherwise follow the official guidelines about using Designer.

Related

PyQt5 Plotly viewer creates multiple save file dialogs

I've created a lightweight browser to view plotly .html files within a PyQt5 application instead of in the default browser, using QWebEngineView based on other questions such as this: Using a local file in html for a PyQt5 webengine
The viewer works, but when multiple windows are open with several plots, attempting to save one of the plots as .png file causes several save file dialogs to open (one for every window that has been open since the program started running).
I tried debugging this, after the download request it seems to jump to sys.exit(app.exec_()), then back to the download request again. Although several dialogs are open, only one plot is actually saved.
Is there a way to ensure only one dialog is created?
To reproduce, run the following code and click plot the button 2 or more times, creating several windows. Use the plotly "download plot as png" option and after saving the plot, one or more additional save file dialogs are presented.
import os
import sys
from pathlib import Path
import plotly
import plotly.express as px
from PyQt5 import QtCore, QtWidgets
from PyQt5 import QtWebEngineWidgets, QtGui
user_profile = Path(os.environ.get("USERPROFILE"))
APP_DATA_FOLDER = user_profile / "AppData" / "Local" / "program"
APP_DATA_FOLDER.mkdir(parents=True, exist_ok=True)
class PlotlyViewer(QtWebEngineWidgets.QWebEngineView):
"""A lightweight browser used to view Plotly
figures without relying on an external browser.
"""
def __init__(
self, fig, title="Plot Viewer", count=0, download_directory=None
):
super().__init__()
self.windows = []
# Create a temporary html file containing the plot
self.file_path = str(APP_DATA_FOLDER / f"temp{count}.html")
plotly.offline.plot(
fig, filename=self.file_path, auto_open=False,
)
# Open the html file with the PlotlyViewer
self.load(QtCore.QUrl.fromLocalFile(self.file_path))
self.setWindowTitle(title)
self.resize(1000, 600)
# When a downloadRequest is received, run the download_file method
self.page().profile().downloadRequested.connect(self.download_file)
def closeEvent(self, event):
# When the plot is closed, delete the temporary file
os.remove(self.file_path)
#QtCore.pyqtSlot(QtWebEngineWidgets.QWebEngineDownloadItem)
def download_file(self, download):
# Get a save_file_dialog... For some reason this happens twice!
plot_path, _ = QtWidgets.QFileDialog.getSaveFileName(
None, "Save Plot As...", str(user_profile), "Image (*.png)"
)
if plot_path:
download.setPath(plot_path)
download.accept()
#staticmethod
def save_file_dialog(export_dir):
file_path, _ = QtWidgets.QFileDialog.getSaveFileName(
None, "Save Plot As...", export_dir, "Image (*.png)"
)
return file_path
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.setFixedSize(150, 100)
MainWindow.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.btn_plot = QtWidgets.QPushButton(self.centralwidget)
self.btn_plot.setGeometry(QtCore.QRect(0, 0, 70, 23))
self.btn_plot.setObjectName("btn_plot")
self.btn_plot.setText("plot")
self.connect_slots()
def connect_slots(self):
self.btn_plot.clicked.connect(self.create_plot)
def create_plot(self):
fig = px.scatter(x=[0, 1, 2, 3, 4], y=[0, 1, 4, 9, 16])
browser_title = "a window title"
plot_window = PlotlyViewer(fig, browser_title, download_directory=user_profile)
plot_window.windows.append(plot_window)
plot_window.show()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())
By default, all QWebEnginePages share the same QWebEngineProfile, meaning that self.page().profile() will always return the same profile object.
Qt connections are always cumulative, even for the same target slot/function: connecting a signal to the same function twice results in calling that function twice for every time the signal is emitted.
Since you're connecting to the signal of the same profile every time a new PlotlyViewer instance is created, download_file will be called for each instance every time a download is requested.
You have three possibilities:
connect the signal just once using the defaultProfile(), and externally from the PlotlyViewer class;
create a new standalone profile (similar to the "private mode") for each view:
self.profile = QWebEngineProfile()
self.setPage(QWebEnginePage(profile))
self.profile.downloadRequested.connect(self.download_file)
check that the page() of the download request belongs to the page of the view:
def download_file(self, download):
if download.page() != self.page():
return
# ...
Further important notes:
most widgets override event handlers, and it's always good practice to call the base implementation (unless you really know what you're doing); you must call super().closeEvent(event) in the closeEvent override;
since you probably don't intend to reuse closed views, you should always delete them, otherwise their resources will unnecessarily occupy memory (and a lot); add self.setAttribute(Qt.WA_DeleteOnClose) in the __init__, or call self.deleteLater() in closeEvent();
self.windows is an instance attribute, adding the plot_window instance to it is completely useless, since it will always contain one object (the instance itself); if you want to keep track of all existing windows you should create the list as an attribute of the parent object (ie. the main window) or as a class attribute (for PlotlyViewer); also, considering the point above, you should delete the reference whenever the view is closed and destroyed;
you shall not edit pyuic generated files; unless you did this for the sake of the example, be aware that it's considered bad practice, and you should instead follow the official guidelines about using Designer;

How to call other functions while update image(opencv) on a label Pyqt5?

I want to update the label with images but when I start to update the label, nothing works until the updating is done for example I want to pause updating or closing the window. It worked perfect when I used tkinter but it doesn't work same method in Pyqt5. Here is the code I have
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QFileDialog, QDialog, QMainWindow,QMessageBox,QGroupBox,QSlider,QPushButton,QRadioButton,QLabel,QCheckBox,QFrame,QWidget,QTabWidget,QProgressBar,QTextBrowser,QTableWidget
from PyQt5.QtCore import QCoreApplication, Qt, QSize,QThread, QRect,QSize
from PyQt5.QtGui import QIcon, QFont,QPixmap,QIcon
import cv2
import os
import glob
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName("Form")
Form.resize(800, 800)
self.label = QtWidgets.QLabel(Form)
self.label.setGeometry(QtCore.QRect(165, 125, 61, 16))
self.label.setObjectName("label")
self.btn1 = QPushButton(Form)
self.image_frame = QtWidgets.QLabel(Form)
self.btn1.setGeometry(QRect(350, 200, 111, 31))
self.btn1.clicked.connect(self.load_images_from_folder)
self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
self.btn1.setText(_translate("Form","Btn"))
def load_images_from_folder(self):
for filename in glob.glob('path/*.jpg'):
img = cv2.imread(filename, cv2.IMREAD_UNCHANGED)
img = cv2.resize(img,(200,200))
print(filename)
self.showimg(img)
def showimg(self,img):
self.image = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
self.image = QtGui.QImage(self.image.data, self.image.shape[1], self.image.shape[0], QtGui.QImage.Format_RGB888).rgbSwapped()
self.image_frame.setPixmap(QtGui.QPixmap.fromImage(self.image))
self.image_frame.setGeometry(QRect(70, 310, 461, 441))
Form.update()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Form = QtWidgets.QWidget()
ui = Ui_Form()
ui.setupUi(Form)
Form.show()
sys.exit(app.exec_())
After I clicked Btn label updates but I can't click Btn again until the updating is finished
In UI environments, control has to be returned as quickly as possible to the main thread. If you have lots of image processing, this means that the function that loads the image (which also calls the function that sets the image) will not release control to the main thread until it's finished.
If you don't, the result is that the UI will be completely frozen (no updates, no user input) until that function returns.
The solution is to use threads, but you also need to consider that UI elements are involved, and no access is allowed to UI elements from external threads. So, the actual solution is to use QThreads and custom signals: Qt signals are thread safe, and they are queued whenever two objects belonging to different threads are trying to communicate.
In this specific case, the thread will be a QThread with a custom signal (which has to be emitted whenever a new image is ready):
class ImageLoader(QtCore.QThread):
imageReady = QtCore.pyqtSignal(object)
def __init__(self):
super().__init__()
self.images = []
def run(self):
while True:
while self.images:
img = cv2.imread(self.images.pop(), cv2.IMREAD_UNCHANGED)
img = cv2.resize(img,(200,200))
self.imageReady.emit(img)
def loadImages(self, imageList):
# if you want to *clear* the current queue
self.images[:] = imageList
# otherwise, just append:
# self.images.extend(imageList)
The implementation then requires to create the thread instance and add the images to the queue in load_images_from_folder:
class Ui_Form(object):
def setupUi(self, Form):
# ...
self.imageLoader = ImageLoader()
self.imageLoader.imageReady.connect(self.showimg)
self.imageLoader.start()
def load_images_from_folder(self):
self.imageLoader.loadImages(glob.glob('path/*.jpg'))
Important notes:
editing files generated with pyuic is considered bad practice, as it almost always leads to unexpected behavior and bugs that are difficult to track; the comment in the header of those files is pretty clear: you should not edit them; instead, create those files, leave them as they are, and then import them in your main script, as explained in the official guidelines about using Designer;
accessing Form (Form.update()), which is fundamentally a global variable, is another bad practice; just like editing pyuic files, it's something that should never be done unless you really know what you're doing (and if you do know, you won't probably do it at all); in any case, setting the pixmap on a label automatically schedules an update, so that function call is completely useless since we're properly using threading, which allows the main event loop to request updates on the UI;
setting fixed geometries is yet another practice usually discouraged; learn how to use layout managers and how to use them in Designer instead; if you want the QLabel to have a fixed size, use setFixedSize();

pyqt5 with .ui file tab menu padding and window size in designer

my running app
here how i created my app step by step:
i am created a tab through designer tool with 3 pages:
a) Account
b) Security c) Performance and save as tab.ui
then i generate tab.py file from tab.ui by using pyuic5
now i added some classes manually in tab.py file >> TabBar, TabWidget and ProxyStyle classes, then change self.tabWidget = QtWidgets.QTabWidget(self.centralwidget) to self.tabWidget = TabWidget(self.centralwidget), and add àpp.setStyle(ProxyStyle()) after app = QtWidgets.QApplication(sys.argv)
my code is working as i shown in pic, but my tab menu padding is not looking good and window size is not full (fit with window if i maximized). someone please look in to it.
Now my question is if we add some other elements in tab.ui and if i generate tab.py file again my previous tab.py code is overlapped which classes i manually added. this is not fine.
i know i am wrong .but tell me the procedure and give me a proper structure than i can start to create my tool in right way.
here is my tab.py code:
from PyQt5 import QtCore, QtGui, QtWidgets
class TabBar(QtWidgets.QTabBar):
def tabSizeHint(self, index):
s = QtWidgets.QTabBar.tabSizeHint(self, index)
s.transpose()
return s
def paintEvent(self, event):
painter = QtWidgets.QStylePainter(self)
opt = QtWidgets.QStyleOptionTab()
for i in range(self.count()):
self.initStyleOption(opt, i)
painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, opt)
painter.save()
s = opt.rect.size()
s.transpose()
r = QtCore.QRect(QtCore.QPoint(), s)
r.moveCenter(opt.rect.center())
opt.rect = r
c = self.tabRect(i).center()
painter.translate(c)
painter.rotate(90)
painter.translate(-c)
painter.drawControl(QtWidgets.QStyle.CE_TabBarTabLabel, opt);
painter.restore()
class TabWidget(QtWidgets.QTabWidget):
def __init__(self, *args, **kwargs):
QtWidgets.QTabWidget.__init__(self, *args, **kwargs)
self.setTabBar(TabBar(self))
self.setTabPosition(QtWidgets.QTabWidget.West)
class ProxyStyle(QtWidgets.QProxyStyle):
def drawControl(self, element, opt, painter, widget):
if element == QtWidgets.QStyle.CE_TabBarTabLabel:
ic = self.pixelMetric(QtWidgets.QStyle.PM_TabBarIconSize)
r = QtCore.QRect(opt.rect)
w = 0 if opt.icon.isNull() else opt.rect.width() + self.pixelMetric(QtWidgets.QStyle.PM_TabBarIconSize)
r.setHeight(opt.fontMetrics.width(opt.text) + w)
r.moveBottom(opt.rect.bottom())
opt.rect = r
QtWidgets.QProxyStyle.drawControl(self, element, opt, painter, widget)
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(800, 600)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
#self.tabWidget = QtWidgets.QTabWidget(self.centralwidget)
self.tabWidget = TabWidget(self.centralwidget)
self.tabWidget.setGeometry(QtCore.QRect(0, 40, 800, 541))
self.tabWidget.setStyleSheet("\n"
" QTabBar::tab { height: 100px; width: 50px; }\n"
" QTabBar::tab {background-color: rgb(34, 137, 163);}\n"
" QTabBar::tab:selected {background-color: rgb(48, 199, 184);}\n"
" QTabWidget>QWidget>QWidget{background: WHITE;}\n"
" ")
self.tabWidget.setTabPosition(QtWidgets.QTabWidget.West)
self.tabWidget.setObjectName("tabWidget")
self.tab = QtWidgets.QWidget()
self.tab.setObjectName("tab")
self.groupBox_3 = QtWidgets.QGroupBox(self.tab)
self.groupBox_3.setGeometry(QtCore.QRect(20, 10, 681, 80))
self.groupBox_3.setObjectName("groupBox_3")
self.groupBox_4 = QtWidgets.QGroupBox(self.tab)
self.groupBox_4.setGeometry(QtCore.QRect(20, 100, 681, 80))
self.groupBox_4.setObjectName("groupBox_4")
self.tabWidget.addTab(self.tab, "")
self.tab_2 = QtWidgets.QWidget()
self.tab_2.setObjectName("tab_2")
self.groupBox = QtWidgets.QGroupBox(self.tab_2)
self.groupBox.setGeometry(QtCore.QRect(30, 20, 251, 191))
self.groupBox.setObjectName("groupBox")
self.groupBox_2 = QtWidgets.QGroupBox(self.tab_2)
self.groupBox_2.setGeometry(QtCore.QRect(290, 20, 271, 191))
self.groupBox_2.setObjectName("groupBox_2")
self.tabWidget.addTab(self.tab_2, "")
self.tab_3 = QtWidgets.QWidget()
self.tab_3.setObjectName("tab_3")
self.tabWidget.addTab(self.tab_3, "")
self.frame = QtWidgets.QFrame(self.centralwidget)
self.frame.setGeometry(QtCore.QRect(-1, 0, 801, 41))
self.frame.setStyleSheet("background-color: rgb(59, 118, 150);")
self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
self.frame.setObjectName("frame")
self.comboBox = QtWidgets.QComboBox(self.frame)
self.comboBox.setGeometry(QtCore.QRect(50, 10, 141, 22))
self.comboBox.setObjectName("comboBox")
MainWindow.setCentralWidget(self.centralwidget)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
self.tabWidget.setCurrentIndex(2)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.groupBox_3.setTitle(_translate("MainWindow", "GroupBox"))
self.groupBox_4.setTitle(_translate("MainWindow", "GroupBox"))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("MainWindow", "Account"))
self.groupBox.setTitle(_translate("MainWindow", "GroupBox"))
self.groupBox_2.setTitle(_translate("MainWindow", "GroupBox"))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), _translate("MainWindow", "Security"))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_3), _translate("MainWindow", "Performance"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
app.setStyle(ProxyStyle())
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())
There are two main and common issues with your approach.
*NEVER* edit the output of pyuic
This happens very often: you get a well formatted python file, and are led to think that you can use that file to write your program.
You may have noticed the warning in that file too:
# WARNING! All changes made in this file will be lost!
As you've already found out, as soon as you need to modify the UI, you'll be caught in the mess of merging the new modifications with the code you already wrote.
The bottom line is that you've to think about those files as resource files (not unlike an image, a database or a configuration file), and they have to be used as such: since they are python files, they can be imported as a module, and their classes have to be used to build the interface of your actual widgets and windows.
There are three main ways to do that, all of them explained in the using Designer documentation.
I strongly suggest you to use the third method (the multiple inheritance approach), as it allows you to have references to UI objects as direct attributes of the instance (self.tabWidget, etc.).
Alternatively, you can completely avoid the pyuic approach at all, and directly import the .ui files using the loadUI function from the uic module.
from PyQt5 import QtWidgets
from PyQt5 import uic
class MyWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
uic.loadUI('mywindow.ui', self)
This allows you to save some time, since you don't have to rebuild the file each time you make modifications; also, sometimes one may forget to do that step, which might create some confusion.
Just remember that that path is always relative to the file that contains the class that will load it (and absolute paths should never be used).
Always use a layout manager
Using fixed widget sizes and positions is usually discouraged, as what you see on your computer will probably be very, if not drastically, different on another's.
That can depend on a series of aspects, but most importantly:
operating system (and its version);
screen settings (resolution, standard or High DPI - such as retina screens);
user customization (default font sizes, some "themes" that use different margins and spaces between objects);
All this potentially makes a fixed layout unusable, as widgets could overlap or become invisible because hidden by others or because the available screen size is not enough to show the interface as you designed it.
Using layout managers simplifies all this, because they will automatically resize the interface ensuring that all widgets will at least use as much space as they need, leaving space for those that might take advantage in using more size.
While this could seem a bit more difficult to manage (especially for complex interfaces), it's just a matter of habit.
Most of the times you'll end up with nested layouts, but there's nothing wrong with them.
In your case, you'll probably use something like the following structure:
a vertical layout as the main layout;
a horizontal layout for the top;
a horizontal spacer, with a fixed width;
the combobox;
another horitonzal spacer for the right margin;
the tabwidget;
a vertical layout for the first tab;
the two vertically aligned group boxes
a horizontal layout for the second tab;
the two horizontally aligned group boxes;
...
As a final note, I don't think you need to use the proxystyle to adjust the size of the rectangle: as you can see, the text is cropped, and that's due to the way you paint and rotate, which also leads to the painting issue of the tab background.
Remove the sizes from the stylesheet and the proxystyle, then use those size values within tabSizeHint():
def tabSizeHint(self, index):
s = QtWidgets.QTabBar.tabSizeHint(self, index)
s.transpose()
s.setHeight(max(s.height(), 50))
s.setWidth(max(s.width(), 100))
return s
Since there's a small overlap between the tabbar and the tabwidget contents, you can ensure that they are correctly aligned by setting the offset of the ::pane and ::tab-bar pseudo elements:
QTabWidget::pane {top: 0px;}
QTabWidget::tab-bar {right: 0px;}
Also, ensure to apply the top frame stylesheet to QFrame objects only (and possibly its object name), otherwise every child widget will inherit it.
self.frame.setStyleSheet("QFrame#frame {background-color: rgb(59, 118, 150);}")
With all of this in mind, you'll end up with a cleaner look, correct tab widget/bar painting and positioning, layout flexibility and, most importantly, the possibility to edit the UI on the fly without further problems and headaches :-)

Update LCD Number countdown

I'm very new to Python and I have made a very simple countdown timer. The GUI was created in Qt Designer. There is a spin box for input of seconds, a start button and an LCD number counter. The counter counts down fine using the code below:
def start_btn_clicked(self):
x = self.Minute_spinBox.value()
for i in xrange(x,0,-1):
time.sleep(1)
print (i)
So that I could see what was happening as I played around with it, I added the print instruction so that it shows the countdown in the Python console as it runs. I then thought I could maybe quite easily have the LCD number display the countdown with something like:
self.lcdNumber.display(i)("%SS")
But no matter what I try, I cant get it to show. With the line above, I get the first number displayed, but then I get an error saying:
self.lcdNumber.display(i)("%SS")
TypeError: 'NoneType' object is not callable
I have tried so many variations that I no longer know where I started and here was me thinking it would be simple. I'd love to know why I cant get it to display the countdown.
Just adding one line of code to my original code will allow the lcdNumber to display the count...
def start_btn_clicked(self):
x = self.Minute_spinBox.value()
for i in xrange(x,0,-1):
time.sleep(1)
app.processEvents() # just this one line allows display of 'i'
self.lcdNumber.display(i)`
And works perfectly
The display function returns None, so doing None("%SS") obviously isn't allowed.
self.lcdNumber.display(i) is enough to show the countdown!
To let Qt paint the widgets while looping run the countdown from another thread. See an example.
import time
from threading import Thread
from PyQt4.QtGui import QApplication, QMainWindow, QLCDNumber
class Window(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.lcdnumber = QLCDNumber(self)
self.resize(400, 400)
t = Thread(target=self._countdown)
t.start()
def _countdown(self):
x = 10
for i in xrange(x,0,-1):
time.sleep(1)
self.lcdnumber.display(i)
if __name__ == "__main__":
app = QApplication([])
window = Window()
window.show()
app.exec_()
The for loop is blocking the GUI.
The slot connected to the button's clicked signal is processed synchronously. This means the event-loop must wait for the slot to return before it can process any more events (including the paint events needed for updating the GUI).
So you need to find a way to process these events whilst the for loop is running. There are various ways of doing this, such as using a QTimer or a QThread. But the simplest way of fixing your particular example would be to use QCoreApplication.processEvents.
Here's an example that shows how to do that:
import sys, time
from PyQt4 import QtCore, QtGui
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.spinbox = QtGui.QSpinBox(self)
self.spinbox.setValue(5)
self.lcdnumber = QtGui.QLCDNumber(self)
self.button = QtGui.QPushButton('Start', self)
self.button.clicked.connect(self.handleButton)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.spinbox)
layout.addWidget(self.lcdnumber)
layout.addWidget(self.button)
def handleButton(self):
for tick in range(self.spinbox.value(), -1, -1):
self.lcdnumber.display(tick)
self.button.setEnabled(not tick)
# continually process events for one second
start = time.time()
while time.time() - start < 1:
QtGui.qApp.processEvents()
time.sleep(0.02)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 300, 200)
window.show()
sys.exit(app.exec_())

Using signal to pop-up a screen

I'm having a little trouble using a signal to make a little screen appear.
Shortening all i have so far, this following code should show my problem.
import sys
from PyQt4 import QtGui, QtCore
qApp = QtGui.QApplication(sys.argv)
class InformatieVenster(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.setWindowTitle('Informatie')
self.setGeometry(100,100,300,200)
informatie = InformatieVenster()
class MenuKlasse(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
about = QtGui.QAction('About...', self)
about.setShortcut('Ctrl+A')
about.setStatusTip('Some text, haha')
self.connect(about, QtCore.SIGNAL('clicked()'), QtCore.SIGNAL(informatie.show()))
menubar = self.menuBar()
self.Menu1 = menubar.addMenu('&File')
self.Menu1.addAction(about)
Menu = MenuKlasse()
Venster = QtGui.QMainWindow()
Venster.menuBar().addMenu(Menu.Menu1)
Venster.setGeometry(200, 200, 300, 300);
size = Venster.geometry()
Venster.show()
qApp.exec_()
When this program is runned, the 'informatie' window automatically pops-up.
However... i only want this to happen every time I click on 'about...' in the menu, or when i use the assigned shortcut.
How may i improve my code such that my problem will be made history?
Greets!
The window is shown, because you are actually calling .show() during your connect. You have to pass a function object, not the result of a function invocation, as argument to .connect(). Moreover the function to be invoked, if a signal is emitted, is called "slot", the second SIGNAL() is completely misplaced.
Replace the connect line with:
self.connect(about, QtCore.SIGNAL('triggered()') informatie.show)
Even better, use the modern connection syntax:
about.triggered.connect(informatie.show)
Btw, do not use absolute sizes in GUI programs. Instead use layout management.

Categories

Resources