Introduction
In this article, I'll explain the use of IronPython embedded into C# as a scripting engine. While doing this, I will also be showing the basics of WPF and how to integrate IronPython in it, so that we may use Python code to edit our application. Python is also a very easy language to pickup, especially if you know C#, because they are quite similar.
Introducing Python
The Python programming language was released in 1991, and was created by Guido van Rossum. Python's syntax is very clear and simple, taking the programmer's effort over computer effort. The language itself is a multi-paradigm language, which is similar to Perl, Ruby, and other languages. Python is an open community-based development model managed by the non-profit Python Software Foundation.
Brief differences of C# and Python
Python's dynamic typing model allows it to determine data types automatically at runtime. There is no need to declare a variable's type ahead of time, which is a very simple concept.
The difference between declaring variables in C# and Python:
C#
int a = 1
string b = "b"
Python
a = 1
b = 'b'
Creating an if
statement in Python is almost the same as in C#, except Python does not use curly ({}) braces to begin and end methods. Instead, a colon (:) is added at the beginning of the statement to begin executing the code. A problem arises because there is no ending indication as there is with C#'s curly braces, so you are stuck putting one statement without any indication of another. This is solved by simply putting a semi-colon (;) at the end of each statement, to indicate we are not done with the method.
The difference between creating an if
statement in C# and Python:
C#
if (a > b)
{
a = 1;
b = 2;
}
else if (a < 3 and b > 3)
{
a = 2;
}
else
{
b = 3;
}
Python
if (a > b):
a = 1;
b = 2;
elif (a < 3 and b > 3):
a = 2
else:
b = 3
A function declared in Python is pretty much the same as the previous if
statements except that it begins with "def
". Python's def
is executable code, therefore when you compile your code, the function does not exist until Python reaches and runs the def
. Function types (like variables) do not need to be declared a type.
The difference between creating functions in C# and Python:
C#
int MyFunction()
{
return 5;
}
Python
def MyFunction():
return 5;
That was a very brief introduction to Python. Also, be aware that a lot of Python's syntax can be typed differently than shown here, but may mean the same thing.
Introduction to IronPython
IronPython was created with the implementation of the Python language, which was built for the .NET environment. The creator of IronPython is Jim Hugunnin, and the first version of IronPython was released in September 5, 2006.
Embedding IronPython
IronPython can be embedded into a WPF (Windows Presentation Framework) application in a few simple steps:
- Reference IronPython and IronMath.
- Add the namespaces:
using IronPython;
using IronMath;
- Declare
PythonEngine
:
engine = new PythonEngine();
By accomplishing these three steps, you have initialized everything needed for the PythonEngine
to begin.
Using IronPython in your application simply focuses on declaring variables and loading Python Script (.py extension) files.
PythonEngine.Globals.Add(key, value);
PythonEngine.CompileFile(string path);
Example of adding variables to the PythonEngine
globals:
int var = 1;
PythonEngine.Globals.Add("var", var);
PythonEngine.Globals["var"] = 3;
Example of compiling a Python (.py) file:
Python
//PythonFile.py
//
name = 'Chris'
age = 21
C#
PythonEngine.CompileFile("PythonFile.py");
string name = PythonEngine["name"].ToString();
int age = (int)PythonEngine["age"] ;
As you can see, PythonEngine.Globals
plays a huge role in C# and Python communication.
Now say, if you want to execute a simple command, for instance, using Python's print
command inside of C#. There is a simple function that does exactly that:
//Execute code
PythonEngine.Execute(print name)
//Ouputs to the stream
PythonEngine.SetStandardOutput(Stream s)
An example of this being used in C#:
string name = "Bob";
MemoryStream stream = new MemoryStream();
PythonEngine.Globals.Add("name", name);
PythonEngine.SetStandardOutput(stream);
PythonEngine.Execute("print name")
byte[] data = new byte[stream.Length];
stream.Seek(0, SeekOrigin.Begin);
stream.Read(data, 0, data.Length);
string strdata = Encoding.ASCII.GetString(data);
You could also hook up an Input and Error stream:
PythonEngine.SetStandardError(Stream s);
PythonEngine.SetStandardInput(Stream s);
Creating the application using WPF
The application I've created here was created using the Windows Presentation Framework, and the goal of this program is to be used to experiment with IronPython and see its benefits. I am going to go over the XAML and C# code used to create the basic UI of the application.
In order to get the "Aero" look of the application, I had to follow these steps:
- Add the
PresentationFramework.Aero
reference.
- After that, right click on the reference PresentationFramework.Aero and select: Copy Local to true.
- Open up the App.xaml and add/edit:
<ResourceDictionary
Source="/presentationframework.aero;component/themes/aero.normalcolor.xaml" />
- Click Build Application.
The Aero look is now on the UI.
The TreeView
that is used in this application consists of a parent Scene
node and the child nodes to the Scene
node. The Scene
node consists of three child nodes, which are Script
, Actors
, and Objects
. The Script
node has a combobox as a child, so the user may be able to select the current script to render to the scene. Actors
and Objects
are left blank for any child. The node display names are set with the Header
attribute, and are expanded with the IsExpanded="True"
property.
<TreeView Name="treeScene" Background="LightGray" Width="135">
<TreeViewItem Header="Scene" IsExpanded="True">
<TreeViewItem Header="Script">
<ComboBox Name="comboScript" />
</TreeViewItem>
<TreeViewItem Header="Actors" IsExpanded="True" />
<TreeViewItem Header="Objects" IsExpanded="True" />
</TreeViewItem>
</TreeView>
Once an Actor, Script, or Object is created, the TreeView
creates and/or updates the new content. Updating the TreeView
with a new Actor consists of these steps:
- Actor is created.
- Add
TreeViewItem
to the Actors node.
- Create another
TreeViewItem
under the Actor node just created.
- Add a
ComboBox
to the newest created TreeViewItem
.
Here is the C# code that implements this process:
public void AddTreeItem()
{
TreeViewItem treeItemActor = new TreeViewItem();
treeItemActor.IsExpanded = true;
treeItemActor.Header = actorName;
TreeViewItem treeItemScript = new TreeViewItem();
treeItemScript.IsExpanded = true;
treeItemScript.Header = "Script";
ActorScript = new ComboBox();
foreach (string scriptName in AIEngine.ScriptFiles.Keys)
{
actorScript.Items.Add(scriptName);
}
treeItemScript.Items.Add(ActorScript);
int actorIndex = ((TreeViewItem)((TreeViewItem)
AIEngine.SceneTree.Items[0]).Items[1]).Items.Add(treeItemActor);
((TreeViewItem)((TreeViewItem)((TreeViewItem)
AIEngine.SceneTree.Items[0]).Items[1]).Items[actorIndex]).Items.Add(
treeItemScript);
}
This process is implemented in the application every time an Actor is created.
Screen
is made from a custom screen which is created by inheriting the DrawingCanvas
class, which inherits the Canvas
class. The DrawingCanvas
class serves as a control where the user can draw his or her objects onto the screen. In order to do this, the class has to have a collection System.Windows.Media.Visuals
(in which the drawings are saved). Also, I created a temporary visual collection so that I could make the square drawing animation without cluttering the main visual collection.
The code here shows how this was implemented:
public class DrawingCanvas : Canvas
{
private List<Visual>
visuals = new List<Visual>();
private List<Visual>
tempvisuals = new List<Visual>();
public bool startTempVisual = false;
protected override int VisualChildrenCount
{
get
{
if (!startTempVisual)
{
return visuals.Count;
}
else
{
return tempvisuals.Count;
}
}
}
protected override Visual GetVisualChild(int index)
{
if (!startTempVisual)
{
return visuals[index];
}
else
{
return tempvisuals[index];
}
}
public void AddVisual(Visual visual, bool tempVisual)
{
if (tempVisual)
{
tempvisuals.Add(visual);
}
else
{
visuals.Add(visual);
}
base.AddVisualChild(visual);
base.AddLogicalChild(visual);
}
public void DeleteVisual(Visual visual, bool tempVisual)
{
if (tempVisual)
{
tempvisuals.Clear();
}
else
{
visuals.Remove(visual);
}
base.RemoveVisualChild(visual);
base.RemoveLogicalChild(visual);
}
}
The DrawingCanvas
class is then embedded into the XAML of MainWindow
:
<local:DrawingCanvas Name="drawingScreen" Background="DimGray"></local:DrawingCanvas>
The "local:
" tag is used to reference DrawingCanvas
from the namespace:
xmlns:local="clr-namespace:AIEditor.Core;assembly=AIEditor.Core"
The grid shown in the background of Screen
is drawn using System.Windows.Media.DrawLine
and added to the visual collection of the DrawingCanvas
.
Upon window loading, this event is fired to draw the background grid:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
visual = new DrawingVisual();
int heightIncrements = (int)drawScreen.RenderSize.Height / 10;
int widthIncrements = (int)drawScreen.RenderSize.Width / 10;
int horizontalLength = (int)drawScreen.RenderSize.Width;
int verticalLength = (int)drawScreen.RenderSize.Height;
float verticalIncrement = 0;
float horizontalIncrement = 0;
float largerIndicator = 0;
using (DrawingContext dc = visual.RenderOpen())
{
for (int h = 0; h < heightIncrements + 1; h++)
{
if (largerIndicator == 5)
{
Point fromPoint = new Point(0, verticalIncrement);
Point toPoint = new Point(horizontalLength, verticalIncrement);
HorizontalLines.Add(new Point[] { fromPoint, toPoint });
Pen pen = new Pen(Brushes.DarkKhaki, .5);
dc.DrawLine(pen, fromPoint, toPoint);
largerIndicator = 0;
}
else
{
Point fromPoint = new Point(0, verticalIncrement);
Point toPoint = new Point(horizontalLength, verticalIncrement);
HorizontalLines.Add(new Point[] { fromPoint, toPoint });
Pen pen = new Pen(Brushes.Gray, .5);
dc.DrawLine(pen, fromPoint, toPoint);
}
largerIndicator += 1;
verticalIncrement += 10;
}
largerIndicator = 0;
for (int w = 0; w < widthIncrements + 1; w++)
{
if (largerIndicator == 5)
{
Point fromPoint = new Point(horizontalIncrement, verticalLength);
Point toPoint = new Point(horizontalIncrement, 0);
VerticalLines.Add(new Point[] { fromPoint, toPoint });
Pen pen = new Pen(Brushes.DarkKhaki, .5);
dc.DrawLine(pen, fromPoint, toPoint);
largerIndicator = 0;
}
else
{
Point fromPoint = new Point(horizontalIncrement, verticalLength);
Point toPoint = new Point(horizontalIncrement, 0);
VerticalLines.Add(new Point[] { fromPoint, toPoint });
Pen pen = new Pen(Brushes.Gray, .5);
dc.DrawLine(pen, fromPoint, toPoint);
}
largerIndicator += 1;
horizontalIncrement += 10;
}
}
drawScreen.AddVisual(visual, false);
}
This drawing starts off by dividing the size of the screen by ten to get the spacing between each grid line. We increment through each of the spacing using a for
loop and set the fromPoint
and toPoint
draw locations that are required to draw a line from point A to point B. horizontalIncrement
and verticalIncrement
hold the current incremented position to draw the next line, and when we draw the line, we want the line to extend to the end of the screen, so we use horizontalLength
and vertialLength
for drawing to the points. The pen color is changed to Dark Khaki when we increment through 5 lines. After we loop through the entire vertical and horizontal increments, we add the final visual to the DrawingCanvas
.
Creating an Actor is a pretty straightforward process in this application. The application follows these steps to create and draw an Actor:
- Click the Create Actor menu item.
- Click on the
DrawingCanvas
screen.
DrawActor
is invoked and a visual is added to the DrawingCanvas
.
Here is the Create Actor menu item XAML:
<ToolBar Height="25"
Margin="0,18,2,0"
Name="toolBarMain" VerticalAlignment="Top"
Grid.Column="1">
<Button Name="btnCreateActor" Content="Create Actor" />
</ToolBar>
Set Actor Tool:
private void btnCreateActor_Click(object sender, EventArgs e)
{
Engine.Draw.CurrentTool = AIDraw.DrawingTools.Actor;
}
Invoke DrawActor
to add the Actor to the DrawingCanvas
visuals:
public void DrawActor(){
visual = new DrawingVisual();
using (DrawingContext dc = visual.RenderOpen())
{
dc.DrawEllipse(drawingBrush, drawingPen,
fromMousePoint, 4, 4);
}
drawScreen.AddVisual(visual, true);
}
At the end of this process, your actor will be added to the visuals of the DrawingCanvas
and added to the Screen
.
The square drawing was the most complicated out of the two because of the animation effect of dragging the edge of the square to any size we feel that is needed, all the while keeping the square shape. This drawing also uses the temporary visuals collection to get its animation effect. The process that is followed when creating a square:
- Click the Create Square menu item.
- Click and drag your mouse on the Screen to draw the square.
- Exit the drawing process and have the Square visual added by letting go of the left mouse button.
Here is the CreateSquare menu item in XAML:
<ToolBar Height="25"
Margin="0,18,2,0"
Name="toolBarMain" VerticalAlignment="Top"
Grid.Column="1">
<Button Name="btnCreateSquare" Content="Create Square" />
</ToolBar>
Set Square Tool:
private void btnCreateSquare_Click(object sender, EventArgs e)<
{
Engine.Draw.CurrentTool = AIDraw.DrawingTools.Square;
}
Invoking DrawSquare
:
public void DrawSquare(Point ToPoint)
{
if (cleanupFirstVisual)
drawScreen.DeleteVisual(visual, true);
visual = new DrawingVisual();
using (DrawingContext dc = visual.RenderOpen())
{
Brush brush = drawingBrush;
dc.DrawRectangle(null, drawingPen, new Rect(fromMousePoint, ToPoint));
}
drawScreen.AddVisual(visual, true);
grabLastVisual = visual;
if (!cleanupFirstVisual)
cleanupFirstVisual = true;
toMousePoint = ToPoint;
}
DrawSquare
is very different compared to DrawActor
. This is because now we have to add temporary visuals so that when we are dragging the square, we delete the previous visual in the collection. If we do not delete the previous visual, we will see countless squares being drawn every time we move the mouse.
While the mouse left button is down and we are moving the mouse (to drag the square size), this event is activated:
private void drawingScreen_MouseMove(object sender, EventArgs e)
{
Point position = MousePosition;
if (drawsquare)
{
DrawSquare(MousePosition);
}
}
Now while this is all happening, we are adding to the DrawingCanvas
temporaryvisual
collection and deleting them periodically. However, now we need an event for when the left mouse button is up, so we can now add the final visual to the main visual collection, and the temporary visual collection is completely cleared:
private void drawingScreen_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
cleanupFirstVisual = false;
if (drawsquare)
{
drawsquare = false;
drawScreen.startTempVisual = false;
drawScreen.DeleteVisual(visual, true);
drawScreen.AddVisual(grabLastVisual, false);
CurrentTool = DrawingTools.Arrow;
}
}
What if we want to cancel our drawing of the square while dragging it? We simply check the right mouse button click event to delete the current visual being drawn:
private void drawingScreen_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
drawScreen.DeleteVisual(visual, true);
}
Now, that was a basic overview of some of the WPF controls in this application.
Using the application (experimenting with IronPython in WPF)
The application presented here is mainly based on experimenting with IronPython in the Windows Presentation Framework. It's set up so you can import an Actor Python script and a Scene Python script. The difference between the two is that the Actor script is initialized once and the Scene script is initialized in every frame. In the game loop, the Scene script is rendered in every frame which invokes the DispatcherTimer
. The DispatcherTimer
allows you to control the intervals of how fast you want your Scene to render, like Frames Per Second.
I've also included some simple samples of Python code in the PythonSamples folder of the zip file. I'll briefly explain the use of these samples.
Drawing Visual sample
The DrawingVisual sample in the DrawingVisual folder is a very easy, straightforward way of drawing using System.Windows.Media.Visuals
and System.Windows.Media.DrawingContexts
in Python.
For starters, let's go into the C# code and see what is needed in order for Python to complete the drawing procedure.
We need to set our DrawingCanvas
class in the IronPython globals:
DrawingCanvas drawScreen;
ScriptEngine.Globals.Add("drawScreen", drawScreen);
And, that is it for the C# code, now onto the Python code.
We need to declare our Actor.py code. This code will be initialized only once, unlike the Scene.py file:
//Actor.py
visual = System.Windows.Media.DrawingVisual()
context = System.Windows.Media.DrawingContext
pen = System.Windows.Media.Pen(System.Windows.Media.Brushes.Purple, 3)
brush = System.Windows.Media.Brush
brush = System.Windows.Media.Brushes.Blue
context = visual.RenderOpen()
context.DrawRectangle(brush, pen, System.Windows.Rect(Point(5,50), Point(400,400)))
context.Close()
drawScreen.AddVisual(visual, False)
The pens and brushes are simple properties provided by the System.Windows.Media
namespace. The visual and context are the defining parts of drawing an object into our DrawingCanvas
. After our declarations, we start visual.RenderOpen()
to say we are going to begin creating our visual. We then use context.DrawRectangle
to initialize our new Rectangle
by inputting our properties that were declared. After the DrawRectangle
declaration, we close the context, so we can stop drawing the new visual. The visual is then added to our drawScreen
, which is the DrawingCanvas
of our Screen. We also set false
as our second parameter in drawScreen.AddVisual
because we do not want the visual to be drawn as a temporary visual.
The Scene.py is left blank, because we do not need the visual to be constantly rendering in the game loop in order to see it on screen.
Now, to see the drawing visual in action. Start up the application and click the Create Actor toolbar item, and select where you want your Actor to be created. Then, click on the Script menu item and select Import. Select Scene.py and Actor.py from the PythonSamples/DrawingVisuals folder. After doing so, go into the TreeView, and under Scene, you should see the Script node, and under that should be a combo box. Select Scene.py in the combo box, and do the same for the Actor with Actor.py. After the Scene and Actor have a script selected, click Play in the toolbar, and you should see a box drawn.
Input sample
The Input folder of PythonSamples includes an example of adding key input communication between IronPython and C#. In order to pass a key event to IronPython, I will have to first create a Key
variable in the IronPython globals:
Key input = new Key();
ScriptEngine.Globals.Add("key", input);
After that has been set, we need to create a key event in the MainWindow
to set the key variable in the ScriptEngine
whenever a key is down:
private void MainWnd_KeyDown(object sender, KeyEventArgs e)
{
AIEngine.ScriptEngine.Globals["key"] = e.Key;
}
Also, we need to keep track of whenever a key is up, so we know when to send a null key:
private void MainWnd_KeyUp(object sender, KeyEventArgs e)
{
AIEngine.ScriptEngine.Globals["key"] = null;
}
When you run the application, click Create Actor and create an Actor on the screen.
Now, since we have all of your C# code updating the key
variable inside the ScriptEngine
, all that's left is the Python code.
First off, let's declare our movement variable for our Actor in our Actor.py file:
#Actor.py
actorPosX = 250
actorPosY = 250
These variables will be the default position of our Actor.
Lets create our Scene.py which will contain the key input checks. All we have to have is an if
statement check for each key we want, and then increment the Actor's position if the key is clicked:
#Scene.py
if (key == System.Windows.Input.Key.W):
actorPosY -= 1
if (key == System.Windows.Input.Key.S):
actorPosY += 1
if (key == System.Windows.Input.Key.D):
actorPosX += 1
if (key == System.Windows.Input.Key.A):
actorPosX -= 1
Actor9.ActorPosition = System.Windows.Point(actorPosX, actorPosY)
The Actor9.ActorPosition
variable is a property in AIEditor.Core.AIActor
that sets the Actor's position and redraws the visual to reflect the new position. As you can see, the Actor9
variable was not declared in the AIActor
class or set at all. When we create an Actor in our scene, our Scene TreeView
updates with the newly created Actor name, this name will be like Actor9, Actor14, etc. All of these Actors are already added to ScriptEngine.Globals
through our normal Actor creation process. So, you may need to update the Actor9 name according to what your actor is named in your application.
You also might be wondering how we were able to use some of these System.Windows
namespaces in our Python script. There is a function in PythonEngine that allows you to load assemblies:
PythonEngine.LoadAssembly(Assembly assembly);
This allowed us to load System.Windows.Point
:
ScriptEngine.LoadAssembly(Assembly.Load("WindowsBase"));
Adding assemblies to your Python script is a huge benefit that will allow you create very well structured IronPython applications.
There is also an import namespace function:
PythonEngine.Import(string namespace)
Now back to the main topic. When you run the application, you should create an actor first from the Create Actor menu item, and then you should import the Actor.py and Scene.py scripts from the PythonSamples folder by clicking the Script menu item and then Import. After both are imported, you should then go to your Actors node, and under it, there should be a TreeViewItem
with a header much like Actor# (# being the number that was assigned to the actor). Expand the nodes until you get to the combo box for Script of the Actor#, then select Actor.py. After doing so, you must also select the Scene.py script in the Script node right below the Scene parent node. After all the Python files are selected, click the Play menu item. This will begin the game loop and compile the script. It will also pop up a message box if you have an error within your script. Now, clicking any of the W, S, A, D keys on your keyboard, you should see your Actor move through the Scene.
To see the drawing visual in action, start up the application and click the Create Actor toolbar item and select where you want your Actor to be created. Then, click on the Script menu item and select Import. Select Scene.py and Actor.py from the PythonSamples/DrawingVisuals folder. After doing so, go into the TreeView and under Scene, you should see the Script node, and under that should be a combo box. Select Scene.py in the combobox and do the same for the Actor with Actor.py. After the Scene and Actor have a script selected, click Play in the toolbar, and you should see a box drawn.