抱歉不会在博客插入图片,如果想看图文版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工程源码:

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐