CVI-EXCEL编程
我前两天的功夫算是白费了,excel2000.fp是一种及其低级的生产力,谁用谁知道,我之所以率先学习基于低级生产力的例程,完全是因为这个excel2000dem.prj做得讨喜,完整地直观地实现了数据读写功能。更邪门儿的是,前两天CVI用着用着,先是不能运行原厂例程具体故障是在main()的LoadPanel这里发生致命错误(但我写的程序能LoadPanel),然后就是再也不能打开我写的那个访问
抱歉不会在博客插入图片,如果想看图文版pdf,请电邮14518918@qq.com或直接下载pdf附件。
已经在GitCode开源,浏览地址https://gitcode.com/zhoujian29/CVI_EXCEL,项目地址https://gitcode.com/zhoujian29/CVI_EXCEL.git。
第一天,学习调用《excel2000.fp》函数包。
先为了找到CVI2010访问Excel的例程,颇费周章。菜单Help—Welcome Page…,选Find Examples…,选Directory Structure,然后点击Activex,双击excel目录下的excel2000demo.cws,才能打开CVI的excel例程。
创建一个自己的CVI工程,CVI_SQL2EXCEL,放置在D:\CVI_EXCEL目录中:
用everything搜索到,还有exceldem.xls,一并复制到D:\CVI_EXCEL目录中,然后将excel2000.fp和Excel2000.h拽到CVI的project tree里面去,而且CVI_SQL2EXCEL.c文件的开篇也应该添加一句#include “excel2000.h”。完了把例程uir文件的控件搬过来,把例程c文件的函数也搬过来。
但刚才excel2000.fp和Excel2000.h这两个文件是从D:\CVI_EXCEL目录中,被拽到CVI的project tree里面去的,仅仅是编译不出错,但是一运行就会报Excel2000.h中的函数找不到。于是只有老打老实地在project tree里面,在incule Files文件夹点鼠标右键add existed files,选中一个excel2000.fp,才能正常运行的。这之后删掉excel2000.fp,重新从D:\CVI_EXCEL目录中,将excel2000.fp和Excel2000.h这两个文件拽到CVI的project tree里面去,也行了。就邪门儿!
第二天,二次封装基于《excel2000.fp》的业务函数,实现字符串的插入。
我找到五年前写的一个将MySQL数据写入EXCEL的CVI源码,目录名是
《ReadXLSmySQL_callDLL_2020821》。发现当年把ANSI字符串转换成WCHAR字符串,是用的原厂函数CA_CStringToBSTR(),这之前我问豆包要了一个代码,死活不对。犹记得当年要实现一大片excel区域的数据写入,必须先在CVI中指定好一个区域,比如”A1:Z1000”,然后按照行列偏移量做目标单元格区域,再进行填值,这其实就是原厂例程的处理方式。2025年的红米笔记本,运行速度果然吊打2020年的Dell笔记本。我今天试了我最新封装的下指哪儿打哪儿版本,很快的,按下<Write A1~J10 Cells>案件会批量写随机字符串到A1:J0,大概1、2秒就完成了,绝然没有当年慢腾腾的深刻体验了。
更邪门儿的是,前两天CVI用着用着,先是不能运行原厂例程具体故障是在main()的LoadPanel这里发生致命错误(但我写的程序能LoadPanel),然后就是再也不能打开我写的那个访问EXCEL文件的程序了。试图用极客卸载“NI软件”,再重装CVI,我当时手滑去选中那个“TDM Excel Add-in”模块,选了之后,安装途中会报缺少文件,最后安装完毕,才发现CVI.exe根本运行不起来:
病急乱投医,只能重置电脑,删除所有C盘上的个人数据和已安装程序,从头来过。再重新安装CVI,仍然遇到CVI.exe运行不起来的故障,这才把安装配置的“TDM Excel Add-in”模块给取消勾选了,才好的。这回学乖了,找了个固态硬盘来做系统恢复盘,趁CVI还能用,赶紧“创建还原点”,三十多G的系统备份,没一、两个小时根本搞不完。搞完了请赶紧创建一个恢复节点:
现在我,很想给2020年的我,两个大耳刮子,当时写个软件开发说明不好吗?我当时的状态,以我现在揣测,应该是只求实现功能,结果全局变量满天飞;不求后人懂不写开发说明,结果把自己的后路都断了。太锤子了,呸呸呸!
花了两天时间,在豆包的协助下将原厂例程中各个句柄全是全局变量的代码,封装成了几个核心业务函数,光是理解参数表中的这么多句柄变量,就已经够费劲的了。
```c
//初始化并启动Excel应用程序
int LaunchEXCELApp(
ExcelObj_App *ExcelAppHandle, // [输出] 用于接收创建的Excel应用句柄
int appVisible, // [输入] 控制Excel应用是否可见(1=可见,0=隐藏)
int *excelLaunched // [输出] 标识Excel是否成功启动(1=成功,0=失败)
);
//安全关闭Excel应用程序并释放相关句柄
int ShutdownExcelApp(
ExcelObj_App *ExcelAppHandle, // [输入输出] 要关闭的Excel应用句柄
int excelLaunched, // [输入] 标识是否由本程序启动的Excel(1=是)
ExcelObj_Workbooks *ExcelWorkbooksHandle, // [输入输出] 工作簿集合句柄
ExcelObj_Workbook *ExcelWorkbookHandle, // [输入输出] 工作簿句柄
ExcelObj_Worksheet *ExcelWorksheetHandle, // [输入输出] 工作表句柄
ExcelObj_Sheets *ExcelSheetsHandle, // [输入输出] 工作表集合句柄
ExcelObj_Range *ExcelRangeHandle // [输入输出] 单元格区域句柄
);
//在已启动的Excel应用中打开一个Excel文件,并获取文件关联对象(工作簿、工作表等)句柄的函数
int OpenExcelFile(
const char *fileName, // [输入] 待打开的Excel文件路径(非空,只读)
ExcelObj_App ExcelAppHandle, // [输入] 已初始化的Excel应用句柄(非指针,传值)
ExcelObj_Workbooks *ExcelWorkbooksHandle, // [输出] 接收Workbooks集合句柄
ExcelObj_Workbook *ExcelWorkbookHandle, // [输出] 接收打开的Workbook句柄
ExcelObj_Worksheet *ExcelWorksheetHandle, // [输出] 接收第一个工作表句柄
ExcelObj_Sheets *ExcelSheetsHandle // [输出] 接收Sheets集合句柄
);
//将ANSI字符串Excel_Cell_String写入Excel工作表ExcelWorksheetHandle的目标单元格Excel_Cell_Position
HRESULT WriteCStringToExcelCell(
ExcelObj_Worksheet ExcelWorksheetHandle, // 工作表句柄
char *Excel_Cell_Position, // 单元格地址,如"B2"
char *Excel_Cell_String // 要写入的字符串
);
//将在Excel应用中已经打开的Excel文件,另存为文件名为fileName文件
int SaveFileas(
const char *fileName, // [输入] 待另存的Excel文件路径
ExcelObj_Workbook ExcelWorkbookHandle // [输出] 接收打开的Workbook句柄
);
// 安全地关闭Excel相关资源,释放相关句柄
int CloseFile(
ExcelObj_Workbook ExcelWorkbookHandle, // Excel工作簿句柄
ExcelObj_Range ExcelRangeHandle, // Excel单元格区域句柄
ExcelObj_Sheets ExcelSheetsHandle, // Excel工作表集合句柄
ExcelObj_Worksheet ExcelWorksheetHandle,// Excel工作表句柄
ExcelObj_Workbooks ExcelWorkbooksHandle // Excel工作簿集合句柄
);
然后实现了一个指哪儿打哪儿的功能:
完了把漫天飞舞着全局变量的原厂例程,改造成调用自制核心业务函数的自用版本。最后GUI也改了,为了方便将来理解,截图把核心业务函数和按键的调用关系也关联上了:
这是源码:
注意,以上代码,是非常老套的基于CVI针对Excel编程的手法,是基于Windows的ActiveX技术实现的。ActiveX 是微软在 1990 年代推出的一套 基于组件的软件技术,核心目的是让不同软件(尤其是网页、桌面应用)能跨平台、跨程序调用可复用的功能模块,本质是微软对早期 “组件对象模型(COM)” 技术的扩展,曾广泛用于实现网页交互、桌面应用功能集成等场景。ActiveX 并非单一技术,而是一套以 COM 为底层标准的 “技术集合”,核心是让软件组件(如一个视频播放器、表单控件、文件阅读器)能被其他程序 “即插即用”。关键是,随着技术发展和安全需求升级,ActiveX 已逐步退出历史舞台!只有我这样的不会C++的老古董,才会抱着纯C编译继承环境的CVI2010,在苦哈哈地实现想要达到的目标。
## 第三天,发现《excelreport.fp》的简洁之美,实现图片的插入。
地铁上看到一篇网帖https://blog.csdn.net/qq_41256212/article/details/86750109,《LabwindowsCVI Excel操作说明及事例》,里面有《excelreport.fp》的主要函数头的说明。因为前两天都是调用的《excel2000.fp》,繁琐得很,遇到《excelreport.fp》就感觉完全是另一个编程风格,简单得多,现摘录如下:
```c
1, ExcelRpt_ApplicationNew,用于启动excel程序:
HRESULT CVIFUNC ExcelRpt_ApplicationNew (int makeVisible, CAObjHandle *applicationHandle);
其中makeVisible为加载的excel程序是否显示的标志,当其值为1时,显示excel程序界面;applicationHandle为获取的excel程序句柄。
2, ExcelRpt_WorkbookOpen,用于打开excel的文件:
HRESULT CVIFUNC ExcelRpt_WorkbookOpen (CAObjHandle applicationHandle,
const char *fileName, CAObjHandle *workbookHandle);
其中applicationHandle为excel程序的句柄,fileName为要打开的excel文件路径,workbookHandle为获取的excel文件句柄。当程序执行成功后,excel程序将显示excel文件内容。
3、ExcelRpt_GetWorksheetFromIndex,获取excel文件中某个sheet的句柄:
HRESULT CVIFUNC ExcelRpt_GetWorksheetFromIndex (CAObjHandle workbookHandle,
int Index, CAObjHandle *worksheetHandle);
其中workbookHandle为excel文件的句柄;Index为excel文件中sheet的索引,其索引值从1开始;worksheetHandle为获得的sheet句柄。
4、ExcelRpt_GetCellValue,获取sheet中某个cell的内容:
HRESULT CVIFUNC ExcelRpt_GetCellValue (CAObjHandle worksheetHandle,
const char *cellRange, enum ExREnum_ExDataType dataType, void *dataValue);
其中worksheetHandle为sheet句柄;cellRange为cell的索引;dataType为数据类型,通常情况下,该值设置为CAVT_CSTRING,这样我们可以读取各种数据,然后进行转换;dataValue为读取的数据指针。
5、ExcelRpt_SetCellValue,设置sheet中某个cell的值:
HRESULT ExcelRpt_SetCellValue (CAObjHandle worksheetHandle, char cellRange[], enum ExREnum_ExDataType dataType, ...);
其中worksheetHandle为sheet句柄;cellRange为cell的索引;dataType为数据类型;…为数据的值。
6、ExcelRpt_SetCellRangeAttribute,设置cell的属性值:
HRESULT CVIFUNC_C ExcelRpt_SetCellRangeAttribute (CAObjHandle worksheetHandle, const char* cellRange,int attribute, ...);
其中worksheetHandle为sheet句柄;cellRange为cell的索引;attribute为属性的名称,…为属性的值。可使用该函数设置cell的背景颜色等。
7、ExcelRpt_WorkbookClose,关闭打开的excel文件:
HRESULT CVIFUNC ExcelRpt_WorkbookClose (CAObjHandle workbookHandle, int saveChanges);
其中workbookHandle为Excel文件句柄;saveChanges为关闭时,是否保存excel文件的标志,通常可设置为1,否则程序更新的数据保存不了。
8、ExcelRpt_ApplicationQuit,关闭打开的excel程序:
HRESULT CVIFUNC ExcelRpt_ApplicationQuit (CAObjHandle applicationHandle);
其中applicationHandle为打开的excel程序句柄。
9、ExcelRpt_GetErrorInfo,得到excel异常字符串:
ERRORINFO * CVIFUNC ExcelRpt_GetErrorInfo(void);
该函数返回ERRORINFO的指针,可以观察ERRORINFO-> description的异常描述。
10、CA_DiscardObjHandle,释放objHandle对象:
HRESULT CVIFUNC CA_DiscardObjHandle(CAObjHandle objHandle);
注意,applicationHandle是excel程序句柄,workbookHandle为Excel文件句柄,worksheetHandle为Excel表格句柄,逐级都要释放,不然windows进程中残留excel进程。
今天下午我涨知识了,请AI豆包写一个CVI2010调用的业务函数,就是往一个excel文件的规定单元格位置,插入一个jpg图片。全程都是豆包写好函数,我就拷贝到CVI验证一下,有编译或运行问题就让豆包改。改了进20多次,我都快崩溃了,求助于你得到原生函数ExcelRpt_InsertPicture()可实现图片插入,最后费尽周折终于成功。不能忍受的是,期间豆包老是犯同样的低级错误,大概有八九次,就是CVI2010遵循C89标准不允许变量在函数中间新增定义(之后的C99标准就是允许的)。
然后我发现执行完豆包给的函数之后,后台进程还残留了一个excel进程,看似没有关完所有handle?问豆包,豆包马上给了个更新版函数,看注释是除了释放工资表和应用程序句柄之外,还新增了一个释放工作簿句柄的代码。但是我肉眼看到新代码里面工作表和工作簿的句柄都是workbookHandle,还被释放了两次。又找豆包,豆包说“你观察得很仔细!之前的代码中确实存在句柄释放逻辑的混淆,工作表句柄(worksheetHandle)和工作簿句柄(workbookHandle)被错误地混为一谈,导致释放逻辑不够清晰”,然后再改,就再也没有excel残留在进程中了。
我感觉是豆包在训练我,我已经被折磨得没有脾气。AI不要脸,人有再大的脾气也没处撒。
格老子,白费了我前两天潜心研究excel2000.fp,没料到excelreport.fp使用起来这么容易理解,简直是数量级的碾压。
如下是几经调教的jpg2excel.c的源码,根本不用我前两天为了调用excel2000.fp而去二次封装业务函数了,excelreport.fp的一个原生函数就是一个完整功能模块,请看官细细品鉴。
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include "excelreport.h"
/* 插入图片到指定Excel文件 */
int jpg2excel(const char *imagePath, // 图片完整路径(如"d:\\pic.jpg")
const char *excelPath, // Excel文件完整路径(如"d:\\test.xlsx")
int sheetIndex, // EXCEL表索引(从1开始,如Sheet1为1)
const char *cellRange, // 目标单元格(如"A1")
const int width, // 图片宽度(像素)
const int height // 图片高度(像素)
)
{
/* 变量初始化 */
int result;
CAObjHandle appHandle; // Excel应用程序句柄
CAObjHandle workbookHandle; // EXCEL文件句柄
CAObjHandle worksheetHandle; // EXCEL工作表句柄
HRESULT hr;
int saveChanges;
int visible;
char colStr[16];
int row;
int i;
int top;
int left;
int releaseResult;
/* 初始化变量 */
result = -1;
appHandle = 0;
workbookHandle = 0;
worksheetHandle = 0;
saveChanges = ExRConst_SaveChanges;
visible = ExRConst_True; //ExRConst_False;
memset(colStr, 0, sizeof(colStr));
row = 0;
i = 0;
top = 0;
left = 0;
releaseResult = 0;
/* 创建Excel应用实例 */
hr = ExcelRpt_ApplicationNew(visible, &appHandle);
if (hr != 0) {
printf("创建Excel应用失败\n");
goto cleanup;
}
/* 打开已存在的Excel文件 */
hr = ExcelRpt_WorkbookOpen(appHandle, excelPath, &workbookHandle);
if (hr != 0) {
printf("打开Excel文件失败: %s\n", excelPath);
goto cleanup;
}
/* 获取指定索引的EXCEL表 */
hr = ExcelRpt_GetWorksheetFromIndex(workbookHandle, sheetIndex, &worksheetHandle);
if (hr != 0) {
printf("获取EXCEL表失败,索引: %d\n", sheetIndex);
goto cleanup;
}
/* 激活EXCEL表 */
hr = ExcelRpt_ActivateWorksheet(worksheetHandle);
if (hr != 0) {
printf("激活EXCEL表失败\n");
goto cleanup;
}
/* 解析单元格位置 */
while ((cellRange[i] >= 'A' && cellRange[i] <= 'Z') ||
(cellRange[i] >= 'a' && cellRange[i] <= 'z')) {
colStr[i] = toupper(cellRange[i]);
i++;
}
if (sscanf(&cellRange[i], "%d", &row) != 1 || row < 1) {
printf("无效的单元格格式: %s\n", cellRange);
goto cleanup;
}
/* 计算图片坐标 */
top = (row - 1) * 20;
left = (colStr[0] - 'A') * 100;
/* 插入图片 */
hr = ExcelRpt_InsertPicture(worksheetHandle, imagePath, top, left, width, height);
if (hr != 0) {
printf("插入图片失败: %s\n", imagePath);
goto cleanup;
}
/* 保存文件 */
hr = ExcelRpt_WorkbookSave(workbookHandle, excelPath, ExRConst_DefaultFileFormat);
if (hr == 0) {
result = 0;
//printf("图片插入成功\n");
} else {
//printf("保存Excel文件失败,错误码: 0x%X\n", hr);
}
cleanup:
/* 释放EXCEL表句柄 */
if (worksheetHandle != 0) {
releaseResult = CA_DiscardObjHandle(worksheetHandle);
worksheetHandle = 0;
}
/* 释放EXCEL文件句柄 */
if (workbookHandle != 0) {
ExcelRpt_WorkbookClose(workbookHandle, saveChanges);
releaseResult = CA_DiscardObjHandle(workbookHandle);
workbookHandle = 0;
}
/* 释放应用程序句柄 */
if (appHandle != 0) {
ExcelRpt_ApplicationQuit(appHandle);
Sleep(200); // 延长等待时间确保进程退出
releaseResult = CA_DiscardObjHandle(appHandle);
appHandle = 0;
}
return result;
}
注意,该函数会根据输入的目标单元格位置cellRange,自动计算图像的左边顶点位置并悬浮在单元格上。而显示在exce文件中的图像大小受输入的width/heigh影响,如果width/heigh小于图片实际长宽,就会压缩图像。但如果width/heigh大于图片实际长宽,也会拉伸放大图像。所以,文件浏览器中,可以鼠标右键看到jpg图片的长款像数值的:
总之,有了excelreport.fp,就请忘掉繁琐到反人类的excel2000.fp吧。
中午我问李大师:如何用cvi编程把一幅眼图jpg插入到excel?
答:excelreport有个函数:ExcelRpt_InsertPicture (, “”, , , , );
随后就给了个截图,完全不失大师风范了,请接受我的膝盖:
于是,这才有了我今天下午被豆包反复锤炼的过程。源码在此:
待我把吐槽喂给豆包请它评审,这厮居然还一本正经地给出评审意见并给出了补充建议:把 “折磨” 变成 “方法论”——你提到 “感觉豆包在训练我”,其实换个角度看,这次折腾反而帮你沉淀了2个 CVI+Excel开发的核心方法论,值得在心得里明确出来!
第四天,重写excel2000dem.prj,放弃excel2000.fp,改用excelreport.fp。
先看成果:
大致操作流程是,
①,按下按键,运行Excel程序;
②,打开一个磁盘上现存的excel文件,可以是空白文件③或是模板文件,选择SheetIndex=1的工作表;
④,按下按键,会往K1写入字符串“NewString”⑤;如果按下<Random A1~J10 Cells>按键,会用随机数填满A1~J10 单元;
⑥,按下按键,会往B12插入《EyeDiagram.jpg》图片⑦。
函数对应关系如下图所示,大部分都是fp的原生函数。前后一共就三个句柄,Excel应用程序句柄、EXCEL文件句柄、EXCEL工作表句柄,字符串也不用转换成WCHAR类型也能完美支持中文显示,关键excelreport.fp有原生函数支持图像插入。我前两天的功夫算是白费了,excel2000.fp是一种及其低级的生产力,谁用谁知道,我之所以率先学习基于低级生产力的例程,完全是因为这个excel2000dem.prj做得讨喜,完整地直观地实现了数据读写功能。
最后来看下工程文件的构成情况,还是蛮清爽的,方便移植:
有一个遗憾没有解决,就是如果电脑的屏幕分辨率不是1920*1080,那么界面上的单元格行高像素值和列宽像素值就需要相应做出变化,才能准确定位到EXCEL365(单元格属性默认值行高13.8列宽8.11)全部单元格均处于默认行高列宽情况下的Position2单元格。尝试找豆包索要精确定位的代码,大部分尝试都以CVI2010没有豆包以为有的函数而失败告终。
这是国庆节定稿的CVI2010工程源码:
更多推荐



所有评论(0)