This post was heavily inspired by the code presented by my old friend Albert Szilvasy during his excellent AU class on using .NET 4.0 with AutoCAD.
Albert took a different approach to the one I’ve previously adopted (which turns out also to have been suggested by Albert, when I look back at my original post), and created a palette to host IronPython code inside AutoCAD, enabling the ability to enter code directly in AutoCAD rather than relying on an external text file.
In this post we’ll take Albert’s technique and implement a command-line interface for querying and executing IronPython script. This approach could also be adapted to work with other DLT languages such as IronRuby, of course.
Here’s the updated C# code which now not only implements PYLOAD functionality, but also a PYEXEC command:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Ribbon;
using IronPython.Hosting;
using Microsoft.Scripting.Hosting;
using System.Collections.Generic;
using System;
namespace PythonLoader
{
public class App : IExtensionApplication
{
public void Initialize()
{
DemandLoading.RegistryUpdate.RegisterForDemandLoading();
CommandsAndFunctions.InitializeRuntime();
}
public void Terminate()
{
}
}
public class CommandsAndFunctions
{
// Some state to allow us to incrementally build and
// execute Python code
private static ScriptEngine _engine = null;
private static ScriptScope _scope = null;
// Initialization function, called on application load
internal static void InitializeRuntime()
{
// Create our base setup for the hosted runtime
ScriptRuntimeSetup setup = new ScriptRuntimeSetup();
// Add our language: the version number may vary
// depending on the specific version of IronPython
setup.LanguageSetups.Add(
new LanguageSetup(
"IronPython.Runtime.PythonContext, IronPython, " +
"Version=2.6.0.20, Culture=neutral",
"IronPython 2.6",
new string[] { "python" }, new string[] { ".py" }
)
);
// Create the runtime and load the AcMgd.dll and
// AcDbMgd.dll assemblies
ScriptRuntime runtime = new ScriptRuntime(setup);
runtime.LoadAssembly(typeof(DBObject).Assembly);
runtime.LoadAssembly(typeof(Application).Assembly);
// Now we set our state
_engine = runtime.GetEngine("python");
_scope = _engine.CreateScope();
// Expose some useful runtime variables
_scope.SetVariable(
"ThisDrawing",
Application.DocumentManager.MdiActiveDocument.AcadDocument
);
_scope.SetVariable(
"ThisDocument",
Application.DocumentManager.MdiActiveDocument
);
_scope.SetVariable(
"Ribbon",
RibbonServices.RibbonPaletteSet.RibbonControl
);
}
[CommandMethod("PYEXEC")]
public static void PythonExecute()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
// Stop if we have a state problem (shouldn't happen)
if (_engine == null || _scope == null)
{
ed.WriteMessage("\nRuntime needs initializing.");
return;
}
// Introduce the environment
ed.WriteMessage("\nInteractive Python scripting...");
string sep = "".PadLeft(80, '_') + "\n";
bool cont = true;
string text = "";
// Loop until cancelled or executed
while (cont)
{
// Echo the current unexecuted buffer contents
if (text != "")
ed.WriteMessage(sep + "\nScript buffer:\n" + text + sep);
// Get a line of Python
PromptStringOptions pso =
new PromptStringOptions("\n>>");
pso.AllowSpaces = true;
PromptResult pr = ed.GetString(pso);
if (pr.Status == PromptStatus.OK)
{
// If a blank line, execute the buffer
if (pr.StringResult == "")
{
try
{
_engine.Execute(text, _scope);
text = "";
cont = false;
}
catch (System.Exception ex)
{
ed.WriteMessage("\nError: " + ex.Message);
cont = false;
}
}
else
{
// Otherwise append the string to the buffer
text += pr.StringResult + "\n";
}
}
else
{
// If anything else but OK was received then
// stop looping
cont = false;
}
}
}
[CommandMethod("-PYLOAD")]
public static void PythonLoadCmdLine()
{
PythonLoad(true);
}
[CommandMethod("PYLOAD")]
public static void PythonLoadUI()
{
PythonLoad(false);
}
public static void PythonLoad(bool useCmdLine)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
short fd =
(short)Application.GetSystemVariable("FILEDIA");
// As the user to select a .py file
PromptOpenFileOptions pfo =
new PromptOpenFileOptions(
"Select Python script to load"
);
pfo.Filter = "Python script (*.py)|*.py";
pfo.PreferCommandLine =
(useCmdLine || fd == 0);
PromptFileNameResult pr =
ed.GetFileNameForOpen(pfo);
// And then try to load and execute it
if (pr.Status == PromptStatus.OK)
ExecutePythonScript(pr.StringResult);
}
[LispFunction("PYLOAD")]
public ResultBuffer PythonLoadLISP(ResultBuffer rb)
{
const int RTSTR = 5005;
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
if (rb == null)
{
ed.WriteMessage("\nError: too few arguments\n");
}
else
{
// We're only really interested in the first argument
Array args = rb.AsArray();
TypedValue tv = (TypedValue)args.GetValue(0);
// Which should be the filename of our script
if (tv != null && tv.TypeCode == RTSTR)
{
// If we manage to execute it, let's return the
// filename as the result of the function
// (just as (arxload) does)
bool success =
ExecutePythonScript(Convert.ToString(tv.Value));
return
(success ?
new ResultBuffer(
new TypedValue(RTSTR, tv.Value)
)
: null);
}
}
return null;
}
private static bool ExecutePythonScript(string file)
{
// If the file exists, let's load and execute it
// (we could/should probably add some more robust
// exception handling here)
bool ret = System.IO.File.Exists(file);
if (ret)
{
try
{
Dictionary<string, object> options =
new Dictionary<string, object>();
options["Debug"] = true;
ScriptEngine engine = Python.CreateEngine(options);
engine.ExecuteFile(file);
}
catch (System.Exception ex)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
ed.WriteMessage(
"\nProblem executing script: {0}", ex.Message
);
}
}
return ret;
}
}
}
As we’re also making use of the Ribbon component, you’ll probably have to add some additional assembly references, such as to AcWindows.dll, AdWindows.dll, PresentationCore, PresentationFramework and WindowsBase (as well as the standard references to AcDbMgd.dll, AcMgd.dll, IronPython.dll, IronPython.Modules.dll, Microsoft.Scripting.dll and Microsoft.Scripting.Code.dll (phew! :-)).
When we execute the PYEXEC command, we get presented with a message on the AutoCAD command-line:
Command: PYEXEC
Interactive Python scripting...
>>:
This is where we can type (or paste) in lines of IronPython code. After each line is entered the unexecuted code in the buffer is echoed to the command-line. To execute the buffer, simply enter return on an empty line.
So if we paste in some code to minimise all the ribbon panels:
for panel in Ribbon.Tabs[0].Panels :
panel.IsCollapsed = True
We see this:
Command: PYEXEC
Interactive Python scripting...
>>: for panel in Ribbon.Tabs[0].Panels:
________________________________________________________________________________
Script buffer:
for panel in Ribbon.Tabs[0].Panels:
________________________________________________________________________________
>>: panel.IsCollapsed = True
________________________________________________________________________________
Script buffer:
for panel in Ribbon.Tabs[0].Panels:
panel.IsCollapsed = True
________________________________________________________________________________
>>:
When we then enter a blank line (hitting return directly), the code gets executed and the panels on the first ribbon tab are all minimised:
The actual interaction implementation could certainly use some refinement, but this is really about the principle rather than the specific details. This sample should prove of use for people who want to just play around with Python inside AutoCAD, to get a better handle on the possibilities of .NET-related scripting inside the product.