Часть 1. DWG. Как побочный кейс стал основным
Путешествие в кроличью нору автоматизации процессов может привести к занятным результатам. Казалось бы, тривиальная задача часто оказывается не таким уж и простым в реализации кейсом. Особенно без поддержки со стороны вендора.
Содержание:
Начало
Меня зовут Макс и некоторое время назад я занимался автоматизацией процессов в конструкторском бюро. Однажды передо мной встала задача автоматизации печати чертежей из AutoCAD. Казалось бы, ничего необычного, задача решалась не раз в разных проектных институтах, и решение должно быть типовое, но... В моем случае это было именно КБ с partial remote командой из 16 инженеров. И их нужно было освободить от бумажного апокалипсиса.
Abstract
Публикация описывает собственную разработку - десктопное приложение для пакетной печати и конвертации DWG-чертежей, которое используется в КБ с 2020 года.
Решение оказалось надежным, легко масштабируемым, простым в использовании и не нуждающимся в постоянной поддержке. Оно позволило сократить время выпуска чертежей на 50%. А еще в процессе разработки было обнаружено несколько сюрпризов, и не все из них удалось обойти без костылей
А самое главное: решение стало инновацией. Ранее операции печати и конвертации выполнялись вручную, занимая до нескольких дней на проект и требуя участия инженеров. Разработанное мной приложение полностью исключило ручной труд при печати, обеспечив автоматическую пакетную обработку чертежей DWG. Одним из ключевых изобретений стал алгоритм автоматического определения области печати и выбора принтера для каждого чертежа.
Follow the white rabbit (c)
Прыжок в кроличью нору или Проблема
Так исторически сложилось, что в КБ используется именно AutoCAD. Альтернативы от ODA по объективным причинам не подошли. сейчас не буду погружаться в технические детали. Достаточно знать, что работать будем с файлами DWG. Каждый DWG-файл состоит из пространств модели (предназначенной для 2d или 3d модели оборудования или площадки) и пространств листов в которые проецируют виды модели и добавляют необходимые форматные рамки и прочие элементы для получения чертежей по ГОСТ.
Файлы чертежей всегда находятся на выделенном файловом сервере. Это позволяет в рантайме видеть изменения всем участвующим в разработке. При выпуске чертежи печатают в 2-4 экземплярах (если заказчик желает странного предусмотрено контрактом) и конвертируют в формат PDF для отправки в электронном виде (основной способ передачи документов с 2020 года). Как показала практика - от бумажных книг в регионах еще не ушли и вряд ли уйдут в ближайшее время
Процесс AS IS описывался достаточно простым алгоритмом:
Для каждого файла DWG в проекте:
Для каждого листа:
Определить принтер и параметры печати
Отправить на печать
Вернуться к 2
Вернуться к 1
Для конвертации в PDF процесс повторить (конвертация - это та же самая печать в файл на программном принтере, в нашем случае это оказалось важным)
Специально обученного человека (или двух), который будет заниматься печатью и сборкой комплектов чертежей как это принято у взрослых в традиционных проектных институтах нет и не будет.
Решение должно позволять пакетно обрабатывать файлы DWG, выводить на печать все чертежи из любых пространств файла в автоматическом режиме с заданными пользователем параметрами. Решение должно устанавливаться на пользовательские ПК, иметь GUI, быть производительным, поддерживаемым, масштабируемым, иметь возможность настройки под стандарты предприятия, а главное - быть настолько простым в использовании чтобы им мог пользоваться пользователь и без технического бэкграунда.
Здесь важно, что в КБ уже проведена работа, позволяющая автоматизацию подобных процессов - стандартизация чертежей в электронном виде обеспечивается программно на этапе их выполнения в AutoCAD. Каким образом - тема отдельного поста.
Море слёз или Обзор рынка
Сразу оговоримся: я человек ленивый и если есть существующие решения- стоит посмотреть в сторону их использования (ИМХО).
AutoIT
Первое, что выдаст Google. Позволяет автоматизировать вообще все что угодно и не требует особых знаний в программировании.
Почему мы не будем использовать AutoIT?
AutoIT работает через эмуляцию кликов и клавиш и привязку к элементам UI, окнам, координатам. Любое изменение версии AutoCAD или UI - и сценарий ломается
Сложно сопровождать (UI-зависимый код)
Сложно передавать между пользователями (напомню - команда partial remote)
PrintConductor/2Printer/PaperCut(любое приложение использующее библиотеки Open Design Alliance)
Позволяет пакетно печатать файлы DWG/DXF на принтер или конвертировать их без AutoCAD. В нашем случае есть риски некорректной обработки составной и нестандартной графики и часто достаточно сложный интерфейс. Угадайте кого будет звать с матами пользователь, чтобы найти нужную настройку или когда у него не выйдет на печать треть чертежа
Скачки наперегонки или Проект
Делать нечего, расчехляем лаптоп и пишем сами. По традиции у нас два стула варианта:
Приложение на базе Open Design Alliance SDK
Выглядит наиболее перспективно с точки зрения разработки. Заходим на сайт ODA и видим предложение купить лицензию минимум на 100 мест. О как... На момент написания статьи есть триальный период, но SDK для использования в России недоступен. Все риски некорректной обработки составной и нестандартной графики - на разработчике. Бизнес не поймет
Автоматизация на уровне CAD-ядра с программой-интерфейсом
Традиционный (почти) подход, подразумевает использование:
AutoCAD
стандартных ключей запуска
встроенного скриптового механизма (.scr, LISP, Automation API)
Решение детерминированно (AutoCAD выполняет программный код независимо от программы-интерфейса), практически отсутствуют риски связанные с прокси-графикой, достаточно простое в поддержке и масштабировании. Плюс наличие у КБ лицензий AutoCAD. На этом варианте и остановимся
Архитектурно решение выглядит так:

Крокет у королевы или Реализация
Что и для чего будем использовать:
инсталлятор - Inno Setup
программа-интерфейс - C++ и QT
программы выполняемые в AutoCAD + VisualLisp
Почему VisualLisp? Поддерржка этого языка Autodesk не прекращалась (и, ИМХО, вряд ли прекратится в будущем). А еще код на VisualLisp выполнится в любой версии CAD-программы. Плюс нет необходимости ежегодно компилировать исполняемые файлы под очередной релиз AutoCAD.
Что будем печатать? Предполагаем что уровень обеспечения стандартизации достаточен для использования всеми сотрудниками КБ определенных блоков рамок чертежа. Мы будем искать в каждом пространстве один или более блок и отправлять на печать занимаемую им область.
Как определим на каком из принтеров печатать? Для этого соберем информацию о доступных в AutoCAD принтерах и их свойствах, и дадим пользователю выбрать в настройках программы какой принтер для какого размера использовать. Настройки будем хранить в конфигах.
Для этого воспользуемся объектной Моделью AutoCAD (потом это еще сыграет с нами злую шутку) - иерархической структуре, где чертеж и его элементы представлены в виде программных объектов с определенными свойствами и методами. Что-то мне это напоминает, да?
Основная иерархия объектов:
Application (Приложение): Корневой объект, представляющий весь сеанс AutoCAD.
Document (Документ/Чертеж): Доступ к текущему открытому чертежу.
Layout (Лист): Каждый документ содержит коллекцию Layouts (Листов). Объект Layout хранит все настройки, необходимые для печати конкретного вида (пространства модели или листа компоновки), включая формат бумаги, ориентацию, используемый плоттер (принтер) и таблицу стилей печати.
Для нас также будут представлять интерес специфические объекты для печати:
Plot (Печать): Объект, который содержит методы для запуска процесса печати, Нас будут интересовать методы PlotToDevice и PlotToFile.
PlotSettings (Настройки печати): Объект, содержащий идентичную информацию о настройках печати, что и Layout, но может существовать отдельно и использоваться как пресет.
Закончим с теорией и начнем кодить. Код разделим на модули:
Скрипты выполняющиеся в среде AutoCAD
Код программы-интерфейса
1. Среда AutoCAD
Здесь мы будем получать параметры печати для принтеров и собственно, печатать. Для начала используя Модель мы извлечем информацию о доступных принтерах и и сохраним ее на диск:
(defun MT-exportPlottersPrefs (path / doc activeLayout oldPaperSize plotDevicePropertyList plotDevicesList iniFile item)
(setq iniFile
(strcat path "\\" "MT_batchplot.ini")
)
(vl-load-com)
(setq doc (vla-get-ActiveDocument (vlax-get-acad-object)))
(setq activeLayout (vla-get-ActiveLayout doc))
(setq plotDeviceList (MT-getPlotDeviceList activeLayout))
(setq plotDevicePropertyList (MT-getDevicePropertyList iniFile plotDeviceList activeLayout doc))
(foreach item plotDevicePropertyList
(progn
(MT-savePlotterDesc
item
(strcat path "\\Printers")
)
)
)
)
В вызываемую функцию мы передаем путь к папке приложения, файлы описания принтеров будем хранить во вложеной папке. В функции MT-getPlotDeviceList мы получаем активный lauout и для него получаем список устройств печати через метод vla-GetPlotDeviceNames. Полученный результат используется для получения списка свойств устройств в коде ниже:
(defun MT-getDevicePropertyList (iniFile plotDeviceList activeLayout doc / plotDevicePropertyList paperSizes)
(setq plotDevicePropertyList '())
(setq oldConfig
(vla-get-configname activeLayout)
)
(foreach name plotDeviceList
(progn
(vla-put-configname activeLayout name)
(vla-RefreshPlotDeviceInfo activeLayout)
(setq first
(cons "device" name)
)
(setq paperSizes (MT-getDevicePaperSizes iniFile activeLayout doc))
(setq second
(list "paperSizes" paperSizes)
)
(setq item
(list first second)
)
(setq plotDevicePropertyList
(cons item plotDevicePropertyList)
)
)
)
(vla-put-configname activeLayout oldConfig)
plotDevicePropertyList
)
Здесь мы применяем каждое устройство печати к текущему layout делаем обновление, и формируем список из элементов вида (("device" . deviceName) data) где объект data вида (("UserXXXX" . "A2") (420 510) T) получаем выполняя код:
(defun MT-getDevicePaperSizes (iniFile activeLayout doc / mediaNames mediaNamesLocal sizeList Width Height name
sectName keyName maxMargin left right top bottom oversize X effectiveWidth
effectiveHeight MarginLowerLeft MarginUpperRight )
(setq sectName "plot")
(setq keyName "_marginMax")
(setq maxMargin (atof (MT-getValue iniFile sectName keyName)))
(setq mediaNames (MT-getMediaNames doc))
(foreach name mediaNames
(vla-put-CanonicalMediaName activeLayout (car name))
(vla-GetPaperSize activeLayout 'Width 'Height)
(vla-GetPaperMargins activeLayout 'MarginLowerLeft 'MarginUpperRight)
(setq
left (vlax-safearray-get-element MarginLowerLeft 0)
right (vlax-safearray-get-element MarginUpperRight 0)
top (vlax-safearray-get-element MarginUpperRight 1)
bottom (vlax-safearray-get-element MarginLowerLeft 1)
)
(setq effectiveWidth
(- Width (+ left right))
)
(setq effectiveheight
(- Height (+ top bottom))
)
(if (and
(< (- Width effectiveWidth ) (* 2 maxMargin))
(< (- height effectiveHeight) (* 2 maxMargin))
)
(setq oversize T)
(setq oversize nil)
)
(setq sizeList(append sizeList(list(list name (list Width Height) (list effectiveWidth effectiveHeight) oversize))))
)
)
Здесь функция MT-getMediaNames используется для получения списка пар имен размеров бумаги (системного и видимого пользователю) используя методы vla-GetLocaleMediaName и vla-GetCanonicalMediaNames.
Полученный список мы сохраняем на диск используя вызов функции MT-savePlotterDesc как список INI-файлов с секциями вида:
["ISO_full_bleed_A4_(210.00_x_297.00_MM)"]
localName="ISO full bleed A4 (210.00 x 297.00 MM")
paperW=210
paperH=297
effectW=210
effectH=297
isOversize=1
А сейчас отправим что-либо на печать. Для этого получим указатель на текущий документ, установим для него текущий Layout с именем tab:
(setq activeTab
(vla-put-ActiveLayout
(vla-get-ActiveDocument
(vlax-get-acad-object)
)
(vla-item layouts tab)
)
)
Установим область печати со сторонами areaW и areaH:
(setq pointFrame0 (vlax-make-safearray vlax-vbDouble '(0 . 1)))
(setq pointFrame1 (vlax-make-safearray vlax-vbDouble '(0 . 1)))
(vlax-safearray-fill pointFrame0 insertPoint)
(vlax-safearray-fill
pointFrame1
(list
(+ (car insertPoint) areaW)
(+ (cadr insertPoint) areaH)
)
)
(vla-SetWindowToPlot activeTab pointFrame0 pointFrame1)
Установим параметры печати:
(vla-put-UseStandardScale activeTab :vlax-true)
(vla-put-StandardScale activeTab acScaleToFit) ; устанавливаем масштаб
(setq origin (vlax-make-safearray vlax-vbDouble '(0 . 1)))
(vlax-safearray-fill origin (list 0.0 0.0))
(vla-put-PlotOrigin activeTab origin) ; устанавливаем смещение
(vla-put-CenterPlot activeTab :vlax-true) ; устанавливаем центрирование
(vla-put-PlotRotation activeTab ac0degrees); устанавливаем поворот
(vla-put-PlotHidden activeTab :vlax-false) ; устанавливаем сокрытие элементов листа
(vla-put-PlotViewportBorders activeTab :vlax-false) ; устанавливаем сокрытие контуров видовых экранов
(vla-put-PlotViewportsFirst activeTab :vlax-true)
(vla-put-PlotWithLineweights activeTab :vlax-true) ; устанавливаем печатать толщин линий
(vla-put-ScaleLineweights activeTab :vlax-false) ; устанавливаем масштабирование типов линий
(vla-put-PlotWithPlotStyles activeTab :vlax-true) ; устанавливаем использование стилей печати
И в отправим на принтер выполнив метод:
(vla-PlotToDevice
(vla-get-Plot
(vla-get-ActiveDocument
(vlax-get-acad-object)
)
)
)
PROFIT!
Если, несмотря на обилие скобок, еще интересен полный код
(defun MT-printFrames (path plotTable copies / config doc item tab height width framesLayer activeTab printerName isOversize name
framesList valuesList toleranceMax layouts x paperName, papersizesConfig canonicalPaperName valuesListItem
paperSizes insertPoint scale isPlotSuccess denom areaW areaH paper printerDesc areaWReal areaHReal isFit)
(vl-load-com)
(setq doc (vla-get-ActiveDocument (vlax-get-acad-object)))
(setq layouts (vla-get-Layouts doc))
; из конфига приложения получим имя слоя, где будем искать блоки рамок и допуски для размеров бумаги
(setq config (MT-readConfig (strcat path "\\" "MT_batchplot.ini")))
(setq framesLayer (MT-getConfigValue config "main" "_commonLay"))
(setq toleranceMax (atof (MT-getConfigValue config "plot" "_toleranceMax")))
; получим список параметров для каждого размера бумаги из конфига
(setq papersizesConfig (MT-readConfig (strcat path "\\" "MT_papersizes.ini")))
(setq framesList (MT-getConfigSectList papersizesConfig))
(foreach x framesList
(setq item
(list
x
(MT-getConfigSectValuesList papersizesConfig x)
)
)
(setq valuesList
(append valuesList
(list item)
)
)
)
; получим список областей печати c найдеными рамками
(setq plotAreaList (MT-getPlotAreaList valuesList framesLayer))
; и начнем печатать все найденные в файле области печати
(foreach x plotAreaList
(setq tab (cdr (assoc "TAB" x)))
(setq name (cdr (assoc "NAM" x)))
(setq insertPoint
(list
(car (cdr (assoc "INS" x)))
(cadr (cdr (assoc "INS" x)))
)
)
(setq scale (cdr (assoc "SCL" x)))
;
(setq valuesListItem (cadr (assoc name valueslist)))
(setq areaW (atof (cdr (assoc "paperW" valuesListItem))))
(setq areaH (atof (cdr (assoc "paperH" valuesListItem))))
; получим имя принтера
(setq printerName (cdr (assoc "printerName" valuesListItem)))
; получим поддерживаемые принтером размеры бумаги
(setq printerDesc (MT-getPlotterDesc (strcat path "\\Printers") printerName))
(setq paperSizes (cadr (assoc "paperSizes" printerDesc)))
; получим бумагу, соответствующую нашей области печати с учетом допусков
(setq paper (MT-getPaperForPrint paperSizes areaW areaH toleranceMax))
; получим параметры листа и реальные размеры области печати
(setq isFit (car (cddddr paper)))
(setq isOversize (cadddr paper))
(setq width (caadr paper))
(setq height (cadadr paper))
(setq effectWidth (caaddr paper))
(setq effectHeight (car (cdaddr paper)))
(setq paperName (cdar paper))
(setq canonicalPaperName (caar paper))
(setq areaWReal (* areaW scale))
(setq areaHReal (* areaH scale))
; устанавливаем Layout
(vla-put-ActiveLayout doc (vla-item layouts tab))
(setq activeTab (vla-get-ActiveLayout doc))
; устанавливаем значения для текущего Layout
(vla-put-ConfigName activeTab printerName)
(vla-put-CanonicalMediaName activeTab canonicalPaperName)
(vla-put-PaperUnits activeTab acMillimeters)
; устанавливаем минимальную и максимальную точку области печати
(setq pointFrame0 (vlax-make-safearray vlax-vbDouble '(0 . 1)))
(setq pointFrame1 (vlax-make-safearray vlax-vbDouble '(0 . 1)))
(vlax-safearray-fill pointFrame0 insertPoint)
(vlax-safearray-fill
pointFrame1
(list
(+ (car insertPoint) areaWReal)
(+ (cadr insertPoint) areaHReal)
)
)
; применяем область печати к активному Layout
(vla-SetWindowToPlot activeTab pointFrame0 pointFrame1)
(vla-put-PlotType activeTab acWindow)
; применяем настройки масштаба и печати к Layout
(if(/= scale 1.0)
(progn
(vla-put-UseStandardScale activeTab :vlax-false)
(vla-put-StandardScale activeTab acVpCustomScale)
(setq num 1.0)
(if (= isFit nil)
(setq denom scale)
(if (= rotate nil)
(setq denom
(max
(/ areaWReal effectWidth)
(/ areaHReal effectHeight)
)
)
(setq denom
(max
(/ areaHReal effectWidth)
(/ areaWReal effectHeight)
)
)
)
)
(vla-SetCustomScale activeTab num denom)
)
(progn
(vla-put-UseStandardScale activeTab :vlax-true)
(if (= isFit nil)
(vla-put-StandardScale activeTab ac1_1)
(vla-put-StandardScale activeTab acScaleToFit)
)
)
)
(setq origin (vlax-make-safearray vlax-vbDouble '(0 . 1)))
(vlax-safearray-fill origin (list 0.0 0.0))
(vla-put-PlotOrigin activeTab origin)
(vla-put-CenterPlot activeTab :vlax-true)
(if (= rotate nil)
(vla-put-PlotRotation activeTab ac0degrees)
(vla-put-PlotRotation activeTab ac90degrees)
)
(vla-put-PlotHidden activeTab :vlax-false)
(vla-put-PlotViewportBorders activeTab :vlax-false)
(vla-put-PlotViewportsFirst activeTab :vlax-true)
(vla-put-PlotWithLineweights activeTab :vlax-true)
(vla-put-ScaleLineweights activeTab :vlax-false)
(vla-put-PlotWithPlotStyles activeTab :vlax-true)
(vla-put-ShowPlotStyles activeTab :vlax-true)
(vla-put-StyleSheet activeTab plotTable)
; отправляем Layout на печать в нужном количестве копий
(while (> copies 0)
(progn
(setq isPlotSuccess (vla-PlotToDevice (vla-get-Plot doc)))
(setq copies (1- copies))
)
)
)
)
Здесь пропущено чтение конфигов, логгирование и установка системных переменных чтобы не отвлекаться но мы помним что мы это тоже делаем.
Итак мы получили LISP-файл (IRL три), который выполняет то что нам требуется. Его мы будем выполнять в AutoCAD для чего нам понадобится скрипт выполняющийся при запуске, который будет загружать наш файл и выполнять методы из него. выгдядеть он будет примерно так:
(setq fileName "myFile.LSP")
(setq path "C:\\MyApp\\")
(load (findfile (strcat path fileName)))
(myFoo path plotStyle 1)
_quit
_y
Теперь перейдем к интерфейсному модулю.
2. Интерфейсный модуль
Здесь мы будем искать установленные на ПК экземпляры AutoCAD, профили AutoCAD, изменять настройки и, конечно, печатать файлы. Для последнего действия мы будем запускать в фоновом режиме выбранный экземпляр AutoCAD и отображать на UI прогресс и результат.
Я сознательно пропущу работу с конфигами, логгированием, реестром Windows и прочие стандартные движения.
Для поиска установленных AutoCAD мы заглянем в реестр. Нас интересует раздел HKCU\Software\Autodesk\AutoCAD. Раздел содержит подразделы для каждого установленного экземпляра AutoCAD с ключем CurVer. Напишем метод возращающий список из объектов со всей необходимой нам информацией:
vector<Acad::AcadApp> AcadSrv::GetInstalledAcad(){
vector<Acad::AcadApp> result;
wstring keyAcad = L"Software\\Autodesk\\AutoCAD";
vector<wstring> subKeys = Reg::RegFunc::GetSubKeys(HKEY_CURRENT_USER, keyAcad);
size_t subKeysSize = subKeys.size();
for (size_t i = 0; i < subKeysSize; i++){
wstring keyPath = keyAcad + L"\\" + subKeys[i];
wstring keyValue = Reg::RegFunc::GetParametr(HKEY_CURRENT_USER, keyPath, L"CurVer");
Acad::AcadApp *acd = new Acad::AcadApp(keyPath + L"\\" + keyValue);
if(acd->GetProductName() != L""){
result.push_back(*acd);
}
delete acd;
}
std::reverse(std::begin(result), std::end(result))
return result;
}
В конце результат переворачиваем для отображения более поздней версии ACAD первой. Получим список объектов вида:
class AcadApp
{
protected:
wstring productName;
wstring release;
wstring acadLocation;
wstring productId;
wstring localeId;
vector<wstring> acadProfiles;
wstring regKeyPath;
public:
AcadApp();
AcadApp(wstring acadKeyPath)
~AcadApp();
wstring GetProductName();
wstring GetRelease();
wstring GetAcadLocation();
wstring GetProductId();
wstring GetLocaleId();
wstring GetRegKeyPath();
vector<wstring> GetAcadProfiles();
vector<wstring> GetPlotters(wstring profile);
vector<pair<wstring, wstring>> GetProfileProperties(
wstring profile,
vector<wstring> keyList);
vector<pair<wstring, unsigned long>> GetProfileSysvars(
wstring profile,
vector<pair<wstring, unsigned long>> keyList);
bool SetProfileSysvars(
wstring profile,
vector<pair<wstring, unsigned long>> keyList);
bool SetProfileSysvars(
wstring profile,
vector<pair<wstring, wstring>> keyList);
bool IsSecureModeOn(wstring profile);
bool SecureModeChange(wstring profile);
private:
void SetProductName();
void SetRelease();
void SetAcadLocation();
void SetProductId();
void SetLocaleId();
void SetRegKeyPath(wstring path);
void SetAcadProfiles();
};
Под капотом Конструктор вызывает приватные сеттеры, в которых все те же обращения к интересным нам ключам реестра в ветках HKLM и HKCU:
AcadApp::AcadApp(wstring acadKeyPath){
SetRegKeyPath(acadKeyPath);
SetProductName();
SetRelease();
SetAcadLocation();
SetProductId();
SetLocaleId();
SetAcadProfiles();
}
void AcadApp::SetProductName(){
wstring currentPath = GetRegKeyPath();
productName = Reg::RegFunc::GetParametr(HKEY_LOCAL_MACHINE, currentPath, L"ProductName");
}
void AcadApp::SetRelease(){
wstring currentPath = GetRegKeyPath();
release = Reg::RegFunc::GetParametr(HKEY_LOCAL_MACHINE, currentPath, L"Release");
}
void AcadApp::SetAcadLocation(){
wstring currentPath = GetRegKeyPath();
acadLocation = Reg::RegFunc::GetParametr(HKEY_LOCAL_MACHINE, currentPath, L"AcadLocation");
}
void AcadApp::SetProductId(){
wstring currentPath = GetRegKeyPath();
productId = Reg::RegFunc::GetParametr(HKEY_LOCAL_MACHINE, currentPath, L"ProductId");
}
void AcadApp::SetLocaleId(){
wstring currentPath = GetRegKeyPath();
localeId = Reg::RegFunc::GetParametr(HKEY_LOCAL_MACHINE, currentPath, L"localeId");
}
void AcadApp::SetRegKeyPath(wstring str){
regKeyPath = str;
}
void AcadApp::SetAcadProfiles(){
wstring profilesPath = GetRegKeyPath() + L"\\Profiles";
acadProfiles = Reg::RegFunc::GetSubKeys(HKEY_CURRENT_USER, profilesPath);
}
C полученной коллекцией объектов и будем работать на UI далее: предложим пользователю выбрать экземпляр AutoCad с именем productName и профиль из списка acadProfiles, в которым будем работать далее.
Сконструирeм строку параметров запуска:
wstring params = fileName +
" /product ACAD /nologo /nossm /p \"" +
profile +
"\" /s " +
scriptFileName;
где fileName - путь к файлу DWG (если мы печатаем) или к файлу шаблона DWT (если собираем информацию о принтерах), profile - выбранный профиль, а scriptFileName - путь к скрипту, который будет выполнен при старте. Скрипт мы будем создавать "на лету" во временном каталоге пользователя и удалять после завершения работы.
Начиная с AutoCAD2013 Autodesk любезно предоставляет приложение AcCoreConsole, которое предоставляет консольный сеанс AutoCAD без пачки сторонних процессов. Идеально для для наших автоматизаторских целей. Запустим его, выполнив методы:
wstring AcadSrv::GetAcadFullName(Acad::AcadApp *acd){
wstring result = acd->GetAcadLocation() + L"\\accoreconsole.exe";
return result;
}
HANDLE AcadSrv::StartApplication(wstring name, wstring params, bool hidden){
HANDLE handle = NULL;
unsigned short show = hidden ? SW_HIDE : SW_SHOWNORMAL;
wstring strTmp = name + L" " + params;
LPWSTR cmdStr = new TCHAR[strTmp.size() + 1];
std::copy(strTmp.begin(), strTmp.end(), cmdStr);
cmdStr[strTmp.size()] = 0;
STARTUPINFO sti;
ZeroMemory(&sti, sizeof(STARTUPINFO));
PROCESS_INFORMATION pi;
sti.dwFlags = STARTF_USESHOWWINDOW;
sti.wShowWindow = show;
bool res = CreateProcess(
NULL,
cmdStr,
NULL,
NULL,
FALSE,
0,
NULL,
NULL,
&sti,
&pi);
if(res){
handle = pi.hProcess;
}
delete[] cmdStr;
return handle;
}
Для скрытого запуска мы передадим false в параметре hidden. Запускать будем в новом потоке, для чего введем класс Thread наследуясь от QThread
namespace Ui {
class Thread : public QThread
{
Q_OBJECT
public:
Thread();
~Thread();
void SetArg(QString str,
QStringList fileNameListIn,
QString appNameIn,
QString profileIn,
QString supportPath,
int timeOut,
bool isSilent);
protected:
void run();
signals:
void progress(int);
void finish(int);
void reportResult(int);
void reportItemResult(int);
public slots:
void Canceled();
private:
bool running;
int count;
QString scriptFileName;
QStringList fileNameList;
QString appName;
QString profile;
QString supportPath;
int timeOut;
bool isSilent;
};
}
где в методе run() мы и будем вызывать StartApplication
void Thread::run(){
int appResult = 0;
running = true;
count = 0;
scriptFileName = Files::FilesQt::ChangeSlash(scriptFileName);
scriptFileName = Files::FilesQt::AddQuotes(scriptFileName);
while (!fileNameList.empty() && (running == true)){
int result = 1;
QString fileName = fileNameList.front();
fileNameList.pop_front();
fileName = Files::FilesQt::ChangeSlash(fileName);
fileName = Files::FilesQt::AddQuotes(fileName);
QString params = "/i " + Files::FilesQt::AddSlash(fileName) +
" /product ACAD /p \"" +
profile +
"\" /s " +
Files::FilesQt::AddSlash(scriptFileName);
if (supportPath != ""){
params = params +
" /supportfilesmap \"" +
supportPath +
"\"";
}
wstring wsCmdLine = params.toStdWString();
wstring wsAppName = appName.toStdWString();
HANDLE handle = Acad::AcadSrv::StartApplication(
wsAppName,
wsCmdLine,
this->isSilent
);
if(handle != NULL){
result = Acad::AcadSrv::WaitForAppFinished(handle, timeOut);
}
if(result == 1){
appResult = 1;
}
count++;
emit progress(count);
emit reportItemResult(result);
}
emit finish(count);
emit reportResult(appResult);
}
Здесь мы уже сталкиваемся с различиями между QString и std::wstring и необходимостью явного приведения типов. Таков костыль путь, если мы стараемся избегать использования QT для уровня ниже интерфейсного.
Теперь мы можем воспользоваться нашим классом Thread, для информирования пользователя о прогрессе будем использовать модальное окно QProgressDialog()
Ввиду большого объема спрячем под кат
void MainWindow::StartACDInThread(QStringList fileNameList,
QString scriptFileName,
QString profile,
QString path,
QString text,
int timeOut,
bool silent){
wstring wsAppName = Acad::AcadSrv::GetAcadFullName(acd);
QString appName = QString::fromStdWString(wsAppName);
QString supportPath;
for(size_t i = 0; i < currentProfilePropertyesList.size(); i++){
if(currentProfilePropertyesList[i].first == L"ACAD"){
supportPath = QString::fromStdWString(currentProfilePropertyesList[i].second);
}
};
if(!path.isEmpty()){
supportPath = path + ";" + supportPath;
}
QPalette plt = this->palette();
plt.setColor(QPalette::Highlight, QColor(77, 166, 255, 255));
count = 0;
threadObject = new Ui::Thread;
threadObject->SetArg(scriptFileName,
fileNameList,
appName,
profile,
supportPath,
timeOut,
silent);
connect(this, &MainWindow::workCanceled, threadObject, &Ui::Thread::Canceled);
connect(threadObject, &Ui::Thread::progress, this, &MainWindow::SetValue);
connect(threadObject, &Ui::Thread::finish, this, &MainWindow::AppStop);
connect(threadObject, &Ui::Thread::reportItemResult, this, &MainWindow::ItemReport);
connect(threadObject, &Ui::Thread::reportResult, this, &MainWindow::AppReport);
threadObject->start();
appRuning = true;
QProgressDialog *pprd = new QProgressDialog();
pprd->setModal(true);
pprd->setWindowFlags(Qt::WindowTitleHint | Qt::CustomizeWindowHint);
pprd->setWindowTitle("Обработка задания");
pprd->setLabelText(text);
pprd->setMinimum(0);
pprd->setMinimumDuration(0);
pprd->setAutoReset(false);
pprd->setAutoClose(true);
pprd->setPalette(plt);
int countMax;
if (!printEnable){
pprd->setCancelButton(0);
QTime time;
time.start();
int i;
pprd->setMaximum(timeOut);
while (appRuning){
i = time.elapsed();
pprd->setValue(i);
qApp->processEvents();
if(pprd->wasCanceled()){
emit workCanceled();
break;
}
}
}
else{
countMax = fileNameList.size();
pprd->setMaximum(countMax);
while(count < countMax){
QString textTmp = " " + QString::number(count + 1) + " из " + QString::number(countMax) + ":\n";
QString fileName = fileNameList[count]; // current drawing file
this->currentFile = fileName.toStdWString();
pprd->setLabelText(text + textTmp + fileName);
pprd->setValue(count);
qApp->processEvents();
if(pprd->wasCanceled()){
pprd->setWindowTitle("Отмена задания");
emit workCanceled();
break;
}
}
}
pprd->setValue(pprd->maximum());
threadObject->wait();
delete pprd;
delete threadObject;
}
Компилируем и наслаждаемся появлением процесса accoreconsole.exe в диспетчере задач и шуршанием бумаги за стеной (нет). Ну, окей... открываем командную строку, в ней accoreconsole.exe, запускаем наш скрипт вручную...

Курение гугла и англоязычных форумов дает ответ:
ActiveX isn't supported in accoreconsole
Штош, будем запускать acad.exe со всем зоопарком который он за компанию потянет. для этого изменим ключи запуска в методе Thread::run():
QString params = Files::FilesQt::AddSlash(fileName) +
" /product ACAD /nologo /nossm /p \"" +
profile +
"\" /b " +
Files::FilesQt::AddSlash(scriptFileName);
if (supportPath != ""){
params = params +
" /s \"" +
supportPath +
"\"";
И исправим имя запускаемого приложения
wstring AcadSrv::GetAcadFullName(Acad::AcadApp *acd){
wstring result = acd->GetAcadLocation() + L"\\acad.exe";
return result;
}
Компилируем заново и теперь действительно слушаем звуки из-за стены. PROFIT!
Кто украл пирожные или Проблемы и костыли
При скрытом запуске внезапно нет способа в рантайме узнать об ошибках и вообще о том что происходит при выполнении скрипта в сеансе AutoCAD. Максимум что мы можем - увидеть не упал ли процесс с ошибкой. Чтобы обойти это мы должны логгировать результаты и ошибки в среде AutoCAD. По этой же причине введен таймаут, после которого мы будем убивать дочерний процесс случае "зависания".
Нельзя просто так взять и прочитать PC3 файлы виртуальных принтеров. Поэтому - танцы с запуском AutoCAD при настройке приложения. Обмен данными между приложениями? Через файловую систему.
Разные типы данных в стандартной библиотеке и QT требуют прямого и обратного приведения. Таков путь.
И на закуску - тот самый волшебный консольный AcCoreConsole, который Autodesk прикрутил в 2012 году специально для задач автоматизации НЕ РАБОТАЕТ с ActiveX и объектной моделью AutoCAD в lisp. Даже сейчас. Скажем НЕТ быстродействию и ДА - всей пачке фоновых процессов, что тянет за собой acad.exe. Слава Autodesk
Показания Алисы или Результат
В процессе решения задачи я был и архитектором, и единственным разработчиком решения - от проектирования до внедрения и поддержки. В результате изысканий и экспериментов было создано десктопное приложение с интуитивно понятным для целевой аудитории (инженер-проектировщик с уровнем владения ПК выше минимального) интерфейсом. Решение не оптимально, но стало инновацией, позволившей пакетно печатать большое количество файлов чертежей. В том числе с внешними ссылками и с прокси-графикой. И снизить количество ошибок при печати.
Как дополнительный кейс Приложение получило возможность сохранения чертежей в PDF формат в автоматическом режиме в указанный каталог.

Приложение позволяло работать с ним по принципу "выстрелил и забыл": при первичной настройке пользователь указывал принтеры которые следовало использовать для чертежей разных размеров, принтер для создания PDF и при последующих запусках все его действия сводились к добавлению файлов, выбору стиля печати и указанию количества экземпляров. Или папки куда сохранять файлы PDF.
Что дальше?
Дальше были презентация, внедрение, вопросы пользователей (решаемые чтением мануала об одной странице). Пользователи оценили как выросла производительность и надёжность печати. Затем пришел ковид и... Ничего не изменилось. Единственное что потребовалось от разработчика, когда все ушли на удаленку - выпустить патч увеличивающий таймауты в конфигах.
2020-2021 годы стали периодом тотальной цифровизации и все выглядело так, что необходимость в печати чертежей уже неактуальна. Уже в 2021 году основным сценарием использования стала добавленная "в довесок" конвертации чертежей в PDF.
Со временем приложение показало себя как простое и удобное для пользователей, гибкое в настройке и надежное даже при ограниченной поддержке и нестабильной сети. И как показатель его жизнеспособности - спустя пять лет после внедрения пользователи продолжают обращаться с вопросами. Значит, решение по-прежнему актуально и работает на практике.
Как то так...
kosmonaFFFt
Увидел у вас в C++ коде new и delete. Рекомендую присмотреться к std::unique_ptr и std::shared_ptr. Ну или их аналоги из Qt.