Execute long running code at program startup after complete GUI is rendered - python

I have an application that can be started from the command line with an optional filename as argument. If present this file should be loaded at startup. Since the processing of the file takes some time, fileOpen() blocks the program and shows a loading indicator.
During normal operation this is ok. However, when I try to do the same at startup (as shown below), the outline of the window is present after show() but its contents is not rendered up until app.exec_().
My Question: How do I handle such a situation?
I cannot put fileOpen() before app.exec_() because then the GUI is not yet rendered completely. And I cannot inform the user that the loading is still processed.
I cannot put ? fileOpen() after app.exec_() because it would not be executed untill the program finishes.
Example Code:
def main(args):
app = QtGui.QApplication()
mainwindow = MainWindow()
mainwindow.show()
if args.filename:
mainwindow.fileOpen(args.filename)
ret_val = app.exec_()
sys.exit(ret_val)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('filename', help='(optional) file to load at startup')
args = parser.parse_args()
main(args)

I have found that a single-shot timer can solve this problem, but I have only tested it on Linux with the Openbox window manager, so I cannot guarantee that it will work on all platforms. You may need to adjust the duration of the timeout to get this to work on your system.
Here is a simple demo that works for me:
import sys
from PyQt5 import QtCore, QtWidgets
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.edit = QtWidgets.QTextEdit(self)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.edit)
def fileOpen(self, path):
QtWidgets.qApp.setOverrideCursor(QtCore.Qt.WaitCursor)
QtCore.QThread.sleep(3)
self.edit.setText(open(path).read())
QtWidgets.qApp.restoreOverrideCursor()
def main():
app = QtWidgets.QApplication(sys.argv)
mainwindow = MainWindow()
mainwindow.setGeometry(600, 100, 300, 200)
mainwindow.show()
QtCore.QTimer.singleShot(50, lambda: mainwindow.fileOpen(__file__))
sys.exit(app.exec_())
if __name__ == '__main__':
main()

Note: This solution does not work for Linux(X11) (see comment and answer by ekhumoro)
Thanks for all the answers. While each of them has some drawbacks (will discuss it below), they brought me to the correct solution:
Call qApp.processEvents() after mainwindow.show():
def main(args):
app = QtGui.QApplication()
mainwindow = MainWindow()
mainwindow.show()
qApp.processEvents()
if args.filename:
mainwindow.fileOpen(args.filename)
ret_val = app.exec_()
sys.exit(ret_val)
Reason: It's exactly what we want to do:
We want to process events related to the drawing of the main window.
Then execute our custom code.
Then continue with the normal event loop.
Discussion of the alternative suggestions:
Why I don't call qApp.processEvents() in fileOpen(): This will be active for all fileOpen() calls. Processing other events during a long running file open call, may result in unexpected behavior if the application is not designed with this in mind, e.g. you could issue a second fileOpen() while the first is running.
Why I don't use a timer to perform fileOpen(): I want to execute the code after the GUI is completely loaded but before any user input. A timer just approixates the correct execution order. Additionally, the correct delay can vary depending on CPU, system usage and other factors, making this solution not very robust.

Related

How to call a method in PyQt5 right after the interface is done loading?

I am making an instrument interface in Qt5 and it works fine. The only issue is that it's slow to start because the interface __init__ contains a time-consuming method (5-10 seconds) used to connect to the instrument. Currently, nothing shows up for several seconds then the whole interface shows up with the "successfully connected to the instrument" message already written in its console (a textEdit widget).
What I would like is to have the interface show up instantly, and only after it's shown it should start the communication protocol. I am sure this is just a matter of moving one line around, but I can't figure it out. Any help is appreciated.
Here is a minimal example of the program structure:
# ================================================
# Interface management.
# ================================================
class RENAMEMELATER(Ui_renamemetoo, QObject):
def __init__(self, parent):
super(Ui_renamemetoo, self).__init__()
self.ui = Ui_renamemetoo()
self.ui.setupUi(parent)
# Redirect IDE console towards GUI console.
sys.stdout = EmittingStream()
sys.stdout.textWritten.connect(self.redirect_console_messages)
sys.stderr = EmittingStream()
sys.stderr.textWritten.connect(self.redirect_console_messages)
# Initialize PC/instrument communication (MOVE SOMEWHERE ELSE?)
self.T = TaborSE5082("USB0::0x168C::0x5082::0000218391::INSTR") # TIME CONSUMING.
def redirect_console_messages(self, text):
"""All print() from the program are appended on a textEdit
instead of the IDE console."""
self.ui.Console_textEdit.append(text.rstrip("\n"))
def close_program(self):
"""Call those functions after the user clicked on exit."""
self.T.CLOSE()
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
print("Program terminated.")
# ================================================
# Program execution.
# ================================================
if __name__ == "__main__":
# Define the app.
if not QtWidgets.QApplication.instance():
app = QtWidgets.QApplication(sys.argv)
else:
app = QtWidgets.QApplication.instance()
# Start the interface.
Form = QtWidgets.QWidget()
prog = RENAMEMELATER(Form)
Form.show()
# Handle what happens at program exit.
app.setQuitOnLastWindowClosed(True)
app.aboutToQuit.connect(prog.close_program)
# Launch.
app.exec()
In the main I can use app.aboutToQuit to close the instrument. Maybe there is some sort of app.isDoneLoading that I could .connect to my instrument initialization in the same way?
Thank you.
A task that takes 5 to 10 seconds is heavy so apart from not showing the GUI you can freeze it so the solution is to run it in another thread:
def __init__(self, parent):
# ...
threading.Thread(target=self.callback, daemon=True).start()
def callback(self):
self.T = TaborSE5082("USB0::0x168C::0x5082::0000218391::INSTR")
# another code

PyQt app.exec() seems to be non-blocking

I have a quite complicated PyQt app (Qt5, running in Spyder), where at the end I do
def main():
from PyQt5 import QtWidgets
if not QtWidgets.QApplication.instance():
app = QtWidgets.QApplication(sys.argv)
else:
app = QtWidgets.QApplication.instance()
main_window = MainWindow()
main_window.show()
status = app.exec_()
print status
sys.exit(0)
if __name__ == "__main__":
main()
(The if-else check is needed because of this(second answer).) When I run this code, my app shows, and the status code -1 is printed at the same time (due to a raised error in spyder/utils/ipython/start_kernel.py). My question is, why this error is printed at all, because I thougt that app.exec_() is a blocking call and the status is not returned until the app is exited somehow. Is this due to Spyder running its own QApplication?
It is not possible to execute the application event-loop more than once. This is easy enough to test with a simple script:
import sys
from PyQt5 import QtCore, QtWidgets
app = QtWidgets.QApplication(sys.argv)
btn = QtWidgets.QPushButton('Test')
btn.clicked.connect(lambda: print(QtWidgets.QApplication.exec_()))
btn.show()
sys.exit(app.exec_())
Output:
QCoreApplication::exec: The event loop is already running
-1
So, if the event-loop is already running, exec just returns immediately without blocking.
(NB: obviously you will need to run the above script in a normal console to test it properly).

Threading a task beside a GUI PyQT4

So i am trying to run a PyQT GUI while another functions is gathering information in the background. If Information is found the GUI should update itself.
I am new in Threading so i googled a lot and found some good HowTo's although it does not work as it should.
when i run the program it just ends itself after 3 s.
Maybe you see some major mistake ive done.
Here is the basic code i am trying to get to run
class scan_Thread(QThread):
def __init__(self, samp_rate, band, speed, ppm, gain, args, prn):
QThread.__init__(self)
self.samp_rate=samp_rate
self.band=band
self.speed=speed
self.ppm=ppm
self.gain=gain
self.args=args
self.prn=prn
def __del__(self):
self.wait()
def run(self):
do_scan(self.samp_rate, self.band, self.speed,
self.ppm, self.gain, self.args, self.prn)
def start_gui():
app = QtGui.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
#app.exec_()
#sys.exit()
def main(options = None):
def printfunc(found_list):
for info in sorted(found_list):
print info
get_thread = scan_Thread(options.samp_rate, options.band, options.speed,
options.ppm, options.gain, options.args, printfunc)
get_thread.start()
start_gui()
Thanks!
Many of the objects of the Qt classes and therefore of PyQt need to start some object of type Application (QCoreApplication, QtGuiApplication or QApplication), but only one of these objects must exist.
In your particular case QThread needs it. The previous classes are responsible for generating the necessary loops.
So you should modify your code to the following:
def main(options = None):
app = QtGui.QApplication(sys.argv) // before creating any PyQt object.
def printfunc(found_list):
for info in sorted(found_list):
print info
get_thread = scan_Thread(options.samp_rate, options.band, options.speed,
options.ppm, options.gain, options.args, printfunc)
get_thread.start()
window = Window()
window.show()
sys.exit(app.exec_())

pyside second qt gui doesn't show up

I have a Qapplication in my python script that gives a logingui to my chat server.
When the login is complete I want to call upon my chat gui. To achieve this I've used the following
code:
app = QApplication(sys.argv)
form = LoginWindow()
form.show()
app.exec_()
#login done
form = ChatWindow()
form.show()
app.exec_()
This worked when I fired it up with an "empty" gui of the chat. So only the necessary things in it for it to boot up. However when I start connecting signals and stuff the second window just doesn't show up anymore. The console prints a statement from the beginning of the init but after that it falls silent and no gui is present.
Does anyone know how I can fix this weird problem? How is switching a form supposed to be done?
The login window should be a subclass of QDialog, so that it can be run separately from the main application. A QDialog has its own event loop, and provides a return code that can be used to check which action was taken by the user.
So, given this, your code would become:
app = QApplication(sys.argv)
dialog = LoginWindow()
if dialog.exec_() == QDialog.Accepted:
window = ChatWindow()
window.show()
app.exec_()
else:
print('Login cancelled')

How to call multiple Dialogs in PyQt?

I have a main dialog and on that dialog there is a button. When the button is clicked, I want to open an another dialog.
Main Dialog code (Function which is called when the button is clicked in the main dialog):
def add_host(self):
x=add_host.Ui_Dialog1()
x.main()
default function:
if __name__ == "__main__":
import sys
global app
app = QtGui.QApplication(sys.argv)
Dialog = QtGui.QDialog()
ui = Ui_Dialog()
ui.setupUi(Dialog)
Dialog.show()
sys.exit(app.exec_())
Secondary dialog (add_host.py) code snapshot:
def main(self):
app1 = QtGui.QApplication(sys.argv)
Dialog1 = QtGui.QDialog()
ui1 = Ui_Dialog1()
ui1.setupUi1(Dialog1)
Dialog1.show()
sys.exit(app.exec_())
So when I run the code, it opens the secondary dialog but when I close it, it just freezes, and I get this error message:
File "testbot.py", line 175, in add_host
x.main()
File "/home/ppp/ppp/add_host.py", line 74, in main
sys.exit(app.exec_())
NameError: global name 'app' is not defined
Which does make sense, but I have no idea how to resolve it. I try several combinations without success, including adding and deleting app.exec_().
You cannot create multiple QApplications inside of the same script and thread. You are only supposed to have one...
This should be more like:
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
Dialog = QtGui.QDialog()
ui = Ui_Dialog()
ui.setupUi(Dialog)
Dialog.show()
sys.exit(app.exec_())
No global app. Although you should be doing your setupUI from within the class of your dialog.
Then when you want to show another dialog from with your app...say main(), you just create it and call show()
Here is a really basic example:
class Dialog(QDialog)
def __init__(self, parent):
super(Dialog, self).__init__(parent)
self.otherDialog = QDialog(parent=self)
self.otherDialog.show()
if __name__ == "__main__":
app = QApplication([])
dialog = Dialog()
dialog.show()
app.exec_()
You create a single QApplication and start its event loop by calling exec_(). From that point on, your main application is free to create more QWidgets. You never create another QApplication again at this point.
Also, I dont understand this part of your code:
def add_host(self):
x=add_host.Ui_Dialog1()
x.main()
The fact that you are calling a main() method on your UI object makes me think you are modifying the UI file and adding functionality to it, which you should not be doing. That ui file is subject to be overwritten every time you make changes in QT Designer and save out a new one. You should only be importing it and using its setupUI() method to apply it to your own custom classes.
A note about organization of your modules
When you are designing a PyQT application, you will always have a single entry point that will be called to start your app. This is the single and only location that should be creating your QApp and starting the event loop, and is usually done with in a if __name__ == "__main__" block to ensure its only done when its the main script. It should not be done within methods of your objects. For all your other modules where you define other Dialog, Widgets, etc, these should simply be classes that you import. As long as you have a running QApp, you are free to create and show these widgets.
Your code sample is a bit confusing - I don't understand why you have two mains, etc - anyway, maybe it's just a typo in add_host.py (app1.exec_() instead of app.exec_())
def main(self):
app1 = QtGui.QApplication(sys.argv)
...
sys.exit(app1.exec_())

Categories