├── parse_design_documents.py
└── README.md
/parse_design_documents.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | import subprocess
4 | import pythoncom
5 | from win32com.client import Dispatch, gencache
6 | from tkinter import Tk
7 | from tkinter.filedialog import askopenfilenames
8 |
9 |
10 | # Подключение к API7 программы Компас 3D
11 | def get_kompas_api7():
12 | module = gencache.EnsureModule("{69AC2981-37C0-4379-84FD-5DD2F3C0A520}", 0, 1, 0)
13 | api = module.IKompasAPIObject(
14 | Dispatch("Kompas.Application.7")._oleobj_.QueryInterface(module.IKompasAPIObject.CLSID,
15 | pythoncom.IID_IDispatch))
16 | const = gencache.EnsureModule("{75C9F5D0-B5B8-4526-8681-9903C567D2ED}", 0, 1, 0).constants
17 | return module, api, const
18 |
19 |
20 | # Функция проверки, запущена-ли программа КОМПАС 3D
21 | def is_running():
22 | proc_list = \
23 | subprocess.Popen('tasklist /NH /FI "IMAGENAME eq KOMPAS*"', shell=False, stdout=subprocess.PIPE).communicate()[0]
24 | return True if proc_list else False
25 |
26 |
27 | # Посчитаем количество листов каждого из формата
28 | def amount_sheet(doc7):
29 | sheets = {"A0": 0, "A1": 0, "A2": 0, "A3": 0, "A4": 0, "A5": 0}
30 | for sheet in range(doc7.LayoutSheets.Count):
31 | format = doc7.LayoutSheets.Item(sheet).Format # sheet - номер листа, отсчёт начинается от 0
32 | sheets["A" + str(format.Format)] += 1 * format.FormatMultiplicity
33 | return sheets
34 |
35 |
36 | # Прочитаем основную надпись чертежа
37 | def stamp(doc7):
38 | for sheet in range(doc7.LayoutSheets.Count):
39 | style_filename = os.path.basename(doc7.LayoutSheets.Item(sheet).LayoutLibraryFileName)
40 | style_number = int(doc7.LayoutSheets.Item(sheet).LayoutStyleNumber)
41 |
42 | if style_filename in ['graphic.lyt', 'Graphic.lyt'] and style_number == 1:
43 | stamp = doc7.LayoutSheets.Item(sheet).Stamp
44 | return {"Scale": re.findall(r"\d+:\d+", stamp.Text(6).Str)[0],
45 | "Designer": stamp.Text(110).Str}
46 |
47 | return 'Неопределенный стиль оформления'
48 |
49 |
50 | # Подсчет технических требований, в том случае, если включена автоматическая нумерация
51 | def count_demand(doc7, module7):
52 | IDrawingDocument = doc7._oleobj_.QueryInterface(module7.NamesToIIDMap['IDrawingDocument'], pythoncom.IID_IDispatch)
53 | drawing_doc = module7.IDrawingDocument(IDrawingDocument)
54 | text_demand = drawing_doc.TechnicalDemand.Text
55 |
56 | count = 0 # Количество пунктов технических требований
57 | for i in range(text_demand.Count): # Прохоим по каждой строчке технических требований
58 | if text_demand.TextLines[i].Numbering == 1: # и проверяем, есть ли у строки нумерация
59 | count += 1
60 |
61 | # Если нет нумерации, но есть текст
62 | if not count and text_demand.TextLines[0]:
63 | count += 1
64 |
65 | return count
66 |
67 |
68 | # Подсчёт размеров на чертеже, для каждого вида по отдельности
69 | def count_dimension(doc7, module7):
70 | IKompasDocument2D = doc7._oleobj_.QueryInterface(module7.NamesToIIDMap['IKompasDocument2D'],
71 | pythoncom.IID_IDispatch)
72 | doc2D = module7.IKompasDocument2D(IKompasDocument2D)
73 | views = doc2D.ViewsAndLayersManager.Views
74 |
75 | count_dim = 0
76 | for i in range(views.Count):
77 | ISymbols2DContainer = views.View(i)._oleobj_.QueryInterface(module7.NamesToIIDMap['ISymbols2DContainer'],
78 | pythoncom.IID_IDispatch)
79 | dimensions = module7.ISymbols2DContainer(ISymbols2DContainer)
80 |
81 | # Складываем все необходимые раpмеры
82 | count_dim += dimensions.AngleDimensions.Count + \
83 | dimensions.ArcDimensions.Count + \
84 | dimensions.Bases.Count + \
85 | dimensions.BreakLineDimensions.Count + \
86 | dimensions.BreakRadialDimensions.Count + \
87 | dimensions.DiametralDimensions.Count + \
88 | dimensions.Leaders.Count + \
89 | dimensions.LineDimensions.Count + \
90 | dimensions.RadialDimensions.Count + \
91 | dimensions.RemoteElements.Count + \
92 | dimensions.Roughs.Count + \
93 | dimensions.Tolerances.Count
94 |
95 | return count_dim
96 |
97 |
98 | def parse_design_documents(paths):
99 | is_run = is_running() # True, если программа Компас уже запущена
100 |
101 | module7, api7, const7 = get_kompas_api7() # Подключаемся к программе
102 | app7 = api7.Application # Получаем основной интерфейс программы
103 | app7.Visible = True # Показываем окно пользователю (если скрыто)
104 | app7.HideMessage = const7.ksHideMessageNo # Отвечаем НЕТ на любые вопросы программы
105 |
106 | table = [] # Создаём таблицу парметров
107 | for path in paths:
108 | doc7 = app7.Documents.Open(PathName=path,
109 | Visible=True,
110 | ReadOnly=True) # Откроем файл в видимом режиме без права его изменять
111 |
112 | row = amount_sheet(doc7) # Посчитаем кол-во листов каждого формат
113 | row.update(stamp(doc7)) # Читаем основную надпись
114 | row.update({
115 | "Filename": doc7.Name, # Имя файла
116 | "CountTD": count_demand(doc7, module7), # Количество пунктов технических требований
117 | "CountDim": count_dimension(doc7, module7), # Количество пунктов технических требований
118 | })
119 | table.append(row) # Добавляем строку параметров в таблицу
120 |
121 | doc7.Close(const7.kdDoNotSaveChanges) # Закроем файл без изменения
122 |
123 | if not is_run: app7.Quit() # Закрываем программу при необходимости
124 | return table
125 |
126 |
127 | def print_to_excel(result):
128 | excel = Dispatch("Excel.Application") # Подключаемся к программе Excel
129 | excel.Visible = True # Делаем окно видимым
130 | wb = excel.Workbooks.Add() # Добавляем новую книгу
131 | sheet = wb.ActiveSheet # Получаем ссылку на активный лист
132 |
133 | # Создаём заголовок таблицы
134 | sheet.Range("A1:J1").value = ["Имя файла", "Разработчик", "Кол-во размеров", "Кол-во пунктов ТТ",
135 | "А0", "А1", "А2", "А3", "А4", "Масштаб"]
136 |
137 | # Заполняем таблицу
138 | for i, row in enumerate(result):
139 | sheet.Cells(i + 2, 1).value = row['Filename']
140 | sheet.Cells(i + 2, 2).value = row['Designer']
141 | sheet.Cells(i + 2, 3).value = row['CountDim']
142 | sheet.Cells(i + 2, 4).value = row['CountTD']
143 | sheet.Cells(i + 2, 5).value = row['A0']
144 | sheet.Cells(i + 2, 6).value = row['A1']
145 | sheet.Cells(i + 2, 7).value = row['A2']
146 | sheet.Cells(i + 2, 8).value = row['A3']
147 | sheet.Cells(i + 2, 9).value = row['A4']
148 | sheet.Cells(i + 2, 10).value = "".join(('="', row['Scale'], '"'))
149 |
150 |
151 | if __name__ == "__main__":
152 | root = Tk()
153 | root.withdraw() # Скрываем основное окно и сразу окно выбора файлов
154 |
155 | filenames = askopenfilenames(title="Выберети чертежи деталей", filetypes=[('Компас 3D', '*.cdw'), ])
156 |
157 | print_to_excel(parse_design_documents(filenames))
158 |
159 | root.destroy() # Уничтожаем основное окно
160 | root.mainloop()
161 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Python на службе у конструктора. Укрощаем API Kompas 3D
2 | 
3 |
4 | Работая в конструкторском отделе, я столкнулся с задачей - рассчитать трудоёмкость разработки конструкторской документации. Если брать за основу документ: [«Типовые нормативы времени на разработку конструкторской документации. ШИФР 13.01.01" (утв. Минтрудом России 07.03.2014 N 003)»](http://www.consultant.ru/document/cons_doc_LAW_199653/), то для расчета трудоёмкости чертежа детали нам необходимы следующие данные:
5 |
6 | * Формат чертежа и количество листов
7 | * Масштаб
8 | * Количество размеров на чертеже (включая знаки шероховатости и выносные линии)
9 | * Количество технических требований
10 |
11 | Из имеющихся инструментов на предприятии имеем: Kompas 3D v14 и Python 3.5.
12 |
13 | В интернете не так много статей о написании программ с использованием API Kompas 3D, и ещё меньше информации о том, как это сделать на Python. Попробую рассказать по шагам, как решалась поставленная задача и на какие грабли приходилось наступать. Статья рассчитана на людей, владеющих основами программирования и знакомых с языком Python. Итак, приступим.
14 |
15 | ## Подготовительная операция:
16 | Убедитесь, что на вашем компьютере установлена программа Kompas 3D, версии не ниже 14 и Python 3. Вам также необходимо установить [pywin3](http://sourceforge.net/projects/pywin32/) (Python for Windows extensions).
17 |
18 | ## Подключение к Kompas 3D:
19 | Система Kompas 3D имеет две версии API: API5, которая предоставляет интерфейс KompasObject, и API7, предоставляющая интерфейс IKompasAPIObject. API версии 5 и 7 во многом дублируют свой функционал, но, со слов разработчиков, в 7-ой версии более выражен объектно-ориентированный подход. В данной статье акцент сделан на 7-ю версию.
20 |
21 | Функция подключения выглядит следующим образом:
22 |
23 | ```
24 | import pythoncom
25 | from win32com.client import Dispatch, gencache
26 |
27 | # Подключение к API7 программы Kompas 3D
28 | def get_kompas_api7():
29 | module = gencache.EnsureModule("{69AC2981-37C0-4379-84FD-5DD2F3C0A520}", 0, 1, 0)
30 | api = module.IKompasAPIObject(
31 | Dispatch("Kompas.Application.7")._oleobj_.QueryInterface(module.IKompasAPIObject.CLSID,
32 | pythoncom.IID_IDispatch))
33 | const = gencache.EnsureModule("{75C9F5D0-B5B8-4526-8681-9903C567D2ED}", 0, 1, 0).constants
34 | return module, api, const
35 | ```
36 | Чуть подробнее о модуле win32com [здесь](http://docs.activestate.com/activepython/3.4/pywin32/html/com/win32com/HTML/GeneratedSupport.html).
37 |
38 | Теперь, чтобы подключиться к интерфейсу, нам понадобиться следующий код:
39 |
40 | ```
41 | module7, api7, const7 = get_kompas_api7() # Подключаемся к API7
42 | app7 = api7.Application # Получаем основной интерфейс
43 | app7.Visible = True # Показываем окно пользователю (если скрыто)
44 | app7.HideMessage = const7.ksHideMessageNo # Отвечаем НЕТ на любые вопросы программы
45 | print(app7.ApplicationName(FullName=True)) # Печатаем название программы
46 | ```
47 | Для более глубокого понимания API заглянем в SDK. На моём компьютере она находится по адресу: C:\Program Files\ASCON\KOMPAS-3D V16\SDK\SDK.chm. Здесь можно подробнее узнать, например, о методе HideMessage:
48 |
49 | 
50 |
51 | После выполнения нашего кода вернём всё на свои места: если Kompas 3D был запущен нами (в процессе работы скрипта), мы его и закроем. Самый простой способ определить, запущен ли процесс, - использовать стандартный модуль subprocess:
52 |
53 | ```
54 | import subprocess
55 |
56 | # Функция проверяет, запущена ли программа Kompas 3D
57 | def is_running():
58 | proc_list = subprocess.Popen('tasklist /NH /FI "IMAGENAME eq KOMPAS*"',
59 | shell=False,
60 | stdout=subprocess.PIPE).communicate()[0]
61 | return True if proc_list else False
62 | ```
63 |
64 | Данная функция проверяет, запущен ли процесс «KOMPAS» [стандартными методами Windows](https://technet.microsoft.com/en-us/library/bb491010.aspx#mainSection). Обратите внимание, что разные версии программы Kompas 3D могут иметь разные наименования процессов!
65 |
66 | ## Считаем количество листов и их формат:
67 | Тут всё просто: у нашего документа doc7 имеется интерфейс коллекции листов оформления LayoutSheets. Каждый лист обладает свойством формата и кратности. Для Компаса, начиная с 15 версии, интерфейс LayoutSheets доступен не только для файлов чертежей, но и для спецификаций и текстовых документов.
68 |
69 | ```
70 | # Посчитаем количество листов каждого из формата
71 | def amount_sheet(doc7):
72 | sheets = {"A0": 0, "A1": 0, "A2": 0, "A3": 0, "A4": 0, "A5": 0}
73 | for sheet in range(doc7.LayoutSheets.Count):
74 | format = doc7.LayoutSheets.Item(sheet).Format # sheet - номер листа, отсчёт начинается от 0
75 | sheets["A" + str(format.Format)] += 1 * format.FormatMultiplicity
76 | return sheets
77 | ```
78 |
79 | Посмотрим на процесс изучения SDK для поиска интересующих нас функций:
80 |
81 | 
82 |
83 |
84 | ## Читаем основную надпись:
85 | Здесь нам поможет всё тот же LayoutSheets:
86 |
87 | ```
88 | # Прочитаем масштаб из штампа, ячейка №6
89 | def stamp_scale(doc7):
90 | stamp = doc7.LayoutSheets.Item(0).Stamp # Item(0) указывает на штамп первого листа
91 | return stamp.Text(6).Str
92 | ```
93 |
94 | На самом деле ячейка №6 для листа с другим оформлением может содержать не масштаб, а совсем иную информацию. Посмотрим, как в Kompas 3D определяются стили оформления чертежа:
95 |
96 | 
97 |
98 | Таким образом, важно проверять, какому файлу и номеру оформления соответствует лист чертежа. Также стоит помнить, что документ может содержать титульный лист! Поэтому придётся усложнить код. Применим [регулярные выражения](https://habrahabr.ru/post/115825/), т.к. текст в ячейке может являться ссылкой:
99 |
100 | ```
101 | import os
102 | import re
103 |
104 | # Прочитаем основную надпись чертежа
105 | def stamp(doc7):
106 | for sheet in range(doc7.LayoutSheets.Count):
107 | style_filename = os.path.basename(doc7.LayoutSheets.Item(sheet).LayoutLibraryFileName)
108 | style_number = int(doc7.LayoutSheets.Item(sheet).LayoutStyleNumber)
109 |
110 | if style_filename in ['graphic.lyt', 'Graphic.lyt'] and style_number == 1:
111 | stamp = doc7.LayoutSheets.Item(sheet).Stamp
112 | return {"Scale": re.search(r"\d+:\d+", stamp.Text(6).Str).group(),
113 | "Designer": stamp.Text(110).Str}
114 |
115 | return {"Scale": 'Неопределенный стиль оформления',
116 | "Designer": 'Неопределенный стиль оформления'}
117 | ```
118 |
119 | Остался последний вопрос: как узнать нужный номер ячейки? Для этих целей удобно создать файл чертежа, в котором интересующие нас ячейки будут заполнены, а после - прочитать все возможные варианты с помощью следующей функции:
120 |
121 | ```
122 | # Просмотр всех ячеек
123 | def parse_stamp(doc7, number_sheet):
124 | stamp = doc7.LayoutSheets.Item(number_sheet).Stamp
125 | for i in range(10000):
126 | if stamp.Text(i).Str:
127 | print('Номер ячейки = %-5d Значение = %s' % (i, stamp.Text(i).Str))
128 | ```
129 |
130 | ## Считаем количество пунктов технических требований:
131 | Согласно SDK, нам всего-то нужно получить интерфейс TechnicalDemand от IDrawingDocument, а
132 | IDrawingDocument можно получить от iDocuments с помощью замечательного метода с говорящим названием IUnknown::QueryInterface. И только в SDK 16 версии Kompas 3D появилось разъяснение, как это сделать:
133 |
134 | 
135 |
136 | С такими разъяснениями легко написать следующее:
137 |
138 | ```
139 | # Подсчет технических требований, в том случае, если включена автоматическая нумерация
140 | def count_TT(doc7, module7):
141 | doc2D_s = doc7._oleobj_.QueryInterface(module7.NamesToIIDMap['IDrawingDocument'],
142 | pythoncom.IID_IDispatch)
143 | doc2D = module7.IDrawingDocument(doc2D_s)
144 | text_TT = doc2D.TechnicalDemand.Text
145 |
146 | count_tt = 0 # Количество пунктов технических требований
147 | for i in range(text_TT.Count): # Проходим по каждой строчке технических требований
148 | if text_TT.TextLines[i].Numbering == 1: # и проверяем, есть ли у строки нумерация
149 | count_tt += 1
150 |
151 | # Если нет нумерации, но есть текст
152 | if not count_tt and text_TT.TextLines[0]:
153 | count_tt += 1
154 |
155 | return count_tt
156 | ```
157 |
158 | Стоит отметить, что данный код полагается на автоматическую нумерацию технических требований. Так что, если автоматическая нумерация не применялась или технические требования набраны с использованием простого инструмента «Текст», код будет сложнее. Оставляю решение данной задачи на читателя.
159 |
160 | ## Считаем количество размеров на чертеже:
161 | При подсчёте размеров, надо иметь в виду, что необходимо посчитать их на каждом из видов чертежа:
162 |
163 | ```
164 | # Подсчёт размеров на чертеже, для каждого вида по отдельности
165 | def count_dimension(doc7, module7):
166 | IKompasDocument2D = doc7._oleobj_.QueryInterface(module7.NamesToIIDMap['IKompasDocument2D'],
167 | pythoncom.IID_IDispatch)
168 | doc2D = module7.IKompasDocument2D(IKompasDocument2D)
169 | views = doc2D.ViewsAndLayersManager.Views
170 |
171 | count_dim = 0
172 | for i in range(views.Count):
173 | ISymbols2DContainer = views.View(i)._oleobj_.QueryInterface(module7.NamesToIIDMap['ISymbols2DContainer'],
174 | pythoncom.IID_IDispatch)
175 | dimensions = module7.ISymbols2DContainer(ISymbols2DContainer)
176 |
177 | # Складываем все необходимые размеры
178 | count_dim += dimensions.AngleDimensions.Count + \
179 | dimensions.ArcDimensions.Count + \
180 | dimensions.Bases.Count + \
181 | dimensions.BreakLineDimensions.Count + \
182 | dimensions.BreakRadialDimensions.Count + \
183 | dimensions.DiametralDimensions.Count + \
184 | dimensions.Leaders.Count + \
185 | dimensions.LineDimensions.Count + \
186 | dimensions.RadialDimensions.Count + \
187 | dimensions.RemoteElements.Count + \
188 | dimensions.Roughs.Count + \
189 | dimensions.Tolerances.Count
190 |
191 | return count_dim
192 | ```
193 |
194 | ## Основная функция скрипта
195 | В результате проделанной работы мы получили следующее:
196 |
197 | ```
198 | def parse_design_documents(paths):
199 | is_run = is_running() # Установим флаг, который нам говорит,
200 | # запущена ли программа до запуска нашего скрипта
201 |
202 | module7, api7, const7 = get_kompas_api7() # Подключаемся к программе
203 | app7 = api7.Application # Получаем основной интерфейс программы
204 | app7.Visible = True # Показываем окно пользователю (если скрыто)
205 | app7.HideMessage = const7.ksHideMessageNo # Отвечаем НЕТ на любые вопросы программы
206 |
207 | table = [] # Создаём таблицу параметров
208 | for path in paths:
209 | doc7 = app7.Documents.Open(PathName=path,
210 | Visible=True,
211 | ReadOnly=True) # Откроем файл в видимом режиме без права его изменять
212 |
213 | row = amount_sheet(doc7) # Посчитаем кол-во листов каждого формат
214 | row.update(stamp(doc7)) # Читаем основную надпись
215 | row.update({
216 | "Filename": doc7.Name, # Имя файла
217 | "CountTD": count_demand(doc7, module7), # Количество пунктов технических требований
218 | "CountDim": count_dimension(doc7, module7), # Количество размеров на чертеже
219 | })
220 | table.append(row) # Добавляем строку параметров в таблицу
221 |
222 | doc7.Close(const7.kdDoNotSaveChanges) # Закроем файл без изменения
223 |
224 | if not is_run: app7.Quit() # Выходим из программы
225 | return table
226 | ```
227 |
228 | ## Диалоговое окно выбора файлов
229 | Для удобного использования нашего скрипта воспользуемся возможностями стандартного модуля tkinter и выведем диалоговое окно выбора файлов:
230 |
231 | ```
232 | from tkinter import Tk
233 | from tkinter.filedialog import askopenfilenames
234 |
235 | if __name__ == "__main__":
236 | root = Tk()
237 | root.withdraw() # Скрываем основное окно и сразу окно выбора файлов
238 |
239 | filenames = askopenfilenames(title="Выберете чертежи деталей",
240 | filetypes=[('Kompas 3D', '*.cdw'),])
241 |
242 | print_to_excel(parse_design_documents(filenames))
243 |
244 | root.destroy() # Уничтожаем основное окно
245 | root.mainloop()
246 | ```
247 |
248 | ## Отчётность
249 | Чтобы не рисовать в tkintere интерфейс пользователя, предлагаю воспользоваться хорошей программой Excel, куда и выведем результат нашего труда:
250 |
251 | ```
252 | def print_to_excel(result):
253 | excel = Dispatch("Excel.Application") # Подключаемся к Excel
254 | excel.Visible = True # Делаем окно видимым
255 | wb = excel.Workbooks.Add() # Добавляем новую книгу
256 | sheet = wb.ActiveSheet # Получаем ссылку на активный лист
257 |
258 | # Создаём заголовок таблицы
259 | sheet.Range("A1:J1").value = ["Имя файла", "Разработчик",
260 | "Кол-во размеров", "Кол-во пунктов ТТ",
261 | "А0", "А1", "А2", "А3", "А4", "Масштаб"]
262 |
263 | # Заполняем таблицу
264 | for i, row in enumerate(result):
265 | sheet.Range("A%d:J%d" % i+2).value = [row['Filename'],
266 | row['Designer'],
267 | row['CountDim'],
268 | row['CountTD'],
269 | row['A0'],
270 | row['A1'],
271 | row['A2'],
272 | row['A3'],
273 | row['A4'],
274 | "".join(('="', row['Scale'], '"'))]
275 | ```
276 |
277 | Если не полениться и правильно подготовить Excel файл, то результат работы нашего скрипта можно сразу представить в наглядном виде:
278 |
279 | 
280 |
281 | На графике изображено участие каждого сотрудника отдела в выпуске документации на изделие.
282 |
283 | ## Заключение
284 | Используя скрипт для чертежа, созданного специально для данной статьи, мы получим следующие результаты:
285 | * Количество размеров: 25 штук
286 | * Количество пуктов технических требований: 3 штуки
287 | * Масштаб: 1:1
288 | * Количетво листов: 1, формата А3
289 |
290 | Трудозатраты, согласно упомянатому в начале статьи документу, составили: 1 час 20 минут. Как ни странно, примерно столько и было потрачено времени на разработку чертежа.
291 | Конечно, для внедрения подобных норм на предприятии нужны более серьёзные исследования и совершенно другие объёмы конструкторской документации. Данная же статья поможет упростить работу с API Kompas 3D при решении аналогичных задач.
292 | Буду рад любым вашим замечаниями и предложениями к статье.
293 |
294 | ## Исходный код
295 | * [GitHub](https://github.com/Weltraum/Article-Python-in-aid-of-the-designer.-Tames-API-Kompas-3D.)
296 |
297 | ## Ресурсы в помощь:
298 | 1. SDK (C:\Program Files\ASCON\KOMPAS-3D V16\SDK\SDK.chm)
299 | 2. [forum.ascon.ru](forum.ascon.ru)
300 |
--------------------------------------------------------------------------------