16. Принципы разработки графического интерфейса. Фреймворк JavaFX
Last updated
Was this helpful?
Last updated
Was this helpful?
Для корректной работы JavaFX рекомендуется установить 8, 9 или 10 версию JDK.
Для работы JavaFX на с более поздними версиями Java требуется специальным образом создавать новый проект. Подробнее читайте .
Отличие программы с графическим интерфейсом от консольной программы заключается не столько в способе подачи информации (использование графических объектов против текстового ввода и вывода информации), сколько в порядке выполнения (control flow) программного кода.
Консольная программа начинает свою работу с точки входа (метод main()
), выполняет все инструкции, указанные в этом методе (включая создание объектов классов и вызовы методы этих объектов), после чего завершает свою работу. Пользователь может вводить значения в консоль, но в строго определенный программистом момент и программа не будет работать до тех пор, пока пользователь не осуществит ввод.
Такой порядок работы программы удобен, если рассматривать программу как математическую функцию или как решение какой-то математической задачи. Программа выполняет набор действий, а после того, как все действия выполнены, программы завершает свою работу.
Программа, использующая графический интерфейс пользователя (graphic user interface или GUI), работает иначе. Работа GUI-программы основана на событиях (event-driven; разработку программы с графическим интерфейсом иногда называют event-driver programming).
В данном случае событие - это изменение состояния объекта, которое может быть зафиксировано и обработано программой. Если говорить проще, событие - это:
взаимодействие пользователя с компонентами графического интерфейса;
выполнение некоторыми объектами определенного условия (например, сработал таймер, была выполнена некоторая операция);
сообщения от операционной система (например, прерывания операционной системы, сбой аппаратного или программного обеспечения и так далее).
Пока что будем считать, что события - это результат взаимодействия пользователя с компонентами графического интерфейса. Пользователь нажал на кнопку, передвинул курсор мыши, ввел символ из клавиатуры, выбрал пункт списка, осуществил скроллинг страницы - все эти действия порождают события (events).
Среда Java (JVM) содержит механизмы, которые позволяют приложению реагировать на события - выполнять некоторые действия в ответ на нажатие кнопки, движение курсора, ввода символа из клавиатуры и так далее.
В Java существует огромное количество различных типов событий – даже самое незначительное действие может служить источником одного, а иногда и целой серии событий.
Важно понять, что разработчик не обязан создавать методы для реакции на все возможные события, которые теоретически могут произойти в программе. Программист должен определить, на какие события программа будет реагировать, и какая будет реакция, а какие события программа будет игнорировать
Схему работы GUI-приложения можно представить следующей схемой. Это очень упрощенная схема, но она дает понимание того, как работает GUI-приложение.
Вы можете обратить внимание на то, что вышеприведенная схема работы GUI-приложения напоминает цикл. На самом деле, это так и есть. Работу приложения можно, очень условно, разделить на три этапа:
Подготовительный этап. На этом этапе программа создает необходимые объекты графического интерфейса (но пока их не показывает), устанавливает связи между объектами интерфейса и выполняет другую подготовительную работу. На этом этапе приложение не обрабатывает события и не взаимодействует с пользователем;
Основной этап работы приложения. Приложение входит в бесконечный цикл (он называется event loop), в ходе которого, приложение на каждой итерации цикла проверяет – произошли ли какие-то события. Если есть произошедшие события – оно отправляет первое в очереди событие на обработку, на следующей итерации – следующее за ним в очереди событие, и так далее. Если нет произошедших событий – приложение просто пропускает эту итерацию;
Завершение работы приложения. Приложение «крутится» в event loop до тех пор, пока не будет подана команда на закрытие приложения (пользователь выбрал пункт «Выйти из приложения», закрыл приложение из диспетчера задач, операционная система принудительно «убила» приложение и так далее). При поступлении команды на закрытие, приложение выходит из бесконечного цикла, после чего, оно может выполнить некоторые заключительные действия, после чего закрывается.
Так как, согласно парадигме Java, всё является объектом, то и графический интерфейс, по сути, является набором объектов различных классов (кнопка, поле ввода, окно приложения – всё это является объектами соответствующих классов), который объединены в библиотеки графического интерфейса.
В Java нет никаких ограничений на реализацию своей библиотеки графического интерфейса – вы можете написать свою библиотеку, которая будет реализовывать свои классы компонентов графического интерфейса. Таким образом, наряду с библиотеками, которые были предложены разработчиками языка Java (AWT, Swing, JavaFX), существует множество библиотек, которые были разработаны сторонними фирмами или просто отдельными сообществами программистов (SWT, Apache Pivot и другие). Рассмотрим библиотеки, которые были предложены разработчиками языка Java.
AWT (Abstract Window Toolkit) – первая предложенная разработчиками библиотека GUI для Java. Главным недостатком данной библиотеки было то, что она использовала нативные компоненты ОС. Таким образом, внешний вид приложения, запущенного под ОС Window мог сильно отличаться от внешнего вида того же приложения, запущенного под OC Linux. На данный момент, эта библиотека считается устаревшей и не используется.
Swing – наиболее известная и вторая по счету библиотека GUI для Java. Обратите свое внимание на тот факт, что библиотека Swing активно использует компоненты AWT, наследуясь от них. Таким образом, хотя AWT и не используется напрямую, но Swing, во многом, опирается на библиотеку AWT.
Библиотека Swing является надежным проверенным средством для разработки GUI, а также имеет множество сторонних компонентов и классов, которые помогут вам в разработке. Недостатком Swing является то, что эта библиотека предназначена только для desktop-приложений и морально устарела, т.к. не поддерживает современные инструменты разработки графических интерфейсов (привязка данных, использование CSS и XML для описания интерфейса и так далее).
Разработчики языка Java, хотя и поддерживают библиотеку Swing, но настоятельно рекомендуют использовать библиотеку JavaFX.
Данная библиотека обладает всеми преимуществами Swing и, дополнительно предлагает более рациональный, простой в употреблении и усовершенствованный подход к построению GUI, а также значительно упрощает воспроизведение объектов благодаря автоматической перерисовке.
Стартовый класс JavaFX-приложения должен расширять класс Application, который входит в состав пакета javafx.application
Точкой входа для JavaFX-приложений является базовый класс Application
. Точка входа программиста вызывает метод класса Application, который запускает JavaFX-приложение.
В классе Application важными для нас являются три метода (они называются методами жизненного цикла, т.к. они вызываются системой в определенный момент «жизни» приложения):
метод init()
вызывается в момент, когда приложение только начинает выполняться. Он служит для выполнения различных инициализаций. Если инициализация не требуется – просто не переопределяйте данный метод;
метод start()
вызывается после init()
. Этот метод в классе Application
является абстрактным, поэтому нам необходимо его переопределить. Именно с него начинается работа приложения. Здесь создаются и настраиваются компоненты графического интерфейса;
метод stop()
вызывается, когда приложение завершается. Именно в нем должны быть произведены все операции очистки или закрытия. Если это не требуется – просто не переопределяем метод.
Когда запускается приложение, JavaFX выполняет следующие действия:
создает объект класса, который наследуется от класса Application
;
вызывает метод init()
;
вызывает метод start()
;
ждет завершения приложения. Приложение может завершиться в следующих случаях:
приложение вызвало метод Platform.exit()
;
закрыто последнее окно и атрибут implicitExit
класса Platform
равен true
;
вызывает метод stop()
.
Для того чтобы запустить JavaFX-приложение на выполнение, следует вызвать статический метод launch()
, определяемый в классе Application
.
Вызов метода launch()
приводит к построению приложения и последующему вызову методов init()
и start()
. Возврат из метода launch()
не происходит до тех пор, пока приложение не завершится.
При выборе терминов для JavaFX, разработчики пользовались театральной терминологией и представляли работу графического интерфейса как театр: в театре есть одна или несколько театральных сцен (окон). На театральной сцене, в течение спектакля (работы программы) сменяют друг друга сцены из спектакля (контейнер), на которых расположены различные декорации (элементы графического интерфейса).
Центральным понятием в JavaFX является Stage
(во многих книгах этот переводится как «подмостки», имеется ввиду «театральные подмостки»). Класс Stage
является нашей театральной сценой, подмостками, основным контейнером, который, как правило, представляет собой обрамлённое окно со стандартными кнопками: закрыть, свернуть, развернуть. Внутри Stage
содержится сцена – объект класса Scene
, которая может быть заменена другим объектом класса Scene
. Объект класса Stage
может содержать одновременно только один объект класса Scene
.
Объект класса Scene
содержит в один элемент – специальную вершину, которая называется корневая вершина (root node). Это самый верхний и единственный узел графа, не имеющий родителя. Все узлы в сцене прямо или косвенно происходят от корневого узла. В качестве корневого узла может выступать любой элемент графического интерфейса, который наследуется от абстрактного класса Parent
.
Корневой узел включает в себя множество различных графических компонент – кнопок, полей, переключателей, надписей – всё то, что нам нужно для создания графического интерфейса. На первом этапе думайте о Stage
просто как об окне приложения, а о Scene
– как о полотне, на котором размещаются нужные вам элементы. Все элементы графического интерфейса будут добавляться в корневую вершину.
В рамках одного окна вы можете менять объекты Scene
и, таким образом, менять содержимое окна. Вы также можете создать другие объекты Stage
– когда вам нужно открыть другие окна в приложении. Стартовый объект Stage
(аргумент primaryStage
) создает за вас JavaFX и передает вам как аргумент метода start()
.
Отдельные элементы сцены называются узлами. Например, кнопка, поле ввода, надпись и так далее. Некоторые узлы могут содержать в себе другие узлы (например, группа радиокнопок). Совокупность всех узлов сцены называется графом сцены, который образует дерево. Базовым классом для всех узлов служит класс Node
. От Node
, прямо или косвенно, происходят другие классы, например Parent
, Group
, Region
и Control
.
Если узел может иметь потомков, тогда он называется узлом ветвления (branch node), если у узла не может быть потомков, он называется листом (leaf node). Узлы ветвления наследуются от класса Parent
.
Рассмотрим небольшой пример. Построим графический интерфейс в виде панели с тремя надписями.
В качестве панели будет использоваться компонент HBox
. Этот компонент является менеджером компоновки (Layout Manager) – специальным контейнером, в который можно помещать элементы, которые будут расположены в определенном порядке.
Компонент HBox
размещает содержащиеся в нем элементы в виде горизонтального ряда. Такой компонент, очевидно, является узлом ветвления, так как он может иметь детей. Текстовая надпись (класс Text
), очевидно, является листом, т.к. вы не можете ничего поместить в текстовую надпись.
Граф сцены для вышеприведенного приложения будет следующим:
Код для такого приложения будет выглядеть следующим образом:
При разработке GUI можно часто столкнуться с проблемой: как сделать так, чтобы при изменении размера окна, расположенные в нем компоненты не смешивались в кучу, а вели себя определенным образом. Необходимо, чтобы элементы либо пропорционально уменьшались или увеличивались в размерах, либо оставались того же размера, появлялись полосы скроллинга, компоненты как-нибудь перемещались по экрану и так далее.
Для того, чтобы самостоятельно не заниматься отслеживанием таких ситуаций, во многих библиотеках графического интерфейса используются специальные компоненты, которые называются менеджерами компоновки (Layout Manager).
Менеджеры компоновки определяют размер и расположение компонентов, а так же, при изменении размера окна пропорционально масштабируют компоненты формы.
При разработке графического интерфейса, необходимо определить – какие менеджеры компоновки подойдут для реализации задуманного интерфейса, после чего расположить их в правильном порядке и добавить в них необходимые элементы.
BorderPane
Класс располагает свои дочерние узлы в одном из пяти регионов: top
, bottom
, left
, right
, center
. Эти регионы могут быть любых размеров. Если не поместить ни одного элемента в какой-либо регион, для него не будет выделено пространство.
Менеджер BorderPane
используется, когда необходимо достичь классического вида приложения – с панелью инструментов вверху, статусной строкой внизу, навигационной панелью слева, дополнительной информацией справа и рабочей областью в центре. Например:
Рассмотрим небольшой пример
HBox
Менеджер разметки HBox
позволяет легко упорядочить элементы в горизонтальный ряд. С помощью метода setPadding()
можно установить расстояние между узлами и краями HBox
. С помощью метода setSpacing()
можно регулировать расстояние между узлами. Менеджер поддерживает установку стиля.
VBox
Менеджер разметки VBox
работает схожим с HBox
образом, за исключением того, что узлы упорядочены в вертикальный ряд.
StackPane
Данный менеджер разметки размещает узлы в один стек, один на другой. Новый добавленный узел будет расположен поверх старого. Данный менеджер полезен, когда необходимо, например, расположить текст поверх фигуры и изображения или для создания комплексных фигур.
GridPane
Данный менеджер разметки позволяет создавать гибкую сеть из рядов и колонок, в которой располагаются узлы. Узел может быть помещен в любую ячейку сетки и объединять ячейки, если нужно. Этот менеджер полезен для создания форм или любой разметки, которая может быть организована рядами и колонками.
Обратите внимание на то, как в разметке GridPane
задаются свойства отдельных колонок и рядов: создается объект класса ColumnConstraints
и RowConstrains
соответственно, в объектах устанавливаются нужные свойства, после чего этот объект передается GridPane
(также, это можно сделать, указав индекс колонки или ряда).
FlowPane
FlowPane
– класс поточной компоновки. Элементы в этой компоновке располагаются построчно с автоматическим переходом на новую строку, если требуется.
Рассмотрим пример:
Попробуем изменять размер окна и увидим, что если кнопки не помещаются в окно по ширине, то кнопки автоматически переносятся на следующую строку.
По умолчанию, поточная компоновка выполняется по горизонтали, хотя можно указать и поточную компоновку по вертикали. Имеется возможность указать и другие свойства компоновки, в том числе промежутки между элементами по горизонтали и по вертикали, а также выравнивание.
TilePane
Работа этого менеджера компоновки схожа с работой FlowPane
, отличие в том, что дочерние узлы помещаются в сетку, ячейки которого имеют одинаковый размер.
Реализуем предыдущий пример, но сделаем кнопку 3 больше остальных. Обратите внимание на отличия между работой FlowPane
и TilePane
Обратите внимание, что TilePane
, исходя из размеров самого большого узла, высчитывает размер ячейки, и каждый элемент помещает в эти ячейки одинакового размера.
AnchorPane
Разметка AnchorPane
позволяет привязывать элементы к верхнему, нижнему, правому, левому краям контейнера. При изменении размеров окна, узлы поддерживают позицию относительно привязки. Узлы могут быть привязаны к нескольким позициям, и к одной позиции может быть привязано более одного узла
Group
Особняком в списке классов-контейнеров стоит класс Group
. Элементы в Group
не привязываются к местоположению, их позиция задается в абсолютных значениях. Если не задано другое, все дочерние узлы группы позиционируются по координате (0,0) (верхний левый угол).
Компонент Group
часто используют для того, чтобы применить различные эффекты и преобразования к целому набору компонентов, которые находятся в группе. Размеры компонента Group
определяются размерами его потомков.
Попробуем добавить пару кнопок в группу
На этом примере мы видим, что две кнопки наложены друг на друга, т.к. обе находятся в верхнем левом углу без всякой компоновки. Если мы попробуем изменить размеры окна, то увидим, что кнопки остаются на своем месте и разметка никак не реагирует на изменение размеров
Попробуем установить для кнопок позицию в группе. Мы видим, что кнопки расположились согласно координатам.
Полезным свойством Group
является возможность устанавливать эффекты и преобразований для всей группы. На примере ниже мы устанавливаем эффект BoxBlur
и этот эффект автоматически применяется ко всем узлам внутри группы.
За создание меню в JavaFX отвечают несколько классов:
MenuBar
– панель меню;
Menu
– раздел меню (Главная, Вставка и т.д.);
MenuItem
– действие в пункте меню;
RadioButtonItem
– для создания пункта меню в виде RadioButton
(выбор одного из нескольких вариантов);
CheckMenuItem
– создание пункта меню в виде CheckBox
(выделение\снятие выделения);
SeparatorMenuItem
- для отделения одной категории от другой.
В JavaFX нет ограничений на расположение панели меню, однако, как правило, панель меню располагается вверху окна. Панель меню добавляется как обычный узел, наряду с другими компонентами графического интерфейса. Панель меню содержит один или несколько разделов. Каждый раздел в панели выглядит как кнопка с текстовым значением. Рассмотрим небольшой пример
При нажатии на раздел меню ничего не происходит, т.к. мы добавили пункты меню. Давайте добавим разнообразные пункты меню в раздел «Файл» и посмотрим на результат
В качестве примера воспроизведем интерфейс приложения «Калькулятор» для Windows 7.
Для начала, нам необходимо понять – какие менеджеры компоновки нам следует использовать. При разработке GUI желательно использовать как можно меньшую вложенность менеджеров разметок и как можно меньший граф сцены.
Для начала, мы можем заметить, что калькулятор состоит из трех вертикальный областей: меню, поле вывода результата и кнопки калькулятора.
Следовательно, здесь уместным будет использование менеджера разметки VBox
, который размещает элементы в вертикальный список. Элемент класса VBox
будет нашим корневым узлом графа (root node).
Создадим метод configureMenu()
, в котором мы будем иницализировать и настраивать объекты меню.
Разделы меню мы заполнять не будем, а пока что попробуем воспроизвести окно результата. Создадим метод configureResultView()
, в котором мы будем создавать и настраивать объекты для реализации окна показа результата.
Текстовое поле (класс Text
) само по себе имеет не очень много возможностей для настройки выравнивания и внешнего вида. Поэтому, чтобы добиться сходства с оригинальным калькулятором, поместим текстовое поле в HBox
, а уже HBox
поместим как второй дочерний узел для VBox
.
Теперь перейдем к третьему элементу VBox
, в котором будут содержаться кнопки. Для того, чтобы выбрать нужную разметку, давайте внимательно посмотрим на область с кнопками.
Нетрудно догадаться, что это разметка типа Grid – то есть, сетка с набором кнопок. Две кнопки («=» и «0» занимают две строчки и два столбца соответственно). Таким образом, используем менеджер GridPane
. Создадим метод configureButtons()
, в котором будем создавать кнопки и помещать их в соответствующие ячейки сетки.
Для начала необходимо настроить кнопки. По умолчанию, размер кнопки определяется ее содержимым. Нам же необходимо будет, чтобы кнопка могла растягиваться в ширину и высоту, чтобы растягиваться в нашей сетке. Лучшим вариантом будет – создать собственный класс, который наследуется от стандартной кнопки и в конструкторе собственного класса вызвать метод setMaxSize()
Теперь наша кнопка будет растягиваться в случае необходимости. Теперь реализуем метод. Безусловно, добавление можно было бы реализовать более компактно, однако, в качестве примера, реализуем добавление каждой кнопки отдельно.
Далее, создадим объект класса ColumnConstraints
, после чего передадим объект этого классов для каждой колонки нашей таблицы. В этом объекте мы указываем, что колонка должна максимально расширяться. Также, установить отступы и зазоры между ячейками таблицы.