可重入VI的两种数据空间分配方式

新版本的LabVIEW在设置VI为可重入时,还有两个选项:“为每个实例预分配空间”和“让各个实例间共享空间”:

image

“为每个实例预分配空间”是旧版本LabVIEW(8.6之前)设置可重入VI时的唯一选项。它是指程序在运行前,编译的时候就为每个可重入VI的实例分配好各自的数据空间。比如说这个子VI被主VI在3处不同的地方调用了,那么就分配3份数据副本给它。但是,这种分配数据空间的方式有两个主要的问题。

其一,很多时候程序运行前不能预期会有几处调用到了这一个可重入子VI,比如递归算法就是如此。递归每迭代一次,就需要生成一个新的递归VI的实例。而递归的深度与输入数据有关,在程序运行时才能确定,因此无法预先得知应当分配多少个副本数据空间,也就没法设置为这种模式。

其二,这种设置方式控件效率太低。假设一个可重入VI A,它在主VI中有三处被调用,分别是实例1、2、3。假设程序总共运行3秒,第0秒运行实例1;第1秒运行实例2;第2秒运行实例3。在这个程序整个运行时间内,始终为可重入VI A开辟了三份数据空间,但是每份数据空间都只被使用了很短一段时间,其余时间都没被用到。若是能够重复利用它们,程序可以节约不少内存。

为了解决以上两点问题,LabVIEW又新添了一种为可重入VI 分配数据空间的方式:“让各个实例间共享空间”。个人觉得这个名称起的很不好,它容易让人误解为:只为可重入VI分配一份数据空间,然后它所有的实例都是用着份数据空间。实际情况当然不是这样的,否则就无法保证可重入VI功能的正确性了。

事实上,采用“让各个实例间共享空间”时,每个可重入VI 的实例让然会有它自己独立的数据空间。程序在逻辑功能上与“为每个实例预分配空间”的方式没有任何区别。唯一的不同在于,数据空间并不是程序一起动就分配好了的,而是只有当一个实例VI被运行到的时候,才为它分配数据空间,而这个实例VI运行结束后,它的数据空间就又被回收回去了。两个同时运行的实例VI是绝对不会共享同一个数据空间的;两个不同时运行的实例VI倒是可以分享同一片数据空间,这已是名称中“共享”的由来。

还是以上文的可重入VI A为例,若它被设置为“让各个实例间共享空间”,程序开始时,只需要为可重入VI A准备一份数据空间,供实例1使用;程序第1秒的时候,实例1运行结束,它的数据空间被收回,因此实例2还可以继续使用这份数据空间;第2秒时,轮到实例3继续使用者份数据空间。若我们改变一下应用程序的逻辑,把实例3在第2.5秒的时候就运行起来,由于原有的一份数据空间还在被实例2所使用,它不能再分配给实例3,这时候,LabVIEW就会为可重入VI A再开辟一份数据空间,供实例3使用。

“让各个实例间共享空间”的设置虽然提高的程序的空间效率,但它并非只有有点,因为数据空间的开辟回收都是需要时间的。因此它实际上是以牺牲时间效率为代价来换取空间效率的提高。

对于一个可重入VI,如果它在应用程序中被调用的地方并不是很多,或者它的各个实例常常会同时运行的,那么就应该把它设为“为每个实例预分配空间”;反之,这个可重入VI会被应用程序频繁的调用,而且每个实例运行的时间都很短,它们的运行时间不大可能会有重叠,就应当把它设置为“让各个实例间共享空间”。用于递归调用时,必须使用“让各个实例间共享空间”方式。

内嵌子VI

从提高程序的可读性、可维护性、可重用性的角度来说,在设计LabVIEW程序时,应当经可能多的使用子VI。基本上,每个相对比较独立的功能都应当被做成子VI,而子VI最大也不应到超过30个节点。

但子VI过多,可能会对程序的运行效率带来一定影响。

首先,调用子VI是有一定的开销的,比如调用子VI时需要把参数压栈等,但是这些开销是非常小的,可以忽略不计。

造成嵌入式子VI提高整个程序的性能的主要原因是在于LabVIEW编译器的优化工作。LabVIEW编译器是可以比较智能的做一些优化工作的,在不改变程序逻辑的前提下,提高生成代码的执行效率。比如下面列出了其中几种常见的编译器优化方法:

  • 去除死代码:把永远的不会被执行到的代码删除。
  • 转移循环中的不变量:若循环每次迭代都做某些相同的运算,编译器会把这个运算挪到循环之外,制作一次就可以了。
  • 相同代码合并:编译器自动发现程序中对同一数据进行的重复运算,把重复的运算去掉。
  • 常量合并:编译器会发现程序中对常量进行的运算,在编译时就计算他们的结果,把结果直接保存在程序中,这样就不需要每次程序运行都对其进行计算了。

LabVIEW编译器的优化有一个局限性,就是这些优化措施只能在一个VI上进行,不能应用于全局。当把一个子VI B的代码合并到上层VI A中去,编译器可能就会发合并后的代码有很多可以优化的地方;若VI A和B的代码分别在不同的VI中,编译器分开查看每个VI中的代码,可能就找不出太多可以优化的地方。

LabVIEW中有一个解决方案,可以兼顾可读性与运行效率,就是在编辑程序时,多划分子VI,而编译程序时,又把子VI合并起来当作一个大VI来处理,这就是嵌入式子VI。

在VI属性对话框的“执行”页面上有个选项是“在调用VI中内嵌子VI”,英文叫“Inline SubVI into calling VIs”。若读者学过C语言和它的inline函数,就会非常容易理解LabVIEW中的这个设置。

image

当这个选项被选中,这个VI就变成了嵌入式子VI。当嵌入式子VI被拖拽到其它VI上,从编辑代码的角度上看,它与其它VI没有什么区别;但是在程序编译的角度来看,它与普通VI是不同的,那就是嵌入式子VI在编译时,并不是独立存在的,它的代码被全部复制到了调用它的VI中。用一个实际的例子来讲,假如一个程序中有两个VI,A和B,A调用了B。假如B是一个普通的VI,这个程序便编辑成可执行代码后,代码中还是有两个VI,A和B;若B是嵌入式子VI,编译好的程序就只剩下一个VI了,被扩充了的A,被扩充的A中包含原A和B两个VI的代码。

需要注意的是:嵌入子VI这个选项并不是用的越多程序效率就越高。使用嵌入式子VI也会给效率带来负面影响,比如它在每处调用子VI的地方都会生成一份拷贝,使得程序体积膨胀,占用过多的内存等。因此,建议把调用不频繁,输入参数常常为常量的VI设为嵌入式子VI,而一般的子VI不需设置。

下面用一个具体的示例来看一下LabVIEW编译器是如何优化程序的:

首先,我编写了一个子VI,这个VI有三个输入;其中两个输入是数据,另一个输入表示对两个输入数据进行何种运算,是相加还是相减等;让后把运算结果输出。

image

image

这个“Inline sub VI.vi”被设置为嵌入式子VI。对于嵌入式子VI,它必须是可重入的(嵌入式子VI的代码在每个子VI被调用的地方都会有一个副本,就更不要说是数据空间了)。LabVIEW暂时不支持嵌入式子VI的调试和自动错误处理。所以,在VI属性对话框中设置嵌入式子VI时,要把其它的设置做相应改动,否则LabVIEW会在其它设置项上打个叹号,提示这里的设置有问题。

image

接下来我在下面的程序中调用的这个子VI:

image

下面咱们要看一下,LabVIEW的编译器是如何对这个程序进行优化的。为了更直观的展示这些优化,我会画一些示意程序框图。这些用于示意的程序框图并不是LabVIEW优化出来的,而是我手工制作的,LabVIEW的优化只针对编译好的可执行代码,它并不会修改VI的源代码(程序框图)。但是经过LabVIEW的优化,main.vi生成的可执行代码,与我制作的示意程序框图编译成的可执行代码是完全相同的。

因为“Inline sub VI.vi”是嵌入式子VI,对于编译器来说,它的代码是被拷贝到main.vi上来的,所以对于编译器来说,它看到的代码是这样的:

image

在这段代码中,条件结构分支选择器的输入是一个常量“Add”,这就意味着程序每次都只会进入“Add”这一分支,而其它分支永远不会被执行到。编译器会把那些执行不到的分支移除,因此,优化后的程序代码等效如下:

image

程序中循环内所作的运算,在每次迭代中都是相同的,因此它可以被挪到循环之外,只运行一次。优化后代码等效如下:

image

程序中的平方运算的输入值是一个常量,因此这一运算会在编译时就完成这一计算,等效优化后代码如下:

image

程序中,对data这个输入数据进行两次完全相同的运算,这是没有必要的,编译器也会将其合并,于是优化后的代码最终等效与如下的代码:

image

可见,一个看似复杂的程序,经过LabVIEW编译器的优化,它的运行效率可以媲美一段极其简单的程序。但是这并不是说程序员可以不关心代码的效率了。编译器毕竟还是能力有限,它只能做简单的优化,程序效率的决定因素还是在于程序员本身的。

可执行代码与源代码分离

我在书中和之前的文章中介绍到了LabVIEW的一个与其它语言不同之处:它把程序源代码和编译好的可执行代码存储在了同一个文件中。我一直觉得这并不是一个好的方法。对于普通LabVIEW用户,这也许不是一个严重问题,甚至很多人都不需要关心LabVIEW是怎么保存文件的,只要他的程序能正确运行就万事大吉。

但是,源代码与可执行代码混合存储的弊病还是给我的开发带来了一定影响。我开发的项目较大,有多个人同时参与开发,这样一来,同一个VI文件就会在不同的开发者之间传来传去。每个开发者有自己的喜好,有人有64-bit的LabVIEW进行开发,有人用32-bit的LabVIEW,有人用Windows,有人用Linux。这本来不应该是一个问题,LabVIEW支持跨平台,相同的源代码不加修改就可以在任何平台下运行。

问题在于不同平台下编译出来的可执行代码是不同的,我的程序复制到同事的电脑上,即便代码不做任何修改,VI也会被修改,因为VI中包含的可执行代码变了。这样,我就没法简单的判断一个VI是否被同事做了修改,还是仅仅编译后的代码变了,非常不利于程序源代码管理。

这个问题可以通过分离VI的源代码与可执行代码来解决,这是LabVIEW2010开始出现的功能。在VI的属性对话框中,有一个选项是“分离编译后代码与源代码”。若这一选项被选中,则VI中只保留程序的源代码,而编译生成的代码则被移出VI之外。

image

被分离出来的编译好的可执行代码又LabVIEW统一管理,在我的Windows 7 系统中,文件夹C:\Users\[user name]\Documents\LabVIEW Data\VIObjCache\[LabVIEW version number]\ 下有一个objFileDB.vidb 文件,这就是用来存储LabVIEW 程序所有可执行代码的数据库。

若程序会在不同平台下协同开发,或者程序会被发布到不同平台下去,都可以考虑采用可执行代码与源代码分离的策略,避免不必要的VI变动。

理论上,可执行代码与源代码分离开来,可以提高程序的加载速度:程序可以各取所需,只加载源代码或只加载可执行代码。不过我并没有太明显的感觉到速度的变化。

异步调用

LabVIEW 2011 中增加了一个新的函数“异步调用”。这个函数可以简化运行异步VI或控制子线程的代码的复杂度。这个新函数在函数选板“编程->应用程序控制”里。

这里用一个例子来说明一下。

假设我们要编写这样一个程序,它的界面有一个仪表盘外加两个控制按钮:
image

当Play按钮按下时,让仪表盘指针旋转。当Stop按下时程序退出。我们假设维持表盘转动是一个持续的任务,并且它不能够阻塞程序的其它任务,比如对Stop按钮的响应。这样我就需要安排一个独立的线程来做维持表盘转动这一任务。

我为这个任务创建了一个VI,这个VI有两个输入参数,分别是表盘和Stop按钮的引用。
image

维持表盘转动的VI的程序代码如下:
image

它每隔10毫秒挪动一下表盘指针的位置,没两秒钟让表盘转一周。10秒后程序停止:
image

维持表盘转动的VI同时检测主程序中Stop按钮是否被按下,如果按下就立刻停止运行。

在LabVIEW2011之前,实现主程序功能的唯一方法是使用VI的“运行”方法,把“维持表盘转动VI”动态调用运行起来。主程序程序框图如下:

image

在这段代码最不尽人意的地方在于,运行子VI之前先要传递参数。这里只能通过VI的“设置控件值”方法为子VI传递参数。这种方法不但编写复杂,安全性也低。首先,它是通过控件名来找到需要传递参数的控件的,LabVIEW无法在编译时检查参数名字是否正确,子VI中的控件若是改了名字,程序编译时不会有问题,但运行时就会出错;其次,它无法检查需要传递的参数的数据类型,传递一个错误类型的数据进去,LabVIEW也不会在编译时出错。

使用“异步调用”函数,这段程序可以被大大改善:
image

需要注意的是,在使用“异步调用”函数前,打开VI引用的时候,一定要传递一个值为0x80的“选项”给“打开VI引用”函数。“异步调用”函数的使用方法与“通过引用调用”十分类似,这里就不赘述了。它们的去要区别在于“通过引用调用”是同步的,程序会停在这个函数上直到被它调用的子VI运行结束;而“异步调用”函数是异步的,程序在这里把子VI启动起来后,并不等待,会继续运行后续程序。

用鼠标在波形图控件上选取一条曲线

前几天上课的时候,有同学问起,LabVIEW是否提供了这样的功能,用鼠标在波形图控件上选中一条曲线。LabVIEW还真有这样的功能,它是波形图控件的一个方法“Get Plot At Position”。这个方法能可以查看波形图上一点附近是否有曲线经过,如果有,就返回这条曲线的索引。我写了一段示例程序:

这段程序的功能是,当鼠标点击在波形图控件的某一条曲线上时,把这条曲线变粗显示。程序框图如下:
image

这个程序开始先造了两条随机数据的曲线。当用户在波形图上点击时,就调用“Get Plot At Position”方法查看是否有点击在了某条曲线上。如果是,则使用“Plot.LineWidth”这一曲线的属性把曲线宽度设置为2,其他曲线宽度都只有1。

程序运行效果如下:(蓝色曲线被加粗)
image

打算挖掘LabVIEW代码关联规则

最近想研究一下:编写LabVIEW程序时,某些节点是否常常搭配使用。比如,“Open Config Data.vi”和“Read Key.vi”就常常放在一起使用。我的目标就是找出所有这种搭配使用的节点。

在大量的已有程序中找出这种搭配关系是一个关联规则挖掘的过程。

LabVIEW程序中每两个节点都有可能搭配在一起,当然只有达到一定支持度和置信度的搭配对于我来说才是有意义的。
支持度是指一种搭配的数量占所有各种搭配数量的比例。比如我收集了LabVIEW程序中,所有搭配数量是10,000个,“Open Config Data.vi”和“Read Key.vi”搭配在一起出现了10次,这种搭配的支持度就是0.001。
“Open Config Data.vi”和“Read Key.vi”搭配除以所有包含“Open Config Data.vi”的搭配就是它的置信度。比如“Open Config Data.vi”除了和“Read Key.vi”搭配还可以和“Write Key.vi”搭配。若“Open Config Data.vi”和“Write Key.vi”搭配的数量也是10,那么“Open Config Data.vi”和“Read Key.vi”搭配的置信度就是0.5。

想调查这件事纯粹是个人好奇,打算看看大家写的程序有无规律可循。我觉得一个好的编程语言是不应当有这种规律的。因为,假如某种搭配的支持度非常高,就应当把这种搭配做成一个子VI,更方便程序员编程。

Wait 与 Wait Until Next ms Multiple

LabVIEW程序中需要定时的时候,常常会用到这两个函数。前几天,跟同事详细解释了一下这两个函数的区别。现在把总结的内容发布上来。

image

Wait 函数比较好理解:给它个输入参数n,每次程序执行到它的时候,它就停下来,等待n毫秒,再继续运行后续程序。

Wait Until Next 函数稍微复杂一点:给它个输入参数n,每次程序执行到它的时候,会暂停在这里,Wait Until Next 函数每隔n毫秒醒来一次,醒来后再继续运行后续程序。

单纯解释概念比较抽象,还是用程序来演示一下。下图左右两个程序的功能相同吗?

image

一般情况下,程序并不要求非常精确的计时,这时 Wait 与 Wait Until Next 区别不大,程序员选用哪个函数都可以。

只有在要求很高的时候,才需要考虑他们之间的细微差别。

我们假设上图程序中 Read Data 和 Write Data 函数的运行时间都是n毫秒。若 n<50,在默认情况下,上图的两个程序,循环每迭代一次,所需时间都大约为100毫秒。

精度

但是,两个程序的计时精度是不同的。使用Wait Until Next 函数的程序的精度远高于使用Wait的程序。

在Windows这样的非实时操作系统中,定时函数是非常不精确的,每次执行,误差几毫秒是也是正常的现象。

Wait 函数,每次运行到它才开始计时,因此单次的误差会被累积。假如每次误差四五毫秒,迭代五次误差可能就达到十几毫秒了。

Wait Until Next 并不需要在每次调用的时候计算延时。假设Wait Until Next 函数从0时间开始计时,那么程序一开始运行,它就可以知道自己每一次醒来的时间分别应当是:100ms, 200ms, 300ms……。假如误差是±4毫秒,那么它实际每次醒来的时间就是100±4ms, 200±4ms, 300±4ms……,这个误差不会被累积。

image

第一次迭代的时间

运行下面两段程序,x-y分别是多少呢?

image

使用Wait函数那个程序,x-y等于500(忽略误差):延时了五次,每次100毫秒,那么总和就是500毫秒。

而使用Wait Until Next 函数那个程序,x-y的值则每次都不确定,但是值的变化范围是确定的,在 400+2n 与 500 之间。这是因为Wait Until Next 开始计时的时间并不是根据程序何时运行来确定的,对于程序来说,这个时间是不确定的。虽然 Wait Until Next 函数保证每次醒来的间隔是100ms,但它却不能确定第一次醒来的时间。第一次醒来可能是0~100ms之间任何一个时间。

如果循环第一次迭代就必须是精确的100毫秒,那么该怎么办呢?办法很简单,让Wait Until Next 第一次睡眠时不做任何事情,从第二次才开始使用它就行了。如下图的程序,每次运行,x-y的结果就会是确定的500

image

并行与串行

在前面看到的程序里,延时函数与循环中其它的代码是并行的,这样,只要其它代码耗时不长,循环每次迭代的时间就是有延时函数的输入参数来决定的。但有时候,延时函数需要与其它代码串行,比如必须在某两个节点之间延时。

image

当串行的时候,再使用Wait函数计时就不那么准确了。比如上图左面那个程序,它的循环没迭代一次的时间变成了2n+100。n这个时间是不确定的,它会受到电脑的配置,CPU负荷等的影响。因此用这种方式计时非常不准确。

而Wait Until Next 只考虑每次醒来的间隔,至于什么时候进入休眠的,并不影响醒来的时间。因此,对于它来说,并行与串行的效果是完全相同的(2n<100时)。

LabVIEW Web UI Builder

LabVIEW Web UI Builder是NI公司去年发布的一个软件,它是一个基于网页的软件,供用户编写简单的或者是需要通过网络发布的LabVIEW程序。

这个软件目前还是“预览版”,它的功能尚未完善,但基础功能已经有了,想见识一下网络版LabVIEW什么样的,现在就可以去玩一下了。这个软件的网页是:http://www.ni.com/uibuilder/。它的主页上有使用介绍以及一些示例程序的界面。

image

我们可以直接点击 Try It Now 按钮,启动 LabVIEW Web UI Builder。LabVIEW Web UI Builder是基于Silverlight编写的,如果网络速度较慢,第一次启动它可能需要等待一段时间。

软件起来以后,首先进入登陆界面。LabVIEW Web UI Builder必须登陆后才能使用,若没有账号,可以免费注册一个。

image

登陆后,就进入了程序的主界面了。大家看得出,这个软件的风格已经完全不像是之前的LabVIEW了,相比之下,他更接近Office的风格。

image

好在LabVIEW Web UI Builder非常简单,即便换了风格,也可以很快上手。先来点击“New VI”按钮创建一个新VI。

image

与桌面版LabVIEW不同,LabVIEW Web UI Builder不再采用窗口的方式,而是使用了单一一个集成开发环境,所有的VI都被放在了一个主环境内进行编辑。主窗口的中部是文档区,VI文件都在这里进行编辑;上方是Ribbon,常用的工具按钮;两侧以及下方摆放了其它一些编辑工具或者信息显示窗口。

尽管LabVIEW Web UI Builder的风格变了,但编程方法和原理并没有变化,它仍然还是LabVIEW。以编写一个简单加法运算的程序为例,先编写程序界面,从控件选板上脱下来三个数值型控件用于控制和显示数据:

image

现在VI的前后面板被和到同一个子窗口中去了,不像以前那样,分属两个窗口。点击VI编辑区,左上角纵向排列的两个按钮中下方那个,就切换到程序框图了。大家看到的程序框图上列出的三个接线端,就是刚才我们放置的三个控件的接线端。

image

由于程序原理与以前的LabVIEW完全相同,我就不赘述了。不过需要注意的是,LabVIEW Web UI Builder不支持鼠标右键菜单。以前在右键菜单中完成的工作,可以使用Adorner来完成。Adorner是指,当鼠标选中一个对象后,在对象周围浮现出来的一些操作选项。比如下图中,我周在为While循环的停止条件创建一个控件。

image

下图是编写好的程序:与以前的LabVIEW完全一致吧!

image

点击Run按钮可以让VI运行起来。

image

使用网页版LabVIEW的一个好处是,可以直接把编写好的程序存在NI的服务器上。保存时,程序会问你存放的Location的。

image

当然想把程序存到本地来也是可以的,点击Ribbon上的Export按钮,可以把程序导出到本地,存成后缀名为vix的文件。

如果大家打开这个vix文件看一下,就会发现,它是一个XML格式的文本文件。以后LabVIEW程序编写的时候可视化操作,保存的时候存成文本,图形化编程语言和文本语言的优势可以得兼,前景相当光明啊。

image

Packed Project Libraries 2 –与Library的比较

Packed Project Library 从名字上来看,就是被包装好了的Project Library。Project Library 是编程时候由程序员创建出来的。比如下图这个工程,我在里面创建了一个叫做“My Algorithm Library.lvlib”的工程库。它包含两个VI,其中一个是私有的。

image

Packed Project Library 并不是手工创建的,他是通过一个项目的生成规范,从 Project Library 编译而来的。比如上图的项目,我创建了一个Packed Library类型的生成规范。我在这个生成规范中指定把“My Algorithm Library.lvlib”编译成Packed Project Library 。

image

编译的结果是在我指定的路径下生成了一个名为“My Algorithm Library.lvlibp”的文件。它的后缀名仅比Packed Library多了一个字母p。

双击这个文件,可以打开它,看到他里面包含的VI:

image

如果需要在其它项目中使用到这个Packed Project Library,我们可以直接把它加到另一个项目中去,下图是一个演示项目:

image

Packed Project Library 看上去和 Project Library 非常相似,用法也完全相同。

Packed Project Library 与 Project Library

  • 都是将功能相关的一组VI封装起来的方法;
  • 库中的VI可以具有层次机构;
  • 库中的VI都带有名字空间,名字空间是带有后缀名的库名;
  • 都可以方便的放在项目管理器里使用

尽管它们十分相似,Packed Project Library 与 Project Library 相比,还是有一些明显区别的:

  • Packed Project Library 是通过编译生成的;
  • Packed Project Library 中的VI是编译后产生的,它们不能被修改;
  • Packed Project Library 包含有私有VI,但用户无法看到也不能使用它们;
  • Packed Project Library 把VI,.lvlib以及其它用到的文件都打成一个压缩包,用户在磁盘上就只能看到一个.lvlibp文件,看不到VI文件;
  • Packed Project Library 很适合作为最终产品发布给用户使用;
  • 在项目中使用Packed Project Library 可以缩短编译时间,因为Packed Project Library 中的VI是已编译好的,不会再随项目编译一遍。(这一条先这样写上,但我还需要再深入研究一下)

Packed Project Libraries 1

我使用Visual Studio的时间远比使用LabVIEW多。Visual Studio可以每次打开一个Solution,一个Solution包含多个Project。一般来说,我会为一个软件产品创建一个Solution。这个软件产品可能由多个文件组成,比如说有一个EXE,两个DLL组成,那么就为他们创建三个Project。每个Project的源代码都是独立的,可以设置自己如何编译,每个Project就可以被分开来开发。Projects之间是可以有依赖关系的,比如说编译EXE的时候可以自动的把另两个DLL先编译一遍。

LabVIEW的“项目”这个概念都是最近几年才有的,因此在这方面没有Visual Studio强大。在LabVIEW中,只有Project这一级,而没有Solution。这在开发大型程序时是有一些不便的。我觉得最严重的问题是程序模块划分不好做:所有的VI都只能放在一个Project中。最终生成的可执行文件是一个比较大的EXE文件;每次编译都有编译项目中所有的文件,很耗时;所有代码都混在一起,不容易彻底分隔开。

有些公司发布的产品不是可执行文件而是一个功能库,供其它程序调用的。他们或者以VI的形式发布,这样他们的VI源程序会被放置到用户的工程中去,很不合理;或者以DLL文件的形式发布,LabVIEW中调用DLL又很麻烦。

这个问题一直到 LabVIEW 2010 发布后才有所改善。LabVIEW 2010 增加了一种新的文件格式 Packed Project Libraries (*.lvlibp)。这种文件格式兼有原来lvlibllb文件格式的一些优点,再模块化程序的时候,可以考虑使用Packed Project Libraries 。

设计可调节大小的程序界面7 – 一些注意事项

有些控件不能改变尺寸

LabVIEW中并非所有的控件都大小可调。大多数尺寸固定的控件都是用来给子VI输入输出参数的,不会用到用户界面上。但也有一些会用于界面,尤其是一些系统风格的控件,比如单选框、复选框控件等。

image

 

有些控件必须保持长宽比

大多数LabVIEW控件可以分别设置它们的高度和宽度。但有一些控件,比如仪表盘、旋钮等,它们的形状是固定的,只能是正圆。也就是无论大小如何,长宽始终保持一比一。

对于这样的控件是不能使用“Scale Object with Pane”,“Fit Control to Pane”的,LabVIEW对于这类控件大小的自动调整存在bug。对于它们,只能使用编程的方式调整尺寸。

image

 

界面最小尺寸

无论采用哪种方式调整界面布局,最好都为界面设置一个最小尺寸。因为任何控件都不可能无限小,所以整个界面缩的太小对于用户也是没有意义的。

image

设计可调节大小的程序界面6 – 借助分隔栏调整控件的位置与尺寸

对于比较复杂的界面,借助于分隔栏同样可以不编程就调整控件的尺寸和位置。我们用下面这样一个比较复杂的界面为例:

image_thumb5

这个界面包含五个控件,假设界面要求如下:水箱、油箱、波形图三个控件需按比例随界面尺寸调整,但他们三个控件之间的距离,以及它们距离界面边框的距离不能变化;信息框的长度随界面调整,但高度不变;停止按钮尺寸保持不变,且始终在界面右下角。

我们可以使用分隔栏来定位每个控件的位置。我为了方便在编辑时调整控件尺寸,以及确保控件尺寸和位置在多次调整界面大小后仍然准确,设置了较多的分隔栏。原则是让每个需调整尺寸的控件都有一个可以撑满的窗格。 之后,在控件的右键菜单中选择“Fit Control to Pane”,让它们的尺寸始终与窗格保持一致。

image_thumb9

水箱、油箱、波形图、信息四个控件都需要设置为撑满窗格,而停止按钮由于大小不变,不需要有此设置。

我做的例子是比较极端的情况,一般来说不设置这么多分隔栏也可以,只要能保持每个窗格内只有一个跟随界面调整的控件就可以了。

界面上各个控件变化方式不同,全在于分隔栏对齐方式的设置。下图是分隔栏对齐方式的设置:

image_thumb15

实际用户的界面一般不会希望显示这么多的分隔栏,可以参考前文介绍的分隔栏隐藏方法将其隐藏。

设计可调节大小的程序界面5 – 仅改变一个主控件的尺寸

大多数程序界面上,只需要有一个能够缩放的控件撑满屏幕就可以了,其它控件没必要调整,只要他们的布局不乱就好。

比如对于有一个波形图控件和一个停止按钮控件组成的界面,我们只需要波形图随界面尺寸的调整而变化。那么我们只要在这个控件右键菜单中选中它的“随方格缩放”属性就可以了。每个界面方格只能有一个控件被设置成随方格缩放。

被设置成这一属性的控件四个边沿的延长线都被深色标注出来了。当界面尺寸被调整时,控件四边到相应的VI前面板四边的距离是不变的,只有被这个四条深色实现包裹出来的矩形区域的尺寸会随界面一起变化。这样一来,可以保证程序的主控件随界面尺寸变化,而其它的控件位置保持稳定。

image_thumb17

若主控件有多个,比如说界面上有两个同等重要的波形控件,那么只要把两个主控件Group在一起。由多个控件绑成的组,在外观布局上的行为属性与单个的控件是类似的。把这个组设为“随窗格缩放”,组里的控件就都可以随界面尺寸一起变化了。

image_thumb6

当界面尺寸发生变化,上图中的“开始”和“停止”按钮之间的距离也会变动,因为他们一个靠近边界线左端,另一个靠近右端。若需要这两个按钮的相对距离保持不变,也把它们绑成一组就可以了。

设计可调节大小的程序界面4 – 等比例改变所有控件的尺寸

如果界面简单,只有一个窗格,当界面尺寸变化时,界面所有控件都可以按比例调整自身大小,则只要在VI属性中设置一条“按窗口大小缩放所有控件尺寸”就可以了。

image_thumb14

这样设置之后的效果是整个界面按比例缩放,所有的控件都一同变化。但是它并不太精确,反复调整界面尺寸几次,再回复到最初的大小,可能会发现有些控件尺寸有少许改变。这是因为前面板的尺寸和控件的尺寸都只能是整数,这样每次调整界面后,控件与前面板尺寸的比例都会有所变化。也就是说每次界面调整都会引入误差,累积误差还是比较明显的。

设计可调节大小的程序界面3 – 编程调整控件的位置尺寸等属性

在我自己编写过的VI中,用作界面的并不多:一个项目中,可能只有几个VI的界面需要显示给用户看,其它几百个VI都只用得到程序框图。由于涉及界面的VI并不多,但凡需要调整界面尺寸的,我都是使用了在程序中动态调整控件的尺寸与位置的方法。当程序接收到PaneSize这一事件后,根据新的界面的尺寸,重新计算每个控件的位置和大小。

比如下面这个程序界面包含一个波形图控件和一个停止按钮。

image_thumb2

当用户改变界面尺寸时,我们希望停止按钮始终停靠在界面右下角,而波形图控件则撑满剩下的界面空间。这些变化都可以通过编程来设置,示例程序如下:

image_thumb11

使用编程的方式使控件布局符合变化的界面尺寸,其优点是调整精确稳定。有些控件,比如仪表盘控件,只能通过这种编程的方式才能准确调整其大小。编程的缺点是较为繁琐。对于比较简单的界面,可以考虑使用分隔栏帮助界面布局,省去编程的繁琐。