Identifying why a Python Qt program pauses when not visible on macOS - python

I have a Qt Python program that logs data over a serial port. I'd like this program to always log data while running even when the application is not visible. Currently, when the application is not visible, the logging will pause after about ~45 seconds. Once the application window becomes visible again, logging resumes. The logging portion of the code is in a second thread using QRunnable and QThreadPool.
I've tried searching for the cause (or solution), but have not had much luck. Part of my problem is that I'm not sure if this issue is related to the OS, IDE, language, etc.
High-level details:
OS: macOS 12.4
IDE: vscode
Language/frameworks: Python3 / Qt (pyside6)
Does anyone have any ideas on why this application/thread might be pausing? Is it possible to have the application to continue to log data even when it is not visible? My hope is that once I'm pointed in the right direction I'll be able to address the issue.
UPDATE
Example code
class LogSignals(QObject):
result = Signal(dict)
class LogWorker(QRunnable):
def __init__():
super().__init__()
self.signals = LogSignals()
def run(self):
try:
for i in range(N_LOG_SAMPLES):
result = self.getSerialData()
self.signals.result.emit(result)
except:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.threadpool = QThreadPool()
def startLog(self):
log_worker = LogWorker()
log_worker.signals.result.connect(self.updateLogData)
self.threadpool.start(log_worker)
#Slot()
def updateLogData(self, result: dict):
#save data
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

You would need to check if your application is active, and start or drop the logging accordingly. You can get this by using:
QWidget *QApplication::activeWindow()
This would return a nullptr if your application does not have an active window. So, you would write something like this:
if (application->activeWindow())
; // Start / Keep logging
else
; // Stop logging

Related

How to use a timer or equivalent within a python thread without blocking the GUI

I am a writing a GUI based application using the PyQT framework which connects to a device, sends commands, reads the corresponding data and displays this in real time to a table, graph widget and to a file.
Once the run button is clicked it starts a thread which sends the external device commands according to a procedure table and emits signals with the data to various methods to change the GUI.
When the run button is clicked it executes the following lines:
worker = Worker(self.runProcedure)
worker.signals.updateResults.connect(self.updateResultsTable)
worker.signals.writeResults.connect(self.writeResultsFile)
worker.signals.finished.connect(self.procedure_complete)
self.threadpool.start(worker)
within the runProcedure commands are sent to the device from the procedure table and the data read from the device is put into a list 'hfData' using code similar to that listed below:
while float(currentForceReading) <= float(target) and stopAlarm == 0:
ts = dt.now().timestamp()
hfData = (connection.readline()).split()
updateResults_callback.emit(ts, hfData) #method to update results table and graphs
writeResults_callback.emit(ts, hfData) #method to write results to a file
One of the options in the application is to hold for a period of time where it continues collecting data from the device without sending new commands.
I am looking for a way to continue taking measurements whilst in this hold time and continue updating the GUI.
I have tried to implement the following code, however this while loop blocks the GUI from updating:
stepHoldTime = float(procedureModel.data(procedureModel.index(row,4), Qt.DisplayRole))
if(stepHoldTime != 0):
endTime = time.monotonic() + stepHoldTime
while(time.monotonic() < endTime):
ts = dt.now().timestamp()
hfData = (connection.readline()).split()
updateResults_callback.emit(ts,hfData)
Is there a correct way to implement this functionality?
Instead of while-loop you could run QTimer which will execute code every few milliseconds.
It is minimal example which shows how to run function every 1000ms and update time in label.
from PyQt5.QtWidgets import QApplication, QLabel
from PyQt5.QtCore import QTimer, QDateTime
class Window(QLabel):
def __init__(self, parent=None):
super().__init__(parent)
self.showTime()
help(QTimer)
self.timer = QTimer()
self.timer.timeout.connect(self.showTime)
self.timer.start(1000)
def showTime(self):
text = QDateTime.currentDateTime().toString('yyyy-MM-dd hh:mm:ss')
self.setText(text)
if __name__ == '__main__':
app = QApplication([])
win = Window()
win.show()
app.exec()
But your problem can be more complex and it may need more complex solution. You may have to show minimal working code which we could run and see problem - and test some ideas to resolve problem.
You can start your "reader" from a separate thread
class YourUi(QtWidgets.QMainWindow):
update_data = QtCore.pyqtSignal(str)
def __init__(self):
super(YourUi, self).__init__()
self.update_data.connect(self.do_update_data)
t = threading.Thread(target=self.update_worker, args=(self.update_data,), daemon=True)
t.start()
#staticmethod
def update_worker(signal):
connection = create_conncetion()
while True:
hfData = (connection.readline()).split()
signal.emit(hfData)
time.sleep(0.1)
def do_update_data(self, hf_data):
# access GUI elemetns from main thread to prevent freezing
self.some_qt_label.setText(str(hf_data))

Python crashes when running a log streamer using Qt

Goal
I have a process that logs on a file (realtime.log) while running and I want to print every new line of that file in my application in realtime. In other words I want to redirect the output from the process to the GUI. This means that I have two different processes running: the "engine" and the GUI.
I have already achieved this by using Tkinter but since I have to make a more complex, professional and good looking GUI I decided to switch to Qt for Python (PySide2).
Problem
Python often crashes when I launch the GUI with the error message: Python has stopped working. The window starts printing the lines and at some point it stops working.
After many attempts and searches I got to a point where the program only crashes if I click on the GUI window. Moreover, the program doesn't crash suddenly but it crashes at the end of the engine's execution.
Environment
Windows 10
Python 3.6.5
PySide2 5.12.6
Code
Note that this is a simplified version.
datalog_path = "realtime.log"
def get_array_from_file(file_path):
try:
with open(file_path, 'r') as file:
lines = file.readlines()
return lines
except:
print('error in file access')
class Streamer(QRunnable):
def __init__(self, stream, old_array, edit):
super().__init__()
self.stream = stream
self.old_array = old_array
self.edit = edit
def run(self):
try:
while self.stream:
array_file = get_array_from_file(datalog_path)
if len(array_file) != len(self.old_array):
for line in array_file[len(self.old_array) - 1:len(array_file)]:
self.edit.append(line)
# print(line)
self.old_array.append(line)
except:
print('problem in streaming funct')
class Window(QMainWindow):
def __init__(self):
super().__init__()
layout = QVBoxLayout()
self.setWindowTitle("DATALOG")
self.thread_pool = QThreadPool()
self.edit = QTextEdit()
self.stream = True
self.old_array = get_array_from_file(datalog_path)
self.streamer = Streamer(self.stream, self.old_array, self.edit)
self.thread_pool.start(self.streamer)
window = QWidget()
layout.addWidget(self.edit)
window.setLayout(layout)
self.setCentralWidget(window)
def closeEvent(self, event):
self.stream = False
event.accept()
if __name__ == '__main__':
app = QApplication(sys.argv)
win = Window()
win.show()
app.exec_()
The #hyde answer points out explains the reason for the problem but its solution is not applicable in PySide2 (in PyQt5 a small modification would have to be made, see this), an alternative is to create a QObject that has the signals:
class Signaller(QtCore.QObject):
textChanged = Signal(str)
class Streamer(QRunnable):
def __init__(self, stream, old_array):
super().__init__()
self.stream = stream
self.old_array = old_array
self.signaller = Signaller()
def run(self):
try:
while self.stream:
array_file = get_array_from_file(datalog_path)
if len(array_file) != len(self.old_array):
for line in array_file[len(self.old_array) - 1:len(array_file)]:
self.signaller.textChanged.emit(line)
# print(line)
self.old_array.append(line)
except:
print('problem in streaming funct')
self.stream = True
self.old_array = get_array_from_file(datalog_path)
self.streamer = Streamer(self.stream, self.old_array)
self.streamer.signaller.textChanged.connect(self.edit.append)
self.thread_pool.start(self.streamer)
While I'm not too familiar with Python Qt, issue is probably, that you use a GUI object edit from a different thread. This is not allowed, the GUI part must all run in the same (main) thread!
To fix this, you need to have some other way for the thread to communicate UI changes. Since your QRunnable is not a QObject, you can't just emit a signal, but you can use QMetaObject::invokeMethod on it's invokable methods. Please let me know if this works directly:
# self.edit.append(line) # can't do this from a thread!
# instead, invoke append through GUI thread event loop
QtCore.QMetaObject.invokeMethod(self.edit,
'append',
QtCore.Qt.QueuedConnection,
QtCore.QGenericArgument('QString', line)

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

UI made in QT Designer shifts behind Title Bar [duplicate]

I'm trying to create an application that contains a web browser within it, but when I add the web browser my menu bar visually disappears but functionally remains in place. The following are two images, one showing the "self.centralWidget(self.web_widget)" commented out, and the other allows that line to run. If you run the example code, you will also see that while visually the entire web page appears as if the menu bar wasn't present, you have to click slightly below each entry field and button in order to activate it, behaving as if the menu bar was in fact present.
Web Widget Commented Out
Web Widget Active
Example Code
import os
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtWebEngineWidgets import *
class WebPage(QWebEngineView):
def __init__(self, parent=None):
QWebEngineView.__init__(self)
self.current_url = ''
self.load(QUrl("https://facebook.com"))
self.loadFinished.connect(self._on_load_finished)
def _on_load_finished(self):
print("Url Loaded")
class MainWindow(QMainWindow):
def __init__(self, parent=None):
# Initialize the Main Window
super(MainWindow, self).__init__(parent)
self.create_menu()
self.add_web_widet()
self.show()
def create_menu(self):
''' Creates the Main Menu '''
self.main_menu = self.menuBar()
self.main_menu_actions = {}
self.file_menu = self.main_menu.addMenu("Example File Menu")
self.file_menu.addAction(QAction("Testing Testing", self))
def add_web_widet(self):
self.web_widget = WebPage(self)
self.setCentralWidget(self.web_widget)
if __name__ == "__main__":
app = QApplication(sys.argv)
main_window = MainWindow()
main_window.showMaximized()
sys.exit(app.exec_()) # only need one app, one running event loop
Development Environment
Windows 10, PyQt5, pyqt5-5.9
EDIT
The problem doesn't seem to be directly related to the menu bar. Even removing the menu bar the issue still occurs. That said, changing from showMaximized() to showFullScreen() does seem to solve the problem.
I no longer believe this is an issue with PyQt5 specifically but rather a problem with the graphics driver. Specifically, if you look at Atlassian's HipChat application it has a similar problem which is documented here:
https://jira.atlassian.com/browse/HCPUB-3177
Some individuals were able to solve the problem by running the application from the command prompt with the addendum "--disable-gpu" but that didn't work for my python application. On the other hand, rolling back the Intel(R) HD Graphics Driver did solve my problem. Version 21.20.16.4627 is the one that seems to be causing problems.

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_())

Categories