XControl
这一节将详细介绍 XControl 的制作方法。为了便于讲解,将以 “界面设计实例” 中提到的棋盘为主要例子,介绍如何制作一个 XControl 控件。在讲解 XControl 的某些特殊功能时,也会介绍一些其它的例子。
设计
XControl 的主要特点是可以把它的界面元素与相关的代码封装在一起,从而 便于发布和重用这些界面组件。但是 XControl 也有不足之处,它的开发难度较大。此外,不合理的 XControl 可能导致程序出现更严重的问题,比如导致程序界面死锁等。所以,在开发 XControl 之前,一定要精心设计它的结构和行为,以避免出现错误。
在开发一个新的界面组件之前,首先要考虑一下以何种方式实现这个组件。
如果一个界面组件极为特殊,且只可能用在某个特定的程序中,那也许就没有必要将其制作成单独的控件了。可以把这个界面组件做成一个独立的子界面 VI,然后通过子面板控件调用这个界面。
如果这个组件需要被多次使用,那么就应该考虑把它做成可重用的独立控件。如果这个控件不包含任何特殊行为,比如一个新型按钮,仅其外观与一般的按钮不同,其它行为都与传统的按钮一模一样。这样的控件适合制作成用户自定义控件。
如果新的组件需要重用,其行为与已有的其它控件又有较大差别,那么就要考虑使用 XControl 了。比如:需要制作一个新按钮,它比传统按钮多一个状态;制作带有动画效果的控件;制作使用中国本土度量单位的数值类控件;制作一个专用于绘制某种特殊曲线的图片控件等。它们都比较适合使用 XControl 来开发。
前面提到的黑白棋的控件,它既有特殊界面,又有特殊行为,又可以应用于不同软件中,它就非常适合做成 XControl。
首先要具体设计这个 XControl 所需的界面和行为。
它的界面部分可以直接利用界面设计实例一节中设计好的界面。在前文提到的几个设计方案中,使用图片下拉列表数组控件的解决方案,编程代码最简单。所以,在设计 XControl 时就采用这个界面方案。
XControl 在程序框图上的接线端的输入输出数据应该是应用程序中最经常需要与 XControl 交互的数据。本例中,应用程序最常使用的数据是棋盘的布局信息。因此,这个 XControl 的输入输出数据应当是一个表示棋盘上棋子的布局的 8×8 的整型数组。
黑白棋控件的属性应当包括:当前该下什么颜色的子、可落子的位置、盘面上每种颜色的子数、上次落子的位置等。
它的方法应当包括:落下一个棋子的方法。这个方法需要包含以下具体的操作:在新位置放置一个棋子;翻转被吃掉的棋子;更新数据和所有属性的值。
还有,当用户在交互界面上摆放下一个子之后,需要发送一个事件通知应用程序。
创建
在项目浏览器上,点击鼠标右键,选择 "新建 ->XControl",就可以创建一个新的 XControl:

XControl 在结构上是一种特殊的库。它包含一些特定的功能 VI,和一些可选的属性、方法 VI 及其它相关文件。在新建的 XControl 上已经包含了 4 个必须的功能 VI(控件):"数据"(Data)、"状态"(State)、"外观"(Façade)、以及 "初始化"(Init)。XControl 还有两个可选的功能 VI:"反初始化" 和 "转换状态以保存"。
数据功能控件:用来定义 XControl 的数据类型;
状态功能控件:定义除数据功能之外所有 XControl 内部使用到的数据;
外观功能 VI:XControl 中最主要的功能 VI,用以实现 XControl 的界面和界面上的行为;
初始化功能 VI:设置 XControl 的初始状态;
反初始化功能 VI:负责清理工作,从内存中删除 XControl 时,释放所有分配给该 XControl 的资源;
转换状态以保存功能 VI:用于把 XControl 内部的某些数据保存在使用它的 VI 中。
反初始化功能 VI 和转换状态以保存功能 VI 在新创建的 XControl 中不存在。如果需要使用到它们,可以在项目浏览窗口的 XControl 上点击鼠标右键,选 "新建 -> 功能",为 XControl 添加一个功能 VI。
XControl 被保存在一个.xctl 文件中。它与 LabVIEW 库文件十分相似,可以把它看作是一种特殊的 LabVIEW 库文件。它的属性设置与 LabVIEW 库的设置也一模一样:

先把这个新创建出来的 XControl 存盘。XControl 功能 VI 的文件名并不一定与其功能名相同。比如,在这个演示中,为了方便更多人使用,可以选择使用英文名称来保存 XControl:

需要注意:XControl 的功能 VI 由 LabVIEW 系统调用,并具有预设的输入输出接口。我们在编写 XControl 的时候一定不要改变它们的输入输出控件,以及接线端的布局。我们也不可以直接在其它的 VI 中直接调用这些功能 VI。XControl 开发者需要做的只是填充功能 VI 所需的程序代码。功能 VI 之间的数据无法直接传递,也不应该使用全局变量,只可以通过由功能控件定义的“数据”和“状态”来传递。
"数据" 功能控件
XControl 有两个功能控件。"数据":用于定义 XControl 的数据类型;"状态":用于定义 XControl 使用到的内部数据的数据类型。
首先考虑 "数据" 功能控件,它是一个.ctl 自定义类型文件。它所定义的数据类型,就是 XControl 控件实例在程序框图上的接线端的数据类型。在这个例子中,使用一个二维的 U16 数组表示棋盘布局,所以在数据功能控件中要使用一个二维数组:

"状态" 功能控件
"状态" 功能控件也是一个自定义类型控件,它是一个簇控件。簇中包含了用于定义 XControl 运行所需的全部变量。
下图显示的就是运行一个黑白棋 XControl 所需的内部数据。

它包含以下几部分:
method:用于定义 XControl 的方法。当用户运行一个 XControl 的某种方法时,设置 method 变量。每个 XControl 方法为 method 变量设置不同的值,这样,在 XControl 的 "外观" 功能 VI 中,就可以知道用户调用的是什么方法了。
current color:用于表明当前应该落什么颜色的棋子,是白色还是黑色。
available black position:这是一个二维整数数组, 表示黑色棋子可以放置的位置。
available white position:一个二维整数数组,表示白色棋子可以放置的位置。
Interactive Action:是一个用户自定义事件。当用户在棋盘上落下一棋子时,XControl 就产生这个事件,通知应用程序,用户走子了。
row 和 column:两个整数,用于记录上次落子的位置。
前文只是简短介绍一下每个数据表示的含义。在后文中使用到它们的时候,还会详细解释它们的内容和使用方法。实际在编写 XControl 的时候,也许不可能在一开始就把 "状态" 功能控件设计得十分完美。可以一边实现 XControl 的各种功能,一边对其进行补充。
"外观" 功能 VI
"外观" (Facade) 功能 VI 是 XControl 最主要的功能 VI,它定义了 XControl 的行为。它的程序框图采用的也是典型的 While 循环配合事件结构的模式。
普通应用程序只有在点击“运行”按钮后才开始执行代码。而对于 XControl,只要它所在的宿主 VI 被打开(即使是在编辑状态下),LabVIEW 底层引擎就会在后台静默启动并持续运行 "外观" 功能 VI。
这意味着,在编辑界面时看到的 XControl 外观,实际上是它的代码正在后台实时运行渲染出来的结果。
在默认生成的 Façade VI 模板中,超时时间往往被设置为一个很小的值(或通过特定的状态机逻辑控制)。这是为了允许 LabVIEW 底层调度器在没有前面板交互时,能够有效管理这个后台运行的 VI 的 CPU 占用率。切记不要在 Façade VI 的事件结构外部或内部放置任何会导致长时间阻塞的代码(如死循环或长 延时),否则会导致整个 LabVIEW 编辑环境卡死。
界面
"外观" 功能 VI 的前面板用于定义 XControl 的界面,这里可以直接使用在界面设计实例一节已经设计好的界面。把前文设计好的棋盘棋子界面拷贝过来就可以了。这个 "外观" 功能 VI 窗口的大小,就是以后用户把 XControl 控件拖拽到 VI 前面板上后,XControl 实例控件显示出的大小。所以这个 VI 的大小要刚好包裹住棋盘:

如果希望制作好的 XControl 在用户 VI 前面板上被使用时,可以被任意调整大小,"外观" 功能 VI 前面板上的控件都应该随着前面板的尺寸自动改变大小。
比如,下图是一个 XControl"外观" 功能 VI 的前面板,它的大小就是这个 XControl 被放到另一个 VI 上之后的初始大小:

下图是正在使用了这个 XControl 的实例控件的 VI,用户可能会拖动 XControl 的实例控件来改变它的大小。这就相当于,改变上图 VI 前面板的大小,在这个前面板上的按钮控件必须跟随前面板的尺寸做出相应调整,在下图中改变 XControl 实例控件尺寸时,按钮的尺寸才会相应地改变。所以上图这个 "外观" 功能 VI 必须被设置为 "根据窗格缩放所有对象"。
对于示例中的棋盘 控件,可以简单地规定不允许用户调整它的尺寸。只要在 "外观" 功能 VI 的属性设置中的窗口外观一页,取消 "允许用户调整窗口大小",即可禁止用户调整 XControl 实例控件的大小。
工作原理
"外观" 功能 VI 的程序框图定义了 XControl 的行为。比如当用户点击了 XControl,XControl 就会作出的反应,这与普通的应用程序界面是一样的。它的程序框图采用的也是循环事件结构模式,但是需要特别注意它与应用程序的不同之处。
普通采用循环事件结构的应用程序会一直运行,等待事件出现并进行处理。而 "外观" 功能 VI 只有当 XControl 有事件发生时,才会被 LabVIEW 自动调用,处理完这个事件后,必须立即退出运行。LabVIEW 要等 XControl 的 "外观" 功能 VI 运行结束后再去完成其它界面处理功能。所以,千万不要试图在 XControl 的 "外观" 功能 VI 里添加持续执行的代码,如控制 XControl 上的动画等。这将会导致 LabVIEW 对其它界面的操作失去反应。
"外观" 功能 VI 的超时事件处理与一般的应用程序都不同,程序在这里设置了退出循环,并且超时时间是 0。这意味着,一旦其它的事件都处理完成,"外观" 功能 VI 程序立即转入处理超时事件,然后立即结束运行。

参数
"外观" 功能 VI 有三个输入参数和三个输出参数。
-
Data In / Data Out:从 XControl 接线端写入或读出的数据。它们的数据类型是由 "数据" 功能控件定义的。"外观" 功能 VI 的程序框图开始运行时,Data In 中的数据就是 XControl 当前的值。程序框图运行过程中可以对这一值进行修改。修改后的值由 Data Out 输出,返回给 LabVIEW。
-
Display State In / Display State Out:是 XControl 运行时用到的所有内部数据。在本书中有时简称它为状态。它的数据类型由 "状态" 功能控件定义。它也可以在程序运行中被改变,它的输入输出方式与 Data 类似。
-
Container State:输入参数。它是一个簇,用于表明 XControl 实例(把一个 XControl 拖拽到一个 VI 的前面板上,就产生了一个 XControl 的实例)在 VI 面板上的状态。它包含三个元素: "Indicator?"、"Run Mode?" 和 "refnum"。 "Indicator?" 表明 XControl 实例是否是一个显示控件。当其值为假时,表明 XControl 实例是一个输入控件。"Run Mode?" 表示 XControl 实例所在的 VI 是否处于运行状态。"refnum" 是指向 XControl 实例的引用。
-
Action:输出参数,用于通知 LabVIEW 程序在这次执行中对 XControl 所做的修改。它包含三个元素:"Data Changed?"、"State Changed?"和"Action Name"。如果在程序中改变了 Data,那么就一定要把"Data Changed?"设置为真,通知 LabVIEW。这样,改变的数据才会生效。同样,如果改变了 State,则一定要把"State Changed?"设置为真。"Action Name"是一个字符串,可以给它输入一段表明这次程序运行的简短文字。这段文字会在 LabVIEW 的菜单项" 编辑 -> 撤销 " 中 出现。
数据更改事件
"外观" 功能 VI 中的事件处理结构主要处理两类事件。一类是针对 XControl 的特殊事件,另一类是用户在界面上操作产生的事件。
针对 XControl 的特殊事件有 4 个:数据更改、显示状态更改、方向更改、执行状态更改。
当有一个数据通过控件的接线端输入给 XControl 的实例时,就会触发数据更改事件。对于数据更改事件的处理,通常是用新的数据更新界面上的控件和 XControl 的状态(Display State)。
在棋盘 XControl 的例子中,它的数据就是棋盘的布局。因此,如果程序给 XControl 设置了新数据,就需要对 XControl 界面上棋子的布局做出相应的更新。
下图是黑白棋控件对数据更新事件的处理代码:根据新的棋盘布局,刷新棋盘在界面上的显示。程序中使用到了一个子 VI。它用于计算黑白棋子可以落下的位置,用于 XControl 检查用户操作的合法性。在这里主要介绍 XControl 的相关内容,所以不再对子 VI 再做详细解释。黑白棋子可以落下的位置被记录在 XControl 的状态中。在更新了棋子的布局后,需要重新计算黑白棋子可以落下的位置,并更新 XControl 的状态。由于 XControl 状态改变,必须通知 LabVIEW 对新状态做相应的处理。所以,这里一定要把 "State Changed?" 设置为真。
