I am trying to build an .app for my pyside gui and I'm having a problem when I use signals to communicate between the main thread and another one. When I run the python code, everything works fine. The problem only occurs when I use the .app built from pyinstaller.
The project is quite big, so it's hard to include any code, so i'll try to explain what I'm doing.
My second thread is used to call some functions from a dylib which controls our devices. The main thread uses a callback to tell the dylib to stop what it's doing. The dylib calls the callback function in a loop, and stops if the value returned from the main thread is different than 0.
As I mentioned, when I run the Python code (under Windows, Ubuntu, Mac), it works perfectly. But when using the .app built with pyinstaller, it looks like the signal is not sent from the main thread. While debugging, I printed stuff and eventually saw that the dylib doesn't receive the returned value from the callback. The dylib does use the callback, and I can see the expected data in the main thread.
So why would the signal not be fired with frozen python code?
Has anybody ever encounter a similar problem?
If you could give me some debugging advices, it would be really helpful?
EDIT
I managed to write a !little code which reproduces the problem. When I run the python code, pressing "ok" followed by "Cancel" sets the message "DLL got the value" in the widget. When freezing the code with Pyinstaller, under mac, the message never gets set on the widget. On Windows 7, everything is fine.
EDIT
Actually, no need for the shared library to cause the problem. The problem occurs only with the Python code.
Here is the Python code:
import sys
from PySide import QtGui
from PySide.QtCore import QThread, Signal, QObject
def functionInDLL(callback):
return_value = 0
while (return_value == 0):
return_value = callback(2)
class MyThread(QThread):
str_signal = Signal(str)
def __init__(self):
QThread.__init__(self)
self.return_value = 0
def returnValueToDll(self, data=0):
return self.return_value
def run(self):
if functionInDLL(self.returnValueToDll) == 42:
print"DLL got the value."
self.str_signal.emit("DLL got the value.")
else:
print"DLL did not get the value."
self.str_signal.emit("DLL did not get the value.")
self.exec_()
def setValueSentToDll(self, data):
self.return_value = data
class MainThreadClass(QObject):
int_signal = Signal(int)
def __init__(self):
super(MainThreadClass, self).__init__()
self.test_thread = MyThread()
self.int_signal.connect(self.test_thread.setValueSentToDll)
def startMyThread(self):
self.test_thread.start()
def sendStopValueToDLL(self):
self.int_signal.emit(42)
class MyGUI(QtGui.QWidget):
def __init__(self):
super(MyGUI, self).__init__()
self.text_information = QtGui.QLabel(text="No started yet...")
self.dll_information = QtGui.QLabel(text="")
self.ok_button = QtGui.QPushButton("OK")
self.cancel_button = QtGui.QPushButton("Cancel")
self.close_button = QtGui.QPushButton("Close")
label_hbox = QtGui.QHBoxLayout()
hbox = QtGui.QHBoxLayout()
hbox.addWidget(self.ok_button)
hbox.addWidget(self.cancel_button)
hbox.addWidget(self.close_button)
vbox = QtGui.QVBoxLayout()
label_hbox.addWidget(self.text_information)
label_hbox.addWidget(self.dll_information)
vbox.addLayout(label_hbox)
vbox.addLayout(hbox)
self.setLayout(vbox)
self.ok_button.clicked.connect(self.onOk)
self.cancel_button.clicked.connect(self.onCancel)
self.close_button.clicked.connect(self.onClose)
self.show()
self.main_thread = MainThreadClass()
self.main_thread.test_thread.str_signal.connect(lambda data: self.setDllStatusOnWidget(data))
def onOk(self):
self.main_thread.startMyThread()
self.text_information.setText("Started")
self.dll_information.setText("")
def onCancel(self):
self.main_thread.sendStopValueToDLL()
self.text_information.setText("Canceled")
def onClose(self):
self.main_thread.test_thread.exit()
self.close()
def setDllStatusOnWidget(self, text=""):
self.dll_information.setText(text)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
ex = MyGUI()
sys.exit(app.exec_())
Thanks
Using Python2.7, Mac Pro Yosemite
Thanks to codewarrior, my bug was solved.
When I replaced the line:
self.main_thread.test_thread.str_signal.connect(lambda data: self.setDllStatusOnWidget(data))
by this line, removing the lambda:
self.main_thread.test_thread.str_signal.connect(self.setDllStatusOnWidget)
See the link for more details. I hope this can help other people.
pyinstaller_issue
Related
I am currently trying to implement some threading functionality in my PySide6 GUI application. I followed a tutorial to try to get started (link is here), and I cannot seem to get it to work. Although that tutorial uses PyQt not PySide, the classes and structure is still similar, and it does seem to launch on another thread. Still though, it freezes the main GUI, which is not desired when this actually faces users.
Here is a sample of my code:
class Worker(QObject):
finished = Signal(str)
progress = Signal(int)
def run(self, file):
"""Long-running task." that calls a separate class for computation""
b = SeparateClass()
b.doComputation()
self.finished.emit()
class DataPlotting(QMainWindow):
def __init__(self):
self.thread = QThread()
self.worker = Worker()
self.report_builder = QPushButton('Call class that threads')
self.report_builder.setEnabled(False)
self.report_builder.clicked.connect(self.qthread_test)
def qthread_test(self):
file = 'some_file.txt'
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.run(file))
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.thread.start()
return
This does accomplish the work that is in the Worker class and spit out the desired results, but it freezes the GUI. I am not really sure what I am doing wrong, as this approach is what has been suggested to prevent freezing GUIs for heavy computation.
Is there something that I am straight up missing? Or am I going about this the wrong way? Any help or guidance is appreciated
I am assuming that you make the appropriate calls to the super class during __init__ for your subclasses of QMainWindow and the QObject.
When your code executes self.thread.started.connect(self.worker.run(file)) that line it runs the function self.worker.run(file) immediately and assigns the result of that function, which is None, as the connected slot to the thread.started signal. Instead of passing the file path as a parameter you can assign it to the worker instance and have the run method grab the path from self during execution.
For example you can try something like this:
class Worker(QObject):
finished = Signal(str)
progress = Signal(int)
def run(self):
"""Long-running task." that calls a separate class for computation"""
file = self.some_file
b = SeparateClass()
b.doComputation()
self.finished.emit()
class DataPlotting(QMainWindow):
def __init__(self):
self.report_builder = QPushButton('Call class that threads')
self.report_builder.setEnabled(False)
self.report_builder.clicked.connect(self.qthread_test)
self.threads = []
def qthread_test(self):
worker = Worker()
thread = QThread()
worker.some_file = 'some_file.txt'
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.finished.connect(thread.quit)
worker.finished.connect(worker.deleteLater)
thread.finished.connect(thread.deleteLater)
thread.start()
self.threads.append(thread)
return
I'm trying this simple thread with a while loop inside. When I'm inside the while loop, Ctrl+C has no effect in stopping my program. Once I go do something else after the while loop, the script stops as intended. What can I do so my script can be gracefully killed both while being in the while loop and after? (Edit: This seems to be a problem exclusive to Windows, iOS and Ubuntu seem to do what I want)
import time, threading
class MainClass(threading.Thread):
def __init__(self):
super().__init__()
def run(self):
while True:
time.sleep(1)
print("Looping")
# Script entry point
if __name__ == '__main__':
a = MainClass()
a.daemon = True
a.start()
a.join()
This is a known issue, explained in Issue 35935.
A way to solve it is to revert to the default kernel behaviour of SIGINT using signal.signal(signal.SIGINT, signal.SIG_DFL), (solution pointed out by the issue OP). As to why this has to be the case is beyond the scope of my knowledge.
This works on Windows using Python 3.8+.
Implementation:
import time, threading
import signal
class MainClass(threading.Thread):
def __init__(self):
super().__init__()
def run(self):
while True:
time.sleep(1)
print("Looping")
# Script entry point
if __name__ == '__main__':
a = MainClass()
a.daemon=True
signal.signal(signal.SIGINT, signal.SIG_DFL)
a.start()
a.join()
I wrote simple program which has pyQt interface with 2 buttons (start and cancel). Start button runs some calculations in the background (by starting update function) and thanks to threading I can still use UI.
But the application crashes after 10sec - 2 minutes. UI just dissapears, program shutdown.
when I use pythonw to run app without console thread crashes after ~25 sec but gui still works.
#!/usr/bin/python
import threading
import sys
from PyQt4 import QtGui, QtCore
import time
import os
class Class(QtGui.QWidget):
def __init__(self):
#Some init variables
self.initUI()
def initUI(self):
#some UI
self.show()
def update(self,stop_event):
while True and not stop_event.isSet():
self.updateSpeed()
self.updateDistance()
self.printLogs()
self.saveCSV()
self.guiUpdate()
time.sleep(1)
#gui button function
def initiate(self):
self.stop_event = threading.Event()
self.c_thread = threading.Thread(target = self.update, args=(self.stop_event,))
self.c_thread.start()
#Also gui button function
def cancelTracking(self):
self.stop_event.set()
self.close()
def main():
app = QtGui.QApplication(sys.argv)
ex = Class()
sys.exit(app.exec_())
ex.update()
if __name__ == '__main__':
main()
I dont know if I'm doing threading right. I found example like this on stack. I'm quite new to python and I'm using threading for the first time.
It is most likely due to calling a GUI function in your separate thread. PyQt GUI calls like setText() on a QLineEdit are not allowed from a thread. Anything that has PyQt painting outside of the main thread will not work. One way to get around this is to have your thread emit a signal to update the GUI when data is ready. The other way is to have a timer periodically checking for new data and updating the paintEvent after a certain time.
========== EDIT ==========
To Fix this issue I created a library named qt_thread_updater. https://github.com/justengel/qt_thread_updater This works by continuously running a QTimer. When you call call_latest the QTimer will run the function in the main thread.
from qt_thread_updater import get_updater
lbl = QtWidgets.QLabel('Value: 1')
counter = {'a': 1}
def run_thread():
while True:
text = 'Value: {}'.format(counter['a'])
get_updater().call_latest(lbl.setText, text)
counter['a'] += 1
time.sleep(0.1)
th = threading.Thread(target=run_thread)
th.start()
========== END EDIT ==========
#!/usr/bin/python
import threading
import sys
from PyQt4 import QtGui, QtCore
import time
import os
class Class(QtGui.QWidget):
display_update = QtCore.pyqtSignal() # ADDED
def __init__(self):
#Some init variables
self.initUI()
def initUI(self):
#some UI
self.display_update.connect(self.guiUpdate) # ADDED
self.show()
def update(self):
while True and not self.stop_event.isSet():
self.updateSpeed()
self.updateDistance()
self.printLogs()
self.saveCSV()
# self.guiUpdate()
self.display_update.emit() # ADDED
time.sleep(1)
#gui button function
def initiate(self):
self.stop_event = threading.Event()
self.c_thread = threading.Thread(target = self.update)
self.c_thread.start()
#Also gui button function
def cancelTracking(self):
self.stop_event.set()
self.close()
def main():
app = QtGui.QApplication(sys.argv)
ex = Class()
sys.exit(app.exec_())
# ex.update() # - this does nothing
if __name__ == '__main__':
main()
The other thing that could be happening is deadlock from two threads trying to access the same variable. I've read that this shouldn't be possible in python, but I have experienced it from the combination of PySide and other Python C extension libraries.
May also want to join the thread on close or use the QtGui.QApplication.aboutToQuit signal to join the thread before the program closes.
The Qt documentation for QThreads provides two popular patterns for using threading. You can either subclass QThread (the old way), or you can use the Worker Model, where you create a custom QObject with your worker functions and run them in a separate QThread.
In either case, you can't directly update the GUI from the background thread, so in your update function, the guiUpdate call will most likely crash Qt if it tries to change any of the GUI elements.
The proper way to run background processes is to use one of the two QThread patterns and communicate with the main GUI thread via Signals and Slots.
Also, in the following bit of code,
app = QtGui.QApplication(sys.argv)
ex = Class()
sys.exit(app.exec_())
ex.update()
app.exec_ starts the event loop and will block until Qt exits. Python won't run the ex.update() command until Qt has exited and the ex window has already been deleted, so you should just delete that command.
I am working on adding a printer interface to some home-brewed Python3 code with a Gtk3 UI, using (mostly) Eclipse Indigo with the PyDev plugin.
While developing the PrintOperation callbacks I found a problem where apparently the gi-introspection fails to find the right underlying library struct for the Cairo Context. The error reported in the console is:
Traceback (most recent call last):
File "/home/bob/Projects/MovieList/src/MovieList/MovieListIO.py", line 203, in on_printDialog_draw_page
cr = context.get_cairo_context()
File "/usr/lib/python3/dist-packages/gi/types.py", line 43, in function
return info.invoke(*args, **kwargs)
TypeError: Couldn't find conversion for foreign struct 'cairo.Context'
At first I thought this was something to do with Eclipse and/or PyDev, because I could run the program within Idle without any error messages. But then I found that when the program was packaged for deployment with the built-in command-line Python tools, the installed version also gave the error. So, I wrote a couple of test scripts abstracting the printer functionality to try to isolate what was going on. In both cases, the key line is in the on_printOperation_draw_page() callback (marked with comments).
Here is the first test script (Script 1, printTestPdf.py), which loads a pdf file using Poppler, and prints it using the system print dialog:
#!/usr/bin/env python3
import os
from gi.repository import Gtk, Poppler
testFile = 'file://' + os.path.join(os.getcwd(), 'printTestPdf.pdf')
pdfDocument = Poppler.Document.new_from_file(testFile, None)
class Example(Gtk.Window):
def __init__(self):
super(Example, self).__init__()
self.init_ui()
def init_ui(self):
self.set_title("Print Pdf Test")
self.resize(230, 150)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("delete-event", Gtk.main_quit)
printButton = Gtk.Button('Press Me')
self.add(printButton)
printButton.connect('clicked', self.on_printButton_clicked)
self.show_all()
def on_printButton_clicked(self, widget):
"""
Handler for the button click.
"""
printOperation = Gtk.PrintOperation()
printOperation.connect('draw-page', self.on_printOperation_draw_page)
printOperation.set_job_name('Print Pdf Test')
printOperation.set_n_pages(pdfDocument.get_n_pages())
printOperation.run(Gtk.PrintOperationAction.PRINT_DIALOG,
parent=self)
def on_printOperation_draw_page(self, printOperation, context, pageNo):
"""
Handler for the draw-page signal from the printOperation.
"""
cr = context.get_cairo_context() # <-- THIS IS THE LINE
page = pdfDocument.get_page(pageNo)
page.render_for_printing(cr)
def main():
app = Example()
Gtk.main()
if __name__ == "__main__":
main()
This is the second script (Script 2, printTestHtml.py), which is almost identical, except it loads an HTML file for printing using weasyprint:
#!/usr/bin/env python3
import os
from gi.repository import Gtk
from weasyprint import HTML
testFile = os.path.join(os.getcwd(), 'printTestHtml.html')
pdfDocument = HTML(filename=testFile).render()
class Example(Gtk.Window):
def __init__(self):
super(Example, self).__init__()
self.init_ui()
def init_ui(self):
self.set_title("Print Html Test")
self.resize(230, 150)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("delete-event", Gtk.main_quit)
printButton = Gtk.Button('Press Me')
self.add(printButton)
printButton.connect('clicked', self.on_printButton_clicked)
self.show_all()
def on_printButton_clicked(self, widget):
"""
Handler for the button click.
"""
printOperation = Gtk.PrintOperation()
printOperation.connect('begin-print', self.on_printOperation_begin_print)
printOperation.connect('draw-page', self.on_printOperation_draw_page)
printOperation.set_job_name('Print HTML Test')
printOperation.set_n_pages(len(pdfDocument.pages))
printOperation.run(Gtk.PrintOperationAction.PRINT_DIALOG,
parent=self)
def on_printOperation_draw_page(self, printOperation, context, pageNo):
"""
Handler for the draw-page signal from the printOperation.
"""
cr = context.get_cairo_context() # <-- THIS IS THE LINE
page = pdfDocument.pages[pageNo]
page.paint(cr) # <-- there is a separate issue here
def main():
app = Example()
Gtk.main()
if __name__ == "__main__":
main()
Both scripts generate an internal pdf document, which is used to render each page on request via the PrintOperation draw_page callback.
Now, whether and how the scripts succeed or fail depends on the context in which they are run. Script 1 always works, except if it is run after a failure of Script 2 in Idle. Script 2 always generates the error message as reported above when run in Eclipse. In Idle, Script 2's behaviour is complex. Sometimes it fails due to a second problem (marked), and does not exhibit the first failure. However, for reasons I have yet to establish, every so often it generates the original error, and when it does, it keeps doing it and Script 1 show the error too, until Idle is re-booted. Running directly from the command line matches the behaviour in Eclipse. I have tried to summarise this behaviour below:
* Eclipse
- Script 1: Always OK
- Script 2: Always Fails
* Command line
- Script 1: Always OK
- Script 2: Always Fails
* Idle
- Script 1: OK, except after failure of Script 2
- Script 2: Intermittent Fail. Knock-on to future runs (up to next error)
This pattern of failure may help determine what the root problem is, but it is beyond me to understand it.
Ignoring the bizarre behaviour in Idle, it is possible the difference between Script 1 and Script 2 holds a clue to my original problem. Why does Script 1 run successfully, while Script 2 generates the introspection error?
If you can offer any suggestions as to what is going wrong I would be most grateful. If you can come up with a solution I will be delighted!
In view of the lack of response I have come up with the following workaround, which uses WebKit instead of weasyprint to do the parsing, rendering and administration of the printing from html:
#!/usr/bin/env python3
import os
from gi.repository import Gtk, WebKit
testFile = 'file://' + os.path.join(os.getcwd(), 'printTestHtml.html')
class Example(Gtk.Window):
def __init__(self):
super(Example, self).__init__()
self.init_ui()
def init_ui(self):
self.set_title("Print Html WebKit Test")
self.resize(230, 150)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("delete-event", Gtk.main_quit)
printButton = Gtk.Button('Press Me')
self.add(printButton)
printButton.connect('clicked', self.on_printButton_clicked)
self.show_all()
def on_printButton_clicked(self, widget):
webView = WebKit.WebView()
webView.load_uri(testFile)
webFrame = webView.get_main_frame()
webFrame.print_full(Gtk.PrintOperation(),
Gtk.PrintOperationAction.PRINT_DIALOG)
def main():
app = Example()
Gtk.main()
if __name__ == "__main__":
main()
What I think is going on is that somehow weasyprint interferes with the introspection process. I have raised this as a bug on the weasyprint home page on github.
Just in case it helps I was looking for a solution to the cairo error.
Had this cairo error happen on my RPi3 with Pandas and Matplotlib. The plot window was showing up blank.
I had to run sudo apt-get install python-gobject-cairo
Based on this: https://github.com/rbgirshick/py-faster-rcnn/issues/221
Well, I am familiar with Qt, but when using PyQt, the syntax of signal/slot really confused me.
When using C++/Qt, the compiler will give you a hint where you are wrong about the signal/slot, but the PyQt default configuration doesn't give a hint about error. Is there a ways or such as debug trigger mode to enable PyQt to display more information?
the Code is as following:
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import time
class workThread(QThread):
def __init__(self,parent = None):
super(workThread,self).__init__(parent)
self.mWorkDoneSignal = pyqtSignal() ## some people say this should be defined as clas member, however, I defined it as class member and still fails.
def run(self):
print "workThread start"
time.sleep(1)
print "workThread stop"
print self.emit(SIGNAL("mWorkDoneSignal"))
class MainWidget(QWidget):
def __init__(self , parent = None):
super(MainWidget,self).__init__(parent)
#pyqtSlot()
def display(self):
print "dispaly"
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
c = workThread()
d = MainWidget()
##In Qt, when using QObject::connect or such things, the return value will show the
## signal/slot binding is success or failed
print QObject.connect(c,SIGNAL('mWorkDoneSignal()'),d,SLOT('display()'))
c.start()
d.show()
app.exec_()
In C++,the QObject::connect return value will show the signal/slot binding is success or not. In PyQt, the return value is True, but it doesn't trigger the slot.
My Question:
1) Is the signal shoud be a class member or instance member?
2) If QObject.connect 's return value can't give the hint of the binding is success or not, is there other ways to detect it?
I want to bind the signal/slot outside the signal sender and slot receiver, so I perfer to use the QObject.connect ways. But how can I write this correct, I tried the following ways,both fail.
QObject.connect(c,SIGNAL('mWorkDoneSignal'),d,SLOT('display'))
QObject.connect(c,SIGNAL('mWorkDoneSignal()'),d,SLOT('display()'))
First, you should really use new style signals with pyqt. In fact, QObject.connect and QObject.emit will not even be there anymore in PyQt5.
def __init__(self,parent = None):
super(workThread,self).__init__(parent)
self.mWorkDoneSignal = pyqtSignal()
This creates an unbound signal and assigns it to a instance variable mWorkDoneSignal, wich dosn't really have an effect. If you want to create an signal, then you really have to declare it on the class.
So if you didn't really create a signal here, then why did this call succeed:
QObject.connect(c,SIGNAL('mWorkDoneSignal()'),d,SLOT('display()'))
The answer lies in the handling of old style signals by PyQt4:
The act of emitting a PyQt4 signal implicitly defines it.
For that reason when you connect a signal to a slot, only the existence of the slot is checked. The signal itself doesn't really need to exist at that point, so the call will always succeed unless the slot doesn't exist.
I tried the following ways,both fail.
QObject.connect(c,SIGNAL('mWorkDoneSignal'),d,SLOT('display'))
QObject.connect(c,SIGNAL('mWorkDoneSignal()'),d,SLOT('display()'))
The first fails because display (without parenthesis) isn't a valid slot.
The second succeeds. The reason it doesn't work is because you emit mWorkDoneSignal, but what you actually need to emit is:
self.emit(SIGNAL("mWorkDoneSignal()"))
Using new style signals, there's no way to mess things like this up:
from utils import sigint
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import time
class workThread(QThread):
mWorkDoneSignal = pyqtSignal()
def __init__(self,parent = None):
super(workThread,self).__init__(parent)
def run(self):
print "workThread start"
time.sleep(1)
print "workThread stop"
self.mWorkDoneSignal.emit()
class MainWidget(QWidget):
def __init__(self , parent = None):
super(MainWidget,self).__init__(parent)
#pyqtSlot()
def display(self):
print "dispaly"
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
c = workThread()
d = MainWidget()
c.mWorkDoneSignal.connect(d.display)
c.start()
d.show()
app.exec_()