pyQt6 + asyncio: connect an async function as a slot - python-asyncio

I've got a situation where I would like to connect an async function that uses awaits to the clicked signal of a QPushButton.
The motivation is that I need to manage some io-bound stuff and using awaits seems to be the most cogent way to do that. The problem is that when one uses awaits, the invoking function needs to be marked as async and so do all functions that invoke THAT function. This "bubbles up to the top" until eventually, there's a button clicked signal that needs to be connected to an async slot function. I am not sure how to do that and there's not much advice. Maybe I got the wrong idea and pyQt6 isn't supposed to mix with asyncio?
Here's an example. It's just a QMainWindow with a button, and I am trying to connect the button's clicked signal to a slot function that happens to be async. It just prints numbers and uses asyncio.sleep(1.0) to wait for 1 second after printing each number.
import sys
import asyncio
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
button = QPushButton("Press This")
self.setCentralWidget(button)
button.clicked.connect(self.my_async_func)
async def my_async_func(self):
for x in range(1,10):
print(x)
await asyncio.sleep(1.0)
print("all done")
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
When I click the button I get the following, and of course, nothing prints.
RuntimeWarning: coroutine 'MainWindow.my_async_func' was never awaited
app.exec()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
How do I tell connect(...) that I want to use an async function?
My previous experience was coming from .net WPF and I was hoping that it would not be a big deal to tie an async function to a UI event.

OK, I found something that works without too much fussing.
I am not sure if it's correct. Please critique and, if possible, explain (I don't know enough about this).
The trick, for pyqt6, is to use qasync to create and start an event loop that async functions seem to be able to take as an argument. The async function also needs to be decorated with #asyncSlot().
import sys
import asyncio
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton
from qasync import QEventLoop, asyncSlot
class MainWindow(QMainWindow):
def __init__(self, loop=None):
super().__init__()
button = QPushButton("Press This")
self.setCentralWidget(button)
button.clicked.connect(self.my_async_func)
self.loop = loop or asyncio.get_event_loop()
#asyncSlot()
async def my_async_func(self):
for x in range(1,10):
print(x)
await asyncio.sleep(1.0,self.loop)
print("all done")
def main():
app = QApplication(sys.argv)
loop = QEventLoop(app)
asyncio.set_event_loop(loop)
window = MainWindow(loop)
window.show()
with loop:
loop.run_forever()
if __name__ == '__main__':
main()

Related

pyqt5, How do I open QDialog in QThread? [duplicate]

This question already has answers here:
Example of the right way to use QThread in PyQt?
(3 answers)
PyQt: show animated GIF while other operations are running
(1 answer)
Closed 11 months ago.
I created a login gui using pyqt5 now. Then log in to the site using the request module. However, it takes a long time to log in, so I want to output a loading screen in the meantime. However, there is a method of using qthread? because I don't know how to perform parallel processing in pyqt5. What can I do?
Loading GUI
import sys
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QDialog
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QMovie
class LoadingApp(QDialog):
def __init__(self):
super().__init__()
self.setWindowFlag(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setFixedSize(200, 200)
self.label_animation = QLabel(self)
self.movie = QMovie('loading.gif')
self.label_animation.setMovie(self.movie)
timer = QTimer(self)
self.startAnimation()
timer.singleShot(3000, self.stopAnimation)
def startAnimation(self):
self.movie.start()
def stopAnimation(self):
self.movie.stop()
self.close()
def LoadingUI_exec(self):
return super().exec_()
Main CODE >> I want to fill the tempform function
from PyQt5 import QtWidgets, QtCore
from loginUI import Ui_Form
from loadingUI import LoadingApp
import sys
class LoginApp(QtWidgets.QWidget, Ui_Form):
def changeForm(self):
if self.lr_button.isChecked():
self.widget_2.hide()
self.widget_3.show()
self.lr_button.setText("Login")
else:
self.widget_2.show()
self.widget_3.hide()
self.lr_button.setText("Register")
def tempform(self):
#The login process is in a different script.
#Therefore, I want to implement a code
#that simultaneously executes the login function and loading gui in this function.
#The method call loadingGUI > LoadingApp().LoadingUI_exec()
def exit(self):
return
sys.exit(0)
def __init__(self):
super(LoginApp, self).__init__()
self.setupUi(self)
self.setWindowFlag(QtCore.Qt.FramelessWindowHint)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.label.setGraphicsEffect(QtWidgets.QGraphicsDropShadowEffect(blurRadius=25, xOffset=0, yOffset=0))
self.label_2.setGraphicsEffect(QtWidgets.QGraphicsDropShadowEffect(blurRadius=25, xOffset=0, yOffset=0))
self.login_button.setGraphicsEffect(QtWidgets.QGraphicsDropShadowEffect(blurRadius=25, xOffset=3, yOffset=3))
self.register_button.setGraphicsEffect(QtWidgets.QGraphicsDropShadowEffect(blurRadius=25, xOffset=3, yOffset=3))
self.widget_3.hide()
self.lr_button.clicked.connect(self.changeForm)
self.exit_button.clicked.connect(QtCore.QCoreApplication.instance().quit)
self.login_button.clicked.connect(self.tempform)
#if __name__ == "__main__":
def LoginUI_exec():
app = QtWidgets.QApplication(sys.argv)
Form = LoginApp()
Form.show()
sys.exit(app.exec_())
In Qt like in most GUIs all widgets live and perform in one thread, you cannot create or change or show widget from another thread. If you want to avoid unresponsive ui while blocking operation is performed, you should move that operation to another QThread using worker, or in your case you can use QNetworkRequest instead of requests, it already have asyncronous signal-slot interface.

Microsoft Azure Speech-to-Text MVC application

I'm working on an application that uses the Microsoft Cognitive services Speech-to-Text API. I'm trying to create a GUI where the transcribed text should show up in a textbox once the start button is pushed and the transcription is stopped once a stop-button is pressed. I'm pretty new to creating GUI's and have been using PyQt5. I have divided the application according to MVC (Model-View-Controller). The code for the GUI is as follows:
import sys
import time
from functools import partial
import azure.cognitiveservices.speech as speechsdk
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
class test_view(QMainWindow):
def __init__(self):
super().__init__()
self.generalLayout = QVBoxLayout()
self._centralWidget = QWidget(self)
self.setCentralWidget(self._centralWidget)
self._centralWidget.setLayout(self.generalLayout)
self._createApp()
def _createApp(self):
self.startButton = QPushButton('Start')
self.stopButton = QPushButton('Stop')
buttonLayout = QHBoxLayout()
self.startButton.setFixedWidth(220)
self.stopButton.setFixedWidth(220)
buttonLayout.addWidget(self.startButton)
buttonLayout.addWidget(self.stopButton)
self.text_box = QTextEdit()
self.text_box.setReadOnly(True)
self.text_box.setFixedSize(1500, 400)
layout_text = QHBoxLayout()
layout_text.addWidget(self.text_box)
layout_text.setAlignment(Qt.AlignCenter)
self.generalLayout.addLayout(buttonLayout)
self.generalLayout.addLayout(layout_text)
def appendText(self, text):
self.text_box.append(text)
self.text_box.setFocus()
def clearText(self):
return self.text_box.setText('')
class test_ctrl:
def __init__(self, view):
self._view = view
def main():
application = QApplication(sys.argv)
view = test_view()
view.showMaximized()
test_ctrl(view=view)
sys.exit(application.exec_())
if __name__ == "__main__":
main()
The Speech-to-Text Transcribe code is:
import azure.cognitiveservices.speech as speechsdk
import time
def setupSpeech():
speech_key, service_region = "speech_key", "service_region"
speech_config = speechsdk.SpeechConfig(subscription=speech_key, region=service_region)
speech_recognizer = speechsdk.SpeechRecognizer(speech_config=speech_config)
return speech_recognizer
def main():
speech_recognizer = setupSpeech()
done = False
def stop_cb(evt):
print('CLOSING on {}'.format(evt))
speech_recognizer.stop_continuous_recognition()
nonlocal done
done = True
all_results = []
def handle_final_result(evt):
all_results.append(evt.result.text)
speech_recognizer.recognizing.connect(lambda evt: print(evt))
speech_recognizer.recognized.connect(handle_final_result)
speech_recognizer.session_stopped.connect(stop_cb)
speech_recognizer.canceled.connect(stop_cb)
speech_recognizer.start_continuous_recognition()
while not done:
time.sleep(.5)
print(all_results)
if __name__ == "__main__":
main()
I know for sure that both of the pieces of code work, but I'm not sure how to build the speech-to-text code into the MVC code. I think it should work with a model and it should be connected through the controller to the view. I tried doing this in multiple ways but I just can't figure it out. I also figured I need some kind of threading to keep the code from freezing the GUI. I hope someone can help me with this.
You need to replace this part
print(all_results)
and push all_results asynchronously to ur code for processing the text.
If not, expose a button in the UI to invoke the speech_recognizer.start_continuous_recognition() as a separate function and pick the results to process. This way you can avoid freezing the UI

PySide: Place/move animated QMovie GIF in QSplashScreen

I'm having trouble getting this to work. I'd like to be able to place an animated GIF in a specific spot on my QSplashScreen.
The GIF has to be animated using multiprocessing and the onNextFrame method so that it will play during the initial load (otherwise it just freezes on the first frame).
I've tried inserting self.move(500,500) everywhere but nothing is working (well not working well enough). Right now, the GIF will play in the spot I want, but then it will snap back to screen center on the next frame, then back to the spot I want, etc. Inserting the move method every possible place hasn't fixed this issue.
Here's the code:
from PySide import QtCore
from PySide import QtGui
from multiprocessing import Pool
class Form(QtGui.QDialog):
def __init__(self, parent=None):
super(Form, self).__init__(parent)
self.browser = QtGui.QTextBrowser()
self.setWindowTitle('Just a dialog')
self.move(500,500)
class MySplashScreen(QtGui.QSplashScreen):
def __init__(self, animation, flags):
# run event dispatching in another thread
QtGui.QSplashScreen.__init__(self, QtGui.QPixmap(), flags)
self.movie = QtGui.QMovie(animation)
self.movie.frameChanged.connect(self.onNextFrame)
#self.connect(self.movie, SIGNAL('frameChanged(int)'), SLOT('onNextFrame()'))
self.movie.start()
self.move(500, 500)
def onNextFrame(self):
pixmap = self.movie.currentPixmap()
self.setPixmap(pixmap)
self.setMask(pixmap.mask())
self.move(500, 500)
# Put your initialization code here
def longInitialization(arg):
time.sleep(args)
return 0
if __name__ == "__main__":
import sys, time
app = QtGui.QApplication(sys.argv)
# Create and display the splash screen
# splash_pix = QPixmap('a.gif')
splash = MySplashScreen('S:\_Studio\_ASSETS\Tutorials\Maya\Coding\Python\_PySide\GIF\dragonGif.gif',
QtCore.Qt.WindowStaysOnTopHint)
# splash.setMask(splash_pix.mask())
#splash.raise_()
splash.move(500, 500)
splash.show()
# this event loop is needed for dispatching of Qt events
initLoop = QtCore.QEventLoop()
pool = Pool(processes=1)
pool.apply_async(longInitialization, [2], callback=lambda exitCode: initLoop.exit(exitCode))
initLoop.exec_()
form = Form()
form.show()
splash.finish(form)
app.exec_()

Play image sequence using Qt QMainWindow

I have an image sequence rendered out. which I want to payback in a simple QMainWindow or QDialog. This is what I have sofar. It loads the images into the qlabel, but I cant see the label being updated, its just show the last loaded image, and nothing in between.
Maybe someone knows something?
from PySide import QtCore, QtGui
import shiboken
import maya.OpenMayaUI as apiUI
import time
def getMayaWindow():
"""
Get the main Maya window as a QtGui.QMainWindow instance
#return: QtGui.QMainWindow instance of the top level Maya windows
"""
ptr = apiUI.MQtUtil.mainWindow()
if ptr is not None:
return shiboken.wrapInstance(long(ptr), QtGui.QWidget)
class Viewer(QtGui.QMainWindow):
def __init__(self, parent = getMayaWindow()):
super(Viewer, self).__init__(parent)
self.setGeometry(400, 600, 400, 300)
self.setUi()
def setUi(self):
self.label = QtGui.QLabel()
self.setCentralWidget(self.label)
def showUi(self):
self.show()
def loadImage(self, path):
self.label.clear()
image = QtGui.QImage(path)
pp = QtGui.QPixmap.fromImage(image)
self.label.setPixmap(pp.scaled(
self.label.size(),
QtCore.Qt.KeepAspectRatio,
QtCore.Qt.SmoothTransformation))
x = Viewer()
x.showUi()
for i in range(1, 11):
x.loadImage("C://anim%03d.png" % i)
time.sleep(0.5)
You change pixmaps in loop and sleep (stop) all GUI thread, that's why your GUI freeze.
http://www.tutorialspoint.com/python/time_sleep.htm
It is not correct. qLabel.repaint() it is bad solution because it still blocks GUI. Of course you can use processEvents but it is bad approach too.
You should use QTimer for this purpose, use timeout() signal, create slot and change pixmaps in this slot. In this case your GUI will not be blocked because QTimer works asynchronously and images will be successfuly changed.
Same code with loop and sleep can help you only when this code will execute in another thread (multi threading) but it is not necessary because there is special class QTimer.

How to interrupt QThread from PyQt GUI?

I'm writting an application that encrypt an image. The main problem is that i want to add to my GUI option to interrupt (or even terminate) an encrypting thread (while it is working) just by clicking an gui button. Gui and algorithm work fine (ia also provide a gui's progressbar connection) but when the thread start to procced i can't click anything on gui (even the terminating button). Beside that button is properly connected becuse if there occured an error in thread and gui i still working i can click the button and it terminate the process.
I thought that gui froze because thread was defined in gui function so I've moved it out of gui to main program function.
I want to point out that i don't create therad subclass (as Maya Posch suggest http://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/)
Here is the code of main function:
def main():
app = QApplication(sys.argv)
cryptoThread = QtCore.QThread()
prog = ProgramWindow()
worker = ic.imageCryptographer()
worker.moveToThread(cryptoThread)
prog.progressButton.clicked.connect(lambda: prog.interruptEncrypting(cryptoThread))
prog.startEncrypting.connect(cryptoThread.start)
worker.encryptSignal.connect(prog.progressbar.setValue)
worker.done.connect(lambda: prog.endEncrypting(cryptoThread, worker))
cryptoThread.started.connect(lambda: worker.compute(prog.shareFlag, prog.binMatrix))
sys.exit(app.exec_())
functions from class ProgramWindow:
def interruptEncrypting(self, thread):
thread.terminate()
thread.wait()
self.interrupt()
return
def endEncrypting(self, thread, worker):
self.keys = worker.keys
thread.quit()
self.progressbarWidget.setVisible(False)
self.saveOption.setEnabled(True)
self.cryptoWorkdeskOption.setEnabled(True)
self.openCryptoWorkdesk()
def interrupt(self):
self.progressbarWidget.setVisible(False)
if self.state==1:
self.buttonSwapWidget.setVisible(True)
elif self.state==2:
self.keyChooseWidget.setVisible(True)
Variables: shareFlag and binMatrix has no connection to thread communication (their are variables necceseray to compute worker methods. StartEncrypting is a signal emited from one of ProgramWindow function.
Thanks in advance for any advice where I made a mistake or what should I do.
Yep, just tried it and can confirm what I already indicated in my comment. Although you moved worker to cryptoThread (and therefore all its methods) the moment you connect cryptoThread.started.connect(lambda: worker.compute(prog.shareFlag, prog.binMatrix)) you create a new lambda object which is not running within cryptoThread but within the main thread. That's why it does not run besides your main application. You would have to connect cryptoThread.started.connect(worker.compute) and pass the arguments in some additional initializer / configure method.
I tested it with the following code:
import sys
import time
from PyQt4 import QtGui, QtCore
class Worker(QtCore.QObject):
def __init__(self, parent=None):
QtCore.QObject.__init__(self, parent)
self.t1 = QtCore.QThread()
self.moveToThread(self.t1)
self.t1.start()
def do_stuff(self):
while True:
print 'loop'
time.sleep(1)
class MainWindow(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.worker = Worker()
self.button = QtGui.QPushButton('start', self)
self.button.clicked.connect(self.worker.do_stuff) # connect directly with worker's method do_stuff
#self.button.clicked.connect(lambda: self.worker.do_stuff()) # connect with lambda object containing do_stuff
app = QtGui.QApplication(sys.argv)
main = MainWindow()
main.show()
sys.exit(app.exec_())

Resources