Reputation: 760
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
Upvotes: 1
Views: 2136
Reputation: 48231
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).
pyqtSlot
decorator is actually necessary; interestingly enough, using them is often source of problems or unexpected behavior.get_seconds
signal; also, QLabel can accept numeric values using setNum()
(both for float and int numbers).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.Upvotes: 5