Python GUI programming platforms for Windows

[Edit]
By popular demand, I've added a section on PyGTK. See bottom of post.

There are several platforms for programming Windows GUI applications in Python. Below I outline a few of them, with a simple "hello world" example for each. Where I've lifted the example from another site, there's a link to the source.

Contents

Tkinter

Tkinter is the ubiquitous GUI toolkit for Python. It's cross platform and easy to use, but it looks non-native on just about every platform. There are various add-ons and improvements you can find to improve the look and feel, but the basic problem is that the toolkit implements its own widgets, rather than using the native ones provided on the platform.

Pros

  • Most portable GUI toolkit for Python
  • Very easy to use, with pythonic API

Cons

  • Non-native look and feel out of the box

Hello world example (code source):

import Tkinter as Tk
la = Tk.Label(None, text='Hello World!', font=('Times', '18'))
la.pack()
la.mainloop()
 

Back to Top

wxPython

wxPython is probably the most popular GUI toolkit for Python. It's a wrapper for the wxWidgets C++ toolkit, and as such it betrays a few unpythonic edges (like lumpy case, getters and setters, and funky C++ errors creeping up occasionally). There are a few pythonification efforts on top of wxPython, such as dabo and (the now apparently moribund) wax.

Pros

  • Highly cross platform
  • Relatively mature and robust
  • Uses native Windows widgets for authentic look and feel

Cons

  • Must include large wx runtime when packaging with py2exe (adds ~7 MB)
  • Cross platform nature makes accessing some native platform features (like ActiveX) difficult to impossible

Hello world example (code source):

import wx

class Application(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, -1, 'My GUI', size=(300, 200))
        panel = wx.Panel(self)
        sizer = wx.BoxSizer(wx.VERTICAL)
        panel.SetSizer(sizer)
        txt = wx.StaticText(panel, -1, 'Hello World!')
        sizer.Add(txt, 0, wx.TOP|wx.LEFT, 20)
        self.Centre()
        self.Show(True)

app = wx.App(0)
Application(None)
app.MainLoop()

Back to Top

.NET with IronPython

IronPython is a .NET implementation of Python. As of 1.0 it has full support for Python 2.4 features, and the 2.0 version will duplicate the Python 2.5 feature set. Although there are many CPython libraries/modules that won't run under IronPython (namely, the ones relying on compiled extensions that have not yet been ported), this lack is partially made up by the huge .NET library.

One cool thing about IronPython is that you can easily create lightweight .exe files that you can ship off to your friends — although you pay for this with a dependency on the .NET runtime, which you can't count on random Windows users to have installed.

Of course, when you go the IronPython route, you take all that comes with it: the good things, like access to .NET libraries and possibly the easiest/cleanest optimization path of any Python implementation (C#); and the bad things, like dependence on the .NET runtime and danger of getting caught on the MS upgrade treadmill.

Another way of getting at the .NET libraries is Python.NET, which adds two files to your Python directory to enable you to call the CLR from CPython.

Pros

  • Leverage .NET libraries
  • Easily create .exe files

Cons

  • Depends on .NET runtime

Hello world example (code source):

import sys
sys.path.append(r'C:\Python24\Lib')

import clr
clr.AddReference("System.Windows.Forms")

from System.Windows.Forms import Application, Form

class HelloWorldForm(Form):

    def __init__(self):
        self.Text = 'Hello World'
        self.Name = 'Hello World'

form = HelloWorldForm()
Application.Run(form)
 

Back to Top

PyQT

PyQT is probably the third most widely used GUI toolkit, after wxPython and Tkinter. It has a dual commercial/GPL license (Edit: but it does let you use other open-source licenses; see comments below). I have to admit that this made it a non-starter for me: I don't want to pay for my toolkit when there are others just as good or better that are free; and when I do release open-source software, I want to choose my own license. For others, the GPL might be a non-issue or a plus, so I've left it off my pro/con list.

[Edit] PyQT will be available under the LGPL as of March 2009. Fantastic news.

Pros

  • Highly cross platform
  • Very easy to use
  • Highly mature
  • Decent looking widgets

Cons

  • Somewhat non-native look and feel (though much better than Tkinter)
  • Must include large runtime when packaging with py2exe

Hello world example (from PyQT docs):

PyQT screen shot
import sys
from PyQt4 import QtGui

app = QtGui.QApplication(sys.argv)

hello = QtGui.QPushButton("Hello world!")
hello.resize(100, 30)

hello.show()

sys.exit(app.exec_())

Back to Top

Pyglet

Pyglet is kind of the new kid on the block in terms of GUI toolkits, but it sure made a splash. It implements its own windowing system, but with no dependencies other than Python (for Python 2.5 users). You will need OpenGL to do decent 3D graphics, but that's hardly a black mark for pyglet — other libraries would love to make it this easy.

Pros

  • High degree of freedom for GUI creation
  • Only depends on Python
  • Large number of widgets

Cons

  • Purposely doesn't duplicate the native platform look and feel
  • Although there are a lot of widgets, you'll have to roll your own for many things the platform gives you for free.

Hello world example (slightly modified from code source):
hello world with pyglet screenshot

from pyglet import font
from pyglet import window

win = window.Window(width=300, height=150, caption="Hello World")

ft = font.load('Arial', 36)
text = font.Text(ft, 'Hello, World!')

while not win.has_exit:
    win.dispatch_events()
    win.clear()
    text.draw()
    win.flip()
 

Back to Top

Win32 with ctypes

Of course, all you really need to write GUI applications on Windows with Python is your trusty ctypes module and a well worn copy of Petzold. The benefit of this style is that you're working right down at the system API level, with nothing to get in your way. The disadvantage is that you're working right down at the system API level, with nothing to relieve you from all that boilerplate (unless you write your own abstraction layer on top; see Venster, below…).

Pros

  • Enables high level of control
  • Straightforward if familiar with Win32 API
  • No added complexity or buried functionality due to need to be cross-platform
  • Lightest of all Windows GUI programming methods using Python

Cons

  • All the complexity and inconsistency of Win32 API in gory detail
  • Lack of high-level libraries (have to write more code)

Hello world example (long, ain't it?):
Win32 GUI screen shot

from ctypes import *
import win32con

WNDPROC = WINFUNCTYPE(c_long, c_int, c_uint, c_int, c_int)

NULL = c_int(win32con.NULL)
_user32 = windll.user32

def ErrorIfZero(handle):
    if handle == 0:
        raise WinError()
    else:
        return handle

CreateWindowEx = _user32.CreateWindowExW
CreateWindowEx.argtypes = [c_int,
                           c_wchar_p,
                           c_wchar_p,
                           c_int,
                           c_int,
                           c_int,
                           c_int,
                           c_int,
                           c_int,
                           c_int,
                           c_int,
                           c_int]
CreateWindowEx.restype = ErrorIfZero

class WNDCLASS(Structure):
    _fields_ = [('style', c_uint),
                ('lpfnWndProc', WNDPROC),
                ('cbClsExtra', c_int),
                ('cbWndExtra', c_int),
                ('hInstance', c_int),
                ('hIcon', c_int),
                ('hCursor', c_int),
                ('hbrBackground', c_int),
                ('lpszMenuName', c_wchar_p),
                ('lpszClassName', c_wchar_p)]

    def __init__(self,
                 wndProc,
                 style=win32con.CS_HREDRAW | win32con.CS_VREDRAW,
                 clsExtra=0,
                 wndExtra=0,
                 menuName=None,
                 className=u"PythonWin32",
                 instance=None,
                 icon=None,
                 cursor=None,
                 background=None,
                 ):

        if not instance:
            instance = windll.kernel32.GetModuleHandleW(c_int(win32con.NULL))
        if not icon:
            icon = _user32.LoadIconW(c_int(win32con.NULL),
                                     c_int(win32con.IDI_APPLICATION))
        if not cursor:
            cursor = _user32.LoadCursorW(c_int(win32con.NULL),
                                         c_int(win32con.IDC_ARROW))
        if not background:
            background = windll.gdi32.GetStockObject(c_int(win32con.WHITE_BRUSH))

        self.lpfnWndProc=wndProc
        self.style=style
        self.cbClsExtra=clsExtra
        self.cbWndExtra=wndExtra
        self.hInstance=instance
        self.hIcon=icon
        self.hCursor=cursor
        self.hbrBackground=background
        self.lpszMenuName=menuName
        self.lpszClassName=className

class RECT(Structure):
    _fields_ = [('left', c_long),
                ('top', c_long),
                ('right', c_long),
                ('bottom', c_long)]
    def __init__(self, left=0, top=0, right=0, bottom=0 ):
        self.left = left
        self.top = top
        self.right = right
        self.bottom = bottom

class PAINTSTRUCT(Structure):
    _fields_ = [('hdc', c_int),
                ('fErase', c_int),
                ('rcPaint', RECT),
                ('fRestore', c_int),
                ('fIncUpdate', c_int),
                ('rgbReserved', c_wchar * 32)]

class POINT(Structure):
    _fields_ = [('x', c_long),
                ('y', c_long)]
    def __init__( self, x=0, y=0 ):
        self.x = x
        self.y = y

class MSG(Structure):
    _fields_ = [('hwnd', c_int),
                ('message', c_uint),
                ('wParam', c_int),
                ('lParam', c_int),
                ('time', c_int),
                ('pt', POINT)]

def pump_messages():
    """Calls message loop"""
    msg = MSG()
    pMsg = pointer(msg)

    while _user32.GetMessageW(pMsg, NULL, 0, 0):
        _user32.TranslateMessage(pMsg)
        _user32.DispatchMessageW(pMsg)

    return msg.wParam

class Window(object):
    """Wraps an HWND handle"""

    def __init__(self, hwnd=NULL):
        self.hwnd = hwnd

        self._event_handlers = {}

        # Register event handlers
        for key in dir(self):
            method = getattr(self, key)
            if hasattr(method, "win32message") and callable(method):
                self._event_handlers[method.win32message] = method

    def GetClientRect(self):
        rect = RECT()
        _user32.GetClientRect(self.hwnd, byref(rect))
        return rect

    def Create(self,
            exStyle=0 ,        #  DWORD dwExStyle
            className=u"WndClass",
            windowName=u"Window",
            style=win32con.WS_OVERLAPPEDWINDOW,
            x=win32con.CW_USEDEFAULT,
            y=win32con.CW_USEDEFAULT,
            width=win32con.CW_USEDEFAULT,
            height=win32con.CW_USEDEFAULT,
            parent=NULL,
            menu=NULL,
            instance=NULL,
            lparam=NULL,
            ):

        self.hwnd = CreateWindowEx(exStyle,
                              className,
                              windowName,
                              style,
                              x,
                              y,
                              width,
                              height,
                              parent,
                              menu,
                              instance,
                              lparam)
        return self.hwnd

    def Show(self, flag):
        return _user32.ShowWindow(self.hwnd, flag)

    def Update(self):
        if not _user32.UpdateWindow(self.hwnd):
            raise WinError()

    def WndProc(self, hwnd, message, wParam, lParam):

        event_handler = self._event_handlers.get(message, None)
        if event_handler:
            return event_handler(message, wParam, lParam)
        return _user32.DefWindowProcW(c_int(hwnd),
                                      c_int(message),
                                      c_int(wParam),
                                      c_int(lParam))

## Lifted shamelessly from WCK (effbot)'s wckTkinter.bind
def EventHandler(message):
    """Decorator for event handlers"""
    def decorator(func):
        func.win32message = message
        return func
    return decorator

class HelloWindow(Window):
    """The application window"""

    @EventHandler(win32con.WM_PAINT)
    def OnPaint(self, message, wParam, lParam):
        """Draw 'Hello World' in center of window"""
        ps = PAINTSTRUCT()
        rect = self.GetClientRect()
        hdc = _user32.BeginPaint(c_int(self.hwnd), byref(ps))
        rect = self.GetClientRect()
        flags = win32con.DT_SINGLELINE|win32con.DT_CENTER|win32con.DT_VCENTER
        _user32.DrawTextW(c_int(hdc),
                          u"Hello, world!",
                          c_int(-1),
                          byref(rect),
                          flags)
        _user32.EndPaint(c_int(self.hwnd), byref(ps))
        return 0

    @EventHandler(win32con.WM_DESTROY)
    def OnDestroy(self, message, wParam, lParam):
        """Quit app when window is destroyed"""
        _user32.PostQuitMessage(0)
        return 0

def RunHello():
    """Create window and start message loop"""

    # two-stage creation for Win32 windows
    hello = HelloWindow()

    # register window class…
    wndclass = WNDCLASS(WNDPROC(hello.WndProc))
    wndclass.lpszClassName = u"HelloWindow"

    if not _user32.RegisterClassW(byref(wndclass)):
        raise WinError()

    # …then create Window

    hello.Create( className=wndclass.lpszClassName,
                  instance=wndclass.hInstance,
                  windowName=u"Hello World")

    # Show Window
    hello.Show(win32con.SW_SHOWNORMAL)
    hello.Update()

    pump_messages()

RunHello()

Back to Top

Venster

Venster was a very promising wrapper over the Win32 API, borrowing heavily from WTL and ATL windowing techniques. Unfortunately, the project hasn't been updated in several years, and doesn't support the latest versions of Python (especially after ctypes.com was dropped).

Pros

  • Rational abstraction layer on top of Win32
  • Use to write native, lightweight (relatively speaking) GUI applications
  • Has most of the cool Win32 tricks like hosting ActiveX and Coolbars

Cons

  • Out of date; not updated in several years

Hello world example (code source):
Venster GUI screen shot

from venster.windows import *
from venster.wtl import *

from venster import gdi

class MyWindow(Window):
    _window_title_ = "Hello World"
    _window_background_ = gdi.GetStockObject(WHITE_BRUSH)
    _window_class_style_ = CS_HREDRAW | CS_VREDRAW

    def OnPaint(self, event):
        ps = PAINTSTRUCT()
        hdc = self.BeginPaint(ps)
        rc = self.GetClientRect()

        msg = "Hello World"
        gdi.TextOut(hdc, rc.width / 2, rc.height / 2, msg, len(msg))

        self.EndPaint(ps)
    msg_handler(WM_PAINT)(OnPaint)

    def OnDestroy(self, event):
        PostQuitMessage(NULL)
    msg_handler(WM_DESTROY)(OnDestroy)

myWindow = MyWindow()
application = Application()
application.Run()

Back to Top

PyGTK

PyGTK seems to have a lot going for it as a cross-platform toolkit. It's also licensed under the LGPL, which I like a lot more than the GPL of PyQT. Unfortunately, it doesn't use native Windows widgets; it does a pretty good job of faking it, but it stands out like a Win32, .NET, or wxPython app wouldn't.

Pros

  • Cross platform
  • Lots of widgets
  • Voluminous (if somewhat disorganized) documenation

Cons

  • Native Win32 widgets not used (looks good, but not quite all the way there)
  • Must include large runtime when packaging with py2exe

Hello world example (code source):
PyGTK screen shot

import pygtk
pygtk.require('2.0')
import gtk

class HelloWorld:

    def hello(self, widget, data=None):
        print "Hello World"

    def delete_event(self, widget, event, data=None):
        print "delete event occurred"
        return False

    def destroy(self, widget, data=None):
        print "destroy signal occurred"
        gtk.main_quit()

    def __init__(self):
        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)

        self.window.connect("delete_event", self.delete_event)
        self.window.connect("destroy", self.destroy)
        self.window.set_border_width(10)

        self.button = gtk.Button("Hello World")
        self.button.connect("clicked", self.hello, None)
        self.button.connect_object("clicked", gtk.Widget.destroy, self.window)

        self.window.add(self.button)

        self.button.show()
        self.window.show()

    def main(self):
        gtk.main()

if __name__ == "__main__":
    hello = HelloWorld()
    hello.main()

Back to Top

55 comments to Python GUI programming platforms for Windows

  • Ibn Saeed

    Hello

    I was just scrolling through the introduction of wxPython in Action by Manning Publications.

    Here is what it says on page 19

    Why choose wxPython?
    The most powerful benefit of wxPython depends on your needs and expertise.
    While we think that all user interface (UI) programmers would benefit from using
    wxPython, the specific features that are most helpful will vary from case to case.

    1.6.1 Python programmers
    If you are already a Python programmer, you’ve probably noticed that Tkinter,
    the interface toolkit distributed with Python, has some problems:

    ■ Tkinter is based on the Tk toolkit, which is somewhat out-of-date in terms
    of the kinds of widgets it supports. By default, it doesn’t support more com-
    plex widgets such as tree controls or tabbed windows. It also doesn’t have a
    particularly rich set of predefined dialogs.

    ■ The Tk toolkit does not use native widget support, resulting in an applica-
    tion that looks foreign on all platforms. In wxPython, dialogs and widgets
    will look like those that are standard on the underlying operating system.
    Your Tk user will find that buttons, fonts, and menus all look slightly differ-
    ent from what might be expected.

    ■ Many programmers find Tkinter itself somewhat clunky to work with. In
    particular, the process by which events are translated to actions in wxPy-
    thon is more flexible and powerful.

    You’ll find that wxPython solves these problems. The toolkit in wxPython is vastly
    more complete and extensive than that of Tkinter and the native widget support
    means your application will look at home in your operating system. Additionally,
    the Python language support is more fluid in wxPython, making for a somewhat
    nicer programming experience.

  • Ibn Saeed

    Hello

    @Ryan, I am finally come up with wxPython and PyQt.

    I will start with the one which would be faster to code and design GUI apps.

    What would you say, which is more verbose, wxPython or PyQt and which is easier to build GUIs compared to the other.

  • Shteve

    @pyglet hello world, I used the tutorial, great btw thx. I found the the window wont close properly unless after the while not loop win.close() follows. This is at least my case just thought you might like to know :P

Leave a Reply

 

 

 

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>