PyZine
 


Article Finder
People
Issue 7 - Revision 6  /   February 27, 2005 


 
  Py Links:
Latest Issue
Issue 08
Issue 07
Issue 06
Issue 05
Issue 04
Issue 02
Issue 01
 
 
Downloads
     
  Articles:
Throughout the quarter we cover topics of interest to Python developers.

COM & Python

Python on .NET

Python at Both Ends of the Web

GUI Testing Approach

Simulating with SimPy

Docutils

Mobile Collection System

 
 
 
     


Python on .NET

- - - - - - - - - - - -

By Brian Quinlan  | October 22, 2004

print
Introduction

Microsoft Corporation’s.NET generally provokes strong reactions from software developers.

 
 
Sidebar - Resources
IronPython http://ironpython.com/
Python for .NET Click to Download
.Net Framework: http://msdn.microsoft.com/netframework/
 

Some developers shun .NET, discounting it as a poor reimplementation of Java or just another way for Microsoft to lock-in developers to its platform. Other programmers embrace .NET as a vast improvement — a way to (finally) drag Windows developers away from C++ and the Microsoft Foundation Classes to a higher-level development model. And yet other coders just scratch their heads, confused about the scope, features, and benefits of .NET. Indeed, thanks to Microsoft marketing efforts, “.NET” is the most overloaded “brand” in the entire computer industry.

But apathy or confusion shouldn’t cause Python developers to disregard .NET. In fact, in the true spirit of Python, .NET allows Python developers to integrate Python with a great deal of valuable Windows software, including key technologies such as XML, SOAP, ADO, and the Windows graphical user interface (GUI). While writing extensive Windows application with Python and .NET is still in the realm of science fiction, recent, important advances have made simple applications quite doable.

Let’s take a look at .NET, specifically the .NET Framework, and see how to write Python applications with it.

The .NET Framework

The .NET Framework (often referred to as “.NET”, albeit confusingly) is Microsoft’s implementation and extension of the ECMA-335: Common Language Infrastructure (CLI) standard. According to ECMA-335, “the CLI allows applications written in multiple high level languages to be executed in different system environments without the need to rewrite the application to take into consideration the unique characteristics of those environments.”

The .NET Framework consists of two main components: the Common Language Runtime (CLR) and the .NET Framework class library.

The Common Language Runtime is the infrastructure that allows applications to access the .NET Framework class library. Similar to Java’s or Python’s virtual machine, the CLR provides common data types (integers, floating-point numbers, strings, and so on), and a virtual machine that executes Intermediate Language (IL) assembly language. IL is similar to Python byte code, but as with the rest of .NET, was designed to be programming language independent.

For example, because of IL programmers can now mix Visual Basic, Visual C++, and a new (Java-like) language called C# (pronounced “C-Sharp”).

The .NET Framework class library (usually just called the “.NET Framework”) provides a wide variety of features in an object-oriented programming library.

Like Python and Java, the .NET Framework organizes code into related, hierarchical groups called “namespaces.” All of the classes in the .NET class library are in one of two top-level namespaces: System or Microsoft.

The System namespace contains the most features and is (largely) platform independent. It draws graphics and creates user interfaces, and contains code that defines and manipulates data structures, event handlers, exceptions, and more.

For instance, System.Collection contains classes that can be used to store other objects. Some of these classes have direct Python equivalents, such as ArrayList (list), Queue, and Hashtable (dictionary). Others, like Systems’ SortedList (really an ordered dictionary), are unique. The System.Data namespace contains classes to manipulate data from OLE databases and from XML files, and System.Diagnostics namespace provides classes for logging, debugging, creating and querying performance counters and for retrieving information on processes.

The Microsoft namespace contains classes related to Microsoft-specific technologies, such as the Windows registry, Windows-specific events, and application scriptability.

Let's Get Coding

Let’s take a break from theory and start coding. To follow along you must first install the .NET Framework and some tools for accessing it from Python.

The .NET Framework is available for Windows, Linux, and Mac OS X. Microsoft distributes the most complete .NET implementation for Windows. Microsoft’s code is available either through Windows update or at http://msdn.microsoft.com/netframework/downloads/framework1_1/. Meanwhile, the Mono project provides a .NET implementation for Linux, Mac OS X, and Windows. It can be downloaded from http://www.mono-project.com/downloads/.

Once you’ve installed a version of the .NET Framework on your computer, you’ll still need some way of accessing the .NET Framework class libraries from Python. There are currently two tools for mixing .NET and Python: Python for .NET and IronPython.

Python for .NET is implemented as a Python extension module that allows CPython scripts to access the .NET Framework. It’s available from http://www.zope.org/Members/Brian/PythonNet/index_html.

The advantage of Python for .NET is that it allows you to continue to use existing Python libraries and extensions while simultaneously taking advantage of the .NET Framework class libraries. Its disadvantage is that is that there is no convenient way to make services available to CLR applications. For example, writing a class that can be directly instantiated by other CLR languages is not possible.

IronPython is written in C# and compiles Python source code to IL assembly, just as Jython is written in Java and compiles Python source code to Java bytecode. (The similarity of the two approaches is not surprising considering that Jim Hugunin is the author of both.) In addition to the ability to compile standalone applications, IronPython also provides a shell for interactive use. IronPython can be found at http://ironpython.com.

IronPython’s primary advantages and disadvantages are the opposite of Python for .NET’s: existing Python C extensions are not available from IronPython. In fact, since most Python modules are layered upon C extensions at some level, and only the math have sys modules have been reimplemented in C#, very few existing Python modules can be used.

However, the advantages of this approach is that it should eventually be possible to make Python classes available to other CLR languages, and IronPython scripts should be executable without the need to install CPython. Another interesting aspect of IronPython is that it has already been demonstrated to run nearly as fast as CPython, and Jim Hugunin suspects that IronPython can be made faster than CPython without a Herculean effort.

At First Glance

Keeping with tradition, the first example, which immediately follows, displays a form containing the text “Hello World”. It can be run using either Python for .NET or IronPython.


import sys
# IronPython and Python for .NET use different import syntaxes.
# We'll check to see if sys.platform contains 'clr' (meaning
# that we are running on the Common Language Runtime
# and therefore using IronPython) and adjust our import
# statements based on that.
if sys.platform.find('clr') != -1:
   # Running on IronPython
   from System.Windows.Forms import Form, Label, DockStyle
   from System.Drawing import ContentAlignment
else:
   # Running on Python for .NET
   from CLR.System.Windows.Forms import Form, Label, DockStyle
   from CLR.System.Drawing import ContentAlignment

# Create a new window (or form in Microsoft's venacular)
form = Form()
form.Text="Hello from .NET" # Sets the title of the form

# Create a new label (a control type that simply contains
# text that cannot be edited).
hello_label = Label()
hello_label.Text="Hello World"
# The label should fill the entire form
hello_label.Dock = DockStyle.Fill
# The text in the label should be centered in the label's area
hello_label.TextAlign = ContentAlignment.MiddleCenter
# Add the label to the form
form.Controls.Add(hello_label)

# Show the form/window as a modal dialog box.
# This method will cause all of the required events e.g.
# window resizing, closing, etc. to be handled
form.ShowDialog()


One important feature demonstrated in this example is the “Dock” property. The “Dock” property is common to all controls in the .NET Framework class library and, when combined with the “Anchor” property, is used to position controls insides window.

In this example the “Dock” property is set to cause the contents of the control to fill the entire window. Hwever, it can also be used to position a status bar at the bottom of a window or to keep a button centered.

By using various settings of the “Dock” and “Anchor” properties, complex control layouts can be achieved. For instance, a list box can expand vertically when a window is resized, but otherwise maintains the same top and horizontal positions.

Delegates and Events

If you consider the previous example, it’s missing something common to nearly all GUI applications: it doesn’t respond to user input.

In the .NET Framework class library, when the user takes an action, such as clicking the mouse or typing on the keyboard, a method or function is called to respond to it.

This approach is somewhat unique among statically-typed object-oriented environments and is possible because, unlike other similar systems, such as C++ and Java, the .NET Framework allows methods to be passed and used between objects. Methods used in this fashion are wrapped in type-safe objects called delegates.

One nice aspect of writing in Python is that both IronPython and Python for .NET automatically create delegates from functions and methods for you, so no explicit wrapping is required.

Since delegates are actual objects, they can be subclassed to offer more sophisticated behavior. The most commonly used delegate subclass is called MulticastDelegate. This class allows many functions or methods to be triggered by a single event. Multicast delegates make it easy to implement powerful observer-style patterns. In a GUI application, for example, many different components may be interested in knowing when the application is shutting down so windows can be closed, network resources freed, and files saved.

In the .NET Framework, when delegates are used to handle events, they are always passed two arguments.

  • The first argument contains the object that triggered the event e.g. the window that was resized, the menu item that was selected, the button that was clicked.
  • The second argument is an EventArgs instance or an instance of one of its subclasses. The EventArgs class itself doesn’t contain any interesting properties or methods but its subclasses contain information about the event that occurred. For example, the KeyPressEventArg subclass has a property “KeyChar” that indicates the key that was pressed.

In the following example, two delegate functions are added (using the “+=” or in-place addition syntax) to monitor changes in the value of a track bar control. One function modifies the transparency of a window, while the other displays the value of the track bar in the status bar at the bottom of the window. A screen capture of the application, obscuring a Notepad window, appears below.

import sys
if sys.platform.find('clr') != -1:
   # IronPython import syntax
   from System.Windows.Forms import \
         Form, TrackBar, StatusBar, DockStyle
else:
      # Python for .NET import syntax
      from CLR.System.Windows.Forms import \
         Form, TrackBar, StatusBar, DockStyle
form = Form()
form.Text="Delegate demo"

# Create a track bar control – a control that similar to a
# scroll bar that allows the user to pick a value in a range.
# The range offered to the user in this example is from
# 20 to 100 with the initial value being 100.
tracker = TrackBar()
# dock the track bar at the bottom of the window
tracker.Dock = DockStyle.Bottom
tracker.Minimum = 20
tracker.Maximum = tracker.Value = 100
form.Controls.Add(tracker)

# Create a status bar – a control normally found at the
# bottom of a window that displays help or status
# information. The "Dock" property of status bars is
# to DockStyle.Bottom by default. If two controls have
# conflicting "Dock" settings then the control that is
# added to the form LAST is accommodated first e.g.
# the statusbar will be shown at the bottom of the
# form with tracker immediately above it.
statusbar = StatusBar()
form.Controls.Add(statusbar)

def update_opacity(sender, event):
   # Update the transparency of the form based on
   # the tracker value.
   form.Opacity = tracker.Value / 100.0

def update_statusbar(sender, event):
   # Set the text of the status bar to the value
   # of tracker
   statusbar.Text = str(tracker.Value) + '%'

# Install the proceeding two functions as delegates to
# observe the changing tracker value.
tracker.ValueChanged += update_opacity
tracker.ValueChanged += update_statusbar

form.ShowDialog()


A Bigger example

The final example ties in a lot of the ideas presented above and adds a few extra twists.

This example application shows an empty window area with a tracker control and a status bar at the bottom. When the empty portion of window is left-clicked, a point is drawn at that location. When two or more points have been created, they are joined by a curved line. The amount that the line curves is controlled by the tracker control.

In addition, if the window is right-clicked, a contextual menu appears giving you the ability to clear the curve drawing or save it as a GIF image. As part of the save progress, you’re shown a standard save dialog box prompting for the name of the file.

When the application window is closed, you’re prompted to save your image unless you have already done so and not made any changes since then.

The application demonstrates handling several different event types, creating a contextual menu, and creating a Panel control to draw in.

This example can only be run using Python for .NET because IronPython does not yet support several required delegate types.

from CLR.System.Windows.Forms import Form, StatusBar, MouseButtons, TrackBar,\
               DockStyle, Panel, ContextMenu, MenuItem,\
               SaveFileDialog, DialogResult, \
               MessageBoxButtons, MessageBoxIcon, \
               MessageBox

from CLR.System.Drawing import Graphics, Image, Bitmap, Brushes, Color, \
               Pens, Point, Size
from CLR.System.Drawing.Drawing2D import SmoothingMode
from CLR.System.Drawing.Imaging import ImageFormat
from CLR.System.Collections import ArrayList
from CLR.System import EventHandler

# Use the .NET Framework's ArrayList class to store a list of points
points = ArrayList()

# Indicates if points have been added since the last time the image
# was saved
needs_saving = False

def clear_points(sender=None, event=None):
   """Remove all of the points from the curve"""
   global needs_saving

   points.Clear()
   needs_saving = False # no need to save an empty image
   form.Refresh() # force the form to redraw itself

def draw_curve(graphics):
   """Draw the curve using the specified Graphics object"""

   # The System.Drawing.Graphics class offers many methods to
   # draw graphics e.g. lines, curves, polygons, ovals, etc.
   # Graphics objects are associated with forms and other
   # objects that can be drawn into e.g. image files,
   # printers.

   # The .NET Framework makes antialiased graphics easy
   graphics.SmoothingMode = SmoothingMode.AntiAlias

   if points.Count > 1: # need at least two points to draw a curve
      graphics.DrawCurve(
         Pens.Black,
         # Must pass in list of points that are part of curve as an
         # array.
         points.ToArray(points[0].GetType()),
         # 0 - straight line, 1 - quite curvy line, 5 - very wierd line
         tension_tracker.Value / 100.0)

   for point in points: # draw a circle at every clicked point
      graphics.DrawEllipse(Pens.Blue, point.X - 3, point.Y - 3, 6, 6)

def save_curve(sender=None, event=None):
   "Prompts the user to save the curve as a GIF image"

   global needs_saving

   # Create a file save dialog box
   save_dialog = SaveFileDialog()

   # This icky syntax is used to specify the types of files that
   # should be shown in the save dialog box.
   save_dialog.Filter = "GIF (*.gif)|*.gif|All files (*.*)|*.*"
   # Warn the user if they attempt to save over an existing file.
   save_dialog.OverwritePrompt = True
   # Set a default file name
   save_dialog.FileName = "curve.gif"

   # Show the save dialog box
   if save_dialog.ShowDialog() == DialogResult.OK:
      # If the user selects "OK" then create a Bitmap, draw the curve in
      # it and save it to a file

      # Create a new Bitmap object to draw the curve in
      image = Bitmap(curve_panel.Width, curve_panel.Height)

      # Create a Graphics object to draw into
      graphics = Graphics.FromImage(image)
      # Draw a white backdrop for the GIF image
      graphics.FillRectangle(
      Brushes.White, 0, 0, image.Width, image.Height)
      draw_curve(graphics)
      image.Save(save_dialog.FileName, ImageFormat.Gif)

      # The save was successful so the curve no longer needs saving
      needs_saving = False

# Create the form that we will be drawing into
form = Form()
form.Text = "Curve drawing"

# Create a new Panel control. Panel controls can be used to
# draw into or to organize other controls. In this case we
# will draw into it.
curve_panel = Panel()
curve_panel.BackColor = Color.White
curve_panel.Dock = DockStyle.Fill
form.Controls.Add(curve_panel)

# Create a context menu to be displayed when the user right-clicks in
# curve_panel
context_menu = ContextMenu()
# When passed as a argument to a function, delegates must be explicitly
# created from functions or methods.
context_menu.MenuItems.Add(MenuItem("Clear", EventHandler(clear_points)))
context_menu.MenuItems.Add(MenuItem("Save", EventHandler(save_curve)))

# Create a trackbar to control the "tension" between
# the points. The greater the tension the more cury
# the curve.
tension_tracker = TrackBar()
tension_tracker.Dock = DockStyle.Bottom
tension_tracker.Minimum = 0
tension_tracker.TickFrequency = 50
tension_tracker.Maximum = 500
tension_tracker.Value = 100
form.Controls.Add(tension_tracker)

bar = StatusBar()
form.Controls.Add(bar)

def tracker_changed(sender, event):
      # Called when the value of tension_tracker changes
      bar.Text = 'Tension: %.2f' % (tension_tracker.Value / 100.0)
      form.Refresh()
tension_tracker.ValueChanged += tracker_changed

def mouse_click(sender, event):
      global needs_saving

      # If the left mouse button was pressed, a point to
      # the curve and force the form to redraw. If the
      # right mouse button was clicked, display a
      # context menu at that point.

      if event.Button == MouseButtons.Left:
         needs_saving = True
         points.Add(Point(event.X, event.Y))
         form.Refresh()
      elif event.Button == MouseButtons.Right:
         context_menu.Show(curve_panel, Point(event.X, event.Y))
curve_panel.MouseDown += mouse_click

def paint(sender, paint_event):
   draw_curve(paint_event.Graphics)
curve_panel.Paint += paint

def closing(sender, cancel_event):
   """Display a save dialog box when the form is closing if
   the curve hasn't been saved.

   cancel_event is a CancelEventArgs object, which allows
   a delegate to cancel an operation. The user will be
   prompted and, if they choose, the closing of the
   form will be aborted."""

   global needs_saving

   if needs_saving:
     # Ask the user if they wish to save the curve
     result = MessageBox.Show(
       "Do you want to save your changes?",
       "Save",
       MessageBoxButtons.YesNoCancel,
       MessageBoxIcon.Question)
     if result == DialogResult.Yes:
       save_curve()
         if needs_saving:
         # The form still requires saving so the user
         # must have canceled from the save dialog box.
         # So set the CancelEventArgs object's Cancel
         # property to true
         cancel_event.Cancel = True
     elif result == DialogResult.Cancel:
       # The user canceled so set the CancelEventArgs
       # object's Cancel property to true
       cancel_event.Cancel = True
form.Closing += closing

form.ShowDialog()


Conclusion

Before you set out to build your own .NET applications using Python, keep in mind that the limitations of the current tools preclude writing sophisticated applications.

Instead, the current tools provide a glimpse into the possibilities that the .NET Framework can provide for Python developers: robust cross-language functionality and best-of-breed applications for Microsoft operating systems.

So download Python for .NET and IronPython, read-up on the .NET Framework class libraries, and keep an eye out for future developments.

Sidebar - The state of the world today

Currently, both Python and .NET and IronPython are quite immature. For example, neither can call .NET methods with “out” or “ref” parameters —parameter types that compensate for the CLR’s lack of a tuple-like facility to enable multiple return values. (Fortunately, neither “ref” nor “out” parameters are used extensively in the .NET Framework class library.) In addition, interfaces and abstract classes, defined by other .NET languages, cannot be implemented/subclassed.

There are also semantic differences between Python for .NET and IronPython, which the table below highlights.

IronPythonPython for .NET

>>> from System import *

>>> from CLR.System import Environment

# Note CLR prefix in Python for .NET
# Python for .NET does not support * imports

>>> str(Environment.OSVersion.Version) '5.1.2600.0'

>>> str(Environment.OSVersion.Version)
''

IronPython maps Python str() calls to CLR ToString() calls

>>> Environment.GetLogicalDrives()
System.String[]('C:\\', 'D:\\')

>>> Environment.GetLogicalDrives()
<CLR.System.String[] object at 0x07554470>:

# IronPython provides more information for some objects

>>> 2.0 ** 10000
Infinity

>>> 2.0 ** 10000
Traceback (most recent call last):
File "", line 1, in ?
OverflowError: (34, 'Result too large')

# IronPython uses the CLR for floating-point numbers, which can be different than CPython semantics

Both tools have their strengths and weaknesses: IronPython provides (mostly) superior semantics but Python for .NET is more complete. You might install both and experiment to see which one suits you best. In the future, both are likely to be important tools for the Python programmer.


Brian Quinlan

shim
shim

 Py is committed to bringing you great Python Articles.

shim
shim


Home   Subscribe   Migration FAQ   Contact PyZine   Write for PyZine   ZopeMag   opensourcexperts.com  

Reproduction of material from any of PyZine's pages without prior written permission is strictly prohibited. Copyright 2003 - 2005 PyZine Zope/Plone hosting by Nidelven IT