repzero repzero - 3 months ago 42
Python Question

PySide Application with asynchronous function execution

I have a sample pyside demo which I created to see the webkit browser communication with python...
I have two buttons in webkit


  • button 1 - when clicked it sleeps for 10 seconds and then prints a message

  • button2 - when clicked it prints a message immediately.



When I clicked on button 1, the whole apps freezes and waits for python to finish sleeping, this means I cannot click on button 2 to do some other stuff. How can I implement an asynchronous method between function calls?

My python codes are below

import sys,json
from time import sleep
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtWebKit import QWebView, QWebSettings
from PySide.QtNetwork import QNetworkRequest
from PySide.QtCore import QObject, Slot, Signal

html_str="""<!doctype>
<html>

<body>hello world
<button id="button" >button1</button>
<button id="button2" >button2</button>
</body>
</html>
<script type="text/javascript">
document.getElementById("button").onclick=function(){
object.reply(" hello ");
}
document.getElementById("button2").onclick=function(){
object.reply2(" hello ");
}
function data_from_js(msg){
var tag=document.createElement('div');
tag.innerHTML="message from python";
document.body.appendChild(tag);
alert(msg['name']);
}
</script>
<style>
body{
border:solid black 1px;
}
</style>
</doctype>"""
class Qbutton(QObject):
from time import sleep
def __init__(self):
super(Qbutton,self).__init__()
@Slot(str)
def reply(self,recd):
#r=QMessageBox.information(self,"Info",msg)
msgBox = QMessageBox()
sleep(10)
msgBox.setText("python just said"+recd)
msgBox.exec_()
return "I am recieving pythonic data"
#r=QMessageBox.question(self,title,recd,QMessageBox.Yes | QMessageBox.No)
@Slot(str)
def reply2(self,recd):
msgBox = QMessageBox()
msgBox.setText("python just said"+recd+ " another time")
msgBox.exec_()
return "I am recieving pythonic data"
@Slot(str)
def send_tojs(self):
pass


class adstar_gui(QWidget):
def __init__(self):
super(adstar_gui,self).__init__()
self.setWindowTitle("Adstar Wordlist Generator")
self.setMaximumWidth(5000)
self.setMaximumHeight(5000)
self.setMinimumWidth(500)
self.setMinimumHeight(500)
self.show()
print "Sample window"

def closeEvent(self,event):
self.closeEvent()
if __name__=="__main__":
Qapp=QApplication(sys.argv)
t=QWebView()
t.setHtml(html_str)
button=Qbutton()
t.page().mainFrame().addToJavaScriptWindowObject("object",button)
t.show()
#t.page().mainFrame().evaluateJavaScript("data_from_js(%s);" % (json.dumps({'name':"My name is Junior"}) ))
QCoreApplication.processEvents()
#sys.exit(Qapp.exec_())
Qapp.exec_()


QUESTION

How can I click on
button 1
in webkit and let python do something in the background when button 1 is clicked? (so that
button 2
function does not need to wait for button 1 function to finish)

Kindly use this demo and improve on it...much appreciated

Answer

There are a couple of issues here. First, it's worth pointing out why the app freezes when you click on button1: the click causes Qt to call the event handler, reply, and Qt can't handle another event until this handler returns (in my experience, all windowing systems work this way). So if you put any long running routine inside an event handler, your application will freeze until the routine finishes. Any time an event handler takes longer than about 0.05s, the user will notice.

As titusjan points out in his answer, it's pretty easy to get Qt to execute a function after a time interval. But I think your question isn't about how to handle a simple time delay, but rather how to handle a long-running process. In my example code, I replaced your ten second delay with a loop that counts ten one-second delays, which I think is a better model for what you are trying to achieve.

The solution is to do the long process in another thread. You have two options: QThreads, which are a part of the Qt environment, and Python threads. Both of them work, but I always use Python threads wherever possible. They're better documented and a little bit more lightweight. The ability to designate threads as daemons sometimes makes application shutdown a little simpler. Also, it's easier to convert a multithreaded program to one that uses multiprocesses. I used a Python thread in the example code below.

The problem then arises, how does the application know when the secondary thread is finished? For that purpose, you must create a custom Qt Signal. Your secondary thread emits this signal when it's done working, and the main app connects up a Slot to do something when that happens. If you're going to make a custom Qt Signal you must declare it in a subclass of QObject, as I did in the example.

Needless to say, all the standard multithreading issues have to be dealt with.

import sys
import json
import threading
from time import sleep
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtWebKit import QWebView, QWebSettings
from PySide.QtNetwork import QNetworkRequest
from PySide.QtCore import QObject, Slot, Signal

html_str="""<!doctype>
        <html>

        <body>hello world
        <button id="button" >button1</button>
        <button id="button2" >button2</button>
        </body>
    </html>
    <script type="text/javascript">
    document.getElementById("button").onclick=function(){
    object.reply(" hello ");
    }
    document.getElementById("button2").onclick=function(){
    object.reply2(" hello ");
    }
    function data_from_js(msg){
        var tag=document.createElement('div');
        tag.innerHTML="message from python";
        document.body.appendChild(tag);
        alert(msg['name']);
    }
    </script>
    <style>
    body{
    border:solid black 1px;
    }
    </style>
    </doctype>"""

class Qbutton(QObject):
    def __init__(self):
        super(Qbutton,self).__init__()
        self.long_thread = LongPythonThread()
        self.long_thread.thread_finished.connect(self.reply2_finished)

    @Slot(str)
    def reply(self,recd):
        print("reply")
        t = threading.Thread(target=self.long_thread.long_thread, args=(recd,))
        t.daemon = True
        t.start()

    @Slot(str)
    def reply2(self,recd):
        print("reply2")
        msgBox = QMessageBox()
        msgBox.setText("python just said"+recd)
        msgBox.exec_()
        return "I am receiving pythonic data"

    @Slot(str)
    def reply2_finished(self, recd):
        print("reply2 finished")
        msgBox = QMessageBox()
        msgBox.setText("python just said"+recd+ " another time")
        msgBox.exec_()

    @Slot(str)
    def send_tojs(self):
        pass

class LongPythonThread(QObject):    
    thread_finished = Signal(str)

    def __init__(self):
        super().__init__()

    def long_thread(self, recd):
        for n in range(10):
            sleep(1.0)
            print("Delayed for {:d}s".format(n+1))
        self.thread_finished.emit(recd)

if __name__=="__main__":
    Qapp=QApplication(sys.argv)
    t=QWebView()
    t.setHtml(html_str)
    button=Qbutton()
    t.page().mainFrame().addToJavaScriptWindowObject("object",button)
    t.show()
    #t.page().mainFrame().evaluateJavaScript("data_from_js(%s);" % (json.dumps({'name':"My name is Junior"}) ))
    QCoreApplication.processEvents()
    #sys.exit(Qapp.exec_())
    Qapp.exec_()
Comments