设计:同一功能对应多种不同界面的应用程序

虽然我以前研究过动态注册事件这一功能(3.7.6),但是还没有想到,这个功能有什么实际用途。直到前几天,一个客户向我介绍了他们工作中遇到的困难,我才突然想到,他们的问题恰好可以使用动态注册事件来解决。

他们的问题是这样的:他们开发了一套软件销售给多个用户。用户们对软件功能的需求是一致的,但他们对软件的界面却有各自不同的需求。比如界面的语言、控件的位置、尺寸、颜色等都不同。对于软件开发人员来说,最好的解决方案是程序的代码(程序框图)只有一份,而界面(VI前面板)有多份。常规方法是无法解决这个问题的,因为LabVIEW中每个VI只能对应一个前面板和一个程序框图。
程序中实现界面的那个VI通常是主VI,这个VI中的代码都是比较复杂的。一个程序维护多份功能相同而又复杂的主VI并不是一种优化的方法,发现一个bug,要到每个VI中去改。

动态注册事件的一个用途就是把界面和程序代码完全分离开来。遇到上文客户提出的需求,可以编写多个只有界面的VI,和一个没有界面只有程序框图的VI(这里说的只有界面和只有程序框图,并不是说那个VI真的没有程序框图或界面,只是说它的程序框图或界面特别简单,没有实质内容)。不同界面VI中控件的标签要有统一命名,以便被实现功能那个VI按照控件名来设置或读取控件参数。每个界面上其它的设置,都可以各不相同。界面VI的程序框图需要有一个空循环,以便它可以持续的运行。

程序真正实现功能的那个主VI不需要有界面,它要在后台运行。主程序的主体框架还是传统的循环事件结构。主程序开启后,把界面VI运行起来,需要使用不同的界面,只要在这里修改一下界面VI的路径就可以了。然后,主程序再通过控件的标签得到每一个控件的引用,再把它们注册到事件结构中去。这样一来,主程序就具备了和传统的主VI一样的能力了,读写界面上控件的值,接收控件发出的事件。

这样一来,程序中不再有代码重复的VI,可维护性大大加强。

————————————————————————

 

2011.3.29 今天应网友要求,给这个设计写了一个示例:

示例由三个VI组成。Main.vi实现软件的功能,Interface1,和Interface2分别是程序两个不同风格的界面。

image

我为程序设计了一个非常简单的功能,在界面上点一下按钮,就返回一个随机数值。程序的功能是在Main.vi中实现的,它采用的是经典的事件结构。与一般程序不同的是,它没有界面控件,它所捕获的事件都是通过动态注册生成的。

image

Main.vi的前面板用于把界面VI上控件的引用传递过来。

image

Interface1.vi,它的前面板就是程序界面:

image

它的程序框图不需要做任何实质工作,只要把界面上的控件的引用传递给Main.vi就可以了。

image

Interface2.vi只是与第一种界面风格略有不同:

image

它的程序框图与前一VI完全相同:

image

 

通过这种设计,把程序的界面与功能完全分离到了两个不同的VI中。因此,可以方便的只改变程序的界面而又完全不动程序的功能代码部分。

示例程序可以从这里下载:http://decibel.ni.com/content/docs/DOC-15583

如果需要在LabVIEW中使用到字典容器

(2.3.6) (13.3.5)

编程的时候经常遇到这种情况:需要保存一些数据,这些数据的组织类似一张表格。表格有很多类型相同的条目组成,每个条目由一个标识符(比如ID,名称等),和其它一些数据组成。程序会保存这张表格、对表格中的条目做修改、根据标识符快速找到某的条目。

实现这样的程序功能在C++或C#中非常简单,只要使用他们提供的Map,Dictionary容器就可以了,这些容器已经把维护和查找这张表的功能都实现好了,用户只需使用它们的几个简单接口函数。遗憾的是,LabVIEW中没有提供类似的容器。LabVIEW通常自己定义一个簇的数组来保存表单中的数据,然后再自己编写查询数据的代码。这样做一是编程复杂,而是查询效率太低。

我曾经想过给LabVIEW添加一个这样功能的容器,但是没有得到批准。不过,我最近发现,原来LabVIEW中有三个函数,可以实现类似的功能,虽然它们原本可能并不是用来做这个功能的。这三个函数的本职工作是为变体数据类型设置属性,它们在“Programming->Classter,Class&Variant->Variant”函数选板下,分别是Get Variant Attribute, Set Variant Attribute, Delete Variant Attribute 几个函数。从它们的名字就能知道他们是做什么的了。

变体数据的的属性由一个名字和一个数据组成。每个属性的名字都必须是唯一的,数据可以是任何数据类型的。这样,它恰好适合于前文提到测程序需求。我们可以把表单中的每一个条目看作是一个属性;条目的标识就是属性的名称;条目的其它部分是属性的数据。创建和修改表单使用Set Variant Attribute,Delete Variant Attribute 函数;查询表单的内容使用Get Variant Attribute函数就可以了。

变体的属性在LabVIEW中是以哈希表格式存储的,它的查询效率极高,所以特别适合用于需要大量查询的程序。

它毕竟不是一个真正的容器,还有一些局限性,比如标识只能使用字符串。

队列消息驱动的状态机与事件驱动的状态机的对比

(4.5)

一般来说一个程序的主程序,就是他的主界面。编写这样的程序是有固定套路的,也就是它的设计模式。

在主程序中,少不了对用户界面操作的处理,因此事件结构时必须的。同时,它也会处理一些非界面上的任务,因此程序也必须还有一个选择结构以应对其它工作。这样一来,就有两种程序结构可供选择了:1.选择结构在外,事件结构在内;2.事件结构在外,选择结构在内。

  队列消息驱动 事件驱动
示意图
工作原理 这是一个典型的状态机结构。使用队列记录消息,控制状态的跳转。
选择结构根据每次发来消息的不同,选择一个分支进行处理。这里有一个特殊的状态“No Action”,在没有其它消息的时候,选择结构进入此分支。这个分支内嵌一个事件结构,用于接收用户的界面操作。
使用事件(包括LabVIEW自带的和用户自定义的事件)来控制程序的运行,在不同分之间跳转。
事件结构有一个特殊分支“用户事件”,用于处理所有的非界面事件。其内部嵌套一个选择结构,以处理不同的事件。
实际上如果为程序的每一个处理分支都定义一个用户事件,那么这个程序中可以不需要选择结构。但定义太多的用户事件,比较麻烦,不如像这样定义一个统一的用户事件,然后在其事件处理分支中再根据事件传来的数据区分其具体是何事件。
发展历史 早些年,状态机结构是相当盛行的。这样的程序结构应该是在状态机基础上一步步改进演化来的。类似的程序结构相当多,截图中这个版本取自NI官方的LabVIEW社区,大概是NI一些系统工程师开发出来的。 我自己在编程过程中不断改进演化而来。状态机盛行那会,我没怎么编写过界面程序。而我开始编写界面程序时,LabVIEW已经具有了事件结构。因此,我开始按照自己认为最简洁的方式来设计界面程序。
封装性 这个架构比较复杂的,架构中包含多个子VI,用户管理消息队列(队列的创建、销毁、消息的入队、出队等)。网络上可以找到一些已经编写好的子VI和模板,但是用起来还是稍显复杂。 LabVIEW自带的事件处理函数已经比较简单了,可以不对其做封装就直接使用。但是我为了让程序更简洁,还是对其作了进一步的封装,把主要功能放在几个子VI中。LabVIEW中某些对话框就是采用此架构编写的,因此,这几个子VI是随LabVIEW一起发布的。无需下载就可以直接使用。
代码可读性、可维护性、扩展性 连线和子VI较多一些,程序复杂度高,是的其几项指标都较差。
此外,界面程序最主要的工作还是对界面事件的相应,其它任务居次要地位。而这个架构中把主要对象放在次要对象中的某一分支内,主次颠倒,看起来比较别扭。
连线和子VI较少,程序复杂度低,因此这几项更好。
调整未被处理的任务 由于程序中用于控制程序流程的消息是用户自己管理的,所以比较灵活。用户可以在任何时候对还没有处理到的消息进行调整,比如删除某些消息、改变其顺序等。
但实际上,这种应用比较罕见。
对事件的管理是在LabVIEW内部进行的。用户不能对其做调整。
其它VI对程序流程的控制 在其它VI中,也可插入新的消息到队列中来,从而控制这个VI的运行。
队列有个优点,就是可以通过队列的名字来得到一个队列,而不需要一定把队列的数据线连出去。这个优势可以使得程序编写稍许简化。
在其它VI中,也可抛出一个事件,来控制这个VI的运行。
事件比队列中的消息有个额外的优势,事件被抛出后,任何VI都可以接收到,这样其它VI不但可以控制当前这个VI的运行,也可以监视当前这个VI有哪些事件发出了。
事件比队列的缺陷在于它不能通过名字来获取,一定要连一根数据线。
使用事件结构定时 这个模式中的事件结构的延时设置有特殊用途,必须设定一个100~300ms的超时事件,超时事件发生后,程序继续向下执行。没有这个超时事件,程序可能会被阻塞在事件结构这里,而失去了对其它状态跳转的响应。
所以这里不能再把超时事件用来做定时用。
任何用户自定义事件都会触发事件结构,因此它不存在阻塞的问题。
程序中如有定时需求(比如每1秒采集一个数据),可以把超时事件做定时器用。
状态跳转时的滞后 程序每次运行至事件结构时,都需要等待100~300ms,超时事件产生后才能继续运行下去,如果这是,有新的消息加入到队列中,程序是不会立刻响应的。 没有响应滞后的问题。
适用场合 各种场合皆可,尤其是需要灵活改变消息的顺序时。 各种场合皆可。

Google Wave就要说拜拜了

今天听说Google放弃Wave这个产品了,真是可惜。我还是挺喜欢这个产品的,它比Email功能强大多了。

不过喜欢归喜欢,我自己也是只有在Wave新出来的时候玩了几下,之后就再也没去看过。不去的主要原因是因为我希望能够在一个地方看到自己所有的email,而不是到多个网站上去挨个查收,既然已经有了Gmail,还去看Wave干嘛呢。

要是Google能够在Gmail中提供一个选项,允许在Gmail中启用Wave一样的界面就好了。

框架插件结构两种实现方法的对比

这里说的框架插件结构是指程序启动时或运行中,去查看硬盘某个路径下,有哪些插件模块,然后把它们调用起来的这样一种程序结构。LabVIEW中实现这样的程序机构有两种方法,一是利用LabVIEW Scripting中动的态调用VI,二是利用LVClass。下面比较一下这两种方法各自的优缺点。

  动态调用VI LVClass
LabVIEW版本 LabVIEW很早的版本就具备了动态调用VI的功能。 LabVIEW 8.2开始才支持面向对象的程序设计。
开发的难度 相对来说比较直观易懂。 对于已经了解了LabVIEW面向对象编程的用户来说,做一个插件也是比较简单的。但是如果完全没接触过面向对象这个概念,还要先花不少时间去学习面向对象编程。
插件的形式 插件是一个单独的VI,插件所有的功能都必须在这个VI中实现。 插件是一个对象(类的实例)。插件对象可以拥有多个方法。一个新的插件被添加进来后,它的所有方法可以自动应用于框架程序中任何调用了插件方法的地方。
总结 适合简单程序 适合大型程序