简介:这是一份LabWindows/CVI基础教程,面向熟悉C语言和Windows平台的开发者,围绕NI公司集成化测控软件开发环境,讲解如何借助其交互式编程方法与丰富控件库,快速构建数据采集、自动测量、过程监控等上位机系统。资源为单个PDF文档,压缩包仅1.08MB,轻量易下载,目前已有978人学习使用。教程以实例教学贯穿始终,首个例子引导读者创建包含Acquire、Clear、Quit按钮的界面,逐步完成UIR文件设计、控件属性修改、回调函数编写与正弦波显示,使初学者直观掌握CVI的事件驱动开发流程。同时,书中特别用黑色斜体标注菜单路径、用椭圆形框标示待修改内容,便于读者跟随步骤操作。后续章节继续深入数据处理、实时系统接口、硬件通信等主题,涵盖从界面设计到调试的完整环节,可帮助零基础读者系统积累LabWindows/CVI的实际开发能力。
1. LabWindows/CVI是什么:给上位机开发者的第一份实测
国内做测控、仪器仪表、数据采集上位机的工程师,对LabWindows/CVI这个名字应该不陌生。它是一个以ANSI C为核心、由NI推出的集成化测控开发平台,最大的特点是:你会C语言,就能在它里面拖控件、搭界面、调库函数,几小时拼出一个带波形显示的数据采集程序。网上关于它的教程不少,但很多一上来就堆菜单截图,读者跟着点完还是不知道“代码是怎么和界面关联的”。这份基础教程的做法相反——它用“三个按钮加一个波形图”的完整小程序,把UIR设计、代码生成、回调函数、函数面板这四件事串成一条线,适合有C语言基础、想在Windows下做上位机开发但没碰过CVI的新手,也适合需要快速评估CVI值不值得上手的老工程师扫读一遍。
2. 从零搭建UIR界面:三个按钮一个Graph的完整配置
2.1 创建User Interface文件与控件放置
LabWindows/CVI的开发流程和写普通C程序有明显差异:你先画界面(UIR文件),再让编译器生成代码框架,最后往回调函数里填逻辑。所以第一步不是写代码,而是建界面文件。
从CVI的Project窗口选择File>>New>>User Interface(*.uir),创建一个用户接口文件。此时会弹出一个带灰色区域的Untitled Panel,这个灰色区域就是面板(Panel)的客户区。在灰色区域上单击鼠标右键,弹出快捷菜单,选择Command Button>>Square Command Button,就能放一个方形按钮。按同样步骤放三个按钮,再从菜单里选择Graph>>Graph,放一个用于显示波形的Graph控件。控件放完之后用鼠标拖到合适位置,布局不需要精确,能区分开就行。
这一步的操作逻辑和Windows窗体设计器几乎一致,但要注意:你在UIR文件里拖出来的控件,本身还不是程序的一部分,它只是一份界面资源的描述。真正的程序要通过后面生成的代码去“加载”这个UIR文件,才能让控件出现在运行时窗口里。所以UIR文件保存时建议命名为FirstSample.uir,路径不要带中文,这在后面的编译运行中能省掉很多麻烦。
2.2 Panel、Graph、Command Button属性修改
控件放上去之后,默认名字是Untitled Panel、Untitled Control,程序编完都不知道谁是谁。所以接下来要改属性。
双击Untitled Panel灰色区域,弹出Edit Panel窗口。把Panel Title改为Demo Panel,同时勾选Auto-Center Vertically(when loaded)和Auto-Center Horizontally(when loaded)两项。这两个选项的作用是让程序运行时面板自动居中,不勾也不影响功能,但勾上后界面在不同分辨率的机器上看起来更规整。
双击Graph控件蓝色区域,弹出Edit Graph窗口,把Label改为ShowWave。这个Label是显示在Graph左上角的文字标签,不是程序里引用的名字。
双击按钮,弹出Edit Command Button窗口。这里有两项是关键:Constant Name是程序里识别该控件的常量名,Callback Function是对应的回调函数名,Label是按钮上显示的文字。具体设置如下:
- Acquire按钮:Constant Name设为ACQUIRE,Callback Function设为AcquireCallback,Label设为Acquire
- Clear按钮:Constant Name设为CLEAR,Callback Function设为ClearCallback,Label设为Clear
- Quit按钮:Constant Name设为QUIT,Callback Function设为QuitCallback,Label设为Quit
这里有个新手容易混淆的点:Label是显示给用户看的,Constant Name是写代码用的,两者可以不一样。教程里特意把Acquire的Label写成带下划线的形式,这是指按下Alt+A就能触发该按钮,属于Windows热键机制,不是CVI特有的功能。
2.3 保存UIR并理解文件之间的关联
属性全部改完后,选择File>>Save保存,命名FirstSample.uir。保存后CVI会自动生成一个与之配套的头文件FirstSample.h,里面定义的就是PANEL、ACQUIRE、CLEAR、QUIT这些控件常量。这个头文件是后面C代码和UIR文件之间的桥梁——C代码里写的panelHandle、ACQUIRE这些名字,全部来自这个自动生成的头文件。
提示:UIR文件名、头文件名、项目文件名不要用中文,也不要用数字开头。CVI生成代码和加载UIR时对路径里的中文字符处理得不太好,这是很多新手在生成代码后编译报错的原因之一。
3. 代码生成机制:CVI为什么能自动写好框架
3.1 Generate All Code与main函数生成
界面设计完成后,让UIR编辑窗口处于当前激活状态,选择Code>>Generate>>All Code。CVI会弹出一个对话框,询问生成方式。这里有几个选项需要理解:
- Generate WinMain() instead of Main():勾选后生成Windows标准入口WinMain,不勾则生成普通C的main函数。测控程序通常不需要控制台窗口,勾上WinMain可以让程序运行时不弹出黑色控制台。
- Select panels to load and display at startup:选择程序启动时加载并显示哪个Panel。程序里只有一个Panel,默认会选中它。
- Select QuitUserInterface Callback:选择退出程序时的终止回调函数,教程里选择QuitCallback。
点击OK后,CVI自动生成一个完整的C源文件,内容大致如下:
#include <cvirte.h> #include <userint.h> #include "FirstSample.h" static int panelHandle; int main (int argc, char *argv[]) { if (InitCVIRTE (0, argv, 0) == 0) return -1; /* out of memory */ if ((panelHandle = LoadPanel (0, "FirstSample.uir", PANEL)) < 0) return -1; DisplayPanel (panelHandle); RunUserInterface (); DiscardPanel (panelHandle); return 0; }这段代码就是CVI程序的固定骨架。InitCVIRTE初始化CVI运行时环境,返回0表示内存不足;LoadPanel把UIR文件里的Panel载入内存,返回面板句柄,小于0说明加载失败;DisplayPanel让面板显示在屏幕上;RunUserInterface启动消息循环,程序进入等待用户操作的状态;DiscardPanel在消息循环退出后释放面板资源。
3.2 回调函数的事件分派机制
自动生成的代码里,除了main函数,还有三个回调函数:AcquireCallback、ClearCallback、QuitCallback。拿QuitCallback来说:
int CVICALLBACK QuitCallback (int panel, int control, int event, void *callbackData, int eventData1, int eventData2) { switch (event) { case EVENT_COMMIT: QuitUserInterface (0); break; } return 0; }这个函数有六个参数,其中panel是触发事件的面板句柄,control是触发事件的控件ID,event是事件类型。CVI的UI消息机制是:用户在界面上做任何操作,系统都会产生一个事件,事件被分派给对应的回调函数,回调函数通过switch语句判断事件类型,再决定做什么。
EVENT_COMMIT是最常用的事件,对应按钮被点击并且完成操作。CVI里还有EVENT_LEFT_CLICK、EVENT_MOUSE_MOVE等鼠标事件,以及EVENT_VAL_CHANGED等值变化事件。这里的核心认知是:回调函数不是你自己去调用的,而是CVI运行时在用户操作时自动调用你注册的函数。教程里说“计算机只生成框架,内部代码需要自己添加”,就是这个意思。
3.3 回调函数与控件常量的绑定逻辑
细心的读者会问:自动生成的代码里,并没有看到把ACQUIRE常量和AcquireCallback函数绑定在一起的语句,那CVI是怎么知道点击Acquire按钮时该调用哪个回调函数的?
答案在UIR文件里。你在Edit Command Button窗口里填写的Callback Function那一项,就是绑定的关键。CVI在生成代码时,会把UIR文件里的这个配置编译进资源文件,运行时加载UIR,自动建立“控件ID+事件→回调函数”的映射关系。所以填回调查函数的名称时不能拼错,拼错了编译能通过,但运行时点击按钮不会有任何反应。
这也是CVI和纯C编程的一个显著区别:在纯C里,函数之间的调用关系是显式写在代码里的;而在CVI里,界面和逻辑的关联有一部分是“配置”出来的,藏在UIR文件的属性里。这个认知到位了,后面排查“按钮点了没反应”这类问题就快得多。
4. 让Acquire按钮画出正弦波形:Sine Pattern函数面板的完整使用
4.1 打开Advanced Analysis库的函数面板
添加AcquireCallback内部代码之前,先把鼠标光标定位在AcquireCallback函数中EVENT_COMMIT那一行的下方空白处。这个位置很重要——后续所有通过函数面板插入的代码,会插入到光标所在的位置。
然后选择菜单Library>>Advanced Analysis…,在弹出的对话框里选择Signal Generation>>Sine Pattern,会弹出Sine Pattern函数面板。这个函数面板是CVI的特色:每个库函数的参数、返回值都以填表的方式列出,填完参数后点击“Insert into Function”之类的按钮,CVI会把格式正确的函数调用语句插入到代码里,不需要手写include、不需要记忆函数签名。
Sine Pattern函数面板里需要理解以下几个参数:
| 参数 | 含义 | 常用值 |
|---|---|---|
| samples | 采样点数,即生成的波形数据个数 | 256或512 |
| amplitude | 正弦波的幅值 | 1.0 |
| frequency | 周期内循环次数,也叫频率 | 10 |
| phase | 初始相位 | 0 |
| array | 输出数组,存放生成的正弦波数据 | wave[] |
这里有个细节要说明:CVI的Array参数栏一般要求你填一个数组名,并且在下方还要填写数组长度变量或者直接填数值。教程里的做法是定义一个名为wave的double数组,然后在函数面板里把array填成wave。插入代码后,还需要把数组长度赋值给代码里的某个变量,比如samples这个值,因为后续调用PlotWaveform画波形时需要知道数组的长度。
4.2 插入生成代码并理解其逻辑
按上述参数填好之后,点击插入按钮,CVI会在AcquireCallback里生成类似下面的代码:
int CVICALLBACK AcquireCallback (int panel, int control, int event, void *callbackData, int eventData1, int eventData2) { double wave[1024] = {0}; int numPoints = 512; switch (event) { case EVENT_COMMIT: SinePattern (numPoints, 1.0, 10.0, 0.0, wave); PlotWaveform (panel, PANEL_SHOWWAVE, wave, numPoints, VAL_DOUBLE, VAL_THIN_LINE, VAL_EMPTY_SQUARE, VAL_SOLID, 1, VAL_RED); break; } return 0; }这段代码是教程流程的核心,逻辑拆开来看是这样:先声明一个double数组wave用来存放波形数据,numPoints是采样点数。EVENT_COMMIT发生时,先调用SinePattern生成正弦波数据,把结果写入wave数组,然后用PlotWaveform把数组里的数据画到Graph控件上。
SinePattern是库函数,原型为SinePattern(int samples, double amplitude, double frequency, double phase, double array[])。amplitude为1.0表示波形幅度是1,frequency为10表示整个序列里包含10个完整周期,运行速度比手写for循环生成正弦波要快得多,而且参数直观。
PlotWaveform函数我一般会去查一下函数面板确认参数顺序,它有十个参数:面板句柄、Graph控件ID、数据数组、点数、数据类型(VAL_DOUBLE表示double数组)、线型(VAL_THIN_LINE是细线)、点型(VAL_EMPTY_SQUARE是空方块)、填充方式(VAL_SOLID是实心)、基线值(1表示以y=1为基线)、颜色(VAL_RED是红色)。插完代码后,如果记不住哪个参数是干嘛的,可以把光标放在函数名上按F1查询帮助,CVI的在线帮助对每个参数都有说明。
4.3 ClearCallback与波形清除的实现
ClearCallback函数的代码相对简单,它的任务是把Graph上已经画出来的波形清掉。教程里面没有直接给出代码,但正常实现起来是这样:
int CVICALLBACK ClearCallback (int panel, int control, int event, void *callbackData, int eventData1, int eventData2) { switch (event) { case EVENT_COMMIT: DeleteGraphPlot (panel, PANEL_SHOWWAVE, -1, VAL_IMMEDIATE_DRAW); break; } return 0; }DeleteGraphPlot是清除Graph内容的库函数,第二个参数是Graph控件ID,第三个参数-1表示删除全部曲线,VAL_IMMEDIATE_DRAW表示立即重绘界面。有一点值得注意:有读者可能会想着用ClrGraph这个函数,这个函数在CVI里也存在,但它清除的是光标、标记等临时元素,删数据曲线要用DeleteGraphPlot,姿势不对会上不了手。
4.4 头文件包含与编译准备
插入完库函数调用后,CVI会在源文件头部自动补上对应头文件,比如SinePattern所属的analysis.h、PlotWaveform所属的userint.h等。这些都不用手动管理,CVI的函数面板会处理。
保存源文件后,在Project窗口里需要确认当前项目中包含FirstSample.uir、FirstSample.h、FirstSample.c这三个文件。如果生成的.c文件没有自动加进项目,右键项目窗口选择Add Files to Project手动添加。这一步容易遗漏,不做的话F7编译时编译器会提示找不到源文件。
5. 避坑记录:UIR设计到编译运行的五件事
5.1 按钮回调函数没触发,点击没反应
现象:程序编译运行成功,界面正常弹出,单击按钮却没有任何动作。
原因:最常见的是Edit Command Button窗口里的Callback Function名字拼错了,或者根本没有填写,CVI默认不生成回调。也有一部分情况是控件放在Panel之外,点击事件被Panel拦截了。
解决:回到UIR编辑窗口,双击按钮打开Edit Command Button,确认Callback Function栏与代码里的函数名完全一致,一个字母都不能差。如果名字正确也不触发,把按钮拖回Panel灰色区域内再试。
5.2 LoadPanel返回负数,程序起动即退出
现象:运行程序后窗口闪一下就不见了,用断点发现LoadPanel返回值小于0。
原因:通常是UIR文件名填错了,或者UIR文件没有被包含在当前项目中。LoadPanel函数的第二个参数是UIR文件名,它查找的是和项目同目录下的文件,路径不对就会失败。
解决:确认项目窗口里有FirstSample.uir文件。如果文件不在项目里,右键项目窗口,Add Files to Project把它加进去。文件名用相对路径最稳,不要写成绝对路径D:\xxx\FirstSample.uir,换机器跑会翻车。
5.3 运行后出现黑色控制台窗口,界面不美观
现象:启动程序后,除了CVI界面外还弹出一个黑色命令行窗口,即使程序是带界面的。
原因:生成代码时没有勾选Generate WinMain() instead of Main()。不勾选时生成的入口函数是普通C的main函数,链接器会把它当作控制台程序处理。
解决:重新生成代码,在Generate All Code的对话框中勾选Generate WinMain() instead of Main(),再编译运行。已存在的工程可以直接修改项目设置,也可以在.c文件里手动把main改成WinMain并调整参数:int WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int nCmdShow),但这步手改容易出错,建议直接重新生成。
5.4 SinePattern函数插不进去,插入位置不对
现象:在函数面板里点了插入,生成的代码跑到别的地方去了,不在AcquireCallback里面。
原因:函数面板插入代码时,插入的是编辑器里光标所在的位置。很多新手打开函数面板后忘了在代码窗口里定位光标,结果代码被插入到main函数或者其他地方。
解决:打开函数面板前,先点一下代码窗中AcquireCallback函数EVENT_COMMIT下方那一行空白处,确认出现闪烁的光标,再去打开Library菜单调用函数面板。插入后立刻检查代码位置。
5.5 界面中文显示乱码
现象:控件Label里写了中文,程序运行后显示为乱码或方框。
原因:老版本CVI在Windows 2000/9x/NT时期的字符编码处理方式与现代Windows不同,Unicode支持不够好。中文字符在UIR文件里保存时编码不一致,运行时就会乱。
解决:控件Label上的中文尽量用英文或者拼音替代,比如用Acquire替代采集按钮的Label。如果必须用中文,用记事本打开UIR文件看看编码格式,改成ANSI编码保存后再载入CVI。这个问题在不同版本的CVI里表现不一样,CVI 2019版本对中文支持好些,但老版本项目迁移到新版本时仍然建议先跑一个小Demo验证中文字符。
6. 编译运行之外的调试技艺:用断点验证你的回调逻辑
CVI自带一套完整的调试工具,和VC类似的逻辑。在源代码行首点击一下就能设置断点(Breakpoint),按F5运行到断点处暂停,按F10单步执行,在Watch窗口添加变量可以实时看数值变化。
调试AcquireCallback时我习惯把断点打在SinePattern这一行上,因为此时wave数组还没被赋值,运行到断点后按F10步过SinePattern,然后在Watch窗口里展开wave数组,看前几个元素是不是接近0、然后又出现正负变化。如果wave数组全0,说明SinePattern没执行成功;如果数值超出1.0很大,说明幅值参数填错了。这个细节看似简单,但能把“按钮没反应”和“波形不对”快速区分开。ClearCallback的调试也很直观,画完波形后断点停在DeleteGraphPlot那一行,步过后观察Graph控件,曲线消失就是对的。
用熟断点之后,CVI还有一个方便的地方:在源代码里右键单击函数名选择Go to Library Function,可以直接跳到库函数的源代码或帮助页。当你怀疑PlotWaveform传参顺序不对时,不查手册,直接跳到它的实现看一眼参数用法,比来回翻文档快得多。
从那以后我每次写完CVI回调函数,都强制走一遍“设断点+跑数据+看数组”的流程,确认数据进了数组再讲后续。希望这些踩坑记录能帮你在CVI上少走几个弯路。
本文还有配套的精品资源,点击获取