PYQT5: instantiating multiple QThread classes for independent timers - python

I have an application that keeps track of the time with a 1-second countdown timer that works perfectly:
class CDWorker(QtCore.QThread):
sinout= pyqtSignal()
def __init__(self, ctrl):
super(CDWorker,self).__init__()
self.working = True
self.num = 0
################################ Trigger the controller callback on timer cycle
def timerEvent(self, ctrl, event):
if len(ctrl.activeDF) != 0:
ctrl.activeDF.apply(ctrl.timerPipeline, axis = 1)
ctrl.getNearest()
super().timerEvent(event)
######################## Spin up the 1-second timer to constantly update time
#QtCore.pyqtSlot()
def run(self, ctrl):
while self.working == True:
loop = QtCore.QEventLoop()
ctrl.DF.apply(ctrl.callback1, axis = 1)
QtCore.QTimer.singleShot(1000, loop.quit)
loop.exec()
and I have a controller class that handles most of the heavy lifting in the app
class Controller:
def __init__(self):
self._app = QtWidgets.QApplication(sys.argv)
self._model = Model()
self._view = View(ctrl = self)
self._view.show()
cdTimer = QtCore.QThread()
cdWorker = CDWorker(ctrl = self)
cdWorker.moveToThread(cdTimer)
cdWorker.run(ctrl = self)
cdTimer.started.connect(CDWorker.run)
This all works perfectly and everything is good. However, now I need another class responsible for scheduling an http request on a 5 minute timer. Every attempt ive made so far to make another Qthread, QTimer, Qthreadpool, etc has either blocked the main eventloop, overridden the original timer, or was just ignored. Ive tried messing with the signal/slots and all too.
How can I get two classes with independent timers to trigger callbacks in my controller class?

Related

Using a QTimer within a PyQt worker thread

I am working with serial device and set a flag (which is global variable) based on the received data. Now I want to reset the flag after a while (for example one second) by using a timer.
Here is the code:
class Inlet_Worker(QObject):
def __init__(self):
super(Inlet_Worker, self).__init__()
self.timer = QTimer(self)
self.timer.timeout.connect(self.Reset_Register_Barcode)
def run(self):
global Register_Barcode
while True :
if client.read_coils(address = 0x0802).bits[0]:
Register_Barcode = True
self.timer.start(1000)
def Reset_Register_Barcode(self):
global Register_Barcode
Register_Barcode = False
However the timer is not working.
I will assume from your example code that your are using a QThread and that you also use QObject.moveToThread on your worker object. This is the correct procedure, but there are some other things you must do to make your timer work.
Firstly, you should use a single-shot timer so as to avoid re-regsitration whilst the current one is active. Secondly, you must explicitly process any pending events, since your while-loop will block the thread's event-loop. Without this, the timer's timeout signal will never be emitted. Thirdly, you should ensure that the worker and thread shut down cleanly when the program exits (which will also prevent any Qt error messages). Finally, if possible, you should use signals to communicate registration changes to the main GUI thread, rather than global variables.
The demo script below (based on your example) implements all of that. After the Start button is clicked, the thread will start and periodically update the regsitration (indicated by the check-box). Hopefully you shoudld be able to see how to adapt it to your real application:
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class Inlet_Worker(QObject):
barcodeRegistered = pyqtSignal(bool)
def __init__(self):
super().__init__()
self._stopped = False
self._registered = False
self.timer = QTimer(self)
self.timer.setSingleShot(True)
self.timer.timeout.connect(self.updateBarcodeRegistration)
def run(self):
count = 0
self._stopped = False
while not self._stopped:
#if client.read_coils(address = 0x0802).bits[0]:
count += 1
if count % 20 == 0 and not self._registered:
self.updateBarcodeRegistration(True)
self.timer.start(2000)
QCoreApplication.processEvents()
QThread.msleep(100)
self.updateBarcodeRegistration(False)
self.timer.stop()
print('Stopped')
def updateBarcodeRegistration(self, enable=False):
print('Register' if enable else 'Reset')
self._registered = enable
self.barcodeRegistered.emit(enable)
def stop(self):
self._stopped = True
class Window(QWidget):
def __init__(self):
super().__init__()
self.thread = QThread()
self.worker = Inlet_Worker()
self.worker.moveToThread(self.thread)
self.button = QPushButton('Start')
self.check = QCheckBox('Registered')
layout = QHBoxLayout(self)
layout.addWidget(self.button)
layout.addWidget(self.check)
self.thread.started.connect(self.worker.run)
self.button.clicked.connect(self.thread.start)
self.worker.barcodeRegistered.connect(self.check.setChecked)
def closeEvent(self, event):
self.worker.stop()
self.thread.quit()
self.thread.wait()
if __name__ == '__main__':
app = QApplication(['Test'])
window = Window()
window.setGeometry(600, 100, 200, 50)
window.show()
app.exec()

How to display a timer using PyQt5 for Python

I'm making a game where I would like a timer to be displayed on the screen after the user clicks on 'NEW GAME', to keep track of how long they've been playing. I have a class that runs the timer fine by itself, but when I incorporate it into the rest of my game and then on top of that, try to display the updated values of the timer, no values in the UI are updated and the printout of the timer doesn't even occur in the terminal. I've tried running the timer in the same thread as the game-setup process and I've also tried creating a new thread to run the timer but neither work. The game loads up and functions fine, with the exception of the timer not counting upwards and not displaying the updated timer values. Where am I going wrong here?
Here is my standalone Timer class, which again, works fine by itself.
from PyQt5 import QtCore
import sys
def startThread(functionName, *args, **kwargs):
print(args)
if len(args) == 0:
t = threading.Thread(target=functionName)
else:
try:
t = threading.Thread(target=functionName, args=args, kwargs=kwargs)
except:
try:
if args is None:
t = threading.Thread(target=functionName, kwargs=kwargs)
except:
t = threading.Thread(target=functionName)
t.daemon = True
t.start()
class Timer(object):
def __init__(self):
super(Timer, self).__init__()
def start_timer(self):
print("Starting timer...")
Timer.timer = QtCore.QTimer()
Timer.time = QtCore.QTime(0, 0, 0)
Timer.timer.timeout.connect(self.tick)
Timer.timer.start(1000)
def tick(self):
Timer.time = Timer.time.addSecs(1)
self.update_UI('%s' % Timer.time.toString("hh:mm:ss"))
def update_UI(self, text_string):
print(text_string)
# This is where the text would be sent to try and update the UI
Timer().start_timer()
This is more or less how my game-setup class is structured - currently I'm showing the version that uses threading:
class BuildUI(PyQt5.QtWidgets.QMainWindow, Ui_game):
def __init__(self):
super(BuildUI, self).__init__()
self.setupUi(self)
self.easy_mode = 38
self.user_available_cells = []
self.start_easy_game.triggered.connect(lambda: self.setup_game(self.easy_mode))
def setup_game(self, hidden_count):
def create_game_board():
startThread(Timer().start_timer)
self.game = BuildGame()
#The BuildGame class is not deliberately not shown
startThread(create_game_board)
class GAME(object):
def __init__(self):
GAME.app = PyQt5.QtWidgets.QApplication(sys.argv)
GAME.UI = BuildUI()
GAME.UI.show()
GAME.app.exec_()
def main():
GAME()
if __name__ == '__main__':
main()
The key to getting this to work is by using signals. Leaving the Timer class exactly as it is, the only modifications to be done are in the initialization of the GAME class, a signal needs to be added at the beginning of the BuildUI class and then using emit() to trigger that signal just before the self.game = BuildGame() call.
class BuildUI(PyQt5.QtWidgets.QMainWindow, sudoku_ui.Ui_sudoku_game):
# This signal just triggers a msgbox to display, telling the user the game is loading
process_start = PyQt5.QtCore.pyqtSignal()
# this is called to automatically close the msgbox window
process_finished = PyQt5.QtCore.pyqtSignal()
# This signal, when called will start the timer
start_game_timer = PyQt5.QtCore.pyqtSignal()
def __init__(self):
super(BuildUI, self).__init__()
self.setupUi(self)
self.easy_mode = 38
self.user_available_cells = []
self.start_easy_game.triggered.connect(lambda: self.setup_game(self.easy_mode))
def setup_game(self, hidden_count):
def create_game_board():
self.game = BuildGame()
# Now that the game is built, the timer can start
# This is the emit which will start the timer
GAME.UI.start_game_timer.emit()
startThread(create_game_board)
class GAME(object):
def __init__(self):
GAME.app = PyQt5.QtWidgets.QApplication(sys.argv)
GAME.UI = BuildUI()
GAME.dialog_box = MsgPrompt()
# This is the key right here - initializing the timer class
GAME.timer = Timer()
# This line below attaches a function to the emit() call - which will kick off the timer
GAME.UI.start_game_timer.connect(GAME.timer.start_timer)
# The below referenced class is deliberately omitted from this post
GAME.UI.process_start.connect(GAME.dialog_box.show_dialog_box)
GAME.UI.process_finished.connect(GAME.dialog_box.hide_dialog_box)
GAME.UI.show()
GAME.app.exec_()
def main():
GAME()
if __name__ == '__main__':
main()

How do I stop a thread when its done working and restart it when a button is pressed?

I have an installer I am creating for a game and as of now there are two buttons. One downloads the game, and one starts the game if it detects the executable. I multi-threaded both buttons so that my GUI will not freeze when I click either button. The problem is, if I click one of the buttons, the other will not work until restarting the application. I need some way for the thread to close after its process is completed so that the thread is open for the other button to work.
Here is what I have so far:
# Import Libraries
import requests, os, sys, zipfile, shutil, subprocess, wx, urllib, time
from threading import *
# Define global variables
url = "{ENTER DROPBOX URL HERE}" # The url to the file we are downloading
myEVT_PROGRESS = wx.NewEventType() # Custom Event Type
EVT_PROGRESS = wx.PyEventBinder(myEVT_PROGRESS, 1) # Bind specific events to event handlers
ID_START = wx.NewId()# Button definitions
EVT_RESULT_ID = wx.NewId()# Define notification event for thread completion
# Version Check
def VersionCheck():
try:
CurrentVersion = os.listdir("./RFMB6_WINDOWS/")[0] # Checks the version currently downloaded
VersionCheck = requests.get('https://pastebin.com/raw/yc30uwAh') # Checks the newest version
NewestVersion = VersionCheck.text # Converts VersionCheck to a string
if CurrentVersion == NewestVersion:
message = 'It looks like you have the newest version already.\n Are you sure you want to download?'
wx.MessageBox(message=message, caption='RFMP GUIntaller | Complete!', style=wx.OK | wx.ICON_INFORMATION)
else:
print('\n\nThere is an update available, would you like to install it?')
pass
except:
print("It looks like you don't have RFMP installed yet. Let me fix that for you.")
# Downloads new file
def Download():
urllib.request.urlretrieve(url, 'RFMP.zip')
# Extracts new file
def Extract():
zip_ref = zipfile.ZipFile("RFMP.zip", 'r')
zip_ref.extractall("RFMB6_WINDOWS")
zip_ref.close()
# Deletes the .zip file but leave the folder
def Clean():
os.remove("RFMP.zip")
class ProgressEvent(wx.PyCommandEvent):
"""Event to signal that a status or progress changed"""
def __init__(self, etype, eid, status=None, progress=None):
"""Creates the event object"""
wx.PyCommandEvent.__init__(self, etype, eid)
self._status = status # field to update label
self._progress = progress # field to update progress bar
def GetValue(self):
"""Returns the value from the event.
#return: the tuple of status and progress
"""
return (self._status, self._progress)
# Thread class that executes processing
class DLThread(Thread):
"""Worker Thread Class."""
def __init__(self, notify_window):
"""Init Worker Thread Class."""
Thread.__init__(self)
self._notify_window = notify_window
self.start()
# This is what runs on a separate thread when you click the download button
def run(self):
# This is the code executing in the new thread.
self.sendEvent('Checking for old files...', 00)
self.sendEvent('Checking for old files...', 100)
time.sleep(.5)
if os.path.exists("RFMB6_WINDOWS"):
self.sendEvent('Removing old files...', 200)
subprocess.check_call(('attrib -R ' + 'RFMB6_WINDOWS' + '\\* /S').split())
shutil.rmtree('RFMB6_WINDOWS')
time.sleep(.3)
self.sendEvent('Removed old files.', 300)
else:
time.sleep(.3)
self.sendEvent('No old files found.', 300)
time.sleep(.3)
pass
self.sendEvent('Downloading Package...', 400)
Download()
self.sendEvent('Downloading complete.', 600)
time.sleep(.3)
self.sendEvent('Extracting...', 650)
Extract()
self.sendEvent('Extraction complete.', 900)
time.sleep(.3)
self.sendEvent('Cleaning up...', 950)
Clean()
time.sleep(.3)
self.sendEvent('Cleaning complete.', 1000)
time.sleep(.5)
done = ("Installation the RFMP Private Alpha has been completed!")
wx.MessageBox(message=done, caption='RFMP GUIntaller | Complete!', style=wx.OK | wx.ICON_INFORMATION)
self._notify_window.worker = None
def sendEvent(self, status=None, progress=None):
# Send event to main frame, first param (str) is for label, second (int) for the progress bar
evt = ProgressEvent(myEVT_PROGRESS, -1, status, progress)
wx.PostEvent(self._notify_window, evt)
class StartAppThread(Thread):
"""Worker Thread Class."""
def __init__(self, notify_window):
"""Init Worker Thread Class."""
Thread.__init__(self)
self._notify_window = notify_window
# This starts the thread running on creation.
self.start()
# This is what runs on a separate thread when you click the download button
def run(self):
try:
subprocess.run('RFMB6_WINDOWS/RFMB6_WINDOWS/RFMB6.exe')
except:
error = ("Failed to locate RFMB6.exe. Please don't move any game files after downloading.")
wx.MessageBox(message=error, caption='RFMP GUIntaller | Error!',
style=wx.OK | wx.ICON_ERROR)
self._notify_window.worker = None
# GUI Frame class that spins off the worker thread
class MainFrame(wx.Frame):
"""Class MainFrame."""
def __init__(self, parent, id):
"""Create the MainFrame."""
wx.Frame.__init__(self, parent, id, 'RFMP GUInstaller',
style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER
^ wx.MAXIMIZE_BOX)
self.SetSize(400, 350)
self.Centre()
DLStart = wx.Button(self.bitmap1, ID_START, 'Download RFMP', size=(175,50), pos=(50,260))
DLStart.Bind(wx.EVT_BUTTON, self.OnButton_DLStart)
AppStart = wx.Button(self.bitmap1, ID_START, 'Start RFMP', size=(175,50), pos=(50,160))
AppStart.Bind(wx.EVT_BUTTON, self.OnButton_AppStart)
self.status = wx.StaticText(self.bitmap1, -1, '', pos=(10,215), style=wx.NO_BORDER)
self.status.SetBackgroundColour((255,255,0)) # set text back color
self.gauge = wx.Gauge(self.bitmap1, range = 1000, size = (375, 30), pos=(10,230),
style = wx.GA_HORIZONTAL)
# And indicate we don't have a worker thread yet
self.worker = None
self.Bind(EVT_PROGRESS, self.OnResult) # Bind the custom event to a function
def OnButton_DLStart(self, event):
# Trigger the worker thread unless it's already busy
VersionCheck()
if not self.worker:
self.worker = DLThread(self)
def OnButton_AppStart(self, event):
if not self.worker:
self.worker = StartAppThread(self)
def OnResult(self, event):
"""Our handler for our custom progress event."""
status, progress = event.GetValue()
self.status.SetLabel(status)
if progress:
self.gauge.SetValue(progress)
class MainApp(wx.App):
"""Class Main App."""
def OnInit(self):
"""Init Main App."""
self.frame = MainFrame(None, -1)
self.frame.Show(True)
self.SetTopWindow(self.frame)
return True
# Main Loop
if __name__ == '__main__':
app = MainApp(0)
app.MainLoop()
Your issue is caused by the fact that self.worker has a value.
You need to reset self.worker.
Below I have adjusted your code to do that and in doing so I have renamed notify_window to parent, simply because it makes what is going on more obvious and fits with python standards. I'm sure that there are many others ways of achieving this, this is just a simplistic way of achieving it, in this case.
import requests, os, sys, zipfile, shutil, subprocess, wx, urllib, time
from threading import *
class DLThread(Thread):
"""Worker Thread Class."""
def __init__(self, parent):
"""Init Worker Thread Class."""
Thread.__init__(self)
self.parent = parent
self.stop_download = 0
self.setDaemon(1)
self.start()
def run(self):
# This is the code executing in the new thread.
'''
This is what runs on a separate thread when you click the download button
'''
x = 0
while self.stop_download == 0:
time.sleep(0.5)
x +=1
if x > 20:
self.stop_download = 1
print ("Downloading App", x)
print("Download finished")
self.parent.worker = None
def stop(self):
self.stop_download = 1
print ("Download Cancelled")
class StartAppThread(Thread):
"""Worker Thread Class."""
def __init__(self, parent):
"""Init Worker Thread Class."""
Thread.__init__(self)
self.parent = parent
self.stop_app_thread = 0
self.setDaemon(1)
self.start()
def run(self):
# This is the code executing in the new thread.
'''
This is what runs on a separate thread when you click the Start App button.
'''
x= 0
while self.stop_app_thread == 0:
print ("Game in progress",str(x))
time.sleep(0.5)
x +=1
print ("Game finished")
self.parent.worker = None
def stop(self):
self.stop_app_thread = 1
# GUI Frame class that spins off the worker thread
class MainFrame(wx.Frame):
"""Class MainFrame."""
#Main Window
def __init__(self, parent, id):
"""Create the MainFrame."""
wx.Frame.__init__(self, parent, id, 'RFMP GUInstaller',
style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER
^ wx.MAXIMIZE_BOX)
self.SetSize(400, 350)
#self.bitmap1 = wx.StaticBitmap(self)
self.bitmap1 = wx.Panel(self)
self.Centre()
# Variables
myEVT_PROGRESS = wx.NewEventType() # Custom Event Type
EVT_PROGRESS = wx.PyEventBinder(myEVT_PROGRESS, 1) # Bind specific events to event handlers
ID_START = wx.NewId()# Button definitions
EVT_RESULT_ID = wx.NewId()# Define notification event for thread completion
# Download button
DLStart = wx.Button(self.bitmap1, ID_START, 'Download', size=(175,50), pos=(50,260))
DLStart.Bind(wx.EVT_BUTTON, self.OnButton_DLStart)
# App Start button
AppStart = wx.Button(self.bitmap1, ID_START, 'Start App', size=(75,50), pos=(50,160))
AppStart.Bind(wx.EVT_BUTTON, self.OnButton_AppStart)
# App Stop button
AppStop = wx.Button(self.bitmap1, ID_START, 'Stop', size=(75,50), pos=(150,160))
AppStop.Bind(wx.EVT_BUTTON, self.OnButton_AppStop)
# Progress bar
self.gauge = wx.Gauge(self.bitmap1, range = 1000, size = (375, 30), pos=(10,230), style = wx.GA_HORIZONTAL)
# And indicate we don't have a worker thread yet
self.worker = None
self.Bind(EVT_PROGRESS, self.OnResult) # Bind the custom event to a function
def OnButton_DLStart(self, event):
# Trigger the worker thread unless it's already busy
if not self.worker:
self.worker = DLThread(self)
def OnButton_AppStart(self, event):
if not self.worker:
self.worker = StartAppThread(self)
def OnButton_AppStop(self, event):
if self.worker:
self.worker.stop()
print ("App Stop command")
def OnResult(self, event):
"""Our handler for our custom progress event."""
status, progress = event.GetValue()
self.status.SetLabel(status)
if progress:
self.gauge.SetValue(progress)
class MainApp(wx.App):
"""Class Main App."""
def OnInit(self):
"""Init Main App."""
self.frame = MainFrame(None, -1)
self.frame.Show(True)
self.SetTopWindow(self.frame)
return True
# Main Loop
if __name__ == '__main__':
app = MainApp(0)
app.MainLoop()

QThread does not update view with events

On menu I can trigger:
def on_git_update(self):
update_widget = UpdateView()
self.gui.setCentralWidget(update_widget)
updateGit = UpdateGit()
updateGit.progress.connect(update_widget.on_progress)
updateGit.start()
then I have:
class UpdateView(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
vbox = QVBoxLayout()
self.pbar = QProgressBar()
vbox.addWidget(self.pbar)
vbox.addStretch(1)
self.setLayout(vbox)
def on_progress(self, value):
self.pbar.setValue(int(value * 100))
class UpdateGit(QThread):
progress = pyqtSignal(float)
def __del__(self):
self.wait()
def run(self):
for i in range(10):
self.progress.emit(i / 10)
sleep(.5)
The app freezes during the processing, afaik it should work as it is in a thread using signals.
Also, it works as expected with the app updating every step when I run it in debug mode via pycharm.
How is my thread set up incorrectly?
A variable created in a function only exists until the function exists, and this is what happens with updateGit, in the case of update_widget when it is set as centralwidget it has a greater scope since Qt handles it. The solution is to extend the scope of the thread by making it a member of the class.
def on_git_update(self):
update_widget = UpdateView()
self.gui.setCentralWidget(update_widget)
self.updateGit = UpdateGit()
self.updateGit.progress.connect(update_widget.on_progress)
self.updateGit.start()

PyQt: moveToThread does not work when using partial() for slot

I am building a small GUI application which runs a producer (worker) and the GUI consumes the output on demand and plots it (using pyqtgraph).
Since the producer is a blocking function (takes a while to run), I (supposedly) moved it to its own thread.
When calling QThread.currentThreadId() from the producer it outputs the same number as the main GUI thread. So, the worker is executed first, and then all the plotting function calls are executed (because they are being queued on the same thread's event queue). How can I fix this?
Example run with partial:
gui thread id 140665453623104
worker thread id: 140665453623104
Here is my full code:
from PyQt4 import QtCore, QtGui
from PyQt4.QtCore import pyqtSignal
import pyqtgraph as pg
import numpy as np
from functools import partial
from Queue import Queue
import math
import sys
import time
class Worker(QtCore.QObject):
termino = pyqtSignal()
def __init__(self, q=None, parent=None):
super(Worker, self).__init__(parent)
self.q = q
def run(self, m=30000):
print('worker thread id: {}'.format(QtCore.QThread.currentThreadId()))
for x in xrange(m):
#y = math.sin(x)
y = x**2
time.sleep(0.001) # Weird, plotting stops if this is not present...
self.q.put((x,y,y))
print('Worker finished')
self.termino.emit()
class MainWindow(QtGui.QWidget):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.q = Queue()
self.termino = False
self.worker = Worker(self.q)
self.workerThread = None
self.btn = QtGui.QPushButton('Start worker')
self.pw = pg.PlotWidget(self)
pi = self.pw.getPlotItem()
pi.enableAutoRange('x', True)
pi.enableAutoRange('y', True)
self.ge1 = pi.plot(pen='y')
self.xs = []
self.ys = []
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.pw)
layout.addWidget(self.btn)
self.resize(400, 400)
def run(self):
self.workerThread = QtCore.QThread()
self.worker.moveToThread(self.workerThread)
self.worker.termino.connect(self.setTermino)
# moveToThread doesn't work here
self.btn.clicked.connect(partial(self.worker.run, 30000))
# moveToThread will work here
# assume def worker.run(self): instead of def worker.run(self, m=30000)
# self.btn.clicked.connect(self.worker.run)
self.btn.clicked.connect(self.graficar)
self.workerThread.start()
self.show()
def setTermino(self):
self.termino = True
def graficar(self):
if not self.q.empty():
e1,e2,ciclos = self.q.get()
self.xs.append(ciclos)
self.ys.append(e1)
self.ge1.setData(y=self.ys, x=self.xs)
if not self.termino:
QtCore.QTimer.singleShot(1, self.graficar)
if __name__ == '__main__':
app = QtGui.QApplication([])
window = MainWindow()
QtCore.QTimer.singleShot(0, window.run);
sys.exit(app.exec_())
The problem is that Qt attempts to choose the connection type (when you call signal.connect(slot)) based on the what thread the slot exists in. Because you have wrapped the slot in the QThread with partial, the slot you are connecting to resides in the MainThread (the GUI thread). You can override the connection type (as the second argument to connect() but that doesn't help because the method created by partial will always exist in the MainThread, and so setting the connection type to by Qt.QueuedConnection doesn't help.
The only way around this that I can see is to set up a relay signal, the sole purpose of which is to effectively change an emitted signal with no arguments (eg the clicked signal from a button) to a signal with one argument (your m parameter). This way you don't need to wrap the slot in the QThread with partial().
The code is below. I've created a signal with one argument (an int) called 'relay' in the main windows class. The button clicked signal is connected to a method within the main window class, and this method has a line of code which emits the custom signal I created. You can extend this method (relay_signal()) to get the integer to pass to the QThread as m (500 in this case), from where ever you like!
So here is the code:
from functools import partial
from Queue import Queue
import math
import sys
import time
class Worker(QtCore.QObject):
termino = pyqtSignal()
def __init__(self, q=None, parent=None):
super(Worker, self).__init__(parent)
self.q = q
def run(self, m=30000):
print('worker thread id: {}'.format(QtCore.QThread.currentThreadId()))
for x in xrange(m):
#y = math.sin(x)
y = x**2
#time.sleep(0.001) # Weird, plotting stops if this is not present...
self.q.put((x,y,y))
print('Worker finished')
self.termino.emit()
class MainWindow(QtGui.QWidget):
relay = pyqtSignal(int)
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.q = Queue()
self.termino = False
self.worker = Worker(self.q)
self.workerThread = None
self.btn = QtGui.QPushButton('Start worker')
self.pw = pg.PlotWidget(self)
pi = self.pw.getPlotItem()
pi.enableAutoRange('x', True)
pi.enableAutoRange('y', True)
self.ge1 = pi.plot(pen='y')
self.xs = []
self.ys = []
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.pw)
layout.addWidget(self.btn)
self.resize(400, 400)
def run(self):
self.workerThread = QtCore.QThread()
self.worker.termino.connect(self.setTermino)
self.worker.moveToThread(self.workerThread)
# moveToThread doesn't work here
# self.btn.clicked.connect(partial(self.worker.run, 30000))
# moveToThread will work here
# assume def worker.run(self): instead of def worker.run(self, m=30000)
#self.btn.clicked.connect(self.worker.run)
self.relay.connect(self.worker.run)
self.btn.clicked.connect(self.relay_signal)
self.btn.clicked.connect(self.graficar)
self.workerThread.start()
self.show()
def relay_signal(self):
self.relay.emit(500)
def setTermino(self):
self.termino = True
def graficar(self):
if not self.q.empty():
e1,e2,ciclos = self.q.get()
self.xs.append(ciclos)
self.ys.append(e1)
self.ge1.setData(y=self.ys, x=self.xs)
if not self.termino or not self.q.empty():
QtCore.QTimer.singleShot(1, self.graficar)
if __name__ == '__main__':
app = QtGui.QApplication([])
window = MainWindow()
QtCore.QTimer.singleShot(0, window.run);
sys.exit(app.exec_())
I also modified the graficar method to continue plotting (even after the thread is terminated) if there is still data in the queue. I think this might be why you needed the time.sleep in the QThread, which is now also removed.
Also regarding your comments in the code on where to place moveToThread, where it is now is correct. It should be before the call that connects the QThread slot to a signal, and the reason for this is discussed in this stack-overflow post: PyQt: Connecting a signal to a slot to start a background operation

Categories