(完整版)IDL入门教程十(组件程序)
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
第十章编写简单的组件程序
本章概述
本章目的是演示如何通过使用组件开发工具包开发一个简单的、具有图形用户界面的IDL程序。
“组件”一词是指用来表示用户与程序输入输出交互作用的图形元素。
按钮、滑动条和文本框都是组件。
组件工具箱是一套IDL API命令的集合,它主要用于开发图形界面程序。
在本章中,将学到以下方面内容:
1.组件定义模块和组件事件处理模块的区别;
2.如何编写组件定义模块;
3.如何编写组件事件处理模块;
4.如何解释和理解组件的事件结构;
5.如何创建不同类型的组件,比如顶层base组件,绘图组件和按钮组件;
6.组件程序模块之间在不使用公共块的情况下如何传递信息;
7.如何编写一个具有可改变大小的图形窗口的组件程序;
8.如何向组件程序中添加简单控制按钮;
组件程序的结构
许多IDL程序(比如前一章的ImageBar程序)只是由单一的程序模块组成,而组件程序至少包含两个种程序模块。
组件定义模块是一个必不可少的程序模块,它可以是过程,也可以是函数。
组件本身的创建或定义就是在组件定义模块中进行的。
并且,还至少有一个事件处理模块(可以是过程也可以是函数),它对组件事件、触发器或用户的交互作用做出反应,并加以处理。
(请看下图81)
甚至简单的组件程序也有几个事件处理模块,每一个模块都对不同的事件或触发器做出反应。
这就要求在事件处理模块和组件定义模块之间对程序的某些信息进行共享。
组件编程的主要技巧在于如何在构成组件程序或应用程序的各种程序模块之间传递程序信息。
图81 组件程序中的信息流从定义模块,通过事件结构传递到事件处理模块。
一般而言,定义模块只执行一次,而事件处理模块却是重复执行。
组件程序也被称为事件驱动程序,这意味着程序事先并不知道程序执行的顺序。
当用户对程序的组件元素进行交互作用时就会产生事件,然后事件处理模块根据事件产生的顺序分别对每个事件进行处理。
一般而言,组件程序的界面或可视部分在定义模块中产生,而组件程序的行为是在事件处理模块中产生。
事实上,大多数组件程序的定义模块代码只被执行一次,而事件处理模块被多次,反复执行。
每次事件产生时,相应的事件处理模块都会被执行一次。
组件程序如何对事件作出反应
除了创建组件界面外,定义模块的另外任务是建立被称为“事件循环”的组件,这个名字不确切,因为根本就没有真正的循环。
当然,也可以将组件程序想象为一种等待状态,等待组件发生什么。
而事实上也确实如此。
但是是哪个组件,或什么在等待呢?
其实是窗口管理器在等待。
在组件定义模块建立了事件循环的同时,它就将程序控制权转交给窗口管理器。
(如下面所示,这两项工作都是由Xmanager来完成的)窗口管理器对属于自己管理的事件或用户的交互操作进行管理。
当事件产生后,窗口管理把关于事件的信息压缩成包,然后传递给IDL。
IDL把事件信息解压后重新装入称为事件结构的里面。
然后,IDL调用合适的事件处理模块来对该事件进行处理。
在调用事件处理模块时,IDL把事件结构作为一个参数,也是唯一的一个参数传递给事件处理模块。
(详细信息请参阅附录A:传递给事件处理模块的事件结构。
)尽管每一事件结构的专有字段各有不同,这取决于产生事件的组件,但是每个事件结构都拥有三个专用字段:ID、Top和Handler。
这一点是相同的。
事实上,正是IDL结构变量中这三个字段的出现,使得此结构成为有别于其他结构的事件结构。
下面将会很快学到这三个字段的重要性及用法。
编写组件定义模块
组件程序中需要的第一件就是组件定义模块,它有三个目的(1)创建和定义共同组成组件程序图形用户界面的组件;(2)建立程序的事件循环;(3)注册到负责事件处理的窗口管理器的程序。
组件定义模块和其它模块一样,可以是过程也可以是函数。
也可以用平常方式定义参数和关键字。
在这里编写的程序将对上章的ImageBar程序进行包装。
并取名为XimageBar。
(程序名称前面的X是IDL表明该程序是组件程序的习惯。
但有时候也并非如此。
)为保证程序的简单明了,起初就为ImageBar定义一个它所需要的参数。
打开一个文本编辑窗口,键入如下所示:
Pro XimageBar,image
也可以像程序ImageBar一样对图像参数进行类似的处理,键入:
If N_Params() EQ 0 Then Begin
Image=GetImage(Cancel=canceled, ‘m51.dat’, Xsize=340, Ysize=440 )
If Canceled Then Return
EndIf
If Size(image, /N_Dimensions) NE 2 Then Begin
Message, ‘Image argument must be 2D.’, /Continue
Return
EndIf
定义和创建程序组件
简单或基本的组件通过使用组件工具箱API种的组件创建程序来建立。
一般来讲,这些程序都是一些函数,它们的语法规则如下所示:
widgetID=Widget_NAME(parentID)
其中NAME是所创建组件的类型,可能的取值有:Base、Button、Draw、DropList、Label、List、Slider、Table和Text。
变量parentID是组件的父亲或在组件层次上处于上一级的组件的标示号。
(请参阅图82的组件层次的例子)
组件结构类似于家谱图。
这样,就可以讨论组件好象它们有家庭关系一样。
我们经常说到组件的父亲、孩子或兄弟。
我们这样描述的其实就是组件之间的相互关系。
定义模块的目的便是创建每个组件和并通过参数parentID来描述它与程序中其它组件的关系。
每个组件结构图都有一个顶层的base组件。
base组件是其它组件的容器。
顶层的base 组件容纳了所有共同组成程序图形用户界面的其它组件。
在组件程序中,只有顶层的base 组件才不需要parentID参数。
在组件创建函数中,所有其余的组件都要求有单一的parentID 参数。
图82:组件层次结构图。
组件A为顶层base组件,同时也是其它组件的容器。
组件B,C,D是组件A的孩子,它们彼此为兄弟关系。
组件F,G,H是组件D的孩子,彼此是兄弟
关系。
注意,作为一个函数,组件创建程序总是返回一个值。
WidgetID的值是创建指定组件的标示符,它是一个唯一的长整型数值。
如果想从现实世界中找到一个比喻,可以把此整数看成社会保险或纳税号码。
这个数值可以将组件区别于其它所有组件,即使它们的名字相同。
如果以后要对这个组件进行处理,就必须在程序中保存这个数,同时这也是程序区分组件的方法。
创建顶层base组件
事实证明,在组件程序中创建最重要的组件就是顶层base组件。
顶层base组件容纳了程序中所有其它组件。
事实上,它就是出现在屏幕上的、包含了程序组件界面的活动窗口。
尽管顶层base组件在创建函数中不需要parentID参数,但是它可能需要通过关键字来设置
组件的其他属性。
这种情况下,希望顶层base组件有一个标题,希望子组件被创建后以列的形式存在,希望可以改变顶层base组件的大小,并希望该程序和在电脑中看到的用到的的其他许多程序一样可以利用按钮来控制程序操作。
定义这样一个顶层base组件,可以键入:tlb=Widget_Base(Title=’XimageBar Program’, Column=1,$
Tlb_Size_Events=1,Mbar=menubarID)
注意,关键字Title、Column、TLB_Size_Events均为输入型关键字(信息传递到程序Widget_Base),而关键字Mbar就是一个输出型关键字(信息从程序Widget_Base中传出来),因为输入型和输出型关键字的语法规则完全相同,所以必须从上下文及相关文档中来区别它们。
(关键字的详细信息请参阅211的“定义关键字”)
在上述的例子中,变量menubaseID未被定义直到Widget_Base命令赋值给它。
变量menubaseID使得在顶层base组件的菜单栏内设置按钮成为可能,因为menubaseID变量可以作为菜单栏按钮的父亲标示符。
创建菜单栏按钮
接下来,定义一个下拉菜单file,其中有一个Quit子菜单。
其代码如下所示:fileID=Widget_Button(menubarID,Value=’File’, Menu=1)
quitID=Widget_Button(fileID,V alue=’Quit’)
注意,变量menubaseID作为菜单项File的父亲是如何使用的。
File按钮,反过来又是Quit按钮的父亲(通过标示符fileID)。
按钮组件通常是不允许作为其它按钮的父亲。
之所以File可以成为其他组件的父亲,是因为关键字Menu在起的作用。
File按钮带了关键字Menu就变成了下拉式菜单,因而可以有孩子。
注意,在菜单项File的创建函数中,并不是一定要求有Menu关键字,尽管虽然设置它没有什么害处。
菜单栏的任何孩子(通过顶层base创建程序中的关键字Mbar),从定义上来说,都是一个菜单及下拉式菜单。
菜单栏按钮本身从来就不产生事件,它们仅仅显示子菜单,即与之相关的菜单项。
这些按钮的值就是按钮上的文本。
一般而言,如果要对按钮的值进行处理,必须记得这些字符串是区分大小写的。
为程序创建图形窗口
下一步将要创建的组件是程序图形窗口。
这是一个绘图组件,绘图组件同普通的IDL 图形窗口虽然不是完全相同但却很相似。
例如,虽然可以在常规图形窗口中使用Cursor命令,但并不想在绘图组件窗口中使用Cursor命令。
(组件程序中,在绘图组件中使用Cursor命令会导致异常奇怪的现象,一些看起来无论如何都与Cursor命令无关的现象。
在绘图组件中使用Cursor命令是完全没有必要的,因为在绘图组件的事件中已经包含了要从Cursor命令获取的信息。
)
创建一个绘图组件并使之成为顶层base组件的孩子,键入如下所示:
drawID=Widget_Draw(tlb,Xsize=400,Ysize=400)
注意,绘图组件的大小,和常规IDL图形窗口一样是以象素为单位的。
在屏幕上实现组件
当组件由组件创建函数创建时,它们只是存在于IDL内存中,并没有在屏幕上显示出来。
它们如果要显示出来,就必须被实现。
实际上,顶层base被实现后其它所有在层次关系上属于它的组件都将实现。
实现组件是与组件交互作用的方式之一。
在IDL中,所有组件的交互作用都是由Widget_Control命令产生。
这个命令要求将交互操作的组件的标示符传递给它。
Widget_Control命令有六十多个不同的关键字,可以以不同的方式与组件进行交互作用。
为实现整个组件层次,可以键入以下命令:
Widget_Control, tlb, /Realize
使绘图组件成为当前图形窗口
不同于常规IDL图形窗口,绘图组件创建后不会成为当前图形窗口。
事实上,在把图形绘制进去之前,就必须特别指定绘图组件窗口为活动窗口或当前的图形窗口。
和操作一个常规IDL图形窗口一样,在操作时,可以用WSet命令和窗口的图形索引号来实现这个。
但必须记住,绘图组件的标示符(如上面的drawID)并不是绘图组件图形索引号。
事实上,绘图组件并不会被赋予一个图形索引号,直到它被实现。
并且,绘图组件的图形窗口索引号就是绘图组件的值。
这和绘图组件标示符(好比是它的保险或纳税号码)差别很大。
注意一定不要混淆这两个。
因为作为一个顶层base的子组件,绘图组件由上面的命令实现后,IDL就将一个图形窗口索引号赋给它。
获得图形窗口索引号后,将绘图组件设置为当前图形窗口,键入如下所示:
Widget_Control, drawID, Get_Value=wid
Wset, wid
注意,关键字Get_Value为输出型关键字,绘图组件的图形窗口索引号存放在变量wid 中。
在绘图组件窗口上显示图形
只需要调用以前编写的程序ImageBar,并把这里收集的参数传递给它,就可以轻易地将图形显示在绘图组件窗口中。
(如果没有自己编写这个程序,可以在与本书配套的文件中找到程序ImageBar3.pro,并在运行之前编译它)这时,该明白这样编写程序的优点了吧。
因为程序可以进入到当前图形窗口而都不必考虑窗口大小。
调用方式如下:
ImageBar, image
保存程序运行时所需要的信息
要程序成功运行,几乎每个组件程序都需要某些信息,这可能是组件标示符、程序数据、图形窗口索引号数和其它信息或变量。
通常,这种信息在一个程序模块中创建或可以获得,但在另一个程序模块却需要它。
例如,大多数组件标示符在组件定义模块中创建,但在程序
发生行为的事件处理模块中使用。
大量组件编程技巧都来自知道如何将信息从一个程序模块传递到另一个模块,这可以通过多个方法来实现。
如许多程序使用公共块使程序模块得到这类信息。
但是定义公共块会导致组件程序更没有实用价值,因为在没有公共块冲突的情况下,任何时候只能有一个程序的实例在运行。
设想一下,比如一个图象处理程序,它将图像数据保存在一个名为data的公共块中,这个程序的两个不同的实例在初始化时载入了不同的图像数据,那么第一个程序处理的数据就不再是自己的数据了。
两个程序都只能处理后被载入到公共块的图象。
由于这个以及其它原因,作者根本就不喜欢用公共块来编写组件程序。
为避免使用公共块,作者使用自称为info的结构来存储必要的程序信息。
这是个匿名的结构变量,它包含了在组件程序中所需要的任何信息。
长期经验表明,在创建结构时最好是结构字段名称和输入的变量名称相同。
虽然不是要求有这个习惯,但它却可以防止混淆。
作者尽力在组件程序中给变量取相同的名字,这样使得当在一段程序代码中看见变量名时,可以容易知道程序正在处理什么。
在这个程序中,作者至少需要三条信息:图象数据、绘图组件的图形窗口索引号(使得在绘图之前可以将它设置为当前窗口)和绘图组件标示符(当需要时可以将它实现)。
info 结构将这样定义,将以下三行代码输入到程序文件中:
Info={image: image, $ ;The image data.
Wid: wid, $ ;The window index number
DrawID:drawID } ;The draw widget identifier
有用的程序信息。
必须保存在全局内存中,这样需要这个信息的组件程序模块就可以获得。
变量info到现在为止还是局部变量,当组件定义模块代码执行完毕后,将被清除和销毁。
如果不把info结构保存在全局内存中,当事件处理模块需要这个信息时它将不再存在。
使用组件用户值保存程序信息
事实上,组件在创建后也是被存储在全局内存中的。
更重要的是,每个组件创建时都有一个内置的分类容器,称为用户值(user value),那里可以存储任何类型的变量。
不管选择何种用途,都可以获得这个用户值。
这种情况下,可以使用组件的用户值来储存info结构的信息。
但是,应该使用哪种组件的用户值呢?事实是,选择哪一个都无所谓。
要达到这个目的,可以使用程序中未用过的组件用户值来实现。
实际中按照惯例,一般使用顶级base的用户值来实现这点。
这个选择的优点待会就可以看到。
将info结构信息拷贝并存储顶级base的用户值中,在程序中键入如下所示:Widget_Control, Tlb, Set_Uvalue=Info
创建事件循环和注册程序
组件定义模块中的最后一步就是创建组件事件循环并将程序注册到窗口管理器中。
如果用C语言编写这个程序,也许要花费半个下午和一页的代码才能完成这项任务。
而在IDL 中只需简单调用一个XManager命令即可。
图83:XImageBar组件定义模块创建的组件结构示意图。
info结构存储在顶级base的用户
中。
XManager命令第一个参数是要注册的程序名称。
一般而言,这和组件定义模块文件的名称是一致的,但并不是一定要如此。
其实它可以为任何名字。
在拼写名称时,最好统一是大写或小写,在以后因为某种原因需要查寻时,名字是区分大小写的。
命令的第二个参数是一级基础组件的组件识别。
知道一级基础识别,XManager就可以知道结构图中所有其它的组件。
注册程序并结束组件定义模块,键入下列两个命令:
Xmanager, ‘Ximagebar’, tlb
End
运行程序
到了这一步,就可以编译并运行程序XimageBar了,但是注意不要产生任何事件。
IDL>.Compile XImageBar
IDL>XImageBar
如果产生事件会出现什么情况呢?试一下并解释原因。
在这个程序中,只有两个组件能产生事件:一个是顶级Base,它在用户改变窗体大小后会产生,另一个是Quit按钮,它在用户选择后也会产生事件。
绘图组件也能产生事件,但它们必须明确地被指定,但在这里没有设置。
File按钮不能产生事件,因为它是菜单按钮(例如,它被设置了关键字Menu)。
如果企图在XImageBar程序中产生一个事件,那么就会产生错误。
这是因为并没有为这个程序编写事件处理模块。
等一会儿即将完成这项工作。
创建无阻塞组件程序
另一件值得注意的是,这个程序是一个阻塞程序。
也就是说,当运行这个程序时,就无法使用IDL命令行。
IDL5的最大的优点之一是,在组件程序运行的同时可以使用IDL命令行,但默认的设置是不具有这种能力。
为使组件程序成为无阻塞程序,就必须在XManager 命令中设置No_Block关键字。
在程序中找到XManager命令的位置,并作如下修改:Xmanager, ‘xImageBar’, /No_Block
这个程序的定义模块可以在与本书配套使用的文档中找到,文件名为xImageBar1.pro。
编写事件处理模块
事件处理模块的目的就是处理程序所产生的事件。
回忆一下,是窗口管理器对事件进行管理的。
当一个事件发生后,窗口管理将事件的信息打包并传递给IDL。
IDL将事件信息解包后重新打包到一个事件结构中,然后直接将事件结构作为事件处理模块的一个也是唯一的一个定位参数传递过去。
事件结构内的字段依产生事件的组件不同而不同(事件结构的详细信息请参阅附录A:事件的结构)。
但是,所有组件的事件结构有三个字段是相同的:ID、Top和Handler。
理解这三字段的意义非常重要。
事件结构中的公共字段
事件结构中的ID字段总是产生事件的组件的标识符。
例如,在正在编写的程序中,如果用户选择了Quit按钮,那么事件结构中的字段ID就被设置为Quit按钮的标识符。
(如在上面的代码中,即为quitID的值。
)
产生事件的组件往往存在于其它组件的层次结构中。
Top字段是用来标识位于该层次结构中最顶级的组件。
换句话说,在ID所标识的组件所在的层次结构中,Top的值就是顶级base的标识符。
在上面的例子中,它应该是tlb的标识符。
Handler字段稍微有些复杂。
每个组件程序至少有一个事件处理模块,但真正的组件程序经常有好几个事件处理模块。
事实上,可以为所创建的每一个组件编写一个事件处理程序,但是程序一般是不会这样编写的。
例如,如果Quit按钮有单独的事件处理程序,顶级Base 的改变大小事件也有单独的事件处理程序,那么编写XImageBar程序也许会简单些。
如果这两个单独事件对应的操作在实现过程中所采用的算法差距很大,那么就经常采用单独的事件处理程序。
程序中创建的每一个事件处理程序都必须与某一个特殊的组件联系起来。
处理产生事件的组件的事件的事件处理程序,与之相关的组件标识符。
Handler包含的值其实就是组件标识符,一个与事件处理程序相关联的组件标识符。
下面的图例将有助于加深理解这点。
如图84中所示,组件A-G已经定义,与顶级base,组件A,相关联着一个事件处理程序。
假设组件G产生一个事件,IDL看是否有一事件处理程序与组件G相关联。
在这里,是没有的,所以事件在组件层次结构图中“上浮”一层。
IDL再看组件F,是否有事件处理程序与之相关联。
仍然没有。
事件继续上浮到组件C,…等,最后,事件找到了与组件A相关联的事件处理程序。
图84:一个简单的组件程序。
组件A-G已经定义。
与顶级base相关联着一个事件处理程序(填充圆所示)。
如果组件G产生一个事件,事件就会上浮至与组件A相关联的事
件处理程序。
然后事件结构就这样填写。
ID字段标识产生事件的组件,即组件G。
Top字段标识组件G所在的层次结构中的顶级组件,即组件A。
Handler字段标识与处理来自组件G事件的事件处理程序相关联的组件,仍然是组件A。
如图85所示,看看一个稍微复杂的组件程序。
这里有两个事件处理程序:一个与组件A相关联,一个与组件F相关联。
如果组件G产生一个事件,那么事件就“上浮”至与组件F相关联的事件处理程序。
这时,事件结构的Handler字段标识就是组件F而不是组件A 了。
当组件G产生的事件进入到与组件F相关联的事件处理程序时,产生什么结果就与组件F的事件处理程序有关了。
事件处理函数
事件处理模块可以是过程,也可以是函数。
如果与组件F相关联的事件处理模块是个过程,那么事件会被那个事件处理程序“吞掉”。
也就是说,事件处理后就被终止。
然后程序就等待下一个事件的发生。
但是假设,事件处理程序是个函数,而不是过程。
函数,回想一下,是要返回值的。
如果事件处理函数的返回值是个结构,且这个结构中有三个已经定义的长整型字段ID、Top和Handler,那么IDL将把返回值作为一个事件结构上浮至层次结构图中的下一个事件处理程序。
如果返回值不符合事件结构的标准,那么这个事件也称为被事件处理函数吞掉了。
图85:一个带有两个事件处理模块的、稍微复杂些的组件程序,产生于组件G的事件
由与组件F相关联的事件处理程序来处理。
因而有选择性地返回事件或处理事件及创建自己的事件是有可能的。
例如,假设想将一个绘图组件用起来像一个按钮,就应该将绘图组件事件打包成一个按钮事件,然后将事件结构传递给层次结构中的下一个事件处理程序。
伪按钮事件的事件处理程序的结束或许会像如下所示:
Event={PSEUDO_BUTTON_EVENT,ID:Event.handler,TOP:Event.Top,$
Handler:0L,Select:1L}
Return
End
注意,Handler字段由长整型零填充,一个无效的组件标识符。
IDL将获取这个返回值,检查到ID、Top和Handler字段为合适的长整型,并在Handler字段中填入适合的值。
因而不必在最初时为查找一个适合Handler而担忧。
最为重要的是,为字段ID和Top找到合适的组件标识符。
例子里显示的值非常具有代表性。
将事件处理程序和组件联系起来
如何才能将事件处理模块和特定的组件联系起来呢?最简单的方法就是,在组件创建时,用关键字Event_Pro(如果事件处理模块是一个过程)或Event_Func(如果事件处理模块是个函数)来指定适当的事件处理模块。
例如,为quit按钮创建一个单独的事件处理过程,可以在组件定义模块中按钮创建程序中做如下修改:
QuitID=Widget_Button(fileID, Value=’Quit’,Event_Pro=’XimageBar_Quit’) 最好为事件处理模块取个独一无二的名字。
作者喜欢以组件定义模块名作为事件处理模块的开端,这样就不会混淆。
注意,名字不区分大小写。
一个组件除外,Event_Pro和Event_Func关键字可用于将事件处理模块和其它任何组件联系起来,这个组件就是由Xmanager直接管理的那个组件(如,顶级base的标识符就是Xmanager命令的参数之一)。
Xmanager直接管理的顶级base已经有它们的事件处理程序,它是通过Xmanager命令中的Event_Handler关键字来设定的。
与顶级base相联系的事件处理程序必须是一个过程而不是函数。
如果在调用Xmanager命令时没有使用Event_Handler关键字,那么与顶级base相关联的事件处理过程就是程序所注册的名字加上_Event。
例如,程序XImageBar默认的事件处理。