Developing with IronPython & Windows Forms

A PyCon 2007 Talk

IronPython: The metal slitherer

Introduction

I have been programming with Python for nearly four years. I've written several articles on Python, and a couple of them are even worth reading. Smile

I am also the author of the following Python projects:

My blog is the Voidspace Techie Blog.

I'm also writing a book, IronPython in Action for Manning publications. Expect to see it out early autumn time.

I'm an agile software developer. I appreciate the beauty and elegance of Python and Ruby, two great dynamic languages. I started programming in Ruby (on Rails) in 2004. Working with IronPython, for Resolver, was my first practical experience of working with Python. Before switching to Ruby/Python I was a Java developer.

Resolver Systems is a small company (6 developers) based in London. Resolver Systems was created to develop a new business application. This is about to go out for a private beta test with our first customers and we should be going public with the details soon.

Resolver the company started in late 2005. Andrzej joined Resolver in March 2006, Michael joined in April 2006.

Resolver is a desktop application for businesses. .NET was initially chosen as the development platform, but a scripting language was needed as an integral part of the application. After discovering and trying IronPython (whilst it was still Beta) the two developers, as they then were, decided to write the whole application with IronPython.

The Resolver codebase currently stands at over twenty thousand lines of production code and around seventy thousand lines of test code. About 1% of the production code is in C# and the rest IronPython.

Resolver integrates Python in a very interesting way...

What is IronPython?

IronPython was started by Jim Hugunin, who now heads a Microsoft team running the development.

IronPython is a very faithful implementation of Python 2.4. All the core language features are there.

Visual Studio Designer and IronPython

In fact since version 1.2.3, the IronPython Community Edition (Fepy, maintained by the incredible Seo Sanghyeon) is now included in Mono.

Mono has partial support for Windows Forms. I think most/all of Windows Forms 1 is there and some of 2. The 1.2.3 release included a lot of fixes and additions to the Windows Forms 2.0 support.

Using .NET: Assemblies

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

import System.Windows.Forms

A Simple Application

We'll now create a trivial Windows Forms application at the interactive interpreter, explaining the steps as we go.

We will create a form, with a title and a button on it that does something when it is pressed.

See the Form on MSDN, and the Form Members leading to the Text Property.

The C# example of this on Text property docs says: public override string Text { get; set; }. Meaning that it is a public property, that can be both fetched and set, and it takes (or returns) a string. (And that on the form, the Text property overrides the one inherited from Control.)

Configuring .NET Classes

import clr
clr.AddReference('System.Windows.Forms')
from System.Windows.Forms import Application, Button, Form

import random       # Used later in this example

form = Form()
form.Text = 'Hello World'

We import all the names we will use in the application here. Instantiate the form, and set the title.

To import the random module from the Python 2.4 standard library, set the path in the IRONPYTHONPATH environment variable.

Windows Forms controls are configured with properties. The controls (widgets) inherit from the Control class, so they all have a lot of properties in common.

Adding Controls

button = Button(Text='Click Me')
form.Controls.Add(button)

Note the alternative way of setting the Text, through a keyword argument in the constructor. This can't be done with C# which doesn't have keyword arguments.

Specifically a ControlCollection. They are enumerable and indexable and you can use len and in on it (to get the number of controls and check for membership respectively).

Instead of using Controls.Add we could have done button.Parent = form, which would have added the button to the form's controls collection for us.

The ControlCollection class has useful methods like Remove(control) and RemoveAt(index).

Event Handlers

button = Button(Text="Click Me")

def onClick(sender, eventArgs):
   button.Text = str(random.random())

button.Click += onClick

onClick is an event handler function (a delegate in C#). We add it to the event using 'add in place'. Note that we can remove it using '-='. It is IronPython 'under the hood' which converts our function into a delegate.

The arguments that Windows Forms event handlers receive are sender and eventArgs. The sender is the control on which the event happened, eventArgs will be an instance of one of many different EventArgs classes, and may contain useful information about the event (state of the mouse buttons, location of the mouse etc). You may also be able to cancel the event by setting Handled or Cancel on the instance.

Application.Run(form)

Subclassing Controls

class MainForm(Form):
    def __init__(self):
        Form.__init__(self)
        self.Text = "Hello World"
        self.Icon = Icon(Path.Combine(IMAGEPATH, "pictures.ico"))
        self.Width = 350
        self.Height = 200

mf = MainForm()
Application.Run(mf)

Note

There is an example in 'Tabbed Images' of sub-classing a Panel so that we can call a protected method on it to make it 'selectable' (so it can be scrolled with the mouse wheel).

The Icon class is from System.Drawing. Path is from System.IO.

The Multi-Tabbed Image Viewer

A small application to illustrate various Windows Forms controls, including a custom executable that embeds the IronPython engine. It is about 18k or so of Python code, and is all presentation layer (very little logic) !

Screenshot

The multi-tabbed image viewer. Smile

510 lines of Python code, 52 lines of C# code.

The Multi Tabbed Image Viewer

The C# is in the custom executable, and also in a class which provides access to win32 clipboard functionality.

Open File Dialog

filter = "Images (*.BMP;*.JPG;*.GIF)|*.BMP;*.JPG;*.GIF|All files (*.*)|*.*"
def onOpen(self, _, __):
    openFileDialog = OpenFileDialog(
        Filter = filter,
        Multiselect = True
    )
    if openFileDialog.ShowDialog() == DialogResult.OK:
        for fileName in openFileDialog.FileNames:
            ...

This method is an event handler. The two unused arguments in the method signature are sender and event.

Initialising with a TabControl

def initTabControl(self):
    self.tabControl = TabControl(
        Dock = DockStyle.Fill,
        Alignment = TabAlignment.Bottom
    )
    self.Controls.Add(self.tabControl)

Here we use keyword arguments in the constructor to set the properties.

We initialise the TabControl in its own method.

self is a sub-class of Form; hence self.Controls.Add(self.tabControl).

It tells the parent control how to layout this control. DockStyle is an enumeration (a .NET type) and DockStyle.Fill means fill all the available space. Alternatives are Top, Bottom, Left, Right and None (the default).

An alternative way to layout controls is to use the Anchor property, which can anchor controls to the edges of their parent so the move in a predictable way when resizing.

If you have several controls which use DockStyle.Top then they will be stacked horizontally - for some reason in the opposite order to which they are added: the last one going to the top of the parent, the one before last underneath the last one and so on. Can be very useful. In our application we first add the Tabcontrol (DockStyle.Fill) followed by the ToolStrip (DockStyle.Top - this appears in the middle), last of all the MenuStrip (DockStyle.Top) which appears at the top.

Creating New TabPages

def createTab(self, image, label):
    tabPage = TabPage()
    tabPage.AutoScroll = True   # Create scrollbars if needed
    tabPage.Text = label          # The tab label
    tabPage.Dock = DockStyle.Fill

    tabPage.Controls.Add(self.getPictureBox(image))

    self.tabControl.TabPages.Add(tabPage)

    self.tabControl.SelectedTab = tabPage

All the names here are from the System.Windows.Forms namespace.

The Menu

menuStrip = MenuStrip(Name="Main MenuStrip", Dock=DockStyle.Top)

fileMenu  = ToolStripMenuItem(Name="File Menu", Text='&File')

openMenuItem  = ToolStripMenuItem(Name="Open", Text='&Open...')
openMenuItem.Click += self.onOpen
openMenuItem.ShortcutKeys = Keys.Control | Keys.O

fileMenu.DropDownItems.Add(openMenuItem)

menuStrip.Items.Add(fileMenu)
self.Controls.Add(menuStrip)

The Toolbar

toolBar = ToolStrip(Dock = DockStyle.Top)

button = ToolStripButton()
button.Image = Bitmap(fileName)
button.ImageTransparentColor = Color.Magenta
button.ToolTipText = button.Name = name
button.DisplayStyle = ToolStripItemDisplayStyle.Image
button.Click += clickHandler        # click handler method or function

toolBar.Items.Add(button)

There is a class called ToolBar. This is an ugly abomination from Windows Forms 1, with an insane API. Use ToolStrip instead.

Creating images (for the picture boxes or for icons) is very easy. You can instantiate the Bitmap class with a filename, or use a static method on the Image class: Image.FromFile.

Custom Executable in C#

static void Main(string[] rawArgs) {
    List<string> args = new List<string>(rawArgs);
    args.Insert(0, Application.ExecutablePath);

    PythonEngine engine = new PythonEngine();
    engine.AddToPath(Path.GetDirectoryName(Application.ExecutablePath));
    engine.Sys.argv = List.Make(args);

    { ... }

    engine.ExecuteFile("main.py");

}

Screenshot on Mono

The multi-tabbed image viewer on Mono 1.2.3 on Windows.

The Multi Tabbed Image Viewer on Mono

Works ok, but not fully, on Mono. Mono treats the filesystem in a case sensitive way (which windows generally doesn't) and the GUI doesn't refresh when you delete a tab. The about dialog is also a bit scrambled.

The Mono open file dialog also appears to have a bug (on windows at least): if you try to open a directory with a space in the path it returns this as the filename rather than opening the directory.

Why Use IronPython?

If you don't need .NET you don't want IronPython. (Probably!) Smile IronPython is at its best for .NET programmers.

For .NET programmers, Python is nicer than C#. It is also a ready made scripting language for embedding in applications.

The .NET runtime (the CLR) has a highly optimised JIT compiler, and has seen a lot of work to ensure that multi-threaded applications can take advantage of multi-core processors. This is something that CPython can't do (for the foreseeable future) because of the Global Interpreter Lock.

AppDomains allow you to run code or applications within their own 'domain', for which you can specify the security settings (like restricting access to the filesystem or network). Again, not currently possible with CPython.

Third party components includes a huge range of sophisticated GUI components. Due to the Windows culture you usually have to pay for them! However we use a couple of big components in Resolver, and there just aren't equivalents available for CPython..

Why Not Use IronPython?

  1. IronPython runs the PyStone benchmark about thirty percent faster than CPython. However, according to the Computer Language Shootout (a better benchmark - but there is no such thing as a perfect benchmark) IronPython is generally a bit slower than CPython.

    We have however seen interesting, and unexplained, speedups from time to time. IronPython does take advantage of the JIT compiler which is the likely cause of this.

    Compiling Python code to assemblies is an intensive task, which can make startup times long for large programs. You can now pre-compile to assemblies to speed this up.

  2. IronPython has no C-API. There are alternative implementations (and wrappers) of many C extensions from the standard library, but not for all of them.

  3. There isn't an awful lot of difference between being dependent on Python and being dependent on .NET though. Mono is good, but still incomplete. .NET is a predominantly Windows platform.

  4. Which is a reason to use another .NET language. We've had to do this in Resolver to use unmanaged code from IronPython.

  5. Similarly you can't create IronPython classes which can be consumed from another .NET language.

It may be possible to solve both these problems using the System.Reflection API to dynamically emit .NET classes, but I haven't yet found time to experiment with this.

Questions

Any questions?