跳到主要内容

用 LabVIEW 编写 Wizard 类型的应用程序

· 阅读需 18 分钟

    Wizard 向导类型的程序,指的是可以类似安装程序那样,一步一步地指导用户完成功能的应用程序。这类程序极为常见。它一边要求用户提供必要的信息,一边给用户发出指导性的意见和反馈。这样,即便是新用户也可以轻松完成任务。但是,向导类型的程序虽然方便了用户,却是增加了开发人员工作的复杂度。
   在过去的七年里,我编写过各种各样的 LabVIEW 应用程序,其中大部分程序的界面都是向导类型风格的。这种风格的软件确实是用途广泛。
在这里,我把曾经用到的编写 Wizard 的各种方法作一简介。随着 LabVIEW 程序的不断改进,以前用过的有些方法今天看来或是十分笨拙、或是已经失去了意义,以后再也不会使用。我记叙下来只是作为对自己过去这一段工作的怀念吧。

一、页面为独立 VI

    我在编写第一个程序时,首先想到的就是一个最直观的编程方案:为 Wizard 的每一个步骤编写一个独立的 VI。关闭当前的 VI,打开一个新的 VI,程序就自然而然从一个页面跳到另一个新页面了。完成后,就发现这并不是一个好主意。因为每个页面都是独立的,数据的交换和状态的控制都不方便。比如说,原来的面板在屏幕左边,按一下Next键,面板突然蹦到屏幕的右边了。

 二、借助 Windows API

    于是,就考虑是否可以采用框架式的结构。当时 LabVIEW 还没有 sub panel 控件,做框架只能借助 Windows API 的帮助,把一个 VI 的面板当作子窗口,插到框架VI的界面中去。当时公司的一些产品就是采用了这种方法。但是,我并不喜欢。它的主要缺点是只能够支持Windows,无法跨平台使用。再者,我是一位地地道道的 LabVIEW fans,怎么能利用 Windows API 呢?我应该寻找一个纯G语言的解决方案。

三、变动控件位置

    下面,就来介绍一个早期的纯 LabVIEW 的解决方案:通过挪动界面上控件的位置来达到界面切换的效果。具体说,就是当用户按下 Next 键,程序就把当前界面上的控件往旁边挪动一段距离,移动到用户界面的可视范围以外,用户就看不到它们了;与此同步,把那些原来在可视范围以外的、而下一页应当显示的控件,挪到可视范围内。这样,用户的感觉就是切换了一个页面。
    这里有一个用这种方法编写的示例。程序的功能是把一个文件分割成几块,或者把已经分割成几块的文件再合并起来。下载地址:

    http://ruanqizhen.googlepages.com/splitter.7z

    从范例中可以看出,这种方法十分麻烦,每一步操作都要考虑如何挪动每一个控件。如果程序太大,就难以维护了。也许现在不会再这么编写程序了,然而在当年 LabVIEW 还不支持 Tab 控件与事件处理结构(event structure)时,这个方法还相当流行的呢。

 

四、Tab 控件+事件处理结构

     LabVIEW 6.1 的出现才第一次大大简化了 Wizard 界面风格程序的编写。LabVIEW 6.1增加了两个非常重要的新特性,一个是Tab控件,一个是事件处理结构。
    有了Tab控件,就可以把 Wizard 中每一页需要的控件分别放在 Wizard 不同的页面上,切换 Tab 的活动页面也就显示了该页面上相应的控件。
    事件处理结构的应用更为广泛。有了它,编程者就不需要再添加额外的代码来监视每个控件的状态改变以及鼠标、键盘等的操作了。
 
    这种利用Tab控件和事件处理结构编写的 Wizard 风格界面程序的方法现在仍然被广泛使用着,下面这个链接就是一个采用这种方法编写的软件:

    http://sine.ni.com/nilex/DisplayLinkAction.do?id=101NILEX

    它的功能是把一个 C 语言开发的仪器驱动程序转换为 LabVIEW 下的驱动程序。程序虽然是我编写的,但版权属于NI公司,所以不能把程序源代码公开给大家。

    这种方法也有它的弊端。因为整个 Wizard 界面会用到的所有控件都集中在同一个 VI 上,这个主VI就可能特别庞大:界面可能有数十个控件,程序框图上的事件处理更为复杂,有近百个事件也不作为奇。如果需要对程序作修改,要找到相应的事件框就已经很困难了,要确定这个改动是否会影响程序的其他部分就更为困难了。
    图1是我编写的一个Tab控件风格的向导型程序,它的主VI中的事件结构中,有近百个事件需要处理。对这样的程序,想找到一个相应的时间都很麻烦,处理好事件之间的关系就更困难了。

    Tab 控件+事件处理结构的架构虽然大大简化了 Wizard 界面风格程序的编写,但是这样的程序很难对他的代码进行更细致的模块划分,并把模块的私用数据隐藏起来。为了使大型 Wizard 程序有更好的可读性,可维护性,还需找到一种更好的架构。

 
图1:使用Tab控件的向导型程序,事件结构中众多事件

五、SubPanel

    主VI太过复杂,是肯定会影响它的可读性和可维护性的。所以,对向导类型程序的进一步改进的重点,就是把主VI进一步模块化,不但是程序代码要模块化,界面也必须模块化。代码模块化相对比较简单,多利用子VI就是了。但是界面的模块化,在之前的LabVIEW中是非常困难的,因为 LabVIEW 没办法在运行时,把不同的 VI 的界面拼在一起。是 LabVIEW 7.1 和 8.0 的一些新功能最终解决了这个问题。

    对程序界面模块化,按一般的思路,第一步就是把每个页面划分成一个独立的模块。这似乎又回到了我们前文提到过的第一、二个阶段。但有所不同的是,旧版本 LabVIEW 功能不全,无法很好的管理被分为模块的页面,而新 LabVIEW 改进的对这方面的支持。
    在 LabVIEW 7.1 中出现了一个新的控件 - SubPanel(子面板)。当一个 VI 运行的时候,它的 SubPanel 控件中,可以显示另一个 VI 的前面板。我们可以利用这个新的控件,我们可以使用插件框架式程序架构来编写向导型的程序。图1是这种插件框架式程序结构的示意图。


图1:插件框架式的程序结构

    插件框架式程序的实现思路是,把向导的每个页面都分配到一个独立的VI上去,这个页面上所有的操作,都有这个页面所在的VI完成。图1左上部分的那些 VI 就是为每个页面编写的VI。这些 VI 都被当作插件,在主程序需要的时候被调用显示在主程序上。
    图1右下角的VI是主程序的 VI。它的界面上主要是一个 SubPanel 控件,这个控件用于显示页面VI的界面。主程序在每一步的时候,分别把对应这一步骤页面的VI的界面显示出来,这样就实现的向导功能。主程序的界面上还有一些公共控件,比如“上一步”“下一步”这样的按钮,这些按钮在所有步骤中都需要,所以可以放在主框架上,不需要再在每个页面中重复了。

    这样的插件框架式程序在运行时,主VI和插件VI是在同时运行的。
    主 VI 的运行流程大致如下:创建或注册程序运行时需要的各种事件 -> 初始化程序 -> 等待和处理事件,主要是管理插件。比如在用户按下“下一步”按钮后,主程序负责把当前的插件移出内存,把对应下一页的 VI 调入内存,运行,并显示界面。 -> 最后负责销毁创建的事件,关闭所有资源,退出。
    插件 VI 的主要程序结构和主 VI 一样,采用的是事件处理结构。它在运行起来以后执行的流程也和主 VI 类似: 创建或注册插件运行时需要的各种事件 -> 初始化程序 -> 等待和处理事件,主要是用户在界面上的操作,和一些后台程序,比如数据处理等等。 -> 销毁创建的事件,关闭插件。

    虽然 SubPanel 在 LabVIEW 7.1 中就出现了,但是我当时却并没有在我的程序里采用上述的设计方案。只是因为当时还有一个棘手的问题没有解决。这个问题就是 VI 太多了,不好管理。
    向导页面的多个插件 VI,他们的功能有很多共同之处。在以前,所有页面都在同一个主 VI 中的时候,那些相同的功能可以通过调用同一个子 VI 来完成。但是,把页面分割成独立 VI 之后,很多情况,我都不得不为每个页面做一整套子 VI,他们在每个页面上完成的功能都类似,但却不能使用同一个子 VI。
    以处理事件为例,我写了一套子 VI 处理页面 VI 的事件。但是由于不同的页面可能会同时在运行,每个页面都有自己的事件,如果调用同一套处理事件的子 VI,不同页面之间会相互干扰。
    另外,如果想创建一个新的页面,最方便的方法莫过于把一个已有页面的 VI、子 VI 全部复制一遍,然后在其基础上做改动。LabVIEW 以前是不允许出现同名 VI 的。把一个页面的 VI、子 VI 全部改名,还要保证调用链接不出现混乱,非常的不方便。所以上述的插件框架方案是我等到到 LabVIEW 8.0 出来以后才开始使用的。

六、Project Library

    LabVIEW 8.0 作为一大升级版本,拥有了很多新特性。其中之一是“Project Library(工程库)”。
    工程库是一组功能相关联的VI或其它文件的集合。一个工 程库是把一组功能相 关的VI,和其她文件按一定结构组合封装在一起,以便于代码的管理和发布。工程库的名字也是库中VI的名字空间(name space)。这个名字空间与C+ +、C#等语言中的名字空间的概念类似。有了它 LabVIEW 就可以在一个程序中使用两个同文件名的 VI,当然,它们的名字空间不能相同,也就是它们存在于不同的工程库中。
    另外,工程库中的 VI 有操作安全设置, 每一个 VI 可以被设置为公有(Public,可以被库外的VI调用);或者私有(Private,只能被库的成员VI调用)。

    工程库给开发插件框架式的程序带来的很大的方便,特别是在 VI 文件的管理方面。
    新的设计思路是这样的:把所有的功能模块都封装在工程库内,比如说每个页面都有一个对应的工程库。专为这个页面使用的所有子 VI 都被加在它的工程库内。并且,不想被其它库使用的的子 VI 都要标记为私有。被这样组织起来的程序,虽然 VI 数量还是很多,但模块划分清楚,不会出现不希望出现的调用关系。安全性,可维护性就大大提高了。
    另外,可扩展性也得到了提高。如果需要添加一个新的页面,只需要把一个已有页面复制一份。复制出来的这一份,只要工程库的名字换个新的就行了。再也不需要一个一个的去改VI的名字了。
    图2是我的一个程序的工程管理窗口:


图2:采用工程库管理程序的VI

    但是,现在的程序结构还是有些令我不太满意的地方-重复的代码太多。不同页面之间,有很多类似的VI。就比如图2中的程序,每个页面都会用到事件处理的一些 VI,他们的代码在每个页面中都是相同的。但是,利用这个工程库组织起来的程序却不能把这些重复的VI提取出来,变成共用的子VI,因为在每个页面里,这些代码相同的VI,处理的数据是不同的。并且这些数据会保留在 VI 的局部或全局变量中,不同的页面如果共用一套子VI,会相互影响,出现数据混乱的。
    直到 LabVIEW 支持了面向对象的编程之后,我们才终于找到了一个完美的解决这一问题的方案。