Wednesday, January 20, 2010

More fun with QWebKit

In the previous post I wrote about calling python methods and accessing properties from JavaScript executed in QWebKit. In this post I'll show you how to do the other way around, calling a JavaScript function from Python.

I would like to thank Rich Moore for commenting on the previous post and pointing out that you should re-add your python object  every time the javaScriptWindowObjectCleared() signal is emitted.

Actually, I think this is almost to simple to do a post about so I'll try doing something fun/useful with it.

QWebFrame contains a public slot:
QVariant evaluateJavaScript(const QString & scriptSource)
You simply pass the JavaScript-snippet to the function. The snippet will be evaluated using the frame as context and returns the result of the last executed statement. This makes it possible to 'click' submit buttons, fill form fields and other interesting stuff in the currently loaded frame. I'll give you an example on how you can create an auto-login GMail widget. The widget will automatically login the user by populating the login form with the user's credentials and 'click' the login button.
#!/usr/bin/env python
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4 import QtWebKit

javaScriptLogin = """
document.getElementsByName('Email').item(0).value='{email}';
document.getElementsByName('Passwd').item(0).value='{password}';
document.getElementsByName('signIn').item(0).click(); void(0);
"""

class GmailWebView(QtWebKit.QWebView):
    def __init__(self, parent=None):
        super(GmailWebView, self).__init__(parent)
        self.loggedIn = False

    def login(self, url, email, password):
        """Login to gmail."""
        self.url = QtCore.QUrl(url)
        self.email = email
        self.password = password  
        self.loadFinished.connect(self._loadFinished)
        self.load(self.url)

    def createWindow(self, windowType):
        """Load links in the same web-view."""
        return self

    def _loadFinished(self):
        if self.loggedIn:
            self.loadFinished.disconnect(self._loadFinished)

        self.loggedIn = True
        jscript = javaScriptLogin.format(email=self.email, password=self.password)
        self.page().mainFrame().evaluateJavaScript(jscript)

    def contextMenuEvent(self, event):
        """Add a 'Back to GMail' entry."""
        menu = self.page().createStandardContextMenu()
        menu.addSeparator()
        action = menu.addAction('Back to GMail')
        @action.triggered.connect
        def backToGMail():
            self.load(self.url)
        menu.exec_(QtGui.QCursor.pos())

def main():
    import sys
    qApp = QtGui.QApplication(sys.argv)

    # Prevents me from posting my password on the blog :)
    password, ok = QtGui.QInputDialog.getText(None, "Password request", "Enter password", QtGui.QLineEdit.Password)
    if not ok:
        return

    gmailWebView = GmailWebView()
    gmailWebView.login('https://mail.google.com/mail',
                       'mario.boikov',
                       password)
    gmailWebView.show()

    sys.exit(qApp.exec_())

if __name__ == "__main__":
    main()
This is just a quick hack, it lacks a bunch of checkings...

Maybe it's time to wipe the dust off my GMonitor-plasmoid and add support for opening my account in a new window based on the GMail widget above.

7 comments:

  1. Hello .. When I launch this script , it open gmail login page , I can see login/password are paste into login form. But just after ( when click/sign in ? ), the script die with 'Illegal instruction' or 'Segmentation fault'. If I enter a wrong password , I can enter my password and sign in ..

    (sorry for my poor English).

    David [a.k.a] ADcomp

    ReplyDelete
  2. ADcomp: Hi!

    I also initially had some problems with the app crashing... I'm uncertain but it could have something to do with the slot being disconnected while the signal is executed. See what happens if you replace the 'disconnect' in _loadFinished() with 'return'.

    I can't reproduce the error here. I'm using PyQt 4.5 (Ubuntu 9.10).

    Keep me updated

    ReplyDelete
  3. Hi Mario,

    Thanks for your answer. If I replace 'disconnect' , It doesn't die anymore with 'Segmentation fault' but sometimes I have to 'reload' ( only white page ).

    python-qt4 4.6-1 / Ubuntu 9.10

    Bye

    ReplyDelete
  4. Nice example. I didn't know decorators could be used like that with pyqt.

    What is the purpose of createWindow()? Does reusing the same view save resources?

    ReplyDelete
  5. Richard: It's been a while since I wrote this post... I think that you need to override createWindow if you want webkit to open a page when a user clicks on a link.

    I wanted to use the same view when the user clicks on a link. You could as well create a new webview which you add to a tabbar or something similar.

    ReplyDelete
  6. Hi Mario,
    After submitting the form with:
    self.page().mainFrame().evaluateJavaScript(jscript)
    How do I get the source code of the new loaded page? Is the source code reloaded automatically to self.page().mainFrame()?
    I tried [print str(self.page().mainFrame().toHtml().toAscii())], but what I got was the source code of the first page(gmail login page) and not the account page where all the emails are listed.

    Any idea what I’m missing?
    Thanks!

    ReplyDelete
  7. Many many thanks for this post - I was looking forever for a Python example for QtWebKit and custom context menu; this post finally told me I was missing menu.exec_(QtGui.QCursor.pos()) ... Cheers!

    ReplyDelete