Начало работы в Autocad с помощью C#

в 8:38, , рубрики: .net, api, autocad, plugin, метки: , , , ,

Предыстория

Я начинающий разработчик, «школьный» уровень знания С++, небольшой (2 года) опыт программирования на С#, нулевой опыт работы в автокаде
Недавно попросили изменить LISP программки автокада, предназначенные для создания/изменения межевых планов и подготовки соответствующих документов MS Word / XML — пофиксить баги и добавить новый функционал.
Поскольку читабельность программ на Лиспе (по крайней мере для меня) оставляет желать лучшего, я решил переписать это на более понятный язык.
Т.к. мне не нужны были миллисекунды прироста скорости, я пропустил С++ и остановился на C#

Под катом то, что я узнал за 2 недели переписывания программ

Пишу эту статью, чтобы:

1. Разложить у себя в голове по полочкам, что я узнал об Autocad.
2. Помочь тем, кто так же как я продирается через очень малое количество документации.
3. Получить в комментариях информацию вида «это ты делаешь неправильно, проще и лучше будет сделать так...»

Начало работы. Создание плагина.

Создаем проект C# по шаблону ClassLibrary
Добавляем ссылки на managed библиотеки API Autocad'a, которые лежат в папке программы.
В моем случае это:
C:Program FilesAutoCAD 2007acdbmgd.dll
C:Program FilesAutoCAD 2007acmgd.dll

Создаем класс, который что-то делает:

using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;

namespace AutocadPlugin
{
	public class test
	{
		[CommandMethod("hello")]
		public void Helloworld()
		{
			var editor = Application.DocumentManager.MdiActiveDocument.Editor;
			editor.WriteMessage("Привет из Autocad плагина");
		}

	}
}

Компилируем, запускаем автокад, загружаем плагин командой netload (открывается окно выбора managed dll)
Теперь при вводе команды hello мы будем получать ожидаемый ответ.

Структура Autocad приложения:

То, что мы видим на экране, графические объекты, унаследованные от Entity
Кроме видимых, есть невидимые информационные объекты — Слои, Типы линий, Стили размерности (Dimension styles), Стили таблиц и т.д.
Все это хранится в Database Table Records, в хранилищах вида TYPETable и классах вида TYPETableRecord.

Идентификаторы объектов

  • ObjectId, известный также как EName (entity name). Число, создаваемое при открытии рисунка.
    Использование — идентификация объекта в базе в течении одной сессии, объект запрашивается из базы по его ObjectId
    Может меняться между разными открытиями, лучше не использовать для сохранения ссылок на объекты
  • Handle
    Число, неизменное между разными открытиями документа, удобно использовать для сохранения связей между объектами при сохранении файла.

Подробнее: 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.

Работа с базой данных

Обычно работа с базой идет с помощью транзакции. Объекты запрашиваются из базы данных, изменяются и commit'ом транзакции сохраняются обратно
Во время транзакции объект запрашивается из БД в одном из 3х режимов ForRead, ForWrite, ForNotify.
Назначение первых двух очевидно, третий как-то используется для механизма событий, с чем я пока не пересекался
В режиме ForWrite автокадом создаются дополнительные объекты, позволяющие отменить изменения в транзации.
Если нужно изменить объект, открытый как «ForRead», вызывается его метод UpgradeOpen().
Если вызвать этот метод на объекте, уже открытом в режиме изменения, метод выдаст исключение.

Пример получения объекта Polyline по его ObjectId:

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;
	}
}

Транзакции могут быть вложенными, при отмене верхнеуровневой транзации отменяются и все дочерние.
В начале, долго не мог разобрать где ошибка — не сохранялись изменения в рисунке. Как оказалось забыл закрыть верхнюю транзацию:

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 изменения будут отменены
	}
}

Словари

Словари я использовал для сохранения своих данных в DWG, чтобы не создавать лишних файлов
Я столкнулся с двумя видами словарей в рисунке — NamedObjectDictionary и ExtensionDictionary
Данные в словарях хранятся в записях (Record), которые в свою очередь хранят типизированные значения.
Адресуются данные по текстовым ключам.

NamedObjectDictionary — глобальный словарь рисунка. Создается автоматически при создании документа.
Его я использовал для хранения ссылок на главные используемые мной объекты.

ExtensionDictionary — словарь, свой для каждого объекта, его нужно создавать вручную.
Проверить его существование можно сравнив поле entity.ExtensionDictionary c ObjectId.Null

Пример записи и получения строкового значения из 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;
	} 
}

Работа с глобальным словарем почти такая же, только объект DBDictionary получается так:
var dictionary = (DBDictionary) transaction.GetObject(db.NamedObjectsDictionaryId, OpenMode.ForWrite);

С чем я еще столкнулся

1. Автозагрузка плагина

После пары десятков раз ввода netload для пятисекундной проверки плагина мне это надоело и я стал искать как упросить процедуру.
В ходе поиска наткнулся на статью, рассказывающую об автозагрузке .NET плагинов. Вкратце — нужно добавить ключ в реестр

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINESOFTWAREAutodeskAutoCADR17.0ACAD-5001:419ApplicationsGeocomplexPlugin]
"LOADCTRLS"=dword:00000002
"MANAGED"=dword:00000001
"LOADER"="C:\GeoComplexAutocadPlugin\AutocadGeocomplexPlugin.dll"

Пояснения:
R17.0 — Autocad 2007
419 для русской версии
409 для английской версии
GeocomplexPlugin — создаваемый раздел
LOADCTRLS = 2 — загружать при старте автокада. Определенными ключами можно сделать запуск «по требованию», плагин загружается, когда вводится одна из его команд
LOADER — путь к плагину

2. Вывод Debug сообщений

Т.к. в плагине нельзя запустить пошаговую отладку в VS, мне пришлось на некоторых этапах выводить отладочные сообщения.
Поскольку editor автокада для этих целей использовать неудобно, сообщения я выводил стандартными средствами Debug.WriteMessage()
Дебаг будет выводиться только при компилировании в режиме отладки, выводимые данные можно посмотреть, запустив программу DebugView

3. Отправка команды в Editor

Некоторые действия не реализованы в Autocad .NET API, или их намного проще сделать командной строкой.
Самый простой способ выполнить команду — выполнить функцию, пример для ZoomExtents:

var doc = Application.DocumentManager.MdiActiveDocument;
doc.SendStringToExecute("_zoom _e ",false,false,true);

В автокаде пробел эквивалентен Enter, поэтому если послать команду без завершающего пробела, в результате в editor'e будет введено _e и он будет ожидать дальнейшего ввода

Однако этот способ не всегда можно использовать. Принцип работы команды SendStringToExecute такой, что команды посылается только после завершения работы фунции, вызыванной командой. Поэтому если вы сначала вызовите эту команду, а потом, например, предложите пользователю выбрать объект на рисунке, фунции выбора будет послано две строки "_zoom", "_e", которые она воспримет как неправильные объекты.
Приходится искать аналоги, выполняющиеся сразу же. В данном случае:

object acad = Application.AcadApplication;
acad.GetType().InvokeMember("ZoomExtents", BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod, null, acad, null);

4. Выбор объектов пользователем

Для выбора объектов используются функции editor'a Get*
Например, выбор нескольких объектов — GetSelection, выбор одного объекта GetEntity…

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);
}

Получение пути к папке документа

Поиск в интернете выдал два способа доступа к полному имени файла:
MdiActiveDocument.Database.Filename
MdiActiveDocument.Name
На первый взгляд они одинаковы, но

По теме — свойством Database.Filename пользоваться не советую. После автосохранения оно указывает не на сам файл, а на автосохранённую копию — в отличие от Document.Name

Путь к папке я получил стандартными средствами .NET:
Path.GetDirectoryName(Application.DocumentManager.MdiActiveDocument.Name);
если возвращаемая строка пустая, значит документ создан, но не сохранен

И еще полезные отрывки кода:

// Изменение координат полилинии (метод расширения):
public static void UpdatePoints(this Polyline polyline, List<Point2d> 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();
	}
}

// Получение координат полилинии (метод расширения):
public static Point2d[] GetPoints(this Polyline polyline)
{
	if (polyline == null)
	{
		Debug.WriteLine("Непредвиденная ошибка! Попытка вызвать метод для несуществующей полилинии");
		return null;
	}
	if (polyline.NumberOfVertices == 0)
		return null;

	var points = new List<Point2d>();
	for (int i = 0; i < polyline.NumberOfVertices; i++)
		points.Add(polyline.GetPoint2dAt(i));
	
	return points.ToArray();
}

Полезные ссылки

1. AutoCAD .NET Developer's Guide
Английский мини-референс, рассказывается суть устройства Autocad с примерами кода на VB.NET, C#.NET, VBA
2. Through the interface
Блог Kean'a Walmsley, очень много Howto. Встречаются примеры на C#, C#+COM, VB.NET, C++. У меня там искать не получается, но половина моих запросов в гугле «как сделать так..» вела на этот сайт
3. caduser.ru, подфорум ".NET"
Общение с русскоговорящими людьми, хорошо разбирающимися в теме. Много раз помогали разобраться в трудных местах

Автор: Namolem


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js