大家好。感谢各位观看本次演示。我将介绍一些 JMP 类,以及如何在 JSL 中使用它们来实现一些动态且可扩展的接口行为。
我叫本·巴雷特,在IDEXX Laboratories工作。我在那里当工程师已经大约10年了。我使用过各种应用程序,包括LabVIEW、MATLAB,也接触过C#。总的来说,在所有这些语言中,我一直都倾向于使用面向对象的编程风格。
不知为何,这对我来说感觉很直观。然而,当我开始在 JSL 中使用对象时,我发现这种直觉并不适用。我需要克服一些细微差别,但一旦我弄明白如何使用它们,就发现它们非常有用。
我先简单概述一下。我不会在这里深入讲解JMP类,只想分享一下我刚开始使用它们时的探索过程。我会分享一些我遇到的难题、遇到的困扰以及我是如何克服的,最后还会举一些我实际应用这些类的精彩例子。
以下是大致的议程,看看我能不能严格按照计划进行。我想先简单谈谈作用域,因为我发现这在我实现某些类时一直是个问题,而这也会引出我总结的一些最佳实践。我还想谈谈JSL中的类与其他语言(例如C#)中的类有何不同。之后,我会展示一些函数式示例代码。
不要指望在这里深入讲解JSL中的类,甚至也不会讲解面向对象设计原则。希望这篇文章能帮助你避免一些麻烦,如果你决定自己深入研究类的话。
这是议程,我已经全部讲解过了。这里快速概述一下JSL中可用的不同作用域。我不敢说自己完全理解它们之间的交互方式以及如何使用它们,但我有一些基本的概念。
这里有四种通用的作用域:本地作用域、全局作用域,以及自定义命名空间。本文档概述了如何与每种作用域进行交互。如果您想了解更多信息,我强烈建议您查看在线文档。但总的来说,我发现,在处理类时,使用命名空间是最佳方法。
如果需要在类中引用命名空间,请创建自定义命名空间,这帮助我避免了很多作用域和冲突问题。我还发现,在类中,Here 命名空间不太可靠,所以我尽量避免使用默认名称为 Here 并将其值设置为 1 的情况。此外,使用局部变量也很有帮助,只要避免使用 local Here 命令即可。但这需要更多维护工作,因为您必须显式定义哪些变量是局部变量。不过,随着我们进行一些演示,这些内容会更加清晰。
我刚才讲解了这些最佳实践。避免使用默认名称,使用自定义命名空间,以及类。你可以把类看作是一个可以复制的命名空间,这样内存中就可以同时存在多个副本。JSL 类和你可能熟悉的类有什么区别?我确信类中还有更多人们期望看到的特性,但我认为这五点已经相当全面地体现了人们对对象的期望。
封装、继承、多态、复制和抽象。继承、多态和抽象彼此相关,JSL 中的类对封装、继承和复制提供了相当不错的支持,但在实现多态行为和抽象方面存在一些细微差别。我将简要讨论这五点。
封装机制得到了很好的支持。定义类时,你会定义与该类关联的属性和方法。但需要注意的是,这些说法都不成立。你无法定义类的真正私有成员。所有成员都是公共的。这就像命名空间一样,类的成员和属性都可以全局访问。
继承也是支持的。如果您创建一个派生自另一个类的类,该子类将继承父类的成员。关于多态性有一些细微差别,我们稍后会详细介绍。为了真正实现多态性,需要一种特定的变通方法,才能使父类在其自身方法中正确地调用子方法。
复制,这正是使用类的关键所在。你可以实例化同一个对象的多个副本,每个副本都有自己独立封装的参数和方法,而且我还没遇到过什么问题。
接下来是抽象。JSL 类中并没有明确的抽象概念,但你可以定义一个父类,并为其添加一些默认方法,这些方法可能返回错误,或者只是执行一些基本行为。如果你熟悉 C# 或其他语言,这可能更接近于虚方法而不是抽象方法。以上是对 JSL 类与其他语言类之间差异的简要概述。在对 JSL 类进行如此快速的概述之后,我将切换到其他内容。
我们已经打开了 JMP 实例。我这里有一些非常简单的例子。首先,我想先讲一个简单的例子,然后再讲一个稍微复杂一点的例子,最后讲一个非常非常复杂的例子。我想讲的第一个例子是一个非常基本的进度条。
这些演示都旨在让 GUI 开发更具可扩展性和模块化,从而充分利用对象的功能。在这个进度条演示中,我将快速运行一下,以便您了解它的功能。进度条的基本实现是我在网上某个帖子中找到的,当时有人在询问相关问题。我会向您展示进度条的详细制作过程,但这些细节对本次演示来说并不重要。
这段代码主要是设置一些东西来定义进度条的外观。它创建了一个窗口,创建了……你可以看到我们在这里创建了对象,稍后我们会讲解这些对象的具体代码。然后它只是把一些东西放到一个窗口中,添加一些随机元素来表示你正在运行的任务。接下来,它会像进行一些处理一样进行迭代,并使用这些方法更新进度条,这些方法本身就是类定义的一部分。
这里没有转折点,但这个对象封装了创建此可视化效果的所有行为。让我打开这个窗口。我们现在进入代码部分。我将转到进度条类的定义,我们先快速最小化这些窗口。您可以看到,使用 `define class` 调用来定义类,给它命名,这就是实例化它的方式,然后在类中定义变量也很简单……定义它们的方式与定义其他变量的方式相同。
现在,这个值被赋给了类的实例,所以你可以通过类引用加上类名来访问它。类中的方法定义方式与函数定义方式相同。它们本质上是名称不同的函数,并被赋值给相应的类对象。我之前提到过,类就像一个命名空间,你可以对其进行操作……创建类的实例后,你可以操作所有这些内容。你可以添加属性,这些属性将被赋值给你创建的对象。你可以使用 JMP 类执行各种非常动态的操作。
我不会深入讲解细节。我只想重点说明这个进度条是如何创建的。初始化函数会被运行。我把它放到侧边栏视图里。在这个例子中,当你创建对象时,初始化函数就会运行。新对象的调用会自动运行 `_init` 函数。
初始化时,这里设置了一个默认配置,但你也可以根据需要提供任何配置。这段代码设置了所有基本内容,最后调用了构建命令。构建方法定义在这里。在构建方法中,我们创建了所有用于实现进度条外观的对象。我们有容器,还有一些间隔框,这些间隔框会放大和缩小以创建进度效果。再次说明,这个功能来自 JMP 社区的一篇帖子,我手头没有原文。
这里设置了进度条的所有细节,包括它的外观。您可以看到这里有一些辅助方法,可以更改高度或宽度,您可以调用这些方法。接下来,如何与进度条交互并更改进度量呢?这些都是方法,它们具有不同的功能。例如,`set` 方法会将进度百分比明确设置为一个值。`step` 方法会将进度条前进一定的步长,然后您可以定义任何其他您想要的功能。
你可以精确地告诉它有多少步,然后在每次迭代时推进它。你可以根据具体任务的需要修改这里的方法。现在我们有了上下文,再回到之前的例子,这非常简单明了。我们只是创建了一个对象,然后在……当我们在这里设置新窗口时,为了利用这个对象,我们必须能够从中获取实际的显示元素。
在进度条中,上层容器就是这个 H 列表框。但如果我们获取对象,实际上只是返回了该值或该容器。在新窗口定义中,如果我们想将进度条插入到特定位置,我们可以直接获取对象,或者获取我们创建的对象并进行操作……这可以解释为类似“获取面板”这样的操作。这样会更清晰,但目前我们是这样写的。
总之,这就是插入的方法。然后,我们仍然引用这个对象,并逐步执行主栏,将其设置为辅助栏的显式值。我们正在更新这里的文本。再执行一次,以便查看其效果。同样,我们只是使用一些随机值进行更新,您可以想象一下如何将其插入到其他任务中。
还有一点需要指出,我发现关闭进度条很有用。如果你这样做,下次它尝试迭代时就会失败,因为它不再拥有对窗口的引用。你已经关闭了进度条,现在当你运行这个更新来尝试更新窗口时,它就无法做到了。所以它会返回一个错误。
因为所有代码都封装在一个 try 语句中,所以如果执行失败,就会跳转到这里。因此,如果你需要中断处理,这是一种快速简便的方法,我之前就多次用到过。这就是进度条,实现起来非常基础。你可以用很多方式自定义它,比如从中获取信息,或者做任何你想做的事情。
接下来我想讲的内容稍微复杂一些,可能需要一些准备工作。我想在这里展示一下这些表格。我用的是大型班级示例表,并对其进行了一些随机化处理,因为我想演示虚拟连接的功能。对于不熟悉虚拟连接的人来说,标准连接可以这样实现……我们有一个包含大量分数的班级。每个学生都有多条记录。他们尝试做某件事 10 次,并获得了分数。表格里只有他们的姓名、尝试次数和分数,但我们还有其他信息。我们想把这些信息也导入到这里。所以我们需要进行连接。我相信任何使用 JMP 的人都做过类似的事情。
这样就能将这些值导入到这张表中,但还有另一种方法。所有这些名称都是唯一的。我们可以将它们设置为链接 ID,然后在这张表中,我们可以引用这张表。现在我们实现了虚拟连接,可以看到所有这些信息。回到这里,我可以隐藏或显示,现在我们可以在这张表中访问这张表这一列的信息,而无需进行连接。我们也可以对这张表执行相同的操作。
这样做有一些非常棒的优势。如果我们进入列信息,找到这里的链接引用属性,就可以设置它接受选中或排除状态。如果我应用此设置,然后我来到这里,当我选择这一行时,它会选中所有相关的行。如果您执行汇总并链接到数据表或数据视图,也会发生类似的情况。
这真是一个很棒的功能。我去年就开始用它了,很快就想在 JSL 脚本中实现自动化,最后我基本上写了一个链接管理器。让我清除这些。清除链接 ID。清除链接引用。现在它们只是普通的未链接表。
接下来,我将打开示例链接管理器。我会运行一些代码。我们将设置所有表格,并初始化链接管理器,稍后我会讲解背后的代码。
好了。不知道之前的错误是什么,但现在链接管理器的基本窗口已经打开了。图形用户界面(GUI)已经启动,但还没有链接任何东西。我们可以使用API来设置链接源,这基本上和我之前设置链接ID的操作相同。运行之后,我们可以看到链接管理器已经更新了。
这里出现了一个新的图形用户界面入口。现在我们可以链接其中一个表了。又出现了一个新的入口。我们可以链接第二个表了。又出现了第二个入口,现在我们就可以进入并修改这些链接配置了。
如果我想要这个分发,也就是想要第二个表,我可以设置选择和排除条件……基本上,如果我在这里选择了某些内容,我可以让它传播到那里。我们已经选择了所有内容。现在,所有这些都应该被选中,此外,原始表中的源表也应该被选中。
通常情况下,如果你想更新这些信息,你需要右键点击,选择“列信息”,然后找到“链接引用”,再找到“分发”或“接收”,修改你想要更改的内容,然后点击“应用”,最后再去另一个表格重复操作。我想要的是更动态的方式,能够让你实时看到更改过程。你可以添加另一个表格,设置多个数据源来完成所有这些操作,所有配置都在这里,你可以随时关闭某些功能,移除链接等等。
我发现这个工具在处理包含多个表格的大型数据集时特别有用。例如,你可能有一个大型汇总表,并且已经对连续数据进行了一些数据处理,现在你想将各个数据关联起来,但又不想创建一个庞大的表格,因为那样会变得难以管理。这个工具就是基于这些需求而开发的。
现在,如果我们要深入研究代码,这会稍微复杂一些。首先我要说明一点,这些代码都没有经过优化。很多部分仍然只是……你可以称之为初稿。我只是让它能运行起来,还没有进行任何清理工作。请记住这一点,因为我接下来会讲解一些代码。
您可以看到我调用的初始化函数,它只是检查是否存在对象,如果没有,则创建一个新对象。可能需要改进的一点是,我最初的想法是,如果我同时运行多个项目,我可能需要多个链接管理器同时处于活动状态,这或许有用,或许没用,但目前这个类基本上是以单例模式实现的。
如果你愿意,可以实例化更多实例,但这样做在某些具体情况下会不太稳定,我稍后会详细说明。本质上,链接管理器维护着一些数组,这些数组定义了表格之间的链接方式、表格之间的链接关系以及链接的当前状态。它提供了一些方法来更改链接并执行其他相关操作。
对于图形用户界面(GUI),我们这里实现了两个类。一个是链接配置类。就是,如果我能打开……让我打开链接管理器窗口。它在哪里?就在这里。
如果我们查看链接管理器窗口,链接配置就是这个可以更改接受/分发、选择排除项等设置的框,这就是配置部分。然后是选项卡,链接源选项卡就是其中之一。每个源都有一个单独的选项卡,因为链接源有一些规则,例如哪些列可以链接,哪些列可以链接到该列等等。创建链接源时,链接管理器会创建一个这样的对象。
还有一大堆其他的事情在进行,比如检查一些东西,设置一些事件来检测表格何时关闭等等,你可以忽略这些。这里才是重点。它创建了一个新的链接源选项卡对象,并提供了表格。
差不多就是这样。它会被添加到图形用户界面(GUI)中,我想应该就在这里。这里有个源列表,所以这个内容会被添加到它后面,它只是一个容器,你可以看到我们用这个函数来获取选项卡,也就是实际的显示对象。这基本上处理了很多复制操作。每次我们创建一个新源时,都会创建一个新的这样的对象,并且会自动获得所有功能。
如果我们进入“源代码”选项卡,这里也有一个初始化过程。它有自己的显示对象,也就是一个页面框,里面有一些对齐操作,还有一个列表对象。这个列表对象包含了你可能用到的所有不同的链接配置。再次提醒,这可能还没有完全优化,但你可以开始看出它的优势……在 JMP 中构建动态界面行为可能更容易。我在其他很多地方都用过这个功能。
你们可能已经注意到我这里有个召回库,我之前提到过,它有一个更复杂的实现方式。我估计没太多时间深入研究它。真希望我能多花点时间,因为这实际上是我第一个使用 JMP 类库的项目。可惜的是,它也还处于初稿阶段。这里面有很多东西其实很容易就能简化。
我想,我现在主要是希望我已经引导你思考如何利用 JMP 中的类来实现更动态的行为,并使代码更简洁。因为我知道,我以前尝试过……我见过一些复杂且动态的 UI 代码,它们把所有东西都直接写在了新建窗口函数里。
在新窗口中,你会看到 20 个标签页,里面全是各种功能和按钮,各自执行着不同的操作。而实际上,实现相同功能有更简洁、更面向对象的方法。这种方法更易于维护、可扩展,也更易读。我认为,在实现用户界面时,使用类有很多好处。
我不会深入探讨召回相关的内容。我唯一要补充的是,我将努力整理所有这些示例代码和召回库,并将其放在一个公开的 Git 仓库中,任何感兴趣的人都可以查看、克隆、fork 仓库,并随意使用它,尝试理解如何在自己的代码中利用 JMP 类来提高效率。
我觉得剩下的唯一一点就是快速回顾一下我之前提到的作用域问题。我们已经看过一些例子,你也了解了如何构建结构来实现这些动态 GUI,但现在我们需要了解其中的难点以及如何解决它们。我想一个很好的例子是……我们看看能不能在这里演示一下。
如果我选择其中之一,我之前提到的作用域问题就与你调用方法的方式以及它们继承的作用域有关。我见过很多在 Here 命名空间中工作时出现的问题。我见过全局命名空间和方法之间的冲突。为了尽可能简化问题,我发现一个万无一失的解决方法,可以避免你在使用类时遇到的任何作用域问题(这里我不会演示具体方法,但这是一个很好的例子,可以让你了解你可能想要如何操作),那就是使用局部修饰符。我喜欢这样写。
这里唯一需要显式定义的变量就是这个。这里,我们设置这个变量的目的是为了解析并创建用于生成引用的属性。那段代码无关紧要。仅仅因为这个变量没有被定义为局部变量,它就可能覆盖另一个作用域中的变量。根据代码中调用此方法的位置,这个变量可能会导致冲突。
我发现避免类的作用域冲突最简洁的方法就是明确定义局部变量。我估计基本上所有方法都需要这样做,因为大部分代码都是在我理解如何解决这些问题之前写的。除了显式命名空间标识之外,唯一能帮助你解决这些问题的方法就是使用显式命名空间标识。
每当我编写库时,我基本上都是在一个新的自定义命名空间中编写,并且我会尝试将所有变量和方法或变量和函数都定义在该命名空间中,这帮助我避免了冲突。
那么,我想,实际上,还有一点需要讨论,虽然这个例子不太恰当,那就是多态性和继承的一些问题。嗯,或许在 recall 库中讨论这个问题会比较合适。
希望情况不会太复杂……我们可以略过一些实现细节,只关注存在的问题。根据目前的情况来看,这个“召回面板”本质上是一个容器,你可以设置它自动召回字段的值。它需要被继承,因此会有子类调用它的一些行为。具体来说,就是这里提到的这个方法。子类需要实现自己的 build 方法版本。
现在,多态性的微妙之处就体现在这里了。如果你尝试从父类中调用某个方法,比如你想调用 `build` 方法,而子类重写了 `build` 方法,那么 `build` 方法就无法正确地传递给子类。我无法解释具体原因,但我认为这与后端类的实现方式有关。不过,有一种方法可以绕过这个问题。我认为,最好在另一个库中演示一下,但可惜的是,我目前不打算深入探讨。
我想我就不深入讨论细节了,只想说,你最后提到多态性,或者说你可以实现一个子类来重写行为,但这可能无法按预期运行,这有点可惜,因为我认为继承是类中最有用的方面之一。
虽然可以继承行为,但在父类中调用这些继承的行为会稍微复杂一些。我想我会这样表述。当我把这些资料放到网上时,我会尽量提供一些更具体的例子,说明为什么这样做行不通。再次强调,我只是想概述一下如何在 Junk JSL 中将类集成到 UI 设计中。
话虽如此,我还是就此打住,免得说得太多了。再次声明,我叫本·巴雷特,如果您有任何疑问或想了解更多相关内容,欢迎随时联系我。最后,非常感谢。