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.
***

wxPython Menu Status Text

wxPython isn’t quite as easy to develop for as some other GUI toolkits but the greatest benefit is that it uses native widgets in Windows. I believe the PyQT bindings also support native widgets and have since QT 4, but I personally find that certain simple layouts behave slightly faster in wxPython than PyQT. Perhaps I’ll test this theory and be surprised whenever I put together a QT-based project next!

One of the unfortunate problems with wx is that the toolkit’s documentation requires a significant amount of introspection, some knowledge of what subject in particular is sought after, and a little bit of experimentation. The examples that ship with wxPython are absolutely fantastic and make up for what the documentation lacks. However, some things aren’t very obvious. Here’s a good example.

If you open Windows Explorer (this is the XP version–Vista and Seven both do something similar) and mouse-over each of the menu items, you’ll notice brief snippets of help text on the status bar. Here’s a shot of the status bar before:

screenshot_01

And here’s a shot while leaving the mouse over “File”:

screenshot_02

Something like this is pretty easy to accomplish in .NET and merely requires attaching a method to the appropriate handler. This isn’t so obvious in wxPython (and I’d imagine the wx toolkit in general) and requires a bit of work.

Your first inclination might be to do something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MyFrame (wx.Frame):
    def __init__ (self):
        wx.Frame.__init__(self, None, -1, "My App", pos=wx.DefaultPosition,
                          size=(300, 300), style=wx.DEFAULT_FRAME_STYLE)
        # In whatever part of your code you're adding the file menu...
        menu = wx.MenuBar()
        file = wx.Menu()
        file.Append(100, "&New\tCtrl+N")
        file.Append(101, "E&xit\tAlt+F4")
        menu.Append(file, "&File")
 
        ##
        # Add some bindings...
 
        # This only works AFTER the menu has been opened
        # and only when we highlight a menu ITEM... oops
        self.Bind(wx.EVT_MENU_HIGHLIGHT, self.OnMenuHighlight)
 
        # Close, but not quite--you'll see why in a minute
        self.Bind(wx.EVT_MENU_OPEN, self.OnMenuOpen)
 
    def OnMenuHighlight (self, event):
        # Do highlight
        pass
 
    def OnMenuOpen (self, event):
        # Process opening a menu
        pass

At first blush it would seem that the wx.EVT_MENU_HIGHLIGHT gives us exactly the event we want. However, once we run our application we’ll be surprised to note that this doesn’t work as expected. The wx.EVT_MENU_HIGHLIGHT is triggered only when a menu item is highlighted. wx.EVT_MENU_OPEN is much closer to what we want, but there’s one small problem. Before I can explain the issue, let’s look into some object introspection.

First, let’s assume that we have the following in our OnMenuOpen() function:

1
2
3
4
5
    def OnMenuOpen (self, event):
        print "event.GetMenu()\n"
        print dir(event.GetMenu())
        print "\nevent.GetMenu().GetMenuBar()\n"
        print dir(event.GetMenu().GetMenuBar())

This will yield something like the following (I’ve snipped out the irrelevant parts):

event.Menu:

['FindItem', 'FindItemById', 'FindItemByPosition', 'GetClassName', 'GetEventHandler',
'GetEvtHandlerEnabled', 'GetHelpString', 'GetInvokingWindow', 'GetLabel', 'GetLabelText',
'GetMenuBar', 'GetMenuItemCount', 'GetMenuItems', 'GetNextHandler', 'GetParent',
'GetPreviousHandler', 'GetStyle', 'GetTitle', 'HelpString']

event.Menu.MenuBar:

['FindFocus', 'FindItemById', 'FindMenu', 'FindMenuItem', 'FindWindowById', 'FindWindowByLabel',
'FindWindowByName', 'Fit', 'FitInside', 'Font', 'ForegroundColour', 'Frame', 'Freeze',
'GetAcceleratorTable', 'GetAdjustedBestSize', 'GetAutoLayout', 'GetAutoWindowMenu',
'GetBackgroundColour', 'GetBackgroundStyle', 'GetBestFittingSize', 'GetBestSize',
'GetBestSizeTuple', 'GetBestVirtualSize', 'GetBorder', 'GetCapture', 'GetCaret', 'GetCharHeight',
'GetCharWidth', 'GetChildren', 'GetClassDefaultAttributes', 'GetClassName','GetClientAreaOrigin',
'GetClientRect', 'GetClientSize', 'GetClientSizeTuple', 'GetConstraints', 'GetContainingSizer',
'GetCursor', 'GetDefaultAttributes', 'GetDefaultBorder', 'GetDropTarget', 'GetEffectiveMinSize',
'GetEventHandler', 'GetEvtHandlerEnabled', 'GetExtraStyle', 'GetFont', 'GetForegroundColour',
'GetFrame', 'GetFullTextExtent', 'GetGrandParent', 'GetGtkWidget', 'GetHandle', 'GetHelpString',
'GetHelpText', 'GetHelpTextAtPoint', 'GetId', 'GetLabel', 'GetLabelTop', 'GetLayoutDirection',
'GetMaxHeight', 'GetMaxSize', 'GetMaxWidth', 'GetMenu', 'GetMenuCount', 'GetMenuLabel',
'GetMenuLabelText', 'GetMenus', 'GetMinHeight', 'GetMinSize', 'GetMinWidth', 'GetName',
'GetNextHandler', 'GetParent', 'GetPosition', 'GetPositionTuple', 'GetPreviousHandler', 'GetRect',
'GetScreenPosition', 'GetScreenPositionTuple', 'GetScreenRect', 'GetScrollPos', 'GetScrollRange',
'GetScrollThumb', 'GetSize', 'GetSizeTuple', 'GetSizer', 'GetSizerProps', 'GetTextExtent',
'GetThemeEnabled', 'GetToolTip', 'GetTopLevelParent', 'GetUpdateClientRect', 'GetUpdateRegion',
'GetValidator', 'GetVirtualSize', 'GetVirtualSizeTuple', 'GetWindowBorderSize', 'GetWindowStyle',
'GetWindowStyleFlag', 'GetWindowVariant']

The answer seems obvious–at first. All we need to do is add event.GetMenu() to our OnMenuOpen() function and… wait. How are we going to know which menu was opened? There’s no identifier we can use. Maybe if we use event.GetMenu().GetLabelText() we can examine the string for “File” or whichever menu the user has opened…

Traceback (most recent call last):
  File "ui.py", line 37, in OnMenuOpen
    print event.GetMenu().GetLabelText()
  File "D:\Python25\Lib\site-packages\wx-2.8-msw-unicode\wx\_core.py", line 1126
8, in GetLabelText
    return _core_.Menu_GetLabelText(*args, **kwargs)
TypeError: Menu_GetLabelText() takes exactly 2 arguments (1 given)

Damn.

We then try various incantations of this code. We might even wind up with something embarrassingly convoluted like event.GetMen().GetMenuBar().Get… Ugh. I think I’ve lost myself after that one.

Fortunately, the solution isn’t terribly difficult. You can even add some additional features yourself, if you like. But here’s the catch: There isn’t anything in the documentation that indicates if it’s even possible using the existing classes to figure out which menu someone has opened. So, if you’ve got “File,” “Edit,” “View,” and “Help” menus, you’re out of luck–at least with the stock wx classes.

Here’s the quick and dirty solution that worked for me. It may not be optimal, but it sure beats screwing around with object introspection and trying to Get() a clue. (See that pun I did there?)

Simply extend wx.Menu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#########
# Menu
class Menu (wx.Menu):
    '''Menu
Overrides the default wx.Menu object to provide offset and status text
monitoring.'''
    offset = -1
    statusText = ""
    def __init__ (self, offset=-1, status=""):
        '''constructor
Create a new menu item.
 
``offset`` indicates what offset this menu item lives at in the menu but can
be any ID you prefer.
 
``Status`` indicates the status text you wish to use with this menu item for
retrieval by the status bar.'''
        wx.Menu.__init__(self)
        self.offset = offset
        self.statusText = status
 
    def GetOffset (self):
        '''GetOffset()
Returns the user-defined offset to identify this menu item's position in the
menu bar.'''
        return self.offset
 
    def GetStatusText (self):
        '''GetStatusText()
Returns text associated with this control for shuttling to the status bar.'''
        return self.statusText
## end Menu

And then modify your OnMenuOpen() function to read:

1
2
3
4
5
6
    def OnMenuOpen (self, event):
        '''OnMenuOpen (event)
Triggered whenever a menu itme is opened. Sets the default status text.'''
        print event.GetMenu().GetLabel(1)
        if hasattr(event.GetMenu(), "GetStatusText"):
            self.statusBar.SetStatusText(event.GetMenu().GetStatusText(), 0)

Then when you create a new menu item, just simply pass the offset and the text to your new class:

1
2
3
4
5
        menu = wx.MenuBar()
        file = wx.Menu(0, "Contains file operations")
        file.Append(100, "&New\tCtrl+N")
        file.Append(101, "E&xit\tAlt+F4")
        menu.Append(file, "&File")

The hasattr() call is needed for submenu items otherwise OnMenuOpen() will trigger an exception whenever the user mouses over a submenu. If you don’t have submenus, you probably don’t need to worry about this. In this example, I assume that we’ve added a wx.StatusBar to our initial class.

Combining this trick with the wx.EVT_MENU_CLOSE event will yield a fairly painless method of altering (and resetting) the status bar text whenever the user opens a menu item.

No comments.
***