Дополнительный инструмент для работы с базой данных чертежа

 Дата публикации: 26.08.2011
Дата редактирования: не редактировалась
Состояние: находится в стадии изменения!!!

    В процессе работы над чертежом порой возникают ситуации, когда для получения нужного набора примитивов необходимо выполнить итерацию по всей базе данных документа и выбрать такие объекты, которые отвечают некоторому, обозначенному нами условию, или же требуется выполнить определённое действие над любым, нужным нам набором примитивов имеющимся в чертеже объектами. Например, нам может потребоваться прочитать имеющиеся у всех имеющихся объектов их расширенные данные (XRecord). Или же требуется выбрать такие элементы, которые для своего отображения используют некий, интересующий нас текстовый стиль. Итерация по всей базе данных требуется не так часто, однако порой без неё либо не обойтись, либо иные решения могут оказаться более сложными в плане их реализации.

    В данной статье будет создан статический класс, содержащий в себе методы расширения для класса Autodesk.AutoCAD.DatabaseServices.Database. Сразу хочу акцентировать внимание на том, что использовать обозначенный функционал рекомендуется к месту (см. ниже разделы о целесообразности/нецелесообразности использования), а не применять везде подряд, т.к. порой процесс итерации по базе данных может оказаться затратным по времени. 

Примечание
    Справедливости ради замечу, что при разовом использовании падение производительности будет не существенное (на извлечение 700 000 объектов уходит около 0,5 секунды для чертежа, объёмом 51 Мб), но если использовать его везде где можно, то в результате по полсекунды может набраться ощутимое падение скорости.

Назначение класса 

    Класс предназначен для извлечения информации из базы данных чертежа и, в случае необходимости, редактирования существующих в ней объектов. В реализации всех методов из обработки исключаются объекты,  идентификаторы которых отвечают хотя бы одному из перечисленных ниже условий:
  1. Значение идентификатора равно ObjectId.Null
  2. Свойство IsValid идентификатора равно false
    Я умышленно устанавливаю такие ограничения, т.к. это своего рода защита от ошибки, которая может возникнуть в случае использования некорректного ObjectId.

Целесообразность использования

    Некоторые методы интерфейса, выполняющие итерацию по БД, целесообразно использовать в таких случаях, когда нужный набор объектов невозможно получить, не выполнив итерации по всей базе данных чертежа, либо иные решения могут оказаться более сложными в плане их реализации. Прочие методы, выполняющие работу над определённым набором объектов, могут применяться везде, где это удобно разработчику.

Пример нецелесообразного использования

    Например, если нужно получить перечень всех имеющихся листов (Layouts) чертежа, то использовать для этого методы, обозначенные в Bushman.AutoCAD.Common.IDBSearcher и выполняющие итерацию по всей БД можно, но это не будет целесообразным решением, т.к. в этом случае процесс выборки нужного набора займёт больше времени,  чем если выполнить запрос напрямую из таблицы Autodesk.AutoCAD.DatabaseServices.BlockTable, считав нужные записи Autodesk.AutoCAD.DatabaseServices.BlockTableRecord. Дело в том, что Bushman.AutoCAD.Common.IDBSearcher в цикле перебирает все примитивы базы данных, причём чем больше примитивов в базе данных, тем дольше будет выполняться поиск (что собственно и логично), в то время как непосредственная итерация по записям Autodesk.AutoCAD.DatabaseServices.BlockTableRecord выполнится быстрее (см. примечание в начале страницы).

Пример целесообразного использования

    Например, если требуется получить все объекты, использующие текстовый стиль (т.е. однострочный/многострочный текст, атрибуты, размерные стили, стили мультивыносок, размеры, мультивыноски, табличные стили, таблицы, типы линий и т.д.). Это может потребоваться например тогда, когда нужно переназначить таким объектам другой текстовый стиль, а старый удалить. Поскольку в AutoCAD существует множество классов, использующих текстовый стиль и их объекты хранятся в разных местах БД, с разными уровнями вложенности, то последовательная итерация по всем объектам чертежа оправдана и соответственно использование Bushman.AutoCAD.Common.IDBSearcher целесообразно.

Определяемся с набором нужного функционала

    Для начала давайте определимся с необходимым нам функционалом. Итак, мною был сформирован следующий набор пожеланий:
  1. Получить в виде строки полный путь к объекту. Путь должен формироваться в формате ИмяКласса|Хэндл, а в качестве разделителя должен использоваться "\".
    Пример имени: "AcDbBlockTable|1\AcDbBlockTableRecord|31\AcDbArc|1148208".
    Такой путь легко понять и извлечь из него нужную информацию: сначала с помощью Split("\") разбиваем полученную строку на массив строк вида:
    • "AcDbBlockTable|1"
    • "AcDbBlockTableRecord|31"
    • "AcDbArc|1148208"
      Каждая из этих строк содержит информацию о конкретном объекте нашего иерархического пути.
      Теперь из каждой такой строки, с помощью Split("|"), можно получить имя неуправляемого класса, а так же хэндл объекта. 
      Т.о. строка пути не только наглядно демонстрирует уровень вложенности и то, объектами каких типов этот путь представлен, но она так же позволяет получить любой из этих объектов, на основании их хэндлов (см. п.2).
  2. Получить ObjectId на основании строкового значения его хэндла.
  3. Получить тип управляемой оболочки (если такая имеется) для указанного имени неуправляемого класса.
  4. Проверить, определён ли в базе данных неуправляемый класс с указанным именем.
  5. Получить сгруппированный по классам набор объектов ObjectId, соответствующих указанным нами условиям выборки. Результирующая выборка должна быть упакована в словарь Dictionary<string, List<ObjectId>>, где в качестве ключа выступает имя класса, а в качестве значения - список List<ObjectId>, представляющий собой идентификаторы объектов этого класса. Т.о. если нам, к примеру, потребуется найти в чертеже все объекты класса AcDbMText, достаточно будет просто по ключу "AcDbMText" извлечь из словаря соответствующий объект List<ObjectId>  - в нём как раз и будут перечислены все нужные нам идентификаторы.
  6. Получить сгруппированный по классам набор объектов ObjectId, при этом правило выборки определяется перечислимым Bushman.AutoCAD.DatabaseServices.DBObjectStatus, перечень значений которого таков:
    • NotErased - выбрать все примитивы, которые ещё не были удалены (свойство IsErased равно false) в рамках текущей сессии работы с базой данных чертежа.
    • Erased - выбрать все примитивы, которые были удалены (свойство IsErased равно true) в рамках текущей сессии работы с базой данных чертежа.
    • Any - выбрать любые примитивы, не зависимо от того, какое значение имеет свойство IsErased.
      Результирующая выборка должна быть упакована в словарь Dictionary<string, List<ObjectId>>, где в качестве ключа выступает имя класса, а в качестве значения - список List<ObjectId>, представляющий собой идентификаторы объектов этого класса. Т.о. если нам, к примеру, потребуется найти в чертеже все объекты класса AcDbMText, достаточно будет просто по ключу "AcDbMText" извлечь из словаря соответствующий объект List<ObjectId>  - в нём как раз и будут перечислены все нужные нам идентификаторы.
  7. Извлечь из базы данных чертежа нужную информацию и упаковать её в экземпляры указанного типа, по обозначенным правилам выборки и инициализации.
  8. На основании массива идентификаторов объектов, извлечь из базы данных чертежа нужную информацию и упаковать её в экземпляры указанного типа, по обозначенным правилам выборки и инициализации.
  9. Выполнить некоторое, указанное нами действие над всеми объектами базы данных, соответствующих обозначенному нами условию выборки.
  10. На основании указанного нами массива ObjectId[], сформировать Dictionary<string, List<ObjectId>>.
    Вот такой скромный функционал мне бы хотелось реализовать в интерфейсе, с целью его дальнейшего использования при написании плагинов под AutoCAD. Все пункты будут реализованы в виде методов, имена и структуры которых будут следующими (нумерация позиций совпадает с предыдущей):
  1. ObjectId[] GetAllObjects(DBObjectStatus status)
  2. Dictionary<string, List<ObjectId>> GetByTypes(Func<Transaction, ObjectId, bool> requirement)
  3. R[] GetData<R>(Func<Transaction, ObjectId, bool> requirement, Func<Transaction, ObjectId, R> result)
  4. void Action(ObjectId[] ids, Action<Transaction, ObjectId> action)
  5. Dictionary<string, List<ObjectId>> GroupByTypes(ObjectId[] ids)
     Визуально такой интерфейс можно представить так:

Теперь давайте напишем код придуманного нами выше интерфейса.

Код интерфейса IDBSearcher

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
//Microsoft
using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Text;
//AutoCAD
using acad = Autodesk.AutoCAD.ApplicationServices.Application;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
namespace Bushman.AutoCAD.Common
{
/// <summary>
/// Интерфейс, предназначенный для извлечения информации из базы данных чертежа и,
/// в случае необходимости, редактирования существующих в ней объектов.
/// В РЕАЛИЗАЦИИ всех методов интерфейса из обработки СЛЕДУЕТ ИСКЛЮЧАТЬ объекты,
/// идентификаторы которых отвечают хотя бы одному из перечисленных ниже условий:
/// 1. Значение идентификатора равно ObjectId.Null
/// 2. Свойство IsValid идентификатора равно false
/// </summary>
public interface IDBSearcher
{
/// <summary>
/// База данных чертежа, из которой должна быть выполнена выборка объектов
/// </summary>
Database TargetDb { get;}
/// <summary>
/// Получить идентификаторы всех объектов, унаследованных от DBObject, имеющихся в базе
/// данных чертежа
/// </summary>
/// <param name="status">Какие именно объекты (удалённые/не удалённые/все) следует выбирать</param>
/// <returns>Возвращается массив ObjectId[]</returns>
ObjectId[] GetAllObjects(DBObjectStatus status);
/// <summary>
/// Получить словарь объектов, соответствующих некоторому условию.
/// Результирующая выборка будет представлена в виде словаря и сгруппирована в списки
/// по именам классов (берётся из ObjectId.ObjectClass.Name), которые в свою очередь
/// выступают в роли ключа.
/// Т.о. можно быстро выбрать все примитивы нужного типа
/// </summary>
/// <param name="requirement">Условие выборки объектов</param>
/// <returns>Возвращается словарь, у которого в качестве ключа используется имя класса
/// (берётся из ObjectId.ObjectClass.Name), а в качестве значения - список, содержащий
/// в себе идентификаторы объектов данного класса</returns>
Dictionary<string, List<ObjectId>> GetByTypes(Func<Transaction, ObjectId, bool> requirement);
/// <summary>
/// Сгруппировать идентификаторы объектов по их классам
/// </summary>
/// <param name="ids">Исходный массив данных</param>
/// <returns>Возвращается словарь, у которого в качестве ключа используется имя класса
/// (берётся из ObjectId.ObjectClass.Name), а в качестве значения - список, содержащий
/// в себе идентификаторы объектов данного класса</returns>
Dictionary<string, List<ObjectId>> GroupByTypes(ObjectId[] ids);
/// <summary>
/// Выбрать данные из базы данных
/// </summary>
/// <typeparam name="R">Тип объектов, возвращаемых в массиве</typeparam>
/// <param name="requirement">Условие выборки объектов</param>
/// <param name="result">Правило построения результирующего объекта</param>
/// <returns>Возвращается массив объектов R</returns>
R[] GetData<R>(Func<Transaction, ObjectId, bool> requirement, Func<Transaction, ObjectId, R> result);
/// <summary>
/// Выполнить какое-то действие над всеми объектами, удовлетворяющими некоторому условию
/// </summary>
/// <param name="ids">Идентификаторы объектов, над которыми нужно выполнить действие</param>
/// <param name="action">Действие, которое необходимо выполнить</param>
void Action(ObjectId[] ids, Action<Transaction, ObjectId> action);
}
}

    Обратите внимание на то, что методу GetAllObjects в качестве параметра передаётся экземпляр DBObjectStatus - это перечисление, написанное нами для удобства. Код перечисления такой:

Код DBObjectStatus

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//Microsoft
using System;
namespace Bushman.AutoCAD.Common {
/// <summary>
/// Перечисление, с помощью которого в методах IDBSearcher
/// можно указывать, идентификаторы (DBObjectId) каких объектов
/// (DBObject) следует выбирать из базы данных (Database)
/// </summary>
public enum DBObjectStatus {
/// <summary>
/// Выбирать все ObjectId, не зависимо от значения
/// свойства IsErased
/// </summary>
Any,
/// <summary>
/// Выбирать только такие ObjectId, значения свойства
/// IsErased которых равно true
/// </summary>
Erased,
/// <summary>
/// Выбирать только такие ObjectId, значения свойства
/// IsErased которых равно false
/// </summary>
NotErased
}
}

    Теперь необходимо написать реализацию интерфейса IDBSearcher. Забегая вперёд хочу отметить, что мы рассмотрим три способа проверки ObjectId на предмет его существования в Database. Поэтому сначала нами будет создан абстрактный класс DBSearcher, реализующий интерфейс IDBSearcher, а уже на его основе мы построим несколько классов, выполняющих одну и ту же задачу, но отличающиеся друг от друга только способом проверки на валидность. 

    Для чего это делается, зачем нам писать несколько вариантов решения? Дело в том, что мы проанализируем все варианты и выберем тот, который обладает наибольшей производительностью. Наличие промежуточного звена в виде абстрактного класса DBSearcher обусловлено желанием вынести общий код в одно место, дабы не дублировать его во всех конечных реализациях. Кроме того, если нам потребуется внести изменение, которое должно отразиться на всех наших вариантах, то достаточно будет внести это изменение в одном месте и оно будет подхвачено везде. Итак, давайте приступим к написанию DBSearcher.