PyQt Progress Bar Update using Thread - pyqt

I've been writing a program that used a MyWindow(QTableWidget) with a thread. A progress bar is displayed above the sub-window(self.win) displayed as a pop-up.
I want a green bar on the status bar to be displayed consecutively, however after resetting the Spyder-kernel, the green bar does not output continuously. And I want to run the 'stop'/'continue' alternately every time I click the push button. This hasn't been resolved for almost three days.
import sys, time
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtCore import QThread, pyqtSignal, pyqtSlot, QRect
from PyQt5.QtWidgets import *
progVal = 0
class thread(QThread):
signalTh = pyqtSignal(int)
def __init__(self, *args):
super().__init__()
self.flag = True
def run(self):
global progVal
if self.flag:
self.signalTh.emit(progVal)
time.sleep(0.1)
def stop(self):
self.flag = False
self.quit()
self.wait(2)
class MyWindow(QTableWidget):
def __init__(self):
global progVal
super().__init__()
self.setupUi()
self.show()
self.test = thread(None)
self.test.signalTh.connect(self.signal_function)
self.test.run()
self.saveData()
def saveData(self):
global progVal
counts = range(1, 51)
for row in counts:
progVal = int(row/len(counts)*100)
self.test.signalTh.emit(progVal)
time.sleep(0.1)
def click1_function(self):
if self.test.flag:
self.test.stop()
self.pb_start.setText('Start!')
else:
self.test.flag = True
self.test.run()
self.pb_start.setText('Stop!')
#pyqtSlot(int)
def signal_function(self, val):
self.progress.setValue(val)
self.progress.update()
self.win.update()
self.update()
def setupUi(self):
self.resize(500, 400)
self.pb_start = QPushButton(self)
self.pb_start.setGeometry(QRect(80, 20, 100, 50))
self.pb_start.setText("Start")
self.pb_start.clicked.connect(self.click1_function)
self.win = QDialog(self)
self.win.resize(330, 100)
self.progress = QProgressBar(self.win)
self.progress.setGeometry(10, 10, 300, 30)
self.progress.setMaximum(100)
self.win.show()
def closeEvent(self, event):
quit_msg = "Are you sure you want to exit the program?"
reply = QMessageBox.question(self, 'Message', quit_msg, QMessageBox.Yes, QMessageBox.No)
if reply == QMessageBox.Yes:
self.test.stop()
event.accept()
else:
event.ignore()
if __name__ == "__main__":
app = QApplication(sys.argv)
myApp = MyWindow()
myApp.show()
app.exec_()

Related

Connecting external threaded log to PyQt5 QPlainTextEdit

I'm new to PyQt and handlers in general, I tried to read from every repo I found but I can't figure out how to fix my issue, as you can see in my code, I'm executing logging thread in the background and I'm trying to show the logs in my QPlainTextEdit console - for some reason I can see the logs in my terminal and the text_box doesn't get the logs at all,
I will appreciate a lot your smart help.
import pandas as pd
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import time
import os
import threading
import json
class ConsolePanelHandler(logging.Handler):
def __init__(self, stream):
# super().__init__()
logging.Handler.__init__(self)
# logging.StreamHandler.__init__(self, stream)
self.stream = stream
def handle(self, record):
rv = self.filter(record)
if rv:
self.acquire()
try:
self.emit(record)
finally:
self.release()
return rv
def emit(self, record):
try:
stream = self.stream
stream(self.format(record))
except RecursionError:
raise
except Exception:
self.handleError(self.format(record))
def thread():
for index in range(20):
logging.warning('scheiBe '+str(index))
time.sleep(1)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
layout = QVBoxLayout()
self.continue_ = QPushButton("Continue")
self.continue_.setStyleSheet("background-color: green")
self.continue_.setFont(QFont('SansSerif', 10))
self.continue_.setFixedSize(QSize(300, 22))
self.pause = QPushButton("Pause")
self.pause.setStyleSheet("background-color: orange")
self.pause.setFont(QFont('SansSerif', 10))
self.pause.setFixedSize(QSize(300, 22))
self.stop = QPushButton("Stop")
self.stop.setStyleSheet("background-color: #FD4B4B")
self.stop.setFont(QFont('SansSerif', 10))
self.stop.setFixedSize(QSize(300, 22))
self.text_box = QPlainTextEdit()
self.text_box.setPlaceholderText("Bugs will be printed here")
self.text_box.setReadOnly(True)
logging.getLogger().addHandler(self.text_box)
logging.getLogger().setLevel(logging.DEBUG)
ConsolePanelHandler(self.appendDebug , logging.DEBUG)
self.text_box.moveCursor(QTextCursor.End)
layout.addWidget(self.continue_)
layout.addWidget(self.pause)
layout.addWidget(self.stop)
layout.addWidget(self.text_box)
self.w = QWidget()
self.w.setLayout(layout)
self.setCentralWidget(self.w)
thread1 = threading.Thread(target=thread, args=(), daemon=True)
thread1.start()
self.show()
def closeEvent(self, event):
close = QMessageBox()
close.setText("Are you sure want to stop and exit?")
close.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel)
close = close.exec()
if close == QMessageBox.Yes:
sys.exit()
else:
event.ignore()
def appendDebug(self, string):
self.text_box.appendPlainText(string +'\n')
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
sys.exit(app.exec())
First, you should not pass the function that updates the text_area instead you should create a pyqtSignal which updates the text_area and pass it to ConsolePanelHandler.
Add ConsolePanelHandler as handler not text_area to logging.getLogger().addHandler()
and it is recommended to use QThread instead of threading.Thread
here is the complete updated code.
# import pandas as pd
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import time
import os
import sys
import threading
import json
import logging
class ConsolePanelHandler(logging.Handler):
def __init__(self, sig):
# super().__init__()
logging.Handler.__init__(self)
# logging.StreamHandler.__init__(self, stream)
self.stream = sig
def handle(self, record):
rv = self.filter(record)
if rv:
self.acquire()
try:
self.emit(record)
finally:
self.release()
return rv
def emit(self, record):
try:
self.stream.emit(self.format(record))
except RecursionError:
raise
except Exception:
self.handleError(record)
class thread(QThread):
def run(self) -> None:
for index in range(20):
logging.warning('scheiBe ' + str(index))
self.sleep(1)
class MainWindow(QMainWindow):
sig = pyqtSignal(str)
def __init__(self):
super().__init__()
self.layout = QVBoxLayout()
self.continue_ = QPushButton("Continue")
self.continue_.setStyleSheet("background-color: green")
self.continue_.setFont(QFont('SansSerif', 10))
self.continue_.setFixedSize(QSize(300, 22))
self.pause = QPushButton("Pause")
self.pause.setStyleSheet("background-color: orange")
self.pause.setFont(QFont('SansSerif', 10))
self.pause.setFixedSize(QSize(300, 22))
self.stop = QPushButton("Stop")
self.stop.setStyleSheet("background-color: #FD4B4B")
self.stop.setFont(QFont('SansSerif', 10))
self.stop.setFixedSize(QSize(300, 22))
self.c = ConsolePanelHandler(self.sig)
self.text_box = QPlainTextEdit()
self.text_box.setPlaceholderText("Bugs will be printed here")
self.text_box.setReadOnly(True)
logging.getLogger().addHandler(self.c)
logging.getLogger().setLevel(logging.DEBUG)
self.sig.connect(self.appendDebug)
self.text_box.moveCursor(QTextCursor.End)
self.layout.addWidget(self.continue_)
self.layout.addWidget(self.pause)
self.layout.addWidget(self.stop)
self.layout.addWidget(self.text_box)
self.w = QWidget()
self.w.setLayout(self.layout)
self.setCentralWidget(self.w)
self.thread1 = thread(self) # self is parent for Qthread so Qthread will be destroyed when it's parent no longer exist
self.thread1.start()
self.show()
def closeEvent(self, event):
close = QMessageBox()
close.setText("Are you sure want to stop and exit?")
close.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel)
close = close.exec()
if close == QMessageBox.Yes:
self.thread1.terminate()
sys.exit()
else:
event.ignore()
#pyqtSlot(str)
def appendDebug(self, string):
self.text_box.appendPlainText(string + '\n')
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
sys.exit(app.exec())

Catch final signal from QTimer object

I have a simple PyQt script. When I click a button, it starts a QTimer object and increments a progress bar. What I want is to change the label of my text when my progress bar reaches 100%. It worked for me once, but I can't get it to work anymore. What am I doing wrong?
Here's the main part of my code.
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('QProgressBar demo')
self.timerButton = QPushButton("Start", self)
self.timerButton.clicked.connect(self.timerStart)
self.timerObject = QTimer(self)
#self.timerObject.destroyed.connect(lambda:self.timerButton.setText("Finished") )
self.timerObject.destroyed.connect(lambda:print("Called" ))
self.progressBar = QProgressBar(self)
self.progressBar.setGeometry(10, 20, 290, 25)
self.timerButton.move(110,150)
self.progressBar.move(10,100)
self.increment = 0
self.resize(300, 300)
self.show()
#pyqtSlot()
def headsUp(self):
if(self.increment >= 100):
self.timerObject.stop()
else:
self.increment += 1
self.progressBar.setValue(self.increment)
return
def timerStart(self):
if (self.timerObject.isActive()):
self.timerObject.stop()
self.timerButton.setText("Resume")
else:
self.timerObject.timeout.connect(self.headsUp)
self.timerButton.setText("Pause")
self.timerObject.start(100)
destroyed is only issued when you delete the object, that a QTimer peer does not imply that it is deleted from memory, therefore it does not emit that signal, a possible solution is to create a signal for the QProgressBar when the value takes the maximum value as shown below:
import sys
from PyQt5 import QtCore, QtWidgets
class ProgressBar(QtWidgets.QProgressBar):
finished = QtCore.pyqtSignal()
def __init__(self, *args, **kwargs):
super(ProgressBar, self).__init__(*args, *kwargs)
self.valueChanged.connect(self.on_valueChanged)
#QtCore.pyqtSlot(int)
def on_valueChanged(self, val):
if val == self.maximum():
self.finished.emit()
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('QProgressBar demo')
self.timerButton = QtWidgets.QPushButton("Start", self)
self.timerButton.clicked.connect(self.timerStart)
self.timerObject = QtCore.QTimer(self)
self.progressBar = ProgressBar(self)
self.progressBar.finished.connect(lambda: print("Called" ))
self.increment = 0
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.progressBar)
lay.addWidget(self.timerButton)
self.resize(300, 300)
#QtCore.pyqtSlot()
def headsUp(self):
if self.increment >= 100:
self.timerObject.stop()
else:
self.increment += 1
self.progressBar.setValue(self.increment)
#QtCore.pyqtSlot()
def timerStart(self):
if self.timerObject.isActive():
self.timerObject.stop()
self.timerButton.setText("Resume")
else:
self.timerObject.timeout.connect(self.headsUp)
self.timerButton.setText("Pause")
self.timerObject.start(100)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
Another best option is to use QTimeLine and its finished signal:
import sys
from PyQt5 import QtCore, QtWidgets
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('QProgressBar demo')
self.timerButton = QtWidgets.QPushButton("Start", self)
self.timerButton.clicked.connect(self.timerStart)
self.timerObject = QtCore.QTimeLine(1000, self)
self.timerObject.setFrameRange(0, 100)
self.progressBar = QtWidgets.QProgressBar(self)
self.timerObject.frameChanged.connect(self.progressBar.setValue)
self.timerObject.finished.connect(lambda: print("Called" ))
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.progressBar)
lay.addWidget(self.timerButton)
self.resize(300, 300)
#QtCore.pyqtSlot()
def timerStart(self):
if self.timerObject.state() == QtCore.QTimeLine.Running:
self.timerObject.stop()
self.timerButton.setText("Resume")
else:
self.timerButton.setText("Pause")
self.timerObject.resume()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
This isn't working because you are connecting to the timers destoryed signal, but the timer is not being destroyed. To use this code as is, call self.timerObject.deleteLater() after you stop the timer.

Pyqt5 stop QThread worker on QAction

hello I have QTheard worker that start with click at QAction.
....
self.start_update = QAction('&Start', self)
self.start_update.triggered.connect(self._start_thread)
self.stop_update.setVisible(True)
self.stop_update.triggered.connect(self._stop_thread)
self.stop_update.setVisible(False)
...
...
def _start_thread(self):
self.start_update.setVisible(False)
self.stop_update.setVisible(True)
self.myworker.start()
...
...
def _stop_thread(self):
self.myworker.stop() # That close app with error
self.stop_update.setVisible(False)
self.start_update.setVisible(True)
...
Please help how to close terminate Worker correctly. Atm worker have __del__ and run (logic) method. Maybe need to add some method for correct worker closing?
Your application might look like this:
import sys
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class AThread(QThread):
threadSignalAThread = pyqtSignal(int)
def __init__(self):
super().__init__()
def run(self):
count = 0
while count < 1000:
QThread.msleep(200)
count += 1
self.threadSignalAThread.emit(count)
class MsgBoxAThread(QDialog):
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
self.label = QLabel("")
layout.addWidget(self.label)
close_btn = QPushButton("Close window")
layout.addWidget(close_btn)
close_btn.clicked.connect(self.close)
self.setGeometry(900, 300, 400, 80)
self.setWindowTitle('MsgBox AThread(QThread)')
class Example(QMainWindow):
def __init__(self, parent=None, *args):
super().__init__(parent, *args)
self.setWindowTitle("Pyqt5 stop QThread worker on QAction")
self.setGeometry(550, 300, 300, 300)
centralWidget = QWidget(self)
self.setCentralWidget(centralWidget)
layout = QVBoxLayout(centralWidget)
self.lbl = QLabel("Start")
layout.addWidget(self.lbl)
bar = self.menuBar()
barThread = bar.addMenu('Thread')
quit = bar.addMenu('Quit')
quit.aboutToShow.connect(app.quit)
self.start_update = QAction('&Start', self)
self.start_update.setShortcut('Ctrl+S')
self.start_update.triggered.connect(self._start_thread)
self.stop_update = QAction('Sto&p', self)
self.stop_update.setShortcut('Ctrl+P')
self.stop_update.setVisible(False)
self.stop_update.triggered.connect(self._stop_thread)
barThread.addAction(self.start_update)
barThread.addAction(self.stop_update)
self.msg = MsgBoxAThread()
self.myworker = None
self.counter = 0
self.timer = QTimer()
self.timer.setInterval(1000)
self.timer.timeout.connect(self.recurring_timer)
self.timer.start()
self.show()
def recurring_timer(self):
self.counter += 1
self.lbl.setText(" Do something in the GUI: <b> %d </b>" % self.counter)
def _start_thread(self):
self.start_update.setVisible(False)
self.stop_update.setVisible(True)
self.myworker = AThread()
self.myworker.threadSignalAThread.connect(self.on_threadSignalAThread)
self.myworker.finished.connect(self.finishedAThread)
self.myworker.start()
def finishedAThread(self):
self.myworker = None
self.start_update.setVisible(True)
self.stop_update.setVisible(False)
def on_threadSignalAThread(self, value):
self.msg.label.setText(str(value))
if not self.msg.isVisible():
self.msg.show()
def _stop_thread(self):
self.stop_update.setVisible(False)
self.start_update.setVisible(True)
self.myworker.terminate()
self.myworker = None
def closeEvent(self, event):
reply = QMessageBox.question(self, 'Question',
"Are you sure you want to close the application?",
QMessageBox.Yes,
QMessageBox.No)
if reply == QMessageBox.Yes:
if self.myworker:
self.myworker.quit()
del self.myworker
self.msg.close()
super(Example, self).closeEvent(event)
else:
event.ignore()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
#ex.show()
sys.exit(app.exec_())

Reset QProgressBar for another run

I have a ProgressBar for downloading data.
Sometimes the task can timeout and if so I want to be able to restart the process.
Task is started
Timer is started
if the timer finishes, end the task
press the button to restart the task
All of the above works fine except for the progressbar does not start again.
I can reset it to 0 but it will not move (before that it was stuck at 100%!)
If the task finishes before the timer runs out, the progress bar works fine again.
How can I restart the progressbar if the timer runs out before the task?
import sys
from PyQt5.QtWidgets import QWidget, QApplication, QProgressBar, QPushButton
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class DownloadThread(QThread):
taskFinished = pyqtSignal()
def __init__(self, parent=None):
super(DownloadThread, self).__init__(parent)
def run(self):
for i in range(20000):
print(i)
self.taskFinished.emit()
class Example(QWidget):
def __init__(self):
super(Example, self).__init__()
self.initUI()
def initUI(self):
# Set Progress Bard
self.progressBar = QProgressBar(self)
self.progressBar.setGeometry(30, 40, 200, 25)
# Button
self.btn = QPushButton('Start', self)
self.btn.move(40, 80)
self.btn.clicked.connect(self.start_download_thread)
# Set Timer
self.timer = QBasicTimer()
self.step = 0
# Display
self.setGeometry(300, 300, 280, 170)
self.show()
def start_download_thread(self):
# Set Progress bar to 0%
self.progressBar.setValue(0)
#Start Thread
self.Download = DownloadThread()
self.Download.taskFinished.connect(self.onDownloaded)
self.onDownloading()
self.Download.start()
def onDownloading(self):
#start the timer
self.progressBar.show()
self.timerSwitch()
def timerSwitch(self):
# Turn timer off or on
if self.timer.isActive():
self.timer.stop()
else:
self.timer.start(2, self)
def onDownloaded(self):
# Stop the timer
self.timerSwitch()
# progress bar 100%
self.progressBar.setValue(1)
self.progressBar.setRange(0, 1)
def timerEvent(self, e):
if self.step >= 100:
self.timer.stop()
# self.Download.quit()
return
self.step = self.step + 1
self.progressBar.setValue(self.step)
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
I solved this by moving the Qtimer() step into the start_download_thread
def start_download_thread(self):
self.step = 0 # reset the steps to 0
This means it resets the steps and the progress bar starts again.
import sys
from PyQt5.QtWidgets import QWidget, QApplication, QProgressBar, QPushButton
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class DownloadThread(QThread):
taskFinished = pyqtSignal()
def __init__(self, parent=None):
super(DownloadThread, self).__init__(parent)
def run(self):
for i in range(20000):
print(i)
self.taskFinished.emit()
class Example(QWidget):
def __init__(self):
super(Example, self).__init__()
self.initUI()
def initUI(self):
# Set Progress Bard
self.progressBar = QProgressBar(self)
self.progressBar.setGeometry(30, 40, 200, 25)
# Button
self.btn = QPushButton('Start', self)
self.btn.move(40, 80)
self.btn.clicked.connect(self.start_download_thread)
# Set Timer
self.timer = QBasicTimer()
# Display
self.setGeometry(300, 300, 280, 170)
self.show()
def start_download_thread(self):
self.step = 0
# Set Progress bar to 0%
self.progressBar.setValue(0)
#Start Thread
self.Download = DownloadThread()
self.Download.taskFinished.connect(self.onDownloaded)
self.onDownloading()
self.Download.start()
def onDownloading(self):
#start the timer
self.progressBar.show()
self.timerSwitch()
def timerSwitch(self):
# Turn timer off or on
if self.timer.isActive():
self.timer.stop()
else:
self.timer.start(2, self)
def onDownloaded(self):
# Stop the timer
self.timerSwitch()
# progress bar 100%
self.progressBar.setValue(100)
self.progressBar.setRange(0, 100)
def timerEvent(self, e):
if self.step >= 100:
self.timer.stop()
return
self.step = self.step + 1
self.progressBar.setValue(self.step)
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

wxPython threading wxGauge

I'm trying to learn about threading with wxPython, so I tried to make this simple example work, but it seems that the progress bar updates happen after the "long process" is done. Could anyone tell me why this is not working ?
#!/usr/bin/env python
import time
import wx
from wx.lib.pubsub import Publisher as pub
class Window(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title="wxGauge")
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.gauge = wx.Gauge(self, range=10)
self.btn = wx.Button(self, label="Start process")
self.sizer.Add(self.gauge)
self.sizer.Add(self.btn)
self.SetSizerAndFit(self.sizer)
pub.subscribe(self.update, "update")
self.btn.Bind(wx.EVT_BUTTON, self.OnClick)
self.Show()
def update(self, msg):
self.gauge.SetValue(msg.data)
def OnClick(self, evt):
for i in range(10):
wx.CallAfter(pub.sendMessage, "update", i + 1)
time.sleep(1)
if __name__ == '__main__':
app = wx.App()
Window()
app.MainLoop()
It is because of the usage of "CallAfter". If call "sendMessage" in this way, it is actually called after the event handler finished.
You can try it in this way:
for i in range(10):
pub.sendMessage("update", i+1)
time.sleep(1)
EDIT:
put this "sendMessage" out of the event handler.
import time
import wx
from wx.lib.pubsub import Publisher as pub
class Window(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title="wxGauge")
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.gauge = wx.Gauge(self, range=10)
self.btn = wx.Button(self, label="Start process")
self.sizer.Add(self.gauge)
self.sizer.Add(self.btn)
self.SetSizerAndFit(self.sizer)
pub.subscribe(self.update, "update")
self.btn.Bind(wx.EVT_BUTTON, self.OnClick)
self.Bind(wx.EVT_TIMER, self.TimerHandler)
self.timer = wx.Timer(self)
self.gaugeIndex = 0
self.Show()
def TimerHandler(self, event):
self.gaugeIndex += 1
pub.sendMessage("update", self.gaugeIndex)
if self.gaugeIndex == 10:
self.timer.Stop()
def update(self, msg):
self.gauge.SetValue(msg.data)
def OnClick(self, evt):
self.timer.Start(1000)
if __name__ == '__main__':
app = wx.App(redirect=False)
Window()
app.MainLoop()

Resources