PyQt5 launch external process with real time output - python

I did a lot of research, but I didn't find a good solution.
I want to create a Gui that has a small area where I can launch an external program and show its output in real time in a text area. This program is console application. In addition, I must have the possibility to stop it in any time (start/stop/re-start).

An example with a thread that outputs the second window and outputs the result from the stream in the main window.
Comments in the text of the program:
import random
from PyQt5 import Qt
class WorkThread(Qt.QThread):
''' Streaming task in its window.
Signals and Slots are used for communication between objects. '''
# Declare a signal, with an argument (int) for transmission in the connected slots
threadSignal = Qt.pyqtSignal(int)
def __init__(self, startParm):
super().__init__()
self.startParm = startParm # Initialize the parameters passed to the task
def run(self, *args, **kwargs):
c = self.startParm
while True:
Qt.QThread.msleep(200)
c += 1
self.threadSignal.emit(c) # We disable the signal and pass arguments to the connected slot
class WorkThreadMain(Qt.QThread):
''' Streaming Main task '''
threadSignalMain = Qt.pyqtSignal(int)
def __init__(self, startParm):
super().__init__()
self.startParm = startParm
def run(self, *args, **kwargs):
c = self.startParm
while True:
Qt.QThread.msleep(1000)
c += 1
self.threadSignalMain.emit(c)
class MsgBox(Qt.QDialog):
""" Window initialization class for visualizing an additional stream
         and a button to close the stream window if the thread is stopped! """
def __init__(self):
super().__init__()
layout = Qt.QVBoxLayout(self)
self.label = Qt.QLabel("")
layout.addWidget(self.label)
close_btn = Qt.QPushButton("Close thread")
layout.addWidget(close_btn)
close_btn.clicked.connect(self.close)
self.setGeometry(900, 65, 400, 80)
self.setWindowTitle('MsgBox for WorkThread')
class MainWindow(Qt.QWidget):
''' Main Window '''
def __init__(self):
super().__init__()
layout = Qt.QVBoxLayout(self)
self.labelMain = Qt.QLabel("The result of the Main task: ")
layout.addWidget(self.labelMain)
self.labelThread = Qt.QLabel("The result of the Thread task: ")
layout.addWidget(self.labelThread)
validator = Qt.QIntValidator(1, 999, self)
validator.setBottom(1)
self.lineEdit = Qt.QLineEdit()
self.lineEdit.setPlaceholderText("Enter the initial parameter for the stream task")
self.lineEdit.setValidator(validator) # self.lineEdit will only take integers from 1 to 999
layout.addWidget(self.lineEdit)
self.btn = Qt.QPushButton("Start thread!")
layout.addWidget(self.btn)
self.btnMain = Qt.QPushButton("Start Main!")
layout.addWidget(self.btnMain)
self.setGeometry(550, 65, 300, 200)
self.setWindowTitle('MainWindow')
self.btn.clicked.connect(self.on_btn)
self.btnMain.clicked.connect(self.on_btnMain)
self.msg = MsgBox()
self.thread = None
self.threadMain = None
def on_btn(self):
''' Starting or Stopping an Additional Stream-WorkThread from the main window '''
# Input parameters for transfer to the stream, if not specified, we pass default `0`
startParm = int(self.lineEdit.text()) if self.lineEdit.text()!="" else 0
if self.thread is None:
self.thread = WorkThread(startParm)
self.thread.threadSignal.connect(self.on_threadSignal)
self.thread.start()
self.btn.setText("Stop thread")
self.lineEdit.hide()
else:
self.thread.terminate()
self.thread = None
self.btn.setText("Start thread")
self.lineEdit.show()
def on_threadSignal(self, value):
''' Visualization of streaming data-WorkThread. '''
self.msg.label.setText(str(value))
self.labelThread.setText("The result of the Thread task: " + str(value)) # We show also in the main window
# We restore the rendering of the stream window if it was closed. The flow is working.
if not self.msg.isVisible():
self.msg.show()
def on_btnMain(self):
''' Starting or Stopping the Main Thread-WorkThreadMain '''
cM = random.randrange(1, 100)
if self.threadMain is None:
self.threadMain = WorkThreadMain(cM)
self.threadMain.threadSignalMain.connect(self.on_threadSignalMain)
self.threadMain.start()
self.btnMain.setText("Stop Main")
else:
self.threadMain.terminate()
self.threadMain = None
self.btnMain.setText("Start Main")
def on_threadSignalMain(self, value):
''' Visualization of streaming data WorkThreadMain '''
self.labelMain.setText("The result of the Main task: " + str(value))
if __name__ == '__main__':
app = Qt.QApplication([])
mw = MainWindow()
mw.show()
app.exec()

Related

Qthread isn't working Not returning signals

i have this WorkerSignals class which used to connect signals with the Qthread class worker, SaveToExcel() is a function that i used to run in Qthread.
class WorkerSignals(QObject):
finished = pyqtSignal()
error = pyqtSignal(tuple)
result = pyqtSignal(object)
progress = pyqtSignal(int)
class Worker(QThread):
def __init__(self,query,filename,choices,fileExtension,iterativeornot):
super(Worker,self).__init__()
self.signals = WorkerSignals()
self.query =query
self.filename = filename
self.choices = choices
self.fileExtension = fileExtension
self.iterativeornot =iterativeornot
#pyqtSlot()
def run(self):
try:
SaveToExcel(self.query,self.filename,self.choices,self.fileExtension,self.iterativeornot)
except:
self.signals.result.emit(1)
finally:
self.signals.finished.emit()
this is the class that i used to create the Qwidget that has ui
class AnotherWindow(QWidget):
def __init__(self,windowname):
super().__init__()
self.layout = QVBoxLayout()
self.label = QLabel()
self.setWindowTitle(windowname)
self.setWindowIcon(QIcon(os.path.join(basedir,'./images/import.png')))
self.setFixedSize(460,440)
self.layout.addWidget(self.label)
# Query
self.textinput = QPlainTextEdit()
self.layout.addWidget(self.textinput)
self.qhboxlayout1 = QHBoxLayout()
self.IterativeRadiobtn1 = QRadioButton('All Locations')
self.IterativeRadiobtn2 = QRadioButton('Current Locations')
self.IterativeRadiobtn2.setChecked(True)
self.qhboxlayout1.addWidget(self.IterativeRadiobtn1)
self.qhboxlayout1.addWidget(self.IterativeRadiobtn2)
self.layout.addLayout(self.qhboxlayout1)
# Check boxes
self.c1 = QCheckBox("sc",self)
self.c2 = QCheckBox("ad",self)
self.c3 = QCheckBox("sr",self)
self.c4 = QCheckBox("fc",self)
self.hboxlayoutchoices = QHBoxLayout()
#adding checkboxes to layout
self.checkboxlist = [self.c1,self.c2,self.c3,self.c4]
for cbox in self.checkboxlist:
self.hboxlayoutchoices.addWidget(cbox)
self.layout.addLayout(self.hboxlayoutchoices)
# filename
self.filename = QLineEdit()
self.layout.addWidget(self.filename)
# Combo box to show the filetype which need to be saved
self.extensions = QComboBox()
self.combodict = {'Excel 97-2003 Workbook (*.xls)':'xls','CSV UTF-8 (Comma delimited) (*.csv)':'csv'}
self.extensions.addItems(self.combodict)
self.layout.addWidget(self.extensions)
# import button
self.exportBtn = QPushButton('Import')
self.layout.addWidget(self.exportBtn)
#import function when button clicked
self.exportBtn.clicked.connect(self.IMPORT)
#setting layout
self.setLayout(self.layout)
def RadioButtonCheck(self):
if self.IterativeRadiobtn1.isChecked():
return True
if self.IterativeRadiobtn2.isChecked():
return False
def IMPORT(self):
self.cboxlist = []
for cbox in self.checkboxlist:
if cbox.isChecked():
self.cboxlist.append(cbox.text())
self.textinput.setReadOnly(True)
self.filename.setReadOnly(True)
self.exportBtn.setDisabled(True)
self.saveFilename = self.filename.text()
self.text = self.textinput.toPlainText()
self.inputextension = self.extensions.currentText()
self.getvalue = self.combodict.get(self.inputextension)
self.truorfalse = self.RadioButtonCheck()
# self.queryThread = threading.Thread(target=SaveToExcel,args=(self.text,self.saveFilename,self.cboxlist,self.getvalue,self.truorfalse))
# self.queryThread.start()
self.worker = Worker(self.text,self.saveFilename,self.cboxlist,self.getvalue,self.truorfalse)
self.worktherad = QThread()
self.worker.moveToThread(self.worktherad)
self.worktherad.started.connect(self.worker.run)
self.worktherad.finished.connect(self.complete)
self.worktherad.start()
def complete(self):
self.msg = QMessageBox()
self.msg.setWindowTitle("Status")
self.msg.setText("Import Done")
self.msg.exec()
self.textinput.setReadOnly(False)
self.filename.setReadOnly(False)
self.exportBtn.setDisabled(False)
self.exportBtn.setText("Import Again")
but when i click the import button the function won't run and just do nothing, I don't have a good knowledge about Qthreading but when i use the python default threading the function will run and import the datas. Still i don't have good clear idea about how to implent the Qthreading for the SaveToExcel function.
self.worker = Worker(self.text,self.saveFilename,self.cboxlist,self.getvalue,self.truorfalse)
in this line you should probably pass the parent field and you should accept the parent field in Worker __init__ method and pass it in super call
(so the thread will automatically destroyed once it's parent object is deleted)
and the Worker class is already a QThread you do not need to create another QThread and move it..
you should just run the self.worker by self.worker.start()
and don't forget to connect those Worker signals to valid pyqtSlot and if possible then connect those before starting the self.worker thread
Updated Code Snippet
self.worker = Worker(parent, self.text,self.saveFilename,self.cboxlist,self.getvalue,self.truorfalse) # Accept parent in __init__ method of Worker
self.worktherad.finished.connect(self.complete)
self.worktherad.start()
And also make complete function a pyqtSlot by adding decorator QtCore.pyqtSlot()

Emit signals from Worker threads to Main window and back using #pyqtSlot decorators

I'm currently learning to build GUI's using PyQt5 and here is my problem:
I have 2 Threads. The first one is QThread object which generates some data.
The other one is QObject function which runs in QThread using .moveToThread method and processes data received from my so-called data generator. On main window I emit signal to do some stuff with value returned from generator separately from main thread in order to avoid freezing GUI.
The processing looks like:
receive data;
append it to list;
if list is full enough to make some calculations -> emit the result back to GUI;
wait some time (e.g. 3 secs) and start again the reception of data.
The problem is in the last point. I freeze the thread after calculations on list items, but it does not block signal emitted from the GUI, so when time.sleep(3) is up, my list already contains 3 items, but I need to start it from 0.
Commented fragments of code are my thoughts on how to do it using #pyqtSlot decorators and QtCore.pyqtSignal() but it doesn`t work.
Let me know if any clarification is needed, maybe I missed something.
Will be glad for any help.
Here is my code:
import sys
from PyQt5.QtWidgets import QWidget, QPushButton, QApplication, QGridLayout, QLabel
from PyQt5.QtCore import QObject, QThread, pyqtSlot
from PyQt5 import QtCore
import random
import time
class DataThread(QThread):
output = QtCore.pyqtSignal(list)
def __init__(self):
super().__init__()
self._run_flag = True
def run(self):
while self._run_flag:
value = round(random.random(), 4)
value_x2 = value * 2
values = [value, value_x2]
self.output.emit(values)
# print(value)
time.sleep(1)
def stop(self):
self._run_flag = False
self.quit()
class CalculationsThread(QObject):
# accepting signals from main data generator thread
calculations_result = QtCore.pyqtSignal(float)
calc_value = QtCore.pyqtSignal(float)
# emitting signal from this thread
kill = QtCore.pyqtSignal()
deny_access = QtCore.pyqtSignal(bool)
def __init__(self, parent=None):
super(CalculationsThread, self).__init__(parent)
self.list_of_heights = []
#pyqtSlot(float)
def work(self, value):
# self.deny_access.emit(True)
self.list_of_heights.append(value)
self.calculations_result.emit(len(self.list_of_heights))
if len(self.list_of_heights) == 10:
result = sum(self.list_of_heights) / len(self.list_of_heights)
self.calculations_result.emit(result)
self.list_of_heights = []
# self.deny_access.emit(False)
time.sleep(3)
class WinForm(QWidget):
variable_value = QtCore.pyqtSignal(float)
def __init__(self, parent=None):
super(WinForm, self).__init__(parent)
self.setWindowTitle('QThreads example')
self.change_flag = 0
self.calculations_flag = 0
self.list_from_data_generator_thread = []
self.thread = None
self.calculations_worker = None
self.thread1 = None
self.label = QLabel('Label')
self.startBtn = QPushButton('Start')
self.startBtn.clicked.connect(self.start_data_generator)
self.endBtn = QPushButton('Stop')
self.endBtn.clicked.connect(self.stop_data_generator)
self.output1_btn = QPushButton("Value 1")
self.output1_btn.clicked.connect(self.show_value_1)
self.output2_btn = QPushButton("Value 2")
self.output2_btn.clicked.connect(self.show_value_2)
self.calculations_label = QLabel("Calculations result: ")
self.calculations_button = QPushButton("Start calculations")
self.calculations_button.clicked.connect(self.start_calculations)
layout = QGridLayout()
layout.addWidget(self.label, 0, 0)
layout.addWidget(self.startBtn, 1, 0)
layout.addWidget(self.endBtn, 1, 1)
layout.addWidget(self.output1_btn, 2, 0)
layout.addWidget(self.output2_btn, 2, 1)
layout.addWidget(self.calculations_label, 3, 0)
layout.addWidget(self.calculations_button, 3, 1)
self.setLayout(layout)
#pyqtSlot(list)
# #pyqtSLot(bool)
def set_label_text(self, value):
# if access_flag:
self.variable_value.emit(value[1])
# if not access flag:
# pass
if self.change_flag == 0:
self.label.setText(f"1: {value[0]}")
if self.change_flag == 1:
self.label.setText(f"2: {value[1]}")
def start_data_generator(self):
self.startBtn.setEnabled(False)
self.endBtn.setEnabled(True)
self.thread = DataThread()
self.thread.output.connect(self.set_label_text)
self.thread.start()
self.endBtn.clicked.connect(self.thread.stop)
def stop_data_generator(self):
self.thread.output.disconnect(self.set_label_text)
self.startBtn.setEnabled(True)
self.endBtn.setEnabled(False)
#pyqtSlot(float)
def output_calculations(self, value):
self.calculations_label.setText(f"Calculations result: {value}")
def start_calculations(self):
self.calculations_worker = CalculationsThread()
self.thread1 = QThread()
self.calculations_worker.moveToThread(self.thread1)
self.variable_value.connect(self.calculations_worker.work)
self.calculations_worker.calculations_result.connect(self.output_calculations)
self.calculations_worker.kill.connect(self.thread1.quit)
self.thread1.start()
self.calculations_button.clicked.disconnect(self.start_calculations)
self.calculations_button.setText("Stop calculations")
self.calculations_button.clicked.connect(self.thread1.quit)
self.calculations_button.clicked.connect(self.stop_calculations)
def stop_calculations(self):
self.calculations_worker.calculations_result.disconnect(self.output_calculations)
self.calculations_button.clicked.disconnect(self.stop_calculations)
self.calculations_button.clicked.disconnect(self.thread1.quit)
self.calculations_button.setText("Start calculations")
self.calculations_button.clicked.connect(self.start_calculations)
def show_value_1(self):
self.change_flag = 0
def show_value_2(self):
self.change_flag = 1
if __name__ == '__main__':
app = QApplication(sys.argv)
form = WinForm()
form.show()
sys.exit(app.exec_())

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()

PyQt5: How to 'communicate' between QThread and some sub-class?

To this question I am referring to the answer from #eyllanesc in PyQt5: How to scroll text in QTextEdit automatically (animational effect)?
There #eyllanesc shows how to make the text auto scrolls smoothly using verticalScrollBar(). It works great.
For this question, I have added some extra lines, to use QThread to get the text.
What I want to achieve here: to let the QThread class 'communicate' with the AnimationTextEdit class, so that the scrolling time can be determined by the text-length. So that the programm stops, when the scrolling process ends.
I must say it is very very tricky task for me. I would like to show the programm flow first, as I imagined.
UPDATE: My code is as follows. It works, but...
Problem with the code: when the text stops to scroll, the time.sleep() still works. The App wait there till time.sleep() stops.
What I want to get: When the text stops to scroll, the time.sleep() runs to its end value.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys
import time
import sqlite3
class AnimationTextEdit(QTextEdit):
# signal_HowLongIsTheText = pyqtSignal(int) # signal to tell the QThread, how long the text is
def __init__(self, *args, **kwargs):
QTextEdit.__init__(self, *args, **kwargs)
self.animation = QVariantAnimation(self)
self.animation.valueChanged.connect(self.moveToLine)
# def sent_Info_to_Thread(self):
# self.obj_Thread = Worker()
# self.signal_HowLongIsTheText.connect(self.obj_Thread.getText_HowLongIsIt)
# self.signal_HowLongIsTheText.emit(self.textLength)
# self.signal_HowLongIsTheText.disconnect(self.obj_Thread.getText_HowLongIsIt)
#pyqtSlot()
def startAnimation(self):
self.animation.stop()
self.animation.setStartValue(0)
self.textLength = self.verticalScrollBar().maximum()
# self.sent_Info_to_Thread()
self.animation.setEndValue(self.textLength)
self.animation.setDuration(self.animation.endValue()*4)
self.animation.start()
#pyqtSlot(QVariant)
def moveToLine(self, i):
self.verticalScrollBar().setValue(i)
class Worker(QObject):
finished = pyqtSignal()
textSignal = pyqtSignal(str)
# #pyqtSlot(int)
# def getText_HowLongIsIt(self, textLength):
# self.textLength = textLength
#pyqtSlot()
def getText(self):
longText = "\n".join(["{}: long text - auto scrolling ".format(i) for i in range(100)])
self.textSignal.emit(longText)
time.sleep(10)
# time.sleep(int(self.textLength / 100))
# My question is about the above line: time.sleep(self.textLength)
# Instead of giving a fixed sleep time value here,
# I want let the Worker Class know,
# how long it will take to scroll all the text to the end.
self.finished.emit()
class MyApp(QWidget):
def __init__(self):
super(MyApp, self).__init__()
self.setFixedSize(600, 400)
self.initUI()
self.startThread()
def initUI(self):
self.txt = AnimationTextEdit(self)
self.btn = QPushButton("Start", self)
self.layout = QHBoxLayout(self)
self.layout.addWidget(self.txt)
self.layout.addWidget(self.btn)
self.btn.clicked.connect(self.txt.startAnimation)
def startThread(self):
self.obj = Worker()
self.thread = QThread()
self.obj.textSignal.connect(self.textUpdate)
self.obj.moveToThread(self.thread)
self.obj.finished.connect(self.thread.quit)
self.thread.started.connect(self.obj.getText)
self.thread.finished.connect(app.exit)
self.thread.start()
def textUpdate(self, longText):
self.txt.append(longText)
self.txt.moveToLine(0)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MyApp()
window.show()
sys.exit(app.exec_())
Thanks for the help and hint. What have I done wrong?
Although in the animation the duration is established it is necessary to understand that this is not exact, this could vary for several reasons, so calculating using the sleep to wait for it to end in a certain time and just closing the application may fail.
If your main objective is that when the animation is finished the program execution is finished then you must use the finished QVariantAnimation signal to finish the execution of the thread, this signal is emited when it finishes executing.
class AnimationTextEdit(QTextEdit):
def __init__(self, *args, **kwargs):
QTextEdit.__init__(self, *args, **kwargs)
self.animation = QVariantAnimation(self)
self.animation.valueChanged.connect(self.moveToLine)
#pyqtSlot()
def startAnimation(self):
self.animation.stop()
self.animation.setStartValue(0)
self.textLength = self.verticalScrollBar().maximum()
self.animation.setEndValue(self.textLength)
self.animation.setDuration(self.animation.endValue()*4)
self.animation.start()
#pyqtSlot(QVariant)
def moveToLine(self, i):
self.verticalScrollBar().setValue(i)
class Worker(QObject):
textSignal = pyqtSignal(str)
#pyqtSlot()
def getText(self):
longText = "\n".join(["{}: long text - auto scrolling ".format(i) for i in range(100)])
self.textSignal.emit(longText)
class MyApp(QWidget):
def __init__(self):
super(MyApp, self).__init__()
self.setFixedSize(600, 400)
self.initUI()
self.startThread()
def initUI(self):
self.txt = AnimationTextEdit(self)
self.btn = QPushButton("Start", self)
self.layout = QHBoxLayout(self)
self.layout.addWidget(self.txt)
self.layout.addWidget(self.btn)
self.btn.clicked.connect(self.txt.startAnimation)
def startThread(self):
self.obj = Worker()
self.thread = QThread()
self.obj.textSignal.connect(self.textUpdate)
self.obj.moveToThread(self.thread)
self.txt.animation.finished.connect(self.thread.quit)
self.thread.started.connect(self.obj.getText)
self.thread.finished.connect(app.exit)
self.thread.start()
def textUpdate(self, longText):
self.txt.append(longText)
self.txt.moveToLine(0)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MyApp()
window.show()
sys.exit(app.exec_())

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