本书剩余几章将描述Lisp-Stat的绘图系统,该系统的主要目的是支持使用、自定义及开发动态统计绘图。作为该努力的一部分,系统将提供获取用户接口工具的方法,比如惨淡和对话框。该系统被设计成一个高级的工具集,即实现在微机或者工作站的窗体系统之上,比如Macintosh Toolbox或者X11系统。
本章将介绍一些基础的视窗系统概念,重点突出Lisp-Stat绘图系统的实际。此外,本章还描述了基本的Lisp-Stat窗体、菜单和对话框。接下来的两章将描述Lisp-Stat 的绘图窗体。
很多流行的微机和工作站都使用一个绘图接口来使他们和他们的程序易于使用。这些接口将屏幕分割为一些独立的窗口,这个独立的窗口可以叠置,也可以像在桌面上移动一张纸一样移动它;菜单用来发布一些用来运行的命令;对话框用来向用户询问信息。
绘图接口有很多共性,作为由Xeror公司开发,由Macintosh普及的系统的后代,我们就不觉得大惊小怪了。但是,与传统的表示菜单、由哪个窗体接收键盘敲入的字符等等操作相比,仍有些不同。这些传统叫做用户接口说明。例如,在Macintosh系统的菜单上通常放置一个菜单框。在SunView系统里,在由三个按键的鼠标上,菜单在窗体的不同部分按下右键的时候会弹出。还有一个例子,在Macintosh系统上,为了选择一个文本集,你可以在文本起始处单击鼠标,再拖拽鼠标覆盖文本集。通过在要扩充的选集后边点击鼠标,同时按下shift键,你也可以扩充一个选集。在SunView里,惯例是点击鼠标左键以开始选择,然后通过拖拽或者点击鼠标中间的按钮来扩展选集。
视窗系统是设计用来支持使用图形用户接口编写程序的编程工具集。视窗系统负责绘制窗体和菜单,并监测用户动作,比如移动或改变窗口大小,然后通知程序所有者该窗体是够需要任何形式的更新。例如一些窗体系统,像Macintosh Toolbox和SunView系统,被设计来实现用户接口直到的特定集合。其它的系统,比如X11系统,被设计可以支持各种各样不同的用户接口,X11系统也想要用到更多种类的不同硬件上。尤其是,它支持带一个、两个、三个按钮的鼠标。
带图形接口的程序的主要特征是需要响应用户产生的事件:当窗体移动或改变大小时,它们可能需要重画,当需要菜单时,有需要解释的按键敲击或者鼠标动作时,它们就要显示。这就使得编写一个这样的程序比编写一个仅仅运行一个任务到结束为止要有相当大的难度,或者甚至比解释一个每次每行敲入的文本的程序还要难。然而,选择一个好的编程策略会使得这种编程变得更容易些。尤其是,桌面虚拟化让使用面向对象方法更加自然,在该编程策略里物理窗体和菜单在软件里是以对象的方式表示的。指向这些对象的用户动作可视为消息。
在面向对象的视窗系统里,程序员只需要构造一个合适的对象集合,并给每个需要响应的对象的消息编写方法。然后视窗系统将运行“事件循环”,监测用户动作并转换任何需要程序处理成消息的事件。例如,如果视窗系统检测到窗体的一部分显露出来了,那么它将向对象发送一个显示窗体的消息要求该对象重画其自身。一个面向对象视窗系统还可以提供一个标准绘图对象的库,然后程序员就能够安排一个新对象来从库里的最适合的对象对对象集合那里来继承。因为库里的对象为大多数消息提供了合理的默认方法,这就极大地减少了程序员编写新方法的数量。另外,它有助于减少与用户接口规范的背离,因此会使得程序更加易用。
Lisp-Stat绘图系统是一个高级的、面向对象的工具集,设计它是为了实现在更多一般的窗体系统之上的绘图系统。为了允许系统保持合理的间接性,一些这种方案是需要的,但是同时还要合理地拟合与用户接口规范之间的不同。一些概念是有一点抽象地对待的,允许以一种最适合本地用户接口的方式,用特定的实现来处理它们。例如,每个Lisp-Stat绘图窗口都有一个适合它的独立的菜单。在Macintosh操作系统的XLISP-STAT里,系统将确保无论什么时候窗体是最前面的窗体时,菜单都会被安装到菜单栏上。在SunView里,当右键在窗体里点击时,系统将弹出菜单。XLISP-STAT的X11版本在绘图窗体里提供了一个按钮,当该按钮按下的时候将弹出菜单。相似地,当窗体被告知有一个鼠标点击事件发生的时候,任何有效的修改都会被通告。在Macintosh操作系统上,该系统有一个按钮鼠标,通过在按住鼠标键的同时按下shift键或者option键,修改信号将被发送。在带三个鼠标按键的系统上,修改信号可能只识别按下的那个鼠标按钮。
图7.1 一个用来检测一个回归模型的菜单
Lisp-Stat支持至少两种窗体:绘图窗体和对话框窗体。有的实现也可能支持其他的窗体类型,像文本窗体。所有的窗体都共享一定的基本特性。这些特性被收集在一起放到基本窗体原型window-proto里。这个原型不能直接使用;它的作用仅仅是收集这些共性特征。对话框窗体和绘图窗体原型都继承自window-proto。window-proto的:isnew方法允许一些特征使用关键字参数来设置。除非另有说明,对更多特化的窗体类型的:isnew方法也能接受这些关键字。
每个窗体都有一个标题,在很多用户接口里,这个标题显示在窗体顶部的标题栏里。例如,如果p是一个由histogram函数创建的直方图窗体,那么它的标题的初始化将由下式给出:
> (def hardness (list 45 55 61 66 71 71 81 86 53 60 64 68 79 81 56 68 75 83 88 59 71 80 82 89 51 59 65 74 81 86)) HARDNESS > (def tensile-strength (list 162 233 232 231 231 237 224 219 203 189 210 210 196 180 200 173 188 161 119 161 151 165 151 128 161 146 148 144 134 127)) TENSILE-STRENGTH > (def abrasion-loss (list 372 206 175 154 136 112 55 45 221 166 164 113 82 32 228 196 128 97 64 249 219 186 155 114 341 340 283 267 215 148)) ABRASION-LOSS > (setq p (histogram hardness)) #<Object: 13feb04, prototype = HISTOGRAM-PROTO>
> (send p :title) "Histogram"如果图形里包含一个变量名为hardness的直方图,那么我们给出窗体更适合的一个标题:
> (send p :title "Hardness") "Hardness"该窗体的:isnew方法允许使用:title关键字指定窗体的初始化标题。
窗体的大小和位置用像素来量度。:size消息可以用来确定窗体的当前大小,返回的结果是窗体的长度和宽度的列表。如果p是我们的直方图窗体,那么下边的表达式的意思是窗体当前值是长度200像素,宽度100像素。
> (send p :size) (250 125)通过向该窗体发送带两个参数的:size消息,这两个参数是新的宽度和高度值,窗体就会改变自身的大小:
> (send p :size 250 150) (250 150)窗体的位置由窗体左上角相对于屏幕左上角的坐标进行描述的,窗体当前的位置可以使用:location消息来设置和获取。
> (send p :location) (58 31)上式的结果意味着p窗体的左上角离屏幕左上角的右侧是100像素,离屏幕左上角下侧是50像素。
> (send p :location 200 100) (200 100)通过上式,窗体可以被移动到一个新的位置。
窗体原型的:isnew方法允许使用:size和:location关键字参数指定窗体的初始化大小和位置。使用这些关键字参数的实参是带两个整型数列表的列表:针对大小尺寸的长度于宽度和针对位置的左坐标和上坐标。
:size和:location消息确定一个窗体的内容的尺寸大小和位置。很多窗体系统在窗体内容周围防止一个框架。你也可以使用:frame-size和:frame-location消息来确定框架的左上角坐标的位置和整个框架的大小。
对于大部分目的而言,将一个Lisp-Stat窗体对象和窗体在屏幕上的图像等同起来是方便的.但是事实上它们是有区别的。对象表示一种与本地视窗系统里的图像进行通信的一种方式。当一个新的窗体对象创建的时候,它的图像立刻可见,除非向它的:isnew方法传递了值为nil的:show关键字。通过向对象发送:hide-window消息,该图像可以暂时从屏幕移除。:show-window消息可以使一个隐藏的窗体再次可见,并把它移动到所有可见窗体的最前端。那么,为了隐藏一个由对象p表示的窗体的图像,我们可以使用以下表达式:
> (send p :hide-window)下边的表达式将使图像再次可见。
> (send p :show-window):show-window消息可以用来将部分或全部被其它的窗体掩盖的那个窗体带到最前端。
移动窗体的图像的第二个方法是向窗体对象发送:remove消息,使用以下消息:
> (send p :remove)该消息将永久性地移除窗体的图像。
区分窗体图像的临时性隐藏和永久性移除的一个原因是本地窗体系统可能需要分配内存来表示图像。隐藏图像操作使一个窗体临时性地不可见而不需要释放它的内存;相反地,一旦一个窗口被移除,它的内存就被释放了。理想情况下,这种内存管理会自动处理,但是这不可能针对所有的本地窗体系统。结果,对不再需要的窗体发送:remove消息是重要的。
大多数用户接口都提供一个标准方法,用来在窗体不需要的时候接触窗体。在Macintosh操作系统上,窗体通常在其左上角有一个小的矩形,即关闭按钮。在SunView系统里,依附于窗体框架的一个菜单包含一个关闭选项。在关闭按钮上点击,或者采取标准动作来接触窗体的话,将向窗体对象发送一个不带参数的:close消息。如果你想让窗体p在关闭的时候隐藏,而不是移除的话,你可以像这样定义一个新的:close方法:
> (defmeth p :close () (send self :hide-window)) :CLOSE
窗体的:isnew方法允许使用:go-away关键字来指定一个窗体是否可以包含一个关闭设备,使用值为nil的这个关键字将产生一个不带关闭设备的窗体。
有时创建一个从属于其它毒性的对话框或者图形是很有用的。当主图形从屏幕移除的时候,丛书图形也应该被移除。为了支持这个语法,你可以通过向主窗体发送一个以从属窗体为参数的:add-subordinate消息来增加从属窗体。从属窗体可以使用:delete-subordinate消息来删除。window-proto原型的:remove方法确保所有安装的从属窗体在主窗体移除的时候也被发送:remove消息。
练习 7.1
略。
绘图菜单是用来让程序采取一些可能的动作的策略。图7.1展示了一个用来检测回归模型的简单的菜单。菜单通常是放置于菜单栏里或者窗体的特定区域,用来响应鼠标点击操作,鼠标释放之前移植都会显示。当鼠标光标从菜单上移过时,菜单项包括光标都是高亮的。如果在高亮的菜单项上释放鼠标,那么响应的动作将被执行。如果鼠标释放的时候没有菜单项是高亮的,那么将不执行任何动作。特定时间不合适的菜单项可能是不可用的。用来切换一些特征开或者关的菜单项,当该特征打开的时候它们前边可能用一个复选框标记。图7.1里的Intercept菜单项被复选,表示当前模型包含一个截距。
Lisp-Stat菜单由继承自menu-proto原型的对象构造而成。每个菜单都包含一个菜单项列表,它们是从menu-item-proto原型继承来的对象。在支持分级菜单的系统上,菜单里的菜单项可能是其它菜单。
对于以一些方式进行选择这样的需求,菜单是可用的。最简单的方法就是在菜单栏里安装菜单。当菜单安装到菜单栏里的时候,它的标题出现在菜单栏里。点击标题将弹出菜单。菜单栏是Macintosh用户接口的标准组件。针对其它用户接口的Lisp-Stat实现提供了一个相似的策略,比如一个包含安装好的菜单的标题的窗体。使菜单可用的其它方法还包括在绘图窗体里安装它们,或者在绘图窗里弹出菜单来响应鼠标点击。这些方法将在下一章里做进一步讨论。
当系统要求召唤一个菜单的时候,将发生以下动作:
让我们来看一下如何构造图7.1里的那个菜单。菜单本身可以通过下式创建:
> (setf model-menu (send menu-proto :new "Model")) #<Object: 144e774, prototype = MENU-PROTO, title = "Model">menu-proto的:isnew方法需要一个参数,菜单的标题。这个标题储存在title槽里,它可以通过使用:title消息来读取和改变。菜单还有一个enabled槽,该槽的值表示当它被安装到菜单栏的时候是否能够用来选择,该槽的值可以通过:enabled读取方法来设置和获取。对于这个由:enabled关键字指定的槽,:isnew方法接受一个值,其默认值为t。
:items消息返回安装在一个菜单里的菜单项的列表。对于我们的菜单,
> (send model-menu :items) NIL初始情况下,菜单中没有菜单项。为了完善该菜单,我们需要构造三个菜单项,并将它们放置到菜单里。
菜单项包含如下槽,title、mark、enabled和action。这些槽可以使用相应的读取方法来读取和修改。title槽出现在菜单里的字符串。mark槽是t或者nil,是哪个值依靠该菜单项前是否有个复选标记。enabled槽表示该槽是否可用。定义:do-action消息的menu-item-proto方法,如果该槽的值不是nil的话,是用来简化不带参数的action槽的调用内容的。那么你可以用两种方式控制菜单项的行为:通过在动作槽里放置一个函数,或者通过定义一个新的:do-action方法。
假设我们为2.5.1节里的数据设置了一个回归模型对象,并作为一个全局变量的值:
> (setf *current-model* (regression-model (list hardness tensile-strength) abrasion-loss)) Least Squares Estimates: Constant 885.161 (61.7516) Variable 0 -6.57083 (0.583188) Variable 1 -1.37431 (0.194309) R Squared: 0.840231 Sigma hat: 36.4893 Number of cases: 30 Degrees of freedom: 27 #<Object: 141a688, prototype = REGRESSION-MODEL-PROTO>然后我们可以构造它的概述信息,并且为我们的菜单绘制菜单项:
> (setf display-item (send menu-item-proto :new "Print Summary" :action #'(lambda () (send *current-model* :display)))) #<Object: 13e21b4, prototype = MENU-ITEM-PROTO, title = "Print Summary"> > (setf plot-item (send menu-item-proto :new "Plot Residuals" :action #'(lambda () (send *current-model* :plot-residuals)))) #<Object: 13e0ee4, prototype = MENU-ITEM-PROTO, title = "Plot Residuals">截距项这样构造:
> (setf intercept-item (send menu-item-proto :new "Intercept" :action #'(lambda () (send *current-model* :intercept (not (send *current-model* :intercept)))))) #<Object: 13dffe4, prototype = MENU-ITEM-PROTO, title = "Intercept">intercept-item的动作函数负责切换截距的打开与关闭。我们可以为:update消息定义一个方法,以确保无论什么时候模型里包含一个截距该菜单项都包含一个复选框标记:
> (defmeth intercept-item :update () (send self :mark (if (send *current-model* :intercept) t nil))) :UPDATE这里的if表达式用来确保传递给:mark消息的番薯是t或者nil。
通过向菜单发送一个或多个菜单项作为:append-items消息作为参数,这些菜单项被置于菜单中。新的菜单项按次序追加到已存菜单项集合的尾部。为了在我们的菜单里安装3个菜单项,我们使用如下方法:
> (send model-menu :append-items intercept-item display-item plot-item) NIL通过向菜单发送带一个或多个参数的:delete-items消息,菜单项可以从菜单栏里移除。如果一个菜单项被安装到一个菜单里,:menu消息返回包含该菜单项的菜单。针对截距项的例子:
> (send intercept-item :menu) #<Object: 141bcf8, prototype = MENU-PROTO, title = "Model">如果这个菜单项没有安装到菜单里,:menu消息返回nil。
最后,我们可以通过向菜单发送:install消息,将菜单安装到菜单栏中:
> (send model-menu :install) NIL在该表达式求值之后,菜单的表达应该已经出现在菜单栏里了。在菜单标题上点击鼠标按键,那么将产生如图7.1所示的菜单。你可以通过向菜单对象发送:remove消息,从菜单栏里移除该菜单。
7.1a 本节中安装自定义菜单的图示
如果你使用类似菜单作为一个大程序里的一部分,你不可能总是允许截距可以改变,你可以通过使用如下表达式使截距项不可用:
> (send intercept-item :enabled nil) NIL菜单包含的槽有title、enabled和items。菜单项包含的槽有title、enabled、mark和action。菜单和菜单项都包含一些没有在上边列出的其它的槽。这些槽用于内部使用,不应该被修改。这里提到的一些槽也可以用来作为与本地窗体系统的接口,如果他们被直接修改了就不能合适地处理以上接口。因此它们仅能使用获取方法来改变。
在构造和调试菜单时,两个附加工具是很有用的,它们是dash-item-proto原型和sysbeep函数。虚线项原型继承自菜单项原型,表示一个包含虚线的不能使用的项。这样的项对于将菜单里的菜单项分组是很有用的。你可以使用下式构建一个新的虚线项:
> (send dash-item-proto :new) #<Object: 1352580, prototype = DASH-ITEM-PROTO, title = "-">sysbeep函数只是产生一个音调,可用来作为报警或者在调试过程中作为虚拟路径的一部分。不给该函数传递参数,它将产生一个标准长度的音调;通过为该参数传递一个整形参数,可以改变音调的长度。
练习 7.2
略。
对话框是一类特殊的窗体,用来从用户处获取信息,或者给用户一个向程序发送指令的机会。一个对话框包含一些不同种类的对话项:
有两类对话框窗体:模态对话框和非模态对话框。模态对话框更常见。一个模态对话框采取这样一种方式:向用户询问一个问题,在程序继续向前运行之前该问题必须得到回答。当出现一个模态对话框的时候,所有的输入都指向那个对话框直到它清除为止,通常是通过点击按钮。另一方面,非模态对话框就像任何一个基于窗体的程序里的窗体一样,不同的是它包含按钮、复选框等等。非模态对话框与菜单的使用很像,当用户点击一个按钮的时候,它也会像菜单操作一样引起一个事件驱动的程序采取一定的动作。在Macintosh操作系统上,打开一个文件的标准对话框是模态对话框;Macintosh操作系统的控制面板是一个非模态对话框。
Lisp-Stat的绘图菜单里的很多菜单项会打开对话框来获得额外的信息。这些对话框中的多数是模态的,可以通过用来构建标准对话框的简单函数来构造。
最简单的模态对话框会通知用户有些重要的事情发生了。message-dialog函数带一个字符串参数,并提供一个包含字符串和一个OK按钮的模态对话框,然后该对话框将等待用户点击按钮。例如,下边的表达式表示图7.2中的对话框:
> (message-dialog "There is no variable named X")
代替声明一个不存在的变量,最好提供给用户继续或者异常终止操作的一个选择。ok-or-cancel-dialog函数表示一个带两个按钮的模态对话框,分别是OK按钮盒Cancel按钮。如果点击OK按钮,将返回t;否则,返回nil。那么你可以使用如下表达式:
> (let ((s (format nil "There is no variable named X. ~%~ Do you want to try another variable?"))) (ok-or-cancel-dialog s)) T > (let ((s (format nil "There is no variable named X. ~%~ Do you want to try another variable?"))) (ok-or-cancel-dialog s)) NIL代替前边的表达式。
ok-or-cancel-dialog带一个额外的可选参数。如果这个参数是true(默认值),那么OK按钮是默认按钮。也就是说,点击回车键与点击OK按钮式一样的。如果这个参数为nil,Cancel按钮是默认按钮。
可以使用choose-item-dialog函数表达一个更广泛的选择,该函数带一个加速字符串和字符串列表,表示一个带加速的对话框,针对列表的每个元素的单选按钮,OK按钮盒Cancel按钮。如果按下了Cancel按钮,返回nil;如果按下了OK按钮,将返回从0开始的被选元素的下标。例如,为了允许对一个回归模型的独立变量的选择,我们可以使用以下表达式来表示一个带有三个选择项的对话框:
> (choose-item-dialog "Dependent variable:" '("Y0" "Y1" "Y2")) 1如果标签为"Y1"的选项被选中,该表达式将返回1。关键字参数:initial在对话框显示的时候,用来指定选择以高亮的那个下标,其默认值为0。
choose-subset-dialog函数也是相似的,但是它通过使用一系列的复选框对话框,允许一个或多个选项供选择。如果点击了OK按钮,它返回包含被选中下标的列表的列表;如果点击了Cancel按钮,它返回nil。返回值的形式允许你区分取消和没有选项选中之间的不同。当对话框显示的时候,:initial关键字用来指定用来复选的选项下标的列表,默认情况,没有复选选项被选中。为了在一个回归模型里选择独立变量,所有选择的变量为默认值,我们可以使用如下表达式:
> (choose-subset-dialog "Independent variables" '("X0" "X1" "X2") :initial '(0 1 2)) ((0 1 2))如果第一项和第三项被选中的话,结果将返回((0 2))。
如果你无法减少来自用户的在一些选项中选择的信息,你可以请求输入一个字符串或一个表达式,为了请求一个字符串,你可以使用get-string-dialog函数。下边的表达式将打开图7.3所示的模态对话框:
> (get-string-dialog "Name of the dependent variable:" :initial "Y") "Y"如果你点击了OK按钮,可编辑文本域的当前内容的字符串将被返回;否则,返回nil。
get-value-dialog与get-string-dialog相似,但是它将可编辑文本域里的文本当成一个将被求值的Lisp表达式。如果按下了OK按钮,那么文本域里的表达式将被读取和求值,值得列表将返回;如果按下Cancel按钮,返回nil。因此如果用户输入表达式(+ 1 2)并按下OK按钮,那么结果将是列表(3);如果用户输入表达式nil,那么将返回(NIL)。这允许你将一个带值nil的结果表达式和一个取消操作区别开来。由:initial关键字提供的可编辑文本域的初始化表达式,将被转变为一个字符串,该字符串是使用~s指令的format和print风格习惯的。
练习 7.3
略。
联系 7.4
略。
最有用的非模态对话框是构造用来控制一个动画的滑块。在2.7节里,我们使用sequence-slider-dialog构造了一个滑块来控制一个动画的能源转换图。这个函数需要一个参数——一个Lisp序列。对话框滚动值即序列的值,滚动条每滚动一次,动作函数就调用一次。动作函数需要一个调用参数,序列的当前值。通过将:action关键字参数传递给sequence-slider-dialog来指定动作函数。举个简单的例子,以下表达式打开了一个序列滑块(译者注:如果这是窗体未出现绘图子窗体的话,可以通过调用菜单栏的Windows->Tile来平铺所有窗口):
> (sequence-slider-dialog (iseq 100 500) :action #'(lambda (x) (format t "Current element: ~s~%" x))) #<Object: 146aaf0, prototype = SEQUENCE-SLIDER-DIALOG-PROTO>
滚动条每滚动到一个合适的位置,打印当前的序列元素。默认地,参数序列的当前元素在值域里显示。如果这样不合适,你可以使用:display关键字参数来提供一个相同长度的显示序列的替代序列。:title关键字可以用来为窗体指定一个标题字符串。标记滑块值的标签可以通过使用:text关键字来代替标签字符串,其默认值是字符串"Value"。
可以向序列滑块对话框对象发送:value消息,来设置或者获取当前序列元素的下标;发送:action消息获取或改变动作函数。滑块的第二种形式可以通过interval-slider-dialog函数来构造。这个函数需要一个参数,即滑块区间的最大值及最小值的列表形式,返回一个区间滑块对象。这个区间被离散成一个实数序列。使用的点的数量可以使用:points关键字指定,其默认值为30。如果参数:nice的值是nil的话,区间参数里的点的数量和端点的值将被精确地使用。否则,它们将会稍微地改变以适应打印更加漂亮的打印值的目的。默认地,该参数的值是t。传递给该函数的:text, :title和:action关键字参数在产生序列滑块时使用,以下表达式构造了一个区间滑块,该滑块滑过单位区间,离散出大约50个点,每次改变滑块位置的时候打印当前值。
可以向区间滑块对话框对象发送:value消息来设置和获取当前值。一个新的值将会四舍五入到区间的离散过的点中里它最近的那个点上。区间滑块的动作函数也可以通过:action消息来设置和获取。
由上节描述的函数产生的标准对话框对于大多数目的是足够胜任的。但是偶尔我们也可能因为特定的问题,想要构造一个更加复杂的对话框。例如,我们可能想要使用图7.4所示的对话框来指定一个回归模型。我们可以使用对话框和对话框项原型来构造这样一个对话框。
有两个对话框原型,dialog-proto是针对非模态对话框的,modal-dialog-proto是针对模态对话框的。这些原型都是继承自window-proto原型的。一个新的对话框可以这样来构造:向合适的对话框原型发送:new消息,其参数是对话框项的列表,可能还有一些关键字参数。:title, :location, :size和:go-away关键字可以用来对新的对话框设置响应的属性。:go-away关键字对模态对话框是被忽略的。一个额外的关键字参数是:default-button,如果使用了该参数,它应该是一个对话框按钮对象或者nil。默认值是nil,意思是没有默认按钮。对话框里的默认按钮可以使用:default-button消息来设置和获取。
传递给对话框的:isnew方法作为第一个参数的对话框项列表,会指定对话框的布局。如果列表只由对话框项组成,那么这些对话框项将按列排列。出现在对话框项列表里的一个列表代表一行。在一个行里的一个列表是一个子列,等等。例如,用来构建图7.3里那个对话框的对话框项列表是这样的:
(prompt-text editable-text (ok-button cancel-button))这里的一个列是由一个以提示为目的的静态文本、一个为了输入字符串的可编辑的文本项和一个有两个按钮的行。与菜单项类似,对话框项本身也是对象。对话框项列表可以使用:items消息来获取。
当一个对话框初始化构造的时候,它表现为非模态的风格,无论你用哪个原型创建。当有用户动作发生在一个特定的对话框项上的时候,系统将通过输入一个字符串或者移动一个滚动条的形式来调节该项,然后向该对话框项对象发送:do-action消息。一般情况,只有按钮,滚动条和可滚动的列表需要重新执行这些消息。
为了使用模态对话框,你可以发送:model-dialog消息。该消息的方法不需要参数,但可以接受一个可选参数。除非该可选参数被置为nil,在返回之前方法将向对话框发送:remove消息。:modal-dialog方法运行一个循环,它强制所有用户动作都指向该对话框。该循环持续运行直到向对话框发送带一个参数的:modal-dialog-return消息,该参数可以是任何Lisp项,它将最为:modal-dialog消息的结果返回。
所有的对话框项都继承自dialog-item-proto原型。与window-proto原型相似,dialog-item-proto不直接使用。它作为六类指定的对话框项的共有功能的集合体的父类而使用,这六类对话框项是切换项(toggle items)、选择项(choice items)、文本想(text items)、按钮(button)、滚动条(scroll bars)和列表项(list items)。
对话框项可以应答:do-action和:action消息。:action消息获取或者设置action槽的值。该槽应该使用nil或者一个带0个或1个参数的函数,使用那个依靠项的类型。如果槽的值是nil的话,:do-action的默认方法将调用action槽的值。对话框项:isnew方法也接受一个:action关键字参数来设置动作函数。
对话框项:isnew方法可以使用:location和:size关键字参数。如果使用了这两个参数,它们将定位该项在对话框中的位置。这两个参数都应该是含有两个整数的列表,单位是像素。如果没有指定大小和位置,提供给对话框:isnew消息的项列表和对话框里的项的数据,将被用来在这些项周围构造一个对话框。你很可能从来不用直接设置对话框项的位置,除非你需要将对话框项对其得很好。但是你可能需要:size关键字来设置可编辑项或滚动条的大小。尺寸参数的值应该是该项的长度与宽度组成的列表。如果及确实需要提供位置,那它应该是该项的左上角坐标的列表,这是相对于对话框窗体的左上角来说的。
为了确定模型是否包含一个截距或者一个特定的独立变量,图7.4所示的对话框包含一些复选框或者一些切换项。切换项继承自toggle-item-proto原型。针对这个原型的:isnew方法需要一个字符串参数,该参数将被用作该项的标签。除了标准的关键字参数,也可以使用:value关键字。为了切换对话框项的开与关,该值应该分别被置为t货值nil。可以向一个切换项发送:value消息来设置和获取当前值。我们的对话框的截距项可以这样创建:
> (setf intercept (send toggle-item-proto :new "Intercept" :value t)) #<Object: 132e764, prototype = TOGGLE-ITEM-PROTO>对话框项在初始化时是复选的,因为:value的值使用了t。该项没有安装动作函数,因为项的值是不需要的直到按下OK按钮。该切换项需要的三个独立变量可以这要构造:
> (setf x-item-0 (send toggle-item-proto :new "X0")) #<Object: 133d810, prototype = TOGGLE-ITEM-PROTO> > (setf x-item-1 (send toggle-item-proto :new "X1")) #<Object: 133d030, prototype = TOGGLE-ITEM-PROTO> > (setf x-item-2 (send toggle-item-proto :new "X2")) #<Object: 133c790, prototype = TOGGLE-ITEM-PROTO>通过在对话框中使用一个选择项和一个单选集合,可以设置回归模型的因变量。就像一个汽车收音机一样,在给定时间只有一个按钮时打开的。它的原型是choice-item-proto,:isnew方法需要一个字符串列表作为按钮的标签。:value关键字可以用来指定初始条件下被选中的那个项的下标,默认地,该下标值为0。:value消息设置和获取当前选中的项的下标。用来选择因变量的选择项可以这样产生:
> (setf y-item (send choice-item-proto :new (list "Y0" "Y1" "Y2") :value 1)) #<Object: 133bd80, prototype = CHOICE-ITEM-PROTO>该表达式初始选中项是"Y1"。
我们的对话框包含三个静态文本项:一个用来标记因变量的选择,一个用来标记自变量的选择,还有一个作为模型名称的提示。还有一个可编辑的文本项用来输入模型名称。静态文本项继承自text-item-proto,可编辑文本项继承自edit-text-item-proto。可以通过向合适的原型发送带初始化文本为第一个参数的:new消息来创建新的文本项。除非你使用:text-length关键字参数来指定文本域的字符数量,系统将使用初始文本确定文本项的大小。如果对话框字体是比例变距字体,将使用平均字符大小。静态文本和可编辑文本项的文本都可以使用:text消息来设置和获取。我们的对话框里的三个静态哎文本项可以这要构造:
> (setf x-label (send text-item-proto :new "X Variables")) #<Object: 133b4f0, prototype = TEXT-ITEM-PROTO> > (setf y-label (send text-item-proto :new "Y Variables")) #<Object: 133ac00, prototype = TEXT-ITEM-PROTO> > (setf prompt (send text-item-proto :new "Name:")) #<Object: 133a4a0, prototype = TEXT-ITEM-PROTO>可编辑文本项这样设置:
> (setf name (send edit-text-item-proto :new "" :text-length 15)) #<Object: 1339d40, prototype = EDIT-TEXT-ITEM-PROTO>我们的对话框需要的最后的项是两个按钮。按钮继承自button-item-proto,可以通过向该原型发送一个带字符串参数的:new消息,即它的标签为参数,来构造一个按钮。按钮通常用来初始化一个动作,因此需要一个使用:action关键字的动作函数,或者一个指定的:do-action方法。例如,一个开始仿真的按钮可以定义成这样:
> (send button-item-proto :new "Start" :action #'(lambda (x) (send simulation :start))) #<Object: 13395f0, prototype = BUTTON-ITEM-PROTO>在模态对话框中按钮通常用来移除对话框,因此应该向对话框发送带合适参数的:modal-dialog-return消息。对于一个Cancel按钮其参数通常是nil。因此,针对我们的回归对话框的Cancel按钮可以定义成这样:
> (setf cancel (send button-item-proto :new "Cancel" :action #'(lambda () (let ((dialog (send cancel :dailog))) (send dialog :modal-dialog-return nil))))) #<Object: 13387b0, prototype = BUTTON-ITEM-PROTO>该:dialog消息返回了包含对话框项的对话框对象。
为了简化模态对话框上按钮的构造,modal-button-proto原型有一个:do-action方法,它可以调用动作函数,并且将结果发送给带:modal-dialog-return消息的对话框。如果action槽的值是nil,那么nil将作为返回方法的参数而传递。因此,Cancelannual可以使用这个原型来构造:
> (setf cancel (send modal-button-proto :new "Cancel")) #<Object: 1337fa0, prototype = MODAL-BUTTON-PROTO>因为没有使用动作函数,action槽的值默认为nil。
按下对话框里的OK按钮,应该返回对话框手机的信息。我们可以定义一个函数collet-values来收集对话框项里的值到一个列表:
> (defun collect-values () (list (send name :text) (send y-item :value) (which (list (send x-item-0 :value) (send x-item-1 :value) (send x-item-2 :value))) (send intercept :value))) COLLECT-VALUESOK按钮可以这样创建:
> (setf ok (send modal-button-proto :new "OK" :action #'collect-values)) #<Object: 1336ea0, prototype = MODAL-BUTTON-PROTO>现在我们已经准备好所有我们的回归对话框需要的项了。对话框本身可以这样设置:
> (setf reg-dialog (send modal-dialog-proto :new (list (list (list y-label y-item intercept) (list x-label x-item-0 x-item-1 x-item-2)) (list prompt name) (list ok cancel)))) #<Object: 1336180, prototype = MODAL-DIALOG-PROTO>这些项按三行为一列排列,每一行有两个子列,一个里边是y-label、y-item和intercept,还有一个里边是用来选择自变量的。第二行包括提示符和用来输入模型名字的可编辑文本域,第三行包含OK按钮和Cancel按钮。下边这个表达式运行模态对话框循环,允许修改选择项、切换项和文本项,直到其中一个按钮点击之前。这对图7.4的那个配置,如果OK按钮被点击的话,那么该表达式返回的结果是这样的:
> (send reg-dialog :modal-dialog) ("m" 1 (0 1) T)因为向对话框发送了不带参数的:modal-dialog消息,对话框将在表达式返回之前自动移除。
其它两种没在回归模型中使用的对话框项也是可用的,它们是滚动条和可滚动的列表。基础滚动条项在一定整数范围区间内滚动,通过向scroll-item-proto原型发送:new消息来构造。:isnew方法不需要参数,但是一些关键字参数是可用的。最小值和最大值可以通过使用:min-value和:max-value关键字来设置,默认值是0和100。通过使用:min-value和:max-value消息可以获取和改变他们。滚动条滑块的初始位置是最小值位置。可以通过向:isnew方法发送:value关键字来指定滚动条滑块的初始化位置,通过使用:value消息,滑块位置的值可以设置和获取。在大多数系统上,滚动条可以以一个单位为增量或者一个更大的增量,这个有页面有关。页面增量的大小可以通过向:isnew方法发送:page-increment关键字来指定,其默认值为5。
当鼠标在滚动条的活动部分按下的时候,滚动条通常会持续调整。每次自动滚动操作发生的时候,系统都会想滚动条对象发送一个不带参数的:scroll-action消息。该消息的默认方法与:do-action方法是相同的。
滚动条通常用来在一个序列或者一个区间内滚动。有两个继承自scroll-item-proto原型的原型被设计来处理这些标准实例。对于interval-scroll-item-proto原型,它的:isnew方法将表示一个区间的列表作为自己的参数。另外,它还接受:points关键字参数来设置点数,接受:text-item参数来指定一个文本对象,该文本对象用来显示当前值。对于sequence-scroll-item-proto原型,它的:isnew方法需要一个序列作为参数。除了:text-item关键字,它还接受:display关键字,用来显示一个备选的显示序列。正对这些原型,通过默认的:do-action方法调用的动作函数带有一个参数,当前序列元素或者区间里当前的点。
举个例子,我们可以使用序列和区间滚动项来创建一个非模态对话框,它包含两个滑块,用来指定大约50次试验的二项式分布的参数。我们可以以两个标签开始,它们用来区分构造的可显示值:
> (setf p-label (send text-item-proto :new "p")) #<Object: 134321c, prototype = TEXT-ITEM-PROTO> > (setf n-label (send text-item-proto :new "n")) #<Object: 134292c, prototype = TEXT-ITEM-PROTO>
两个用来显示当前值的文本项这样构造:
> (setf p-value (send text-item-proto :new "" :text-length 10)) #<Object: 1340d6c, prototype = TEXT-ITEM-PROTO> > (setf n-value (send text-item-proto :new "" :text-length 10)) #<Object: 13403dc, prototype = TEXT-ITEM-PROTO>滚动条项可以这样构造:
> (setf p-scroll (send interval-scroll-item-proto :new '(0 1) :text-item p-value :action #'(lambda (x) (format t "p = ~g~%" x)))) #<Object: 133f9ac, prototype = INTERVAL-SCROLL-ITEM-PROTO> > (setf n-scroll (send sequence-scroll-item-proto :new (iseq 1 50) :text-item n-value :action #'(lambda (x) (format t "n = ~g~%" x)))) #<Object: 133e70c, prototype = SEQUENCE-SCROLL-ITEM-PROTO>最后一个对话框项类型是列表项。这种项表示表示文本项的一维或者二维的可滚动列表。每次至少一个单元格可以选择,选集可以设置和获取,单元格的文本可以改变。另外,在单元格上的双击操作可以被探测到。为了创建一个列表项,可以向list-item-proto原型发送一个带一个序列和一个字符串的二维数组为参数的:new消息。默认地,将构建一个单列的列表。可见列的数目可以使用:columns关键字设置。:do-action方法默认情况下带一个值为nil的可选参数,用来调用动作函数。系统为每次点击调用一次:do-aciton,如果点击是双击的话将传递一个非nil参数,也就是说在前次点击的位置两次点击时间足够近。
:set-text方法带一个字符串和一个下标为参数,然后将下标指定的单元格的文本改变成提供的字符串。如果该列表项是使用字符串序列构造的话,下标应该是一个数字,如果是使用数组的话,它应该是所选单元格的行与列的下标。
:selection方法设置或者返回当前所选单元格的下标。nil下标意思是没有单元格被选中。
练习 7.5
略。
screen-has-color函数可以用来确定显示器是否是彩色显示器,它没有参数,返回t或者nil来指示显示器是否支持彩色。
可以调用不带参数的active-windows函数来获得当前显示在屏幕上的或者临时隐藏的所有的窗体对象的列表。
4.5.2节里描述的*features*变量,如果一个窗体系统接口是可用的,它将包含符号windows。如果显示器支持彩色,那么该特征列表还包含符号color。如果系统支持层级菜单,还会定义一个hieratchical-menu特征。这些特征可以通过使用#+和#-读取宏来进行条件求值。
Lisp-Stat系统被设计工作在一个单进程环境里。结果,一些比较耗时的绘图方法会阻止其它所有时间知道它自身完成。为了解决这个问题,绘图窗体允许一个空闲消息,该消息在没有正在处理的时间可用时将向每个窗体发送。一些系统可能还提供一个系统级别的空闲事件队列,在没有时间处理的时候,用以执行与特定窗体体无关的动作。