☰
LabWindows/CVI入门:用C语言快速开发上位机测控界面
2026/10/24 6:16:54 网站建设 项目流程

简介:LabWindows/CVI 基础教程面向熟悉 C 语言且了解 Windows 操作系统的测控软件开发者,旨在帮助初学者快速掌握 LabWindows/CVI 的编程思想与开发步骤。教程以实例驱动,从一个生成正弦波的入门程序入手,带领读者依次完成创建用户界面、添加 Acquire / Clear / Quit 三个命令按钮与图形显示控件,设置 Panel 标题与自动居中属性,编写按钮回调函数并实现绘图、清除、退出等逻辑,完整覆盖事件驱动编程、控件交互和调试流程。资源为 1 个 PDF 文件,包体约 1.08MB,内容紧凑,便于按章学习;目前已有 978 人学习下载,适合作为 CVI 上位机开发的起步读物。后续章节还涉及数据处理、实时系统接口与硬件通信等主题,能够在入门基础上进一步拓展至实际测控项目。

1. 先别被名字吓到:LabWindows/CVI 到底是做什么的

很多刚开始接触测控领域的朋友,一听到 LabWindows/CVI 这个名字就有点发怵——又长又绕,还带一串缩写,总觉得自己还差得远。其实它没有想象中那么高冷。你可以把它理解成一套「给 C 语言加了测控工具箱」的开发环境:写逻辑用的还是你熟悉的 C 语言,它只是把画界面、画波形、读数据这些琐碎又专业的事,提前做成了现成的功能模块。用它写出来的程序,通常被称为上位机——也就是电脑屏幕上那个用来控制仪器、显示数据、做过程监控的窗口。如果你学过 C 语言,又恰好想往检测系统、数据采集、自动测量这些方向发展,从它入门是一条比较顺的路。接下来我会用第一个小例子带你走一遍完整流程,跑完你就知道它一点都不玄。

2. 先把环境和思路摸熟:三个文件、事件驱动和几个窗口

开始动手之前,建议先花二十分钟把 CVI 的「家底」认识清楚。磨刀不误砍柴工,尤其是它和你之前写过的纯 C 程序思路不太一样,如果不明白这一点,后面每一步都会觉得别扭。

2.1 认识三兄弟:.uir、.c、.h 各管什么

不管是入门还是以后做实际项目,你打交道最多的就是三种文件。

先是 .uir 文件,全称是 User Interface Resource,中文叫用户界面文件。你可以把它理解成一张「装修效果图」:面板上放几个按钮、几块显示区域、表格长什么样、窗口标题写什么,全都在这个文件里用鼠标拖出来。它存的是界面的布局和属性,里面不包含任何业务逻辑。换句话说,界面长什么样,完全由 .uir 决定。

然后是 .c 文件,这就是普通的 C 语言源码文件。所有响应按钮、处理数据、画波形、算结果的代码都写在这里。界面归界面,逻辑归逻辑,两者分开存放,这是 CVI 项目的一个基本组织方式。

最后是 .h 头文件。它一般不用你手写,是 CVI 根据界面文件自动生成的。每次你保存 .uir 文件,CVI 会把面板和每个控件的「编号」整理成常量,写进这个头文件里。比如你给 Acquire 按钮的 Constant Name 填了 ACQUIRE,那么在代码里,Acquire 按钮就通过 PANEL_ACQUIRE 这个常量被程序识别。这样做的道理和门牌号类似:你不需要记住按钮在内存里的确切位置,只需要拿着这个名字就能找到它。

初学者最容易困惑的是三者的关系。用一个生活例子:.uir 是火锅店的装修布局,决定哪些位置放桌子和调料台;.c 是后厨,负责真正出菜;.h 是菜单,把桌号和菜品对应起来。程序运行时,main 函数先用 LoadPanel 把 .uir 装修好的面板装进内存,然后按钮被按下时,.c 里的回调函数执行,期间通过 .h 里的常量找到对应控件去操作。

我第一次学的时候,一直没搞清楚 .h 是哪来的,结果自己手写了一个头文件,里面的常量和界面根本对不上号,编译一直报错。后来才明白,只要把界面改好并保存,头文件会跟着自动更新,完全不用碰。这个认知让我少走了很多弯路。

2.2 事件驱动:按钮按下去之后,程序在忙什么

这是 LabWindows/CVI 和传统 C 程序相比最大的差异点,也是新手最先要扭转的思路。

以前你写 C 程序,通常是顺序执行:main 里从上到下跑,遇到函数就调用,跑完就结束。而 CVI 程序是事件驱动的,它运行起来之后,不是一口气把代码跑完,而是进入一个「待命状态」,等用户操作。用户的每一次点击、键盘输入、定时器触发,都会产生一个「事件」,程序把它派发给对应的回调函数去处理。处理完之后,又回到待命状态,继续等待下一次操作。

对应到我们的第一个例子:程序启动后停在界面上,你按下 Acquire 按钮,这个「按下」动作就是一次事件;CVI 发现这个事件应该交给 AcquireCallback 函数去处理,于是进入那个函数,执行生成正弦波、画曲线的代码;执行完再回到待命状态。整个过程可以看成「等待—响应—再等待」的循环。

回调函数里那个 switch (event) 就是在判断「这次来的是什么事件」。对按钮来说,最终要处理的通常是 EVENT_COMMIT,意思是「用户完成了一次有效操作」。实际上按钮还有按下、弹起等更细的事件,但对大多数入门程序来说,只在 EVENT_COMMIT 里写处理代码就够了,其他事件暂时用不到。

理解了这一点,再去看自动生成的代码就不会发怵。main 函数里那句 RunUserInterface() 不是普通函数调用,它表示把控制权交给界面系统,让程序进入前面说的那个待命循环。后面我们添加的代码,基本都是往各个回调函数里填内容,而不是在 main 里堆逻辑。这也是为什么教程反复强调「回调函数」——它是事件驱动程序的灵魂。

2.3 每个新手都要逛熟的三个窗口

安装并启动 CVI 之后,你会看到几个经常切换的窗口,先跟它们混个脸熟,操作时就不会找不到北。

第一个是项目窗(Project Window)。它管理整个工程包含哪些文件:.uir、.c、.h 都列在这里。新建的文件记得保存到这个工程里,运行时才能顺利加载资源。如果你发现某个文件没出现在项目窗里,编译时大概率会报错找不到文件。

第二个是用户界面编辑窗。每当你打开一个 .uir 文件,就会弹出这个可视化编辑器。面板、按钮、Graph 都直接在这里拖放和改属性。第一部分的全部操作基本都在这里完成,后面学新控件(比如 Timer、Table、Chart)也是从这个窗口添加。

第三个是源代码窗。Generate All Code 生成的 .c 文件就在这里打开。注意,平时自己手写代码是直接在 .c 文件里写,而用「函数面板」往代码里插入库函数时,光标位置决定了代码插到哪里。这个细节很多人翻过车,后面我会专门讲。

把这几个窗口的角色记住,接下来的操作就有地图了:在用户界面编辑窗画界面,在源代码窗写回调,在项目窗管理文件。版本较新的 CVI 2019 以及常见的发行版,界面排布与这套逻辑基本一致,所以早年教程上的操作步骤放到现在依然能跟着做,不用太担心「版本太老没法跟上」。

3. 亲手做第一个界面:三个按钮加一块波形区域

这一章我们开始动手。目标很明确:做出一个面板,上面有三个按钮和一个显示波形的 Graph。做完这一步,你就掌握了 CVI 界面设计的基本功——之后做任何上位机界面,流程都是同一套。

3.1 新建一个用户界面文件

打开 CVI 之后,第一步不是急着写代码,而是从菜单选择 File>>New>>User Interface(*.uir)。这个操作会创建一个新的界面文件,并自动弹出空白的 Untitled Panel 窗口。这个空白面板,就是你程序运行时的窗口,它的灰色区域就是摆放控件的容器。

创建时要注意保存位置。因为后面运行程序要按文件名加载资源,建议在硬盘上单独建一个文件夹存放工程,例如建立一个名为模拟项目X 的目录,再把 .uir 文件命名为 FirstSample.uir。位置越规整,后面找起来越省心。很多新手随手把文件丢在桌面,等到编译时报错找不到文件,翻遍整个电脑才明白是路径乱了。

我个人的习惯是:一打开工程就先保存一次文件,哪怕界面还没画好。养成这个习惯以后,至少不太会因为「没保存就生成代码」这种小问题导致编译不过。这个小习惯看起来不起眼,实际用起来能省不少事。

3.2 把控件拖上去:三个按钮和一块 Graph

现在给空白面板添加控件。在面板灰色区域点鼠标右键,会弹出一个控件菜单。选择 Command Button>>Square Command Button,就会在面板上创建一个方形按钮。按同样操作创建三个按钮,再选择 Graph>>Graph 创建一个用于显示图形的控件。

四个控件创建好之后,用鼠标直接拖动它们的位置,调整到你觉得顺眼的样子。界面布局没有严格标准,入门阶段怎么顺手怎么来,关键是能看清哪个按钮干什么用。通常的做法是把三个按钮放在左侧或下方,把 Graph 放在中间偏右的大块区域,这样运行时波形有足够的空间显示。

这里有一个小提示:创建多个同类型控件时,CVI 默认会给它们起名为 Untitled Control、Untitled Control_2 之类的名字,Constant Name 也是默认值。先不用管它,下一步统一改名就行。

注意:控件创建后如果不小心拖到了面板外面,运行时会显示不出来。只要在编辑窗口里把它拖回灰色区域即可,不影响工程文件。

3.3 修改属性:从 Untitled 变成有名字的控件

双击面板空白处,会弹出 Edit Panel 窗口。把 Panel Title 从 Untitled Panel 改成 Demo Panel,这是程序运行时窗口顶部显示的名字。再勾选 Auto-Center Vertically(when loaded) 和 Auto-Center Horizontally(when loaded),这样程序运行时窗口会自动居中,视觉效果更友好。完成后点 OK。

接着双击 Graph 控件,在 Edit Graph 窗口里把 Label 改为 ShowWave。Label 是显示在控件旁边的文字标签,用来告诉使用者这块区域是干嘛的。这里只改显示名,Graph 的坐标轴范围后面遇到波形显示问题时再调。

然后逐个双击三个按钮,编辑属性。这里要重点说三个字段,它们是整个 CVI 回调机制的核心。

第一个是 Constant Name,它是这个控件在代码中的唯一标识,相当于门牌号。第二个是 Callback Function,它填写的是当用户点击这个按钮时要调用的函数名。第三个是 Label,它决定按钮表面显示的文字。

按照下表配置三个按钮:

按钮Constant NameCallback FunctionLabel
第一个按钮ACQUIREAcquireCallbackAcquire
第二个按钮CLEARClearCallbackClear
第三个按钮QUITQuitCallbackQuit

配置完成后点击 OK,然后保存文件。此时界面部分已经完成,后面所有活动都转移到代码上来。注意一点:属性面板里的 Label 如果写成 Acquire 这样的形式,程序运行时按 Alt + A 也能触发这个按钮,这是 CVI 自带的热键功能,不需要额外写代码。

4. 让按钮真正干活:生成正弦波并跑通第一个程序

界面画好了,但它还不会动。这一章我们把它变成能响应的程序。好消息是,CVI 会自动生成大部分基础代码,我们只需要往两个回调函数里填关键逻辑就行。

4.1 让 CVI 自动生成代码骨架

确认当前激活的是用户界面编辑窗,然后选择菜单 Code>>Generate>>All Code…。这时会弹出一个对话框,问你想生成哪些代码。

在 Select QuituserInterface Callback 里勾选 QuitCallback,意思是把退出程序这个动作绑定到这个函数上。对话框里还有几个选项:Generate WinMain instead of Main 勾选后入口函数会变成 WinMain,桌面程序一般保持默认的 Main 即可;Select panels to load and display at startup 是选择哪个面板在启动时显示,只有一个面板时默认就行,不用额外操作。

点 OK 后,源代码窗打开,里面已经生成好了一个基本可运行的程序。自动生成的代码大致如下:

#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; } int CVICALLBACK AcquireCallback (int panel, int control, int event, void *callbackData, int eventData1, int eventData2) { switch (event) { case EVENT_COMMIT: break; } return 0; } /* ClearCallback 与 QuitCallback 结构类似 */

这段代码里有几个函数值得看懂。InitCVIRTE 负责初始化 CVI 运行时环境,返回 0 表示初始化失败,程序直接退出。LoadPanel 从磁盘加载 FirstSample.uir 界面文件,返回面板句柄,小于 0 表示加载失败。DisplayPanel 把面板显示出来,RunUserInterface 进入前面说的事件待命循环。QuitCallback 里那句 QuitUserInterface(0) 则是退出这个循环的钥匙。

自动生成的 AcquireCallback 和 ClearCallback 目前只有空壳,真正的功能代码需要我们自己添加。

4.2 给 Acquire 按钮填上「生成正弦波」的代码

先在源文件顶部定义一个全局数组,用来存放生成的正弦波数据。CVI 里添加库函数有一个很有特色的方式——函数面板。把鼠标光标放到 AcquireCallback 函数中 case EVENT_COMMIT: 下面的空白行,然后选择菜单 Library>>Advanced Analysis…,在分类里找到 Signal Generation>>Sine Pattern,弹出函数面板。

在函数面板中填写参数:Output Array 选择刚才定义的数组 Wave,Number of Samples 填 100,Amplitude 填 1.0,Frequency 填 10,Phase 填 0。面板上有清晰的参数提示,照着填即可。填好后点击插入按钮,代码就会出现在你光标所在的位置。

插入后,AcquireCallback 里会多出一行调用。接下来还需要让 Graph 把数组画出来,继续用函数面板,从 Library>>User Interface>>Controls>>Graph 中选择 PlotY 函数,按提示把数据画到 ShowWave 控件上。两个函数都插好之后,代码看起来是这样:

double Wave[100]; int CVICALLBACK AcquireCallback (int panel, int control, int event, void *callbackData, int eventData1, int eventData2) { switch (event) { case EVENT_COMMIT: /* 生成 100 个点的正弦波数据 */ SinePattern (100, Wave, 1.0, 10.0, 0.0); /* 把正弦波画到 ShowWave 控件上 */ PlotY (panelHandle, PANEL_SHOWWAVE, Wave, 100, VAL_DOUBLE, VAL_LINE, VAL_NONE, VAL_SOLID, 1.0, VAL_RED); break; } return 0; }

解释一下参数:SinePattern 的第一个参数是样本数 100,第三个参数 1.0 是幅值(波峰高度),第四个 10.0 是频率,最后一个 0.0 是初始相位。PlotY 里的 PANEL_SHOWWAVE 对应你给 Graph 起的 Constant Name,Wave 是数据数组,100 是点数,后面的 VAL_DOUBLE 表示数据源是 double 类型,VAL_LINE 表示用折线方式绘制,VAL_RED 是线条颜色。这些枚举值不用背,函数面板里都能选。

4.3 给 Clear 按钮填代码并运行

Clear 按钮的功能是清除波形,同样把光标放在 ClearCallback 的 EVENT_COMMIT 分支里,从函数面板选择 User Interface>>Controls>>Graph>>DeleteGraphPlot,参数选择 PANEL_SHOWWAVE、-1(表示删除所有曲线)、VAL_IMMEDIATE_DRAW(立即重绘)。插入后代码大致是:

int CVICALLBACK ClearCallback (int panel, int control, int event, void *callbackData, int eventData1, int eventData2) { switch (event) { case EVENT_COMMIT: DeleteGraphPlot (panelHandle, PANEL_SHOWWAVE, -1, VAL_IMMEDIATE_DRAW); break; } return 0; }

到这里,这个程序已经完整了。编译运行:点击工具栏的运行按钮,窗口弹出并居中显示。点 Acquire,Graph 里出现一条正弦曲线;点 Clear,曲线被清除;点 Quit,程序退出。

我第一次跑通这个例子时很有成就感——第一次体会到「界面加逻辑」的完整流程,发现自己其实已经掌握了 CVI 最核心的工作方式。往后不管做什么项目,本质都是在重复这一步:画界面、绑回调、插代码。

5. 新手最常见的五个坑:一次排查一个,别慌

任何工具第一次上手都会踩坑,这里把新手最容易遇到的情况集中列出来,按现象、原因、解决三步写,遇到问题可以直接对号入座。

5.1 编译报错:找不到头文件或控件常量

现象:编译时报错说打不开 FirstSample.h,或者提示 ACQUIRE、PANEL_SHOWWAVE 这类标识符未定义。

原因:常见于两种情况。一是生成代码之前,.uir 文件没有保存或者没保存到工程所在目录,导致 CVI 无法同步生成头文件;二是在改完控件属性后没有重新生成代码,代码里的常量名和界面里的对不上。

解决:先保存 .uir 文件,再重新执行 Code>>Generate>>All Code…,并在项目窗确认 FirstSample.h 已经包含在工程里。如果还不行,把项目窗里的 .h 文件删掉后重新生成一次。

5.2 按钮点了没反应:回调函数没绑定上

现象:程序能启动,界面也正常,但点击 Acquire 按钮没有任何反应,也不报错。

原因:大多数时候是控件的 Callback Function 没填,或者填的函数名和实际函数名不一致。属性面板里 Constant Name 与 Callback Function 是两回事,前者是控件标识,后者才是事件发生时调用的函数,漏填或拼错都会导致事件派发不到。

解决:双击按钮打开属性编辑窗,核对 Constant Name 和 Callback Function 是否与代码里的函数名完全一致。改完后保存 .uir,重新生成代码再编译。

5.3 波形显示得很奇怪:Y 轴范围和数据类型的锅

现象:画出来的曲线只显示一部分,或者整条线拧成一团,看不出正弦形状。

原因:最常见的是 Graph 的 Y 轴范围没设置好。正弦波理论幅值在 -1 到 1 之间,如果 Y 轴范围设成了 0 到 100,波形自然被压扁成一条直线。另一个常见原因是把数组类型传错了,比如用 float 数组却告诉 PlotY 是 VAL_DOUBLE。

解决:双击 Graph 打开属性窗,把 Y 轴范围先设为 -1.5 到 1.5,或者直接选择自动刻度。传参时留意 PlotY 里数据类型枚举,让它和数组声明保持一致。

5.4 函数面板插代码插错位置:库函数跑到函数外

现象:用函数面板插入 SinePattern 后,代码没有出现在想要的位置,甚至跑到函数外面,导致编译报语法错误。

原因:函数面板是按光标位置插入代码的。如果光标不在函数体内,插进去的代码就会落在错误位置。

解决:插入前先单击函数体内的空行,再打开函数面板。插入后立刻看一眼代码位置,不对就撤销重来。这条也是我当初翻车最多的地方。

5.5 程序一闪而过:界面都没看到就结束了

现象:点击运行后,窗口闪现一下立即关闭,根本来不及操作。

原因:通常是 LoadPanel 的文件名写错或路径不对,导致面板没加载成功,程序提前 return 退出。也有可能是 RunUserInterface 之前代码里出现了异常返回。

解决:检查 LoadPanel("FirstSample.uir", PANEL) 里的文件名是否与你保存的文件一致,文件是否和工程文件在同一个目录下。如果仍然一闪而过,优先处理编译信息里提示的那一行代码。

6. 跑通第一个程序之后,下一步学什么

第一个正弦波程序的意义,不只是「我做出一个会动的界面」,而是让你把 CVI 的完整工作流程走通了一遍:设计界面、绑定回调、用函数面板插代码、编译运行。这套流程是后续所有功能的骨架,往后的学习都可以沿着它展开。

接下来的学习建议按顺序推进。先花时间把常用控件认熟,尤其是 Timer 控件,它会按固定间隔触发事件,是做定时采集、循环刷新数据的基础;还有 Table 控件,用于在界面上显示批量数据。第二个方向是数据采集,如果你手头有采集卡或仪器,学 DAQ 库和 VISA 串口通信,这是 CVI 在测控领域的主场,也是「上位机开发」这个词真正的用武之地。第三个方向是高级分析,比如滤波、FFT 变换,用函数面板就能完成,写起来比纯 C 方便很多。第四个方向是多线程和实时性,把耗时的采集放到线程里,界面才不会卡。

如果一时不知道先从哪个下手,建议沿着这条线走:控件 → 定时器 → 数据采集 → 分析处理。每一步都像这次的第一个程序一样,用一个最小例子跑通,再逐步加复杂度。这个思路对绝大多数上位机开发入门都适用,不会走偏。

坦白讲,我最初学 CVI 时也踩过不少坑,尤其是回调函数绑定那次,折腾了很久才发现是属性面板里填错了一个名字。从那以后,我每次新建工程都强制自己走一遍固定流程:先建目录存文件,再画界面、检查属性、保存、生成代码、最后才写逻辑。流程固定下来之后,出错率低了很多,排错也快了很多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询