Введение в JavaFX – Модель шахмат

ОГЛАВЛЕНИЕ

Пример снимка экрана программы «Шахматы», написанной на JavaFX

Введение

Компания Sun недавно выпустила новый язык программирования для платформы Java, называемый JavaFX. Его главной задачей является облегчение разработки сетевых приложений с широкими возможностями (RIA), которые могут запускаться на различных устройствах, включая компьютеры, мобильные телефоны и Blu-ray проигрыватели. Чаще всего его сравнивают с новыми языками программирования сетевых приложений с широкими возможностями (RIA) от Microsoft (Silverlight) и Adobe (AIR). JavaFX не ограничен областью создания RIA. В этой статье описана разработка приложения «Шахматы», запускающегося на рабочем столе. Приложение называется моделью шахмат, потому что использованный алгоритм всего лишь выбирает ходы случайным образом. Так что если вы не сможете победить эту шахматную программу, значит вы худший игрок в шахматы. И это весьма плохо. Вероятно, в будущем будет написана лучшая программа для игры в шахматы и  появится статья под названием «Умные шахматы».

Предпосылки 

Наилучшим способом самостоятельного изучения JavaFX будет начало разработки программного проекта на тему, которой вы интересуетесь, и завершение проекта на новом языке программирования. Примерно в течение месяца мы писали программу «Шахматы» на JavaFX. Эта статья описывает опыт обучения и дает введение в основы программирования на JavaFX.

Многие ожидают, что JavaFX будет похож на Java, только с новым набором основных классов, которые нужно будет изучить. Они ошибаются. JavaFX - это полностью отличающийся язык программирования. Чтобы помочь опытным программистам Java, читающим данную статью, мы привели примеры преобразования кода из JavaFX в Java, что помочь вам лучше понять пример кода JavaFX, приведенный в этой статье.

Использование кода

Мы используем интегрированную среду разработки (IDE) при работе (Eclipse и Visual Studio) и действительно получаем  удовольствие от работы с ними. Однако для данного проекта мы решили не использовать IDE. Ниже описывается, какие программные продукты вам нужно будет загрузить, в зависимости от того, используете вы командную строку или IDE:

  • Чтобы скомпоновать программу «Шахматы» из командной строки, вы должны загрузить JavaFX 1.1 SDK.
  • Если вы предпочитаете работать в IDE, вы можете загрузить NetBeans IDE 6.5 для JavaFX 1.1.
  • Если вы предпочитаете Eclipse, а не NetBeans, то для Eclipse доступен плагин.
  • Чтобы преобразовать 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. Если вы выполняете компоновку с помощью командной строки, то вам потребуется скачать последнюю версию JDK по адресу java.sun.com.

Чтобы скомпоновать исходный код из командной строки и запустить программу «Шахматы», введите следующие команды в командную строку (выполняйте это из директории, которая содержит исходный код):

Компиляция и запуск программы «Шахматы»

javac *.java
javafxc *.fx
javafx Main

javac – это компилятор командной строки, который компилирует файлы исходного кода Java, javafxc - это компилятор командной строки, который компилирует файлы исходного кода JavaFX, и javafx запускает программу «Шахматы» с помощью запуска специально определенного файла JavaFX (в данном случае начинает выполняться код, скомпилированный из файла Main.fx).


Точки интереса

JavaFX - это очень молодой язык программирования и определенно  воспринимается как бета-версия продукта. Раньше мы никогда не находили столько ошибок в языке программирования, и при этом использовали его всего лишь в течение месяца для написания одного приложения. Нам удалось найти способы обойти некоторые ошибки, но не все из них. Мы так и не смогли изменить иконку для приложения.

Язык подает немалые надежды,  в числе которых и то, что эти ошибки будут устранены, когда продукт разовьется до серьезного уровня. Нам нравиться функция связывания, и создавать GUI (Графический интерфейс пользователя) нам  было намного легче по сравнению с первым опытом создания GUI в Java. Java – это очень развитый язык (он существует почти десятилетие), и для Java было написано очень много кода. Было бы хорошо иметь возможность создавать экземпляр этой большой библиотеки существующего кода Java непосредственно из кода JavaFX.

Если вы решите заниматься программированием на JavaFX, учтите, что много примеров кода на JavaFX, найденных  нами в Google, не работают в последней версии JavaFX (v. 1.1). JavaFX значительно изменился с середины 2008 года. Многие статьи, опубликованные до середины 2008 года, не работают в версии 1.1, при этом большая часть кода, опубликованная в конце 2008-го и в 2009 годах, работает прекрасно.

Начало работы

Давайте начнем с простой программы Hello World (здравствуй мир) и постепенно приступим к созданию шахматной программы. Как вы вскоре увидите, фактически программа Hello World научит вас многому. Здесь приводится код для Hello World на JavaFX:

Программа Hello World, написанная на JavaFX

println("Hello World!");

Это всего лишь одна строка кода. Простая программа, состоящая из одной строки, многому нас научит. Эквивалентная программа на Java выглядит так:

Программа Hello World, написанная на Java

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

Отсутствие необходимости в main или классе

Чему данный пример может научить? Если вы раньше работали с Java, то вы привыкли помещать все в классы. Потому что в Java никакой код не может существовать вне класса. Как видите, в JavaFX это не так.

В Java требуется создавать метод main, который объявляется как public static void, принимающий единственный параметр String[] args (аргументы командной строки). В JavaFX это не требуется. Вы просто помещаете инструкции в файл сценария JavaFX и запускаете файл. Метод main не требуется.

Если вам нужно получить доступ к аргументам командной строки, вы можете сделать это с помощью специальной функции run. run – это специальная функция, служащая главной точкой входа в сценарий. Функция run выглядит примерно так:

"run" – это вспомогательная функция, служащая главной точкой входа в сценарий JavaFX

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

Эта программа просто выводит на экран первый аргумент командной строки. Удивительно, но она выполняется без ошибки: она просто выводит новую строку, если не было передано ни одного аргумента командной строки.

Последовательности против массивов

Давайте продолжим обсуждение функции run. Она принимает единственный параметр, являющийся последовательностью объектов типа String и называющийся args. Последовательности в 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 создает новую последовательность, когда элементы вставляются в или удаляются из последовательности. Таким же образом работают Strings в Java. Вы можете изменять 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]

Цикл for в JavaFX работает только с последовательностями. Он имеет вид for(имя(  )Переменной in. Следующий код показывает, как преобразовать простой цикл Java for в цикл JavaFX: последовательность) { ... }

Различия между циклами "for" в Java и JavaFX

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

// эквивалентный цикл for JavaFX
for(i in [0..9]) {
    println("Hello World!");
}

В JavaFX можно использовать ключевое слово indexof, чтобы получить порядковый номер элемента в последовательности. Например:

Организация цикла по числовой последовательности и использование ключевого слова "indexof"

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

// Вывод
0 = 100
1 = 102
2 = 104
3 = 106
4 = 108
5 = 110

Переменные и параметры

Можно увидеть два различных вида объявления переменных/параметров в коде JavaFX. Первый вид имеет параметр args в функции run (объявленный как тип String[]) и второй вид был у переменной colors в примере последовательности (нет объявления типа). Все переменные и параметры должны быть объявлены в JavaFX, однако указывать тип необязательно. Если тип переменной не объявляется, среда выполнения определяет его на основе типа объекта, назначенного этой переменной. Многие предпочитают указывать тип переменной. Это можно сделать, поместив символ : после имени и вслед за двоеточием указать тип переменной. Например, так можно назначить тип переменной colors:

Объявлять тип переменной необязательно

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

Параметры функций просто объявляются с именем переменной и необязательным типом. Переменные объявляются как var или как def. Различие между двумя видами объявления заключается в том, что vardef нельзя изменить. Переменная, объявленная как def, сходна с переменной, объявленной как final в Java. Ниже приводится правильный код: можно изменить, а

Переменные, объявленные как "def", можно изменить

// правильный код
var i = 7;
i = 2;

Однако этот код не будет компилироваться:

Переменные, объявленные как "def", нельзя изменить

// неправильный код, не будет компилироваться
def i = 7;
i = 2;

Параметры функций не могут содержать ключевые слова var или def. Все параметры функций рассматриваются как defs, и поэтому их нельзя изменить.


Функции

Все функции должны объявляться с использованием ключевого слова function. Для нас это требование было довольно неожиданным. Языки сценариев обычно известны своей краткостью. Это ключевое слово не кажется необходимым и является довольно большим, целых 8 символов. Компилятор должен иметь возможность распознать функцию, когда он видит ее.

Каждая функция имеет возвращаемый тип (тип возвращаемого результата). Если не объявлять возвращаемый тип, по умолчанию он будет равен Void. Это не опечатка, в Java void записывается с 'v' в нижнем регистре, при этом в Javafx Void записывается с заглавной буквы 'V'.

Возвращаемые функцией типы помещаются по аналогии с типами в переменные и параметры. Возвращаемый тип объявляется путем помещения символа двоеточия  : после закрывающей круглой скобки, вслед за которой указывается возвращаемый тип. Например, следующая функция возвращает Integer (целое число):

Функция в JavaFX, возвращающая "Integer"

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

Встроенные типы данных

Как видно из последнего примера кода, ключевое слово Integer представляет целочисленный тип данных. В Java это эквивалентно int. Если вам нужна десятичная дробь, можно использовать встроенный тип Number,Byte, Short, Number, Integer, Long, Float, Double, и Character (все начинаются с заглавной буквы). который представляет число с плавающей точкой. Обычно есть два числовых типа, которые можно использовать, но если вам нужны переменные определенного размера, вы можете использовать любой из следующих встроенных числовых типов:

Другие встроенные типы данных являются Boolean (логическими), они подобны bool в Java, Duration,String. При присвоении значения типу Duration type по существу указывается количество времени вместе с единицами его измерения. Например: обозначающий продолжительность времени, и

Различные "durations" (длительности) в JavaFX

100ms;  // равняется 100 миллисекундам
200s;   // 200 секунд
300m;   // 300 минут
400h;   // 400 часов

Strings действует одинаково в JavaFX и в Java. Объекты JavaFX String имеют такие же методы, что и объекты Java String, но конкатенация (соединение) строк Strings действует по-разному. Следующие примеры показывают операции над строками String в Java вместе с эквивалентными операциями в JavaFX:

Объединение строк "String" в Java и в JavaFX

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

// эквивалентный код JavaFX
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: где на экран выводился первый аргумент командной строки с помощью кода

Преобразование выражений к типу "Strings" в JavaFX

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

def i = 7;
println("Значением foo({i}) является {foo(i)}");

{i} преобразует значение i к типу String, в то время как {foo(i)} преобразует результат foo(i) к типу String. Ониобъединяются со Strings (строкой) "The value of foo(", ") is ", and ")". Эквивалентный код на Java:

Преобразование выражений к типу "Strings" в Java

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

int i = 7;
System.out.println("Значением foo(" + i + ") является " + Integer.toString(foo(i)));

Возврат дополнительного ключевого слова

В случае если вы не заметили, мы написали две функции foo(), каждая из которых возвращает Integers. Однако никакая из функций не использует ключевое слово return. Ключевое слово return не обязательно в JavaFX. Если вы не включите ключевое слово return, возвращается значение последнего выполненного выражения. Можно использовать ключевое слово return так же, как в следующем примере:

Вы можете использовать ключевое слово "return", если хотите

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

def i = 7;
println("Значением foo({i}) является {foo(i)}");


Графический интерфейс пользователя

Давайте теперь приступим к созданию шахматной программы. Сначала необходимо создать окно для нашего приложения. В Java для этого нужно использовать класс JFrame. Эквивалентным классом в JavaFX является Stage. Ниже показано, как окно создается в Java и в JavaFX:

Создание окон в Java и в JavaFX

// код Java
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
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 содержит множество ошибок

Ниже приводятся скриншоты каждого из двух приложений. Java приложение имеет пользовательскую иконку с изображением шахмат, в то время как JavaFX приложение имеет заданную по умолчанию иконку Java приложения. Это первая из тех нескольких ошибок, которые мы нашли в JavaFX. Невозможно загрузить пользовательскую иконку в приложение на JavaFX. Есть форумы, в которых обсуждаются ошибки - bug1 и bug2. Согласно документации JavaFX 1.1 docs, вы должны иметь возможность установить иконку для Stage. К сожалению, это не работает. Так что придется оставить иконку Java по умолчанию. Есть надежда, что эта проблема вскоре будет разрешена.

JavaFX версии 1.1 все еще содержит много ошибок, к примеру, вы не можете изменить иконку для главного окна.

Создание объектов

В Java новые классы создаются с помощью ключевого слова new. В JavaFX вы не должны использовать ключевое слово new. Обычно новый объект создается путем присваивания переменной имени класса и, после этого, присваивания начальных значений атрибутам класса внутри фигурных скобок ({}). Начальное имя атрибута класса устанавливается путем указания имени атрибута, вслед за ним указывается символ двоеточия : , а после двоеточия указывается начальное значение атрибута класса. Вы можете помещать запятые между каждым присвоением значений атрибутам класса, но это необязательно. В предыдущем примере запятые используются только между атрибутами, которым мы присваиваем значения в одной и той же строке (атрибуты x, y, width и height). Атрибут icons является последовательностью, поэтому мы помещаем иконку для Stage внутрь квадратных скобок []. Иконка является экземпляром объекта Image. Экземпляр этого класса создается аналогичным образом путем присвоения начальных значений атрибутам класса внутри фигурных скобок. В этом случае мы записываем в атрибут url местоположение графического файла, используемого в качестве иконки. Атрибут onClose инициализируется значением указателя на функцию. Функция просто вызывает 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. По-нашему, код выглядит понятнее, когда мы создаем объекты без него и присваиваем  начальные значения всем атрибутам класса, используя закрывающие круглые скобки, {}, как показано в первом примере.


Сохранение настроек приложения

Мы всегда предпочитаем сохранять настройки приложения в домашнем каталоге пользователя. В случае приложений с графическим пользовательским интерфейсом самое меньшее, что нужно сделать - это сохранить местоположение и размер главного окна приложения. Java существует в течение долгого времени и уже имеет набор классов, помогающих выполнить эту задачу. Давайте рассмотрим, как это делается в Java, и затем сделаем то же самое в JavaFX.

Сохранение настроек приложения в Java

// код Java
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) {
        // получаем домашний каталог пользователя
        String homeDir = System.getProperty("user.home");

        // они объявлены как final, так что к ним можно получить доступ

        // во внутреннем анонимном классе ниже
        final String settingsFilename =
        homeDir + File.separator + "mySettings.properties";
        final Properties props = new Properties():

        // Загрузка сохраненных настроек
        try {
            FileInputStream input = new FileInputStream(settingsFilename);
            props.load(input);
            input.close();
        } catch(Exception ignore) {
            // исключение игнорируется, поскольку ожидалось, что
       // файл установочных параметров иногда может не существовать
            // при первом запуске приложения он точно не будет существовать
        }

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

        // похожий код для загрузки savedY, savedWidth, savedHeight пропущен для краткости

        // Создаем и отображаем окно, такой же код, как и выше, исключая то, что при закрытии ничего не делается
        // Также делаем JFrame конечным, чтобы к нему можно было получить доступ
        // во внутреннем анонимном классе
        final JFrame frame = new JFrame("Chess");
        frame.setBounds(savedX, savedY, savedWidth, savedHeight);
        // отличается от предыдущего примера
        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) {
                // Сохраняем настройки при выходе
                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) {
                    // если не получается сохранить настройки,
               // в следующий раз будут использоваться настройки по умолчанию
                }

                // выход из приложения
                System.exit(0);
            }
        });
    }
}

Свойства

Класс Properties Java используется для сохранения настроек приложения. Этот класс содержит словарь, состоящий из пар ключ/значение. Метод load используется для загрузки сохраненных свойств, и метод storegetProperty вызывается, чтобы получить значение для определенного ключа. Также передается значение параметра по умолчанию, это значение возвращается, если не установлен ключ в объекте Properties. Мы сохраняем properties, используя метод setProperty. Это один из нескольких способов сохранения настроек приложения в Java. используется для сохранения свойств. Метод

WindowListener (слушатель окна)

Нужно загрузить настройки приложения перед отображением окна, потому что требуется установить сохраненную позицию и местоположение окна. Можно сохранить настройки приложения, добавив WindowListener в JFrame. WindowListener будет отправлено сообщение, когда пользователь попытается закрыть окно приложения. Когда это происходит, размер и местоположение JFrame сохраняются в объекте Properties и затем сохраняются в домашнем каталоге пользователя. Можно выйти из приложения, вызвав System.exit(0).

Сохранение настроек в JavaFX

Можно использовать похожие классы для сохранения настроек приложения в нашем JavaFX классе. Единственное отличие состоит в том, что не требуется добавлять WindowListener, просто нужно обновить функцию onClose класса Stage, чтобы сохранить настройки приложения перед выходом из приложения. Это показано в следующем фрагменте кода:

Сохранение настроек приложения в JavaFX

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

// ищем файл настроек в домашнем каталоге пользователя
def homeDir = System.getProperty("user.home");
def settingsFile = "{homeDir}{File.separator}"mySettings.properties";

// загружаем настройки в класс Properties
def props: Properties = new Properties();
try {
    def input: FileInputStream = new FileInputStream(settingsFile);
    props.load(input);
    input.close();
} catch(ignore: Exception) {
// если файл не существует, будут использоваться значения по умолчанию
}

// чтение сохраненных настроек
var savedX: Number;
try {
    savedX = Double.parseDouble(props.getProperty("xPos", "100"));
} catch(e: NumberFormatException) {
    savedX = 100;
}
// похожим образом загружаем настройки savedY, savedWidth и savedHeight

// создаем окно, как и раньше, только добавив вызов saveSettings() в функцию onClose

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) {
        // если настройки не получается сохранить, в следующий раз будут использоваться настройки по умолчанию
    }
}

Как видите, можно использовать такие же классы Java для сохранения настроек приложения в JavaFX, как и используемые в Java приложениях. Это одна из лучших возможностей JavaFX, можно использовать большую библиотеку существующих классов Java в программах на JavaFX. Опыт работы с Java Framework можно применить для программирования на JavaFX.

Нужно заметить, что атрибуты x, y, width и height класса Stage все определены как Numbers. Поэтому, если мы сохраняем их на диске, они все сохраняются с десятичной точкой. Это кажется странным, так как размер и местоположение объекта Stage задается в виде числа пикселей. Не имеет смысла использовать десятичную дробь, так как задаются только целые пиксели, 1.5 пикселя не имеет смысла, может быть только 1 или 2 пикселя. Неизвестно, ошибка это или нет. Мы уже отправляли два сообщения об ошибках в JavaFX, а потому  не уверены,  есть ли смысл в данном случае отправлять другое сообщение об ошибке. Однако кажется странным решение использовать Number вместо Integer (целое число).


Программа «Шахматы»

После создания окна следующим шагом является создание шахматной доски. Мы создадим пользовательский компонент шахматной доски для графического пользовательского интерфейса на JavaFX. Если бы мы делали это в Java, наша шахматная доска расширяла бы JPanel и мы бы добавили эту JPanel к нашей JFrame. В JavaFX наша шахматная доска будет расширять Scene и будет добавлена к нашему объекту Stage.

Перед созданием класса Board extends (расширяет) Scene нужно рассказать немного о доске, которую мы хотим создать. При создании программы на JavaFX мы решили использовать преимущество, предоставляемое большими возможностями нового языка для работы с графикой. В этом случае нам стоит использовать графику SVG для наших шахматных фигур, чтобы они хорошо выглядели в любом разрешении. Пользователь должен иметь возможность изменять размер шахматной доски, и она должна выглядеть хорошо независимо от расположения границ главного окна приложения. Следующий скриншот объясняет, как должна выглядеть доска при изменении размеров главного окна.

Шахматная доска изменяет свои размеры и положение в зависимости от размеров главного окна

Доска пытается занять как можно большую часть окна, насколько это возможно при сохранении квадратной формы. Если ширина окна больше, чем его высота, или его высота больше, чем ширина, то доска центрируется в окне. Если окно имеет квадратную форму, доска не займет окно полностью, но займет большую часть окна. При этом вокруг доски всегда остается небольшая рамка. Ширина и высота этой рамки по крайней мере равняются размеру одного квадрата на шахматной доске. Так что доска имеет следующие 3 атрибута:

  • squareSize – Размер одного квадрата на шахматной доске
  • xOffset – Если ширина окна больше, чем его высота, этот атрибут говорит о том, насколько далеко нужно передвинуть доску, чтобы расположить ее по центру окна
  • yOffset – Если высота окна больше, чем ширина, этот атрибут говорит о том, насколько далеко нужно передвинуть доску, чтобы расположить ее по центру окна.

Связывание в JavaFX

Связывание – это новая возможность в JavaFX, позволяющая связать значение одной переменной с другой переменной или связать значение метода с другой переменной. Связывание выполняется с помощью ключевого слова bind. Так как значения атрибутов squareSize, xOffset и yOffset зависят от размера шахматной доски, свяжем их значения с размером шахматной доски. Ниже приводится код для шахматной доски:

Шахматная доска использует "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 для компоновки этого проекта. Нужно скомпоновать файл Coord.java, используя компилятор Java, перед компоновкой файлов на JavaFX с помощью компилятора JavaFX.

Этот класс использует множество связываний. Первое связывание применяется для атрибута класса class squareSize. squareSize в данном случае связывается с выражением. Если значение выражения изменяется, значение атрибута squareSize будет автоматически изменяться на соответствующее новому значению выражение. Выражение, с которым связан этот атрибут, является блоком кода. Значение этого блока кода равняется последней строке, выполненной в блоке кода. В данном случае, последняя выполненная строка зависит от значений атрибутов width и height объекта Scene. Значение squareSize будет установлено в height / 10; или width / 10;.

Класс Board будет помещен в класс Stage (главное окно приложения). Board будет заполнять всю клиентскую область Stage, обычно это целое окно, исключая рамку. Если размер окна изменяется, то атрибуты width и height класса Board будут изменяться. Когда их значения изменяются, среда выполнения JavaFX будет автоматически изменять значение атрибута squareSize.

Аналогично, атрибуты xOffset и yOffset связаны со значениями width и height.

postinit вместо конструкторов

Следующие связывания можно увидеть в блоке кода postinit. Как мы говорили раньше, классы JavaFX не имеют конструкторов. Однако всегда нужно выполнять какой-то код при первоначальном создании объекта. JavaFX позволяет определить код, который будет выполняться при создании объекта, с помощью использования ключевого слова postinit. Код, помещенный в блок кода postinit, будет автоматически выполняться при создании объекта, подобно вызову конструктора в Java.

В блоке Boards postinit создается Rectangle (прямоугольник) для каждого квадрата на шахматной доске. Местоположение и размер каждого из Rectangles на доске связаны со значениями squareSize, xOffset и yOffset. Это делается с помощью помещения ключевого слова bind после атрибутов x, y, width и height каждого из Rectangles, используемых для каждого из квадратов, и помещения выражения, с которым атрибуты должны быть связаны, после ключевого слова bind.

JavaFX имеет новый набор модификаторов доступа

Класс Board использует модификатор доступа, который не существует в языке программирования Java, public-read. JavaFX использует новый набор модификаторов доступа, описанный ниже:

  • default (по умолчанию) – Если вы не используете модификатор доступа для переменной или функции, по умолчанию устанавливается доступ только для сценария (script-only). Это означает, что можно получить доступ к переменной/функции только в пределах текущего файла сценария. JavaFX не имеет модификатора доступа private, модификатор доступа по умолчанию в JavaFX эквивалентен private в Java. (Замечание: мы используем термины «переменная» и «функция», потому что в JavaFX не требуется использовать классы, однако если вы используете класс, лучше применять термины «атрибут» и «метод».)
  • package – Этот модификатор делает переменную или функцию доступной для любого другого кода в этом же пакете. Это аналогично модификатору доступа по умолчанию в Java (Java не использует ключевое слово package как модификатор доступа).
  • protected - Этот модификатор делает переменную или функцию доступной для любого другого кода в этом же пакете и также для всех подклассов. Java использует такое же ключевое слово, имеющее такое же значение.
  • public - Этот модификатор делает переменную или функцию доступной в любом месте для доступа с целью чтения или записи. Java использует такое же ключевое слово, имеющее такое же значение.
  • public-read – Этот модификатор доступа используется в классе Board. Этот модификатор доступа можно применять только к переменным, его нельзя применять к функциям. Он открывает доступ к переменной с целью чтения из любого места, ограничивая доступ с целью записи рамками данного сценария. Java не имеет подобного модификатора доступа. Можно получить такую же функциональность в Java, объявив атрибут private и предоставив метод public, извлекающий значение атрибута. Пример этого показан в коде ниже.
  • public-init – Переменная, обозначенная как public-init, может записываться из любого места при первоначальном создании объекта. После создания объекта ее можно прочитать из любого места, но запись в переменную возможна только с помощью сценария, в котором определяется переменная. Как и модификатор доступа public-read, модификатор доступа public-init можно использовать только для переменной, но нельзя использовать для функции. Эквивалентный модификатор доступа не существует в Java. Вы можете получить такую же функциональность в Java, объявив атрибут private и предоставив метод public, извлекающий значение атрибута, и позволив устанавливать начальное значение атрибута через параметр, передаваемый конструктору. Пример этого дается в коде ниже:

Код "public-read" and "public-init" на JavaFX, преобразованный в код Java

// код JavaFX
public-read size: Integer;
public-init height: Integer;

// Такой же код на Java
public class MyClass {
    // только этот класс может записывать в атрибут size
    private int size;
    private int height;

    // другие классы могут записывать только в атрибут height с помощью этого конструктора
    // после создания объекта только этот класс может записывать в атрибут height
    public MyClass(int initHeight) {
        height = initHeight;
    }

    // любой может прочитать значение атрибута size
    public int getSize() {
        return size;
    }

    // любой может прочитать значение атрибута height
    public int getHeight() {
        return height;
    }
}

Есть одна вещь, которую мы можем сделать в Java, но которую не смогли сделать в JavaFX. В Java мы всегда можем объявить атрибут класса как final, в JavaFX можем это сделать, используя ключевое слово def. Однако, в Java можно позволить любому устанавливать начальное значение этого атрибута, передавая значение в конструктор. В JavaFX этого сделать нельзя. Мы пытались объявить переменную как public-init def, но это вызывает ошибку, если не присвоить значение переменной в том же месте.

В JavaFX вы не можете инициализировать переменную "def" (то есть final) из другого класса

// ниже приведен корректный код Java, для которого
// не существует аналога в JavaFX
public class Test {
    public final int i;

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

// следующий код в JavaFX не будет компилироваться
public class Test {
    public-init def i: Integer;
}

def test: Test = Test { i: 7 }

// здесь есть сообщение об ошибке
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

// если устанавливать значение атрибута i, получается еще больше сообщений об ошибке
class Test {
    public-init def i: Integer = 4;
}

def test: Test = Test { i: 7 }

// здесь есть сообщения об ошибке
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, которое кажется нам неправильным. (Первым было решение объявить атрибуты x, y, width и height класса Stage как NumbersIntegers.) вместо


Шахматные фигуры SVG

Шахматные фигуры, которые мы использовали в этой шахматной программе, взяты по адресу openclipart.org. Этот сайт имеет хороший выбор масштабируемой векторной графики (SVG) с открытым исходным кодом, которую вы можете использовать в своих приложениях. Благодаря им мы смогли использовать красивую графику в своем приложении, не являясь художниками. Все были бы впечатлены, если бы JavaFX позволял непосредственно загружать графику SVG. Но это не так. Чтобы использовать графику, нам пришлось преобразовать ее из SVG в графический формат JavaFX. Чтобы преобразовать SVG в графику JavaFX, нужно загрузить JavaFX 1.1 Production Suite по адресу javafx.com. Для преобразования графики SVG в графику JavaFX, нужно использовать программу "SVG to JavaFX Graphics Converter", поставляемую вместе с набором средств. Здесь приводится скриншот шахматных фигур, используемых в программе:

Используемые шахматные фигуры в графике формата SVG

 

Загрузка и отображение шахматных фигур

Мы обнаружили очередную ошибку в JavaFX, пытаясь загрузить графические изображения шахматных фигур в формате JavaFX. Согласно документации, можно загрузить фигуры с помощью следующего кода:

Этот код должен загружать графические файлы JavaFX, но он не работает

// Загрузка графического файла JavaFX 
var pieceNode = FXDLoader.load("{__DIR__}res/WKing.fxz");
// добавление (в) графики в объект Group
var group = Group {
    content: [
        pieceNode
    ]
}
// добавление графики к доске
insert group into board.content

Приведенный выше код не работает. __DIR__ является специальной константой, возвращающей URL каталога, в котором содержится текущий файл исходного кода JavaFX. Во всех примерах кода, которые мы видели, все ресурсы (графика, иконки и т.д.) для программ на JavaFX доступны относительно __DIR__. Это работает в большинстве случаев. Например, для загрузки изображения вы можете использовать следующий код:

Загрузка изображения в JavaFX

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

Однако если попытаться загрузить изображение с помощью класса FXDLoader, используя этот метод, произойдет ошибка. Проблема в том, что значение, возвращаемое __DIR__ , является URL. Это означает, что пробелы преобразуются в %20. FXDLoader выдает сообщение об ошибке, говорящее, что файл не найден. Для решения этой проблемы нужно преобразовать 20% обратно в пробелы, как это делается в следующем коде:

Решение проблемы с загрузкой графики в JavaFX

// решение проблемы, преобразование 20% в пробел, чтобы FXDLoader мог загрузить графику
def rootDir = "{__DIR__}".replaceAll("%20", " ");

// загрузка графического файла JavaFX
var pieceNode = FXDLoader.load("{rootDir}res/WKing.fxz");
// добавление графики в объект Group
var group = Group {
    content: [
        pieceNode
    ]
}
// добавление графики к доске
insert group into board.content

Мы отправили очередной отчет об ошибке, сообщающий, что FXDLoader нужно обновить, чтобы он мог обрабатывать URL, как в случае с классом Image. В примере кода от Sun рекомендуется использовать __DIR__ для загрузки других ресурсов. Он должен работать таким же образом с графическими объектами JavaFX, используя класс FXDLoader.

Пользовательский курсор мыши в JavaFX

Мы решили загрузить пользовательский курсор мыши. Хотелось, чтобы наша шахматная программа подсвечивала фигуру, над которой находится мышь, и курсор мыши принимал форму открытой руки, показанную на рисунке ниже, и принимал форму закрытой руки, когда на фигуре выполнен щелчок мыши, и она перетаскивается. Также должен подсвечиваться квадрат, на который фигура будет помещена, если отпустить кнопку мыши. Это полезно в случаях, когда пользователь перетаскивает фигуру над углом квадрата. Без подсказки пользовательского интерфейса вы можете не знать, в какой квадрат фигура будет помещена при отпускании кнопки мыши.

Подсвеченные фигуры и пользовательские фигуры при перемещении мыши над фигурой и при перетаскивании фигуры с помощью мыши

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;
// Внутри класса Piece (фигура) создаются пользовательские функции слушателей событий
// мыши для обеспечения возможности изменения курсора
onMouseEntered: function(e: MouseEvent) {
    currentCursor = grabCursor;
    board.cursor = currentCursor;
}
onMouseExited: function(e: MouseEvent) {
    currentCursor = Cursor.Default;
    board.cursor = currentCursor;
}
// аналогично для других функций мыши - отпущенный, нажатый, произошел щелчок по кнопке мыши

  Загрузить исходный код - 65.49 KB