PyQT4 QWidget must receive 'close' signal twice before closing - python

I'm writing a PyQT4 (4.11) program that performs a long slow task which ideally needs a progress bar. The program works almost perfectly if I don't use threads, sub-classing a QWidget which only contains a QProgressBar and a layout. Instantiating this sub-class as form, I can call form.show() to put it on screen, and then my long slow loop can update the progress by calling form.progressbar.setValue(progress). There are two problems with this:
if the user tries to interact with the window they get 'not responding' messages from the windows manager/OS desktop process. This is because events are not being processed.
because events aren't being processed, the user can't cancel the long slow loop by closing the window.
So I tried making the long slow loop run in a separate thread, using a signal to update the progress bar. I overrode the closeEvent of my QWidget so that it can cancel interaction with the hardware device (all wrapped in mutexes so the device communication doesn't get out of sync). Again, this almost works. If I cancel, the application quits. If I leave it to run to completion though, I have to close the window manually (i.e. click the close icon or press alt-f4), even though I'm sending a close signal to the QWidget. As you can see in the code below there are some complications, since the app can't close immediately if cancelled, because it has to wait for some hardware clean up to happen. Here is a minimal version of my code
import sys
import os
import time
from PyQt4 import QtCore, QtGui
class Ui_ProgressBarDialog(QtGui.QWidget):
def __init__(self, on_close=None):
QtGui.QWidget.__init__(self)
self.setupUi(self)
self.center()
#on_close is a function that is called to cancel
#the long slow loop
self.on_close = on_close
def center(self):
qr = self.frameGeometry()
cp = QtGui.QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
def setupUi(self, ProgressBarDialog):
ProgressBarDialog.setObjectName(_fromUtf8("ProgressBarDialog"))
ProgressBarDialog.resize(400, 33)
self.verticalLayout = QtGui.QVBoxLayout(ProgressBarDialog)
self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
self.progressBar = QtGui.QProgressBar(ProgressBarDialog)
self.progressBar.setProperty("value", 0)
self.progressBar.setObjectName(_fromUtf8("progressBar"))
self.verticalLayout.addWidget(self.progressBar)
self.retranslateUi(ProgressBarDialog)
#This allows the long slow loop to update the progress bar
QtCore.QObject.connect(
self,
QtCore.SIGNAL("updateProgress"),
self.progressBar.setValue
)
#Catch the close event so we can interrupt the long slow loop
QtCore.QObject.connect(
self,
QtCore.SIGNAL("closeDialog"),
self.closeEvent
)
#Repaint the window when the progress bar's value changes
QtCore.QObject.connect(
self.progressBar,
QtCore.SIGNAL("valueChanged(int)"),
self.repaint
)
QtCore.QMetaObject.connectSlotsByName(ProgressBarDialog)
def retranslateUi(self, ProgressBarDialog):
ProgressBarDialog.setWindowTitle("Please Wait")
def closeEvent(self, event, force=False):
if self.on_close is not None and not force:
self.on_close()
app = QtGui.QApplication(sys.argv)
filename = str(QtGui.QFileDialog.getSaveFileName(
None,
"Save as",
os.getcwd(),
"Data files: (*.dat)"
))
loop_mutex = thread.allocate_lock()
cancelled = False
can_quit = False
result = None
def cancel_download():
global cancelled
if can_quit:
return
if QtGui.QMessageBox.question(
None,
'Cancel Download',
"Are you sure you want to cancel the download?",
QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
QtGui.QMessageBox.No) == QtGui.QMessageBox.Yes:
with loop_mutex:
selected_device.cancelDownload()
cancelled = True
while not can_quit:
time.sleep(0.25)
form = ProgressBarDialog.Ui_ProgressBarDialog(cancel_download)
form.setWindowTitle("Please Wait")
form.progressBar.setMaximum(1000)
form.progressBar.setValue(0)
form.show()
def long_slow_loop(mutex, filename):
global can_quit, result
progress = 0
temp_binary_file = open(filename, "wb")
#The iterator below does the actual work of interacting with a
#hardware device, so I'm locking around the "for" line. I must
#not fetch more data from the device while a cancel command is
#in progress, and vice versa
mutex.acquire()
for data in ComplexIteratorThatInteractsWithHardwareDevice():
mutex.release()
temp_binary_file.write(datablock)
progress += 1
form.emit(QtCore.SIGNAL("updateProgress"), progress)
mutex.acquire()
if cancelled:
break
mutex.release()
result = not cancelled
temp_binary_file.close()
if cancelled:
os.unlink(filename)
#having set can_quit to True the following emission SHOULD
#cause the app to exit by closing the last top level window
can_quit = True
form.emit(QtCore.SIGNAL("closeDialog"), QtGui.QCloseEvent(), True)
thread.start_new_thread(do_dump, (loop_mutex, filename))
app.exec_()
if result == True:
QtGui.QMessageBox.information(None, 'Success', "Save to disk successful", QtGui.QMessageBox.Ok)

It turns out the secret is to use a QThread which emits some signals when the thread exits. Either "finished" or "terminated" depending on whether the exit was normal or abnormal. Here's some code
import sys
import os
import time
from PyQt4 import QtCore, QtGui
class Ui_ProgressBarDialog(QtGui.QWidget):
def __init__(self, process, parent = None):
QtGui.QWidget.__init__(self, parent)
self.thread = process
self.setupUi(self)
self.center()
self.thread.start()
def center(self):
qr = self.frameGeometry()
cp = QtGui.QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
def setupUi(self, ProgressBarDialog):
ProgressBarDialog.setObjectName("ProgressBarDialog")
ProgressBarDialog.resize(400, 33)
self.verticalLayout = QtGui.QVBoxLayout(ProgressBarDialog)
self.verticalLayout.setObjectName("verticalLayout")
self.progressBar = QtGui.QProgressBar(ProgressBarDialog)
self.progressBar.setProperty("value", 0)
self.progressBar.setObjectName("progressBar")
self.verticalLayout.addWidget(self.progressBar)
self.retranslateUi(ProgressBarDialog)
#Close when the thread finishes (normally)
QtCore.QObject.connect(
self.thread,
QtCore.SIGNAL("finished()"),
self.close
)
#Close when the thread is terminated (exception, cancelled etc)
QtCore.QObject.connect(
self.thread,
QtCore.SIGNAL("terminated()"),
self.close
)
#Let the thread update the progress bar position
QtCore.QObject.connect(
self.thread,
QtCore.SIGNAL("updateProgress"),
self.progressBar.setValue
)
#Repaint when the progress bar value changes
QtCore.QObject.connect(
self.progressBar,
QtCore.SIGNAL("valueChanged(int)"),
ProgressBarDialog.repaint
)
QtCore.QMetaObject.connectSlotsByName(ProgressBarDialog)
def retranslateUi(self, ProgressBarDialog):
ProgressBarDialog.setWindowTitle("Please Wait")
def closeEvent(self, event):
if self.thread.exit_status == self.thread.RUNNING:
if QtGui.QMessageBox.question(None, 'Cancel Download', "Are you sure you want to cancel the download?", QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) == QtGui.QMessageBox.Yes:
if self.thread.exit_status == self.thread.RUNNING:
self.thread.exiting = True
while self.thread.exiting:
time.sleep(0.25)
elif self.thread.exit_status == self.thread.SUCCESS:
self.thread.exit_status = self.thread.CANCELLED
else:
if self.thread.exit_status == self.thread.RUNNING:
event.ignore()
app = QtGui.QApplication(sys.argv)
filename = str(QtGui.QFileDialog.getSaveFileName(
None,
"Save as",
os.getcwd(),
"Data files: (*.dat)"
))
class DeviceDataStreamHandler(QtCore.QThread):
RUNNING = 1
SUCCESS = 0
FAILURE = -1
CANCELLED = -2
class CancelledError(Exception):
pass
def __init__(self, parent = None, **kwargs):
QtCore.QThread.__init__(self, parent)
self.exiting = False
self.exit_status = DeviceDataStreamHandler.RUNNING
self.device = device
self.filename = filename
def run(self):
progress = 0
try:
temp_binary_file = open(self.filename, "wb")
#getDataStream is an interator returing strings
for data in self.device.getDataStream():
temp_binary_file.write(data)
progress += 1
self.emit(QtCore.SIGNAL("updateProgress"), progress)
if self.exiting:
raise DeviceDataStreamHandler.CancelledError()
self.exit_status = DeviceDataStreamHandler.SUCCESS
except DeviceDataStreamHandler.CancelledError:
self.exit_status = DeviceDataStreamHandler.CANCELLED
self.device.cancelDownload()
except Exception as E:
self.exit_status = DeviceDataStreamHandler.FAILURE
self.error_details = str(E)
finally:
temp_binary_file.close()
if self.exit_status == DeviceDataStreamHandler.CANCELLED:
os.unlink(filename)
self.exiting = False
class HardwareDeviceObject(object):
def __init__(self):
#initialises comms with a hardware device
self.event_count = 0
self.capture = False
def startCapture(self):
self.capture = True
def cancelDownload():
self.capture = False
def read(self):
#returns a string sent from the device
time.sleep(1)
self.event_count += 1
return self.event_count
def getDataStream(self):
class DataStreamIterator(object):
def __init__(self, ds, max_capture_count = 100):
self.ds = ds
self.capture_count = 0
self.max_capture_count = max_capture_count
def __iter__(self):
return self
def next(self):
#return string received from device
if self.ds.capture and self.capture_count < self.max_capture_count:
self.capture_count += 1
return self.ds.read()
else:
raise StopIteration()
self.startCapture()
return DataStreamIterator(self)
capture = DeviceDataStreamHandler(device = HardwareDeviceObject(), filename = filename)
form = ProgressBarDialog.Ui_ProgressBarDialog(capture)
form.setWindowTitle("Dumping sessions")
form.progressBar.setMaximum(100) #expect 100 outputs from the device
form.progressBar.setValue(0)
form.show()
app.exec_()
if capture.exit_status == DeviceDataStreamHandler.SUCCESS:
QtGui.QMessageBox.information(None, 'Success', "Save to disk successful", QtGui.QMessageBox.Ok)
elif capture.exit_status == DeviceDataStreamHandler.FAILURE:
QtGui.QMessageBox.critical(None, 'Error interacting with device', "{}".format(capture.error_details), QtGui.QMessageBox.Ok)

Related

Closing QWidget window

I am using following code to close Qwidget window automatically after some period of time
class ErrorWindow2(QtGui.QWidget):
def __init__( self ):
QtGui.QWidget.__init__( self, None, QtCore.Qt.WindowStaysOnTopHint)
msgBox = QMessageBox(self)
msgBox.move (500,500)
msgBox.setIcon(QMessageBox.Critical)
msgBox.setText("Test 2")
msgBox.setWindowTitle("ERROR")
msgBox.setStandardButtons(QMessageBox.Ok)
self.errWin2Timer = QtCore.QTimer()
self.errWin2Timer.timeout.connect(self.closeBox)
self.errWin2Timer.setSingleShot(True)
self.errWin2Timer.start(10000)
ret = msgBox.exec_()
if ret == QtGui.QMessageBox.Ok:
return
else:
return
def closeBox(self):
self.close()
def closeEvent(self, event):
logger.debug("Reached Error window 1 close event")
if self.errWin2:
self.errWin2.stop()
self.errWin2.deleteLater()
event.accept()
But the problem is that self.close doesn't work. What is the best possible way to close the window automatically after some period of time?
The problem is that when you put ret = msgBox.exec_() before the constructor finishes executing, the window object has not finished being built, so there is nothing to close, so when the dialog is closed the window that was just opened will be displayed. I finish building. The idea is to finish building the window and then call ret = msgBox.exec_() and for that we will use a QTimer.singleShot().
On the other hand, the closeEvent method is not necessary since I was trying to do it. IMHO is to eliminate the self.errWin2Timer from memory (although it seems that there was a typo since you use errWin2 instead of errWin2Timer) but being son of the window is not necessary since in Qt if the parent dies the children will also die.
from PyQt4 import QtCore,QtGui
class ErrorWindow2(QtGui.QWidget):
def __init__( self ):
super(ErrorWindow2, self).__init__(None, QtCore.Qt.WindowStaysOnTopHint)
self.errWin2Timer = QtCore.QTimer(self,
interval=10*1000,
singleShot=True,
timeout=self.close)
self.errWin2Timer.start()
QtCore.QTimer.singleShot(0, self.showMessageBox)
def showMessageBox(self):
msgBox = QtGui.QMessageBox(self)
msgBox.move (500,500)
msgBox.setIcon(QtGui.QMessageBox.Critical)
msgBox.setText("Test 2")
ret = msgBox.exec_()
if ret == QtGui.QMessageBox.Ok:
print("OK")
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
w = ErrorWindow2()
w.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 launch external process with real time output

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

Terminating QWidget doesn't work

I used self.close to close the widget that is running. Whole code is as follows:
class timeTracker(QWidget):
'''time tracker main application v_0.8'''
def __init__(self, parent=None):
super(timeTracker, self).__init__(parent)
self.grid = QGridLayout()
self.setLayout(self.grid)
while not self.connectivity():
print(self.connectivity())
ret = QMessageBox.warning(self, "Not connected to network. Please check the connection", "", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
if ret == QMessageBox.No:
self.close()
qApp.quit()
break
qApp.quit()
# self.time_widget = timeline.timeWidget()
# self.grid.addWidget(self.time_widget, 0, 0)
def connectivity(self):
try:
urllib.request.urlopen("http://www.google.com", timeout=1)
return True
except urllib.error.URLError as error:
return False
if __name__ == "__main__":
application = QApplication(sys.argv)
main_widget = timeTracker()
main_widget.show()
main_widget.move(10, 10)
sys.exit(application.exec_())
What I expect from this code is, as long as the connectivity function is returning True, user will be repeatedly asked to do the certain action if they click yes. If they click no, I would like to terminate the whole widget. But when I click no, then it just goes through the lines after the self.close. What can I do to actually terminate the program?
To close an application forcefully as in this case we can use sys.exit()
if ret == QMessageBox.No:
sys.exit(0)

Can't show window from thread

I have several threads that need to work with window. Here's thread definition:
class MyThread(QtCore.QThread):
def __init__(self, id, window, mutex):
super(MyThread, self).__init__()
self.id = id
self.window = window
self.mutex = mutex
self.connect(self, QtCore.SIGNAL("load_message_input()"), self.window, QtCore.SLOT("show_input()"))
def run(self):
self.mutex.lock()
self.emit(QtCore.SIGNAL("load_message_input()"))
self.connect(self.window, QtCore.SIGNAL("got_message(QString)"), self.print_message)
self.window.input_finished.wait(self.mutex)
self.mutex.unlock()
def print_message(self, str):
print "Thread %d: %s" % (self.id, str)
And here's window definition:
class MyDialog(QtGui.QDialog):
def __init__(self, *args, **kwargs):
super(MyDialog, self).__init__(*args, **kwargs)
self.last_message = None
self.setModal(True)
self.message_label = QtGui.QLabel(u"Message")
self.message_input = QtGui.QLineEdit()
self.dialog_buttons = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel)
self.dialog_buttons.accepted.connect(self.accept)
self.dialog_buttons.rejected.connect(self.reject)
self.hbox = QtGui.QHBoxLayout()
self.hbox.addWidget(self.message_label)
self.hbox.addWidget(self.message_input)
self.vbox = QtGui.QVBoxLayout()
self.vbox.addLayout(self.hbox)
self.vbox.addWidget(self.dialog_buttons)
self.setLayout(self.vbox)
self.input_finished = QtCore.QWaitCondition()
#QtCore.pyqtSlot()
def show_input(self):
self.exec_()
def on_accepted(self):
self.emit(QtCore.SIGNAL("got_message(QString)"), self.message_input.text())
self.input_finished.wakeOne()
And here's main:
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
mutex = QtCore.QMutex()
threads = []
window = test_qdialog.MyDialog()
for i in range(5):
thread = MyThread(i, window, mutex)
thread.start()
threads.append(thread)
for t in threads:
t.wait()
sys.exit(app.exec_())
I can't figure out why window isn't shown when executing the script.
Update:
For some reason other threads don't stop on line with self.mutex.lock(). Can't figure out why.
You have several problems in your code:
If you want a QThread to use slots you need to create an event loop for it (which is easy, just call QThread.exec_), but QThreads with event loops needs to be coded differently (next I'll post you an example)
You need to connect on_accepted to accepted if you want to emit the messages, unless you use the auto-connect features of Qt.
If you want to use QThread first you need to start a QApplication so for t in threads: t.wait() can't be executed before the call to QApplication.exec_ (in my example just removed it).
The last but not less important issue: If you want your threads to consume resources exclusively you should think of a consumer-producer approach (the problem is that when you emit a signal every slot will get a copy of the data and if you try to block a thread with an event loop the application just freezes, to solve the problem of consumer-producer I pass an extra mutex to the signal of the message and try to lock it [never blocking!] to know if the thread con consume the event)
As promised there is an example of how to use event loops on QThreads:
from PyQt4 import QtCore, QtGui
class MyThread(QtCore.QThread):
load_message_input = QtCore.pyqtSignal()
def __init__(self, id, window):
super(MyThread, self).__init__()
self.id = id
self.window = window
self.load_message_input.connect(self.window.show_input)
self.window.got_message.connect(self.print_message)
self.started.connect(self.do_stuff)
def run(self):
print "Thread %d: %s" % (self.id,"running")
self.exec_()
#QtCore.pyqtSlot()
def do_stuff(self):
print "Thread %d: %s" % (self.id,"emit load_message_input")
self.load_message_input.emit()
#QtCore.pyqtSlot("QString","QMutex")
def print_message(self, msg, mutex):
if mutex.tryLock():
print "Thread %d: %s" % (self.id, msg)
self.do_stuff()
class MyDialog(QtGui.QDialog):
got_message = QtCore.pyqtSignal("QString","QMutex")
def __init__(self, *args, **kwargs):
super(MyDialog, self).__init__(*args, **kwargs)
self.last_message = None
self.setModal(True)
self.message_label = QtGui.QLabel(u"Message")
self.message_input = QtGui.QLineEdit()
self.dialog_buttons = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel)
self.dialog_buttons.accepted.connect(self.accept)
self.dialog_buttons.accepted.connect(self.on_accepted)
self.dialog_buttons.rejected.connect(self.reject)
self.hbox = QtGui.QHBoxLayout()
self.hbox.addWidget(self.message_label)
self.hbox.addWidget(self.message_input)
self.vbox = QtGui.QVBoxLayout()
self.vbox.addLayout(self.hbox)
self.vbox.addWidget(self.dialog_buttons)
self.setLayout(self.vbox)
self.input_finished = QtCore.QWaitCondition()
#QtCore.pyqtSlot()
def show_input(self):
print "showing input"
window.show()
window.setModal(True)
#QtCore.pyqtSlot()
def on_accepted(self):
print "emit: ", self.message_input.text()
self.got_message.emit(self.message_input.text(), QtCore.QMutex())
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
mutex = QtCore.QMutex()
threads = []
window = MyDialog()
for i in range(5):
thread = MyThread(i, window)
thread.start()
threads.append(thread)
print "start app"
sys.exit(app.exec_())
Note: almost always the thread who receives the signal first will be the one with id 1.
My recommendation, do not use slots in your threads (which will make safe the use of mutex and wait-conditions) and implement a consumer-producer approach for the messages.
You are waiting for the threads to exit, before calling app.exec_(). You should probably monitor the threads in a GUI idle loop or connect to the thread's finished() signal.

Categories