技术笔记
高级动态功能
“ Manipulate 简介” 和
“ 动态简介” 提供了使用 Wolfram 语言的人机互动的特性所需要的绝大多数的信息,这些特性可以通过
Manipulate (操作)、
Dynamic (动态)和
DynamicModule (动态模块)函数来实现. 这个教程给出了关于
Dynamic 和
DynamicModule 运作的进一步细节,并且描述了让复杂的人机互动例子取得最佳表现的高级特性和技巧.
这个教程中的很多例子只显示一个输出数值并用
Pause (暂停)来模拟缓慢计算. 而在实际生活中,你将会做有用的计算并显示复杂的图像或大型的数值表格.
请注意这是一个实际动手的教程. 你在读到每一个输入句时需要实际地对其执行计算,并且观察会发生什么. 如果你不一边读一边计算那么随同的课文是不会有意义的.
Module (模块)和
DynamicModule (动态模块)有相似的语法并在很多方面的行为相似,至少是第一眼看上去如此. 但是,它们在有些方面是本质上不同的,例如什么时候它们的变量被局部化,局部数值存储在什么地方,以及在什么空间变量是唯一的.
Module 运作时把所有出现的局部变量用新建立的,并独特命名的变量代替. 这些变量构造时将避免与 Wolfram 语言内核的当前会话中的任何变量相冲突.
你可以通过允许这些局部变量在还没有被赋值时从模块的背景中
“ 逃离
” 来看它们的名字:
两个例子都产生了看上去独立的滑块,它们允许对变量
x 的单独的复本进行独立的设置.
Module 之内的滑块存在的问题是一个不同的内核会话可能会碰巧共有同样的局部变量名. 所以如果这个笔记本被保存了然后某段时间之后被重新打开了,滑块可能会和另外一个碰巧在那时有相同局部变量的
Module 里的变量相
“ 连接
” .
这不会对在
DynamicModule 里的滑块发生,这是因为
DynamicModule 等到对象在前端显示时才把变量局部化并且产生对该前端的当前会话唯一的局部名字. 局部化在
DynamicModule 第一次以输出形式建立时发生,然后每一次在包含有
DynamicModule 的文件被打开时再一次重复,所以不同的会话中产生的例子之间名字上的冲突是永远不会发生的.
由
Module 产生的变量是纯粹的内核会话变量;当一个内核会话结束时,变量不可挽回地遗失了. 而
DynamicModule 会在输出单元里产生一个结构,它负责保持这些变量的值,允许将它们保存在文件中. 这是一个有些微妙的概念,最好是用两个类比来解释. 第一,你可以把
DynamicModule 看成是
Module 的一个持久性的版本.
这个例子里的模块按顺序计算一系列的表达式,并且从一行到另一行所有局部模块变量的数值都被(显然地)保存了. 在复合表达式中你可以有任何多行,但是它们都必须从一开始就在哪里;一旦
Module 已经完成了执行,它随同它的所有局部变量一起消失.
DynamicModule ,另一方面,创建了一个环境,在其中对
DynamicModule 主体之内出现的
Dynamic 中的表达式的计算就像在上一例子中的复合表达式里的增加额外的行一样. 从一个动态更新到下一个,所有变量的数值都被保存了,就犹如独立的计算是复合表达式里的独立行一样,都处于由
DynamicModule 创建的局部变量系列中.
对变量数值的保存不仅延伸到同一会话中接下来的动态计算,而且延伸到所有未来的会话. 因为所有局部变量的数值都被存储并保存在笔记本文件中,如果笔记本在一个全新的 Wolfram 系统会话中被打开,数值还是会在那里,并且动态更新会从中断的地方继续.
DynamicModule 像一个能无限延伸的
Module .
另一个考虑
Module 和
DynamicModule 之间的区别的方式是,
Module 把它的变量局部化在一定的
时间 段(当模块的主体在被计算时),
DynamicModule 在输出中一定的
空间 区域来局部化它的变量.
只要输出的那个空间持续存在,为它所定义的变量的值就会被保存下来,并允许它们在
DynamicModule 的范围(区域)内,在接下来的对
Dynamic 表达式的计算中使用. 把输出保存到一个文件里就将那一点资产冻结起来,等待文件再一次被打开的时机. (在计算机科学的术语里,这有时被称为一个经冷冻干燥或序列化对象.)
DynamicModule 的跨会话保留状态的能力也是在一个文件中延伸编辑概念的一个方式. 正常的情况下,在你编辑一个文件中的文字或表达式,然后保存那个文件,再重新打开它时,你期望它是以你离开它时的样式打开. 编辑意味着改变一个文件的内容.
普通的内核变量没有这个特性;如果你给
x 赋值,然后退出再重新启动 Wolfram 系统
, x 就不再有那个数值了. 这有几个原因,其中一个并非最不重要的,是
x 的数值应该保存在
哪里 的问题.
DynamicModule 回答这个问题的方式是定义一个具体的位置(输出单元),来保留特定变量(局部变量)的值. 任意的编辑操作,像移动一个滑块,在一个输入区里打字,或拖拉一个动态图像对象,都改变这些局部变量的值. 并且既然这些数值在文件被保存时都被自动地保留了,滑块,以及其它的对象,它们在打开时和离开时完全相同. 因此
DynamicModule 使得任何量成为可编辑,方式同在笔记本文件中编辑和保存文字和表达式一样.
前端对 DynamicModule 变量值的所有权
Wolfram 语言里的普通变量由内核所有. 它们的值处于内核里,当你要 Wolfram 系统在前端显示变量值时,它会与内核启动一个交易来提取这个值. 这对于参考普通变量值的动态输出也是一样的.
当一个滑块被移动时,其余的499个和它同步移动. 这要求和内核之间500个单独的交易来检索
x 的值.(Wolfram 语言的语义学是相当复杂,以至于无法保证连续几次计算
x 时,每一次都会返回同样的值:通过某种方法和所有的滑块共同使用一个从内核检索到的值是不可能使前端提高效率的.)
然而由
DynamicModule 声明的变量,是由前端所有的. 它们的值处于前端,所以当前端需要一个值时,它可以用很少的开销从局部提取.
如果一个复杂的函数应用于这样的一个变量,它的值当然必须送给内核. 这是透明地发生的,系统的每一方都被实时告知变量值的任何变化.
在一个给定的情况下,最好是用一个正常的内核变量还是一个
DynamicModule 变量取决于几个因素. 最重要的一个事实是在笔记本被保存时所有
DynamicModule 变量的值都被保存在文件中. 如果你需要一个在会话之间保留的值,它必须是在
DynamicModule 中声明. 另一方面,比如说,一个临时的拥有一个大型数字表格的变量,也许不是一个
DynamicModule 变量的好的选择,因为它会大大地增加文件的大小. 在一个
DynamicModule 里嵌套一个
Module 或者反过来,或在前端和内核之间分配变量都是十分合理的,
在很多情况下,限制工作性能的因素是从内核中提取信息所需要的时间:通过将变量对于前端局部化,速度有时会急剧增加.
动态输出的规则很简单:如果你现在计算
expr ,那么
Dynamic [ expr ] 应该总是显示所得到的值. 如果一个变量值,或系统的某个其它状态改变了,那么动态输出应该被立即更新. 当然,为高效起见,不是任何变量每次变化时每一个动态输出都应该被重新计算. 跟踪相互依赖关系使得动态输出只在必要时才被计算是很关键的.
对于第一个表达式,任何时候
a 、
b 或
c 的值发生变化,或者与
a 、
b 或
c 相联系的任何形式发生变化,它的值都可能会改变. 第二个表达式当
a 是
True (真)时取决于
a 和
b (但不取决于
c ),当
a 是
False (假)时取决于
a 和
c (但不取决于
b ). 如果
a 既不是
True 也不是
False ,那么它只取决于
a (因为
If 语句返回未计算值).
先验地找出这些依赖关系是不可能的(对此相应有定理可证),相反地,系统会跟踪哪些变量或其它可跟踪实体在计算一个给定的表达式的过程中确实遇到过. 然后将所得数据和一些变量相联系. 而这些变量是用来确定,在一个给定变量得到一个新的值时,哪些动态表达式需要被告知.
系统设计的一个重要的目标是允许通过参考它们的动态输出来监控变量值,而且除了绝对必要,不给系统增加更多的负担,尤其是如果变量的数值正在被迅速地改变.
当动态输出被创建时,它会被计算,而
x 这个符号会贴上一个信息标签,在其值发生变化时,用来确认需要更新的那个输出.
当循环开始,
x 第一次被赋予一个新的值时,和它相联系的数据被查阅,并且前端被告知动态输出需要被更新. 和
x 相联系的数据然后被删除. 基本上系统忘记了关于动态输出的全部信,接下来在循环中的赋值绝对不会因为存在一个监控
x 数值的动态输出而带来任何速度上的惩罚.
很久之后(这是从计算机的时间尺度上讲;从人类的时间尺度上,仅仅是一秒钟的一部分),当显示屏接下来被重画,以及包含对
x 的参考的动态输出被重新计算时,动态输出和变量之间的联系又一次被引起注意,它们之间的联系再次重新建立.
同时循环继续运行. 下一次在荧屏更新后赋值完成时,另一个通知会被传送给前端,而后这个过程继续重复.
默认时,由变量数值变化触发的动态输出最多每秒钟更新二十次(这个速率可以用
SystemOption (系统选项)
"DynamicUpdateInterval" (
“ 动态更新间隔
” )来改变). 在前面的例子中你一般会看到数值随着每一次更新上跳几万或几十万(你的计算机越快倍数就越多),计算的总的速度仅减慢百分之一、二,如果你有一个多处理器的系统,这几乎是零.
你也许会预期用一个动态输出来监控在一个紧凑的循环中迅速变化的符号的值会很大程度地减慢那个循环. 但是事实上费用对变量改变的速率而言是零级的,实际上这通常是极少的.
动态输出只是当它们在荧屏上可见时才被更新. 这种最优化使得你能有数目不限的,全部都在不停改变的动态输出,而不会招致无限量的处理器的负担. 被卷出荧屏,卷到当前文件位置之上或之下的输出,将会放在那儿不理直到下一次它们被卷到荧屏之上,那时它们才会在显示之前被更新. (所以它们停止更新的事实一般是不明显的,除非它们有副作用,而这是一般来说不提倡的.)
动态输出可以取决于除了变量之外的事物,在这种情况下跟踪也是很小心和有选择性地完成的.
这给出一个以显示屏坐标表示的迅速更新的当前鼠标位置的显示:
只要输出在荧屏上是可见的,那么每当鼠标移动时就会有一定量的 CPU 活动,这是因为随着鼠标的每一个动作,这个特定的动态输出会立即被重画. 但是如果它被卷到荧屏之外,CPU 的使用将消失.
一般情况下,每当系统探测到任何它认为需要更新的理由时,动态输出便会更新(关于这是什么意思,细节请看
"动态对象的自动更新") .
Refresh (刷新)可以通过确切地指定什么应该或不应该触发更新来改变这种行为.
带有
TrackedSymbols (被跟踪的符号)选项的
Refresh 能用来指定一个那些应该被跟踪的符号的列表,其它所有的更新理由都被忽略了.
当你移动第二个(
y )滑块时,什么都不会发生,但是当你移动第一个滑块时,表达式被更新来反映两个变量的当前值. 你也许会说在移动了第二个滑块之后,动态输出是错误的,因为它没有反映系统当前的状态. 而这基本上正是
Refresh 命令存在的全部原因. 它允许你超越系统要求,在动态输出可能过时时总能对其进行更新.
TrackedSymbols ->Automatic (被跟踪的符号 -> 自动)这个设置可用来只跟踪那些明确(词汇上)出现在
Refresh 的第一个自变量给出的表达式里的符号. 例如,如果你使用一个函数,它依赖于一个不在
Refresh 里面词汇上出现的全局变量,这个全局变量的值的变化不会引起更新,而在一般情况下是会的.
Refresh 也能用来以有规律的时间间隔引起更新. 重要的是要明白这
不 是一个应该被轻易使用的特性. 对于
Dynamic 的设计很基本的一点是它不需要以一个固定的时刻表来更新,因为它总是在有益时就会立即更新. 但是有些情况下,这或者不能发生,或者只是不幸地没有发生.
一个可能会使人伤脑筋的情况是
RandomReal . 每一次你计算
RandomReal [ ] (随机实数 []),你都得到一个不同的答案,你也许就会认为
Dynamic [ RandomReal [ ] ] (动态[随机实数 []])因此应该不停地尽快地更新自己. 但是在一般情况下这不会很有用,而且实际上会对一些在内部使用随机性的算法有负面的后果(例如,一个在
Dynamic 里面的 Monte Carlo 积分很可能不应该不停地更新,因为事实上它会每次给出一个稍微有些不同的答案).
由于这个原因,从它不触发更新的意义上讲,
RandomReal [ ] 并不是
“ 过分敏感
” 的,如果你希望看到新的随机数,你必须使用
Refresh 来指定输出更新频度. 另外一个不过分敏感的函数的例子是文件系统的操作.
一个不大可能的事情是,含有
课堂助手 面板的文件的大小发生变化,这时
Dynamic 将不会被更新. 如果你希望监控一个文件的大小,你需要使用
Refresh 来指定一个询问间隔. (在足够高级的操作系统上,理论上 Wolfram 语言是可能有效地接收文件系统活动的通知的,未来的 Wolfram 语言的版本实际上也许会自动地更新这样的表达式. 就和其它的
Dynamic 表达式一样,自动纠正总是目标.)
最后,几个你也许认为会触发动态更新的函数而实际上都不会:例如,
DateList (日期列单)和
AbsoluteTime (绝对时间). 就像
RandomReal 一样,让这些函数自动触发更新反倒会带来麻烦,并不值得. 并且
Refresh 能简单地用来创建时钟似的对象.
Clock (时钟)是有意以时间为基础设计的
“ 过分敏感
” 的函数.
在"
刷新 " 这一节的例子中,
Refresh 总是
Dynamic 里面的最外层函数. 你也许会感到疑惑为什么它的选项不就是
Dynamic 的选项. 但实际上把
Refresh 放在表达式里尽可能深的层次往往很重要,尤其是当它指定一个基于时间的更新间隔的时候.
当复选框被选时,
Refresh 引起频繁的时钟更新,消耗 CPU 时间来保持最新状态. 然而,当复选框没有被选时,计算不再延伸到
Refresh 表达式,输出保持静态,所以没有 CPU 时间消耗. 如果
Refresh 被用在
Dynamic 里面的整个表达式上,CPU 时间会被不断地消耗,即使时钟没有被显示.
“ No clock
” 这些字会毫无意义地不断地被更新. (这个更新是看不见的;显示屏不会闪烁,但是 CPU 时间仍然被消耗.)
Dynamic 表达式是可以嵌套的,并且系统只是在必要时才很谨慎地更新它们. 特别是当
Dynamic 的内容包含进一步的人机互动元素时,跟踪在一个给定的变量改变时,什么会持续静态什么会更新是很重要的.
第一个滑块的位置决定在它下面的滑块的个数,而那些滑块的每一个都依次和一个数据列表里的一个元素值相联系. 因为滑块的个数是可变的,并且相应第一个滑块的位置动态地变化,产生它们的表格需要在
Dynamic 里面.
这个例子是可行的,但是现在假设你希望把列单里的每一个数字的值显示在它的滑块旁边.
现在每当你点击下面的一个滑块,它只移动一步,然后停止. 这里的问题是在表格里第二列中的
data [ [ i ] ] 表达式在外层
Dynamic 中产生一个对
data 里的值的依赖.
一旦
data 改变了,外层
Dynamic 的内容,包括你试图拖拉的滑块,就被销毁并被一个几乎相同的复本替代(其中
data [ [ i ] ] 的一个数据的显示值已经被改变了). 换一句话讲,拖拉滑块的行为销毁了它,避免任何进一步的活动.
现在你可以拖拉任何滑块来看动态更新的数值. 这之所以可行是因为外层
Dynamic 现在仅仅依赖于
n 的值,滑块的个数,而不是依赖于
data 的值. (严密地说,这是因为
Dynamic 是
HoldFirst (先保持):当它被计算时,它的第一自变量里的表达式从来都没有被触动,所以没有注册任何依赖性.)
在使用多级的嵌套
Dynamic 表达式建立大的,复杂的界面时,这都是需要牢记在心的重要问题. 即使在最复杂的情况下,Wolfram 语言都努力做到恰到好处. 例如,
Manipulate 的输出由一组高度复杂的相互联系的嵌套的
Dynamic 表达式组成:如果依赖性的跟踪系统不正确地工作,
Manipulate 不会正常工作.
Wolfram 系统由两个单独的进程组成,前端和内核. 这在计算机科学的语言上讲是真正单独的进程:两个独立的执行线程,拥有单独的存储空间,并在一个 CPU 任务监视器里分别显示.
前端和内核通过几个Wolfram Symbolic Transfer Protocol (WSTP) 连接互相交流,称为主要连接,抢先式连接(preemptive link),和服务连接. 主要的和抢先式连接是前端能给内核发送计算要求,内核能做出回应的途径. 服务连接是反向工作,由内核向前端发送要求.
主要连接被用于
Shift + Enter 计算. 前端保持一个等待执行的计算请求的队列,并通过该连接发送. 当你对一个或多个输入单元使用
Shift + Enter 时,它们都被加入到这个队列中,然后一个个地被处理. 在任何一个时刻,内核只知道一个主要连接计算,即那个它当前正在进行的计算(如果有的话). 同时,前端保持完全功能;你可以打字,打开和储存文件,等等. 通常情况下一个主要连接的计算需要多长时间并没有一个任意的限制. 人们经常要做需要几天才能完成的计算.
在前端能发送一个计算并能得到一个答复这个意义上讲,抢先式连接是以和主要连接相同的方式工作的,但是它在两端的管理却大不相同. 在前端这一边,抢先式连接是用来处理正常的
Dynamic 更新. 没有队列;相反,前端一次发送一个计算并且直到等到结果之后才继续它其它的工作. 所以把抢先式连接计算限制到最多一两秒钟很重要. 在任何一个抢先式连接计算的过程中,前端是完全被锁住的,任何打字或其它的活动都是不可能的.
在内核这一边,来自抢先式连接的计算请求比从来自主要连接的,包括当前正在运行的主要连接的计算(如果有的话)有优先权. 如果内核在处理一个主要连接计算时,进来一个抢先式连接计算请求,主要连接的计算会在一个安全的地方停下来(通常在几个微秒之内). 然后抢先式连接的计算开始运行直到完成,在这之后主要连接的计算重新开始并且允许如以前一样继续. 净效果和一个线程机制相似,尽管不是完全相同的. 多个快速的抢先式连接计算能够在一个单一的,长的,缓慢的主要连接计算的过程中执行,给人一个内核在同时处理几个问题的印象.
抢先式连接计算能改变变量的值,包括那些被同时运行的主要连接计算所使用的变量. 这里并不会产生矛盾,并且交错(interleave)是以一种完全安全的方式完成的,尽管它可能在你明白这是怎么回事之前会产生些相当奇怪的行为.
然后计算这个命令,并且在它完成所需要的十秒钟之内任意地拖拉滑块:
你不会看到任何事情发生(除了滑块移动之外),但是当第二个计算结束时,你将看到它已经记录了
x 的十个不同的值,代表了在计算
x 以建立列表的十个点上滑块恰好所处的位置.
Dynamic 通常使用抢先式连接来作它的计算. 计算是同步的,并且前端被锁住直到它完成为止. 这在有些情况下是不可避免的,但在其它情况下可能是次优的. 通过设置
SynchronousUpdating ->False (同步更新 -> 假)选项,你能告诉前端使用主要连接的队列,而不是抢先式连接. 前端然后显示一个灰色框子的占位器直到它收到从内核来的回答为止.
在下面这种情况下,默认的(同步的)更新是合适的,因为前端需要知道计算
Dynamic [ x ] 的结果来用正确的字体大小绘画:
这里,输出单元在第二个动态表达式完成之前就被画出. 一个灰色框的占位器持续一秒钟直到获知结果. 重新计算这个例子来再一次看到那个灰色框:
点击滑块会以一到十秒钟之间的时间延迟更新显示. 注意单元括号轮廓是加重的,就像单元是在用
Shift + Enter 计算一样. 这表示计算是列队的,在计算正在进行时你能在前端继续其它的工作.
当可能在
Dynamic 次表达式的周围画一个荧屏并在之后填充它们的数值时,异步更新对显示完全的
Dynamic 次表达式是有用的,这同一个网页浏览器在一个图像周围画文字然后在图像完成下载之后再把它插入是很一样的.
为什么不总是使用异步
Dynamic 表达式呢?有几个原因. 第一,它们被排成队列,所以按定义在另一个
Shift + Enter 计算在进行中时它们不运行. 对正常的(同步的)更新情况不是这样.
同时,为了对鼠标的移动有反应许多控件也需要是同步的. 使它们异步可能导致和其它控件奇怪的互动.
迅速地移动滑块,你将看到一个突变的,扭曲的正弦波,因为
n 的数值在对
Table 命令的计算过程中改变了. 这是正确的,预料之中的行为,但是这很可能不是你所希望的.
如果你用同步的
Dynamic 表达式这个问题就不会发生了,一般情况下这个问题不会对
DynamicModule 局部变量发生,也可以通过在开始异步计算之前把任何可能变化的变量的值存储到第二个变量中来避免其发生.
ControlActive 和 SynchronousUpdating
Automatic
作为一个一般的规则,如果你有一个
Dynamic ,目的是对一个滑块或其它的连续动作控件进行互动的反应,那么它应该能够在一秒钟以下,最好是远小于一秒钟内完成计算. 如果计算需要的时间比这长,你将不会得到令人满意的互动效果,无论是
Dynamic 是同步地还是异步地更新.
但是如果你有一个例子它就是不能足够快地完成计算,然而你希望能使它对一个滑块进行反应,这个情况下该怎么办呢?一个选择是使用异步更新并接受你不会得到实时的互动效果的事实. 如果那是你希望做的,在滑块或其它控件里设置
ContinuousAction ->False (连续行动 -> 假)是一个好主意;这样在控件被释放之后你只得到一个更新,避免在你到达你想停止的值之前,在一个拖拉进行的过程中开始一个可能会很长的计算.
单元括号只是在你释放了滑块之后才加重轮廓,表示有计算活动:
另一个更好的解决办法是在互动的控件拖拉运行的过程中提供一个某种形式的快速计算的预演,然后在滑块被释放时计算全部的,缓慢的输出. 有这么几个特性是专门支持这一点的.
第一个是函数
ControlActive (控件激活),如果一个控件当前被拖拉,它返回它的第一自变量,如果不是,就返回它的第二自变量. 不像
Dynamic ,
ControlActive 是一个普通的函数,它在内核中计算,立即返回它的一个或另一个自变量. 它能被嵌套在函数或选项值里.
第二个特性是对
Dynamic 的选项设置
SynchronousUpdating ->Automatic (同步更新的 -> 自动),这使得
Dynamic 在一个滑块被拖拉时同步,在滑块被释放时异步. 在一起使用,这两个特性能用来在一个滑块被拖拉时执行一个迅速的,同步更新的显示,在滑块被释放时执行一个稍慢的,异步更新的显示.
一个简单的数字在滑块被拖拉时同步地显示,在它被释放时,一个图像异步地产生:
这个例子显出无论最后的显示需要多长的时间来计算前端都能保持有反应,并且预演和最后的显示可以是完全不一样的.
当然,在绝大多数情况下,你会希望预演是最终显示的某种缩小的,稀释的,骨架的,或其它省略的形式. 这样这个粗陋的形式能够足够快地给出一个平滑的预演,并且,最后版本的计算,即使需要花一些时间,不会妨碍前端. 实际上,这个行为非常有用以至于它是 Plot3D 和其它画图函数中的默认行为.
这里显示了一个三维图形,在滑块被拖拉时只有少数目的画图点,当滑块被释放时图形用大量的画图点来使它变得精致:
默认下,
Plot3D 产生一个相似的预演,尽管没有那么极端的质量区分范围:
你也许已经注意到一个细微的地方. 在以上的三个例子中的任意一个的输出被第一次放到笔记本里时,你看到一个粗陋画出的(控件激活状态)版本,这之后很快接着一个更精炼的(控件非激活)版本. 这是故意的:系统在提供一个快速的预演使得你看见一些结果而不是只看见一个灰色的长方形. 第一个更新是同步地完成的,就如一个控件在被拖拉一样.
Dynamic 中的 ImageSizeCache
ImageSizeCache (图象尺寸隐存)是对 Dynamic 的一个选项,它指定一个用来显示值还没有被计算的 Dynamic 的矩形的尺寸. 一般它不在输入中指定,相反是由前端自动地产生并且和 Dynamic 表达式一起保存在文件中.
首先注意带有 SynchronousUpdating ->True (同步更新 -> 真)这个默认数值的 Dynamic 表达式永远不会有机会使用它们的ImageSizeCache 的选项的数值,因为它们总是在被显示之前被计算,而一旦被计算,就会用实际的图像尺寸.
另一方面,带有
SynchronousUpdating ->False (同步更新 -> 假)的
Dynamic 表达式会在它们被第一次计算时显示成一个灰色的矩形. 这种情况下,矩形的尺寸是由
ImageSizeCache 选项的值决定的. 这使得笔记本周围的内容被画在正确的地方,以至在
Dynamic 完成更新时,没有任何不必要的闪烁和笔记本内容的移动. (HTML 的用户会认出这是和
img 标签的宽度和高度参数相类似的.)
一般不必要明确地指定
ImageSizeCache 选项,因为一旦
Dynamic 的值被成功计算,系统就会自动地设置. (对计算结果进行测量,并将实际的尺寸复制到
ImageSizeCache 选项里.)如果
Dynamic 输出被保存在一个文件中,这个被自动计算的数值会被保留.
在输入表达式被计算时,一个小的灰色的矩形出现;因为这个
Dynamic 从未被计算过,没有对它的合适的图像尺寸的隐存,所以用了一个默认的尺寸.
三秒钟之后,结果出来,动态输出被显示了. 在这个时候,就有了一个实际的尺寸,并被复制到
ImageSizeCache 选项中. 你能通过在输出单元的任何地方点击并从菜单中选择看到那个值. (这向你显示了代表那个单元的基本表达式,同你在保存这个单元时在笔记本文件里出现的完全一样.)注意
ImageSizeCache 选项的存在.
现在在原始的单元表达式中一个无害的空间里输入一个空格(来强迫对一个单元内容重新解析),并再选择来重新格式化那个单元. 这次你会看到一个尺寸是最终输出的灰色矩形,这持续三秒钟,接下来的是恰当的输出. 这也是如果打开一个含有以前保存的,异步的动态输出的笔记本时你会看到的.
SynchronousUpdating ->Automatic 设置的行为是相似的,但是有细微的区别. 就像我们在
"ControlActive 和 SynchronousUpdating Automatic" 的例子中看到的,在
Automatic 设置下,一个同步的预演计算在输出被最初放置时完成,这样在缓慢的,异步的值被计算之前(可望)提供一个
Dynamic 表达式内容的快速显示. 因为第一个计算是同步的,灰色的矩形都不再显示了.
不过这个预计算只有在
ImageSizeCache 选项不存在时才被完成. 一个带有
SynchronousUpdating ->Automatic 和一个指定明确尺寸大小的
ImageSizeCache 选项的
Dynamic 不会完成一个同步的预计算,相反会显示一个灰色的矩形(按正确的尺寸)等候第一个异步计算的结果.
如果你不考虑它的实际效果,这一行为最初看起来让人有些费解. 一般来说,
Dynamic 表达式,除了它们最初在一个计算中作为输出第一次出现外,总会有一个
ImageSizeCache 选项(由前端自动产生). 任何时候它们从一个文件中被打开,它们都会有一个已知的,隐存的尺寸.
在占动态输出有绝大多数的
Manipulate 中,默认的设置是
SynchronousUpdating ->Automatic ,前面描述的行为让输出在最初生成时清楚地显示一个预演图像. 当打开一个含有数十个
Manipulate 输出的文件时,你将看到一个在网页浏览中常见的有用行为:文字会立即显示,而图像(以及其它的动态内容)随后依其速度尽快填充. 所以你可以快速地翻卷一个文件,而不会因在第一个文件显示前预先计算许多预演图像而延迟.
如果初次显示
Manipulate 的输出时初始计算不是同步的,那么就会有闪烁和周围环境的尺寸更新/移动,因为尺寸是未知的. 但是当
Manipulate 输出是从一个文件里打开时,尺寸已知,最终的输出就能没有闪烁地平滑地显示.
在内核中计算之后,ControlActive 能触发含有它的 Dynamic 的更新,但是这是以一个极度不对称的形式进行的,即只有从激活到未激活的状态时才进行. 当从另一个方向,即从未激活到激活进行转变时,ControlActive 本身不触发任何更新.
这种有些不寻常的行为的原因是 ControlActive 是一个完全全局的概念. 当 Wolfram 系统中任何地方的任何控件当前正在被拖拉时,它都返回激活状态,即使这些控件与碰巧包含 ControlActive 的参考 Dynamic 毫无关系. 如果 ControlActive 自身引起更新,那么一旦你点击任何控件,所有包含对 ControlActive 的参考的 Dynamic 表达式(例如,一个默认的动态 Plot3D 的输出)都将会立即更新,这完全是无意义的. 相反地,只有那些有某种其它原因需要更新的输出才会选用 ControlActive 当前的值.
另一方面,当控件被释放时,最好是完成任何以控件激活的形式绘制的输出,使它们具有最终完美的外貌. 因此,当 ControlActive 进入它的未激活状态时,它自身需要对任何可能在激活状态时绘制的 Dynamic 表达式发布更新.
这个 Active/Inactive 显示会更新,因为动态输出里的
x 变化了:
仔细观察在你点击滑块时会发生什么. 如果你点击并按下鼠标但不移动它,显示会保持
Inactive . 但是一旦你移动它,显示更新为
Active . 这是因为
x 变化了,引起
Dynamic 整体上更新,因此选取
ControlActive 的当前状态.
现在小心地释放鼠标按钮但不移动鼠标. 注意显示确实回复到
Inactive ,虽然
x 没有改变.
在
DynamicModule (动态模块)中声明的变量被局部化在一个笔记本中一个单元内的一个特别的矩形中. 在有些情况下,人们想要把这样的一个局部变量的范围延伸到其它的单元甚至其它的窗口里. 例如,你也许希望在一个单元里有一个按钮,它能打开一个对话框,使你能改变在和打开对话的按钮一样的范围中声明的变量.
这可以用 Wolfram 语言中更加超常的构造之一,一个
DynamicModule 虫洞来完成.
DynamicModule 接受
DynamicModuleParent (动态模块母体)选项,它的值是参考前端任何地方的另一个
DynamicModule 的
NotebookInterfaceObject (笔记本界面对象). 为了变量局部化的目的,带有这个选项的
DynamicModule 将被视为是处于被参考的那一个
DynamicModule 里面,不管两者实际上在什么地方(即使它们是在分开的窗口里).
设置这样的一个虫洞的窍门是使得
NotebookInterfaceObject 必须参考母体
DynamicModule . 这个参照只有在
DynamicModule 已经创建并以输出形式被放置后才能被产生,并且它只有对当前的会话才有效.
为了使这个过程更简易一些,并且为了避免所有对明确的
NotebookInterfaceObject 的参靠,
DynamicModule 也接受
InheritScope (继承范围)选项,它自动地产生
DynamicModuleParent 选项的正确值来使得新的
DynamicModule 函数就好像是在创建它的
DynamicModule 的范围里面. 这有点让人迷惑,所以用一个例子来说明.
计算这个来创建一个带有一个
“ +
” 按钮和一个数字的输出:
点击那个“ +” 按钮让一个 DynamicModule 局部变量的值增加,显示在输出尾端. 来减小那个数,你必须点击 Make - Palette 按钮,这创建一个新的(很小的)漂浮的含有一个 - 按钮的面板窗口.
这个
“ -
” 按钮生存于由
DynamicModule 的
InheritScope 选项创建的虫洞中. 点击这个按钮减少在远处另一个窗口里的一个
DynamicModule 的范围里的一个局部的和私有的变量的值.
InheritScope 只能当创建第二个
DynamicModule 的程序是在位于第一个
DynamicModule 内的按钮或其它动态对象里面执行时才能使用. 通过明确地使用
DynamicModuleParent ,有可能连接任意的多个现有的
DynamicModule ,但这样做有点微妙,超出了本文的范围.