Developing with IronPython & Windows Forms

A Dynamic Language on .NET

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:

I've written a tutorial on IronPython and Windows Forms. My blog is the Voidspace Techie Blog.

I'm particularly grateful to IronPython. Because of it I'm now able to earn a living programming with Python, for Resolver Systems.

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

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

Talk Aims

Along with this we will discuss (briefly) why you might (or might not) want to use IronPython, we'll also encounter and explain some .NET terminology along the way. The talk will be illustrated with code snippets from the The Multi-Tabbed Image Viewer, which of course will be shown in action.

Python

Python Rocks!

Python

Python is an Open Source, Object-Oriented, cross-platform, dynamically typed, interpreted programming language.

Python is used for a wide variety of purposes: games (Eve Online, Civilization IV), science (particularly bioinformatics and genomics), industry (Seagate), GIS, film industry, desktop applications, system administration, web development. It is used by companies like Google, NASA, Yahoo and Industrial Light and Magic.

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

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.

The Visual Studio support includes the designer and debugger. It requires the full version of Visual Studio, plus the SDK.

IronPython and .NET

IronPython on .NET

The IronPython engine is actually an IronPython compiler. It compiles Python code into .NET bytecode (IL) in memory. The compiled Python assemblies can be saved to disk (making binary only distributions possible). (I'll explain what an assembly is in a few moments.)

Because of the dynamic nature of Python they retain a dependency on the IronPython dlls.

IronPython has seamless integration with the .NET framework. Types can be passed back and forth from native .NET code with no conversion, making it very simple to use .NET classes from IronPython. This means that extending IronPython with C# (including accessing unmanaged code and the win32 APIs) is much easier than extending CPython with C.

An exception to the seamless integration is that .NET 'attributes' are inacessible from IronPython. Additionally you can't consume IronPython assemblies from C#. In order to support the Python feature that you can dynamically swap the __class__ attribute on instances at runtime (and for memory efficiency) IronPython re-uses a single class under the hood. This means that classes created in IronPython can't be used directly by other .NET languages.

Setting Up IronPython

  1. Install .NET 2.0 redistributable (Horrible Microsoft URL)

Microsoft seem to be pushing out the .NET framework through Windows Update now (possibly as an optional update?). So many people are finding they don't need to do this. Good news for distributing IronPython applications.

  1. Download IronPython Surprised (http://www.codeplex.com/IronPython)

You can download the pre-built binary distribution, or the source code. IronPython is released under a sensible open source license, similar to the BSD License. (You can make derivative works for commercial purposes.)

  1. Setup the environment variable IRONPYTHONPATH pointing to the Python 2.4 standard library.

This means you need Python 2.4 installed. The default location of the Python standard library will be C:\Python24\Lib. You can just copy the Python standard library to another folder and add that location to IRONPYTHONPATH (or manually to sys.path) and you can distribute the standard library with your applications.

The Interactive Interpreter

The IronPython interactive interpreter is ipy.exe, which is also used to run scripts.

This is the equivalent of python.exe.

There is also ipyw.exe which is the equivalent of pythonw.exe and runs scripts without a console box.

To switch on tab completion (very useful) and colour highlighting, run ipy.exe with the arguments:

ipy -D -X:TabCompletion -X:ColorfulConsole
The IronPython Console

Using .NET: Assemblies

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

import System.Windows.Forms

Note

Namespaces

Usually, but not always, the namespace within an assembly will have the same name as the assembly.

You can also load an assembly with a 'strong name', specifying the exact version you want:

ref = "System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
clr.AddReferenceByName(ref)

Alternatively you can load an assembly object from a specific path, and then add a reference to that assembly object.

from System.Reflection import Assembly
assembly = Assembly.LoadFile(assemblyPath)
clr.AddReference(assembly)

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.

The random module is from the Python 2.4 standard library.

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.

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

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

The Z-order is named because it is the 'Z-axis'. Controls you add first are higher in the Z-order and so if they overlap you will see the one you added first. This is more of a problem if you layout your GUI using absolute positions.

You can use methods on the ControlCollection (the collections property) to reorder the controls, or call the BringToFront method on a control.

Event Handlers

def onClick(sender, eventArgs):
   sender.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)
Our Example Form with Button

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.

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.

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.

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)

Context Menu

IronPython: Context Menu

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.

The System Message Box

try:
    return Bitmap(fileName)
except ArgumentException:
    MessageBox.Show(fileName + " doesn't appear to be a valid image file",
                    "Invalid image format",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Exclamation)
The System Message Box

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.

Further Topics

More things we could have shown you or talked about...

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. Resolver started developing with IronPython around October 2005. At that point it was almost a daily occurrence to discover bugs in IronPython, create a bugtest to tell us when it was fixed and then implement a workaround.

    Now this happens very rarely. We recently discovered a bug in __call__ and keyword arguments, I don't remember the last time we discovered a bug prior to that.

  4. There isn't an awful lot of difference between being dependent on Python and being dependent on .NET though. Also see the note that goes with Setting Up IronPython.

  5. Mono is good, but still incomplete. .NET is a predominantly Windows platform.

  6. Which is a reason to use another .NET language. We've had to do this in Resolver to use unmanaged code from IronPython. 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?