65.9K
CodeProject 正在变化。 阅读更多。
Home

JavaFX 入门 - 虚拟国际象棋

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (27投票s)

2009年4月17日

CPOL

30分钟阅读

viewsIcon

144213

downloadIcon

4167

本文描述了我通过编写一个国际象棋程序来学习 JavaFX 编程语言的经历。

Screen shot of Chess JavaFX Program

用 JavaFX 编写的国际象棋程序屏幕截图示例

引言

Sun 最近发布了一种名为 JavaFX 的 Java 平台新编程语言。它的主要目的是使开发富互联网应用程序 (RIA) 更容易,这些应用程序可以在各种设备上运行,包括 PC、手机和蓝光播放器。它最常与微软 (Silverlight) 和 Adobe (AIR) 的新 RIA 语言进行比较。JavaFX 不限于创建 RIA。在本文中,我开发了一个在桌面运行的国际象棋应用程序。我称之为虚拟国际象棋,因为我使用的算法只是随机选择一个走法。所以如果你不能打败这个国际象棋程序,那么你就是一个比我更差的玩家。这很糟糕。也许有一天我会编写一个更好的国际象棋程序,并发表一篇名为智能国际象棋的文章。

背景

我已经用 Java 编程超过 5 年了,最近决定自学 JavaFX。我认为学习 JavaFX 的最佳方式是给自己分配一个我感兴趣的编程项目,并用新语言完成该项目。因此,在大约一个月的时间里,我用 JavaFX 编写了一个国际象棋程序。本文描述了我的学习经验,并介绍了 JavaFX 编程的基础知识。

我原以为 JavaFX 会和 Java 一样,只是需要学习一套新的框架类。我错了。JavaFX 是一种完全不同的编程语言。为了帮助阅读本文的经验丰富的 Java 程序员,我提供了一些从 JavaFX 到 Java 的代码翻译,以帮助您更好地理解本文中发布的 JavaFX 示例代码。

Using the Code

我在工作中使用了 IDE(Eclipse 和 Visual Studio),并且非常喜欢使用它们。但是,对于这个项目,我决定不使用 IDE。我喜欢尽可能地锻炼我的 VI 技能。以下列表描述了您需要下载的内容,具体取决于您是使用命令行还是 IDE

  • 为了从命令行构建国际象棋程序,您必须下载 JavaFX 1.1 SDK。
  • 如果您喜欢在 IDE 中工作,可以下载适用于 JavaFX 1.1 的 NetBeans IDE 6.5。
  • 如果您更喜欢 Eclipse 而不是 NetBeans,则可以使用插件。
  • 为了将 SVG 转换为 JavaFX 图形,您还需要下载 JavaFX 1.1 Production Suite。

所有这些下载都可以在 javafx.com 上获取,除了 Eclipse 插件可以在 kenai.com 上获取。我从未使用 NetBeans 或 Eclipse 构建 JavaFX 程序,所以我不确定它们的效果如何。

源代码文件包含 Java 1.6 和 JavaFX 1.1 源代码文件。因此,您还需要一个 Java 1.6 编译器来构建代码。NetBeans 和 Eclipse 将自带 Java 编译器,如果您从命令行构建,则需要从 java.sun.com 下载最新的 JDK。

为了从命令行构建源代码并运行国际象棋程序,请在命令行中输入以下内容(在包含源代码的目录中执行此操作)

编译和运行国际象棋程序

javac *.java
javafxc *.fx
javafx Main

javac 是编译 Java 源代码文件的命令行编译器,javafxc 是编译 JavaFX 源代码文件的命令行编译器,javafx 通过运行指定的 JavaFX 文件(在本例中它开始运行从文件 Main.fx 编译的代码)来运行国际象棋程序。

关注点

JavaFX 是一种非常年轻的编程语言,它确实感觉像一个测试版产品。我以前从未在编程语言中发现过这么多错误,而且我只用它写了一个月的应用程序。我找到了一些错误的解决方法,但不是所有。我从来无法更改应用程序的图标。

该语言确实有很多优点,希望随着产品的成熟,这些错误能及时得到解决。我真的很喜欢绑定功能,发现创建 GUI(图形用户界面)比我刚开始学习如何在 Java 中制作 GUI 时容易得多。Java 是一种非常成熟的语言(它已经存在了十多年),并且已经为 Java 编写了大量代码。能够直接从 JavaFX 代码中实例化这个庞大的现有 Java 代码库是件好事。

如果您决定冒险进行一些 JavaFX 编程,请注意我在 Google 上找到的许多 JavaFX 示例代码在最新版本的 JavaFX (v. 1.1) 中不起作用。JavaFX 自 2008 年中期以来肯定发生了相当大的变化。我发现 2008 年中期之前发布的大多数文章在 1.1 版中不起作用,而 2008 年底和 2009 年发布的大多数代码似乎都能正常工作。

我们走吧!

让我们从一个简单的 Hello World 程序开始,并在此基础上构建我们的国际象棋程序。你很快就会看到,你从 Hello World 程序中学到了很多东西。这是 JavaFX 中 Hello World 的代码

用 JavaFX 编写的 Hello World 程序

println("Hello World!");

就是这样!一行代码。这个简单的一行程序有很多东西要教给我们。Java 中的等效程序是

用 Java 编写的 Hello World 程序

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

不需要 Main 或 Class

那么我们从中学到了什么呢?如果你来自 Java 背景,你习惯于将所有东西都放在类中。那是因为在 Java 中,任何代码都不能存在于类之外。正如你所看到的,在 JavaFX 中情况并非如此。

此外,在 Java 程序中,你必须创建一个被声明为 public static voidmain 方法,该方法接受一个名为 String[] args 的参数(命令行参数)。在 JavaFX 中,这不是必需的。你只需将指令放在 JavaFX 脚本文件中并运行该文件。不需要 main

如果需要访问命令行参数,可以使用名为 run 的特殊函数。run 是一个特殊函数,用作脚本的主入口点。run 函数如下所示

“run”是一个可选函数,充当 JavaFX 脚本的主入口点

function run(args: String[]) {
    println("{args[0]}");
}

该程序只是打印出第一个命令行参数。令人惊讶的是,如果没有传递命令行参数,它也可以无错误地运行。如果没有传递命令行参数,它只是打印出一个新行。

序列与数组

我们再来谈谈 run 函数。它接受一个名为 args 的参数,它是一个 String 对象序列。JavaFX 中的序列看起来像 Java 中的数组。它们通过在类型后面加上方括号 [] 来声明。例如,String[] 声明一个 Strings 序列。序列是不可变的,就像数组一样。但是,你可以编写代码来插入元素到序列中和从序列中删除元素,如下所示

您可以向序列添加和从序列中删除元素

var colors = ["Red"];
insert "Blue" into colors;
insert "Green" after colors[1];
insert "White" before colors[0];
delete "White" from colors;
delete colors[0];
println("{sizeof colors}");

由于序列是不可变的,当元素插入或从序列中删除时,JavaFX 最终会创建一个新序列。这与 Java 中 Strings 的工作方式类似。您可以在代码中修改 String,但由于 Strings 是不可变的,如果您修改 String,运行时最终会为您创建一个新的 String

JavaFX 有一些巧妙的方法可以使用特殊的 .. 代码创建数字序列。以下代码提供了几个构建数字序列的示例

在 JavaFX 中创建数字序列的示例

def seq1 = [1..10];  // seq1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
def seq2 = [1..10 step 2];  // seq2 = [1, 3, 5, 7, 9]
def seq3 = [10..1 step -1];  // seq3 = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

JavaFX 中的 for 循环只作用于序列。它的形式是 for(varName in sequence) { ... }。以下代码演示了如何将简单的 Java for 循环转换为 JavaFX 循环

Java 和 JavaFX “for” 循环的区别

// Java for loop
for(int i=0; i<10; i++) {
    System.out.println("Hello World!");
}

// equivalent JavaFX for loop
for(i in [0..9]) {
    println("Hello World!");
}

在 JavaFX 中,您可以使用 indexof 关键字获取序列中项目的索引号。例如

遍历数字序列并使用“indexof”关键字

for(i in [100..110 step2]) {
    println("{indexof i} = {i}");
}

// Output
0 = 100
1 = 102
2 = 104
3 = 106
4 = 108
5 = 110

变量和参数

现在你已经在 JavaFX 代码中看到了两种不同的变量/参数声明。第一个是 run 函数中的 args 参数(声明为类型 String[]),第二个是序列示例中的 colors 变量(没有类型声明)。所有变量和参数都必须在 JavaFX 中声明,但类型是可选的。如果你不声明变量的类型,运行时会根据分配给变量的对象的类型来确定。我个人更喜欢给我的变量一个类型,如果你愿意,可以在名称后面加上 :,然后是类型。例如,我们可以这样给 colors 变量赋值

声明变量类型是可选的

var colors: String[] = ["Red"];

函数参数只需声明一个变量名和可选类型。变量声明为 vardef。两者的区别在于 var 可以修改,而 def 不能。声明为 def 的变量类似于 Java 中声明为 final 的变量。因此,以下是有效代码

定义为“def”的变量可以修改

// valid code
var i = 7;
i = 2;

而以下代码将无法编译

定义为“def”的变量不能修改

// invalid code, won't compile
def i = 7;
i = 2;

函数参数不能包含 vardef 关键字。所有函数参数都被视为 defs,因此不能修改。

函数

所有函数都必须使用关键字 function 声明它们是一个函数。我发现这个要求非常令人惊讶。脚本语言通常以其简洁性而闻名。这个关键字似乎没有必要,而且它也是一个大词,足足 8 个字符。看起来编译器应该能够在看到函数时识别它。

每个函数都有一个返回类型。如果您不声明返回类型,则默认为 Void。这不是拼写错误,在 Java 中 void 是小写 'v',而在 JavaFX 中是 Void,大写 'V'。

函数返回类型的放置与变量和参数中的类型类似。我们通过在右括号后放置 :,然后是返回类型来声明返回类型。例如,以下函数返回一个 Integer

JavaFX 中返回“Integer”的“function”

function foo(i: Integer): Integer {
    i * 4;
}

内置数据类型

如您在最后一个代码示例中看到的,关键字 Integer 表示一个整数。在 Java 中,这等效于一个 int。如果您需要一个小数,可以使用内置类型 Number,它表示一个浮点数。通常,这些是您将使用的两种数字类型,但如果您需要特定大小的变量,可以使用以下任何内置数字类型:ByteShortNumberIntegerLongFloatDoubleCharacter(所有首字母大写)。

其他内置数据类型包括 Boolean,它就像 Java 中的 boolDuration,它是一个时间持续时间,以及 String。当赋值给 Duration 类型时,您基本上会给出时间量,然后是时间单位。例如

JavaFX 中不同的“持续时间”

100ms;  // equal to 100 milliseconds
200s;   // 200 seconds
300m;   // 300 minutes
400h;   // 400 hours

Strings 在 JavaFX 和 Java 中工作方式类似。JavaFX String 对象具有与 Java String 对象相同的方法,但连接 Strings 的方式不同。这里有一些示例展示了 Java 中的 String 操作,然后是 JavaFX 中的等效操作

Java 和 JavaFX 中的“字符串”连接

// Java code
String strA = "A";
String strB = "B";
String strC = strA + strB;
String strD = strC.replaceAll("A", "C");

// equivalent JavaFX code
var strA: String = "A";
var strB: String = "B";
var strC: String = "{strA}{strB}";
var strD: String = strC.replaceAll("A", "C");

表达式

这是我们第二次在 String 中看到 {} 的用法。当放在字符串中时,{} 将封闭的表达式转换为 String。我们在 run 函数中看到了这一点,我们用代码 println("{args[0]}") 打印出第一个命令行参数。我们还将其用于连接 Strings,如前面的代码示例所示。我们可以将任何表达式放在 {} 中。例如,以下代码打印 The value of foo(7)is 49

在 JavaFX 中将表达式转换为“字符串”

function foo(i: Integer): Integer {
    i * i;
}

def i = 7;
println("The value of foo({i}) is {foo(i)}");

{i}i 的值转换为 String,而 {foo(i)}foo(i) 的结果转换为 String。这些与字符串 "The value of foo("、") is " 和 ")" 连接。等效的 Java 代码将是

在 Java 中将表达式转换为“字符串”

int foo(int i) {
    return i * i;
}

int i = 7;
System.out.println("The value of foo(" + i + ") is " + Integer.toString(foo(i)));

返回可选关键字

如果你没有注意到,我写了两个 foo() 函数,每个都返回 Integers。然而,这两个函数都没有使用 return 关键字。在 JavaFX 中,return 关键字是可选的。如果你不包含 return 关键字,则返回最后执行的表达式的值。如果你愿意,你可以使用 return 关键字,如下面的示例所示

如果你想,你仍然可以使用“return”关键字

function foo(i: Integer): Integer {
    return i * i;
}

def i = 7;
println("The value of foo({i}) is {foo(i)}");

图形用户界面

基础知识讲够了,让我们开始制作我们的国际象棋程序。我们要做的第一件事是为我们的应用程序创建一个窗口。在 Java 中,您可以使用 JFrame 类来完成此操作。JavaFX 中的等效类是 Stage。以下显示了如何在 Java 和 JavaFX 中创建窗口

在 Java 和 JavaFX 中创建窗口

// Java code
import java.io.*;
import javax.swing.*;

public class Main {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Chess");
        frame.setBounds(100, 100, 400, 400);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        String workingDir = System.getProperty("user.dir");
        String iconFilename =
		workingDir + File.separator + "res" + File.separator + "Icon32.png";
        ImageIcon icon = new ImageIcon(iconFilename);
        frame.setIconImage(icon.getImage());
        frame.setVisible(true);
    }
}

// JavaFX code
import java.io.*;
import java.lang.*
import javafx.scene.image.*;
import javafx.stage.*;

def stage: Stage = Stage {
    title: "Chess"
    x: 100, y: 100, width: 100, height: 100
    icons: [ Image {url: "{__DIR__}res{File.separator}Icon32.png" } ]
    onClose: function() {
        System.exit(0);
    }
}

JavaFX 有 Bug

以下是这两个应用程序的屏幕截图。Java 应用程序是带有自定义国际象棋图标的那个,而 JavaFX 应用程序带有默认的 Java 应用程序图标。这是我在 JavaFX 中发现的几个错误中的第一个。我搜索了 Google,看起来你无法为 JavaFX 中的程序加载自定义应用程序图标。以下是讨论该错误的论坛,错误1错误2。根据 JavaFX 1.1 文档,您应该能够为 Stage 设置图标。不幸的是,它似乎不起作用。所以我们现在只能使用默认的 Java 图标。希望这个问题能尽快解决。

JavaFX 1.1 版本仍有 Bug,无法更改主窗口的图标

Screen shot of JavaFX and Java Windows, JavaFX cannot load a custom icon.

创建对象

在 Java 中,新类的创建使用 new 关键字。在 JavaFX 中,您不必使用 new 关键字。您通常通过将变量分配给类名,然后在花括号 ({}) 中为类属性分配初始值来创建新对象。您通过提供属性名,后跟 :,再后跟该类属性的初始值来设置类属性的初始名称。您可以在每个类属性赋值之间放置逗号,但这是可选的。在上面的示例中,我只在我分配在同一行上的属性(xywidthheight 属性)之间使用了逗号。icons 属性是一个序列,所以我将 Stage 的图标放在方括号 [] 中。该图标是 Image 对象的一个实例。因此,我通过在花括号内初始化类属性来类似地创建此类的实例。在这种情况下,我设置了用于图标的图像位置的 urlonClose 属性被初始化为指向一个函数。该函数只调用 System.exit(0)

如果您愿意,仍然可以使用 new 关键字来实例化类。例如,这是一个与上一个示例功能相同的 JavaFX 程序,但使用 new 创建 Stage。JavaFX 中的类没有构造函数。由于 Stage 是一个 JavaFX 类,我们不能通过向构造函数传递值来为对象赋初始值(没有构造函数)。因此,我们像下面的代码所示,在创建新对象后为类属性赋值

您仍然可以使用“new”关键字创建 JavaFX 对象,但我不太喜欢它

import java.io.*;
import java.lang.*;
import javafx.scene.image.*;
import javafx.stage.*;

def stage: Stage = new Stage();
stage.title = "Chess";
stage.x = 100;
stage.y = 100;
stage.width = 400;
stage.height = 400;
stage.icons = [ Image { url: "{__DIR__}res{File.separator}Icon32.png" } ];
stage.onClose = function() {
    System.exit(0);
};

创建 Java 对象时,使用 new 关键字是有意义的,因为 Java 对象有构造函数,您可以使用 new 关键字向这些构造函数传递参数。创建 JavaFX 对象时,我倾向于不使用 new 关键字。我认为,当我在不使用它的情况下创建对象并使用封闭的花括号 {} 实例化所有类属性时,代码看起来更简洁,如第一个示例所示。

保存应用程序设置

我总是喜欢将应用程序设置保存在用户的家目录中。对于 GUI 应用程序,我至少会保存应用程序主窗口的位置和大小。Java 已经存在很长时间了,并且已经有一套类来帮助我们完成这项任务。让我们首先看看如何在 Java 中完成这项任务,然后我们将在 JavaFX 中做同样的事情。

在 Java 中保存应用程序设置

// Java code
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import javax.swing.*;

public class Main {
    public static void main(String[] args) {
        // get the users home directory
        String homeDir = System.getProperty("user.home");

        // these are declared final so they can be accessed in the
        // anonymous inner class below
        final String settingsFilename =
		homeDir + File.separator + "mySettings.properties";
        final Properties props = new Properties():

        // Load the saved settings
        try {
            FileInputStream input = new FileInputStream(settingsFilename);
            props.load(input);
            input.close();
        } catch(Exception ignore) {
            // we ignore the exception since we expect the
	   // settings file to not exist sometimes
            // on the first run of the application it will definitely not exist
        }

        int savedX;
        try {
            savedX = Integer.parseInt(props.getProperty("xPos", "100"));
        } catch(NumberFormatException e) {
            savedX = 100;
        }

        // similar code to load savedY, savedWidth, savedHeight left out for brevity

        // Create and show the window, same code as before except on close we do nothing
        // We also make the JFrame final so it can be accessed
        // in the anonymous inner class
        final JFrame frame = new JFrame("Chess");
        frame.setBounds(savedX, savedY, savedWidth, savedHeight);
        // different from previous example
        frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        String workingDir = System.getProperty("user.dir");
        String iconFilename = workingDir + File.separator +
			"res" + File.separator + "Icon32.png";
        ImageIcon icon = new ImageIcon(iconFilename);
        frame.setIconImage(icon.getImage());
        frame.setVisible(true);

        frame.addWindowListener(new WindowAdapter() {
            @Override public void windowClosing(WindowEvent e) {
                // Save settings on exit
                Rectangle frameBounds = frame.getBounds();
                props.setProperty("xPos", frameBounds.x);
                props.setProperty("yPos", frameBounds.y);
                props.setProperty("width", frameBounds.width);
                props.setProperty("height", frameBounds.height);

                try {
                    FileOutputStream output = new FileOutputStream(settingsFile);
                    props.store(output, "Saved settings");
                    output.close();
                } catch(Exception ignore) {
                    // if the settings can't be saved we'll
	           // just use the defaults next time
                }

                // exit the application
                System.exit(0);
            }
        });
    }
}

属性

我们使用 Properties Java 类来保存应用程序设置。此类包含一个键/值对字典。我们使用 load 方法加载保存的属性,并使用 store 方法保存属性。我们调用 getProperty 方法获取指定键的值。我们还传入一个默认值参数,如果 Properties 对象中未设置该键,则返回此值。我们使用 setProperty 方法保存 properties。这是在 Java 中保存应用程序设置的几种方法之一。

窗口监听器

我们在显示窗口之前加载应用程序设置,以便可以设置窗口的保存位置和大小。我们通过向 JFrame 添加 WindowListener 来保存应用程序设置。当用户尝试关闭应用程序窗口时,我们的 WindowListener 将收到通知。当这种情况发生时,我们将 JFrame 的大小和位置存储在我们的 Properties 对象中,并将其保存到用户的家目录。然后我们通过调用 System.exit(0) 退出应用程序。

在 JavaFX 中保存设置

我们可以在 JavaFX 类中使用相同的类来保存应用程序设置。唯一的区别是我们不需要添加 WindowListener,我们只需更新 Stage 类的 onClose 函数以在退出应用程序之前保存应用程序设置。这在下面的代码中显示

在 JavaFX 中保存应用程序设置

import java.io.*;
import java.lang.*;
import java.util.*;
import javafx.scene.image.*;
import javafx.stage.*;

// find the settings file in the users home directory
def homeDir = System.getProperty("user.home");
def settingsFile = "{homeDir}{File.separator}"mySettings.properties";

// load the settings into the Properties class
def props: Properties = new Properties();
try {
    def input: FileInputStream = new FileInputStream(settingsFile);
    props.load(input);
    input.close();
} catch(ignore: Exception) {
    // if the file doesn't exist, that's OK we will just use the default values
}

// read the saved settings
var savedX: Number;
try {
    savedX = Double.parseDouble(props.getProperty("xPos", "100"));
} catch(e: NumberFormatException) {
    savedX = 100;
}
// similarly load savedY, savedWidth, and savedHeight settings

// create our window as before, only add saveSettings() call to onClose function
def stage: Stage = Stage {
    title: "Chess"
    x: savedX, y: savedY, width: savedWidth, height: savedHeight
    icons: [ Image {url: "{__DIR__}res{File.separator}Icon32.png" } ]
    onClose: function() {
        saveSettings();  // save settings before exiting
        System.exit(0);
    }
}

function saveSettings(): Void {
    props.setProperty("xPos", "{stage.x}");
    props.setProperty("yPos", "{stage.y}");
    props.setProperty("width", "{stage.width}");
    props.setProperty("height", "{stage.height}");

    try {
        def output: FileOutputStream = new FileOutputStream(settingsFile);
        props.store(output, "Saved Chess Settings");
        output.close();
    } catch(ignore: Exception) {
        // if the settings can't be saved we'll just use the defaults next time
    }
}

正如你所看到的,我们可以使用与 Java 应用程序相同的 Java 类来在 JavaFX 中保存我们的应用程序设置。这是 JavaFX 最好的功能之一,我可以在我的 JavaFX 程序中使用现有的 Java 类的庞大库。因此,我在使用 Java 框架方面的经验也适用于我的 JavaFX 编程。

需要注意的一点是,Stage 类的 xywidthheight 属性都定义为 Numbers。因此,当我们将它们保存到磁盘时,它们都带有小数点。这对我来说似乎很奇怪,因为 Stage 对象的大小和位置是以像素数指定的。使用小数是没有意义的,因为您只能指定整数像素,1.5 像素没有意义,它要么是 1 像素要么是 2 像素。我不确定这是否是一个错误。我已经向 JavaFX 提交了两个错误报告。我不确定这里是否需要另一个错误报告。使用 Number 而不是 Integer 似乎只是一个奇怪的决定。

国际象棋程序

现在我们已经有了窗口,下一步是制作一个棋盘。我将在 JavaFX 中创建一个自定义棋盘 GUI 组件。如果我在 Java 中做这件事,我的棋盘将扩展 JPanel,然后我将这个 JPanel 添加到我的 JFrame 中。在 JavaFX 中,我的棋盘将扩展 Scene,并添加到我的 Stage 对象中。

在我创建我的 Board extends Scene 类之前,让我稍微解释一下我想要创建的棋盘。在构建 JavaFX 程序时,我想利用这种新语言的图形功能。在这种情况下,我想为我的棋子使用 SVG 图形,以便它们在任何分辨率下都看起来很好。这意味着我的棋盘必须是用户可调整大小的,并且无论应用程序主窗口的边界如何,它都必须看起来很好。以下屏幕截图应该解释我希望在调整主窗口大小时棋盘的样子。

棋盘根据主窗口的大小调整和重新定位自身

Resizeable Chess Board.

棋盘尝试尽可能多地占据窗口空间,同时保持正方形。如果棋盘的宽度大于高度,或者高度大于宽度,则棋盘在窗口中居中。如果窗口是正方形,棋盘不会占据整个窗口,但会占据大部分窗口。它总是会在棋盘周围留下一小块边框。此边框的宽度和高度至少是棋盘上单个方格的大小。所以我们的棋盘有以下 3 个属性

  • squareSize - 棋盘上单个方格的大小
  • xOffset - 如果窗口宽度大于高度,这告诉我们必须将棋盘移动多远才能使其在窗口中居中
  • yOffset - 如果窗口高度大于宽度,这告诉我们必须将棋盘移动多远才能使其在窗口中居中。

JavaFX 绑定

绑定是 JavaFX 中的一个新功能,它允许您将一个变量的值绑定到另一个变量,或者将一个方法的值绑定到另一个变量。绑定是使用 bind 关键字执行的。由于 squareSizexOffsetyOffset 属性的值取决于棋盘的大小,我们将这些值绑定到棋盘的大小。这是我们棋盘的代码

我们的棋盘使用“bind”动态调整大小和重新定位

import javafx.scene.*;
import javafx.scene.paint.*;
import javafx.scene.shape.*;

public class Board extends Scene {
    def LIGHT_COLOR: Color = Color.web("lemonchiffon");
    def DARK_COLOR: Color = Color.web("brown");

    public-read var squareSize = bind {
        if(width > height) {
            height / 10;
        } else {
            width / 10;
        }
    }

    public-read var xOffset = bind {
        if (width > height) {
            (width - height) / 2;
        } else }
            0;
        }
    }

    public-read var yOffset = bind {
        if (width > height) {
            0;
        } else {
            (height - width) / 2;
        }
    }

    def board = [ Coord.A8, Coord.B8, Coord.C8, Coord.D8,
			Coord.E8, Coord.F8, Coord.G8, Coord.H8,
                  Coord.A7, Coord.B7, Coord.C7, Coord.D7,
			Coord.E7, Coord.F7, Coord.G7, Coord.H7,
                  Coord.A6, Coord.B6, Coord.C6, Coord.D6,
			Coord.E6, Coord.F6, Coord.G6, Coord.H6,
                  Coord.A5, Coord.B5, Coord.C5, Coord.D5,
			Coord.E5, Coord.F5, Coord.G5, Coord.H5,
                  Coord.A4, Coord.B4, Coord.C4, Coord.D4,
			Coord.E4, Coord.F4, Coord.G4, Coord.H4,
                  Coord.A3, Coord.B3, Coord.C3, Coord.D3,
			Coord.E3, Coord.F3, Coord.G3, Coord.H3,
                  Coord.A2, Coord.B2, Coord.C2, Coord.D2,
			Coord.E2, Coord.F2, Coord.G2, Coord.H2,
                  Coord.A1, Coord.B1, Coord.C1, Coord.D1,
			Coord.E1, Coord.F1, Coord.G1, Coord.H1 ];

    postinit {
        for (square in board) {
            def i: Integer = indexof square;
            insert Rectangle {
                fill: if (square.getIsWhite()) LIGHT_COLOR else DARK_COLOR
                x: bind xOffset + ((i mod 8) + 1) * squareSize
                y: bind yOffset + ((i / 8) + 1) * squareSize
                width: bind squareSize
                height: bind squareSize
            } into content;
        }
    }
}

Coord 只是一个 enum ,它让我更容易在棋盘上放置棋子。Coord 还跟踪该坐标处的方格是深色还是浅色方格。在国际象棋中,皇后总是放在她自己的颜色上,棋盘左下角的方格总是深色的。这有助于你避免将国王和皇后放错位置。JavaFX 不能创建 enum,所以 Coord 类是用 Java 编写的。这就是为什么你需要 Java 和 JavaFX 编译器来构建这个项目。你必须使用 Java 编译器构建 Coord.java 文件,然后才能使用 JavaFX 编译器构建 JavaFX 文件。

这个类使用了很多绑定。我们看到的第一个绑定是针对类属性 squareSize。在这种情况下,squareSize 绑定到一个表达式。如果表达式的值发生变化,squareSize 属性的值将自动更改为等于表达式的新值。此属性绑定的表达式是一个代码块。此代码块的值等于代码块中执行的最后一行。在这种情况下,执行的最后一行取决于 Scenewidthheight 属性的值。squareSize 的值将被设置为 height / 10;width / 10;

我们的 Board 类将放置在我们的 Stage 类(应用程序的主窗口)中。我们的 Board 将填充 Stage 的整个客户端区域,这基本上是整个窗口,不包括框架。如果我们更改窗口的大小,我们的 Board 类的 widthheight 属性将被修改。随着这些值的更改,JavaFX 运行时将自动为我们调整 squareSize 属性的值。

同样,xOffsetyOffset 属性也绑定到 widthheight 的值。

postinit 而不是构造函数

我们看到的下一组 binds 位于 postinit 代码块中。正如我之前所说,JavaFX 类不包含构造函数。您通常需要在对象首次实例化时执行代码。JavaFX 允许您通过使用 postinit 关键字指定在对象实例化时执行的代码。放置在 postinit 代码块中的代码将在对象实例化时自动执行,类似于 Java 中的构造函数调用。

Boards postinit 块中,我们为棋盘上的每个方块创建一个 Rectangle。棋盘上每个 Rectangle 的位置和大小都绑定到 squareSizexOffsetyOffset 的值。这是通过在每个方块使用的每个 Rectanglexywidthheight 属性之后放置 bind 关键字,并在 bind 关键字之后放置应绑定属性的表达式来完成的。

JavaFX 有一套新的访问修饰符

Board 类使用了一个 Java 编程语言中不存在的访问修饰符,public-read。JavaFX 使用了一套新的访问修饰符,我将立即描述

  • 默认 - 如果您不为变量或函数使用访问修饰符,则它默认为仅脚本访问。这意味着您只能从当前脚本文件内部访问变量/函数。JavaFX 没有 private 访问修饰符,JavaFX 中的默认访问修饰符等效于 Java 中的 private。(注意:我使用术语变量和函数,因为 JavaFX 不需要您使用类,但是如果您正在使用类,我想更好的术语是属性和方法。)
  • package - 这使得变量或函数可供同一包中的任何其他代码使用。这类似于 Java 中的默认访问修饰符(Java 不使用 package 关键字作为访问修饰符)。
  • protected - 这使得变量/函数可供同一包中的任何其他代码以及所有子类使用。Java 使用相同的关键字,其含义也相同。
  • public - 这使得变量/函数对所有人开放,拥有读写权限。Java 使用相同的关键字,含义也相同。
  • public-read - 我们在 Board 类中使用了此访问修饰符。此访问修饰符只能应用于变量,不能应用于函数。它授予每个人对变量的读取权限,同时限制脚本的写入权限。Java 没有可比较的访问修饰符。您可以通过将属性声明为 private 并提供一个检索属性值的 public 方法来在 Java 中获得相同的功能。下面的代码中给出了一个示例。
  • public-init - 标记为 public-init 的变量在对象首次创建时可以被任何人写入。对象创建后,它可以被任何人读取,但只能由定义该变量的脚本写入。与 public-read 访问修饰符一样,此 public-init 访问修饰符只能用于变量,不能用于函数。Java 中不存在等效的访问修饰符。您可以通过将属性声明为 private,提供一个检索属性值的 public 方法,并允许通过构造函数的参数设置属性的初始值来在 Java 中获得相同的功能。下面的代码中给出了一个示例

“public-read”和“public-init”JavaFX 代码翻译为 Java 代码

// JavaFX code
public-read size: Integer;
public-init height: Integer;

// Similar code in Java
public class MyClass {
    // only this class can write to the attribute size
    private int size;
    private int height;

    // other classes can only write to height through this constructor
    // after the object is created only this class can write to height
    public MyClass(int initHeight) {
        height = initHeight;
    }

    // everybody can read the value of size
	public int getSize() {
        return size;
    }

    // everybody can read the value of height
    public int getHeight() {
        return height;
    }
}

有一样事情我可以在 Java 中做到,但在 JavaFX 中却做不到。在 Java 中,我经常会有一个声明为 final 的类属性,这在 JavaFX 中可以使用 def 关键字做到。然而,在 Java 中,我可以通过构造函数传递一个值来赋予任何人设置此属性初始值的权限。在 JavaFX 中,你无法做到这一点。我尝试将变量声明为 public-init def,但如果我不立即初始化变量,就会产生错误。

在 JavaFX 中,您不能从另一个类初始化“def”(即 final)变量

// the following is valid Java code for which there
// seems to be no comparable code in JavaFX
public class Test {
    public final int i;

    public Test(int initI) {
        i = initI;
    }
}

// the following JavaFX code will not compile
public class Test {
    public-init def i: Integer;
}

def test: Test = Test { i: 7 }

// here is the error message
Test.fx:2: The 'def' of 'i' must be initialized with a value here.
				Perhaps you meant to use 'var'?
    public-init def i: Integer

1 error

// if I initialize the attribute i, I get more error messages
class Test {
    public-init def i: Integer = 4;
}

def test: Test = Test { i: 7 }

// here are the error messages
Test.fx:2: modifier public-init not allowed on a def
    public-init def i: Integer = 4;

Test.fx:5: You cannot change the value(s) of 'i'
	because it was declared as a 'def', perhaps it should be a 'var'?
    def test: Test = Test { i: 7 }

2 errors

能够将变量声明为 public-init def 似乎很有用。我希望将其用于我的棋子,在创建棋子时将其初始化为黑色或白色。一旦棋子的颜色设置好,就不能再更改,所以我想将其声明为 def,但未能做到。我不得不将其声明为 public-init var,并且必须确保在首次初始化后不再更改它。这是 JavaFX 中第二个似乎错误的設計决定。(第一个是决定将 Stage 类的 xywidthheight 属性声明为 Numbers 而不是 Integers。)

SVG 棋子

我在这个国际象棋程序中使用的棋子来自 openclipart.org。这个网站有大量可供您在应用程序中使用的开源 SVG 图形。非常感谢他们为我的程序提供了漂亮的图形,因为我并不是一个艺术家。我最初的印象是 JavaFX 可以直接加载 SVG 图形。事实并非如此。为了使用这些图形,我必须将它们从 SVG 转换为 JavaFX 图形格式。为了将 SVG 转换为 JavaFX 图形,您需要从 javafx.com 下载 JavaFX 1.1 Production Suite。您可以使用此工具套件附带的“SVG to JavaFX Graphics Converter”程序将 SVG 图形转换为 JavaFX 图形。这是此程序中使用的棋子的屏幕截图

使用的 SVG 棋子图形

Screen shot of SVG Chess Pieces.

加载和显示棋子

当尝试加载我的 JavaFX 图形格式的棋子时,我遇到了另一个 JavaFX 错误。根据文档,我应该能够使用以下代码加载棋子

此代码应该加载 JavaFX 图形文件,但它不起作用

// load the JavaFX graphics file
var pieceNode = FXDLoader.load("{__DIR__}res/WKing.fxz");
// add the graphic to a Group object
var group = Group {
    content: [
        pieceNode
    ]
}
// add the graphic to the board
insert group into board.content

上面列出的代码不起作用。__DIR__ 是一个特殊常量,它返回包含当前 JavaFX 源文件的目录的 URL。在我看到的所有示例代码中,JavaFX 程序的所有资源(图形、图标等)都是相对于 __DIR__ 访问的。在大多数情况下,这都有效。例如,要加载图像,您可以输入如下代码

在 JavaFX 中加载图像

var myImage = Image { url: "{__DIR__}images/SampleImage.jpg" backgroundLoading: true};

但是,如果您尝试使用 FXDLoader 类通过此方法加载图像,它将失败。问题是 __DIR__ 返回的值是 URL。这意味着空格会转换为 %20。FXDLoader 会因文件未找到消息而失败。解决此问题的方法是将 %20 转换回空格,如下面的代码所示

加载 JavaFX 图形的修复

// bug fix, convert %20 to space so FXDLoader can load the graphics
def rootDir = "{__DIR__}".replaceAll("%20", " ");

// load the JavaFX graphics file
var pieceNode = FXDLoader.load("{rootDir}res/WKing.fxz");
// add the graphic to a Group object
var group = Group {
    content: [
        pieceNode
    ]
}
// add the graphic to the board
insert group into board.content

所以我又提交了一个错误报告,说 FXDLoader 需要更新以像 Image 类一样处理 URL。Sun 的示例代码建议使用 __DIR__ 加载其他资源。它应该与使用 FXDLoader 类的 JavaFX 图形对象一样工作。

JavaFX 中的自定义鼠标光标

最好尽快总结一下,这篇文章有点长了。我做的最后一件事可能对 JavaFX 程序员有用,那就是加载自定义鼠标光标。我希望我的国际象棋程序突出显示鼠标悬停的棋子,并将鼠标光标更改为下图所示的张开手,并在单击并拖动棋子时更改为闭合手。如果棋子掉落,我还会突出显示棋子将放置的方格。这在用户悬停在方格角上时很有用,如果没有此 UI 提示,您可能不知道棋子将掉落到哪个方格中。

鼠标悬停和鼠标拖动时高亮棋子和自定义光标

Screen shot highlighted pieces and custom cursors on mouse over and mouse drag.

JavaFX 有一套自己的默认光标可供使用。如果您想创建自己的自定义光标,您必须为您的自定义光标创建一个新的 java.awt.Cursor 对象,如下面的代码所示

自定义光标

import java.awt.*;
import java.net.*;
import javax.swing.*;
import javafx.scene.Cursor;

public class CustomCursor extends Cursor {
    public-init var imageURL: String;
    public-init var cursorName: String;

    public override function impl_getAWTCursor(): java.awt.Cursor {
        var toolkit = Toolkit.getDefaultToolkit();
        var image: Image = toolkit.getImage(new URL(imageURL));
        var point: Point = new Point(16, 16);
        var cursor: java.awt.Cursor =
		toolkit.createCustomCursor(image, point, cursorName);
    }
}

这段代码有点令人困惑,因为我们使用了两个不同的 Cursors。首先,我们的新类 CustomCursor 扩展了 JavaFX Cursor 类 (javafx.scene.Cursor) 类。其次,我们的类创建了一个新的 Java Cursor 对象 (java.awt.Cursor)。所以这是两个不同的 Cursor 类。我注意到的一件事是,我正在重写的 impl_getAWTCursor() 方法没有列在 JavaFX 1.1 API 文档中。这可能是因为 JavaFX API 文档目前很糟糕(感觉远不如 Java API 文档完整),或者这可能是一个不应被重写的隐藏 API。

就 JavaFX API 文档比 Java API 文档差而言,我想这是意料之中的。Java 存在的时间更长,是一种更成熟的语言。但是,我确实对 JavaFX API 文档有一点非常不喜欢。在 Java 文档中,所有类都列在单个 HTML 窗格中。因此,如果我正在搜索特定的类,我可以在浏览器中轻松找到它。在 JavaFX 中,如果您知道要查找的类名,但不知道它所在的包,那么在文档中查找起来可能会非常困难。在 JavaFX API 文档中,他们不会在侧边窗格中列出所有类(像 Java 文档那样),而是列出所有包名。您必须单击包名才能查看该包中的类。因此,如果您想查找 Cursor 类的文档,但不知道它在 javafx.scene 包中,那么查找起来可能会很困难。文档可能看起来更漂亮,但它们并没有更实用。

回到加载我们的自定义光标。以下代码将我们的自定义光标加载到国际象棋程序中

加载自定义光标

def rootDir = "{__DIR__}".replaceAll("%20", " ");
def grabCursor = CustomCursor {
    imageURL: "{rootDir}res/grab.png"
    cursorName: "Grab"
}
def grabbingCursor = CustomCursor {
    imageURL: "{rootDir}res/grabbing.png"
    cursorName: "Grabbing"
}

var currentCursor = Cursor.Default;
// Inside of the Piece class we create custom mouse listener
// functions to change the cursor
onMouseEntered: function(e: MouseEvent) {
    currentCursor = grabCursor;
    board.cursor = currentCursor;
}
onMouseExited: function(e: MouseEvent) {
    currentCursor = Cursor.Default;
    board.cursor = currentCursor;
}
// similar for other mouse functions released, pressed, clicked

再见

就这样。我发现 JavaFX 是一种有趣的语言,但它感觉有 bug。我发现了一些 bug,一些设计决策似乎是错误的,API 文档不如 Java 的文档完整,而且网上存在的许多代码都是为早期版本编写的,不适用于最新版本(1.1)的 JavaFX。考虑到 bug 的数量以及该语言与早期版本相比发生了很大变化,您可能需要等到产品成熟后再使用 JavaFX 构建主要应用程序。但是,如果您只是编写一个简单的国际象棋应用程序(或其他简单的程序),它是一种有趣的语言。

我的第一篇文章!

我已经成为 CodeProject 会员近 9 年了,我终于发表了我的第一篇文章。我希望能在今年五月毕业(计算机科学硕士)。我长期以来一直全职工作,同时兼职上学。希望毕业后我能有更多时间在 CodeProject 上写更多的文章。我希望您喜欢这篇文章,如果您喜欢,请投我一票。

历史

  • 1.0 版 - 2009 年 4 月 16 日
© . All rights reserved.