Porting from PyQt to PySide

I recently put together a small project in PyQt, but the license is something that makes me a bit unsettled. I wanted to release the sources under the MIT or BSD licenses, but the requirements of the GPL prohibit carrying out the spirit of either of these, at least in terms of practicality (no downstream commercial use). Thus, I’ve been somewhat torn: I appreciate the GPL, but I more deeply appreciate the MIT/BSD license family for the greater extent of freedom they afford. I disagree that allowing commercial use somehow precludes “freedom” because developers have to eat–and for that matter, I’ll probably purchase a PyQt license at some point in the future, if needed, because it is actively maintained by a small business.

Since starting the project, I rediscovered PySide, an LGPL-licensed alternative to PyQt that acts mostly as a drop-in replacement for the latter. There are some differences that require minor changes to your sources before it can act as a complete replacement, but they’re so minimal as to be of no consequence. Of the ones that I’ve encountered, I’ll document them here since it may not be immediately obvious (although some of the easier cases are).

Signal() and Slot() declarations

PySide’s signal and slot declarations are identical (more or less) to PyQt’s with the exception that they lack a “pyqt” prefix. Thus, the following code:

class MyDialog (QtGui.Dialog):
    progressFinished = QtCore.pyqtSignal()

Must be rewritten as:

class MyDialog (QtGui.Dialog):
    progressFinished = QtCore.Signal()

Likewise, @QtCore.pyqtSlot() decorators are supported by PySide but must be rewritten as @QtCore.Slot(). To this extent, PySide eliminates a handful of unnecessary characters and provides more obvious translation from C++.

setCheckState doesn’t accept integers

PyQt is somewhat more forgiving when passed Python data types and will usually attempt to do the right thing. PySide expects the bindings to follow their C++ cousins more or less exactly, including setting a checkbox’s state via setCheckState(). Thus, checkbox states cannot simply be assigned with the integers 0, 1, or 2 as is possible in PyQt. Instead, the developer must use QtCore.Qt.CheckState.Checked, QtCore.Qt.CheckState.PartiallyChecked, or QtCore.Qt.CheckState.Unchecked. This code does not appear to be compatible between PyQt and PySide.

Python data types may behave unexpectedly in signals

Similar to CheckState, signal arguments are not automatically wrapped (or converted) from Python data types to C++ data types. In one particular case I encountered, larger integers may generate “overflow” errors when exceeding the boundaries of a signed 32-bit integer (2.1 billion, or thereabouts). The only solution in this case is to declare the signal arguments as a Python object as per this example. Other incompatibilities may exist in signal declarations, but this is the one that I encountered fairly early on. Lists and dictionaries are correctly wrapped and behave as expected.

Most other behaviors (threads included) are identical between PyQt and PySide, which is useful for more complicated applications that require offloading activities to one or more threads and error messages appear similar enough (for the most part). More importantly, PySide is a mature enough alternative to PyQt for most uses, although I’ve yet to try it under Windows. It appears binary-only installation is supported on that platform.

No comments.
***

PyQt, py2exe, and pyinstaller

As I wrote in a previous installment of my links of the week, I discovered a really nice replacement for py2exe. However, pyinstaller doesn’t seem to play nice with PyQt…

The documentation and mailing lists suggest adding hooks for “hidden imports” in the pyinstaller/hooks directory. I’ve been having problems with this for quite some time:

Traceback (most recent call last):
  File "", line 47, in 
NameError: name 'QtGui' is not defined

Whenever I attempt to run an executable built from my Qt application. I’ve tried creating a hooks-PyQt4.QtGui.py, hooks-PyQt4.QtGui.py, and a number of permutations thereof without much luck. So, I decided to go back to trying out py2exe only to discover that the problem I was having with it was due to a really stupid mistake on my part.

As it turns out, you cannot simply create a setup.py setup script anywhere you like, even during testing. Here’s my application layout (roughly):

Chatly/
  chatly/
    client/
      ui/
        __init__.py
        qtui.py
        ui.py
      __init__.py
      client.py
    server/
    __init__.py

I had placed my setup.py under Chatly/chatly/client temporarily in the hopes it’d save some typing when indicating the script path during the build process. ‘Course, if I had been thinking clearly about previous .eggs I’ve built, I should have known this wouldn’t work. Nevertheless, I tried it and ran into this:

D:\python25\lib\distutils\dist.py:263: UserWarning: Unknown distribution option:
 'console'
  warnings.warn(msg)
usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
   or: setup.py --help [cmd1 cmd2 ...]
   or: setup.py --help-commands
   or: setup.py cmd --help

error: invalid command 'py2exe'

(Yeah, this is from Windows. Since I’m building a Windows app in Python, it’s a necessity. Cygwin doesn’t work for this in particular, unfortunately. I hate the command prompt–it’s too anemic and lacks the utility of a proper shell.)

Since I’ve always created Python applications in distributable packages (like eggs) the right way, I’ve never experienced this particular error. When I was searching Google for clues, I expected some obvious answer. There weren’t any. Some people discovered they had py2exe installed twice (how?) and others were simply not using the correct arguments passed to setup(). Since my setup.py looked like this:

1
2
3
from setuptools import setup
import py2exe
setup(windows=["chatly/client/ui/qtui.py"])

I was pretty sure that wasn’t a problem. I did read the py2exe documentation, after all.

Then I tried something. I figured that if, perhaps, py2exe were causing a problem, I could simply uninstall it, run the setup.py as it existed, and it’d generate an ImportError, right?

Nope. Same error.

That was when I began to realize it was some rather stupid mistake on my part with the choice of positioning for setup.py. When I moved it to the project root outside any package where it’s supposed to live, it worked fine. Oops.

The problem with building a PyQt application still troubled me, though. The documentation for py2exe is fairly extensive (thankfully), and after some digging around, I discovered that the missing QtGui import–rather, the one that wasn’t working–could be resolved similarly to the _qt issue mentioned in py2exe’s documentation. The result was a setup.py that looks like this:

1
2
3
4
from setuptools import setup
import py2exe
setup(windows=["chatly/client/ui/qtui.py"], options={"py2exe": {
         "includes": ["sip", "PyQt4.QtGui"]}})

PyQt4.QtGui is required to import the Qt GUI classes since they’re evidently not imported by py2exe’s internals during build. sip is also required (it’s a part of the PyQt build process) and without it, nothing will work.

The finished product is a distributable package that works rather nicely–on Windows.

No comments.
***

PyQt Center on Screen

wxPython has been annoying me with the inconsistent behavior of its splitter window. Perhaps I’m doing something wrong, but the framework in general isn’t exactly fun to develop in. I’ve since discovered PyQt and while it’s not exactly Pythonic, it seems to make more sense. However, if you’re moving from wxPython to PyQt, you might be aware that there’s no obvious method of centering the main window on the screen. I’ve seen at least one method so far (to which I replied as my alter ego, Zancarius), but it doesn’t take into account something fairly minor but of some significance:

Windows have borders around them in most desktop environments!

Window borders can range from fairly minor (4 pixels around each side adding a total of 8 pixels horizontally and vertically) to imposing (16 pixels or more). Unfortunately, QWidget.geometry() and friends only return a QSize object describing the geometry of the contained window rather than the containing window. It seems insignificant, sure, but if you want pixel-perfect alignment, you have to use something else:

QWidget.frameSize()

frameSize() will return the size of the entire window, border included. Here’s a method you can include in your own classes as in this example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ExampleWindow (QtGui.QMainWindow):
    def __init__ (self, parent=None):
        '''constructor'''
        QtGui.QMainWindow.__init__(self, parent)
        self.setGeometry(0, 0, 650, 550)
        self.setWindowTitle("My Example Application")
        self.centerOnScreen()
 
    def centerOnScreen (self):
        '''centerOnScreen()
Centers the window on the screen.'''
        resolution = QtGui.QDesktopWidget().screenGeometry()
        self.move((resolution.width() / 2) - (self.frameSize().width() / 2),
                  (resolution.height() / 2) - (self.frameSize().height() / 2))

centerOnScreen() works by taking the desktop’s current resolution and dividing it by 2 to get half of the viewport. Next, it takes half of the window size (including the border) and subtracts that from the viewport half. This provides an origin for the upper-left corner of the window to be located such that the window itself is fully “centered.”

Simple, huh?

No comments.
***