In my PySide2 application there is a downloadCodesButton button clicking on which should execute a function in a separate QThread thread and display a value in a countCodesLabel label. But after clicking on the button nothing happens. Why?
My code:
import random
import colorama
import time
import os
import sys
import design
import tempfile
from PySide2 import QtWidgets, QtCore, QtGui
class Threads(QtCore.QObject):
running = False
downloadCodesReadySignal = QtCore.Signal(str, object)
myvar = None
def __init__(self, myvar, parent=None):
self.myvar = myvar
super().__init__()
def downloadCodes(self):
countCodes = 0
with open(self.myvar, 'r') as f:
line = f.readline()
while line:
codes.append(line)
countCodes = countCodes + 1
line = f.readline()
self.downloadCodesReadySignal.emit(countCodes)
class WarGenApp(QtWidgets.QMainWindow, design.Ui_Form):
def __init__(self):
super().__init__()
self.setupUi(self)
self.downloadCodesButton.clicked.connect(self.downloadCodes)
#QtCore.Slot(str, object)
def downloadCodesReady(self, countCodes):
self.countCodesLabel.setText(str(countCodes))
def downloadCodes(self):
fname = QtWidgets.QFileDialog.getOpenFileName(self, 'Открыть файл', '', 'Text files (*.txt)')[0]
self.thread = QtCore.QThread()
self.Threads = Threads(myvar=fname)
self.Threads.moveToThread(self.thread)
self.Threads.downloadCodesReadySignal.connect(self.downloadCodesReady)
self.thread.started.connect(self.Threads.downloadCodes)
self.thread.start()
def main():
app = QtWidgets.QApplication(sys.argv)
form = WarGenApp()
form.show()
app.exec_()
if __name__ == '__main__':
main()
What am I missing?
Your problem is at passing information into the slot. QtCore.Signal(str, object) expects a string and a object, but you only pass an object. Just change the #QtCore.Slot(str,object) to #QtCore.Slot(object) and adjust the signal calls accordingly and everything should work. Also, it might be a good idea to terminate the thread when it is done.
#QtCore.Slot(object)
def downloadCodesReady(self, countCodes):
self.countCodesLabel.setText(str(countCodes))
self.thread.terminate()
Finally, you are appending some undefined codes list in the downloadCodes function.
def downloadCodes(self):
countCodes = 0
with open(self.myvar, 'r') as f:
line = f.readline()
while line:
# codes.append(line) This one crashes the code
countCodes = countCodes + 1
line = f.readline()
self.downloadCodesReadySignal.emit(countCodes)
Eventhough the thread crashes, it does not automatically terminate itself. It just seems, that nothing happens. Might be a good idea to add some exit condition in case the code won't run to safely exit and terminate the thread.
Related
I have a websocket server running in python, and for every new connection a new thread will be created and the requests will be served.
In the main-thread [Gui-thread],i am initialing QApplication([]). the use case is, when i process the request i wanted to wait and get text response from the user through QInputDialog.
when ever i run it, there is a event-loop running but is not showing the gui. because all the gui elements can be displayed from Gui-thread itself.
I have tried various approaches using QSignals/slots and Pypubsub but unable to achieve what is required. please do suggest some idea to get the use-case done. a pseudo-code is well appreciated.
Below mentioned code are some examples i tried. i am using thread in below examples because, as i mentioned each request from a connection is executed with the thread assigned to the connection. and the text from QInputDialog is required by the thread.
thanks in advance.
below is the websockets server code which serves the request calling server_extentions function, i have to show QInputDialog everytime i get a incoming request.
import websockets
import asyncio
from PyQt5.QtWidgets import QInputDialog, QApplication
app = QApplication([])
async def server_extentions(websocket, path):
try:
while(True):
request = await websocket.recv()
# this is where i need to show input dialog.
text, ok = QInputDialog.getText(None, "Incoming message", request)
if ok:
response = text
else:
response = "NO REPLY"
await websocket.send(response)
except websockets.ConnectionClosed as exp:
print("connection closed.")
start_server = websockets.serve(server_extentions, '127.0.0.1', 5588)
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(start_server)
loop.run_forever()
finally:
loop.run_until_complete(loop.shutdown_asyncgens())
loop.close()
----edit-----
Below is some general idea, i tried using pypubsub.
import threading
import pubsub.pub
from PyQt5.QtWidgets import QInputDialog, QApplication
class MainThread:
def __init__(self):
self.app = QApplication([])
pubsub.pub.subscribe(self.pub_callback, "lala")
def pub_callback(self):
print("this is Main thread's pub callback.")
QInputDialog.getText(None, "main-thread", "lala call back : ")
def start_thread(self):
self.th = threading.Thread(target=self.thread_proc)
self.th.start()
def thread_proc(self):
pubsub.pub.sendMessage("lala")
m = MainThread()
m.start_thread()
-----edit 2 -------
below is something i tried with QSignal. [check the comment in the code, How to call a function with Mainthread].
import threading
from PyQt5.QtWidgets import QInputDialog, QApplication
from PyQt5.QtCore import pyqtSignal, QObject, QThread
class TextDialog(QObject):
sig = pyqtSignal(str)
def __init__(self):
QObject.__init__(self)
def get_text(self):
print("class Thread2, showing QInputDialog.")
text, ok = QInputDialog.getText(None, "Lala", "give me some text : ")
if ok:
self.sig.emit(text)
return
self.sig.emit("NO TEXT")
return
class Thread1:
def thread_proc(self):
td = TextDialog()
td.sig.connect(self.get_text_callback)
td.moveToThread(m.main_thread)
# here i dont understand how to invoke MainThread's show_dialog with main thread. [GUI Thread]
#m.show_dialog(td)
def get_text_callback(self, txt):
print("this is get_text_callback, input : " + str(txt))
class MainThread:
def __init__(self):
self.app = QApplication([])
self.main_thread = QThread.currentThread()
def main_proc(self):
th1 = Thread1()
th = threading.Thread(target=th1.thread_proc)
th.start()
def show_dialog(self, text_dialog: TextDialog):
print("got a call to MainThread's show_dialog.")
text_dialog.get_text()
m = MainThread()
m.main_proc()
exit()
For this type of applications it is better to implement the worker-thread approach. This approach the main idea is to implement QObjects, move them to a new thread and invoke the slots asynchronously (through QEvents, pyqtSignals, QTimer.singleShot(...), QMetaObject::invokeMethod(...), etc) so that the tasks are executed in the thread that Live the QObject.
import threading
from functools import partial
from PyQt5 import QtCore, QtWidgets
class TextDialog(QtCore.QObject):
sig = QtCore.pyqtSignal(str)
#QtCore.pyqtSlot()
def get_text(self):
print("class Thread2, showing QInputDialog.")
text, ok = QtWidgets.QInputDialog.getText(
None, "Lala", "give me some text : "
)
if ok:
self.sig.emit(text)
return
self.sig.emit("NO TEXT")
return
class Worker1(QtCore.QObject):
#QtCore.pyqtSlot(QtCore.QObject)
def thread_proc(self, manager):
print(
"current: {}- main: {}".format(
threading.current_thread(), threading.main_thread()
)
)
manager.td.sig.connect(self.get_text_callback)
QtCore.QTimer.singleShot(0, manager.show_dialog)
#QtCore.pyqtSlot(str)
def get_text_callback(self, txt):
print(
"current: {}- main: {}".format(
threading.current_thread(), threading.main_thread()
)
)
print("this is get_text_callback, input : %s" % (txt,))
class Manager(QtCore.QObject):
def __init__(self, parent=None):
super().__init__(parent)
self.td = TextDialog()
#QtCore.pyqtSlot()
def show_dialog(self):
print("got a call to MainThread's show_dialog.")
self.td.get_text()
class Application:
def __init__(self):
print(
"current: {}- main: {}".format(
threading.current_thread(), threading.main_thread()
)
)
self.app = QtWidgets.QApplication([])
# By default if after opening a window all the windows are closed
# the application will be terminated, in this case after opening
# and closing the QInputDialog the application will be closed avoiding
# that it will be noticed that get_text_callback is called,
# to avoid the above it is deactivated behavior.
self.app.setQuitOnLastWindowClosed(False)
self.manager = Manager()
def main_proc(self):
#
self.thread = QtCore.QThread()
self.thread.start()
self.worker = Worker1()
# move the worker so that it lives in the thread that handles the QThread
self.worker.moveToThread(self.thread)
# calling function asynchronously
# will cause the function to run on the worker's thread
QtCore.QTimer.singleShot(
0, partial(self.worker.thread_proc, self.manager)
)
def run(self):
return self.app.exec_()
if __name__ == "__main__":
import sys
m = Application()
m.main_proc()
ret = m.run()
sys.exit(ret)
Looking to thread my app. Below is a portion of the code. From my understanding, I need to emit a signal from my thread class, and in my Main class, I start the thread. I just want to be able to move the window around and not lose functionality while the final_button action is taking place. Any help would be appreciated.
Edit: based on eyllanesc's feedback, changed my code to be an if statement rather than a while True statement. The problem still persists - once I press the final button, the app loses control. The window can't be moved and the title bar shows Not Responding until the final button actions finish. I'm wondering if I can adjust my code below to fix the issue. Thanks.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QFileDialog, QMessageBox
from PyQt5.QtCore import pyqtSignal
from config_assign_ui import Ui_ConfigAssign
import pyodbc
import pandas as pd
class Main(QtWidgets.QMainWindow, Ui_ConfigAssign):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
self.setupUi(self)
self.thread = Thread()
self.thread.start()
self.thread.final_button.connect(self.final_button)
self.combo_list()
self.ImportMapButton.clicked.connect(self.import_map)
self.ExceptButton.clicked.connect(self.except_file)
self.FinalButton.clicked.connect(self.final_button)
def combo_list(self):
# do a SQL query to show a list of customer options
def import_map(self, df_map):
name = QFileDialog.getOpenFileName(self, "Open Map...", "T:/Drive",
"Worksheets (*.xlsx;*.xlsm;*.xlsb;*.xls)")
file = str(name[0])
if file != '':
while True:
try:
xl = pd.ExcelFile(file)
self.df_map = xl.parse('Sheet1')
rows = self.df_map.shape[0]
rows = "{:,}".format(rows)
self.labelMapCount.setText(rows)
except:
mb = QMessageBox()
mb.setWindowTitle('Problem with File')
mb.setText("Please try again")
mb.setStyleSheet("QLabel{min-width:180 px}")
mb.exec_()
break
# check columns of file
cols = ['CategoryId','CategoryName','SubcategoryId','SubcategoryName','MakeId','MakeName']
cols_file = list(self.df_map)
if cols_file != cols:
mb = QMessageBox()
mb.setWindowTitle('Column Error')
mb.setText('Columns names do not match template')
mb.setStyleSheet('QLabel{min-width:220 px}')
mb.exec_()
break
else:
break
def except_file(self, df_except):
if self.ExceptCheckBox.isChecked():
# do some processing of an exception file that the user can import
def final_button(self):
# check if df_map exists
try:
print(self.df_map.head())
map_avail = 1
except AttributeError:
mb = QMessageBox()
mb.setWindowTitle('Map Missing')
mb.setText('Please import map first')
mb.setStyleSheet("QLabel{min-width:200 px}")
mb.exec_()
map_avail = 0
if map_avail == 1:
#perform a bunch of actions
class Thread(QtCore.QThread):
final_button = pyqtSignal()
def __init__(self):
Thread.__init__(self)
def run(self):
while True:
time.sleep(2)
self.final_button.emit()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = Main()
window.show()
sys.exit(app.exec_())
I am looking for a design pattern based on threading.Thread, multiprocessing or Queue to upload a list of items with a timeout. The thread allows the GUI to remain responsive. If the connection hangs, then the timeout should trigger and the program should gracefully exit.
The example below works, but the GUI remains blocked. How could this be improved to allow for uploading of the list, manual canceling of the process, timeout of the upload process plus a non-blocking GUI?
from PySide.QtGui import *
from PySide.QtCore import *
import sys
import time
import threading
class UploadWindow(QDialog):
def __init__(self, parent=None):
super(UploadWindow, self).__init__(parent)
self.uploadBtn = QPushButton('Upload')
mainLayout = QVBoxLayout()
mainLayout.addWidget(self.uploadBtn)
self.uploadBtn.clicked.connect(self.do_upload)
self.progressDialog = QProgressDialog(self)
self.progressDialog.canceled.connect(self.cancelDownload)
self.progressDialog.hide()
self.setLayout(mainLayout)
self.show()
self.raise_()
def do_upload(self):
self.uploadBtn.setEnabled(False)
self.progressDialog.setMaximum(10)
self.progressDialog.show()
self.upload_thread = UploadThread(self)
self.upload_thread.start()
self.upload_thread_stopped = False
#List of items to upload
for i in range(10):
self.upload_thread = UploadThread(i)
self.upload_thread.start()
self.upload_thread.join(5)
self.progressDialog.setValue(i)
if self.upload_thread_stopped:
break
self.progressDialog.hide()
self.uploadBtn.setEnabled(True)
def cancelDownload(self):
self.upload_thread_stopped = True
class UploadThread(threading.Thread):
def __init__(self, i):
super(UploadThread, self).__init__()
self.i = i
self.setDaemon(True)
def run(self):
time.sleep(0.25) #simulate upload time
print self.i
if __name__ == '__main__':
app = QApplication(sys.argv)
w = UploadWindow()
sys.exit(app.exec_())
The GUI is not responsive because you do all the work in do_upload, never going back to the main loop.
Also, you call Thread.join(), that blocks everything until the thread is done (see https://docs.python.org/2/library/threading.html#threading.Thread.join)
You should use PySide.QtCore.QThread to take advantage of signals and slots.
Here is a nice example in C++. I implemented it in Python3.4 with PyQt here, but you should be able to use it with PySide too.
You might also want to look at PySide.QtCore.QProcess, to avoid using threads.
Here, I put some code together that does what I think you want.
For a real project, be sure to keep track of what's uploaded better &/or use something safer than .terminate() to stop the thread on demand.
import sys
from PySide import QtGui, QtCore
import time
class MySigObj(QtCore.QObject):
strSig = QtCore.Signal(str)
tupSig = QtCore.Signal(tuple)
class UploadThread(QtCore.QThread):
def __init__(self, parent=None):
super(UploadThread, self).__init__(parent)
self.endNow = False
self.fileName = None
self.sig = MySigObj()
self.fileNames = []
self.uploaded = []
#QtCore.Slot(str)
def setFileNames(self, t):
self.fileNames = list(t)
def run(self):
while self.fileNames:
print(self.fileNames)
time.sleep(2)
name = self.fileNames.pop(0)
s = 'uploaded file: ' + name + '\n'
print(s)
self.sig.strSig.emit(s)
self.uploaded.append(name)
if len(self.fileNames) == 0:
self.sig.strSig.emit("files transmitted: %s" % str(self.uploaded))
else:
time.sleep(1) #if the thread started but no list, wait 1 sec every cycle thru
#that was this thread should release the Python GIL (Global Interpreter Lock)
class ULoadWin(QtGui.QWidget):
def __init__(self, parent=None):
super(ULoadWin, self).__init__(parent)
self.upThread = UploadThread()
self.sig = MySigObj()
self.sig.tupSig.connect(self.upThread.setFileNames)
self.upThread.sig.strSig.connect(self.txtMsgAppend)
self.sig.tupSig.connect(self.upThread.setFileNames)
self.layout = QtGui.QVBoxLayout()
self.stButton = QtGui.QPushButton("Start")
self.stButton.clicked.connect(self.uploadItems)
self.stpButton = QtGui.QPushButton("Stop")
self.stpButton.clicked.connect(self.killThread)
self.testButton = QtGui.QPushButton("write txt\n not(?) blocked \nbelow")
self.testButton.setMinimumHeight(28)
self.testButton.clicked.connect(self.tstBlking)
self.lbl = QtGui.QTextEdit()
self.lbl.setMinimumHeight(325)
self.lbl.setMinimumWidth(290)
self.layout.addWidget(self.stButton)
self.layout.addWidget(self.stpButton)
self.layout.addWidget(self.testButton)
self.layout.addWidget(self.lbl)
self.setLayout(self.layout)
self.l = ['a', 'list', 'of_files', 'we', 'will_pretend_to_upload', 'st', 'uploading']
self.upThread.start()
def tstBlking(self):
self.lbl.append("txt not(?) blocked")
def uploadItems(self):
t = tuple(self.l)
self.sig.tupSig.emit(t)
self.upThread.start()
def killThread(self):
self.upThread.terminate()
time.sleep(.01)
self.upThread = UploadThread()
#QtCore.Slot(str)
def txtMsgAppend(self, txt):
self.lbl.append(txt + " | ")
if __name__ == '__main__':
app=QtGui.QApplication(sys.argv)
widg=ULoadWin()
widg.show()
sys.exit(app.exec_())
In the end I solved this problem by adapting the approach outlined here, which I find to be an elegant solution. The class I created is shown below:
class UploadThread(threading.Thread):
#input_q and result_q are Queue.Queue objects
def __init__(self, input_q, result_q):
super(UploadThread, self).__init__()
self.input_q = input_q
self.result_q = result_q
self.stoprequest = threading.Event() #threadsafe flag
def run(self):
'''Runs indefinitely until self.join() is called.
As soon as items are placed in the input_q, then the thread will process them until the input_q is emptied.
'''
while not self.stoprequest.isSet(): #stoprequest can be set from the main gui
try:
# Queue.get with timeout to allow checking self.stoprequest
num = self.input_q.get(True, 0.1) #when the queue is empty it waits 100ms before raising the Queue.Empty error
print 'In thread, processing', num
time.sleep(0.5)
self.result_q.put(True) #Indicate to the main thread that an item was successfully processed.
except Queue.Empty as e:
continue
def join(self, timeout=None):
self.stoprequest.set()
super(UploadThread, self).join(timeout)
In the main thread, the upload thread is created and the input_q is loaded with the items to upload. A QTimer is created to regularly check on the progress of the upload by checking what has been placed into the result_q. It also update the progress bar. If no progress has been made within a timeout, this indicates the upload connection has failed.
An advantage of using Queue.Queue objects for communicating between threads, is that multiple threads that share the same input and result queue can be created.
I wrote a program to batch change the filenames of many pictures, using PyQt5.
When I run it, it runs through some pictures successfully, and randomly crash without a Python Traceback, showing "has stopped working" in windows 8.1. Even if I use the same testcases, the program will sometimes crash after 20+ pictures, sometimes only 2, sometimes runs to the end.
I don't even know the program crashed in which line. How do I solve this problem?
Here is the program, I minimized the code that still work, and randomly crash as before.
# -*- coding: utf-8 -*-
import sys
import os
import re
from PyQt5 import Qt
if __name__ == '__main__':
application_Variable = Qt.QApplication(sys.argv)
from MainWindow import Ui_MainWindow_Widget
from FileName import Ui_FileName_Widget
class filename_class: # just a class to get path, filename, append
def __init__(self, fullpath):
self.re_path_temp = re.match(r".+/", fullpath)
self.path = self.re_path_temp.group(0)
self.name = fullpath[len(self.path):]
self.append = self.name[len(self.name[:self.name.rfind('.')])-len(self.name)+1:]
class fileNameDefine_Widget(Qt.QWidget): # the rename widget
def __init__(self, filename):
super(fileNameDefine_Widget, self).__init__()
self.fileWidget = Ui_FileName_Widget()
self.fileWidget.setupUi(self)
self.filename = filename
self.file_append = filename_class(self.filename).append # get the append
self.fileWidget.InputFileName_LineEdit.textChanged.connect(self.input_file_name_Change)
def input_file_name_Change(self):
self.export_name = self.fileWidget.InputFileName_LineEdit.text() + "." + self.file_append
self.fileWidget.ExportFileName_LineEdit.setText(self.export_name)
self.fileWidget.InputFileName_LineEdit.setEnabled(True)
class MainWindow_Class(Qt.QWidget): # the main widget
def __init__(self):
super(MainWindow_Class, self).__init__()
self.main = Ui_MainWindow_Widget() # init
self.main.setupUi(self)
self.root_directory = r"D:\TLCTest"
self.file_list = Qt.QFileDialog.getOpenFileNames(caption="Select file", directory=self.root_directory)[0]
self.count = 0 # count which file are being processed
self.show()
self.initiate_change_filename()
def initiate_change_filename(self):
file = self.file_list[self.count]
# show the picture
self.pixmap = Qt.QPixmap()
self.pixmap.load(file)
self.graphicsPixmapItem = Qt.QGraphicsPixmapItem(self.pixmap)
self.graphicsScene = Qt.QGraphicsScene()
self.graphicsScene.addItem(self.graphicsPixmapItem)
self.main.graphicsView.setScene(self.graphicsScene)
# start the rename widget
self.fileName_Widget = fileNameDefine_Widget(file)
self.fileName_Widget.fileWidget.InputFileName_LineEdit.returnPressed.connect(self.submit) # press enter to submit
self.fileName_Widget.show()
def submit(self):
filename = self.fileName_Widget.filename
path = filename_class(filename).path
final_name = self.fileName_Widget.fileWidget.ExportFileName_LineEdit.text()
os.rename(filename, path + final_name)
self.count += 1
if self.count == len(self.file_list):
exit()
else:
self.fileName_Widget.close()
self.initiate_change_filename()
if __name__ == '__main__':
my_Qt_Program = MainWindow_Class()
my_Qt_Program.show()
sys.exit(application_Variable.exec_())
As ekhumoro's comment, this problem was caused by repeatedly creating a fileNameDefine_Widget object like:
def initiate_change_filename(self):
self.fileName_Widget = fileNameDefine_Widget(file)
Instead, one should create this object once, and init it every time required, like:
class TLC_processor_Class(Qt.QWidget):
def init(self):
# Blabla
self.fileName_Widget = fileNameDefine_Widget(file)
def initiate_change_filename(self):
self.fileName_Widget.__init__(file)
I have a script, that works fine:
if __name__ == '__main__':
app = QApplication(sys.argv)
bot = GBot()
bot.search('hot tea', num=20)
if signal.signal(signal.SIGINT, signal.SIG_DFL):
sys.exit(app.exec_())
app.exec_()
When I call search(), program starts working, and and loads website:
def _loadFinished(self, ok):
current_url = self.page().currentFrame().url().toString()
if str(current_url).endswith('.com/'):
self.home_search()
else:
self.get_links_text_from_page()
if self.count >= self.desired_number_of_results:
self.close()
After load finished 1 time, it checks for another condition and desides what to do next.
At the end, after program loads multiple websites. Desired data collected in variable called self.results.
So my question is how I can return result from search(), by checking condition of loadFinished().
Another words, I need to come up with some sort of algorithm that will check if loadFinished will not load any other websites, and than search() function will return desired variable. I was thinking to create another variable self.result = False than change the condition in loadFinished() and in search() place everything in while loop, and after that return result. But it doesn't work...
search()
def search(self, keyword, num=None, output=None):
self.keyword = keyword
if output is "json":
# need to return `self.results` ONLY after program finished. because before that,
# this variable is empty
self.load('somewebsite.com')
pass
Looks like you could use a generator here. In this QWebView example, loadWebsites is called until StopIteration is raised, in which case procDone is emitted with the number of loaded websites. The output for that signal is captured in the slot on_procDone. (The output in this case is 3, because of ["http://www.example.com"]*3):
#!/usr/bin/env python
#-*- coding:utf-8 -*-
from PyQt4 import QtCore, QtGui, QtWebKit, QtNetwork
class myWindow(QtWebKit.QWebView):
procDone = QtCore.pyqtSignal(int)
def __init__(self, parent=None):
super(myWindow, self).__init__(parent)
self.websites = iter(["http://www.example.com"]*3)
self.websitesTotal = 0
self.loadFinished.connect(self.on_loadFinished)
self.procDone.connect(self.on_procDone)
self.loadWebsites()
def loadWebsites(self):
try:
website = self.websites.next()
except StopIteration:
self.procDone.emit(self.websitesTotal)
else:
self.load(QtCore.QUrl(website))
#QtCore.pyqtSlot(bool)
def on_loadFinished(self, ok):
self.websitesTotal += 1
print "Loaded: {0}".format(self.url().toString())
self.loadWebsites()
#QtCore.pyqtSlot(int)
def on_procDone(self, total):
print "Total of websites: {0}".format(total)
self.websitesTotal = 0
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
app.setApplicationName('myWindow')
main = myWindow()
main.show()
sys.exit(app.exec_())