First experience writing plugins for Autocad in C #

Background


I am a novice developer, a “school” level of C ++ knowledge, a little (2 years) programming experience in C #, zero experience in AutoCAD.
Recently I was asked to change LISP programs for AutoCAD, designed to create / change boundary plans and prepare the corresponding MS Word / documents XML - fix bugs and add new functionality.
Since the readability of programs in Lisp (at least for me) leaves much to be desired, I decided to rewrite this in a more understandable language.
Because I didn’t need milliseconds of speed increase, I skipped C ++ and settled on C #



I am writing this article to:


1. Put in my head on the shelves that I learned about Autocad.
2. To help those who, like me, get through a very small amount of documentation.
3. Get in the comments information like "you are doing it wrong, it’s easier and better to do so ..."

Beginning of work. Create a plugin.


We create a C # project using the ClassLibrary template. We add
links to the managed Autocad API libraries that are in the program folder.
In my case, this is:
C: \ Program Files \ AutoCAD 2007 \ acdbmgd.dll
C: \ Program Files \ AutoCAD 2007 \ acmgd.dll

Create a class that does something:
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
namespace AutocadPlugin
{
	public class test : IExtensionApplication
	{
		[CommandMethod("hello")]
		public void Helloworld()
		{
			var editor = Application.DocumentManager.MdiActiveDocument.Editor;
			editor.WriteMessage("Привет из Autocad плагина");
		}
		public void Initialize()
		{
			var editor = Application.DocumentManager.MdiActiveDocument.Editor;
			editor.WriteMessage("Инициализация плагина.."+Environment.NewLine);
		}
		public void Terminate()
		{
		}
	}
}


Inheritance from IExtensionApplication is optional, Autocad will automatically pick up all public classes in libraries, but, as I was told, it will be faster. Plus you can control the Initialize / Terminate plugin.

We compile, run AutoCAD, load the plug-in with the netload command (the managed dll selection window opens)
Now when we enter the hello command, we will get the expected response.

Autocad application structure:


What we see on the screen, graphical objects inherited from Entity
In addition to visible, there are invisible information objects - Layers , Line Types , Dimension styles , Table Styles , etc.
All this is stored in Database Table Records , in TYPETable type repositories and TYPETableRecord type classes .

Object Identifiers


  • ObjectId , also known as EName (entity name). The number created when the picture is opened.
    Usage - identification of an object in the database during one session, the object is requested from the database by its ObjectId.
    It can change between different openings, it is better not to use it to save links to objects
  • Handle - a number that does not change between different openings of a document; it is convenient to use it to preserve relationships between objects when saving a file.

Read more: Handles are persistent between AutoCAD sessions, so they are the best way of accessing objects if you need to export drawing information to an external file which might later need to be used to update the drawing. The ObjectId of an object in a database exists only while the database is loaded into memory. Once the database is closed, the Object Ids assigned to an object no longer exist and maybe different the next time the database is opened.

Work with the database


Typically, working with the database is done using a transaction. Objects are requested from the database, modified, and commit transactions are saved back.
During the transaction, the object is requested from the database in one of the 3 ForRead, ForWrite, ForNotify modes.
The purpose of the first two is obvious, the third is somehow used for the event mechanism, which I have not yet crossed with.
In ForWrite mode, autocad creates additional objects that allow you to undo changes in the transaction.
If you need to change an object opened as "ForRead", its UpgradeOpen () method is called.
If you call this method on an object already open in change mode, the method will throw an exception.

An example of obtaining a Polyline object by its ObjectId
// UPDATE: Следует аккуратно работать с полилинией, полученной таким способом. При попытке изменения некоторых частей, например при вызове функции UpgradeOpen вылетит исключение, т.к. транзакция уже закрыта
public static Polyline GetPolylineByEname(ObjectId ename)
{
	if (ename == ObjectId.Null) return null;
	Polyline polyline = null;
	var db = Application.DocumentManager.MdiActiveDocument.Database;
	using (var transaction = db.TransactionManager.StartTransaction())
	{
		return transaction.GetObject(ename, OpenMode.ForRead) as Polyline;
	}
}


Transactions can be nested; when a top-level transaction is canceled, all children are canceled.
In the beginning, for a long time I could not make out where the error was - the changes in the picture were not saved. As it turned out, I forgot to close the upper transaction.
Illustration
public void OwnerTransFunction()
{
	var db = Application.DocumentManager.MdiActiveDocument.Database;
	using (var transaction = db.TransactionManager.StartTransaction())
	{
		// много кода
		ChangePolylineInChildTransation()
	}
}
public void ChangePolyline()
{
	var db = Application.DocumentManager.MdiActiveDocument.Database;
	using (var transaction = db.TransactionManager.StartTransaction())
	{
		// получение полилинии
		// ее измененние
		transaction.Commit(); // на этом этапе данные сохранены в базу, но после завершения внешней транзации без Commit'a изменения будут отменены
	}
}


UPDATE: As suggested in the comments, it is preferable to always call transaction.Commit (), except when you need to cancel the transaction. If the transaction is not committed, transaction.Abort () is automatically called, which incurs additional costs.

Dictionaries


I used dictionaries to save my data in DWG, so as not to create unnecessary files.
I came across two types of dictionaries in the picture - NamedObjectDictionary and ExtensionDictionary The
data in the dictionaries are stored in records (Record), which in turn store typed values.
Addresses data on text keys.

NamedObjectDictionary - global drawing dictionary. Automatically generated when a document is created.
I used it to store links to the main objects I use.

ExtensionDictionary - a dictionary, its own for each object, it must be created manually.
You can verify its existence by comparing the entity.ExtensionDictionary field with ObjectId.Null

Example of writing and getting a string value from ExtensionDictionary
public static void SetExtDictionaryValueString(ObjectId ename, string key, string value)
{
	if (ename == ObjectId.Null) throw new ArgumentNullException("ename");
	if (String.IsNullOrEmpty(key)) throw new ArgumentNullException("key");
	var doc = Application.DocumentManager.MdiActiveDocument;
	using (var transaction = doc.Database.TransactionManager.StartTransaction())
	{
		var entity = transaction.GetObject(ename, OpenMode.ForWrite);
		if (entity == null)
			throw new DataException("Ошибка при записи текстового значения в ExtensionDictionary: entity с ObjectId=" + ename + " не найдена");
		//Получение или создание словаря extDictionary
		var extensionDictionaryId = entity.ExtensionDictionary;
		if (extensionDictionaryId == ObjectId.Null)
		{
			entity.CreateExtensionDictionary();
			extensionDictionaryId = entity.ExtensionDictionary;
		}
		var extDictionary = (DBDictionary) transaction.GetObject(extensionDictionaryId, OpenMode.ForWrite);
		// Запись значения в словарь
		if (String.IsNullOrEmpty(value))
		{
			if (extDictionary.Contains(key))
				extDictionary.Remove(key);
			return;
		}
		var xrec = new Xrecord();
		xrec.Data = new ResultBuffer(new TypedValue((int) DxfCode.ExtendedDataAsciiString, value));
		extDictionary.SetAt(key, xrec);
		transaction.AddNewlyCreatedDBObject(xrec, true);
		Debug.WriteLine(entity.Handle+"['" + key + "'] = '" + value + "'");
		transaction.Commit();
	}
}
public static string GetExtDictionaryValueString(ObjectId ename, string key)
{
	if (ename == ObjectId.Null) throw new ArgumentNullException("ename");
	if (String.IsNullOrEmpty(key)) throw new ArgumentNullException("key");
	var doc = Application.DocumentManager.MdiActiveDocument;
	using (var transaction = doc.Database.TransactionManager.StartTransaction())
	{
		var entity = transaction.GetObject(ename, OpenMode.ForRead);
		if (entity == null)
			throw new DataException("Ошибка при чтении текстового значения из ExtensionDictionary: полилиния с ObjectId=" + ename + " не найдена");
		var extDictionaryId = entity.ExtensionDictionary;
		if (extDictionaryId == ObjectId.Null)
			throw new DataException("Ошибка при чтении текстового значения из ExtensionDictionary: словарь не найден");
		var extDic = (DBDictionary)transaction.GetObject(extDictionaryId, OpenMode.ForRead);
		if (!extDic.Contains(key))
			return null;
		var myDataId = extDic.GetAt(key);
		var readBack = (Xrecord)transaction.GetObject(myDataId, OpenMode.ForRead);
		return (string)readBack.Data.AsArray()[0].Value;
	} 
}


The work with the global dictionary is almost the same, only the DBDictionary object is obtained like this:
var dictionary = (DBDictionary) transaction.GetObject(db.NamedObjectsDictionaryId, OpenMode.ForWrite);


What else have I encountered


1. Plugin startup
After a couple of dozens of times entering netload for a five-second check of the plugin, I got tired of it and began to look for how to simplify the procedure.
During the search, I came across an article telling about the autoload of .NET plugins. In short - you need to add the key to the registry
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Autodesk\AutoCAD\R17.0\ACAD-5001:419\Applications\GeocomplexPlugin]
"LOADCTRLS"=dword:00000002
"MANAGED"=dword:00000001
"LOADER"="C:\\GeoComplexAutocadPlugin\\AutocadGeocomplexPlugin.dll"

Explanations:
R17.0 - Autocad 2007
419 for the Russian version
409 for the English version
GeocomplexPlugin - the created section
LOADCTRLS = 2 - load at startup. Certain keys can be used to launch on demand, the plug-in is loaded when one of its
LOADER commands is entered - the path to the plug-in


2. Debug
Because in the plugin, you cannot run step-by-step debugging in VS, I had to display debugging messages at some stages.
Since the editor of AutoCAD is inconvenient to use for these purposes, I output messages using the standard Debug.WriteMessage ()
tools. The debug will be displayed only when compiling in debug mode, the output can be viewed by running the DebugView


UPDATE program : Solved the step-by-step debugging problem: I
defeated this problem this way:
In the autocad configuration file, acad.exe.config installed the runtime on v4.0:


Changed the plug-in runtime to 4.0 client profile
And commented on the AllowPartiallyTrustedCallers attribute on the assembly settings AssemblyInfo.cs



3. Sending a command in Editor
Some actions are not implemented in the Autocad .NET API, or they are much easier to do with the command line.
The easiest way to execute a command is to execute a function, an example for ZoomExtents:
var doc = Application.DocumentManager.MdiActiveDocument;
doc.SendStringToExecute("_zoom _e ",false,false,true);

In AutoCAD, a space is equivalent to Enter, so if you send a command without a trailing space, as a result, _e will be entered in editor'e and it will wait for further input.

However, this method cannot always be used. The principle of the SendStringToExecute command is that the command is sent only after the completion of the function called by the command. Therefore, if you first call this command, and then, for example, prompt the user to select an object in the figure, two lines of "_zoom", "_e" will be sent to the selection function, which it will perceive as incorrect objects.
We have to look for analogues that run immediately. In this case:
object acad = Application.AcadApplication;
acad.GetType().InvokeMember("ZoomExtents", BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod, null, acad, null);



4. User selection of objects
The editor’s Get * functions are used to select objects.
For example, selecting several objects - GetSelection, selecting one GetEntity object ...
var editor = Application.DocumentManager.MdiActiveDocument.Editor;
var promtResult = editor.GetEntity("Выберите участок");
editor.WriteMessage(Environment.NewLine);
if (promtResult.Status == PromptStatus.OK)
{
	editor.WriteMessage("Selected Object's ID: " + promtResult.ObjectId+Enviropment.NewLine);
}



Getting the path to the document folder
A search on the Internet produced two ways to access the full file name:
MdiActiveDocument.Database.Filename
MdiActiveDocument.Name
At first glance, they are the same, but
On the topic - I do not recommend using the Database.Filename property. After autosave, it does not indicate the file itself, but an autosave copy - unlike Document.Name

I got the folder path using standard .NET tools:
Path.GetDirectoryName (Application.DocumentManager.MdiActiveDocument.Name);
if the returned string is empty, then the document is created but not saved


And some more useful code snippets:


Changing the coordinates of a polyline (extension method)
// Изменение координат полилинии (метод расширения):
public static void UpdatePoints(this Polyline polyline, List newPoints)
{
	if (polyline == null)
		throw new ArgumentNullException("polyline");
	if (newPoints.Count < 2)
		throw new ArgumentException("Попытка установить для полилинии одну точку");
	using (var transaction = Application.DocumentManager.MdiActiveDocument.Database.TransactionManager.StartTransaction())
	{
		// открываем новый объект полилинии, привязывая его к текущей транзации
		var pline = transaction.GetObject(polyline.ObjectId,OpenMode.ForWrite) as Polyline;
		if (pline == null)
			throw new DataException("Ошибка! Полилиния не найдена в базе");
		var bulge = pline.GetBulgeAt(0);
		var start_width = pline.GetStartWidthAt(0);
		var end_width = pline.GetEndWidthAt(0);
		var prevPointsCount = pline.NumberOfVertices;
		// добавление новых точек
		// нельзя сначала удалить все точки, а потом добавить новые, т.к. Autocad не позволит сделать линию с 0 или 1 точкой
		// сначала добавляем новые
		for (int i = prevPointsCount; i < prevPointsCount + newPoints.Count; i++)
			pline.AddVertexAt(i, newPoints[i - prevPointsCount], bulge, start_width, end_width);
		// потом удаляем старые
		for (int i = prevPointsCount - 1; i >= 0; i--)
			pline.RemoveVertexAt(i);
		transaction.Commit();
	}
}



Getting the coordinates of the polyline (extension method):
// Получение координат полилинии (метод расширения):
public static Point2d[] GetPoints(this Polyline polyline)
{
	if (polyline == null)
	{
		Debug.WriteLine("Непредвиденная ошибка! Попытка вызвать метод для несуществующей полилинии");
		return null;
	}
	if (polyline.NumberOfVertices == 0)
		return null;
	var points = new List();
	for (int i = 0; i < polyline.NumberOfVertices; i++)
		points.Add(polyline.GetPoint2dAt(i));
	return points.ToArray();
}


useful links


1. AutoCAD .NET Developer's Guide
English mini-reference, describes the essence of the Autocad device with sample code for VB.NET, C # .NET, VBA
2. Through the interface
Kean'a Walmsley's blog, a lot of Howto. There are examples in C #, C # + COM, VB.NET, C ++. I can’t search there, but half of my Google requests “how to do this ..” led to this site
3. caduser.ru, sub-forum “.NET”
Communication with Russian- speaking people who are well versed in the topic. Many times helped to understand difficult places

Also popular now: