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 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: \P ython24 \L ib' ) 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):

import sys

from PyQt4 import QtGui PyQt4QtGui 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):



from pyglet import font

from pyglet import window pygletfontpygletwindow 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?):



from ctypes import *

import win32con ctypeswin32con 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):



from venster. windows import *

from venster. wtl import * venster.venster. 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):



import pygtk

pygtk. require ( '2.0' )

import gtk pygtkpygtk.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