10.3 创建JavaFX桌面GUI应用程序

我们将使用桌面工具包JavaFX GUI来创建一个简单的Kotlin桌面应用程序。JavaFX是最新的GUI工具包,大多数流行的Java运行时环境(Java Runtime Environment,JRE)版本都提供了它。用于Windows、macOS和基于桌面的Linux的Java版本都提供了JavaFX;最初,随Raspberry Pi的Raspbian操作系统预安装的Java SE Embedded 8也提供了JavaFX,但Oracle在较近的更新中将其删除了。另外,Solaris用户也无法使用JavaFX。

10.3 创建JavaFX桌面GUI应用程序 - 图1 Oracle以开源许可的方式提供用于Java SE Embedded的JavaFX的源代码,因此高级用户依然能够在Raspberry Pi上编译并运行基于JavaFX的应用程序,但这种使用JavaFX的方式不在本章的探讨范围内。

强烈建议你在开发本章的项目时随时参阅JavaFX文档,这个文档可通过如下链接找到:http://docs.oracle.com/javase/8/javase-clienttechnologies.htm

Kotlin非常适合用于JavaFX开发。由于Kotlin能够通过属性名访问类的属性,因此在Kotlin中,你无需调用获取/设置函数,而是可以像访问字段一样访问属性,这让代码整洁得多。但在幕后,Kotlin还是会调用设置/获取函数。下面是一个使用Java编写的简单示例:

  1. // Java代码示例,请不要在Eclipse中输入它们
  2. @Override
  3. public void start(Stage stage) {
  4. stage.setTitle("Kotlin JavaFX Demo")
  5. stage.setScene(new Scene(new Pane(), 300.0, 300.0))
  6. }

使用Kotlin编写时,代码如下:

  1. // Kotlin代码示例,请不要在Eclipse中输入它们
  2. override fun start(stage: Stage) {
  3. stage.title = "Kotlin JavaFX Demo"
  4. stage.scene = Scene(Pane(), 300.0, 300.0)
  5. }

虽然没什么大不了,但你可能也会认为Kotlin代码要整洁、易读些。所幸Eclipse IDE Kotlin插件会在自动补全建议中显示属性,这一点你稍后就会看到。

10.3.1 定制项目

我们将对前一节在Eclipse IDE中导入的项目进行定制。为此,请执行如下操作。

(1) 将目录src/test/kotlin中的文件HelloTest.kt删除:右击这个文件并选择Delete…。

(2) 将目录src/main/kotlin中的文件Hello.kt重命名为App.kt:在Package Explorer单击文件Hello.kt,并按F2。

(3) 打开文件pom.xml,找到<properties>部分,将其中的属性hello.HelloKt改为javafxdemo.AppKt。这个类将包含程序的JVM入口方法main()

(4) 刷新这个Maven项目:右击项目名并选择Maven>Update Project。

10.3.2 创建可运行的应用程序

打开目录src/main/kotlin中的文件App.kt,将其内容替换为如下代码:

  1. package javafxdemo
  2. fun main(args: Array<String>) {
  3. }

将光标放在package语句后面并添加一个空行,再输入如下代码:

  1. class KotlinJavaFXDemo :

再输入Appl并按Ctrl + 空格(Apple机上为cmd + 空格),Eclipse IDE将打开一个包含建议的弹出窗口:

10.3 创建JavaFX桌面GUI应用程序 - 图2

在列表中找到并双击javafx.application包中的Application类,也可单击并按回车键。这将自动输入类名Application,并添加相应的import语句。接下来添加(),让这个子类自动调用父类javafx.application.Application的不接受任何参数的构造函数。现在的代码应类似于下面这样:

  1. package javafxdemo
  2. import javafx.application.Application
  3. class KotlinJavaFXDemo : Application() {
  4. }
  5. fun main(args: Array<String>) {
  6. }

Application是JavaFX工具包中的一个类,这是一个抽象类,包含多个具体方法和一个抽象方法,其中的抽象方法的Java定义如下:

  1. // Java代码
  2. public abstract void start(Stage primaryStage)

基于JavaFX的应用程序必须扩展Application类,并实现其方法start(),因此我们必须在KotlinJavaFXDemo类中提供这个方法的实现。将光标放在KotlinJavaFXDemo类的定义中,按Ctrl + 1(在macOS中为cmd + 1)并双击Implement Members:

10.3 创建JavaFX桌面GUI应用程序 - 图3

10.3 创建JavaFX桌面GUI应用程序 - 图4 Ctrl + 1(在macOS中为cmd + 1)是在Eclipse IDE命令Quick Fix(快速修复)的快捷键,可快速修复常见问题。

现在这个类应类似于下面这样:

  1. class KotlinJavaFXDemo : Application() {
  2. override fun start(primaryStage: Stage?) {
  3. TODO()
  4. }
  5. }

遗憾的是,这并不会自动添加导入Stage的语句;有鉴于此,将光标放在类名Stage?中的问号前面,并按Ctrl +空格(在macOS中为cmd +空格),再选择javafx.stage包中的Stage类。这将添加所需的import语句。

将类名Stage后面的问号删除(因为这个方法是JavaFX提供的,不可能为null),再将函数start()的实现替换为如下代码。输入每个类名时,都请使用快捷键Ctrl + 空格(macOS中为cmd + 空格)启动自动补全功能,并选择javafx包中的类:

  1. override fun start(primaryStage: Stage) {
  2. primaryStage.title = "Kotlin JavaFX Demo"
  3. val pane = Pane()
  4. val scene = Scene(pane, 500.0, 500.0)
  5. primaryStage.scene = scene
  6. primaryStage.show()
  7. }

这些代码实现的功能很多,下面来详细说说。

  • Stage对象是JavaFX应用程序的顶级容器。JavaFX默认自动创建一个主窗口,并将指向这个窗口的引用(一个Stage对象)传递给方法start
  • 我们将这个主窗口的title属性(标题)改为Kotlin JavaFX Demo。默认情况下,窗口的标题为空。
  • 创建了一个Pane()对象。Pane对象可以有子对象,其中每个子对象都可以是GUI元素,在Pane对象渲染自己时会被自动渲染。
  • 我们创建了一个Scene对象。Scene对象包含根对象,而根对象包含所有要绘制的子对象。就这里而言,这好像毫无用处,因为这里只有一个Pane()对象,但稍后你就会看到,窗口通常包含多个对象。我们告诉JavaFX,我们希望这个场景(scene)的高度和宽度都为500像素。
  • 我们将这个Scene对象赋给窗口primaryStage。知道所需的尺寸后,JavaFX创建一个500像素×500像素的对话框。
  • 最后,让窗口primaryStage可见。

10.3 创建JavaFX桌面GUI应用程序 - 图5 JavaFX通常使用基本类型double变量(而不是整数)来指定位置和尺寸。

我们还需要实现JVM入口函数main()。要启动JavaFX应用程序,必须调用Application类的公有静态方法launch。请将函数main()的实现替换为如下代码:

  1. fun main(args: Array<String>) {
  2. Application.launch(KotlinJavaFXDemo::class.java)
  3. }

10.3 创建JavaFX桌面GUI应用程序 - 图6 请确保这个函数没有放在类中,否则它将成为普通的实例方法。仅当这个方法不在任何类中时,Kotlin才会将它编译为可用作JVM入口方法的静态方法。

方法launchApplication类的一个静态方法,在Kotlin中,这意味着只能通过类名Application来访问它。调用这个方法时,必须指定一个这样的参数,即指向扩展Application类的Java类的引用;在Kotlin中,这是通过将它转换为class.java对象实现的。

前一章说过,当一个方法放在顶层(不在任何类中)时,Kotlin将创建一个类,这个类的名称由源代码文件名和后缀Kt组成。因此,就这里而言,方法main将放到javafxdemo包的AppKt类中,这个类也是我们前面修改构建文件pom.xml时指定的主类。

现在应该能够运行这个应用程序了,为此可按Ctrl + F11(在macOS中为cmd + F11),也可单击工具栏中的启动按钮。程序启动后,将出现一个空窗口:

10.3 创建JavaFX桌面GUI应用程序 - 图7

这不太令人激动,下面着手在窗口中打印一些内容。

10.3.3 编写扩展函数

Kotlin提供了一种有趣的功能——扩展函数。扩展函数是与特定类型相关联的函数。与扩展函数相关联的类型称为接受类型(receiver type),是扩展函数将添加到其中的类。接受类型的实例可像调用其其他方法一样调用扩展函数,唯一的差别是必须先导入扩展函数才能调用它。无需通过继承就可添加扩展函数,因此可给任何类添加扩展函数,包括closed(Java中为final)类。下面给JavaFX类Pane添加一个扩展函数,以令人眼花缭乱的方式打印一条消息。

在Package Explorer中,右击目录src/main/kotlin并选择New>Kotlin File…。在文本框Package中输入javafxdemo.extensions,并在文本框Name中输入PaneExtensions,再单击Finish按钮。这将创建新文件,其中包含必要的package语句。在这个文件中添加如下函数:

  1. import javafx.scene.layout.Pane
  2. fun Pane.prettyPrint(y: Double, text: String) {
  3. }

通过在函数名前面加上类名Pane和句点,让Kotlin知道这是一个扩展函数,Pane类及其子类的所有实例都可调用它。导入这个扩展函数(我们稍后将这样做),就可像下面这样调用它:

  1. // 示例代码(请不要在Eclipse中输入它们)
  2. val pane = Pane()
  3. pane.prettyPrint(50.0, primaryStage.title)

在调用prettyPrint的代码看来,prettyPrint就像是Pane类的一个普通方法一样。在函数Pane.prettyPrint()中,可访问用来调用它的Pane实例,为此只需使用this引用即可。明白这些后,我们来实现扩展函数prettyPrint

10.3 创建JavaFX桌面GUI应用程序 - 图8 别忘了使用Ctrl +空格(在macOS中为cmd + 空格)来输入所有的类名,以自动添加必要的import语句;另外,务必选择javafx包中的相应类。

扩展函数prettyPrint的代码如下:

  1. fun Pane.prettyPrint(y: Double, text: String) {
  2. val t = Text()
  3. t.text = text
  4. t.font = Font.font("Verdana", FontWeight.BOLD, 30.0)
  5. t.fill = Color.DARKBLUE
  6. t.x = 0.0
  7. t.y = y
  8. this.children.add(t)
  9. }

为让你能够从正确的包中选择类(或手动编写正确的import语句),下面列出了上述代码中用到的所有类的全限定类名:

  • javafx.scene.layout.Pane
  • javafx.scene.text.Text
  • javafx.scene.text.Font
  • javafx.scene.text.FontWeight
  • javafx.scene.paint.Color

打开目录src/main/kotlin中的文件App.kt,并添加如下import语句:

  1. import javafxdemo.extensions.prettyPrint

10.3 创建JavaFX桌面GUI应用程序 - 图9 注意,在扩展函数的全限定名中,并不包含源代码文件名PaneExtensions,这是因为在Kotlin中,包名可以与源代码文件的目录结构完全不同。

如果我们要给这个文件中的其他类添加扩展函数prettyPrint,这完全可行;在这种情况下,只需前述import语句,就可调用所有名为prettyPrint的扩展函数。

打开文件App.kt,在JavaFXDemo类的方法start()中,将如下代码行添加到代码行val scene = Scene(pane, 500.0, 500.0)的后面:

  1. pane.prettyPrint(100.0, primaryStage.title)

现在方法start()应类似于下面这样:

  1. override fun start(primaryStage: Stage) {
  2. primaryStage.title = "Kotlin JavaFX Demo"
  3. val pane = Pane()
  4. pane.prettyPrint(50.0, primaryStage.title)
  5. val scene = Scene(pane, 500.0, 500.0)
  6. primaryStage.scene = scene
  7. primaryStage.show()
  8. }

再次运行这个程序,它应使用很大的字体打印文本Kotlin JavaFX Demo

10.3 创建JavaFX桌面GUI应用程序 - 图10

作为最后的修饰,我们来对打印的消息应用一种效果。为此,打开文件PaneExtensions.kt,并在代码行this.children.add(t)前面添加如下代码:

  1. val shadow = InnerShadow()
  2. shadow.offsetX = 2.0
  3. shadow.offsetY = 2.0
  4. t.effect = shadow

为了让这种效果更突出,将代码行t.fill = Color.DARKBLUE改为t.fill = Color.YELLOW

再次运行这个程序,现在它看起来要漂亮些:

10.3 创建JavaFX桌面GUI应用程序 - 图11

10.3.4 布局窗格

JavaFX有多个内置的布局窗格(layout pane)类。可像前一个示例那样使用Pane类来手动放置每个子控件——通过计算它们的X和Y坐标,但这样做很繁琐,在窗口包含多个控件且可调整大小时尤其如此。

在大多数情况下,更佳的解决方案是使用布局窗格类,这些类负责自动放置子控件以及调整它们的大小。下表列出了最重要的内置布局。

布局类 描述
BorderPane 提供5个子窗格:上、左、中、右和下
HBox 在一个水平框中将每个子控件都放在前一个右边
VBox 在一个垂直框中将每个子控件都放在前一个下方
StackPane 将每个控件都堆叠在前一个上面,从而将控件组合起来
GridPane 以行列方式创建一个类似于网格的结构
FlowPane 将每个控件都放在前一个右边,到达最大宽度后重起一行,并将下一个控件放在这行的第一列。也可按先列后行的方式依次放置控件
TilePane 类似于FlowPane,但每个子控件的尺寸都相同
AncorPane 支持将子控件锚定在固定的位置(上、下、左、右或中);调整大小时,确保子控件相对于锚点的位置不变

由于所有的布局窗格都是Pane的子类,因此可混合使用不同的布局窗格。本章后面将演示一些较为常用的布局窗格。

10.3 创建JavaFX桌面GUI应用程序 - 图12 有关布局的完整信息,请参阅Oracle JavaFX官方手册的Work with Layouts部分,其网址为http://docs.oracle.com/javase/8/javafx/layout-tutorial/

10.3.5 实现基于BorderPane的布局

接下来在窗口中添加非常简单的动画。为此,首先来创建一个BorderPane,它将作为场景的根窗格。我们将把当前的Pane(它包含函数prettyPrint的输出)赋给BorderPanetop子窗格,让其充当应用程序的彩色标题。

打开文件App.kt,并重写函数start:使用下面的代码替换原来的代码。与往常一样,对于还未导入的类,别忘了使用Ctrl + 空格(或cmd + 空格)来输入它们的名称:

  1. override fun start(primaryStage: Stage) {
  2. primaryStage.title = "Kotlin JavaFX Demo"
  3. val textField = TextField()
  4. val mainPane = BorderPane()
  5. mainPane.top = createHeaderPane(primaryStage.title)
  6. val scene = Scene(mainPane, 500.0, 500.0)
  7. primaryStage.scene = scene
  8. primaryStage.show()
  9. }

我们对代码进行了重构:对于表示标题的Pane实例,我们将创建它的代码放在一个独立的函数中,这个函数将在稍后编写。我们创建了一个TextField实例,用于放置用户输入的文本。还创建了一个名为mainPane的实例,并将其top子窗格设置为headerPane。现在,场景的根节点为mainPane

下面来编写函数createHeaderPane()。将其放在函数start()的后面,并包含如下代码:

  1. fun createHeaderPane(title: String): Pane {
  2. val pane = Pane()
  3. pane.prettyPrint(30.0, title)
  4. return pane
  5. }

运行这个项目,会发现没有什么变化。然而,拜BorderPane实例所赐,我们现在能够轻松地在窗口左边、中间、右边和底部添加新窗格了。

下面在窗口底部添加一个文本输入框,让用户能够输入将在屏幕上移动的文本。首先来编写一个函数,用于创建包含一个标签和文本框的HBox(水平框)。请将这个函数放在函数createHeaderPane()的后面。

10.3 创建JavaFX桌面GUI应用程序 - 图13 对于还未导入的类,别忘了使用Ctrl + 空格(或cmd + 空格)来输入它们的名称,并选择javafx包中的相应类。为了节省篇幅,本章后面不再提醒你这样做。

同样,我们将在一个独立的函数中创建并设置HBox窗格。为此,在前一个函数后面添加如下代码:

  1. fun createInputPane(textField: TextField): Pane {
  2. val label = Label("Input text:")
  3. label.minWidth = 65.0
  4. val inputPane = HBox()
  5. inputPane.children.add(label)
  6. inputPane.children.add(textField)
  7. HBox.setHgrow(textField, Priority.ALWAYS)
  8. return inputPane
  9. }

下面来详细说说这些代码。

  • 将一个文本输入框(在JavaFX中这种控件名为TextField)作为参数传递给了这个函数。之所以这样做,是因为调用各种函数时,需要提供指向这个对象的引用;在这里,我们需要使用这个引用将文本框添加到HBox中。
  • 我们创建了一个尺寸固定(宽65像素)的标签。
  • 接下来,我们创建了一个HBox布局窗格。前面说过,HBox布局窗格将其子节点(控件)依次放置在一行中。
  • 将标签和textField控件都添加到了HBox类实例inputPane的子控件列表中。
  • 通过调用HBox类的静态方法setHGrow,我们让HBox知道应让textField控件占据其余下的所有空间。
  • 请注意,HBoxPane的子类,因此我们将函数createInputPane()返回类型指定为Pane而不是HBox。以后如果需要修改这个函数的实现,可保持其返回类型不变。

下面来将这个HBox添加到BorderPane中,否则将无法看到函数createInputPane()的效果。为此,向上滚动到函数start(),并在其中的代码行mainPane.top = createHeaderPane (primaryStage.title)后面添加如下代码:

  1. mainPane.bottom = createInputPane(textField)

现在再次运行这个应用程序,你将在窗口底部看到一个标签和一个文本框:

10.3 创建JavaFX桌面GUI应用程序 - 图14

如果你尝试调整窗口的大小,将发现标签和文本框依然位于窗口底部(这是因为当你调整窗口的高度时,LayoutPane会自动移动其底部部分),同时文本框的宽度总是窗口宽度减去标签宽度(这是因为我们调用了HBox类的静态方法setHGrow)。

10.3.6 实现动画

我们要让用户输入的文本在屏幕上移动。为了实现这种动画,需要跟踪一些值,因此我们将创建一个处理动画的类。不同于Java,Kotlin支持在同一个源代码文件中定义多个类,而不管这些类使用的访问限定符是什么。请在KotlinJavaFXDemo类的后面添加如下代码:

  1. class AnimatedText {
  2. val animatedText = Text()
  3. val animationPane = Pane()
  4. var directionX = 3.0
  5. var directionY = 3.0
  6. fun getPane(textField: TextField): Pane {
  7. animatedText.x = 0.0
  8. animatedText.y = 0.0
  9. animatedText.font = Font.font("Verdana", FontWeight.BOLD, 15.0)
  10. animationPane.children.add(animatedText)
  11. return animationPane
  12. }
  13. }

通过将javafx.scene.text.Text对象的xy属性都初始化为0.0,让文本一开始位于变量animationPane表示的窗口的左上角(0,0)。变量directionXdirectionY用于实现动画:每一步都使用它们将文本沿水平和垂直方向都移动3像素。请注意,我们将包含用户输入的TextField控件传递给了函数getPane()

现在,我们需要创建一个每帧都调用的函数,它计算文本的新位置并在必要时调整移动方向。为了让你能够完成这种任务,JavaFX提供了AnimationTimer类,这是一个抽象类,包含一个在每帧中都将调用的抽象方法——handle()。这个方法将帧的时间(一个表示上次调用后过去了多长时间的long值)作为参数,但为简单起见,我们没有在这个示例中使用它。如果你要在实际应用程序中实现逼真的动画,必须使用这个值来计算上次调用后过去了多长时间,并相应地移动物体。

我们在AnimatedText类中实现AnimationTimer类,为此将光标放在代码行var directionY = 3.0的末尾,添加一个空行,再添加如下代码:

  1. val timer = object : AnimationTimer() {
  2. override fun handle(now: Long) {
  3. if (animatedText.x < 0.0 || animatedText.x > animationPane
  4. .width - animatedText.layoutBounds.width)
  5. directionX = -directionX
  6. if (animatedText.y < 0.0 || animatedText.y > animationPane
  7. .height)
  8. directionY = -directionY
  9. animatedText.x += directionX
  10. animatedText.y += directionY
  11. }
  12. }

语法object : AnimationTimer()可能看起来有点怪异。前面说过,AnimationTimer是一个抽象类,这意味着不能直接实例化它,因为扩展它的类必须提供它的抽象方法的实现。在刚才所说的代码行中,我们创建了一个扩展抽象类AnimationTimer的匿名对象。注意,我们没有给这个类指定名称,而是只指定了要将指向这个对象的引用存储到引用变量timer中。由于AnimationTimer类唯一的抽象方法是handle,因此匿名类只需重写这个方法。在这个方法中,我们检查文本的当前位置,并决定是否要在X和Y轴上沿相反的方向移动,再相应地更新对象animatedText的X和Y位置。

必须启动这个定时器(timer),我们在AnimatedText类的函数getPane()中这样做。为此,在代码行animationPane.children.add(animatedText)的后面,添加如下代码行:

  1. timer.start()

我们需要将这个窗格加载到主场景的BorderPane布局的一个子窗格中,为此向上滚动到函数start(),并在其中的代码行mainPane.bottom = createInputPane(textField)后面添加如下代码行:

  1. mainPane.center = AnimatedText().getPane(textField)

如果你现在运行这个应用程序,将发现没有任何变化。另外,当你在文本框中输入文本后,也不会发生任何事情。为什么会这样呢?因为我们还没有处理TextField中的输入,也没有在对象animatedText中以硬编码的方式指定文本。现在该来改变这种状况了。如果你使用的是当前市面上流行的其他的GUI工具包,这意味着需要给输入控件TextField添加一个侦听函数,再将其输入传递给对象animatedText,但JavaFX提供了一种强大得多的功能。

在JavaFX中,可将一个控件的属性绑定到另一个控件的属性。这种绑定可以是单向的(这意味着如果属性A被绑定到属性B,属性A将在它发生变化时自动更新属性B,但反过来不会),也可以是双向的(如果绑定了属性A和B,其中任何一个属性发生变化时,都会更新另一个属性)。显然,双向绑定的开销很高,且在这个示例中不需要双向绑定,因为在这个应用程序中,只有TextField控件能接受变化。为了将textField控件的属性text绑定到控件animatedText,在AnimatedText类的getPane()方法中,在代码行return animation Pane前面添加如下代码行:

  1. animatedText.textProperty().bind(textField.textProperty())

这样,当textFieldtext属性发生变化时,animatedTexttext属性将自动更新,但反过来不会。为防范这一点,JavaFX将在你通过代码修改animatedText的属性text时引发异常。

请运行这个应用程序。当你在文本框中输入文本后,这些文本将在窗口内来回移动:

10.3 创建JavaFX桌面GUI应用程序 - 图15

如果你输入很长的文本,或者在文本移到窗口右边时缩小窗口,将发现文本可能不再沿水平方向移动,甚至不见了(当你缩小窗口后,它依然停留在原来的区域附近)。

下面来尝试找出导致这种问题的原因。请停止运行这个程序,我们将使用调试器来找出导致这种问题的原因。

10.3.7 调试程序

选择Eclipse菜单Run>Debug或按F11,程序将在调试模式下运行。输入一些文本,将窗口增大,等文本移到窗口右边后迅速缩小窗口,尝试让文本消失或停止不动。

出现这种情况时,使用调试器来尝试找出其中的原因:

10.3 创建JavaFX桌面GUI应用程序 - 图16

按下面的说明打开并使用调试器。

(1)让程序继续运行并返回到Eclipse IDE。

(2)打开文件App.kt并找到代码行directionX = -directionX。在这行代码的左边有行号,请右击这个行号并在出现的菜单中选择Toggle Breakpoint。

(3)执行到包含断点的代码行后,程序将停止执行,并询问你是否要切换到Debug透视图(Eclipse提供的专门用于调试的透视图)。请选择Yes,因为这样Eclipse将显示针对调试进行了优化的用户界面。

(4)在Debug透视图中,左上角的窗口显示了当前应用程序中所有正在运行的线程,而右上角显示了当前函数的变量及其值。由于函数handle没有使用局部变量,因此你只能看到变量thisAnimatedText的实例变量)和now(函数handle的参数)。

(5)在包含代码的窗口中,包含断点的代码行将呈高亮显示。

(6)在工具栏中,找到并单击工具提示为“Resume”的按钮。

(7)你将发现程序将再次在运行到断点时立即停止。单击Resume按钮多次,你将发现每次调用函数handle时,好像都修改了directionX。

(8)为找出原因,在Variables选项卡中展开this变量,你将看到匿名的AnimationTimer实例的变量。

(9)注意,其中包含变量this$0。展开它,它包含AnimationTimer实例的父对象(一个AnimatedText对象)的变量。展开条目animatedText,这将显示对象animatedText的所有属性。找到属性x 并查看其值,你将发现它大于0。

由于animatedText.x大于当前窗口余下的空间,因此每次调用函数handle时都将改变方向,导致文本停止移动。在工具栏中,找到并单击工具提示为“Terminate”的按钮,再切换到Kotlin透视图:找到并单击Eclipse IDE窗口右上角的Kotlin按钮。

为了修复问题,可在修改移动方向时重置文本的位置。这样做,可确保文本在窗口内(除非窗口太小)。下面首先处理X方向。找到如下两行代码:

  1. if (animatedText.x < 0.0 || animatedText.x > animationPane.width -
  2. animatedText.layoutBounds.width)
  3. directionX = -directionX

将上述代码替换为如下代码:

  1. if (animatedText.x < 0.0) {
  2. animatedText.x = 0.0
  3. directionX = -directionX
  4. } else if (animatedText.x > animationPane.width - animatedText
  5. .layoutBounds.width) {
  6. animatedText.x = animationPane.width - animatedText
  7. .layoutBounds.width
  8. directionX = -directionX
  9. }

对Y位置做同样的处理。找到如下两行代码:

  1. if (animatedText.y < 0.0 || animatedText.y > animationPane.height)
  2. directionY = -directionY

将这些代码替换为如下代码:

  1. if (animatedText.y < 0.0) {
  2. animatedText.y = 0.0
  3. directionY = -directionY
  4. } else if (animatedText.y > animationPane.height) {
  5. animatedText.y = animationPane.height
  6. directionY = -directionY
  7. }

再次运行这个应用程序。现在它稳定得多了,不会在你调整窗口大小或输入的文本太长时停滞不前。当文本不在窗口内时,它将向后移动,以便在窗口内(条件是窗口足够大)。