高级操作(Manipulate)功能

这个教程包含 Manipulate 命令的高级特性. 它假设你已经阅读了 "Manipulate 简介" 因此对这个命令的用途和它是怎样工作的有了一个好的总体的了解.
这个教程在有些地方,也假设你已经熟悉了在 "动态简介""高级动态功能" 中包含的低级动态机制.
请注意这是一个实际动手的教程. 你在读到每一个输入句时需要实际地对其执行计算,并且观察会发生什么. 如果你不一边读一边计算那么随同的课文是不会有意义的.
自动重新计算的控制
一些 Manipulate 的例子会旋转,即使在没有滑块被移动时也连续地重新计算它们的内容. 实际上有些时候这正是你想要做的. 例如,这里是一个下垂的三角形,总是在中间凹陷垂落. 你能用滑块把它往上拉,但是一但你停止移动,它就又开始向下降落了.
这种情况发生的原因是 y 变量被第一个自变量中程序改变,系统正确而善意地注意到既然 y 的数值已经改变了,那么需要把内容重新计算和显示,这进而导致了 y 的值又一次改变,依次反复直到,在这个例子中,我们到达了 y=-1 的一个稳定点为止. 这之后 y 的数值不再变化,并且内容也不再被持续地重新绘制,直到你触动滑块. (如果你的系统里有一个 CPU 活动监视器你能证实当三角形在下垂时,Wolfram 系统在用 CPU 的时间,但是一旦它达到了底端,Wolfram 系统Wolfram 系统对 CPU 的使用停止.)
当然也可以建立一个不停止的例子. 这里我们声明两个变量,初值都赋为零,并在 Manipulate 的主文中包括一个程序来连续地根据一个变量的值更新另一个变量的值.
每当步长被从零移开时,内容区会就会连续地更新,一个 CPU 监视器会指示 Wolfram 系统正在使用 CPU 时间. 你让这种情况持续多久,它就会持续多久. (幸好的是它并不完全地消耗 CPU,在前端的其它活动不会被这个活动阻碍;在它运行时,你能继续编辑,计算,等等. 你可以把它看成是一个动画图像或在网页浏览器里运行的一个小型应用程序.)
然而在某些情况下,持续的再计算是没有意义和不受欢迎的. 考虑这样一个有些做作的例子:任何时候它在荧屏上出现时(即在一个开着的窗口里并且没有被移动到完全从显示屏消失),它就会不停地重新计算它自己,消耗 CPU 时间尽管什么都没有改变.
这发生的原因是 temp 变量在计算时它的值被改变了,尽管 n 的数值还没有改变(即每次它都会被重新设置成两个不同的值). 这种旋转是没有意义的因为 temp 的值在它还没有被用之前就被设置了.
另一个导致不经意和无意义的旋转的方式是在 Manipulate 的主文中做一个函数定义或其它复杂的赋值,就像这个例子.
在这两个情况下,这个问题都可以通过把引起问题的那些变量在一个 Module 里设成局部变量来解决. (这无论如何这都是一个好的编程习惯,远不止是为了避免无意义的更新)
不管你对局部 Module 变量做什么都不会造成重新触发,因为这是 Module 定义的一部分,即一次调用后的变量值不会存留到下一次(所以下一轮的结果不会仅仅因为当前一轮运行中对局部变量所做的任何动作而有任何不同).
另一个解决的办法是使用 ManipulateTrackedSymbols (跟踪的符号)选项来控制哪一个变量能被允许引起更新行为. 默认的值, Full (全部),意味着所有在第一个自变量中明确出现(词汇的)的符号都会被跟踪. (这意味着,除了其它事情之外,在你使用的 Manipulate 例子中函数定义里的临时变量和其它如此的问题将不会引起无限重复计算问题,这是因为它们不在第一个自变量中明显地出现,而只是通过你调用的函数间接地出现.)
来看第二个例子,如果由于某种原因你不想要 f 成为一个局部的 Module 变量,并且你不能把它的定义移到 Manipulate 之外(在更复杂的例子中,这两种情况有些时候都是很可能会出现的),你能用 TrackedSymbols 来取消由 f 触发的更新:
这个例子只在移动滑块从而改变 n 的数值时才更新内容区域.
具体在什么时候一个给定的动态表达式会被更新这个话题是很复杂的,这在 "动态简介" "高级动态功能" 中被提到了. 在阅读那些文件时,始终注意 Manipulate 只是在 Dynamic 中把它的第一个自变量包围起来并且把它的 TrackedSymbols 选项的数值传送个在那里面的 Refresh. 所有与更新有关的事情都是由那个 DynamicRefresh 处理的.
嵌套操作
你能把一个 Manipulate 放到另一个里面. 例如,这里我们在一个外层 Manipulate 中用滑块来控制在内层 Manipulate 中的那些滑块的数目.
尽管嵌套多层 Manipulate 是可能的,并且是可行的,但它很可能不是世界上最有用的特性. 但是通过嵌套一次,你实际上已近创建了一个参数化的用户界面的构造界面. 外层的 Manipulate 允许你控制那些决定由内层 Manipulate 呈现的用户界面的参数. 用某些比前面的例子稍微更复杂的程序,就能做很有意义的事情.
相互依赖的控件
使 Manipulate 中的一个滑块的范围取决于另外一个滑块的位置是可能的. 例如,Binomial[n,m](二项式 [n, m])函数只有在 m<=n 时才有意义,所以你也许希望做成一个 m 滑块并使它的范围是从 1 到当前 n 的值. 这只要在对 m 的变量指定中使用 n 就可做到,例如:
注意如果你先把两个滑块都向右移动一部分,再把 n 滑块向左移,m 滑块会自动地向右移,这是因为它的最大值在减小. 如果你把 n 向左移动地足够远,以至于它比当前 m 的数值还小,m 滑块会显示一个红色的超出范围的指示物,这是因为 m 现在它比允许的最大值还大.
你也许想知道在最大值比它当前的数值还小时,为什么 m 不自动地被重新设置到当前的最大值. 原因是有些时候最好是避免扰乱那个值,如果你想让它自动地重新设置,很容易通过手工来做到. 例如,你可以给第一个自变量的程序里加一个 If 语句.
一般来说,你能无限制地在其它变量的定义里用 Manipulate 变量,尽管这样做当然可能产生奇怪的互动,没有帮助反而使人困惑.
这个例子显示了另一种情况,用一个复选框来控制一个滑块的范围:像这样的情况在你想提供细微和粗略的范围时是有用的,例如:
缓慢计算的处理
Manipulate 不预先计算通过移动它的滑块你能得到的所有可能的输出值:除了最简单的情况之外那是会完全不实用的. 那样的话它必须在每一个滑块被拖拉时实时地进行计算,格式化,并显示当前的数值. 显然不管你的计算机有多快,在一有限时间里能完成的计算量总是有限的,如果你在 Manipulate 第一个自变量里用的表达式计算的时间超过大约一秒钟,你不会对使用 Manipulate 有一个很满意的经历.
很多非常有趣并强大的计算都能在一秒钟一下完成,随着计算机变得越来越快,这个范围只会增加(人不会变得更快,所以在该例子变得看起来太缓慢之前,可利用的时间量在很长一段时间里不会改变). 但是有些计算就是不能那么快地完成,如果你想在 Manipulate 之内使用它们就必须寻找替代的办法. 幸好有一些不错的方法来处理缓慢的计算.
为本节的目的起见,我们打算使用 Pause (暂停)来模拟一个缓慢的计算. 这样做的主要原因是任何实际的计算在不同用户的计算机上都会以非常不同的速度运行,以至于用任何一个例子都难以阐明我们要说明的问题. 所以当你看见一个 Pause 命令时,请想象一个非常复杂和有趣的事情正在进行,正在产生一个无比精细和富有启发的输出.
为了对这个问题有一些感性认识,试着拖拉以下的滑块. 尽管这个例子并不是不可接受,它几乎不值得一试. 如果延迟的时间增加到几秒钟,它会变得更加无聊. (如果超过五秒钟你就会开时看见 $Aborted (中止)而不是数字,这是因为系统在保护它自己以免进行过于长的计算,在这种情况下这些计算会阻碍其它在前端的活动.)
最简单的改善方式是在 Manipulate 里加 ContinuousAction->False (连续行动 -> 假)这个选项.
在这个例子里滑块被拖拉时会平滑而瞬时地移动, 但是输出区里的数值并不企图实时地跟踪. 相反,它只是在滑块被释放时才更新.
一个更细微的区别是当这个例子中的数值更新时,它并不妨碍其它的前端活动. 你能从这个事实中看到这一点,那就是每当滑块被释放时,单元括号都会加重一秒钟,而在这一秒钟之内,你可以继续在前端打字或做其它的工作. 对这样的无妨碍的计算没有五秒钟的限制,所以通过使用 ContinuousAction->False 选项,任意长的计算都可使用. (然而一个需要一分钟才能完成的事情最好还是用一个正常的 Shift+Return 计算,而不是在 Manipulate 里计算.)
一个更加成熟的替代方法是用 ControlActive (控制激活)函数来在滑块被拖拉时展现一个替代的,更简单的和更迅速的显示,而只有在它被释放时才作长的计算.
ControlActive 取两个自变量:如果表达式是在一个 控件(例如,一个滑块)被鼠标拖拉时计算的,则返回第一个自变量,如果没有任何 控件当前是激活的,则返回第二个自变量. (到底在什么时候哪个自变量被返回,细节请看关于 ControlActive 的文件.)
在这个例子里我们仅用 x 作为滑块在被拖拉时显示的预演,用被一个框包围的 x ,加上一秒钟的延长,来作为在滑块被释放时所呈现的最终显示. 注意我们已经从上面的例子中把 ContinuousAction->False 这个选项去掉了.
注意只有当滑块被释放时单元括号才加重,表明这是一个无妨碍的计算. 当滑块在被拖拉时,为了最大的互动效果计算是以一种妨碍的方式完成的.
这里是一个可以使用 ControlActive 的更为现实一点的例子. 这个例子显示出当滑块在被托拉时, DensityPlot (密度图)的默认行为是如何使用较少的抽样点的.
但是即使是滑块被释放之后用了更大的数字也不足以产生一个令人满意的图形. 如果我们只是设置一个固定的,更大数量的画图点,结果会漂亮,但是互动效果不会足够好.
(激活和不激活的形式之间仍然还是有区别的,这是因为在默认时几个不同的选项,而不仅仅是 PlotPoints(画图点),取决于 ControlActive.)
通过在 PlotPoints 选项的值中明确地使用 ControlActive,可获得速度和质量的最佳.
结果是这么一个例子,它显示一个粗陋的,但差不多是瞬时的,图形的预演,然后在滑块被释放时花很多秒钟来建构一个高分辨率的版本.
下一节 解释一个更复杂解决办法,它适用于这样的情况,即参数的某些变化要求一个缓慢的计算但其它的能更迅速地更新显示.
在操作里使用动态
你最好在完成这一节之前先看一下 "动态简介" ,它提到了明确的 Dynamic 表达式的使用,这在这个教程里没有解释.
当你在 Manipulate 里移动滑块(或其它的控件)时, 第一个自变量里的表达式对每一个新划过的参数值都会重新计算. "缓慢计算的处理" 讨论了如果第一个自变量的计算太慢以至于不能平滑地实现 Manipulate 的人机互动的效果时可以采取的几个一般措施. 但是在有些情况下有可能把计算分隔成较慢和较快的部分,从而达到好的多的效果.
考虑这个例子,其中一个滑块控制一个三维图形的内容,另外一个控制它的视角.
n-滑块被移动时,明显必须重新计算这个三维图形,这是因为它的形状事实上变化了. 这个图形当滑块被拖拉时变得参差不齐,然后在你释放滑块之后很快就有了改善,这是正常的. 相反,当你移动 v-滑块时,没有必要重新计算那个函数,因为只是视角改变了. 但是 Manipulate 无法知道这一点(在更复杂的情况下以任何自动化的方式来作出这种区分真的是不可能的),所以每一次 v 改变时整个图形都从头产生.
为了改善这个例子,我们可以告诉 Manipulate 应该把 ViewPoint 选项从输出的其余部分分离出来进行更新,我们可以通过把这个选项的右边用 Dynamic 环绕做到这一点.
注意现在当 v-滑块被移动时,图形不再回到那个锯齿形的外观,并且实际上比以前旋转得更快了. 这是因为图形不再随每一个移动而重新产生了.
想要确切地解释为什么可以这么用,需要对 Dynamic 的内部机制有一个了解,这在 "动态简介""高级动态功能" 中进行了解释. 简而言之, Manipulate 总是对它第一个自变量给出的表达式外包一个 Dynamic,并且一般情况下用在第一个自变量里的变量的任何变化都会触发那个 Dynamic 的更新. 但是当一个变量仅出现在一个显式的 Dynamic 中并且这是嵌套在由 Manipulate 隐式产生的另一个里,那么外层 Dynamic 的更新不会被触发,只是它所在的内层 Dynamic 会更新.
全面解释在 Manipulate 中显式地使用 Dynamic 所可能做的事情超过了本文的范围,但是另一个情况值得一看,它涉及到这么一个情况,即某个计算的缓慢部分只包含了输入变量的某一部分.
在下面的一个例子里我们组建一个大型数字表格(在这个例子里用了 RandomReal (随机实数),但是在一个实际的例子中可能会是复杂得多和更慢的计算,甚至是涉及到从网络里读入的外部数据). 组建了数据之后,我们用一个相当简单和快速的函数来显示它(在这里仅通过把坐标数值提升到一个乘方函数来阐明).
注意到当 n-滑块被移动时,点子的数目变化了,并且它们到处跳动,这是因为每次都有一个新的随机数组产生. 但是当 p-滑块被移动时,更新更平滑一些,并且点子不到处跳动. 这是因为环绕着 p 的使用的内层 Dynamic 阻止了第一个自变量被整体重新计算. 所以没有新的随机点子产生,只有对已存在的点子的呈现更新了.
SynchronousUpdating->False (同步的更新->假)选项用来促使外层 Dynamic(那一个由 Manipulate 隐式产生的)异步地更新(通过当 n-滑块被移动时单元括号加重这一事实是可以看得出来的). 异步更新不能给出同样平滑的更新,但是如果计算花费很长的时间,它不会妨碍前端的其它活动.
内层 Dynamic 使用默认下的同步更新,所以当 p-滑块被移动时,更新是平滑和迅速的.
作为实践,你可以用这里演示的技巧做这样一个例子:当一个滑块被改变时,它花费许多秒钟,甚至几分钟的时间来作出反应,然而当那些不需要重复这种长时间计算的其他滑块被改变时,却仍能维持迅速的人机互动的效果.
你也可以在 Manipulate 里使用 Dynamic 来使输出动态地响应不同于 Manipulate 控制变量的值的事情. 例如,这里是一个从前面一节中取来的一个例子,只是这里我们让它动态地响应当前鼠标的位置.
任何时候鼠标在图形区域里,那些直线的中心就会跟随着它(不用点击). 对进一步的细节查阅关于 MousePosition (鼠标位置)的文件.
还要记住的重要一点是 Manipulate 并不是 Wolfram 语言里唯一的一种创建人机互动的用户界面的方法. Manipulate 的意图是它是一个简单的,然而强大的,用来在一个很高级别上定以用户界面的工具. 但是当你达到了它所能做的事情的极限时,无论是在 控件的布局上,更新行为上,或者和外部系统互动上,总是可以(并且经常并不是那么困难的)通过使用例如 DynamicEventHandler (事件处理器)这样的函数来降低到一个级别低等一些的界面编程上.
控制区的动态对象
我们在 "Manipulate 简介" 中看到,Manipulate 的控制区可以添加各种元素,例如标题和分隔符,就像下面这个例子中一样.
事实上对于在控制区里能放入什么基本上没有任何限制,包括任意的格式化结构和动态对象,甚至包括不属于 Manipulate 控件指定部分的 控件. 任何放在变量序列的东西,无论是一个字符串或是具有头部 DynamicStyle,或 ExpressionCell 的,都会自动地被理解为是一个将被插入到控制区里的注释.
你最好在完成这一节之前阅读 "动态简介" ,因为我们会提到显式 Dynamic 表达式的使用,这在本教程中没有讲解.
设想你想画出组合形成 Lissajous 图形的单独的 xy 的正弦函数. 对此,你可以把所有的三个函数都放到输出区里,使用 Grid 来对它们进行布局.
其实对于这个演示是有很多可讨论的. 但是假设你希望不变动主输出区,但要显示一个大的,突出的 Lissajous 图形本生,而把单独的正弦函数显示成和控制每一个方向的 控件相联系的小得多的图形. 你可以通过把一个动态图形对象放到控制区里来做到这一点,即:
就像在本节开始的例子里通过把标题编列在变量指定序列中从而使它们和 控件混合在一起一样,这里我们把动态更新了的图形放在变量指定序列里. 在这些子图形中显式地使用了 Dynamic 以便在 控件被移动时这些图形会更新. (主输出区不需要一个显式的 Dynamic ,这是因为 Manipulate 自动地用 Dynamic 环绕它的第一个自变量.)
这里有必要简单谈谈为什么这样的一个例子是可行的. 原因是 Manipulate 的输出不是一个仅仅把一组 控件和一个单一的输出区联系起来的特殊的,固定的对象. 相反,如同用 "动态简介" (事实上它包括一个关于怎样手工建立一个简单 Manipulate 的例子)中描述的技巧从低级别所能获取的性能一样, Manipulate 的输出是用同样的格式,布局,用户界面,和动态人机互动特性建立起来的. 在某些方面 Manipulate 和较低级别的人机互动的特性之间的关系就像 PlotGraphics 之间的关系一样. 执行一个高级 Plot 命令的结果是一个低级别的 Graphics 对象,如果 Plot 不能产生你想要的具体的图形,你总是可以直接使用 Graphics . 你可以使用 Prolog (序言)和 Epilog (结束语)选项来给一个 Plot 添加任意的图形元素. 另外任何用 Graphics 不能得到的图形输出,用 Plot 也不可能得到. Plot 对 Wolfram 语言里任何在较低级别上不可得到的特性都没有特殊的访问权利.
同样地,对级别较低的函数所不能获得的特性, Manipulate 也没有特殊的权利获得:任何 Dynamic 做不到的事情, Manipulate 都不可能做到,它只是一个更高级别的,更方便的用来建立一定样式界面的函数.
所以当你在 控件标签里用动态对象时,就像在上面的例子中,你只不过是在已经相当复杂的,组成一个 Manipulate 命令输出的Panel 对象, Grid 对象, Dynamic 对象,和 DynamicModule 对象组中又加入了几个 Dynamic 对象. 确实没有任何不同的地方,只不过是更多了一些,所以新的动态元素和其它的一起相互平滑地操作是不奇怪的.
完全在 Manipulate 的控制区里建立完全任意的人机互动的动态用户界面是可能的,虽然这样做不总是有意义的.
控制区控件的任意布局
即使运用上一节中所述的技术在 Manipulate 控制区中加入任意动态内容,到现在为止,控制区内部的布局仍是非常简单的. 这是因为 ControlPlacement 选项仅允许您选择每个控件或批注的一面. 一旦设置选定,控件和批注只是简单地堆积到所选择面的一列上.
默认布局的简单性表现了 Manipulate 的一个重要设计原则. Manipulate 允许您忽略界面程序员通常担忧的细节问题,如控件和批注的具体位置,而是更注重界面的总体描述,迅速创建一个可行的界面.
但是,当您选择关注这些细节时,我们也提供了允许您任意控制控件布局的功能.
首先,在前面一节我们提到过,您可以将 Style 用作 Manipulate 的参数,对一段文字使用字体选项. 这里再次重复这一例子.
Style 并不是这种方式下被支持的唯一函数. Wolfram 语言的所有布局与样式构件均可以作为 Manipulate 的参数. 布局函数 GridRowFramedItemPanelText 都是被支持的,TabViewOpenerViewPaneSelector 及其它与视窗相关的构件也是被支持的. 对所有这些有用对象的有关信息,请参见布局与表格.
在讨论 Manipulate 中构件的任意布局时,理解另外一个构件 Control 是很重要的. Control 使得在Manipulate 中使用所有这些布局构件成为可能,而同时不必舍弃 Manipulate 用于指定变量和控件的简洁语法.
在最简单的形式中,Control 的封装看起来是完全多余的.
其实在这种情况下,Control 封装是可有可无的. 这两种界面的行为或显示形式均没有什么不同. 并且注意到 Control 的参数是含有一个局部变量及其定义域的熟悉列表,正像您所见到的任何其它 Manipulate 变量一样.
然而,Control 真正的强大之处在于,它可以用于 Manipulate 参数内部的任意位置,包括前面提及的那些布局和样式构件的内部. 因此,如果您想将一组控件水平放置,而不是垂直放置,您可以通过构造一个包含 Control 表达式列表的 Row,其中每一个表达式仍然使用这些简洁的高级的方式指定控件.
注意 Control 封装并不仅仅是语法糖. 如果没有这些封装,Manipulate 将不会知道 {u,{True,False}} 是一个控件指定. 相反,当它在 Row 内部遇到 {u,{True,False}} 时,它将仅仅将其像 Row 的任何其它元素一样计算与显示,并且 u 将被当作全局变量,而不是一个 Manipulate 的局部变量.
再回到原来的例子,这里用 OpenerView 对其进行了重新加工,用到的是与 ColumnControl 一起,对控件组进行编组并隐藏的功能. 同时增加了一行用于调整图形坐标轴与尺寸的控件.
请注意,改编后的代码比原代码的两倍还多. 这已经不再是一个轻便高级的界面描述. 它指出了在 Manipulate 的通常应用中将完全忽略具体细节. 这样做绝对需要进行权衡.
在界面布局的实际操作时,您应该仔细权衡. 您可能发现在 Manipulate 中使用 Control 及其它布局构件仍然比从零开始创建一个 Dynamic 界面要方便. 或者,您甚至可能发现,在 Manipulate 的外部使用 Control ,以利用创建控件的简洁语法很有用,即使在构建自定义界面时也是如此.
自制控制区外观
建议读者在阅读这一节之前读 "动态简介",因为我们会提到使用显式的 Dynamic 表达式,这在本教程中没有讲解.
设想你想使用一种不受 Manipulate 支持的 控件,例如你自己用图形和动态函数建立的一种. 这里是一个程序块,它定义了一个自制的滑块样式,在滑标的位置上显示滑块的值. 不要担心是否理解这个程序的工作细节,尽管除了在正确的地方画理想的元素这个细节之外它也不是过于复杂的.
这里是一个这个新的 控件的样子的例子:点击任何地方来移动滑标,就像使用一个一般的滑块一样.
Manipulate 里使用一个自制的 控件时,您是引入了一个用来产生那个 控件对象的纯函数作为变量指定的一部分. 只要你的函数符合所有内置 控件函数的惯例,将变量(在 Dynamic 里)作为第一个自变量,而将范围作为第二个自变量,你就可以只用函数名,而相应的自变量会由 Manipulate 自动地传送给它. 这里我们看到我们自制的 控件在一个简单的 Manipulate 里的使用.
## 这个注释意思是说所有的自变量都将被传送到这个函数中,不仅仅是第一个.)
注意如果你在纯函数中提供必要的信息,你不需要指定最小和最大值作为变量指定的一部分.
但是,如果你那样做了,那么 Manipulate 将不知道你在滑块中选折使用的范围,这意味着那一个非常好的自动运行的特性(就像在 Manipulate 相关文件中描述的一样)不能工作. 所以一般来说在变量指定中包含范围,并让那个 控件函数继承它是一个好的主意.
自然,也可以把标准的和自制的控件自由地合并起来的;这里我们把我们的两个新的滑块和由 Manipulate 自动提供的一个 SetterBar 合用在一起.
把自制的控件和控制器区里的其它动态元素组合起来也是可能的(在上一节讨论了).
通过这个例子,我们可以了解到,在创建复杂界面时,Manipulate 能起多么大的作用. 但是,重要的一点是记住 Manipulate 不是 Wolfram 语言中创建界面的唯一方法. "动态简介" 提供了更进一步的信息和例子来显示怎样创建不受 Manipulate 提供的模式所限制的自由形式的界面.
Manipulate 里建立像这样一个界面的优点之一是它能让你使用自动运行(在面板的右上角点击那个加号并选择自动运行),根据一个合理的插值模式来变动每一个变量,来使这个例子按它的进度运行.
另一方面, Manipulate 把你限制在一定的一组布局和行为之中,尽管它们是很灵活的和可扩展的,但是和"动态简介"中描述的低级别特性所能做到的相比还是仍旧固定的.