Создание плагинов для AutoCAD с помощью .NET API (часть 6 – поиск и изменение объектов на чертеже)
Это шестая часть цикла про разработку плагинов для AutoCAD. В ней поговорим про поиск объектов на чертеже, а также про их изменение.
Введение
В первой части статьи мы рассмотрим поиск объектов на чертеже, во второй — коротко поговорим про их изменение. Однако прежде чем начать рассмотрение этих вопросов, давайте подготовим каркас тестового примера, который мы будем использовать на протяжении всей статьи.
- на нулевой слой: три линии, полилинию, окружность, текст;
- на слой «layer-1»: две линии, окружность, текст, вхождение блока «block-1»;
- на слой «layer-2»: линию, дугу, окружность, вхождение блока «block-1», вхождение блока «block-2».
Код, реализующий этот план действий, приведен далее. Все выполняемые операции: создание слоев и определений блока, вставка текста, графических примитивов и вхождений блока — мы уже рассматривали в прошлых статьях цикла. Единственное, что еще не рассматривалось, — это задание слоя и цвета для объекта чертежа, но оно занимает буквально одну строку кода и, я надеюсь, сложностей не вызовет.
Итак, создаем проект, выполняем первоначальную настройку (указываем версию .NET, отключаем CopyLocal ) и подключаем уже привычные библиотеки AcMgd и AcDbMgd. Далее помещаем туда функции, создающие объекты для нашего примера.
Код несложный и принципиальных вопросов вызывать не должен. Давайте быстро пробежимся по некоторым нюансам.
Для начала заметим, что создание всех необходимых объектов происходят прямо во время загрузки плагина. Поскольку создаваемые объекты нужны нам для работы примеров и необходимы для работы любой из команд плагина, их вроде бы можно создавать в рамках метода Initialize() .
Однако насколько это правильно и разумно — вопрос очень спорный. Во-первых, создавая что-то автоматически, мы тем самым лишаем пользователя выбора и ставим его перед веселым фактом: привет, чувак, у тебя на чертеже теперь десять новых слоев и сто новых объектов!
Весело будет, но не слишком. Особенно пользователю.
Вторая неприятность состоит в том, что пользователь может случайно или намеренно удалить с чертежа некоторые из созданных объектов. И в этом случае он никак не сможет заново создать эти объекты; для этого ему придется закрыть и заново запустить AutoCAD.
- Никак не помешают пользователю и не создадут излишнюю нагрузку на чертеж.
- Никогда не потребуют повторного вызова (либо же у пользователя должен быть способ этот вызов сделать — вспомните пример из абзаца выше про случайное удаление объектов чертежа).
При заполнении объектами нулевого слоя (функция layer_0_createObjects() ) принадлежность к этому слою явно указывается для каждого создаваемого объекта. Пример:
Это понятно и несложно, однако при работе с большим количеством объектов можно запросто забыть задать слой, что нехорошо.
Альтернативный способ, уменьшающий вероятность подобной ошибки, был кратко упомянут в одной из предыдущих статей, посвященной слоям. Это использование свойства Clayer .
Суть способа состоит в том, что если при создании объекта мы вообще не укажем ему слой, то он будет присвоен автоматически на основе значения свойства Clayer базы данных текущего документа. Просмотреть это значение можно так:
Присваивается значение аналогично:
А вот так можно получить ObjectID слоя, зная его имя:
В нашем примере, чтобы не обращаться каждый раз к таблице слоев, я создал две глобальные переменные — layer_1 и layer_2 , в которые сохраняю ObjectID слоев «layer-1» и «layer-2» при их создании. А позже, в процедурах layer_1_createObjects() и layer_2_createObjects() , я использую следующую конструкцию:
Зачем нужно сохранять текущее значение Clayer и восстанавливать его в конце функции? Ну, одна из причин — чтобы пользователь после работы нашей функции смог продолжить добавлять объекты на тот же слой, что и до работы нашей процедуры. Вторая причина — чтобы самому не путаться и не выяснять потом, почему это в половине запусков объекты добавляются не туда. Если подключить немого воображения, то, наверное, можно придумать и еще причины. Но зачем?
Хотелось бы еще раз сделать акцент на том, что этот цикл статей рассказывает об основах создания плагинов. Для использования в реальном проекте приведенные примеры должны быть доработаны — в частности, необходимо позаботиться о том, что все используемые объекты реально существуют.
В нашем примере такие проверки не проводятся. Посмотрите: я помещаю графические объекты на созданные ранее слои, помещаю на чертеж вхождения созданных при загрузке блоков — но нигде не проверяю, что эти слои и определения блоков существуют. В реальных проектах пренебрегать такими проверками настоятельно не рекомендуется. Особенно это касается случаев, когда между получением ссылки на объект и операцией над объектом могут произойти какие-нибудь действия пользователя.
В разделе, посвященном редактированию объектов чертежа, я опишу еще несколько ошибок, которые также необходимо отслеживать.
Теперь, когда мы обсудили особенности примера, можно наконец-то перейти к поиску объектов на чертеже. Мы разберем два разных подхода: просмотр объектов чертежа через обращение к объекту ModelSpace и получение идентификаторов объектов чертежа с помощью метода Editor.SelectAll() .
Можно искать объекты и другими способами. Но тут я уже ничем не помогу, ибо лично с ними не сталкивался.
Ближе к концу этой статьи будет ма-аленький бонусный нанораздел, где можно будет оценить масштабы извращений блестящих технических решений, которые приходится использовать людям программистам для оптимизации работы с большим количеством объектов.
1 Поиск объектов чертежа с помощью обращения к объекту ModelSpace
1.1 Итерация по всем объектам чертежа
Для начала давайте посмотрим, как можно перебрать в цикле все объекты, имеющиеся на чертеже.
Принцип простой: открываем пространство модели ( ModelSpace ) и получаем ссылки на все объекты внутри него. Затем приводим эти объекты к типу Entity и обрабатываем нужные нам свойства.
Пример почти полностью заимствован отсюда (англ.).
Результат, как можно видеть, полностью совпадает с ожиданиями: у нас 16 объектов, цвета которых чередуются.
Из небольших новшеств: в этом примере мы использовали незнакомое нам свойство WorkingDatabase класса HostApplicationServices . Оно позволяет получить БД документа, который активен (имеет фокус ввода) в настоящий момент. Еще одно новшество — метод GetBlockModelSpaceId() класса SymbolUtilityServices , который позволяет быстро получить ObjectId пространства модели ( ModelSpace ).
Получив ссылку на пространство модели, мы просто пробегаемся по всем его объектам, приводим их к типу Entity (чтобы можно было просматривать их свойства) и выводим в консоль данные о слое, типе и цвете очередного объекта.
1.2 Поиск объектов заданного типа
Давайте попробуем выделить все окружности. Сделаем это на базе предыдущего примера.
Действовать будем так: просмотрим по порядку все объекты чертежа, определим тип каждого объекта; если объект окажется окружностью — выведем информацию о нем в консоль. В прошлой статье нам уже встречалась ссылка (англ.), где описаны пять способов узнать тип объекта. Как всегда, не будем отступать от традиций и используем один из самых простых способов:
Аналогично можно попробовать другие типы. Например, найти все линии:
Найти все аннотации (текстовые элементы):
Все вхождения блоков:
А если вдруг подзабудете, как именно называется тип, соответствующий объекту — можно просто добавить такой объект на чертеж вручную и запустить пример итерации по объектам — он выведет название типа в консоль AutoCAD.
1.3 Поиск объектов с заданными свойствами
Попробуем вместо типа поискать какое-нибудь свойство — например, цвет. Найдем все салатовые объекты.
Это очень просто: строку
и смотрим, что получается.)
Аналогично можно осуществлять поиск и по другим атрибутам — например, по слою:
Сам я глубоко в свойства объектов не залезал. Советую при работе в Visual Studio просто набрать " Entity test; test. " — после этого IntelliSense высветит все доступные программисту свойства и методы. Можно пробежаться по этому списку и ознакомиться со свойствами и методами класса Entity:
Класс Entity задает свойства, характерные для любого объекта чертежа: слой, цвет и т. п. Если требуется осуществить поиск по свойствам, специфичным для какого-то конкретного класса объектов, то необходимо вначале найти все объекты такого типа, затем выполнить приведение к этому типу и просмотреть значение нужного свойства.
Для примера давайте найдем все вхождения блока «block-1». Класс Entity не содержит сведений об имени определения блока; зато эта информация есть в свойстве Name класса BlockReference .
Мы найдем два вхождения блока «block-1».
2 Поиск объектов чертежа с помощью метода Editor.SelectAll()
2.1 Итерация по всем объектам чертежа
Немного истории: упоминание этого метода есть в одной из статей Kean Walmsley (англ.). Кроме того, описание есть на AutoCAD DevBlog (англ.) и форуме Сообщества программистов Autodesk в СНГ (rus).
Если в предыдущем варианте мы просматривали каждый объект в отдельности, то здесь концепция меняется: мы сразу получаем массив идентификаторов всех интересующих нас объектов, при необходимости отфильтровывая нужные.
Начнем, как обычно, с примера итерации по всем объектам.
Мы вызываем метод Editor.SelectAll() . Поскольку никаких фильтров не задано, нам должны вернуться идентификаторы ( ObjectID ) всех объектов на чертеже. Они записываются в переменную типа PromptSelectionResult . На всякий случай нужно убедиться, что метод отработал корректно — для этого мы проверяем статус результата ( PromptSelectionResult.Status ). Если что-то не в порядке, значение этого свойства будет отлично от PromptStatus.OK — в этом случае мы завершаем выполнение функции.
Если же метод Editor.SelectAll() отработал корректно, мы получаем идентификаторы всех объектов, возвращенных этим методом. Для этого мы используем метод PromptSelectionResult.Value.GetObjectIds() . После этого мы просто обрабатываем все объекты в цикле — точь-в-точь как в первом разделе, когда мы обращались к ModelSpace .
Важный момент! Согласно своему описанию (англ.), метод Editor.SelectAll() должен возвращать только объекты на слоях, которые НЕ являются заблокированными (locked) или замороженными (frozen). Однако в этом случае документация, похоже, привирает: Editor.SelectAll() всегда возвращает все объекты на чертеже, вне зависимости от состояний слоев, на которых они находятся. Подробнее почитать про эту забавность можно в блоге Kean Walmsley (англ.), в AutoCAD Devblog (англ.), на форумах сообщества Autodesk (англ.).
В общем, мои выводы: у меня в AutoCAD 2010 этот подход работает. Возможно, он заработает в более новых версиях .NET API. Но что это — ошибка в документации или баг в API, который однажды, быть может, пофиксят, — я сказать не могу.
Краткий итог: аккуратнее с этим.
Другой важный момент. Для использования метода Editor.SelectAll() , очевидно, необходимо иметь объект класса Editor . При работе с документом, который непосредственно открыт в AutoCAD, проблем не возникнет; но использовать данный метод для обработки БД сторонних документов (не открытых в настоящий момент в AutoCAD) не получится (англ.).
2.2 Использование фильтров
Давайте снова попробуем выделить все окружности. Разумеется, можно сделать все аналогично предыдущему разделу: получить идентификаторы всех объектов и смотреть тип каждого объекта — это сработает. Однако при использовании метода Editor.SelectAll() есть возможность действовать по-другому.
Принцип такой: вначале мы задаем фильтр для объектов с помощью класса SelectionFilter , а затем применяем этот фильтр в методе Editor.SelectAll() . В результате у нас остаются только удовлетворяющие условию фильтра объекты.
Работает. Теперь давайте разберемся, как.)
Итак, метод Editor.SelectAll() может принимать на вход объект типа SelectionFilter , который и задает фильтр. Этот фильтр инициализируется с помощью массива объектов типа TypedValue . Конструктор TypedValue принимает на вход два параметра:
Эти значения связаны с форматом DXF (rus). Вот что говорит по этому поводу документация (англ.):
В рассмотренном примере мы указали в качестве typeCode значение 0. Откуда оно взялось и что означает — можно посмотреть в списке (англ.)
Таким образом, коду «0» соответствует тип объекта.
Вместо зубодробительных числовых констант можно использовать любезно предоставленное разработчиками API перечисление Autodesk.AutoCAD.DatabaseServices.DxfCode :
Например, в рассмотренном примере мы могли бы вместо
И это сработает!
Вот только не надо спрашивать меня, почему это значению 0 соответствует элемент перечисления с названием «Start». Понятия не имею.
В общем, можете забить весь свой код непонятными числовыми константами (что, конечно, хреново). Или можете забить его соответствующими элементами перечисления Autodesk.AutoCAD.DatabaseServices.DxfCode с не менее непонятными, взятыми с потолка названиями (что, разумеется, хреново). Можете также создать свое собственное перечисление с необходимыми константами и использовать его (что, сами понимаете, хреново). Добро пожаловать в увлекательный мир программирования! Налево пойдешь — коня потеряешь, и так далее.
Поскольку в любом случае у вас получится нечитаемое спагетти — можете выбрать любой способ или даже использовать все три. А если сойти с ума через полгода не входит в ваши планы — пишите комментарии, это поможет продержаться чуть дольше.
Быстро-быстро пробежимся по простым возможностям фильтров.
Разумеется, искать мы можем не только окружности. Найдем все линии:
Найдем все вхождения блоков:
Список некоторых имен классов объектов можно найти тут (англ.).
Найдем все объекты на слое «layer-1»:
В одном объекте TypedValue можно перечислить несколько имен через запятую. В этом случае условия будут объединены операцией «ИЛИ» («OR»).
Давайте найдем все объекты, которые являются линиями ИЛИ окружностями:
Найдем все объекты, которые находятся на слое «layer-1» ИЛИ «layer-2»:
ВАЖНО: после запятой между объединяемыми параметрами НЕ ДОЛЖНО быть пробела!
Наконец, можно добавить несколько объектов TypedValue к массиву условий. В этом случае условия будут объединены операцией «И» («AND»).
Найдем все объекты, которые являются линиями И находятся на нулевом слое:
Найдем все вхождения блока «block-1»:
Найдем все объекты, которые являются линиями И находятся на нулевом слое ИЛИ слое «layer-1»:
Ну вот, вроде и не больно… Было. Пока что.
2.3 Фильтры посложнее
Давайте взглянем на украденный творчески переработанный (я написал перевел комментарии) пример из блога Kean Walmsley.
Пусть нам надо найти все линии на слое «layer-1» и все круги на слое «layer-2». Очевидно, мы не сможем этого сделать, просто добавив несколько объектов TypedValue : максимум, чего можно добиться этим способом, — это найти все линии и все круги на обоих слоях сразу.
Итак, нам нужно реализовать выбор по такому условию:
((СЛОЙ == «layer-1») И (ТИП == «Линия»)) ИЛИ ((СЛОЙ == «layer-2») И (ТИП == «Окружность»))
В синтаксисе AutoCAD наше условие можно записать так:
- <OR
- <AND
- Layer == «layer-1»
- Entity type == «LINE»
- Layer == «layer-2»
- Entity type == «CIRCLE»
После применения фильтра на базе этих условий мы увидим следующий вывод в консоли AutoCAD:
Принцип простой: осознаем, какое условие нам нужно, разбиваем его на элементарные части, а затем объединяем эти части с помощью операторов OR, AND, NOT, XOR…
Почитать подробнее про сложные условия можно здесь (англ.).
Все, хватит уже о фильтрах. Сколько можно…
3 Альтернативные способы поиска объектов на чертеже
Помимо Editor.SelectAll() , есть ряд других способов поиска (точнее, выделения) объектов на чертеже. Вот ссылки: английская документация, русский перевод.
Как можно увидеть, существуют еще десять более узконаправленных вариантов, чем Editor.SelectAll() . Зато мы рассмотрели самый мощный из них.
Я некоторое время думал, стоит ли вообще включать это в статью. В итоге, решил, что пусть будет — вот только зачем?
В общем, если у вас есть чертеж с огромным количеством объектов, и нужно все их обработать, и выхода нет, и скоро рассвет — то вот вам ссылочка (англ.), где Андрей Бушман hwd, Александр Ривилис и зарубежные эксперты развлекаются, оптимизируя эту задачку.
В этом примере мы получаем доступ не к видимому чертежу ( ModelSpace ), а ко всей БД документа целиком. Таким образом, мы увидим не только изображенные на чертеже объекты, но и слои, определения блоков и т.п.
Тут применено какое-то особо сильное колдунство с прямым доступом к объектам БД по их хендлам. Я вначале попытался было вкурить код, но вовремя вспомнил, что уже давно завязал, и делать это совершенно необязательно. Если вы — программист, то welcome! Что же касается меня… Пожалуй, разбор этого элементарного фрагмента кода оставим читателям в качестве несложного домашнего задания.
UPD.: В комментариях Андрей Бушман привел ссылку на наиболее рациональный способ итерации по объектам.
4 Модификация объектов
Мне кажется, что модификация объектов происходит в разы проще поиска. Вот основной принцип: получив ObjectID , мы открываем сам объект, приводим его к нужному типу и вносим необходимые изменения в свойства. Ну или удаляем объект с чертежа, используя соответствующий метод.
4.1 Удаление объекта с чертежа
- открыть объект на запись;
- вызвать метод Erase();
- зафиксировать транзакцию.
Для примера давайте удалим с чертежа все окружности.
Конечно, в данном случае можно было и не использовать метод UpgradeOpen() , а сразу открывать объект на запись и удалять:
Важно! Если объект находится на заблокированном слое — доступ к нему на запись получить не удастся, и мы сможем полюбоваться на такое сообщение:
Поэтому еще раз напомню о необходимости как минимум упаковывать все требующие доступа на запись операции в конструкцию try. catch с последующим перехватыванием исключений. То есть что-то вроде такого:
В этом случае мы вместо системного исключения увидим простое сообщение в консоли AutoCAD.
Разумеется, использование try. catch в таком виде — это тот еще быдлокод. Ведь мы же знаем, что в случае заблокированного слоя нас ждет ошибка eOnLockedLayer , так что можем перехватывать не все ошибки подряд, а только Autodesk.AutoCAD.Runtime.Exception с соответствующим кодом ошибки ( ErrorStatus ) — как-то так:
Тут подумайте сами — либо решите, что «яжпрограммист», и обрабатывайте наиболее вероятные ошибки по отдельности, либо решите, что путешествия в дебри кода — это не для вас, и глубокая фильтрация ошибок НЕ_НУЖНА. По-хорошему, такое решение должно приниматься с учетом поставленной задачи, оценки рисков и т. д.
Но — подчеркну еще раз — подобную ситуацию нужно иметь в виду в любом случае.
UPD.: Андрей Бушман подсказал, что метод GetObject , вообще говоря, имеет следующую сигнатуру:
Последний параметр позволяет в том числе работать с заблокированными слоями. Для примера, вот такой код у меня корректно отработал даже с заблокированными слоем:
После завершения транзакции состояние слоя, на котором расположен объект, не изменяется.
В общем, похоже, что вместо конструкции try. catch можно использовать параметр forceOpenOnLockedLayer . В этом случае мы отказываемся от метода UpgradeOpen() и сразу открываем объект методом GetObject() на запись ( OpenMode.ForWrite ) с установленным флагом forceOpenOnLockedLayer .
4.2 Изменение типовых свойств объектов
Под «типовыми свойствами» я имею в виду те свойства, которые присущи всем объектам на чертеже AutoCAD. Это, например, слой и цвет.
- открываем объект на запись;
- модифицируем нужное свойство;
- фиксируем транзакцию.
Цвет всех объектов изменен на оранжевый. Однако объекты внутри вхождений блоков цвет не поменяли и остались черными — несмотря на то, что для всех вхождений блоков был успешно задан оранжевый цвет:
Нужно иметь эту особенность в виду.
Рассмотренный способ можно использовать для модификации всех изменяемых свойств, которые доступны в классе Entity . Но как и в предыдущем случае, вы должны быть на 146% уверены в том, что плагину удастся получить доступ к объекту на запись. Если степень уверенности меньше — используйте флаг forceOpenOnLockedLayer или средства индивидуальной защиты конструкцию try. catch и перехватывайте возможные исключения.
4.3 Изменение специфичных свойств объектов
Допустим, мы хотим поменять радиус окружности. Используя класс Entity , мы не сможем это сделать, поскольку у него нет свойства, отвечающего за радиус. В данном случае нужно приводить объект к типу Circle :
Разумеется, необходимо убедиться, что мы имеем дело именно с окружностями. Попытка привести к типу Circle , например, вхождение блока добром не кончится:
Используйте конструкцию try. catch ! Или по крайней мере обеспечивайте фильтрацию, как в нашем примере, либо же проверяйте тип непосредственно перед приведением:
Подчеркну, что ни фильтр, ни проверка не обезопасят от ситуации с заблокированным слоем и ошибкой eOnLockedLayer . Для борьбы с ней используйте либо флаг forceOpenOnLockedLayer , либо конструкцию try. catch , либо фильтры / проверки, которые отсекут заблокированные объекты.
4.4 Перемещение простых объектов
Если положение объекта на чертеже задается одной точкой (как, например, у окружности или блока), то для перемещения этой фигуры достаточно изменить соответствующие свойство. Для примера давайте переместим все вхождения блоков в начало координат:
Аналогом свойства Position для окружности является свойство Center .
4.5 Сложные операции над объектами
AutoCAD .NET API позволяет осуществлять масштабирование и поворот объектов. Наверное, даже можно как-то модифицировать вершины многоугольников и ломаных. К сожалению, я с такими вещами не работал и ничего по этому поводу сказать не могу.
Начать поиск информации, если уж придется, можно с руководства по .NET API (ссылка (англ.), зеркало (англ.)). Ну и форумы AutoCAD в помощь. Можно также задать вопрос на форумах Сообщества программистов Autodesk в СНГ.