21 June 2011

Frequently overlooked (and practical) PyQt4 features

To this day I have only made four or five full-fledged PyQt4 applications (all being university projects). Still I've come across a couple of interesting and rarely mentioned features. Recently I noticed them in a tiny WebKit based browser written by Roberto Alsina. It's best to provide examples so I'm going to use snippets from devicenzo (it's a goldmine) and some of my own, when appropriate.

It's important to say that many of the following features are specific to PyQt4, hence it's easy to overlook them while reading only (original) Qt4 documentation. Unsurprisingly PyQt4 (and PySide to be impartial) is usually more Pythonic.


Property setting / slot connecting on object creation

This is probably the most useful PyQt4-specific thing. Let's take the example from official documentation: you want to create some QAction object, customize some of it's properties and attach an event listener (connect a signal to a slot in Qt's terminology). You'd write something along these lines:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from PyQt4 import QtGui

# ...

def __init__(self, parent):
    act = QtGui.QAction("&Save", self)
    # Setting some properties.
    act.setShortcut(QtGui.QKeySequence.Save)
    act.setStatusTip("Save the document to disk")
    # save() is some custom slot method.
    act.triggered.connect(self.save)
Obviously it's not beautiful -- especially all those set* methods -- but we will make it better.
First of all we can use a special method of QObject (and its all child classes) which is pyqtConfigure:
1
2
3
4
5
6
7
8
9
from PyQt4 import QtGui

# ...

def __init__(self, parent):
    act = QtGui.QAction("&Save", self)
    # Setting all at once.
    act.pyqtConfigure(shortcut=QtGui.QKeySequence.Save,
        statusTip="Save the document to disk", triggered=self.save)
It's a really clean way to initialize all necessary properties and slots in one place. Although that's not all, now we make it even shorter:
1
2
3
4
5
6
7
from PyQt4 import QtGui

# ...

def __init__(self, parent):
    act = QtGui.QAction("&Save", self, shortcut=QtGui.QKeySequence.Save,
        statusTip="Save the document to disk", triggered=self.save)
You'll notice something disturbing: the docs say QAction's constructor takes at most three arguments! That's true but we can use a shortcut to pyqtConfigure and just specify all those additional properties as (optional) keyword arguments -- the magic.
Real life example from devicenzo:
1
2
3
4
5
# ...
self.tabs = QtGui.QTabWidget(self, tabsClosable=True, movable=True,
    currentChanged=self.currentTabChanged, elideMode=QtCore.Qt.ElideRight,
    tabCloseRequested=lambda idx: self.tabs.widget(idx).deleteLater()
)
What's particularly interesting about this piece is the last argument: tabCloseRequested. Again it's inlined signal-to-slot connection but here the slot method is an ordinary lambda (defined in-place). This is another useful PyQt4 pattern -- cleaner and probably faster than normal Python function.

By the way, there's another gem hidden in this example, the deleteLater() function (slot). Its use is to schedule [an] object for deletion [...] when control returns to the event loop. That definition isn't really helpful considering we are used to objects "disappearing" automatically through garbage collecting. Still, we have to remember there's a whole C++ object layer underneath PyQt4. deleteLater() is (most probably) the only way to make sure an object is finally gone (deallocated) in the right time. Although you rarely have to use it, I noticed it's needed to reclaim memory taken by QTableWidgetItems and apparently (see example) QTabWidget's tabs. Just be careful with widgets storing large numbers of other widgets. Usually Qt4's dependency mechanism and Python's reference counting will suffice.

Lastly, it's also possible to define your own PyQt4-style property -- again see the docs.


What is the source of incoming signal

In the slot function it's usually good to know where the signal comes from -- especially when you have one function taking care of many objects' signals. The most natural way to do this would look like this:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from PyQt4.QtGui import QToolButton

# ...

def __init__(self, parent):
    btn = QToolButton(self, text="Foo")
    btn.clicked.connect(lambda: button_clicked(btn))

def button_clicked(self, button):
    # Doing something specific to the button.
    if button == x:
        pass
We're just using lambda to smuggle signal sender reference into the slot function. It may be even Pythonic but it's not really clean. Fortunately QObject (and every widget) provides us with a helpful sender() method which returns the sender object -- exactly what we need. Now, previous example evolves into this:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from PyQt4.QtGui import QToolButton

# ...

def __init__(self, parent):
    btn = QToolButton(self, text="Foo", clicked=button_clicked)
    
def button_clicked(self):
    # Doing something specific to the button.
    if self.sender() == x:
        pass
Notice how I also used previously discussed feature of inline signal/slot connection. Also, do remember that calling sender() only makes sense inside a slot function scope.


Quick tip: Qt4 debugging with (i)pdb / pudb

Since Qt4 has its own event loop it's not that easy to debug PyQt4 applications using standard tools. Python support for QtCreator is nowhere near. To be able to use your favourite Python debugger (pdb/ipdb/pudb) prepare the following snippet and call it every time you want to pause the execution:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def debug():
    from PyQt4.QtCore import pyqtRemoveInputHook
    pyqtRemoveInputHook()
    # Load either ipdb.
    from ipdb import set_trace
    # Or pdb.
    # from pdb import set_trace
    # Or pudb.
    # from pudb import set_trace
    # Enter debugging mode.
    set_trace()
Your debugger will show up when the execution reaches debug() call. It's somewhat crude way of debugging since there are IDEs that allow you to debug PyQt4 application in usual step-in/-over/-out way. As far as I've read Komodo IDE works great and PyCharm is reportedly also well equipped.

3 comments:

  1. awesome, very nice,
    though i didnt liked how the code gets using the configurator, is really harder to read, and prone to errors.
    with the set approach you can comment out a line if its necesary.

    but once you have everything well proved you can use that form.

    ReplyDelete
    Replies
    1. I'm not sure I understood fully but you shouldn't worry about occasional errors or typos. PyQt4 won't let you initialize class (or configure) with keyword arguments it doesn't understand.
      I have no way to test it right now but you'd probably get AttributeError.

      Delete