Understanding the Requirements for an Extension
It’s important to understand that an extension, any extension, probably ties your code to Windows. Whenever you use an extension with IronPython, you rely on something other than the Python libraries to perform a task, which means you lose the platform independence for which Python is so famous. In short, extensions provide considerable flexibility and help you provide additional capabilities for IronPython, but this flexibility isn’t without cost. Every time you make a design decision of this sort, you must pay a price in the following:
- Reduced reliability: Due to increased failure points.
- Weakened security: More languages mean more places where someone could leave a security hole.
- Impaired speed: Marshaling data between language barriers takes time.
- Fewer platforms: In order to use an extension, you must find a platform that supports both IronPython and the extension language.
Writing an extension isn’t always straightforward. It isn’t as simple as writing some class library code and putting it in a DLL. In fact, you must spend considerable effort thinking about how an extension should be designed to make it useable. The following list considers just a few of the most important factors for your extension.
- Python language requirements: IronPython may not support every feature that the static language supports. For example, you may find that IronPython doesn’t support a particular static language operator, such as the ++ operator.
- IronPython developer mentality: An extension that performs tasks in a way that runs completely counter to the way that an IronPython developer normally does them isn’t very useful, because the IronPython developer will have to think too hard about using the extension. The best kind of extension is one that feels natural to the IronPython developer.
- Flexibility: An extension should provide some significant advantage in flexibility. When you write an extension, write it with the benefit to the IronPython developer in mind, not simply because the functionality the extension provides is interesting.
The one factor that you don’t need to consider is whether something is doable. Normally, if you can perform a task with the static language you want to use to build the extension, then you can do it with IronPython as well. Sometimes, you have to massage the data or present the technique in a way that doesn’t match your normal methodology, but you can normally perform the task with a bit of effort.
Considering IronPython and Static Language Differences
IronPython is a dynamic language (a language that does things like decide variable type at run time, which is contrasted with a static language that decides everything during compile time). As such, it has some significant advantages for the human developer that a static language can’t provide. It’s true that the concept of language is foreign to the computer, but the human developer relies on certain characteristics of language to accomplish tasks quickly and with few errors. Consequently, as part of defining the reason to use an extension, you must consider the differences between IronPython and the static language of your choice.
Defining Why You Use a Static Language with IronPython
Typically, you use a static language with IronPython to gain a specific advantage. For example, IronPython doesn’t create graphical user interfaces very well, so using a static language to perform this task could provide a significant advantage in development time. In addition, you could probably reuse code that you already have on hand, which may reduce debugging time as well. Look for the advantages that you can gain when using a static language with IronPython. If you have problems describing the material benefit of an extension, then perhaps you really should look at another solution.
Make sure you consider the strengths of the static language when making your selections. For example, C# is often the best choice for Win32 API interaction because it supports unsafe pointers — a requirement for certain specialized Win32 API tasks. Of course, you should make sure that the use of the Win32 API is actually required. Perhaps a third-party library already has the solution you require and with a lot less work. Visual Basic.NET is often the best choice for database work because it takes care of so many tasks in the background for the developer. You don’t have to worry so much about coercing data types because Visual Basic addresses the need for you in the background.
Sometimes the use of a static language is practical. For example, you might have an overwhelming number of developers on your team who know C# or Visual Basic.NET, but know nothing about IronPython. In general, this is one of the poorest reasons to use a static language with IronPython, but the reality of development today is that you often use the tools you have on hand to accomplish the task. No one can afford to have developers sitting on their hands simply because the dynamic language is the best choice for a particular job.
Understanding Line Noise
There are good reasons to avoid using a static language with IronPython. You can write most code in IronPython using far fewer lines than a static language requires. Fewer lines of code translate into higher developer productivity and sometimes into fewer coding errors as well.
The additional code that a static code developer must write is often referred to as line noise. The code doesn’t substantially translate into useful output, but the static language requires it. For example, IronPython doesn’t require that you declare the type of a variable — you simply leave this task to IronPython.
While the extra code in a static language does tend to reduce the potential for unintended output, it can also make the code harder to read. With every benefit, there’s a corresponding negative. When you decide to use an extension with IronPython, you need to consider when it’s appropriate to work through the extra code and cumbersome features of static languages and when IronPython is truly the better choice.
Let’s look at a quick example. Say you want to create an array of names in a function and pass them back to a caller. Here’s the C# code to perform the task.
[code]
public String[] GetNames()
{
String[] Result = new String[4];
Result[0] = “John”;
Result[1] = “Amy”;
Result[2] = “Jose”;
Result[3] = “Carla”;
return Result;
}
public void ShowNames()
{
String[] TheNames = GetNames();
foreach (String Name in TheNames)
{
Console.WriteLine(Name);
}
}
[/code]
The code in GetNames() creates an array of String, fills it with names, and returns those names to the caller, ShowNames(). At this point, ShowNames() uses a foreach loop to display each name individually. Now take a look at the same functionality written in IronPython.
[code]
def GetNames():
return “John”, “Amy”, “Jose”, “Carla”
def ShowNames():
for Name in GetNames():
print Name
[/code]
The code performs the same task in both cases, but as you can see, the IronPython code is significantly shorter. In addition, the IronPython code is actually easier to read.
Considering Scoping Issues
One of the most important differences between IronPython and static languages such as C# is that IronPython doesn’t have the concept of scope within classes. Everything in an IronPython class is public, so you always have access to every element. Of course, this presents a dilemma for languages that do support scope. When creating an IronPython extension, your static language scope declarations will change as follows:
- Public members remain public.
- Protected members become public.
- Protected Internal members become public.
- Private members remain private and don’t appear at all to IronPython.
- Internal members become private and don’t appear at all to IronPython.
Creating the Simple C# Extension
The example in the following sections provides a simple set of calculations. Think of it as the basic four-function calculator with a bit extra added. The example doesn’t do anything fancy, but it does demonstrate techniques you need to build any C# extension for IronPython. The rest of the examples in this chapter build on this example, so you should at least scan the techniques presented in the sections that follow.
Creating the Project
A C# extension project in Visual Studio is nothing more than the typical class library. The following steps help you create the project for this example. You can use the same steps when working with the other examples — all you need to do is change the project name.
- Choose File ➪ New ➪ Project. You see the New Project dialog box shown in Figure 16-1.
- Choose the Visual C# folder in the Installed Templates list.
- Select .NET Framework 3.5 or an earlier version of the .NET Framework. Don’t select the .NET Framework 4.0 entry. The list of templates changes when you change the .NET Framework version.
- Select the Class Library template.
- Type Calcs in the Name field and click OK. Visual Studio creates a class library project for you.
- Right-click Class1.cs in Solution Explorer and choose Rename from the context menu. Visual Studio makes the filename editable.
- Type Calcs.CS for the new filename and press Enter. Visual Studio displays a dialog box that asks whether you’d like to rename all of the Class1.cs references to match the new filename.
- Click Yes. The project is ready for use.
At the time of this writing, IronPython doesn’t support extensions written using the .NET Framework 4.0. You must create your extensions using the .NET Framework 3.5 or earlier. Otherwise, the extension will simply fail to load and IronPython won’t provide anything in the way of an explanation (at least, nothing useable). If you suspect that you’ve targeted the wrong .NET Framework version, choose Project ➪ ProjectName Properties. Select the Application tab of the Properties window and change the entry in the Target Framework field to .NET Framework 3.5, as shown in Figure 16-2. The IDE may ask permission to modify features in your setup and require that you restart your project to see the effects of the change.
Developing the C# Extension
The C# extension does have a few tricks to it, but generally speaking, if you know how to create a class library, you already know how to create the code for a C# extension. Listing 16-1 shows the code for the example extension.
Listin g 16-1: A simple calculations extension
[code]
public class Calcs
{
private Int32 Data;
public Calcs(Int32 Value)
{
this.Data = Value;
}
public override string ToString()
{
return Data.ToString();
}
public static Calcs operator +(Calcs Value1, Calcs Value2)
{
return new Calcs(Value1.Data + Value2.Data);
}
public static Calcs operator -(Calcs Value1, Calcs Value2)
{
return new Calcs(Value1.Data – Value2.Data);
}
public static Calcs operator *(Calcs Value1, Calcs Value2)
{
return new Calcs(Value1.Data * Value2.Data);
}
public static Calcs operator /(Calcs Value1, Calcs Value2)
{
return new Calcs(Value1.Data / Value2.Data);
}
public Calcs Inc()
{
return new Calcs(this.Data + 1);
}
public Calcs Dec()
{
return new Calcs(this.Data – 1);
}
}
[/code]
In most cases, you want to create a constructor that accepts the kind of data you want to manipulate with the extension. In this case, the constructor accepts an Int32 value. Interestingly enough, the constructor is the only place where you normally reference the data type of the data directly. In all other cases, you work with the data type indirectly by using the extension class.
Another issue is displaying the data in IronPython. The default implementation of the ToString() method displays the class name, which isn’t helpful. Consequently, you must override the default implementation of ToString() and provide your own output. In this case, the method simply returns the current value of the private variable Data as a string.
This example deals with operators. Of course, there are two kinds of operators, unary and binary. The method you implement for each kind of operator is different.
To create a binary operator, you must consider that the operator will work with two instances of the Calcs class. In short, the operator works with the base class and you must declare it as static. In this example, the + operator is binary, so the code declares it as static. The method also accepts the two instances of the Calcs class as input. In order to return output, the method must create a new instance of the Calcs class with the sum of the two input values. Notice that the method never defines what kind of data it works on, simply that the data is contained in an instance of the Calcs class.
Creating a unary operator is different because you’re working with a single instance of the Calcs class in this instance. To create a unary operator, you simply declare the method as a non-static member of the class, as shown for the Inc() and Dec() methods. In this case, because you’re working with a single value, the code uses this.Data (the internal representation of the data value of the single value) to perform the math. You may wonder why the code simply doesn’t create a ++ operator method. A ++ operator method would look like this and wouldn’t work in a unary manner within IronPython.
[code]
public static Calcs operator ++(Calcs Value1)
{
return new Calcs(Value1.Data + 1);
}
[/code]
If you compiled the class now, you could view it in the IronPython console. The following code provides the steps for loading the extension into memory.
[code]
import clr
clr.AddReferenceToFile(‘Calcs.DLL’)
import Calcs
dir(Calcs.Calcs)
[/code]
Figure 16-3 shows the output of the dir(Calcs.Calcs) call. Notice that Inc() and Dec() appear as you expect. However, there aren’t any entries for +, -, *, and / methods. These operators still work as you expect, but IronPython shows a Python equivalent for the operators in the form of the __add__(), __radd__(), __sub__(), __rsub__(), __mul__(), __rmul__(), __div__(), and __rdiv__(). These methods don’t appear unless you define the operators in your class.
If you’re looking at the class in the IronPython console, you might want to give it a quick try before you close up the console and move on to the next part of the example. Try this code and you’ll see an output of 15 from the __add__() method.
[code]
Value1 = Calcs.Calcs(10)
Value2 = Calcs.Calcs(5)
print Value1.__add__(Value2)
[/code]
Adding the IronPython Project
At this point, you have a C# extension (or module) to use with IronPython. Of course, you’ll want to test it. The easiest way to do this is to add the IronPython project directly to the current solution. The following steps describe how to perform this task.
- Right-click the solution entry in Solution Explorer and choose Add ➪ Existing Project from the context menu. You see the Add Existing Project dialog box shown in Figure 16-4.
- Locate IPY.EXE on your hard drive and highlight it. Click Open. You see a new project entry added to the solution.
- Right-click the ipy entry in Solution Explorer and choose Set as Startup Project from the context menu. This step ensures that choosing one of the startup options from the Debug menu starts the IronPython application.
- Right-click the ipy entry in Solution Explorer and choose Properties from the context menu. You’ll see the General tab of the ipy Properties window shown in Figure 16-5.
- Type -D TestCalcs.py in the Arguments field.
- Click the ellipses in the Working Directory field to display the Browse for Folder dialog box. Locate the output folder of the Calcs.DLL (or other extension) file. Click OK. The IDE adds the correct directory information to the Working Directory field.
- Open Windows Explorer. Locate the CalcsCalcsbinDebug folder. Right-click in the right pane and choose New ➪ Text Document from the context menu. Name the file TestCalcs.py and press Enter. Click Yes if asked if you want to rename the file extension.
- Right-click the solution item in Solution Explorer and choose Add ➪ Existing Item from the context menu to display the Add Existing Item dialog box shown in Figure 16-6.
- Locate the TestCalcs.py file in the solution and click Add. Visual Studio adds TestCalcs.py to the Solution Items folder in Solution Explorer and automatically opens the file for you. You’re ready to add test code for the application.
Creating the IronPython Application
Now that you have a file to use for the IronPython application, it’s time to add some code to it. The example code fully exercises everything you can do with the C# extension. Listing 16-2 shows the code you add to the TestCalcs.py file.
Listin g 16-2: Testing the extension using IronPython
[code]
# Add a reference to the CLR
import clr
# Obtain access to the extension.
clr.AddReferenceToFile(‘Calcs.DLL’)
import Calcs
# Create an instance of the class and fill it with data.
Value1 = Calcs.Calcs(10)
# Print the original value, then decrement and increment it.
print ‘Original Value1 Content: ‘, Value1
print ‘Value1 + 1: ‘, Value1.Inc()
print ‘Value1 – 1: ‘, Value1.Dec()
# Create a second value and display it.
Value2 = Calcs.Calcs(5)
print ‘nOriginal Value2 Content: ‘, Value2
# Use the two values together in different ways.
print ‘nValue1 + Value2 = ‘, Value1 + Value2
print ‘Value1 – Value2 = ‘, Value1 – Value2
print ‘Value1 * Value2 = ‘, Value1 * Value2
print ‘Value1 / Value2 = ‘, Value1 / Value2
# Pause after the debug session.
raw_input(‘nPress any key to continue…’)
[/code]
The example begins by importing support for the Common Language Runtime (CLR). It then uses the AddReferenceToFile() method to reference the Calcs.DLL file and imports the code into IronPython. These steps are similar to those that you used to test the DLL initially.
The next step is to create an instance of the Calcs class, Value1. The code references Calcs twice — once for the namespace and a second time for the class itself. The next few code steps display the value of Value1 and show how to use the Inc() and Dec() methods. If you set Value1 equal to the output of Inc() or Dec(), it truly would increment or decrement the value of Value1. Because IronPython doesn’t support the ++ operator, however, you can’t use the ++ operator in your extension. On the other hand, you could implement the += and -= operators.
You can’t really test binary operators without a second variable, so the code creates a second instance of Calcs, Value2. The example then shows how the +, -, *, and / operators work. Figure 16-7 shows the output from this example.
Using C# for User Interface Support
It’s a painful process because you don’t have access to any designers, but the process is definitely doable. You may very well decide to use IronPython directly for all your graphics needs, simply to avoid using another language. However, C# or Visual Basic.NET make better choices for creating a user interface because you do get access to the designer support that these languages provide. With this in mind, the following sections describe how you can add graphic support to IronPython using a C# extension.
Defining a Library of Dialog Boxes
If you’re using IronPython as your main application language and relying on a static language for ancillary support, such as the user interface requirements, it makes sense to create all the dialog boxes you require and place them in a library. Of course, if the application is relatively complex, you might use several physical DLLs to perform the task or rely on a single DLL, but rely on multiple projects to accommodate a team of developers The point is that you need to plan how to store the dialog boxes in a manner that makes it efficient to work on the project.
There’s a tendency by some developers to create generic dialog boxes and then manipulate them in code. This technique does work well when you use the dialog boxes in the static language. However, the approach can become counterproductive when using the dialog boxes in IronPython. The IronPython code can become so complicated that it becomes unreliable and hard to maintain. In general, use specific dialog boxes whenever possible, which won’t require many (or any) changes.
IronPython doesn’t have a representation of every C# or Visual Basic.NET feature. For example, in the section “Developing the C# Extension” section earlier in this chapter, you’ll discover that IronPython doesn’t support the C# ++ operator, but it does support the += operator. It’s best to perform data manipulation in the static language environment when possible or pass the raw data to IronPython in a form it can readily use. For example, you might pass a list of field values to IronPython as a dictionary.
Marshaling data between languages can reduce application performance. You may find situations where you need to process data in a thread to maintain acceptable performance for the user. However, before you take time to create a complex threading solution, ask users to try the application in a test environment to determine whether the performance is acceptable.
Creating the Dialog Box Library in C#
Your dialog box library can support dialog boxes at two levels. It’s possible to meet some IronPython needs using a simple message box or prompt box. Because these solutions are already programmed for you, supporting them through the static language, where the features are easily accessed, is a good way to save on development and debugging time. You can customize the implementation of these standardized features to make them easy to use within IronPython — reducing the need to import a lot of managed assemblies into IronPython.
Of course, many user-interface needs require something more advanced than a simple message box. The following sections describe how to create simple message boxes and complex Windows Forms in C# that you can use in your IronPython application. The goal is to use the right kind of interface element for a given task and to make the interface element easy to access and process from within IronPython. The section “Creating the Simple C# Extension” earlier in this chapter describes how to set up the solution used for this example.
Defining Simple Message Boxes
This example is interesting because it shows how you can create overrides of your methods. The MessageBox.Show() method has 21 overrides in C#. Of course, you might not need all those overrides and the example shows only five of them. Before you can work with message boxes in a C# class, you need to add a reference to the System.Windows.Forms.DLL and add the following using statement.
[code]
using System.Windows.Forms;
[/code]
Now that you have the prerequisites in place, it’s time to look at some code. Listing 16-3 shows the code used to create this example.
Listin g 16-3: Creating a simple message box class
[code]
public class Dialogs
{
public Dialogs()
{
}
public String ShowMessage(String Msg)
{
return MessageBox.Show(Msg).ToString();
}
public String ShowMessage(String Msg, String Title)
{
return MessageBox.Show(Msg, Title).ToString();
}
public String ShowMessage(String Msg, String Title, Int16 Buttons)
{
return MessageBox.Show(Msg, Title,
(MessageBoxButtons)Buttons).ToString();
}
public String ShowMessage(String Msg, String Title, Int16 Buttons,
Int16 Icon)
{
return MessageBox.Show(Msg, Title,
(MessageBoxButtons)Buttons, (MessageBoxIcon)Icon).ToString();
}
public String ShowMessage(String Msg, String Title, Int16 Buttons,
Int16 Icon, Int16 DefaultButton)
{
return MessageBox.Show(Msg, Title,
(MessageBoxButtons)Buttons, (MessageBoxIcon)Icon,
(MessageBoxDefaultButton)DefaultButton).ToString();
}
}
[/code]
The code begins with the usual constructor. The constructor doesn’t really need to do anything in this case. Of course, you could set up the constructor to accept some of the required inputs, such as the message and message box title, but sending the information with the ShowMessage() method works just fine, too. The constructor could also set up default settings, if desired, that the developer could override with specific versions of ShowMessage().
The ShowMessage() method declarations come next. The methods are relatively simple. Each one calls a different override of the MessageBox.Show() method. Notice that you must coerce the MessageBoxButtons, MessageBoxIcon, MessageBoxDefaultButton values from the inputs. You could ask the caller to provide the actual enumerated values, but that approach would reduce the benefit of using this approach for working with message boxes, because the developer would need to load the required .NET assemblies anyway.
Even when working with simple message boxes, you can encounter a few problems. For example, the enumerations provided in the static environment make it simple to select a particular button combination or icon. IntelliSense displays the list of values from which you can choose. However, IronPython doesn’t provide IntelliSense, so there isn’t any simple method of selecting a button combination or icon from a list. The example uses numbers, which works fine for the button combinations because they’re numbered 0 through 5. However, the icons have values of 0, 16, 32, 48, and 64, which are hardly easy to remember. The default button values are equally odd at 0, 256, and 512. Tables 16-1 through 16-3 show the values for the message box enumerations. In a production environment, you’d probably create text equivalents for the developer, which you could translate in the extension, or provide some type of enumeration for the developer.
Using Enumerations with IronPython
There’s a way around the issue of enumerated values in .NET calls. You can simply choose to create your own enumeration. For example, let’s say you want to overcome the problem of working with the MessageBoxButtons enumeration. In this case, you create an enumeration and a new override of the ShowMessage() method as shown here.
[code]
public enum ButtonTypes
{
OK,
OKCancel,
AbortRetryIgnore,
YesNoCancel,
YesNo,
RetryCancel
}
public String ShowMessage(String Msg, String Title, ButtonTypes Buttons)
{
return MessageBox.Show(Msg, Title,
(MessageBoxButtons)Buttons).ToString();
}
[/code]
Notice that you must still use coercion to make the MessageBox.Show() call. However, the IronPython developer now has an enumeration to use when making the call. Here’s a typical call from within IronPython.
[code]
MyDialog.ShowMessage(‘Hello’, ‘Title’, MyDialog.ButtonTypes.OKCancel)
[/code]
The resulting message box would contain ‘Hello‘ as the message, ‘Title‘ as the message box title, and two buttons, OK and Cancel.
Considering Developer Help
As your extensions gain in complexity, you need to start providing some help to the IronPython developer. Most IronPython developers will spend part of their time in the interpreter trying things out. The developer will look to your documentation for help in using the extension you create. There are two forms of help, as shown here.
[code]
help(MyDialog.ShowMessage)
MyDialog.ShowMessage.__doc__()
[/code]
It turns out that IronPython automatically provides a form of the help() function help for you as shown in Figure 16-8. In this case
Unfortunately, IronPython doesn’t provide the __doc__() method by default. You must define it for yourself as part of the class you create. Here’s a simple __doc__() method you can use with the example. Of course, a production version would contain far more information.
[code]
public String __doc__()
{
return “This is a help string”;
}
[/code]
When you try this method out at the Python prompt, you see the outline shown in Figure 16-9. You can use all of the normal formatting characters to make the help provided by the __doc__() method look nice. For that matter, you could store the information externally and simply read it in as needed.
Defining Complex Forms
At some point, simple message boxes simply won’t do the job for you. After all, you’ll want forms that contain a number of fields that you can use to process complex information from the user. In this case, you must create a standard Windows form for your extension. To accomplish this task, you begin by adding the form using the following steps.
- Right-click Dialogs in Solution Explorer and choose Add ➪ New Item. Select the Windows Forms entry in the Installed Templates list. You see the Add New Item dialog box shown in Figure 16-10.
- Highlight the Windows Form entry. Type TestForm.CS in the Name field and click Add. Visual Studio adds the new form to your project and automatically opens it for editing.
At this point, you can create the form just as you normally would for any static application. Figure 16-11 shows the form used for this example. It’s simple, but it contains multiple dataentry fields and multiple exit options.
Before you assume anything about this form, note that it does differ in a few ways from the forms you’ve created for your static applications. The first difference is that the buttons that close the form, rather than do something within the form, must have the DialogResult property set to a unique value or you won’t be able to tell which button the user clicked. For this example, the DialogResult for btnOK is OK, while the DialogResult for btnCancel is Cancel.
The second difference involves a problem with getting information from the form you create to the IronPython application. You could contrive all sorts of odd methods for accomplishing the task, but the simplest method is to set the Modifiers property for the individual controls (txtName and txtColor) to Public. In this case, using Public doesn’t create a problem because IronPython sets everything to public. In all other respects, there’s no difference between this form and any other form you’ve created in the past.
To make things simple, this example doesn’t use any code-behind for the form itself. Any codebehind works as you’d expect. There isn’t any difference between calling the form from IronPython than calling it from within your C# application.
Accessing the Dialog Box Library from IronPython
At this point, you have a nice collection of dialog box and form classes to use in an IronPython application. Of course, a production application would probably have quite a few more forms in it, but you have enough for testing and experimentation purposes. The following sections describe how to use these classes.
An Alternative Method for Adding the IronPython Project
There are a number of ways to configure a test setup for your extensions. The section “Adding the IronPython Project” earlier in this chapter shows one technique. The technique works well when you want to maintain separate builds of your extension. However, you might want to maintain just one build — the build you’re currently using for debugging, testing, or experimentation. Use the following steps to create a centralized test configuration.
- Right-click Dialogs in Solution Explorer and choose Properties from the context menu. Select the Build tab. You see the Properties window shown in Figure 16-12.
- Click Browse next to the Output Path field to display the Select Output Path dialog box shown in Figure 16-13. Because you’ll add the IronPython test file at the solution level, you need to send the output to the solution level as well.
- Select the first Dialogs entry in the list and click OK. Visual Studio adds an absolute path to the Output Path field that you must change for every machine that uses the application. As an alternative, you could type ..(two periods and a backslash) in the field to place the output in the solution folder.
- Select the next configuration in the Configuration field.
- Perform Steps 2 through 4 for each configuration. Make sure each configuration uses the same output directory. Normally, your project will only contain Debug and Release configurations.
- Right-click the solution entry in Solution Explorer and choose Add ➪ Existing Project from the context menu. You see the Add Existing Project dialog box shown in Figure 16-4.
- Locate IPY.EXE on your hard drive and highlight it. Click Open. You’ll see a new project entry added to the solution.
- Right-click the ipy entry in Solution Explorer and choose Set as Startup Project from the context menu.
- Right-click the ipy entry in Solution Explorer and choose Properties from the context menu. You see the General tab of the ipy Properties window shown in Figure 16-5.
- Type -D DialogTest.py in the Arguments field.
- Click the ellipses in the Working Directory field to display the Browse for Folder dialog box. Locate the solution folder for the project (the first Dialogs folder). Click OK. The IDE adds the correct directory information to the Working Directory field.
- Right-click the solution entry in Solution Explorer and choose Add ➪ New Item from the context menu. You see the Add New Item dialog box shown in Figure 16-14.
- Type DialogTest.py in the Name field and click Add. Visual Studio adds the new file to the Solution Items folder in Solution Explorer and opens the file automatically for editing.
Performing the Message Box and Form Tests
It’s finally time to test the message boxes and forms you’ve created. The code in this section performs a few simple tests and demonstrates how to obtain output from the message boxes and forms you’ve created. You can use this code as a starting point for more complex processing in your own application. Listing 16-4 shows the test code for this application.
Listin g 16-4: Testing the extension using IronPython
[code]
# Define the message box tests.
def TestMessages():
# Create a message box object.
MyDialog = Dialogs.Dialogs()
# Test a simple message box.
print ‘Testing a simple message box.’
print ‘Simple message box output: ‘,
print MyDialog.ShowMessage(‘Hello’)
# Perform a more complex test.
print ‘nA more complex message box.’
print ‘Complex message box output: ‘,
print MyDialog.ShowMessage(‘Hello Again’, ‘Title 2’, 3, 64, 256)
# Define the form test.
def TestForm():
# Create the form instance.
MyForm = Dialogs.TestForm()
# Display the form and test the dialog result.
print ‘nThe form example.’
if MyForm.ShowDialog().ToString() == ‘OK’:
# Display the results.
print ‘The user clicked OK.’
print ‘User Name: ‘, MyForm.txtName.Text
print ‘Favorite Color: ‘, MyForm.txtColor.Text
# Display an alternate result.
else:
print ‘The user clicked cancel.’
# Import the Common Language Runtime.
import clr
# Access the extension.
clr.AddReferenceToFile(‘Dialogs.DLL’)
import Dialogs
# Test the message box code.
TestMessages()
# Test the form code.
TestForm()
# Pause after the debug session.
raw_input(‘nPress any key to continue…’)
[/code]
The test code begins by importing CLR and gaining access to the Dialogs namespace. This example demonstrates one of the benefits of using a namespace, easy access to multiple classes. It’s a good way to organize a library of forms to make them easy to access and to avoid naming conflicts.
The TestMessages() function contains the code to test the Dialogs.Dialogs class. This code begins by creating a Dialogs.Dialogs instance, MyDialog. In this case, the application begins by creating a simple message box and displaying it onscreen. This message box lacks a title and contains only an OK button. When the user clicks OK, the program prints the dialog result to screen.
The second test is a little more complex. This time the code relies on the most complex form of the ShowMessage() method to display a dialog box that contains a message, title, icon, and multiple buttons as shown in Figure 16-15. Notice that the figure shows that the message box also has the middle button selected by default. Pressing Enter will automatically select this default option. Normally, message boxes select the first button as the default. Depending on which button the user clicks, the application will display a message with the appropriate dialog result. You could also use this dialog result as part of an if…else statement to choose an appropriate course of action.
The TestForm() method begins by creating an instance of Dialogs. TestForm, MyForm. The dir() function will show you that MyForm now has access to all of the functionality normally associated with a Windows Forms class, but without importing any of the bulk associated with the System.Windows .Forms assembly. As with any Windows Form, you call ShowDialog() to display the form. However, the result of displaying the form is going to be something that IronPython can’t use directly. The way to overcome this problem is to call ShowDialog().ToString(). In this case, the output is a string that describes which button the user has clicked.
This portion of the example shows how to process the form data locally. When the user clicks OK, the dialog result is ‘OK‘ and the if statement succeeds. The code accesses the MyForm.txtName.Text and MyForm.txtColor.Text properties to determine what the user has typed. When the if statement fails, the code displays a message telling you that the user clicked Cancel. Figure 16-16 shows typical output from this example.
Using C# for Win32 Support
The Python language doesn’t really support much in the way of platform-specific functionality and that’s by design. One of the tenets of cross-platform compatibility is not to make an issue out of the platform on which the code runs. However, in some cases, you really do need to access the platform and discover things about it. For example, you might want to know more about the environment in which your application is executing, such as the size of the console window. You might even want to clear the console window (a feature that is missing from the IronPython console, without which your sessions can appear messy). An application may need to know something about the security in place for the current session. In short, you might have many reasons for wanting to know something more, but Python (and by extension, IronPython) largely lacks the functionality to provide this information.
The example in the following sections plays to a strength of C#, which is to interact with the Windows platform through a feature called Platform Invoke (P/Invoke). This example goes outside the managed .NET environment and relies on the Win32 API to access Windows functionality that you can’t access through .NET.
Creating the P/Invoke Code
Before you can write any P/Invoke code, you need to add the following using statement.
[code]
using System.Runtime.InteropServices;
[/code]
This statement provides access to the various special programming features that C# provides for accessing the Win32 API.
If you haven’t worked with the Win32 API in the past, you might find the use of structures, enumerations, and pointers confusing. In reality, all these events take place somewhere in the background when you execute any application. At some point, your managed code ends up interacting with the Win32 API to perform tasks because the basic Windows DLLs still rely on the Win32 API. Normally, CLR hides all these details from view so you don’t need to worry about them. Listing 16-5 shows the Win32 API access code — the lower-level code that does all the hard work for this example.
Listin g 16-5: Win32 API access code and structures
[code]
// This special class contains an enumeration of
// standard handles.
class StdHandleEnum
{
public const int STD_INPUT_HANDLE = -10;
public const int STD_OUTPUT_HANDLE = -11;
public const int STD_ERROR_HANDLE = -12;
};
// The GetStdHandle() function returns a handle to any
// standard input or output.
[DllImport(“kernel32.dll”, SetLastError=true)]
public static extern IntPtr GetStdHandle(int nStdHandle);
// This sructure contains a screen coordinate.
[StructLayout(LayoutKind.Sequential, Pack=1)]
public struct COORD
{
public short X;
public short Y;
}
// Obtains the current display mode–fullscreen or fullscreen hardware.
[DllImport(“Kernel32.DLL”)]
public static extern bool GetConsoleDisplayMode(ref UInt32 lpModeFlags);
// An enumeration used to determine the current display mode.
public enum ConsoleDispMode
{
CONSOLE_WINDOWED = 0, // Only implied by function.
CONSOLE_FULLSCREEN = 1, // The console is fullscreen.
CONSOLE_FULLSCREEN_HARDWARE = 2 // The console owns the hardware.
}
// Obtains the size of the largest console window possible.
[DllImport(“Kernel32.DLL”)]
public static extern COORD
GetLargestConsoleWindowSize(IntPtr hConsoleOutput);
// Returns the console mode information.
[DllImport(“Kernel32.DLL”)]
public static extern bool GetConsoleMode(
IntPtr hConsoleHandle,
ref UInt32 lpMode);
public enum ModeFlags
{
// Input mode flags
ENABLE_PROCESSED_INPUT = 0x0001,
ENABLE_LINE_INPUT = 0x0002,
ENABLE_ECHO_INPUT = 0x0004,
ENABLE_WINDOW_INPUT = 0x0008,
ENABLE_MOUSE_INPUT = 0x0010,
// Output mode flags
ENABLE_PROCESSED_OUTPUT = 0x0001,
ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
}
[/code]
Many of the Win32 API functions require you to know specific integer or hexadecimal values. Even C++ developers can’t remember these numbers. Normally, a C++ developer relies on define statements that put the numbers into human-readable form. The P/Invoke code used in this chapter does the same thing, but sometimes it places the numbers in an enumeration to make them even easier to use. The StdHandleEnum class provides a list of standard handles (pointers) for Windows devices: input, output, and error. However, these aren’t the actual handles.
In order to get the standard Windows handle, an application must call the GetStdHandle() function. This function is in kernel32.dll. The [DllImport()] attribute tells the compiler where to look for an external Win32 API function that you want to use in your code. In this case, the attribute also tells the compiler that you want any error information that the Win32 API can provide. The use of extern before the function name tells the compiler that the requested DLL contains a function of the name that follows. You can now call this function directly and CLR will automatically perform any required marshaling for you.
Many of the Win32 API calls provide coordinates — x and y locations that tell where something is or how large it is. The COORD structure provides a means of transferring this kind of information between the .NET environment and the Win32 API environment. Windows uses a very basic view of structures. Unfortunately, .NET often causes problems by trying to optimize the data structures and causes P/Invoke calls to fail even though they should succeed. The [StructLayout()] attribute tells the compiler how to create a data structure in memory, which overrides the normal optimization process.
You may create applications that need to run in full-screen mode, if for no other reason than that they require the additional screen real estate to present information to the user. The GetConsoleDisplayMode() function tells you what mode the console is currently in. If the console is in the wrong mode, you can ask the user to change the mode or simply stop the application before the screen mode causes any problems. This function returns flags, not an enumerated value. At least one of the flags is always set, but the return value can have multiple flags set. The ConsoleDispMode enumeration makes it easier to work through the flag settings and provide usable output. The section “Defining the GetCurrentDisplayMode() Method” later in this chapter provides more information about this function.
In some cases, you need to know the largest size console window that the system will support. The GetLargestConsoleWindowSize() function provides this information. You can use other Win32 API functions to adjust the size of the window to meet application requirements (which is a topic for another book). The section “Defining the GetConsoleWindowSize() Method” provides more information about this function.
It’s also handy to know what kinds of operations the console window will support. For example, it’s good to know whether the console window will respond to the mouse. The GetConsoleMode() function provides this kind of information. The output is in the form of flags that you must interpret in your code. The GetConsoleMode() function is special in that the output you receive depends on the kind of device handle you provide. The output differs when you provide an input handle, versus an output handle. The section “Defining the GetConsoleInfo() Method” provides additional information about how this technique works.
Developing the IronPython Callable Methods
The P/Invoke code shown in Listing 16-5 does expose the Win32 API calls needed to perform certain tasks with IronPython. Theoretically, you could rely on just the code in Listing 16-5 to gain the access you require in IronPython. However, the task would be difficult because you’d need to work through the required bit manipulations. It’s better to place the code you need to access the Win32 API in easily called methods, which is the purpose of the code in the following sections.
Defining Common Variables and the Constructor
Win32 API calls often reuse information. It’s not uncommon for functions to ask for the same information over and over. For example, any function that works with a window will probably need the handle for that window. With this requirement in mind, Listing 16-6 shows the common variables and the constructor used for this example.
Listin g 16-6: Common variables and constructor
[code]
UInt32 DisplayMode = 0; // The current display mode.
IntPtr hOut; // Handle to the output device.
IntPtr hIn; // Handle to the input device.
UInt32 ConsoleMode = 0; // The console mode information.
public ConMode()
{
// Obtain a handle to the console screen and console input.
hIn = GetStdHandle(StdHandleEnum.STD_INPUT_HANDLE);
hOut = GetStdHandle(StdHandleEnum.STD_OUTPUT_HANDLE);
}
[/code]
The common variables include the current display mode (such as windowed), the console mode information (such as whether it accepts mouse input), and the handles for the input and output devices. These variables represent common pieces of information that the developer requires for multiple calls.
The constructor initializes the input and output handles using the GetStdHandle() function. The input argument simply tells Windows which handle you want. The output is an IntPtr, a special kind of variable that points to something. An IntPtr is a safe pointer, meaning you can use it without problems in a managed language. C# also supports unsafe pointers that you should use only as a last resort.
Defining the GetCurrentDisplayMode() Method
Sometimes you need to know whether the console is presented in a windowed or full-screen mode. A windowed console can get covered up and needs to share resources with other windows. In addition, the text in a windowed console can be small and hard to read. On a positive note, using a windowed console makes it easier to share data between applications. In most cases, the user will prefer that you use a windowed console to make it easier to multitask between applications. Listing 16-7 shows how to detect the current console display mode.
Listin g 16-7: Obtaining the current display mode
[code]
public OutputMode GetCurrentDisplayMode()
{
// Get the current display mode.
if (GetConsoleDisplayMode(ref DisplayMode))
// Determine if the console is in windowed mode.
if (DisplayMode == (UInt32)ConsoleDispMode.CONSOLE_WINDOWED)
return OutputMode.Windowed;
else
{
// If the console is fullscreen mode, determine which
// of the potential conditions are true.
switch (DisplayMode)
{
case (UInt32)ConsoleDispMode.CONSOLE_FULLSCREEN:
return OutputMode.Fullscreen;
case (UInt32)ConsoleDispMode.CONSOLE_FULLSCREEN_HARDWARE:
return OutputMode.HadwareAccess;
case (UInt32)ConsoleDispMode.CONSOLE_FULLSCREEN +
(UInt32)ConsoleDispMode.CONSOLE_FULLSCREEN_HARDWARE:
return OutputMode.FullscreenHardwareAccess;
}
}
// Return a default value.
return OutputMode.Unknown;
}
[/code]
The code begins by calling GetConsoleDisplayMode() to obtain the display mode as a numeric value. The information is returned in DisplayMode, not as a return value from the function call. The function itself returns a success value that indicates the call was successful. The first if statement says that if the call is successful, then DisplayMode will contain the console display mode, and that the application should proceed to process it. Because DisplayMode provides a return value, you must include the ref keyword when passing it to the Win32 API.
Now that the code has a display mode value, it needs to process it. If a console is in windowed mode, all the code has to do is return a value that says it’s windowed. However, full-screen mode requires some additional processing. When a console is in full-screen mode, it can also have access to the hardware. This is virtual hardware access, but it still feels to the application as if the access is direct. Consequently, the code must now determine whether the console is simply in full-screen mode or it’s in full-screen mode with hardware access.
The call could fail, but it’s unlikely to. Even so, the GetCurrentDisplayMode() handles the potential problem by providing the OutputMode.Unknown return value. This value simply says that the method couldn’t determine the current console display mode.
Defining the GetConsoleWindowSize() Method
Sometimes an application needs to know the maximum windowed console that a machine can accommodate. You might need additional room to display complex textual information. The Win32 API returns this information in a COORD structure that simply states the number of rows and columns of text that a console can support at maximum size. The following code shows the GetConsoleWindowSize() method used to obtain this information.
[code]
public COORD GetConsoleWindowSize()
{
// Determine the largest screen size possible.
return GetLargestConsoleWindowSize(hOut);
}
[/code]
This method is easy. All it does is call the GetLargestConsoleWindowSize() function with the output handle. Make sure you provide the output handle, and not the input handle, when making this call. The X and Y members of COORD contain the maximum screen size on return from the call.
Defining the GetConsoleInfo() Method
Consoles can support a number of input and output methods. For example, a console can support the mouse, which may make it easier for the user to interact with your character-mode application. If a console provides support for echo, it re-displays commands sent to it from batch files and other forms of automation. Consequently, you might find it useful to know just what the console will do for you. Listing 16-8 shows how to determine the input and output handling that a console provides.
Listin g 16-8: Obtaining the console characteristics
[code]
public struct ConsoleData
{
public Boolean Echo;
public Boolean LineInput;
public Boolean MouseInput;
public Boolean ProcessedInput;
public Boolean WindowInput;
public Boolean ProcessedOutput;
public Boolean LineWrap;
}
public ConsoleData GetConsoleInfo()
{
// Create the required structure.
ConsoleData Output = new ConsoleData();
// Retrieve the input information.
if (GetConsoleMode(hIn, ref ConsoleMode))
{
if ((ConsoleMode & (UInt32)ModeFlags.ENABLE_ECHO_INPUT) ==
(UInt32)ModeFlags.ENABLE_ECHO_INPUT)
Output.Echo = true;
else
Output.Echo = false;
if ((ConsoleMode & (UInt32)ModeFlags.ENABLE_LINE_INPUT) ==
(UInt32)ModeFlags.ENABLE_LINE_INPUT)
Output.LineInput = true;
else
Output.LineInput = false;
if ((ConsoleMode & (UInt32)ModeFlags.ENABLE_MOUSE_INPUT) ==
(UInt32)ModeFlags.ENABLE_MOUSE_INPUT)
Output.MouseInput = true;
else
Output.MouseInput = false;
if ((ConsoleMode & (UInt32)ModeFlags.ENABLE_PROCESSED_INPUT) ==
(UInt32)ModeFlags.ENABLE_PROCESSED_INPUT)
Output.ProcessedInput = true;
else
Output.ProcessedInput = false;
if ((ConsoleMode & (UInt32)ModeFlags.ENABLE_WINDOW_INPUT) ==
(UInt32)ModeFlags.ENABLE_WINDOW_INPUT)
Output.WindowInput = true;
else
Output.WindowInput = false;
}
// Retrieve the output information.
if (GetConsoleMode(hOut, ref ConsoleMode))
{
if ((ConsoleMode & (UInt32)ModeFlags.ENABLE_PROCESSED_OUTPUT) ==
(UInt32)ModeFlags.ENABLE_PROCESSED_OUTPUT)
Output.ProcessedOutput = true;
else
Output.ProcessedOutput = false;
if ((ConsoleMode & (UInt32)ModeFlags.ENABLE_WRAP_AT_EOL_OUTPUT)
== (UInt32)ModeFlags.ENABLE_WRAP_AT_EOL_OUTPUT)
Output.LineWrap = true;
else
Output.LineWrap = false;
}
// Return the results.
return Output;
}
[/code]
This is one of the few situations in the chapter where you need to send a number of pieces of information back to IronPython. The ConsoleData structure contains an entry of each piece of information that the GetConsoleInfo() provides. An IronPython application can set the output of the call to a variable and then use the variable content to determine precisely how the console is configured.
The GetConsoleInfo() method is a little more complicated than the other calls in the extension. This method relies on the GetConsoleMode() function to obtain console information. However, notice that the method calls the GetConsoleMode() function twice, once with the input handle and again with the output handle. This method demonstrates how the use of the wrong handle could cause problems because the output from the GetConsoleMode() function differs with the handle you provide as input.
The return value from the GetConsoleMode() function is a series of flags. Notice how the code uses if statements to determine whether each flag is set. When a flag is set, the feature is enabled and the code sets that value in the ConsoleData data structure, Output, to true. The method ends by returning the fully completed ConsoleData data structure to the caller.
Writing an IronPython Application to Use P/Invoke
If you’ve been following along with the example, you know it’s finally time to use the ConMode class with IronPython. It’s now possible to determine the display mode, the size of the console window, and the capabilities it provides. Listing 16-9 shows the code used for testing this extension.
Listin g 16-9: Testing the Win32 API extension
[code]
# Import the Common Language Runtime.
import clr
# Access the extension.
clr.AddReferenceToFile(‘Win32API.DLL’)
import Win32API
# Create an instance of the class.
TestWin32 = Win32API.ConMode()
# Check the display mode.
print ‘The display mode is: ‘,
print TestWin32.GetCurrentDisplayMode()
# Obtain the largest possible window size.
print ‘nThe largest possible window size is: ‘
Size = TestWin32.GetConsoleWindowSize()
print ‘tColumns: ‘, Size.X
print ‘tRows: ‘, Size.Y
# Display the console characteristics.
print ‘nThe console has these characteristics:’
Chars = TestWin32.GetConsoleInfo()
print ‘tEcho Enabled: ‘, Chars.Echo
print ‘tLine Input Enabled: ‘, Chars.LineInput
print ‘tMouse Input Enabled: ‘, Chars.MouseInput
print ‘tProcessed Input Enabled: ‘, Chars.ProcessedInput
print ‘tWindow Input Enabled: ‘, Chars.WindowInput
print ‘tConsole Can Produce Processed Output:’, Chars.ProcessedOutput
print ‘tConsole Uses Line Wrap: ‘, Chars.LineWrap
# Pause after the debug session.
raw_input(‘nPress any key to continue…’)
[/code]
The code begins by importing CLR support. It then creates a reference to Win32API.DLL and imports the Win32API namespace into the IronPython environment. The next step is to create an instance of the Win32API.ConMode class, TestWin32.
At this point, the code begins checking each console feature in turn, beginning with the console display mode, which doesn’t require any additional processing. The GetConsoleWindowSize() method call requires that the code display the Size.X (columns) and Size.Y (rows) values separately.
The GetConsoleInfo() method call comes next. This particular call requires a little more processing because it returns more information. The output of the call appears in Chars as a ConsoleData data structure. As you can see, the code simply displays the true or false value of each of the data structure members. Figure 16-17 shows the output from this example.
One of the most important issues when making Win32 API calls from IronPython is to ensure that the C# extension processes the data in an easy-to-use manner. In addition, you should provide a consistent method for returning the data from the C# extension to IronPython, such as using data structures (as shown in the example).