
简介这是一套面向 LabWindows/CVI 初学者的 Word 操作例程源码包重点演示如何借助 ActiveX/COM 技术实现 CVI 程序与 Microsoft Word 的深度交互。例程覆盖创建新文档、设置文本内容与格式、打开已有文档、保存修改以及关闭退出等完整流程是理解 CVI 外部接口调用与文档自动化的直观样本。压缩包共含 37 个文件主要文件类型为 C 源代码、头文件、工程文件、工作区文件、用户界面文件并配有对象库和功能库等辅助文件按 Word 97 与 Word 2000 两套接口分别组织目录便于对比不同版本控件调用的差异。整体约 693KB体积精简目前已累计 350 人次浏览学习。从该例程中还能学到 Word 对象模型的基本用法、报表生成的封装思路以及测试结果文档化的实现路径稍加改造即可用于自动输出测试报告或数据归档等实际项目。1. CVI Word 例程给测试系统补上自动出报告这最后一环一套测试系统跑完一轮几十个指标在界面上跳过了接下来最磨人的环节是把这些数整理成一份像样的 Word 报告——截图、贴表、调格式少说折腾半小时。CVI WORD 例程干的事就是在 LabWindows/CVI 的 C 代码里把这一步自动化采集完数据调几个封装好的函数Word 文档自己生成标题段落、测试数据表格、波形截图、保存路径全按代码走。它适合所有用 CVI 写上位机的人——产线检测、实验室仪器、自动化测试台凡是最终要交付 Word 报告的场景都能拿它当基础模板。核心价值不是「能生成 Word」而是把报告生成变成测试流程里的一个普通步骤跟保存一份日志一样顺手。2. 技术底座与工程骨架CVI 是怎么和 Word 谈拢的2.1 三条路里为什么最后选了 ActiveX在 CVI 里出 Word 报告常见思路有三条。第一条是直接用fprintf写 RTF 或 HTML 文件实现最快但排版能力弱插图要手工拼img标签表格一复杂就露怯。第二条是生成 CSV 或文本让用户自己贴进 Word这只能叫「半自动」测试员照样要动手。第三条就是 ActiveX/COM 直接控制 Word 进程让 C 代码扮演「操作 Word 的那个人」。CVI 对 ActiveX 的支持相当成熟工程里可以加载 Word 的类型库用自带的 ActiveX Library Generator 把 Word 的 COM 接口生成一堆 C 包装函数运行时就通过 IDispatch 后期绑定去调。我一般会选这第三条路原因很朴素——报告格式是业务刚需排版、表格、图片替换都得可控而 RTF 那套越写越像在刀刃上跳舞。这个例程走的就是 ActiveX 路线不需要在目标机器上装额外运行库只要 Word 本身活着就能用。2.2 一个典型工程的目录长什么样拿到的例程解压后目录结构通常是这样打包方式不同会略有出入但核心文件不缺这几样ReportDemo/ ├── report.prj /* CVI 工程文件双击打开 */ ├── report_main.c /* 入口造演示数据 调用生成 */ ├── report_word.c /* Word 封装层打开/写内容/存盘 */ ├── report_word.h /* 对外的几个函数声明 */ ├── word_typelib.c /* ActiveX 生成物Word 对象模型的 C 包装 */ ├── word_typelib.h └── template.docx /* 可选预置排版的报告模板 */report_word.c是例程里最值得读的文件它把 COM 细节全包在里面外部流程根本不用碰 IDispatch 那一堆接口。word_typelib.c和.h是工具生成的正常开发不要去手改重新生成一次就行。template.docx只在模板化改造时才用得上纯动态生成报告的用法里它可存在可不存在。2.3 跑通之前先确认三件事第一目标机器装了完整版 Word不是只装了查看器或 WPS 的兼容模式——COM 接口注册表项是完整安装才会有的。第二CVI 的 ActiveX 支持库没被精简掉在 CVI 环境里随便调一个 COM 调用能通就说明没问题。第三位数匹配这件事提前确认CVI 如果是 32 位尽量配 32 位 Office64 位 Office 在部分机器上会调不起来这一条我放到避坑章细说。另外有个小习惯解压路径和工程路径不要带中文。CVI 的字符串是 ANSI 体系路径带中文进 COM 调用后很容易出现乱码或「文件不存在」这种玄学错误后面有专门一节讲。3. 核心模块拆解从测试数据到 Word 文档的完整调用链这一章是例程的命根子。下面代码属于示意实现函数名和 Variant 包装方式以你本机 CVI 的 ActiveX 支持库声明为准但调用逻辑和顺序是例程通用的。把这条链走通报告生成就跑起来了。3.1 打开 WordCAObjHandle 与 Word.Application 的绑定所有操作的第一步是拿到 Word 的 COM 接口。例程里一般用一个全局句柄保存后续所有文档、选区、表格操作都从它身上取static CAObjHandle hWord 0; static CAObjHandle hDocs 0; int WordReport_Open(BOOL bShow) { HRESULT hr CA_GetIDispatch(Word.Application, hWord); if (hr 0) { LogError(CA_GetIDispatch failed: 0x%08X, hr); return -1; } /* 调试时传 bShowTRUE能亲眼看到 Word 在动 正式流程建议 bShowFALSE后台跑不影响界面 */ CA_SetProperty(hWord, Visible, 0, CAVT_BOOL, bShow); hr CA_GetProperty(hWord, Documents, hDocs); if (hr 0) { WordReport_Close(); return -2; } return 0; }CA_GetIDispatch做的是「创建 Word 进程 拿到它的 IDispatch 接口」两件事字符串Word.Application是注册表里的 ProgID换别的 Office 组件同理。第一次调用会悄悄拉起 WINWORD.EXE这一步大概耗时几百毫秒所以放到后台线程更稳妥。Visible属性控制窗口显隐调试阶段建议设成TRUE你会看到光标自己在动很有成就感上了产线就老老实实设FALSE。拿到的hDocs是文档集合新增文档必须通过它不能直接凭空创建。3.2 写标题和段落Selection 的黑匣子Word 里有个「光标」概念COM 里对应 Selection 对象。所有打字动作本质是往光标位置塞内容塞完光标自动后移。例程里的写法大致是static CAObjHandle hDoc 0; static CAObjHandle hSel 0; int WordReport_AddDocument(void) { HRESULT hr CAInvokeMethod(hDocs, Add, hDoc, 0); if (hr 0) return -1; CA_GetProperty(hWord, Selection, hSel); CAVariant txt; CA_VariantSetCString(txt, 某产品出厂测试报告); CAInvokeMethod(hSel, TypeText, 0, CAVT_CSTRING, txt); CA_VariantClear(txt); CAInvokeMethod(hSel, TypeParagraph, 0); return 0; }Documents.Add返回新文档句柄hDocSelection从 Word 应用对象上取。TypeText等价于手动敲键盘TypeParagraph等价于回车。参数CAVT_CSTRING告诉 ActiveX 层「这是个 ANSI 字符串」需要先装进CAVariant再传。清理用CA_VariantClear否则内存积累多了也是一个坑。写长报告时有个血泪经验纯靠TypeTextTypeParagraph堆段落段落间距和字体样式会非常松散难看。例程里通常会配合 Font 和 ParagraphFormat 设置字号、加粗、对齐。如果只是演示功能这些可以不做真要上生产建议直接跳到第 6 章的模板方案别在代码里硬调排版。这属于「代码能跑和报告能看是两回事」的典型差异。3.3 插测试结果表格先数行数再填单元格测试报告的核心是数据表。Word 的表格模型是Tables集合添加一张表和逐格填数据是两段操作int WordReport_AddTable(const char *headers[], int rowCnt, int colCnt) { CAObjHandle hRange 0; CAObjHandle hTables 0; CAObjHandle hTable 0; CAObjHandle hCell 0; HRESULT hr; /* 取当前文档全文范围作为表格插入位置 */ CA_GetProperty(hDoc, Range, hRange, CAVT_OBJ, 0); CA_GetProperty(hRange, Tables, hTables); /* Tables.Add(Range, NumRows, NumColumns) rowCnt 是数据行数表头单独占一行所以要 1 */ CAInvokeMethod(hTables, Add, hTable, CAVT_OBJ, hRange, CAVT_INT, rowCnt 1, CAVT_INT, colCnt, 0); /* 注意Word 的行列从 1 开始数不是 C 数组从 0 开始 */ for (int r 1; r rowCnt 1; r) { for (int c 1; c colCnt; c) { char cellText[128]; if (r 1) { strncpy(cellText, headers[c - 1], sizeof(cellText) - 1); } else { snprintf(cellText, sizeof(cellText), %.3f, testData[r - 2][c - 1]); } CAInvokeMethod(hTable, Cell, hCell, CAVT_INT, r, CAVT_INT, c, 0); CAObjHandle hCellRange 0; CA_GetProperty(hCell, Range, hCellRange); CA_SetProperty(hCellRange, Text, 0, CAVT_CSTRING, cellText, 0); CA_FreeObjHandle(hCellRange); CA_FreeObjHandle(hCell); } } return 0; }Tables.Add接收三个关键参数插入位置Range、行数、列数。这里最容易踩坑的就是「表头多占一行」和「行列从 1 开始」两个叠加因素——数组习惯 0 起Word 从 1 起循环写错一位表格就会多一行空行或者少一列数据。逐格填充用Cell(r, c).Range.Text这个调用嵌套了两层属性用完记得逐层释放句柄。参数headers是表头字符串数组testData是二维数组具体数据来源按你的测试项目替换。3.4 插入波形截图图片路径必须绝对、必须干净测试报告不加图总感觉缺了灵魂。CVI 配套的波形图控件导出 PNG 后用InlineShapes.AddPicture塞进文档int WordReport_InsertImage(const char *pngPath) { CAObjHandle hInShapes 0; CAObjHandle hRange 0; CA_GetProperty(hDoc, InlineShapes, hInShapes); CA_GetProperty(hDoc, Range, hRange, CAVT_OBJ, 0); /* InlineShapes.AddPicture(FileName, LinkToFile, SaveWithDocument, Range) LinkToFile0 表示嵌入SaveWithDocument1 表示图片随文档保存 */ CAInvokeMethod(hInShapes, AddPicture, 0, CAVT_CSTRING, pngPath, CAVT_BOOL, 0, CAVT_BOOL, 1, CAVT_OBJ, hRange, 0); }两个参数值得多看一眼。LinkToFile为 0、SaveWithDocument为 1意思是图片内容直接写进 docx 文件不依赖外部图片路径——否则你把这个 docx 发给别人图就裂了。pngPath必须是绝对路径相对路径在某些 Office 版本下会莫名失败。我一般先在程序里把截图存到C:\Temp\report_xxx.png这种全英文临时目录再调这个函数省掉中文路径的乱码隐患。3.5 保存、关闭、退出释放顺序决定你有没有残留进程写完内容最后收尾顺序极其重要void WordReport_Save(const char *filePath) { CAVariant vPath; CA_VariantSetCString(vPath, filePath); /* SaveAs2(FileName, FileFormat)16 表示 docx 格式 */ CAInvokeMethod(hDoc, SaveAs2, 0, CAVT_CSTRING, vPath, CAVT_INT, 16, 0); CA_VariantClear(vPath); } void WordReport_Close(void) { if (hDoc) { CAInvokeMethod(hDoc, Close, 0, CAVT_BOOL, 0, 0); CA_FreeObjHandle(hDoc); hDoc 0; } if (hDocs) { CA_FreeObjHandle(hDocs); hDocs 0; } if (hWord) { CAInvokeMethod(hWord, Quit, 0); CA_FreeObjHandle(hWord); hWord 0; } }Close先关文档Quit再退 Word 应用最后CA_FreeObjHandle释放 COM 引用。顺序反了WINWORD.EXE就会常驻任务管理器删都删不掉。句柄置零是个好习惯WordReport_Close变成可重入函数重复调用也不崩。文件格式参数 16 对应 docx如果目标环境是老旧 Word改成 0 存成 doc 格式也兼容。保存路径同样建议全英文这一条在很多人的现场翻过车。4. 避坑记录Word 自动化里最常见的五个事故现场4.1 WINWORD.EXE 一直残留任务管理器一片 Word 进程现象程序跑完一轮任务管理器里多了好几个 WINWORD.EXE越跑越多最后系统变卡。原因Quit之后 COM 句柄没有释放或者文档没关就急着退出。Word 的 COM 对象是计数管理的句柄不清零进程认为还有人引用它就赖着不走。解决严格按「关文档 → 退应用 → 释放句柄」的顺序收尾WordReport_Close里每个CA_FreeObjHandle之后立即置空。如果程序异常退出导致残留先杀进程再改代码别指望 Office 自己清理。4.2 换一台机器就报「未注册」或自动化错误现象在自己电脑上跑得好好的拷到另一台机器一调CA_GetIDispatch就失败错误码指向REGDB_E_CLASSNOTREG。原因CVI 生成 Word 类型库时把当时机器上 Office 版本的 GUID 写死了。目标机的 Office 版本或语言包不一样GUID 对不上类就没注册。解决在目标机上用 ActiveX Library Generator 重新生成一遍word_typelib.c和.h。如果不想折腾干脆改成纯后期绑定不依赖类型库直接拿Word.ApplicationProgID 走 IDispatch这样换机器最省心。例程里两种写法都有看你的部署环境决定。4.3 32 位 64 位不对付接口调不通现象开发环境能跑部署到某些新配的电脑上报告功能静默失败没有弹窗。原因常见组合是 32 位 CVI 配了 64 位 Office。COM 跨位数调用在部分系统环境下就是不行Office 的 COM 注册表项在 64 位系统里有独立重定向32 位进程找半天找不到。解决最省事的办法是目标机装 32 位 Office工控机器一般没人在意 Office 自身位数。要是没法换 Office就得确认你的 CVI 环境是否有 64 位版本、项目能否迁移。排查时打开组件服务看 DCOM 配置里的 Word 项位数和账户配置不对劲都会导致静默失败。4.4 中文内容或中文路径乱码现象报告里中文标题变成一串问号或者指定保存到D:\测试报告\时报「文件未找到」。原因CVI 的字符串是 ANSI 编码Word 的 COM 接口期望 UnicodeBSTR。中间 Variant 转换时如果代码页对不上中文就废了。路径乱码则是文件系统层 ANSI 转 Unicode 的经典事故。解决路径强制全英文文件名用时间戳或序号拼这是最稳的。中文内容尽量通过 BSTR 相关接口传入。如果例程里用的是CA_VariantSetCString在中文环境下建议手动转成宽字符再CA_VariantSetWString。这个问题在中文版 Windows 上几乎必现别抱侥幸心理。4.5 表格行列对不上数据错位现象生成的表格多了一行空行或者最后一列的数据跑到表头里去了。原因Word 表格行列从 1 开始C 数组从 0 开始。循环里用r rowCnt而不是r rowCnt 1就会漏掉最后一行表头那行没单独处理数据就跟表头错位了。解决写表格循环前先明确两个数字总行数 数据行数 1表头循环从 1 到总行数。填充单元格时用headers[c - 1]做表头索引数据用data[r - 2][c - 1]这样一进一出基本不会错。改完先拿三行两列的假数据跑一遍肉眼验完再上真实数据。5. 从例程到集成把报告生成塞进 CVI 多线程流程5.1 为什么不能直接放 UI 线程例程在演示时通常直接按钮回调里调WordReport_Open但上真实系统就不行了。Word 启动加保存快则几百毫秒慢则两三秒如果还要往文档里插五六张波形图整个界面直接冻结。用户第一反应是再点一次按钮结果开了两个 Word 实例在抢文件翻车现场就是这么来的。常见做法是丢进后台线程界面照常转。5.2 worker 线程的最小骨架CVI 里可以用线程池函数也可以用朴素的CreateThread。关键是线程内部要自己初始化 COMtypedef struct { char outPath[260]; double *data; int dataCnt; } ReportTask; static void ReportWorker(void *arg) { ReportTask *t (ReportTask *)arg; /* worker 线程必须自己初始化 COM否则 CALLBACK 直接失败 */ HRESULT hr CoInitialize(NULL); if (hr 0) { WordReport_Open(0); WordReport_AddDocument(); WordReport_AddTable(g_headers, t-dataCnt, g_cols); WordReport_InsertImage(C:\\Temp\\wave_1.png); WordReport_Save(t-outPath); WordReport_Close(); CoUninitialize(); } free(t-data); free(t); }CoInitialize是 COM 的 apartment 初始化主线程可能已经初始化过但 worker 线程是独立天地不调就等着调用失败。所有WordReport_系列函数必须在同一个线程里做完那些static CAObjHandle不能跨线程共享——COM 对象和初始化它的线程有绑定关系跨线程用会直接崩掉。任务投递之后主线程不要碰t-data所有权已经转移给 worker 了。5.3 和采集流程的配合方式采集完成后打包任务是最顺的衔接采集线程把一轮结果深拷贝进ReportTask结构体投递到后台采集立刻开始下一轮两边互不阻塞。报告生成完要通知界面用 CVI 的PostMessage或者设一个完成标志都行。这里有个新手易犯的错误——直接传全局缓冲的指针给 worker下一轮采集马上覆盖了这份数据报告里全是同一份数。任务里的数据必须深拷贝宁可多花一点内存也要保证报告是当时那一轮的快照。6. 二次开发用 Word 模板 书签填充替代硬编码排版6.1 模板和书签的工作方式硬编码排版的痛点是改格式要动代码。二次开发时我习惯先做一份template.docx在需要填标题、插表格、放图片的位置用 Word 的「插入 → 书签」功能埋好标记命名成bm_title、bm_table、bm_image。代码不再用一堆TypeText敲排版而是定位书签、填内容CAObjHandle hBmks 0, hBm 0, hRange 0; CA_GetProperty(hDoc, Bookmarks, hBmks); CAInvokeMethod(hBmks, Item, hBm, CAVT_CSTRING, bm_title, 0); CA_GetProperty(hBm, Range, hRange); CA_SetProperty(hRange, Text, 0, CAVT_CSTRING, 某产品出厂测试报告, 0);这样排版全在 Word 里解决代码只做数据映射。换格式只需要改模板C 代码一行不用动。6.2 书签被吃掉的问题和验证习惯有一个坑要提前说对书签的Range.Text赋值后书签本身会被替换掉同一份模板第二次跑就找不到bm_title了。稳妥做法是替换前记住书签名替换后用Bookmarks.Add把书签补回去或者只在书签后面做追加插入。我自己的验证习惯是生成一版后手工打开用「查找书签」逐位置检查再脚本批量生成几十份抽查文件大小和页数有没有异常。从那以后我每次接到「出报告」的需求都先让需求方落一份模板再写代码排版问题在 Word 里解决代码里只留数据映射。希望帮到你。本文还有配套的精品资源点击获取