动态简介

这个教程描述 Dynamic(动态),DynamicModule(动态模块)和相关函数的原理,并且详细地介绍它们互相之间以及和 Wolfram 语言的其余部分怎样相互作用.
这些函数是一个更高级别的函数 Manipulate (操作)的基础,这个函数提供了一个简单然而强有力的方法来创建大量的交互式的例子、程序和演示项目,全部都是在一个方便的,尽管相对刻板的结构中. 如果这个结构能解决手头的问题,你不需要看 Manipulate 以外的内容,你也不需要读这个教程. 但是,如果你希望建立范围更广泛的结构,包括复杂的用户界面,那么请继续阅读这个教程.
这是一个实际动手的教程. 你在读到每一个输入句时需要实际地对其执行计算,并且观察会发生什么. 如果你不一边读一边计算那么您将无法理解随同的文本描述.
动态的基本原理
普通的 Wolfram 语言 会话由一系列的静态的输入和输出组成,这形成了一个按照输入顺序完成的计算的纪录.
一个接着一个地计算这四个输入单元的每一个:
第一个输出仍然显示的是当 x 是5时的值,尽管现在的输入是7. 如果你想看到你所做的事情的历史,这当然是很有用的. 但是,你也许经常希望有一个本质上不同的输出,它自动更新以致总是反映出它当前的值. 这一种新的类型的输出由 Dynamic 提供.
计算以下的单元;注意因为当前 x 的值是7,结果将是49:
实际上在一般情况下当你第一次计算一个包含封装在 Dynamic 中的变量的输入时,你将得到和当你没有用 Dynamic 时同样的结果. 但是如果你接下来改变了变量的值,显示的输出将会追溯地变化.
一个接着一个地计算以下的单元,并且注意上面显示的值的变化:
最前的两个静态输出仍旧分别是25和49,但是那个动态输出现在显示的是100,即最后一个 x 值的平方. (当然在 x 的值又一次变化时,这句话就会立即变得不正确了.)
对于能放到一个动态输出里的值的类型是没有任何限制的. 仅仅因为 x 最初是一个数字并不意味着它在接下来的计算中不能成为一个公式或甚至一个图形. 这也许看起来是一个简单的特性,但它是一组非常强大的交互式功能的基础.
每当 x 的值被改变时,上面的动态输出便自动更新. (你也许需要向前滚动来观看它.)
Dynamic[expr]
一个作为 expr 的动态更新了的当前值而显示的对象
基本动态表达式.
动态和控件
Dynamic 经常和诸如滑块以及复选框这样的控件连接在一起使用. Wolfram 语言中提供的全部控件在 "控件对象" 中进行了讨论;这里用滑块来阐明它们是如何工作的. 和其它的控件一起使用 Dynamic, 其原理基本上是一样的.
一个滑块由计算 Slider (滑块)函数来创建,其中第一个自变量是位置,可选的第二个自变量指定范围和步长,默认的范围是从0到1,默认的步长是0.
这是一个处于中间位置的滑块:
点击滑标并移动它. 滑标移动了,但是其它什么事情也没有发生,因为这个滑块没有和任何内容相联系.
这里把滑块的位置和 x 变量当前的值联系起来. (这个形式在下面会更详细地介绍.)
这里新创建了一个 x 的动态输出,因为前一个很可能现在已经不在你的荧屏上了:
拖拉前一个滑块. 当滑块移动时,x 的数值变化了,动态输出实时更新.
滑块也对 x 数值的变化作出反应.
为了看到这一点,计算这个语句:
你应该看到滑块跳了一下,并且 x 的动态输出同时变化了.
这里创建了另一个 x 滑块:
注意如果你移动你现在有的两个滑块中的任意一个,另一个锁定同步地移动. 两个都是动态地和双向地和 x 的当前数值相联系.
动态和其它函数
Dynamic 和如同 Slider 的控件结构在很多方面就像 Wolfram 语言中的任何其它函数一样. 它们能在一个输出, 表格,和甚至排版的数学表达式里的任何地方出现. 这些函数出现时,总是伴随着动态显示或与它们相连接的表达式或变量的当前值的实时改变这样的行为. Dynamic 是一个简单的建筑块,但是 Wolfram 语言其余的部分把它变成一个灵活工具来创建灵巧的、敏捷的、常常又是有趣的小巧互动显示.
这里做了一个 x 滑块的表格,它们同步更新:
你能把一个滑块和它当前值合并在一个单一的输出中:
Dynamic 的强大的功能在于它能轻松地显示 x 的任何函数:
使用整数值的滑块,你能创建动态更新的代数表达式:
你能和 Panel(面板)、Row(行)、Column(列)、Grid(格子)以及其它的格式构件一起使用动态表达式:
注意最后一个例子和 Manipulate 的输出相似. 这不是巧合,因为 Manipulate 实际上产生一个 Dynamic、控件和格式构件的组合,这和你自己用这些低级别的函数所能做的事情没有根本上的不同.
动态输出中变量的局部化
这里是和一个简单图形相联系的另一个滑块复本
这是一个和另一个函数相联系的滑块:
如果你让这两个输出在屏幕上都看得见,并且拖拉任何一个滑块,你将注意到它们在互相通讯. 在一个例子里移动滑块,另一个例子也移动. 这是因为你在两个例子中都用了全局变量 x. 尽管这在某些情况下是会很有用的,但是在绝大多数情况,你可能更乐意让这两个滑块独立地移动. 解决的办法是一个叫做 DynamicModule 的函数.
DynamicModule[{x,y,},expr]
在对 expr 中的 Dynamic 对象的所有计算过程中保留符号 xy 的相同的局部情形的一个对象
DynamicModule[{x=x0,y=y0},expr]
指定 xy 的初始值
Dynamic 对象变量局部化和初始化.
DynamicModule 有和 Module (模块)相同的自变量,并且类似地也是用来局部化和初始化变量,但是在它们怎样运行上有重要的差别.
这里是用了 x专用的(private)值的同样的两个例子:
注意这两个例子现在互相独立地工作:
多个 DynamicModule 能被放到一个单一的输出中,并且在输出中,它们分别保持与各自区域相联系的变量的值:

你也许很想用 Module 来代替 DynamicModule,实际上这在第一眼看上去好像是可行的. 但是,这不是一个好的主意,有几个原因,这在 "高级动态功能" 里有详细的讨论.

DynamicModule 在前端工作,不在内核里工作. 它在运算时保持不变,当它被格式化成输出时,它创建一个隐藏的对象,嵌在处理局部化的输出表达式里. 只要输出的那个空间是存在的(也就是,没有被删除),那个代表 DynamicModule 的隐藏的对象将保留变量的值,允许它们接下来在 DynamicModule 作用域之内对 Dynamic 表达式的计算中使用.
如果你保存了一个包含 DynamicModule 的笔记本,关上那个笔记本,之后在一个新的 Wolfram 系統中把它重新打开,所有局部变量的值将仍旧被保留,在 DynamicModule里的滑块将会在同样的位置. 这种情况在滑块和全局变量相连接(像这个教程中最前面的例子)的时候,或者在滑块和 Module 里而不是 DynamicModule 里的局部变量相连接的时候,将会发生. 这样的变量把它们的值存储在当前的 Wolfram 语言的内核会话中,一旦你退出 Wolfram 系統它们就遗失了.
除了把变量局部化于特定的输出区域之外,DynamicModule 提供了相关选项以便在包含一个 DynamicModule 的表达式被打开时自动初始化函数定义,并且在这个表达式被关掉或被删除时清除其值. 更多的细节在 DynamicModule 中能找到.
动态的第二个自变量
动态连接在默认下是双向的. 和一个变量相连接的多个滑块是一起移动的,这是因为它们都反映和控制同一个变量的值. 当你拖拉一个滑块的滑标时,系统将构造并计算形式为 expr=new 的表达式,这里 expr 是赋予 Dynamic 的第一自变量的表达式, new 是建议的新值,取决于你把滑块的滑标拖拉到哪里. 如果赋值成功,那么新值被接受. 如果赋值失败,滑块将不移动.
当你移动第一个时这两个滑块向相反的方向移动. 但是,试图移动第二个滑块给出一个错误,这是因为你不能给表达式 1-x 赋一个新的值:
你可以在 Dynamic 的第一自变量中保留一个任意的表达式,但通过选用第二自变量来改变动态执行的计算. 这是指定更新第一自变量里的变量值的反函数的一个方便的办法. Wolfram 语言不试图通过 Dynamic 的第一自变量自动地推导出这样的反函数;你自己必须提供一个.
Dynamic[expr,f]
在交互式改变或编辑 val 的过程中连续计算 f[val,expr]
反函数.
下面的例子说明 x 的值是如何更新并使得第二个滑块能够互动. 你能移动任意的一个滑块,另一个滑块做出向相反的方向移动的反应:
现在第二个滑块里的动态执行的表达式是一个纯函数 (x=1-#)&,它被赋予 # 中建议的新值. 注意这个函数负责实际上完成你想改变的任何变量的赋值;如果你想改变 x,你不能仅仅就说 (1-#)&.
能在鼠标的位置和 Wolfram 系統的状态之间插入一个你自己的任意函数是一个很强大的功能,你可以用它来完成远超出简单的反函数的用途. 在第二自变量中给出的函数实际上能做任何它想做的事.
这里定义了止动装置,当滑标处于一个整数的一定容差内时把滑块拉到整数值的位置:
这使得变量呈现有理数(整型分数)而不是小数:
为了对跟踪行为有完全的控制,可以另外指定别的函数,在鼠标点击滑块滑标的开始、中间和结束时调用. 如果你熟悉普通的用户界面编程,那么你将看出这些正是单独的鼠标按下,鼠标拖拉和鼠标释放事件的高级别事件函数.
这在点击和拖拉操作进行时改变背景的颜色:
Dynamic 的第二个自变量也能让你限制一个滑块的移动从而实现几何约束效果.
你只能将这个 Slider2D (二维滑块)的滑标沿着一个圆移动:
应该在表达式的何处放置动态?
Dynamic 的基本行为是在输出单元中建立一个输入表达式的复本. 更具体一些,DynamicHoldFirst (先保持)的属性,在计算时保持不变.
计算 Dynamic[x+y] 的结果是 Dynamic[x+y],这你可以通过检查输出的 InputForm (输入形式)表达形式来看到:
在一般的输出中你看不见 Dynamic,这是因为,在前端的格式化的显示中,Dynamic[x+y] 由这么一个对象代表,这个对象包含一个没有被计算的输入 (x+y) 的复本,但是以被计算的表达式的值显示. Dynamic 的封装仍然在输出中存在,但是它是不可见的.
因为 Dynamic 完全在前端完成它的工作,你不能把它用在需要使用一个表达式的值才能工作的函数中.
例如,这样是可行的:
但是这就不行了:
Plot 这个命令需要有具体的 x 的数值才能画出一个图形,但是在被画图的函数里的 Dynamic[x] 在内核中不会被计算成任何内容. 它只保持为 Dynamic[x] 不变,使得 Plot 命令不能作出任何有意义的事情.
另一个看这个事情的方式是,Plot 命令里面的表达式并不在输出的任何地方直接地出现. Dynamic 是一个在前端工作的格式化函数,不是在内核中,所以如果它被使用在永远不会被放置在输出中的地方时,那么就很有可能是一个错误.
在合并 Dynamic 和控件时,把 Dynamic 放在正确的地方是特别重要的.
这个例子如预期一样进行;移动滑块 x 的值就变化:
这个例子起先看上去不错,但如果你移动滑块,x 不变化:
那是因为当用 Dynamic 封装 Slider[x] 时,在计算它的内容时,x 的值被代入,结果只是其第一自变量,滑块,为一具体数字,没有跟踪剩下的变量名. 这种情况下的滑块是一个静态滑块的动态显示.
所需要的是一个静态滑块,其中包含了一个对变量值的动态参照. 对于控件,有一个简单的规则来决定在什么地方放 Dynamic. 任何控件函数,例如 Slider(滑块)、Checkbox (复选框)或 PopupMenu(弹出菜单)的第一个自变量都几乎总是 Dynamic[var].
除了在这些情况下 Dynamic 在某一特定的位置工作以外,对于把 Dynamic 放在哪里经常有很大的灵活性. 它经常作为一个输入表达式的最外层函数来使用,但这并不是绝对必需的,在更复杂的应用中,Dynamic 通常用在表达式里更深的一层,甚至是可以嵌套的.
这里显示了 x 的值的十个复本的一个表格.
Dynamic 把整个表达式括了起来,所以对 Table 命令的计算一直延迟到输出显示在笔记本中为止. 任何时候改变 x 的值,Table 命令将被重新计算.
这个例子中的输出看上去是一样的:
但是在这个情况下 Table 命令是被立即计算的,产生十个单独的 Dynamic 表达式的一个列表,在整体的结果已被放到笔记本中之后分别计算 x.
x 被改变时,第一个例子给内核送去一个单个的要求来获得 Table[x,{i,10}] 的值,而第二个例子给内核送去10个单独的要求来得到 x 的数值. 也许看上去第一个例子明显地效率更高,在这个情况下它确实是的. 但是,你也应该避免另一个极端:即在一单个的 Dynamic 中包括太多的东西,这也会使得效率降低.
这给 xy 设初值来建立一个和 x 的数值相连接的新的滑块:
这是一个带有两组动态表达式的选项卡视图,两个都显示 x(一个简单的数字)和 y(一个三维图形)的动态值:
拖拉滑块,你会发现第一个选项卡里的 x 的值更新得相当快. 在绝大多数的计算机里这基本上是瞬时的. 但是,第二个选项卡里的更新更缓慢一些. 为了保持最新的值,每一个单独的 Dynamic 表达式都(非常仔细地)跟踪究竟什么时候需要被重新计算. 在第二个选项卡里,输出强迫整个表达式 {x,y},包括那个大的,慢的三维图形,每当 x 的值变化时都被重新计算. 通过在第一个选项卡里使用两个单独的 Dynamic 表达式,你使得 x 的值能被更新而不需要重新计算 y,它事实上还没有被改变. (你最好在继续往下读之前删除最后一个输出,因为只要它是在荧屏上可见,它就会减慢任何包含全局变量 x 的例子.)
对于每一种情况下应该把 Dynamic 放在什么地方,很难作出一个概括声明,但是一般来说如果你是在构造一个大的,复杂的输出,其中只有小的部分会改变,那么 Dynamic 很可能应该仅仅用于那些部分. 另一方面,如果所有的或绝大多数的输出将会随一个单一变量值的改变而变化,那么很可能最好是将 Dynamic 用于整个输出.
选项中的动态
如果选项的值在使用之前会被传送到前端,则可以将 Dynamic 用在选项的右边. 这在某种程度上是一个与 "应该在表达式的何处放置动态?" 中的相关讨论的一个细微区别.
画图命令中像 PlotPoints (画点)的选项不能把 Dynamic 用在右边,因为画图命令在图形产生之前需要知道一个具体的数值. 记住 Dynamic 具有延迟计算直到表达式到达前端的效果,对于PlotPoints,这已太晚了, 因为它立即需要那个值. 然而,对于在前端完成工作的函数的选项,则通常,并且不无益处地,接受 Dynamic 作为其选项值.
例如,你能用两种方法来控制一块文字的大小.
Dynamic 可以用在一个整体 Style (样式)的表达式上:
或者将 Dynamic 只用于 FontSize (字体大小)的选项值里:
Dynamic 放在选项值里有两个可能的优点. 第一,假设动态重新产生的表达式非常大,比如说,整个文档. 每一次字体大小改变时都把它从内核重新传送到前端是低效率的,如果把 Dynamic 用于整个表达式,则这种传送是必需的.
第二,一个 Dynamic 表达式的输出是不可编辑的(因为它会在任何时候被重新产生),这使得第一个例子的输出是不可编辑. 但是第二个例子中的文字可以自由地编辑, 因为它是普通的静态输出:仅仅是选项值是动态的.
动态选项值也能在 选项设置 中设置. 它们允许在单元、笔记本或全局的级别上,以及在样式表里. (但是注意,如果你把一个动态选项值设置在一个会被很多单元继承的位置,例如在一个样式表里,那么它对工作性能可能会造成很大的影响.)
你也能通过 SetOptions (设置选项)来设置动态选项值:
笔记本的背景颜色和全局变量 x 连接以后,它现在就能被一个滑块或一个程序所控制:
当然,最好能返回到正常状态:
动态和无限循环
如果你不小心,你可以很容易地把 Dynamic 放到一个无限循环中.
这里是快速由小到大计数,只要是出现在荧屏上,它就不停地运行:
这不是一个程序错误(但是如果把它保留在那里会分散你的注意力的话,就把上面的输出删除掉).
因为在无限循环的每一个周期后更新输出并重画屏幕,这一功能实际上是相当有用的. 一般来说,系统会对打字、计算等等保持反应,即使在无限更新的 Dynamic 急速进行时.
制造一个自己触发,在某处停止变化的 Dynamic 也是有用的.
这是一个下垂的滑块,无论你把它拖拉到什么位置它总是降回到零:
如果你有一个 CPU 监视器正在运行,你就会看到当滑块在下降时在 CPU 上有一个小的负载(主要是为了重新画荧屏的),但是一旦它到达零,负载降低到零. 动态跟踪系统注意到 x 的数值没有变化:因此,直到某人再把 x 的数值改变之前(例如,当你点击滑块时)进一步的更新是没有必要的. "高级动态功能" 更详细地描述了动态跟踪系统是怎样工作的.
一个有用的窍门
因为它有 HoldFirst 这个特性,Dynamic 不计算它的第一个自变量. 这是 Dynamic 运作的基础,但是它可能会导致一个意外的行为.
例如,假设你想修改一个数值列表的数,你想对每一个数值都创建一个滑块来控制它.
这创建了那个列表和它当前值的一个动态显示:
下面试图制造一个滑块的列表,每个滑块对应上述列表中的一个元素,并用 list[[i]] 来取出单个成员:
出乎意料的是,这行不通!你能看见在滑块周围有一个错误的指示,它们不能被移动,并且上方的动态输出从来不变化. 你也许会很快地做出结论认为部分提取语句不能像这样和控件一起使用. 其实事实根本不是这样.
问题出现在,变量 iTable 命令赋予了一个临时的数值,但是那个数值从来没有被使用,因为 DynamicHoldFirst 的.
看一下那个滑块列表的 InputForm 就会明白这个问题:
需要做的是用它的临时数值替换变量 i,即使在保持不计算的表达式里面.
这能用 /. 来完成,或者用下面这个看起来有些奇特但是符合语言习惯的形式来做:
这个输出表明, Dynamic 事实上能和部分提取语句一起很好地工作,这是一个非常有用的属性.
动态里面的缓慢计算
Dynamic 用于一个需要很长时间,或者是超过几秒钟才能完成计算的函数不是一件好事情.
如果你计算这个例子,你将必须等待大约5秒钟才会看见输出 $Aborted
在等待 Dynamic 的输出计算的过程中,前端是冻结的,并且打字和其它的动作都是不可能的. 因为更新普通的动态输出锁住前端,所以重要的是把放在 Dynamic 里面的表达式限制为那些能相对迅速完成的计算(最好是在一秒钟左右). 幸运的是计算机以及 Wolfram 语言都很快,所以范围广泛的函数,包括复杂的二维和三维的图形,能轻易地转瞬间被计算.
为了永远地避免锁住前端,动态计算被内部地封装在 TimeConstrained (时间约束)里,带有一个超时的数值,默认下是5秒钟. (这能用 DynamicEvaluationTimeout 选项来改变. )在某种极端情况下,TimeConstrained 会失败以放弃计算,在这种情况下,几秒钟之后,前端会显示出一个对话框允许你终止动态更新直到引起问题的那个输出被删除为止.
幸运的是,如果你需要在 Dynamic 里有计算慢的内容,在这种情况下还有一个可选的办法. SynchronousUpdating->False 这个选项允许动态以一种不会锁住前端的方式来计算. 在对这样的异步的 Dynamic 的计算过程中,前端照常运行,但是主要的 Shift+Enter 计算队列忙于计算 Dynamic,所以进一步的 Shift+Enter 计算将等到 Dynamic 结束为止. (正常的同步 Dynamic 计算不会干预 Shift+Enter 计算.)
计算这个例子,你会看到一个灰色的占位矩形大约持续10秒钟,之后结果将被显示:
重要的是,在那10秒钟的暂停期间你可以在前端自由地继续干其它的事情.
"高级动态功能" 给出了关于同步和异步动态计算之间区别的更多的细节. 一般地,你不应该计划使用异步的动态计算,除非是绝对必要的. 它们更新不是那么迅速,并且能和控件以及其它同步计算以一种非常令人吃惊,尽管技术上不是错误的方式进行互相作用.
进一步的阅读
如果你准备使用复杂的结构,特别是那些涉及嵌套的 Dynamic 表达式,那么 DynamicDynamicModule 的实现细节是值得了解的. 这在 "高级动态功能" 中讨论.