Well I was scripting a software designed via Python which I'll be using signals and slots too much in an PyQt5 application. I got an idea of creating a dictionary where all signals come in and each signal will have its own key in order to access (or basically to connect it to a function). The problem is that I get this error 'AttributeError: 'PyQt5.QtCore.pyqtSignal' object has no attribute 'connect' for some reason. I read about this error and found out that I have to declare the signals outside the constructor to get it working but unfortunately that will break my idea so that, I came here so somebody can solve my issue.
Here is the code if you still don't understand:
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QMainWindow
from PyQt5.QtCore import QRunnable, pyqtSlot, QThreadPool, QObject, pyqtSignal
class WorkerSignals(QObject):
signals = {}
def __init__(self, **kwargs):
QObject.__init__(self)
if (kwargs is not None):
for key, value in kwargs.items():
self.signals[key] = value
class Worker(QRunnable):
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
self.fn = fn
self.args = args
self.kwargs = kwargs
#pyqtSlot()
def run(self):
self.fn(*self.args, **self.kwargs)
And an example of creating signals:
worker_signals = WorkerSignals(result=pyqtSignal(str), error=pyqtSignal(str))
worker_signals.signals['result'].connect(self.on_receive_result)
worker_signals.signals['error'].connect(self.on_receive_error)
As indicated in the docs:
A signal (specifically an unbound signal) is a class attribute. When a
signal is referenced as an attribute of an instance of the class then
PyQt5 automatically binds the instance to the signal in order to
create a bound signal. [...]
So it is not only necessary that is it declared outside the constructor but it must be a static attribute since it serves as a prototype to create the signals that belong to the instance. A possible solution is to use type to create dynamic classes:
from PyQt5 import QtCore
d = {
"result": QtCore.pyqtSignal(str),
"error": QtCore.pyqtSignal(str)
}
WorkerSignals = type("WorkerSignals", (QtCore.QObject,), d)
if __name__ == '__main__':
import sys
app = QtCore.QCoreApplication(sys.argv)
worker_signals = WorkerSignals()
def on_result(text):
print("result:", text)
def on_error(text):
print("error:", text)
worker_signals.result.connect(on_result)
worker_signals.error.connect(on_error)
def emit_result():
worker_signals.result.emit(" 1+1=2 ")
def emit_error():
worker_signals.error.emit(" :( ")
QtCore.QTimer.singleShot(1000, emit_result)
QtCore.QTimer.singleShot(2000, emit_error)
QtCore.QTimer.singleShot(3000, app.quit)
sys.exit(app.exec_())
Related
I have a decorator that is supposed to try / except any method given to the wrapper so that it can log on the GUI any errors encountered in order to give the user a chance to understand what is going on if the application is not working
The decorator is used on multiple methods of different classes and encapsulates slots and is therefore declared outside of the classes ( outside the only class in this simplified example)
Currently the following code does not work as there is never the right amount of arguments given ( always one too many)
I tried different ways of writing the decorator but my understanding of them is limited and it turned out into a guesswork, witch also means that even if I stumbled onto the solution by chance I would not know why it works and not all of the previous errors
Here is a simplified code that illustrates my issue, there are different spin boxes that connect to different slots each taking a different amount of arguments and the same decorator is used on all of them
import sys
from functools import partial
from PyQt5.QtWidgets import QSpinBox, QWidget, QHBoxLayout, QApplication
def except_hook(cls, exception, error_traceback):
sys.__excepthook__(cls, exception, error_traceback)
def my_decorator(method_reference):
def wrapper(*args, **kwargs):
try:
return method_reference(*args, **kwargs)
except Exception as e:
print("saved from " + str(e))
return wrapper
class Foo(QWidget):
def __init__(self):
super(Foo, self).__init__()
layout = QHBoxLayout()
sb1 = QSpinBox()
sb2 = QSpinBox()
sb3 = QSpinBox()
sb1.valueChanged.connect(partial(self.__pressed, sb1))
sb2.valueChanged.connect(partial(self.__pressed_two, sb2, "arg1"))
sb3.valueChanged.connect(partial(self.__pressed_three, sb3, "arg1", "arg2"))
layout.addWidget(sb1, 0)
layout.addWidget(sb2, 1)
layout.addWidget(sb3, 2)
self.setLayout(layout)
#my_decorator
def __pressed(self, spin_box):
print("spin box now has a value of " + str(spin_box.value))
#my_decorator
def __pressed_two(self, spin_box, arg1):
print("spin box now has a value of " + str(spin_box.value))
print("this is arg 1 : " + arg1)
#my_decorator
def __pressed_three(self, spin_box, arg1, arg2):
print("spin box now has a value of " + str(spin_box.value))
print("this is arg 1 : " + arg1)
print("this is arg 2 : " + arg2)
sys.excepthook = except_hook # by default QT will hide all errors behind a code, this is used to expose the issue
app = QApplication(sys.argv)
foo = Foo()
foo.show()
sys.exit(app.exec_())
Could it be possible to point out why my solution is not working? I did try to find examples and explanations and am successful in making decorators that would work for methods that are not used as pyqt slots but I cannot work this one out.
The issue doesn't seem to be with your decorator, but rather with connecting a method to the valueChanged signal. Since this signal emits data (the new value being set), you need to add an additional argument to the __pressed method that represent this data.
This is a simplified version of your code - you can see that the exception is properly handled by the decorator when the value in the spinbox gets too high.
import sys
from PyQt5.QtWidgets import QSpinBox, QWidget, QHBoxLayout, QApplication
def my_decorator(method_reference):
def wrapper(*args, **kwargs):
try:
return method_reference(*args, **kwargs)
except Exception as e:
print("saved from " + str(e))
return wrapper
class Foo(QWidget):
def __init__(self):
super(Foo, self).__init__()
layout = QHBoxLayout()
self.setLayout(layout)
self.spinbox = QSpinBox()
self.spinbox.valueChanged.connect(self.pressed)
layout.addWidget(self.spinbox)
#my_decorator
def pressed(self, new_value):
print(f"spin box now has a value of {new_value}")
if new_value > 5:
raise ValueError(f"Value {new_value} is too high!")
if __name__ == '__main__':
app = QApplication(sys.argv)
foo = Foo()
foo.show()
sys.exit(app.exec_())
Now, in my experience with PyQt, you don't always need to add the extra argument representing the data being sent by the signal. But in this case I'm assuming it is (somehow) mandatory due to the wrapping of the slot method. People more experienced with the library are free to correct me :-)
I am using PYQT5 to build a GUI and I am using APScheduler to manage the jobs I would like to run. I have the scheduler items and timer items broken into there own classes and then connecting them in the main file.
The issue I am having is once the timer finishes one cycle, I try to add time to the Timer class and start it over for the next count down before the scheduler is supposed to run again. I am getting two errors or warnings and I am not sure how to fix them. They are:
QObject::killTimer: Timers cannot be stopped from another thread
QObject::startTimer: Timers cannot be started from another thread
Once these are thrown the GUI updates but no longer counts down. I will attach the simplest version that I have found that will reproduce the errors. Any help is greatly appreciated and thank you for your time.
Main.py
import sys
from PyQt5 import QtCore
from PyQt5 import QtWidgets
from Timer import Timer
from Schedule import Scheduler
import datetime
class MyMainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)
vbox = QtWidgets.QVBoxLayout()
central_widget.setLayout(vbox)
self.start_pushButton = QtWidgets.QPushButton()
self.start_pushButton.setText("Start")
self.start_pushButton.clicked.connect(self.start_schedule)
vbox.addWidget(self.start_pushButton)
self.pages_qsw = QtWidgets.QStackedWidget()
vbox.addWidget(self.pages_qsw)
self.time_passed_qll = QtWidgets.QLabel()
vbox.addWidget(self.time_passed_qll)
self.my_timer = Timer()
self.my_timer.get_seconds.connect(self.update_gui)
self.sch = Scheduler()
def start_schedule(self):
self.sch.add(self.hello)
self.sch.start()
self.start_my_timer()
def start_my_timer(self):
next_run = self.sch.next_occurance().replace(tzinfo=None) # This removes the time zone.
a = datetime.datetime.now()
difference = next_run - a
self.my_timer.addSecs(difference.seconds)
self.my_timer.timer_start()
def hello(self):
print("hello world")
self.start_my_timer()
#QtCore.pyqtSlot(str)
def update_gui(self,seconds):
self.time_passed_qll.setText(str(seconds))
app = QtWidgets.QApplication(sys.argv)
main_window = MyMainWindow()
main_window.show()
sys.exit(app.exec_())
Timer.py
from PyQt5.QtCore import QTimer, pyqtSignal
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import datetime
class Timer(QTimer):
get_seconds = pyqtSignal(str)
def __init__(self):
super().__init__()
self.time_left = 1
self.timeout.connect(self.timer_timeout)
def addSecs(self, secs):
self.time_left += secs
def timer_start(self):
self.start(1000)
self.update_gui()
def timer_timeout(self):
self.time_left -= 1
if self.time_left <= 0:
self.stop()
self.update_gui()
def update_gui(self):
self.get_seconds.emit(str(self.time_left))
Schedule.py
from datetime import datetime
from apscheduler.schedulers.qt import QtScheduler
class Scheduler():
def __init__(self):
self.id = 'test_job'
self.sched = QtScheduler()
def add(self,job_function):
self.sched.add_job(job_function, 'cron', day_of_week='mon-fri',hour ='9-18',minute = '2,7,12,17,22,27,32,37,42,47,52,57',second = '5', id=self.id)
def start(self):
self.sched.start()
def next_occurance(self):
for job in self.sched.get_jobs():
if job.id == self.id:
return job.next_run_time
As the error explains, you cannot start and stop a QTimer from another thread, and since APScheduler works on different threads that the reason of your issue: self.hello is called from the APScheduler thread, not the thread from which your Timer is created.
To access objects created in different threads, you need to use signals and slots in order to let Qt manage communications between different threads.
So, the solution could be to subclass your Scheduler by inheriting from QObject (in order to be able to create signals and connect to them), then use a custom signal each time a job is executed and use that signal to restart the timer.
To achieve that, I use a createJob function which actually runs the job and emits a started signal when the job is started, and a completed when completed.
Unfortunately I cannot test the following code as I'm unable to install APScheduler right now, but the logic should be fine.
Main.py
class MyMainWindow(QtWidgets.QMainWindow):
def __init__(self):
# ...
self.sch = Scheduler()
self.sch.completed.connect(self.start_my_timer)
def hello(self):
print("hello world")
# no call to self.start_my_timer here!
Schedule.py
from datetime import datetime
from apscheduler.schedulers.qt import QtScheduler
from PyQt5 import QtCore
class Scheduler(QtCore.QObject):
started = QtCore.pyqtSignal(object)
completed = QtCore.pyqtSignal(object)
def __init__(self):
self.id = 'test_job'
self.sched = QtScheduler()
def add(self, job_function, *args, **kwargs):
self.sched.add_job(self.createJob(job_function), 'cron',
day_of_week='mon-fri', hour='9-18',
minute='2,7,12,17,22,27,32,37,42,47,52,57',
second='5', id=self.id, *args, **kwargs)
def createJob(self, job_function):
def func(*args, **kwargs):
self.started.emit(job_function)
job_function(*args, **kwargs)
self.completed.emit(job_function)
return func
def start(self):
self.sched.start()
def next_occurance(self):
for job in self.sched.get_jobs():
if job.id == self.id:
return job.next_run_time
Note that I'm emitting the started and completed signals with the job_function argument (which is a reference to the job, self.hello in your case), which might be useful to recognize the job in case you want to react differently with multiple jobs. I also added basic support for positional and keyword arguments.
Also note that I'm only giving a very basic implementation (your function only prints a message). If you need to interact to UI elements in the job function, the same problem with QTimer will rise, as no access to UI elements is allowed from threads outside the main Qt thread.
In that case you'll need to find another way. For example, you could add a job (that is not actually run from the scheduler) and emit a signal with that job as argument, and connect to a function that will actually run that job in the main thread.
Main.py
class MyMainWindow(QtWidgets.QMainWindow):
def __init__(self):
# ...
self.sch = Scheduler()
self.sch.startJob.connect(self.startJob)
def startJob(self, job, args, kwargs):
job(*args, **kwargs)
self.start_my_timer()
def hello(self):
self.someLabel.setText("hello world")
Schedule.py
from datetime import datetime
from apscheduler.schedulers.qt import QtScheduler
from PyQt5 import QtCore
class Scheduler(QtCore.QObject):
startJob = QtCore.pyqtSignal(object, object, object)
# ...
def add(self, job_function, *args, **kwargs):
self.sched.add_job(self.createJob(job_function, args, kwargs), 'cron',
day_of_week='mon-fri', hour='9-18',
minute='2,7,12,17,22,27,32,37,42,47,52,57',
second='5', id=self.id)
def createJob(self, job_function, args, kwargs):
def func():
self.starJob.emit(job_function, args, kwargs)
return func
As said, the code above is untested, you'll need to check for possible bugs (maybe I made some mistake with the wildcard arguments).
Finally, some small suggestions:
There are very few and specific cases for which using pyqtSlot decorator is actually necessary; interestingly enough, using them is often source of problems or unexpected behavior.
It's usually better to leave signal arguments as they are without any conversion, so you shouldn't convert the time to a string for the get_seconds signal; also, QLabel can accept numeric values using setNum() (both for float and int numbers).
Be more careful with spacings (I'm referring to self.sched.add_job): for keyword arguments, spaces should only exist after commas (read more on the Style Guide for Python Code); while it actually doesn't represent an issue, it greatly improves readability.
I am building an API that I would like to use in applications using pyqt and other guis.
If a programmer is using a gui other that Qt I don't want her to have to import pyqt etc.
My problem is that I use a task to subscribe to messages and pass them through to the main task.
I accomplish this by passing a parameter (qt) which is either True or False depending on what gui we are implementing.
I then create a class that generates a SubscriberParent class of either a QtCore.QThread or a Thread. The ultimate SubscriberThread
is of type SubscriberThreadParent.
The main thread either polls the interTaskQueue or uses Qt's signals and slots to process the message.
class Gui(object):
def __init__(self, qt):
if qt:
from PyQt5 import QtCore
from PyQt5.QtCore import Qt, pyqtSignal
class SubScriberThreadParent(QtCore.QThread):
def __init__(self):
QtCore.QThread.__init__(self)
else:
from threading import Thread
class SubScriberThreadParent(Thread):
def __init__(self):
Thread.__init__(self)
class SubscriberThread(Gui.SubScriberThreadParent):
def __init__(self, qt, dsParam, SubScriberThreadParent):
if qt:
from PyQt5 import QtCore
from PyQt5.QtCore import Qt, pyqtSignal
pubIn = pyqtSignal(str, str)
SubScriberThreadParent.__init__(self)
self.interTaskQueue = dsParam.interTaskQueue
.
.
.
.
#
# Pass the message on to the main application
#
def alertMainApp(self, bodyTuple):
if self.qt:
btz = '{0}'.format(bodyTuple)
self.pubIn.emit(btz)
LOGGER.info("Emitted Alert ")
else:
if self.interTaskQueue != None:
self.interTaskQueue.put(bodyTuple) # Pass it to the main thread
LOGGER.info("Queued Alert.")
else:
LOGGER.error("No Inter-task data transfer method available to the subscriber task!")
The error I get with this approach is: "AttributeError: type object 'Gui' has no attribute 'SubScriberThreadParent'"
How can I make this work?
I also would like to know the scope of the conditional imports.
In order for one class to inherit from Gui.SubScriberThreadParent you will have to have created an instance of Gui and assigned something to a class variable.
Consider this code:
class Gui:
def __init__(self, qt):
if qt:
class Foo:
att = 'Foo'
Gui.Parent = Foo
else:
class Bar:
att = 'Bar'
Gui.Parent = Bar
def makeThread():
class Thread(Gui.Parent):
def __init__(self):
self.att = Gui.Parent.att
return Thread()
def main():
Gui(False)
t = makeThread()
print(t.att)
main()
Output:
With Gui(False) as above, the output is:
Bar
When changed to Gui(True), the output is:
Foo
I have a strange problem with using the QtCore.pyqtSlot decorator in front of a function that is decorated with a different decorator. The problem below demonstrates this problem. It instantiates a minimalistic Qt program with a single button. On pressing the button, the following happens:
(1) signals 'signal_A' and 'signal_B' are connected to the instances method_A and method_B, respectively.
(2) the signal_A and signal_B signals are emitted.
(3) The expected behavior is that method_A and method_B are executed. However, it turns out that both signal_A and signal_B trigger method_A, or method_B (oddly, this nondeterministic, and is different for each run of the program!).
This appears to be a bug.
The issue is present when using the PyQt4 bindings to Qt. It is not present when using PySide (use the '--pyside' parameter to verify). I have seen the problem both in Python 2 and 3, and both in Windows and Linux.
The issue appears to be related to the insertion of a decorator between the '#QtSlot' and the method definition. The 'null_decorator' in the test program should do nothing (if my understanding of decorators is correct), but the behavior of the program changes when I remove it.
Any help in understanding what is going on here would be appreciated.
#! /usr/bin/env python3
import sys, time
if "--pyside" not in sys.argv:
# default to PyQt4 bindings
from PyQt4 import QtCore, QtGui
QtSignal = QtCore.pyqtSignal
QtSlot = QtCore.pyqtSlot
else:
# alternatively, use PySide bindings
from PySide import QtCore, QtGui
QtSignal = QtCore.Signal
QtSlot = QtCore.Slot
def null_decorator(f):
def null_decorator_wrapper(self, *args, **kwargs):
return f(self, *args, **kwargs)
return null_decorator_wrapper
class TestClass(QtCore.QObject):
def __init__(self, *args, **kwargs):
super(TestClass, self).__init__(*args, **kwargs)
#QtSlot()
#null_decorator
def method_A(self):
print("method_A() executing!")
#QtSlot()
#null_decorator
def method_B(self):
print("method_B() executing!")
class DemonstrateProblemButton(QtGui.QPushButton):
signal_A = QtSignal()
signal_B = QtSignal()
def __init__(self, *args, **kwargs):
super(DemonstrateProblemButton, self).__init__(*args, **kwargs)
self.clicked.connect(self.on_clicked)
#QtSlot()
def on_clicked(self):
# Create TestClass instance
instance = TestClass()
# connect the signals
self.signal_A.connect(instance.method_A)
self.signal_B.connect(instance.method_B)
# emit the signals
self.signal_A.emit()
self.signal_B.emit()
def main():
# instantiate the GUI application
app = QtGui.QApplication(sys.argv)
button = DemonstrateProblemButton("Demonstrate Problem")
button.show()
return QtGui.QApplication.exec_()
if __name__ == "__main__":
exitcode = main()
sys.exit(exitcode)
Using the null_decorator like that will result in all the slots having the same name (i.e. "null_decorator_wrapper"), and so PyQt may not be able to distinguish between them.
There are several ways to fix this in your example.
Firstly, you could ensure the slots have different signatures:
#QtSlot()
#null_decorator
def method_A(self):
print("method_A() executing!")
#QtSlot(int)
#null_decorator
def method_B(self, foo=0):
print("method_B() executing!")
Secondly, you could explicitly specify the slot names:
#QtSlot(name="method_A")
#null_decorator
def method_A(self):
print("method_A() executing!")
#QtSlot(name="method_B")
#null_decorator
def method_B(self, foo=0):
print("method_B() executing!")
Thirdly, you could automagically set the name in the null_decorator:
def null_decorator(f):
def null_decorator_wrapper(self, *args, **kwargs):
return f(self, *args, **kwargs)
null_decorator_wrapper.__name__ = f.__name__
return null_decorator_wrapper
PS: The behaviour of the pyqtSlot decorator is clearly stated in the PyQt docs (in particular, see the description of the name parameter), and is certainly not a bug.
I'm having troubles using PyQt4 slots/signals.
I'm using PyLIRC and I'm listening for button presses on a remote. This part I have gotten to work outside of Qt. My problem comes when emitting the signal from the button listening thread and attempting to call a slot in the main thread.
My button listener is a QObject initialized like so:
buttonPressed = pyqtSignal(int)
def __init__(self):
super(ButtonEvent, self).__init__()
self.buttonPressed.connect(self.onButtonPressed)
def run(self):
print 'running'
while(self._isListening):
s = pylirc.nextcode()
if (s):
print 'emitting'
self.buttonPressed.emit(int(s[0]))
The onButtonPressed slot is internal to the button listener for testing purposes.
To move the button listener to another thread to do the work, I use the following:
event = ButtonEvent()
eventThread = QThread()
event.moveToThread(eventThread)
eventThread.started.connect(event.run)
Then in the main thread, I have my VideoTableController class that contains the slot in the main thread that doesn't get called. Inside of __init__ I have this:
class VideoTableController(QObject):
def __init__(self, buttonEvent):
buttonEvent.buttonPressed.connect(self.onButtonPressed)
Where onButtonPressed in this case is:
#pyqtSlot(int)
def onButtonPressed(self, bid):
print 'handling button press'
if bid not in listenButtons: return
{ ButtonEnum.KEY_LEFT : self.handleBack,
#...
So when I start the event thread, it starts listening properly. When I press a button on the remote, the onButtonPressed slot internal to the ButtonEvent class is properly called, but the slot within VideoTableController, which resides in the main thread, is not called. I started my listening thread after connecting the slot to the signal, and I tested doing it the other way around, but to no avail.
I have looked around, but I haven't been able to find anything. I changed over to using QObject after reading You're doing it wrong. Any help with this is greatly appreciated. Let me know if you need anything else.
EDIT: Thanks for the responses! Here is a big chunk of code for you guys:
ButtonEvent (This class uses singleton pattern, excuse the poor coding because I'm somewhat new to this territory of Python also):
import pylirc
from PyQt4.QtCore import QObject, pyqtSignal, QThread, pyqtSlot
from PyQt4 import QtCore
class ButtonEvent(QObject):
"""
A class used for firing button events
"""
_instance = None
_blocking = 0
_isListening = False
buttonPressed = pyqtSignal(int)
def __new__(cls, configFileName="~/.lircrc", blocking=0, *args, **kwargs):
if not cls._instance:
cls._instance = super(ButtonEvent, cls).__new__(cls, args, kwargs)
cls._blocking = blocking
if not pylirc.init("irexec", configFileName, blocking):
raise RuntimeError("Problem initilizing PyLIRC")
cls._isListening = True
return cls._instance
def __init__(self):
"""
Creates an instance of the ButtonEvent class
"""
super(ButtonEvent, self).__init__()
self.buttonPressed.connect(self.button)
### init
def run(self):
print 'running'
while(self._isListening):
s = pylirc.nextcode()
if (s):
print 'emitting'
self.buttonPressed.emit(int(s[0]))
def stopListening(self):
print 'stopping'
self._isListening = False
#pyqtSlot(int)
def button(self, bid):
print 'Got ' + str(bid)
def setupAndConnectButtonEvent(configFileName="~/.lircrc", blocking=0):
"""
Initializes the ButtonEvent and puts it on a QThread.
Returns the QThread it is running on.
Does not start the thread
"""
event = ButtonEvent().__new__(ButtonEvent, configFileName, blocking)
eventThread = QThread()
event.moveToThread(eventThread)
eventThread.started.connect(event.run)
return eventThread
Here is the VideoTableController:
from ControllerBase import ControllerBase
from ButtonEnum import ButtonEnum
from ButtonEvent import ButtonEvent
from PyQt4.QtCore import pyqtSlot
from PyQt4 import QtCore
class VideoTableController(ControllerBase):
listenButtons = [ ButtonEnum.KEY_LEFT,
ButtonEnum.KEY_UP,
ButtonEnum.KEY_OK,
ButtonEnum.KEY_RIGHT,
ButtonEnum.KEY_DOWN,
ButtonEnum.KEY_BACK ]
def __init__(self, model, view, parent=None):
super(VideoTableController, self).__init__(model, view, parent)
self._currentRow = 0
buttonEvent = ButtonEvent()
buttonEvent.buttonPressed.connect(self.onButtonPressed)
self.selectRow(self._currentRow)
#pyqtSlot(int)
def onButtonPressed(self, bid):
print 'handling button press'
if bid not in listenButtons: return
{ ButtonEnum.KEY_LEFT : self.handleBack,
ButtonEnum.KEY_UP : self.handleUp,
ButtonEnum.KEY_OK : self.handleOk,
ButtonEnum.KEY_RIGHT : self.handleRight,
ButtonEnum.KEY_DOWN : self.handleDown,
ButtonEnum.KEY_BACK : self.handleBack,
}.get(bid, None)()
And here is my startup script:
import sys
from PyQt4 import QtCore, QtGui
from ui_main import Ui_MainWindow
from VideoTableModel import VideoTableModel
from VideoTableController import VideoTableController
from ButtonEvent import *
class Main(QtGui.QMainWindow):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.buttonEvent = ButtonEvent()
self.bEventThread = setupAndConnectButtonEvent()
model = VideoTableModel("/home/user/Videos")
self.ui.videoView.setModel(model)
controller = VideoTableController(model, self.ui.videoView)
self.bEventThread.start()
def closeEvent(self, event):
self.buttonEvent.stopListening()
self.bEventThread.quit()
event.accept()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
buttonEvent = ButtonEvent()
myapp = Main()
myapp.show()
sys.exit(app.exec_())
It turns out I was just making a foolish Python mistake. The signal was being emitted correctly, and the event loop was running properly in all threads. My problem was that in my Main.__init__ function I made a VideoTableController object, but I did not keep a copy in Main, so my controller did not persist, meaning the slot also left. When changing it to
self.controller = VideoTableController(model, self.ui.videoView)
Everything stayed around and the slots were called properly.
Moral of the story: it's not always a misuse of the library, it may be a misuse of the language.
It seems that the quickest workaround would be change your ButtonEvent code here:
...
def run(self):
print 'running'
while(self._isListening):
s = pylirc.nextcode()
if (s):
print 'emitting'
self.buttonPressed.emit(int(s[0]))
...
to this:
#pyqtSlot()
def run(self):
print 'running'
while(self._isListening):
s = pylirc.nextcode()
if (s):
print 'emitting'
self.buttonPressed.emit(int(s[0]))
The short explanation to this issue is that PyQt uses a proxy internally, and this way you can make sure to avoid that. After all, your method is supposed to be a slot based on the connect statement.
Right... Now, I would encourage you to give some consideration for your current software design though. It seems that you are using a class in a dedicated thread for handling Qt button events. It may be good idea, I am not sure, but I have not seen this before at least.
I think you could get rid of that class altogether in the future with a better approach where you connect from the push button signals directly to your handler slot. That would not be the run "slot" in your dedicated thread, however, but the cannonical handler.
It is not a good design practice to introduce more complexity, especially in multi-threaded applications, than needed. Hope this helps.
I haven't actually tested this (because I don't have access to your compiled UI file), but I'm fairly certain I'm right.
Your run method of your ButtonEvent (which is supposed to be running in a thread) is likely running in the mainthread (you can test this by importing the python threading module and adding the line print threading.current_thread().name. To solve this, decorate your run method with #pyqtSlot()
If that doesn't solve it, add the above print statement to various places until you find something running in the main thread that shouldn't be. The lined SO answer below will likely contain the answer to fix it.
For more details, see this answer: https://stackoverflow.com/a/20818401/1994235