为了账号安全,请及时绑定邮箱和手机立即绑定

PYQT5 线程与计划和计时器的问题

PYQT5 线程与计划和计时器的问题

芜湖不芜 2023-08-08 10:51:04
我正在使用它PYQT5来构建 GUI,并使用它APScheduler来管理我想要运行的作业。我将调度程序项目和计时器项目分解为自己的类,然后将它们连接到主文件中。我遇到的问题是,一旦计时器完成一个周期,我会尝试为课程添加时间Timer,并在调度程序再次运行之前重新开始下一次倒计时。我收到两个错误或警告,但我不知道如何修复它们。他们是:QObject::killTimer: Timers cannot be stopped from another threadQObject::startTimer: Timers cannot be started from another thread一旦抛出这些,GUI 就会更新,但不再倒计时。我将附上我发现的可以重现错误的最简单版本。非常感谢任何帮助,并感谢您的宝贵时间。Main.pyimport sysfrom PyQt5 import QtCorefrom PyQt5 import QtWidgetsfrom Timer import Timerfrom Schedule import Schedulerimport datetimeclass 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_())
查看完整描述

1 回答

?
繁花如伊

TA贡献2012条经验 获得超12个赞

正如错误所解释的,您无法从另一个线程启动和停止 QTimer,并且由于 APScheduler 在不同的线程上工作,因此问题的原因:是self.hello从 APScheduler 线程调用的,而不是从创建计时器的线程调用的。


要访问在不同线程中创建的对象,您需要使用信号和槽,以便让 Qt 管理不同线程之间的通信。


因此,解决方案可能是通过继承 QObject 来子类化您的 Scheduler(以便能够创建信号并连接到它们),然后在每次执行作业时使用自定义信号并使用该信号重新启动计时器。


为了实现这一目标,我使用了一个createJob实际运行作业的函数,并started在作业启动时发出信号,并completed在作业完成时发出信号。


不幸的是,我无法测试以下代码,因为我现在无法安装 APScheduler,但逻辑应该没问题。


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

请注意,我使用参数(在您的情况下是对作业的引用)发出started和completed信号,如果您想对多个作业做出不同的反应,这可能有助于识别作业。我还添加了对位置和关键字参数的基本支持。job_functionself.hello


另请注意,我仅提供一个非常基本的实现(您的函数仅打印一条消息)。如果您需要与作业功能中的 UI 元素进行交互,那么 QTimer 也会出现同样的问题,因为不允许从主 Qt 线程之外的线程访问 UI 元素。


在这种情况下,您需要寻找另一种方法。例如,您可以添加一个作业(实际上并非从调度程序运行)并以该作业作为参数发出信号,然后连接到将在主线程中实际运行该作业的函数。


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

如前所述,上面的代码未经测试,您需要检查可能的错误(也许我在通配符参数上犯了一些错误)。

最后,一些小建议:

  1. 实际上有必要使用装饰器的情况非常少pyqtSlot;有趣的是,使用它们通常是问题或意外行为的根源。

  2. 通常最好保留信号参数原样而不进行任何转换,因此您不应将时间转换为信号的字符串get_secondssetNum()此外,QLabel 可以使用(浮点型和整数型)接受数值。

  3. 对空格要更加小心(我指的是self.sched.add_job):对于关键字参数,空格只能存在于逗号之后;虽然它实际上并不代表问题,但它极大地提高了可读性。


查看完整回答
反对 回复 2023-08-08
  • 1 回答
  • 0 关注
  • 124 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信