
上一篇写到一半的时候有个朋友问我Polyworks 的宏脚本都已经写了十几篇了为什么还要折腾 Python 去连 COM这个问题正好戳在了关键点上——宏脚本跑在 Polyworks 进程内部再方便也解决不了外部系统调度、批量处理和数据对接的问题。想通这一点之后我花了两周时间把 Python 连接 Polyworks COM 组件的路子彻底走了一遍这篇笔记就把整个过程、踩过的坑和最终能跑通的代码骨架完整记录下来。这篇内容适合两类人一类是用 Polyworks 做测量但被大量重复手工操作折磨的工程师另一类是已经写过几期 Polyworks 宏脚本、想往自动化方向再走一步的开发者。不需要你精通 COM但你最好对 Python 的基本语法有过手经验知道 pip 怎么装包就行。1. Python 连接 Polyworks 的 COM为什么非走这条路不可1.1 Polyworks 自带宏脚本的边界在哪里Polyworks 本身提供了宏录制和脚本能力这在日常单点操作里确实很好用。但我在实际项目中碰到了三个绕不过去的问题第一个是跨系统数据流转。现场测量设备跑完一轮之后结果数据要进企业数据库、要推送 MES 系统、还要按订单号归档生成报告。宏脚本做这种事非常别扭字符串拼接、数据库驱动、网络请求这些能力它都有但写起来处处受限。用 Python 就不一样pandas、openpyxl、requests 这些库拿来即用。第二个是算法扩展。测量数据拿到之后需要做统计分析、异常判定、甚至训练一下简单的分类模型。宏脚本环境里做这种事情几乎等于重新发明轮子而 Python 环境里这些都是成熟方案。第三个是可维护性。宏脚本散落在各台测量电脑上版本管理基本靠复制粘贴文件名。把核心逻辑用 Python 统一管起来至少能放进 Git 里做版本管理。注意我这里说的边界不是否定宏脚本。恰恰相反简单交互式的日常操作用宏脚本最快。Python 方案应该定位成自动化引擎而不是替代手动操作的另一种方式。1.2 COM 方案相比宏脚本的本质变化要理解 Python 连接 Polyworks必须先搞清楚 COM 是什么。COM 的全称是 Component Object Model是 Windows 平台上的一种组件通信标准。打个比方COM 就像是给软件开了一扇带标准门锁的后门外部程序只要拿对了钥匙就能进去调用它暴露出来的功能。Polyworks 把它的核心测量和分析能力封装成了 COM 组件暴露给外部程序调用。这意味着调用发生在进程之外。宏脚本是 Polyworks 内部执行的一旦主程序崩溃脚本也随之中断。Python 通过 COM 连接后是独立的进程即使 Polyworks 界面卡死Python 端还能做异常处理、保存中间数据、甚至重启 Polyworks。语言无关。COM 是二进制层面的标准接口任何支持 COM 的语言都能调用不止 PythonC#、VB、甚至 PowerShell 都可以。自动化链路可以被外部程序编排。一个触发信号过来Python 可以自动打开 Polyworks、载入项目、执行测量序列、导出结果、关闭软件全程无需人工干预。明白了这个底层关系后面的编码思路就很清晰了——你操作的不是 Polyworks 那个可见的交互界面而是它在后台暴露出来的一套逻辑对象。2. 环境准备Python 到底该装 32 位还是 64 位2.1 先解决 Python 本体安装这个系列前面几篇笔记里反复出现过 Python 安装相关的高频搜索词我在这里再啰嗦一遍最关键的几条因为它直接影响 COM 连接能否成功。第一Python 版本选 3.8 以上一步到位装 3.10 或 3.11。老版本 3.6、3.7 已经陆续退出维护而且 pywin32下面要用的核心库对 3.11 的适配已经很成熟。第二安装时勾选 Add python.exe to PATH。这一步很多人跳过导致后面在命令行敲 python 提示找不到命令。安装路径尽量不要带中文和空格Windows 下C:\Python311这种路径最省心。第三如果网络下载慢pip 源要提前配好。国内环境下直接装 pywin32 经常卡住我习惯在用户目录下新建pip.ini配置文件把镜像源指到国内地址。这一步能省掉大量等待时间。安装完成后在命令行验证python --version pip --version两条命令都正常输出版本号Python 环境才算是真正就绪。2.2 关于 32 位和 64 位的关键判断这是整篇笔记里最容易被忽略、却又最容易导致 COM 连接失败的一个点——Polyworks 的安装版本位数与 Python 版本位数的关系。先说结论如果你的 Polyworks 是 32 位版本老版本尤其常见那么最稳妥的策略是安装 32 位 Python。反过来Polyworks 是 64 位就用 64 位 Python。原理是COM 组件发挥效力的前提是客户端和组件能在系统里正常建立跨进程连接。位数不一致时Windows 会尝试通过系统级的混位数转发机制来协调这取决于组件注册时的设置但很多工业软件在注册 COM 组件时并没有处理好这个环节导致位数不匹配时调用直接报错。怎么确认 Polyworks 位数打开 Windows 的任务管理器找到 Polyworks 主进程如果进程名后面带有(32 位)字样说明是 32 位版本Windows 10/11 的任务管理器默认不一定显示这一列可以在详细信息标签页右键列头勾选平台字段就能看到每个进程的位数了。另外在系统的控制面板 - 程序 - 程序和功能里多数软件也会标注位数。我在调试时犯过一个低级错误电脑上装的是 64 位 PythonPolyworks 是 32 位版本用 Dispatch 获取对象时报了Class not registered排查了一整天最后把 Python 换成 32 位之后问题直接消失。这个细节值得你提前规避。2.3 pywin32 还是 comtypes两种 COM 客户端库的取舍Python 连接 COM 的主力库有两个一个是 pywin32另一个是 comtypes。两个我都实际用过下面是基于真实体验的对比对比项pywin32 (win32com)comtypes安装方式pip install pywin32pip install comtypes类型库处理MakePy 机制可生成绑定模块运行时动态加载类型库动态调用灵活性较好DynamicDispatch 可做极晚期绑定较好接口定义清晰遇到类型库损坏时需要手动清理 gen_py 缓存相对鲁棒上手难度资料多资料多到滥资料少需要看官方示例事件接收WithEvents 支持较好支持稍繁琐对 Polyworks 这种接口文档不算特别完整的工业软件来说我更推荐首选 pywin32。原因有三个网上能搜到的 COM 调用示例大部分基于 pywin32抄作业成本低。它的Dispatch函数支持动态分发即使类型库注册信息有问题也能尝试运行期绑定。makepy工具可以生成类型库的 Python 模块方便你用dir()查看可用的接口成员。注意 pywin32 安装完成后如果你是纯 Python 环境不是 Anaconda建议执行一次python Scripts/pywin32_postinstall.py -install这步的作用是注册一些配套的 COM 服务和 PythonCOM 库不执行通常也能跑基础功能但做了更保险。2.4 先验证本机 COM 组件是否真的注册好了在写任何代码之前先确认 Polyworks 的 COM 组件已经注册到系统里。最简单的方法是打开 Windows 的注册表编辑器定位到HKEY_CLASSES_ROOT\*在根键下按 CtrlF 搜索Polyworks能看到一个形如Polyworks.Application或带版本号的 ProgID 键就说明组件已经注册。如果搜不到需要回到 Polyworks 的安装目录找注册脚本一般是.bat或.exe格式的注册工具右键管理员运行。ProgID 是 COM 组件的门牌号之后 Python 代码里就要靠这个字符串来定位组件。不同版本的 Polyworks ProgID 可能不同以注册表里看到的为准。3. 最小连接代码从拿到对象到释放对象3.1 用 Dispatch 还是 EnsureDispatch先看一段能跑的极简代码import pythoncom import win32com.client # 初始化 COM 线程模型 pythoncom.CoInitialize() try: # 连接 Polyworks COM 组件 app win32com.client.Dispatch(Polyworks.Application) # 尝试获取版本信息具体成员名以类型库为准 try: version app.Version print(Polyworks 版本:, version) except AttributeError: print(已连接 Polyworks当前版本通过类型库检查确认) finally: # 释放对象引用 app None pythoncom.CoUninitialize()这段代码里有个关键问题Dispatch和EnsureDispatch选哪个Dispatch是动态分发。它不在编译期检查方法名是否存在而是运行时把调用请求转发给 COM 对象。好处是即使类型库有问题也能先跑起来坏处是方法名拼错了不报错而是等调用时才抛AttributeError或COMError。EnsureDispatch会先加载类型库并生成绑定模块然后基于标准 Python 模块的方式调用。好处是代码提示友好、方法名正确性前置检查坏处是类型库注册信息如果不够标准这一步直接失败。我的建议是排除问题时用 Dispatch写正式代码时用 EnsureDispatch。先动态跑通逻辑再用 EnsureDispatch 固化成型。要切换绑定方式只需修改一行app win32com.client.EnsureDispatch(Polyworks.Application)EnsureDispatch有一个副作用它会在%TEMP%\gen_py目录下生成 Python 模块缓存。如果之后 Polyworks 升级了旧缓存可能导致调用的还是旧接口这时需要删除 gen_py 缓存或运行win32com.client.gencache.EnsureDispatch后手动清理。3.2 初始化 COM 和线程模型的坑我第一次写 COM 连接代码时忘了调用CoInitialize结果在Dispatch那行直接报错大意是无法创建 COM 对象原因是当前线程还没有初始化 COM 库。这个错误的根因是Windows 的 COM 机制要求每个使用 COM 的线程必须显式初始化自己的线程模型。Python 的win32com内部有些封装不帮你做这一步必须手动处理。建议的写法是用上下文管理器风格import pythoncom class ComScope: def __enter__(self): pythoncom.CoInitialize() return self def __exit__(self, *args): pythoncom.CoUninitialize()然后在需要 COM 的代码块外面包一层with ComScope():。多线程场景下尤其注意一点每个子线程都要自己调用 CoInitialize不能依赖主线程的初始化。线程模型另一个坑在事件处理。如果 Polyworks 的 COM 接口支持事件通知后面第 6 节细讲而你又依赖 PyQt、Tkinter 之类的事件循环那 COM 的事件回调很可能不触发。根本原因是 COM 的 apartment 模型要求 STA 线程必须处理消息泵而 UI 框架通常有自己的消息循环两者没有整合。3.3 连接异常的排查顺序连接失败时先看报错类型再按下面的顺序排查Class not registeredCOM 组件没有注册检查注册表 ProgID重新执行 Polyworks 注册脚本。Error loading type library类型库损坏或位数不匹配先清理 gen_py 缓存再确认 Python 位数与 Polyworks 位数一致。Access is denied权限不足把 Python 进程提权到管理员或者检查 Polyworks 是否正在以管理员身份运行两者权限级别不一致时 COM 调用会被拒绝。The message filter indicated a problem with the applicationPolyworks 正忙拒绝外部调用。这通常发生在 Polyworks 界面弹出模态对话框时。解决办法是在调用前确保 Polyworks 处于空闲状态。建议把上面的极简代码和排错清单先跑通一遍确认能拿到对象了再继续往下读类型转换那才是真正的劝退重灾区。4. 类型转换暗坑Python 和 COM 之间不是自动翻译4.1 字符串与 bool 的自动转换也有例外COM 里字符串标准类型是 BSTRPython 侧会转成str这一般是自动的。但在传参时有个容易被忽略的点BSTR 允许空指针而 Python 的 None 转成 COM 空指针后有些 Polyworks 的接口会把空字符串当成未设置而不是清空。表达意图要准确。比如导出测量结果到某个路径时传入空字符串可能被 Polyworks 理解为使用默认路径而不是报错。这对自动化逻辑的确定性是个隐患。我的习惯是在调用前做严格的参数校验def safe_bstr(value, default): if value is None: return default return str(value)COM 里的布尔类型VARIANT_BOOL在 Python 里会转成bool看似没问题但有个特殊性VARIANT_TRUE的值是-1不是1。大部分封装帮你处理了但如果你从某个接口拿到原始数值对比时要注意别写成if result 1。4.2 SAFEARRAY 与数组参数拿到的不一定是列表COM 的数组通常用 SAFEARRAY 表示。Python 端拿到的可能是元组也可能是win32com封装的数组对象取决于接口定义。如果是元组直接用索引访问即可。但如果你拿到的是一个 array 对象直接用len()和索引有时候会报错。实操中最稳妥的做法是一拿到数据就做一次显式转换def to_list(data): if data is None: return [] if isinstance(data, (list, tuple)): return list(data) # 某些 COM 数组对象的可迭代行为不完全等同于 list try: return list(data) except TypeError: return [data[i] for i in range(len(data))]这里要特别提醒的是Polyworks 的测量点坐标数组长度可能是 0。空数组在 COM 里可能表现为None、空元组、或一个带长度为 0 的 SAFEARRAY 包装对象不做兼容处理的话很容易在后续遍历时崩掉。4.3 输出参数和 ByRef最容易卡死的地方很多 COM 接口的方法是输出参数风格——不是通过返回值返回结果而是通过修改入参变量来输出数据。这类方法在 Python 里有两种处理姿势。第一种是传一个可变的引用对象。pywin32 允许传入一个 Python 对象调用结束后从同一个对象里取结果。示例# 伪代码假设 GetPoint 方法第一个参数是输出参数 pointHolder [0.0, 0.0, 0.0] app.GetPoint(pointHolder) x, y, z pointHolder第二种是使用 pywin32 对[out, retval]的自动处理。COM 规范里[out, retval]参数会作为返回值返回这种情况最简单point app.GetPoint() x, y, z point[0], point[1], point[2]关键问题是你怎么知道当前方法属于哪种两个办法。一是查 Polyworks COM 接口文档或 SDK 帮助里的方法签名凡是有[out]标记的都是输出参数。二是用 Python 直接试验如果调用一个方法时它要求你先传一个占位变量进去基本上就是引用传参模式。我最开始写批量导出功能时在测量点坐标获取接口上卡了两天因为文档里写的是GetCoordinates([out] double* x, [out] double* y)我一开始用返回值方式拿拿到的是空元组。改成引用传参之后数据才正常。这个卡点几乎每个人都会遇到请做好心理准备。4.4 VARIANT 的 VT_EMPTY 与 VT_NULL 彻底理解VARIANT 是 COM 里的一种万能容器可以装下任意类型。Python 的win32com会把大部分 VARIANT 自动转换成对应 Python 类型但有两类特殊值容易出幺蛾子VT_EMPTY变量包含空值但类型未知Python 端通常表现为None或win32com.client.VARIANT包装。VT_NULL变量明确包含数据库意义上的 NULLPython 端同样可能表现为None。如果在调用后拿到了None但你确定 Polyworks 那边应该有数据很大概率是接口返回了VT_NULL。这时候不要直接用if result is None: continue跳过先打印repr(result)看看确认是空值还是包装对象。有些封装类型在repr里能显示出底层 VARIANT 类型代码这能帮你判断到底是接口问题还是数据问题。5. 实战批量导出测量结果到本地文件5.1 场景定义与数据流先给一个实际场景车间里一台装着 Polyworks 的测量电脑每天早上要对一批工件跑检测程序跑完之后导出测量数据到共享目录然后把结果写入 Excel 报表。人工操作流程是打开 Polyworks 项目 → 加载测量程序 → 执行测量 → 导出坐标数据 → 手动填报表。用 Python COM 自动化之后这个流程变为定时触发 → Python 启动/连接 Polyworks → 加载项目文件 → 触发测量序列 → 等待测量完成 → 导出 CAD 比较结果 → 数据清洗 → 生成报表这里有个关键设计决策测量过程是在 Polyworks 内部跑的Python 只负责调度和取数。不要在 Python 里试图模拟测量逻辑Polyworks 的算法是经过大量验证的核心资产外部脚本只做控制流和数据搬运。5.2 代码骨架通用模式加占位说明因为不同版本 Polyworks 的 COM 接口名称有差异下面的代码我把方法名写成通用占位名并加上注释说明你实际使用时需要对照类型库进行替换。重点是结构不要照抄名字就跑去用。import time import pythoncom import win32com.client from pathlib import Path CLASS_PROGID Polyworks.Application # 根据注册表实际 ProgID 修改 def ensure_polyworks_ready(): 连接 Polyworks 对象并确保其处于可工作状态 pythoncom.CoInitialize() try: app win32com.client.Dispatch(CLASS_PROGID) # 有些版本需要先显示窗口有些版本支持静默模式 # app.Visible False return app except pythoncom.com_error as e: print(f连接失败: {e}) raise def run_measurement_sequence(app, project_path: str, program_name: str): 加载项目并执行测量程序 # 1. 加载项目文件方法是 OpenProject/OpenProjectModel 之类的名字 # project app.OpenProject(project_path) # 2. 激活测量会话 # session project.ActivateSession(program_name) # 3. 触发执行同步或异步模式务必确认 # result session.RunMeasurement() # 4. 如果回调机制存在可以先挂载回调再 RunMeasurement print(f开始处理项目: {project_path} / {program_name}) time.sleep(2) # 调试用正式代码应改为事件/轮询 def export_results(app, output_dir: str): 将测量结果导出到本地目录 out Path(output_dir) out.mkdir(parentsTrue, exist_okTrue) # 接口名以类型库为准一般会有 ExportResults/ExportReport 之类 # app.ExportResults(str(out / result.csv), 1) # 导出后做二次校验检查文件是否生成且非空 result_file out / result.csv if not result_file.exists() or result_file.stat().st_size 0: raise RuntimeError(导出失败目标文件为空) def main(): app None try: app ensure_polyworks_ready() run_measurement_sequence(app, rD:\Work\project.pwk, Morning_Check) export_results(app, r\\file-server\quality\20240605) print(自动化流程完成) except Exception as e: print(f流程异常: {e}) raise finally: if app is not None: app None # 释放 COM 引用 pythoncom.CoUninitialize() if __name__ __main__: main()这段骨架里有三个设计是我基于实际项目总结的定义了ensure_polyworks_ready独立函数。连接 COM 和准备工作状态放在一起方便重试和替换。导出后立刻做文件存在性校验。COM 调用就算没有抛异常也不代表数据真的落盘了必须做产物校验。finally 块里释放引用。COM 对象不释放会导致 Polyworks 进程无法干净退出长期跑自动化的机器容易出现内存泄漏。5.3 断线、超时与异常恢复COM 自动化跑在真实车间里环境远比开发机恶劣。网络波动、Polyworks 崩溃、测量超时都可能导致脚本挂起。这里提供一套健壮性处理思路。超时控制Python 标准库层面的超时对 COM 调用不直接生效。稳妥做法是把整个测量等待过程放到独立线程里主线程用join(timeout)控制最大等待时间。超过阈值之后不要把线程强制杀掉Python 的线程杀不掉而是设置一个放弃结果标志然后转向异常处理。进程重启如果 Polyworks 挂了COM 调用会抛RPC_E_SERVERFAULT这类异常。捕获到之后尝试用os.system(taskkill /f /im polyworks.exe)强制结束进程等待几秒后重新Dispatch。这套监测-杀进程-重启-重连的流程在无人值守场景下非常管用。断点续传自动化的每一步都生成一个状态标记文件。比如项目已加载写到step1.done测量已完成写到step2.done。下次脚本启动时先读取这些标记跳过已完成步骤。不要依赖内存状态——COM 进程一旦崩溃所有状态都没了。6. 跑了三个月之后才真正理解的坑6.1 看不见的方法和类型库缓存问题用 EnsureDispatch 绑定后Python 的dir(app)并不一定输出完整的方法列表因为它来自加载的类型库而 Polyworks 的类型库可能对某些接口成员设置了隐藏标记或者文档里写了但实际版本不允许外部调用。我自己遇到的是文档说某个对象有GetReport方法实际调用时报方法不存在。折腾很久才发现是该接口在生成 Python 绑定模块时因为类型库的版本签名问题没有正确导出。解决办法是清掉 gen_py 缓存重新生成rm -rf %TEMP%\gen_py然后重新运行脚本EnsureDispatch 会重新解析类型库。如果重新生成后还是看不到再用 Dispatch 动态调用试一次再不行基本可以断定该方法是内部接口外部程序无权限访问别在这上面浪费时间。6.2 事件接收不到的真相Polyworks 的 COM 组件支持事件时你可以在 Python 里订阅事件比如测量完成数据处理完成等。用 pywin32 的win32com.client.WithEvents建立事件接收器。class PolyworksEvents: def OnMeasurementFinished(self, *args): print(测量完成事件触发, args) app win32com.client.Dispatch(Polyworks.Application) event_sink win32com.client.WithEvents(app, PolyworksEvents)这段代码看起来正常但实际运行中事件可能一直不触发。原因在于事件回调依赖当前线程的 COM 消息循环。如果脚本主线程在调用方法后直接time.sleep空转不会主动分发 COM 消息事件就永远无法到达 Python 端。解决办法有两个方向。一是手动泵消息import pythoncom # 阻塞等待事件直到某个退出事件触发 pythoncom.PumpMessages()二是把 COM 调用和等待放到一个线程主线程保持 PyQt/Tkinter 事件循环。我在 Qt 界面里就是这么干的子线程跑 COM 调用主线程用QTimer轮询一个标志位事件触发后子线程更新标志位界面做响应。6.3 手动写一个探针来挖接口比看文档更快Polyworks 的 COM 文档并不是每一版都完整与其花几个小时猜某个方法签名不如直接让 Python 帮你把接口成员挖出来。下面是一个我用着顺手的探针函数import win32com.client prog_id Polyworks.Application app win32com.client.gencache.EnsureDispatch(prog_id) # 输出所有类成员方法和属性 members [m for m in dir(app) if not m.startswith(_)] for m in members: print(m)拿到成员名之后再对某个具体成员查看它的 docstring 或类型信息from win32com.client import selecttlb # 列出当前加载的 PolygonWorks 类型库模块 app._oleobj_.GetTypeInfoCount()再进一步可以直接在 Python 中调用成员并打印返回值的 Python 类型通过观察实际返回结构逆向工程的接口行为。这个方法比看文档更可靠因为文档可能过期但运行中的接口不会骗你。6.4 关于 Release 的执念与执念的对立面最后聊一个很多人忽略的细节COM 对象的释放。Python 的垃圾回收机制会回收没有引用的 COM 对象但不会立即释放底层 COM 引用计数尤其当对象被循环引用或绑定到全局变量时。在无人值守的长时间运行脚本里这会慢慢积累出问题Polyworks 进程不退出、内存持续增长、最终导致系统卡顿。建议你用完的 COM 对象主动设为None。对每个独立功能封装成函数让 COM 对象成为局部变量函数退出后引用自然消失。可以手动调用 pythoncom 的CoUninitialize来提醒 COM 库进行线程级清理。但也不要走另一个极端在循环里反复创建和销毁 COM 对象。每个 COM 对象创建过程都有不小的开销连接失败重试还可能触发 Polyworks 的权限策略导致后面的连接被暂时拒绝。合理的做法是长连接 定期检测连接状态断开才重连不断开就一直复用。这三条经验是踩了无数次坑才沉淀下来的写在这篇笔记里希望能让后面走这条路的人少走几段弯路。Polyworks 的 COM 接口会有版本差异但自动化框架和底层原理是通用的你把自己的骨架搭稳了换版本只是换几个方法名的问题。