четверг, 1 августа 2013 г.

AutoCAD и nanoCAD: о совместимости .NET кода

nanoCAD хорошо развивается и кое в чём даже переплюнул компанию Autodesk. Причём это "кое-что" является весьма важной способностью, отсутствующей в AutoCAD и непонятно когда она в нём появится (в чём лично я вообще сомневаюсь). В данной заметке хочу разместить обсуждение кода, ориентированного на начинающих программистов и опубликованного в этой статье на хабре.

В обозначенной статье автор на примере решения некоторой простой задачи показывает, что .NET программирование под nanoCAD почти ничем не отличается от .NET программирования под AutoCAD, являясь совместимым на уровне исходников (если я правильно понял цель презентации).

Я посмотрел предлагаемый исходный код и внёс в него ряд исправлений и изменений, дабы исходный пример стал более интересным (на мой взгляд): например, добавил справочную систему и вызов её клавишей F1, добавил локализацию, а так же вынес важные настройки во внешний конфигурационный файл (в оригинале они были жёстко прописаны в коде).

Модифицированный мною проект будет протестирован в AutoCAD 2014 x64 и в nanoCAD 5.0. В коде я не использовал ничего особенного, т.о. всё это может быть успешно откомпилировано и под более ранние версии AutoCAD, включая AutoCAD 2009 (позднее проверил: откомпилировал и успешно запустил в AutoCAD 2009 x64 SP1; работает всё, кроме вызова пользовательской справки при нажатии F1 - вместо этого запускается справка AutoCAD).

В конце этой заметки даю ссылку на исходный код подправленного мною варианта. Там же сообщается о совместимости данного кода в AutoCAD\nanoCAD.

Пока, дабы дать общее представление, перечисляю то, что на мой взгляд стоит упомянуть, дабы те, кто только начинает писать на .NET не перенимали вредных привычек. Подправленная версия не идеальна, однако, на мой взгляд, она всё же несколько лучше оригинала. Итак, поехали...

Замечания к исходному коду, опубликованному на хабре

1. Нет смысла для каждой команды создавать отдельный проект. Логичней было бы создать две команды в одном проекте. Я перенёс всё в один проект и изменил имена команд, дабы они не конфликтовали между собой:
ImportCoords_ver_1
ImportCoords_ver_2
2. Псевдонимы пространств имён я изменил так, как мне было бы удобно (это дело вкуса):
   1:  #if ACAD
   2:      using App = Autodesk.AutoCAD.ApplicationServices;
   3:      using cad = Autodesk.AutoCAD.ApplicationServices.Application;
   4:      using Db = Autodesk.AutoCAD.DatabaseServices;
   5:      using Ed = Autodesk.AutoCAD.EditorInput;
   6:      using Gem = Autodesk.AutoCAD.Geometry;
   7:      using Rtm = Autodesk.AutoCAD.Runtime;
   8:      using System.Collections.Generic;
   9:  #else
  10:      using App = HostMgd.ApplicationServices;
  11:      using cad = HostMgd.ApplicationServices.Application;
  12:      using Ed = HostMgd.EditorInput;
  13:      using Db = Teigha.DatabaseServices;
  14:      using Gem = Teigha.Geometry;
  15:      using Rtm = Teigha.Runtime;    
  16:  #endif
Лишние "юзинги" убраны, т.к. не нужны.

3. Для уменьшения затрат времени на поиск нужных классов, рекомендуется
добавлять такие строки:

   1:  [assembly: Rtm.CommandClass(typeof(HostMgd.Samples.Importer))]
   2:  [assembly: Rtm.ExtensionApplication(typeof(HostMgd.Samples.Importer))]
Если проект небольшой, то разница в скорости будет незаметна, однако
для серьёзных проектов она может оказаться ощутимой.

4. Атрибуты командных методов изменены так, чтобы полностью задействовать
их функционал, например: 
   1:  [Rtm.CommandMethod("Namespace", "ImportCoords_1", "ImportCoords_1",
   2:          Rtm.CommandFlags.Modal, null, helpFile, "id_1")]
Как видим, теперь, снята проблема конфликта имён (если др. библиотека
определяет одноимённую), так же определены глобальное и локальное
имя команды. Кроме того, добавлена возможность вызова справочной
системы с нужным разделом (если набрав в консоли имя команды, вместо
Enter нажать F1).

К сожалению, разработчики Autodesk читают инфу из локализованных
ресурсов только для получения локализованного имени команды.
Имя файла справочной системы они там не смотрят и требуют
прописывать его жёстко в коде, что является плохим решением. Если бы
они читали из ресурсов и имя файла справки, то изменив локализацию нашего плагина,
мы бы тем самым изменили бы и целевой файл справочной системы. Т.е.
если юзер указал локализацию русскую, то и справка будет открываться
русская, а для англ. локализации - английская. Зачем Autodesk обрубил
это - х.з., скорее всего потому, что как обычно: "не подумали".

5. Как следует из п.4, добавлен файл справочной системы. Как его
вызывать - см п.4. Для каждой команды открывается тот раздел справки,
который описывает эту команду. Чтобы справка открывалась не только под отладкой, нужно либо (худший вариант) в каталоги поиска САПР (Support File Search Path) добавить путь к каталогу плагина (в абсолютной или относительной форме), либо (лучший вариант) воспользоваться обёрткой над функцией acedSetFunHelp).


Здесь следует сделать небольшое отступление: в AutoCAD 2014 для вызова справки плагина достаточно набрать в командной строке имя команды и нажать F1 вместо Enter. Однако в AutoCAD 2009 это не срабатывает - открывается справка самого AutoCAD. Если в любой версии AutoCAD успеть нажать клавишу F1 во время выполнения пользовательской команды (при условии, что не открыто модальное окно), то тогда будет успешно открыта именно пользовательская справка (это работает как в AutoCAD 2014, так и в AutoCAD 2009). Т.о. если в код метода добавить, к примеру, такую строку:

   1:  ed.GetString("Get any text, please");

то появляется возможность успеть нажать F1 до завершения работы пользовательской команды, в следствии чего пользовательская справка успешно откроется и в AutoCAD 2009. В nanoCAD, к сожалению, не срабатывает даже такой способ.

6. Добавил локализацию и возможность динамического её переключение (с русского на английский и обратно). Как это сделать - см. п.8.





7. Жёстко прописывать конфигурационные настройки в коде - это плохое
решение. Поэтому я вынес их во внешний xml файл. Файл настроек
запоминает не только фильтр выбора файлов, но и состояния опций,
указанных в предыдущем вызове метода. Запись\чтение файла настроек
осуществляются посредством XML сериализации\десериализации.

8. В том же конфигурационном файле хранится имя локализации, посредством
которой должен вестись диалог с пользователем. Дело в том, что по
умолчанию САПР определяет локализацию на основании исполняемого
потока. Зачастую юзеры используют англ. версию САПР, но интерфейс
плагинов предпочитают видеть на русском. Выбор англ. версии программ
обусловлен тем, что они менее глючные, чем руссифицированные. Именно
по этой причине в нашей организации используется англоязычная версия
САПР. Для того, чтобы назначить русскую локализацию, следует в файле
settings.xml элементу cultureName назначить значение ru-RU. Для того,
чтобы интерфейс стал английским, нужно либо удалить имеющееся значение, либо
прописать en-US.

9. Назначая точкам координаты в текущей системе координат, автор
зачем-то делает инверсию:
   1:  point.TransformBy(ucsMatrix.Inverse());
получая при этом неверный результат, если текущая система координат
не мировая. Должно было быть так:
   1:  point.TransformBy(ucsMatrix);
В этом случае получим правильный результат.

10. Для всех переменных писать "this." нет никакого смысла. Это занима-
ет много места и времени. Писать "this" имеет смысл лишь тогда, когда
нужно избежать конфликта имён, либо когда нужно передать ссылку на
самого себя.

11. Сделал более качественную обработку ошибок парсинга. Теперь, если
файл координат содержит ошибки, в консоль САПР выводится информация
о том, в какой строке какая координата имеет неверный формат.

12. Не следует для парсинга использовать метод Parse, если в наличии
имеется TryParse. Скорость их работы отличается на несколько
порядков (в случае ошибок формата). Я когда-то показывал результаты
сравнения здесь.

13. Нет никакого смысла в коде под каждую команду заново прописывать
код парсинга и прорисовки точек. Какой смысл в этом дубляже? Я
изолировал обе операции в классе Creator и модифицировал методы
так, чтобы они юзали один и тот же код.

14. Нет смысла писать уродливые конструкции вида:
1: if (tabCheckBox.Checked == true) {/*...*/}
Вместо этого лучше писать так:
1: if (tabCheckBox.Checked) {/*...*/}

15. В версии без GUI, автор пишет такой код:
1: if (Path.GetExtension(sourceFileName.StringResult) == ".txt")
Это неправильно и подвержено ошибкам. Что если расширение
будет написано прописными буквами? Корректней было бы так:
1: Path.GetExtension(sourceFileName.StringResult).Equals(".txt", StringComparison.CurrentCultureIgnoreCase)
    Однако, несмотря на то, что второй вариант, показанный мною, работает
правильно, он так же является уродливым в виду того, что данная задача
легко решается банальным назначением фильтра:
   1:  opt.Filter = pref.Filter;

16. Нет никакого смысла писать комментарии для кода, смысл которого и
так очевиден. Например:

   1:  // Parse string values
   2:  double coordX = double.Parse(coord[0], nfi);
   3:  double coordY = double.Parse(coord[1], nfi);
   4:  double coordZ = double.Parse(coord[2], nfi);
   5:   
   6:  // Create a point object
   7:  DBPoint point = new DBPoint(new Point3d(coordX, coordY, coordZ));
   8:  point.TransformBy(ucsMatrix.Inverse());
   9:   
  10:  // Append the point to the database
  11:  btr.AppendEntity(point);
  12:   
  13:  // Add the object to the transaction
  14:  tr.AddNewlyCreatedDBObject(point, true);

Лишняя вода не нужна.

17. Огромное количество разного рода прочих правок в коде (проще
читать подправленный вариант кода, чем всё подряд здесь перечислять).

О совместимости кода

Модифицированный мною код проекта был собран на платформе .NET 4.0 для AutoCAD 2014 x64, а так же на .NET 3.5 SP1 для nanoCAD 5.0.

В AutoCAD 2014 x64 всё работает как и ожидалось: переключаются локализации, открывается пользовательская справочная система (для каждой команды в соответствующем ей разделе справки), а вот в nanoCAD 5.0, к сожалению, возникли некоторые проблемы:
  1. В nanoCAD почему-то игнорируется локализация ресурсов, которая должна была бы выполняться на основе локализации потока, в котором работает GUI. В результате используется локализация en-US, хотя в конфигурационном файле текущей указана ru-RU.
  2. AutoCAD читает локализованное имя команды из ресурсов, воспринимая значение аргумента как ключ записи. Но nanoCAD воспринимает значение как имя самой команды. Т.о. Если атрибуту передано значение ImportCoords_1, то AutoCAD для этого ключа найдёт локализованную запись и определит, что локализованное имя команды на самом деле - ImportCoords_ver_1. А вот nanoCAD смотрит только глобальное имя команды, которое в моём случае совпадает с именем ключа, т.е. ImportCoords_1.
  3. Попытка запустить команду ImportCoords_ver_1 выдаёт в консоли такую ошибку:

    Error during command executing: 'Указатель для этого метода был пустым.'.

    Ошибка возникает в строке кода:

       1:  Ed.PromptOpenFileOptions opt = new Ed.PromptOpenFileOptions(resMng.GetString("GetFileName", culture));

    Если проблемную строку заменить на такую:

       1:  Ed.PromptOpenFileOptions opt = new Ed.PromptOpenFileOptions("Select File");

    То всё начинает работать. Т.о. почему-то возникают проблемы при попытке получения локализованного ресурса. Чем это обусловлено - я пока не понял и несколько удивлён, т.к. использую для этого "родной" механизм, предоставленный в .NET, да и в AutoCAD всё работает как нужно. Для того, чтобы код должным образом работал как в AutoCAD, так и в nanoCAD, пришлось внести небольшое изменение:

       1:  #if ACAD
       2:      // TODO: This ain't working for nanoCAD
       3:      Ed.PromptOpenFileOptions opt = new Ed.PromptOpenFileOptions(resMng.GetString("GetFileName", culture));
       4:  #else
       5:      Ed.PromptOpenFileOptions opt = new Ed.PromptOpenFileOptions("Select File");
       6:  #endif
  4. Команда ImportCoords_ver_2 работает нормально (если не учитывать то, что используется не та локализация, которая указана текущей в файле настроек плагина).
  5. Попытка открытия справочной системы плагина (нажатием F1 после полного набора имени команды в консоли) приводит к открытию справки nanoCAD, вместо той, которая ожидалась. Добавить дополнительные каталоги поиска в nanoCAD (может в этом проблема, как это изначально было в AutoCAD???) - пока не представляется возможным (не реализовано в nanoCAD).
  6. Брэйкпоинты, в режиме отладки, в nanoCAD у меня изначально не работали. В AutoCAD подобная проблема решалась путём правки файла acad.exe.config, указывая конкретную целевую версию .NET Framework, но в nanoCAD я не нашёл файла NCad.exe.config, поэтому его следует создать вручную самому (указав в нём целевую версию платформы) - тогда брэйкпоинты начинают работать.
Выводы
Как видим, написанный под AutoCAD код пока, к сожалению, не является совместимым с nanoCAD настолько, насколько этого бы хотелось... Возможно именно поэтому автор исходной версии кода не затронул тему локализации и справки. Надеюсь, что со временем ситуация изменится в лучшую сторону.

А теперь, собственно, ссылка на подправленный мною вариант: тынц.

3 комментария:

dows комментирует...

у нас (в отличии от AutoCAD) первая буковка в названии продукта - маленькая: nanoCAD :-)

Спасибо за анализ кода и комментарии )

Andrey Bushman комментирует...

Я подправил тэг топика, но тэг почему-то так и остался с заглавной буквы. На будущее учту.

Анонимный комментирует...

А как это в Нанокад воткнуть?