Краткий обзор GUI-фреймворков для Java и мое первое простенькое GUI-приложение на Swing. Процесс разработки простой GUI программы на языке Java

JFace — это доплнительный слой абстракции над графической библиотекой SWT, предоставляющий возможности для разработки графического интерфейса приложения на основе модели MVC(Model View Controller). Основные компоненты JFace включают:

  • Viewers — классы для инкапсуляции задач предоставления данных для отображения, их фильтрации, сортировки и т.п.
  • Actions and contributions — вводят семантику описания пользовательских действий и того, как они должны быть доступны пользователю.
  • Image and font registries — предоставляют классы для управления ресурсами, такими как изображения и шрифты.
  • Dialogs and wizards — фреймворк для построения сложного диалогового взаимодействия с пользователем.
  • Field assist — предоставляет возможности для реализации вспомогательной функциональности для полей, такой как отображение статуса поля или подсказки о содержимом.

В первой части будет рассмотрено создание простого окна с использованием JFace, добавление к нему меню, статусной строки и панели инструментов.

Настройка проекта

Для того, чтобы разрабатывать графические приложения с использованием JFace в Eclipse, необходимо подключить jar-файлы SWT и JFace.

  • В случае, если в Eclipse установлено PDE, то это можно сделать, указав в настройках проекта использовать библиотеку SWT, и поставить галочку для поддержки JFace.
  • Если же PDE не установлен, то необходимо подключить jar-файлы к проекту вручную. Найти их можно в директории plugins эклипса, называются они org.eclipse.swt_*.jar и org.eclipse.jface_*.jar

Создание окна

Окно приложения в JFace наследуется от класса ApplicationWindow, предоставляющего возможности для создания меню, панелей управления и строки статуса.

Простейший вариант создания окна приведен в следующем примере:

import org.eclipse.jface.window.ApplicationWindow; import org.eclipse.swt.widgets.Display; public class MainWindow extends ApplicationWindow { public static void main(String args) { MainWindow window = new MainWindow(); // Создаем наше окно window.setBlockOnOpen(true); // Устанавливаем флаг - ждать закрытия окна window.open(); // Открываем окно Display.getCurrent().dispose(); // Освобождаем ресурсы } public MainWindow() { super(null); // Вызываем конструктор родительского класса } }

Создание содержимого окна

Для того, чтобы создать какое-то содержимое окна, необходимо переопределить метод Control createContents(Composite parent) класса ApplicationWindow. Метод должен возвращать компонент, который будет являться содержимым окна.

В этом же методе можно установить и заголовок окна, вызвав метод setText(String text) шелла, доступного по вызову getShell()

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

Собственно, пример:

protected Control createContents(Composite parent) { getShell().setText("My window"); // Устанавливаем заголовок окна Label lbl = new Label(parent, SWT.NONE); // Создаем новый элемент, в нашем случае - просто метку lbl.setText("Window contents"); // Устанавливаем ей текст lbl.setBackground(new Color(Display.getCurrent(), 255, 255, 255)); // Устанавливаем белый цвет фона return lbl; }

Добавление статусной строки

Добавление к окну приложения статусной строки реализуется не просто, а очень просто. Для этого надо всего-навсего вызвать защищенный метод addStatusBar(). Наверное, наиболее подходящим местом для такого вызова является конструктор окна, или какой-нибудь метод его инициализации.

Для того, чтобы отобразить текст в статусной строке необходимо вызвать метод setStatus(String status), аргументом которого и является та самая строка, которую необходимо отобразить.

Создание меню

Для создания строки меню в JFace необходимо, вызвать метод addMenuBar(), как и для создания строки статуса.
Но затем необходимо добавить пункты меню на панель. Осуществляется это следующим образом:

  • Раздел меню описывается классом MenuManager. В конструкторе ему может быть передана строка — имя раздела, где символ «&» означает, что следующий за ним символ будет ключом при навигации с помошью клавиши Alt. Добавляется раздел меню на панель с помощью конструкции getMenuBar().add(menu).
  • Пункт меню описывается классом Action. Сам класс является абстрактным, пункт меню должен быть унаследован от него и переопределять метод void run(), в котором размещается код, выполняемый при выборе пункта меню пользователем. На самом деле, метод можно и не переопределять, но тогда, зачем этот пункт меню нужен?=) Имя пункта может быть задано путем передачи в конструктор, или вызовом метода void setText(String text). После создания пункт меню добавляется в раздел вызовом метода add у объекта раздела меню. Например: menu.add(menuItem)
  • Подменю создаются очень просто: Необходимо в один раздел меню методом add добавить другой раздел. Вот и все.

В следующем примере мы создаем раздел меню File и один пункт Hello в нем, выбор которого вызывает появление текста в статусной строке окна. Код создания меню я оформил в один метод, который вызывается из конструктора окна:

private void createMenu() { addMenuBar(); // Добавляем панель меню MenuManager fileMenu = new MenuManager("&File"); // Создаем новое меню getMenuBarManager().add(fileMenu); // Добавляем меню на панель fileMenu.add(new Separator()); // Добавляем разделитель в меню fileMenu.add(new Action("&Hello") { // Создаем новое действие, указываем его текст @Override public void run() { // Код выполняемый при активации действия setStatus("Hello world!!"); // Мы просто отображаем новое значение в статусной строке } }); // И добавляем действие в качестве пункта меню }

Создание панели инструментов

Для того, чтобы создать панель инструментов в окне необходимо в конструкторе окна вызвать метод addToolBar(int style), в который передать стиль компонента панели.

Для доступа к созданной панели используется метод getToolBarManager(). Для добавления действия на панель используется метод add панели, куда передается действие.

В простейшем случае создание панели и добавление на нее кнопки будет выглядеть:

private void createToolBar() { addToolBar(SWT.NONE); getToolBarManager().add(new Action("&Hello") { // Создаем новое действие, указываем его текст @Override public void run() { // Код выполняемый при активации действия setStatus("Hello world!!"); // Мы просто отображаем новое значение в статусной строке } }); }

Создание нескольких перемещаемых панелей инструментов

Вместо одной панели инструментов можно создать набор перемещаемых панелей (CoolBar). Для этого необходимо:

  1. Вместо метода addToolBar вызвать метод addCoolBar
  2. Для доступа к набору панелей используется метод getCoolBarManager()
  3. Создать панель инструментов, на которую можно будет добавлять действия. Для этого необходимо создать новый экземпляр класса ToolBarManager, который будет представлять панель и вызвать метод add у CoolBarManager, передав туда панель.
  4. На созданную панель можно добавлять действия

Пример кода, создающего две плавающие панели:

private void createCoolbar() { addCoolBar(SWT.NONE); ToolBarManager tm = new ToolBarManager(); getCoolBarManager().add(tm); tm.add(helloAction); ToolBarManager tm2 = new ToolBarManager(); getCoolBarManager().add(tm2); tm2.add(helloAction); tm2.add(helloAction); }

Важно! Окно приложение может иметь или одну панель управления или набор перемещаемых, попытка создания и того и другого приведет к ошибке!

Возможные проблемы

Здесь будут описаны некоторые проблемы, с которыми можно столкнуться:

  • При добавлении пунктов меню после запуска приложения они не появляются — чтобы изменения меню после создания окна отобразились на нем, необходимо вызвать метод getMenuBarManager().update(true) — указать обновить панель меню.

Ссылки

Дополнительная информация по JFace может быть найдена по следующим ссылкам:

  • Русскоязычная вводная статья на ibm.com
  • Цикл англоязычных статей на ibm.com
  • Описание API Eclipse — среди прочего там есть и пакеты, относящиеся к SWT и JFace

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

И так, какие инструменты нам необходимы:

  • Java Virtual Machine (OpenJDK или Oracle JDK)
  • Intellij IDEA (или другое IDE для Java)

После установки необходимого софта, открываем Intellij IDEA и создаем новый проект: File -> New Project…

Я назвал проект guiBase . Как видно на скрине, папка src не содержит ничего, поэтому создаем в ней наш главный класс, содержащий функцию main .

Public class Main { public static void main(String args) { System.out.println("Hello, Govzalla!"); } }

Содеражние главного класса видите выше. Мы уже сейчас можем создать проект (Build project ) и запустить его (Run ). Внизу в терминале вашего IDE вы увидите сообщение “Hello, Govzalla!“ . Но как вы сами поняли — GUI он не поддерживает.

На данном этапе у нас уже есть работающая программа, но без поддержки GUI. А сейчас в той же папке src создадим GUI Form : New -> GUI Form

Открываем созданную GUI форму, нажимаем на JPanel и задаем его идентификатор в поле field name , я задал panel .

После чего перетаскиваем на форму с правой стороны JTextField , JPasswordField и JButton :

Осталось добавить код и связать нашу форму с ним. Когда мы добавляли форму MainWindow , автоматически создался и класс MainWindow , этот класс является классом созданной формы, т.е. именно этот класс будет обслуживать все события данной формы.

Хотя класс нашего окна содержит необходимые элементы, но даже сейчас он не имеет ничего общего с GUI, поэтому расширим его с помощью JFrame и унаследуем всю основную и необходимую функциональность GUI.

В данный момент мы имеем форму MainWindow и класс MainWindow расширенный с помощью JFrame . Сейчас нам необходимо определить все добавленные GUI элементы как содержание класса MainWindow
this.getContentPane().add(panel);
После чего содержание файла MainWindow.java будет изменено следующим образом:

Import javax.swing.*; public class MainWindow extends JFrame { private JTextField textField1; private JPasswordField passwordField1; private JButton button1; private JPanel panel; public MainWindow() { this.getContentPane().add(panel); } }

Если попробуете запустить код, вы снова увидите то же самое сообщение “Hello, Govzalla!“. Дело в том, что мы создали класс и форму к нему, но не создали инстанцию этого класса.

Пришло время изменить файл Main.java и добавить туда код создания нашего GUI:

Import java.awt.*; public class Main { public static void main(String args) { // Создаем инстанцию класса MainWindow MainWindow mainWindow = new MainWindow(); // Упаковываем все элементы с нашей формы mainWindow.pack(); // Изменяем размеры окна mainWindow.setSize(new Dimension(200, 200)); // Отображаем созданное окно mainWindow.setVisible(true); } }

Запускаем код

Нажав на кнопку Button вы заметите, что программа никак не реагирует. Дело в том, что мы еще не добавили слушатель (Listener ) для событий (Events ) кнопки Button.

Слушатель событий (Event listener ) JButton должен быть имплентацией адаптера ActionListener , поэтому добавим следующий код в тело класса MainWindow :

Private class MyButtonListener implements ActionListener { @Override public void actionPerformed(ActionEvent actionEvent) { } }

Метод actionPerformed () будет обрабатывать все события кнопки button1, но для начала еще необходимо указать кнопке button1 какой класс будет обрабатывать, поэтому добавим следующий код в конструктор класса MainWIndow:
this.button1.addActionListener(new MyButtonListener());
Чтобы наш обработчик не был бессмысленным добавим следующий код в метод actionPerformed ():

@Override public void actionPerformed(ActionEvent actionEvent) { if (textField1.getText().equals(passwordField1.getText())) { JOptionPane.showMessageDialog(null, "Success"); } else { JOptionPane.showMessageDialog(null, "Failure"); } }

Сейчас уже программа будет правильно реагировать на события, не на все события, конечно. Например, если попытаться отключить программу нажав на крестик, окно исчезнет, но программа все еще будет работать, т.к. не добавлен обработчик событий главного окна.

What is Swing?

Java Swing is a lightweight Graphical User Interface (GUI) toolkit that includes a rich set of widgets. It includes package lets you make GUI components for your Java applications, and It is platform independent.

The Swing library is built on top of the Java Abstract Widget Toolkit (AWT ), an older, platform dependent GUI toolkit. You can use the Java GUI components like button, textbox, etc. from the library and do not have to create the components from scratch.

In this tutorial, you will learn-

Java Swing class Hierarchy Diagram

All components in swing are JComponent which can be added to container classes.

What is a container class?

Container classes are classes that can have other components on it. So for creating a GUI, we need at least one container object. There are 3 types of containers.

  1. Panel : It is a pure container and is not a window in itself. The sole purpose of a Panel is to organize the components on to a window.
  2. Frame : It is a fully functioning window with its title and icons.
  3. Dialog : It can be thought of like a pop-up window that pops out when a message has to be displayed. It is not a fully functioning window like the Frame.

Java GUI Example

Example : To learn to design GUI in Java
Step 1) Copy the following code into an editor

Import javax.swing.*; class gui{ public static void main(String args){ JFrame frame = new JFrame("My First GUI"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(300,300); JButton button = new JButton("Press"); frame.getContentPane().add(button); // Adds Button to content pane of frame frame.setVisible(true); } }

Step 2) Save, Compile, and Run the code.
Step 3) Now let"s Add a Button to our frame. Copy following code into an editor

Import javax.swing.*; class gui{ public static void main(String args){ JFrame frame = new JFrame("My First GUI"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(300,300); JButton button1 = new JButton("Press"); frame.getContentPane().add(button1); frame.setVisible(true); } }

Step 4) Execute the code. You will get a big button

Step 5) How about adding two buttons? Copy the following code into an editor.

Import javax.swing.*; class gui{ public static void main(String args){ JFrame frame = new JFrame("My First GUI"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(300,300); JButton button1 = new JButton("Button 1"); JButton button2 = new JButton("Button 2"); frame.getContentPane().add(button1); frame.getContentPane().add(button2); frame.setVisible(true); } }

Step 6) Save , Compile , and Run the program.
Step 7) Unexpected output =? Buttons are getting overlapped.

Java Layout Manger

The Layout manager is used to layout (or arrange) the GUI java components inside a container.There are many layout managers, but the most frequently used are-

Java BorderLayout

A BorderLayout places components in up to five areas: top, bottom, left, right, and center. It is the default layout manager for every java JFrame

Java FlowLayout

FlowLayout is the default layout manager for every JPanel . It simply lays out components in a single row one after the other.

Java GridBagLayout

It is the more sophisticated of all layouts. It aligns components by placing them within a grid of cells, allowing components to span more than one cell.

Step 8) How about creating a chat frame like below?

Try to code yourself before looking at the program below.

//Usually you will require both swing and awt packages // even if you are working with just swings. import javax.swing.*; import java.awt.*; class gui { public static void main(String args) { //Creating the Frame JFrame frame = new JFrame("Chat Frame"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(400, 400); //Creating the MenuBar and adding components JMenuBar mb = new JMenuBar(); JMenu m1 = new JMenu("FILE"); JMenu m2 = new JMenu("Help"); mb.add(m1); mb.add(m2); JMenuItem m11 = new JMenuItem("Open"); JMenuItem m22 = new JMenuItem("Save as"); m1.add(m11); m1.add(m22); //Creating the panel at bottom and adding components JPanel panel = new JPanel(); // the panel is not visible in output JLabel label = new JLabel("Enter Text"); JTextField tf = new JTextField(10); // accepts upto 10 characters JButton send = new JButton("Send"); JButton reset = new JButton("Reset"); panel.add(label); // Components Added using Flow Layout panel.add(label); // Components Added using Flow Layout panel.add(tf); panel.add(send); panel.add(reset); // Text Area at the Center JTextArea ta = new JTextArea(); //Adding Components to the frame. frame.getContentPane().add(BorderLayout.SOUTH, panel); frame.getContentPane().add(BorderLayout.NORTH, mb); frame.getContentPane().add(BorderLayout.CENTER, ta); frame.setVisible(true); } }

В некоторых ситуациях, в процессе разработки программного обеспечения, возникает необходимость, когда разным группам программистов надо «договориться» о том, как их программы будут взаимодействовать. Каждая группа должна иметь возможность писать свой код не зависимо от того, как пишет его другая группа. Интерфейсы и есть этот «договор». Мы уже пользовались интерфейсами в уроке .
В этом уроке мы изучим их подробнее: для чего они нужны, как их объявлять и т.д.

Представьте себе будущее, где автомобилями управляют компьютеры без участия человека. Производители автомобилей пишут программное обеспечение (на Java, конечно 🙂), которое управляет машиной — остановиться, поехать, повернуть и т.д. Другие разработчики делают системы, которые принимают данные GPS (Global Positioning System) и используют эти данные для управления автомобилем. Производители автомобилей публикуют интерфейс-стандарт, который описывает методы для управления машиной. Таким образом сторонние разработчики могут знать какие методы вызывать, чтобы заставить автомобиль двигаться, а производители автомобилей могут изменять внутреннюю реализацию своего продукта в любое время. Ни одна из групп разработчиков не знает как написаны их программы.

Интерфейсы Java

Интерфейсы в Java во многом напоминают классы, но могут содержать только константы, сигнатуры методов и . В интерфейсах нет описания методов. Нельзя создать объект типа интерфейса (но можно использовать в качестве типа — интерфейсные ссылки, об этом позже) — интерфейсы могут только реализовываться некоторым классом или наследоваться другим интерфейсом. Объявление интерфейса очень похоже на объявление класса:

Public interface OperateCar { // константы, если есть // enum перечисление направлений RIGHT, LEFT // сигнатуры методов int turn(Direction direction, double radius, double startSpeed, double endSpeed); int changeLanes(Direction direction, double startSpeed, double endSpeed); int signalTurn(Direction direction, boolean signalOn); int getRadarFront(double distanceToCar, double speedOfCar); int getRadarRear(double distanceToCar, double speedOfCar); }

Сигнатуры методов объявляются без скобок и заканчиваются точкой с запятой. Для использования интерфейса вы должны написать класс, который будет реализовывать ваш интерфейс. Класс который реализует интерфейс, должен описывать все методы, объявленные в интерфейсе. Например:

Public class OperateBMW760i implements OperateCar { // здесь методы интерфейса OperateCar с описанием // например: int signalTurn(Direction direction, boolean signalOn) { // поворот RIGHT или LEFT поворотник on или off } }

В примере выше — производители автомобилей пишут программное обеспечение каждый по-своему, но с одинаковым интерфейсом. Сторонние разработчики — клиенты интерфейса, могут писать собственно ПО с использованием методов, объявленных в интерфейсе.

Интерфейсы в качестве API

Пример с автомобилями показывает как интерфейсы могут использоваться в качестве API (Application Programming Interface ) или интерфейса программирования приложений. Использование API — обычная практика при разработке коммерческого ПО. Обычно компании-разработчики продают программное обеспечение, содержащее набор методов, которые другие компании хотят использовать в своих продуктах.

Интерфейсы и множественное наследование

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

Объявление интерфейса

Объявление интерфейса содержит ключевое слово interface , имя интерфейса, список родительских интерфейсов через запятую (если есть) и тело интерфейса. Например:

Public interface GroupedInterface extends Interface1, Interface2, Interface3 { // число e double E = 2.718282; // сигнатуры методов void doSomething (int i, double x); int doSomethingElse(String s); }

Модификатор доступа public означает что интерфейс может быть использован любым классом в любом пакете. Если не определить интерфейс как публичный, он будет доступен только в рамках своего пакета. Интерфейс может наследовать другие интерфейсы, как классы могут наследовать другой класс. В отличие от классов, интерфейсы могут наследовать любое количество других интерфейсов.

Реализация интерфейса

Для объявления класса, реализующего интерфейс, вы должны использовать ключевое слово implements после которого перечисляются интерфейсы.
Класс может реализовывать много интерфейсов. Объявление реализуемых интерфейсов идет после объявления наследуемого (extends) класса (если есть).

Пример простого интерфейса

Рассмотрим интерфейс, который определяет метод для сравнения объектов.

Public interface Relatable { // this (объект, который вызывает isLargerThan) // объект-параметр должен быть того же класса // возвращет 1, 0, -1 // если объект больше, равен или // меньше чем other public int isLargerThan(Relatable other); }

Чтобы иметь возможность сравнивать объекты мы должны реализовать интерфейс Relatable . Любой класс может реализовать интерфейс Relatable , если есть способ сравнить объекты. Для строк сравнивать можно количество символов, для книг — количество страниц, для студентов — вес и т.д. Для плоских геометрических фигур отличной характеристикой будет площадь, для трехмерных — объем. Все эти классы могут реализовать метод isLargerThan() . Если вы знаете, что класс реализует интерфейс Relatable , то вы смело можете сравнивать объекты этого класса.

Реализация интерфейса Relatable

Напишем класс Rectangle , реализующий интерфейс Relatable .

Public class RectanglePlus implements Relatable { public int width = 0; public int height = 0; public Point origin; // четыре конструктора public RectanglePlus() { origin = new Point(0, 0); } public RectanglePlus(Point p) { origin = p; } public RectanglePlus(int w, int h) { origin = new Point(0, 0); width = w; height = h; } public RectanglePlus(Point p, int w, int h) { origin = p; width = w; height = h; } // метод для передвижения прямоугольника public void move(int x, int y) { origin.x = x; origin.y = y; } // вычисление площади public int getArea() { return width * height; } // метод для реализации интерфейса Relatable public int isLargerThan(Relatable other) { RectanglePlus otherRect = (RectanglePlus)other; if (this.getArea() < otherRect.getArea()) return -1; else if (this.getArea() > otherRect.getArea()) return 1; else return 0; } }

Так как класс RectanglePlus реализует Relatable , размеры любых двух объектов типа RectanglePlus можно сравнивать.

Метод isLargerThan в качестве аргумента принимает объекты типа Relatable . При реализации метода в примере выше мы используем приведение типов, потому что компилятор не поймет что, other — объект типа RectanglePlus и вызов метода other.getArea() без приведения типов приведет к ошибке.

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

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

Рассмотрим пример — метод, который ищет больший объект из двух любых объектов, класс которых реализует интерфейс Relatable:

Public Object findLargest(Object object1, Object object2) { Relatable obj1 = (Relatable)object1; Relatable obj2 = (Relatable)object2; if ((obj1).isLargerThan(obj2) > 0) return object1; else return object2; }

Приведением object1 к типу Relatable , мы делаем возможным вызов метода isLargerThan .

Так же для любого класса, реализующего интерфес Relatable, можно реализвать методы:

Public Object findSmallest(Object object1, Object object2) { Relatable obj1 = (Relatable)object1; Relatable obj2 = (Relatable)object2; if ((obj1).isLargerThan(obj2) < 0) return object1; else return object2; } public boolean isEqual(Object object1, Object object2) { Relatable obj1 = (Relatable)object1; Relatable obj2 = (Relatable)object2; if ((obj1).isLargerThan(obj2) == 0) return true; else return false; }

Переопределение интерфейсов

Допустим, вы написали интерфейс DoIt:

Public interface DoIt { void doSomething(int i, double x); int doSomethingElse(String s); }

Предположим, позже, вы захотели добавить в него третий метод:

Public interface DoIt { void doSomething(int i, double x); int doSomethingElse(String s); boolean didItWork(int i, double x, String s); }

Если вы сделаете это изменение, то все классы, реализующие этот интерфейс сломаются, т.к. они перестанут его реализовывать.

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

Public interface DoItPlus extends DoIt { boolean didItWork(int i, double x, String s); }

Теперь пользователи вашего интерфейса смогут перейти к использованию нового интерфейса или остаться со старым без боли.

Скажу сразу - при работе с графикой, скорее всего, со временем Вам придется воспользоваться всеми предоставленными инструментами без исключения для достижения наилучшего визуального эффекта. Подробное описание о том «что и как» можно найти - это официальный туториал по Graphics2D. Его должно быть более чем достаточно, чтобы ввести Вас в курс дела.

Я уже привел небольшой пример написания своего UI, но есть и другие варианты кастомизации интерфейса. Каждый отдельный J-компонент производит свою Lightweight-отрисовку при помощи метода paint(), который можно легко переопределить и изменить. Напрямую (не всегда, но чаще всего) его лучше не использовать (не буду вдаваться в подробности, так как это целая тема для отдельного топика). Для следующего примера используем метод paintComponent(). Рассмотрим как его можно применить поближе…

Начну с примера - текстовое поле с визуальным фидбэком при отсутствии содержимого:

JTextField field = new JTextField()
{
private boolean lostFocusOnce = false ;
private boolean incorrect = false ;

{
// Слушатели для обновления состояния проверки
addFocusListener (new FocusAdapter()
{
public void focusLost (FocusEvent e)
{
lostFocusOnce = true ;

repaint ();
}
});
addCaretListener (new CaretListener()
{
public void caretUpdate (CaretEvent e)
{
if (lostFocusOnce)
{
incorrect = getText ().trim ().equals ("" );
}
}
});
}

protected void paintComponent (Graphics g)
{
super.paintComponent (g);

// Расширенная отрисовка при некорректности данных
if (incorrect)
{
Graphics2D g2d = (Graphics2D) g;

// Включаем антиалиасинг для гладкой отрисовки
g2d.setRenderingHint (RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);

// Получаем отступы внутри поля
Insets insets;
if (getBorder () == null )
{
insets = new Insets (2, 2, 2, 2);
}
else
{
insets = getBorder ().getBorderInsets (this );
}

// Создаем фигуру в виде подчеркивания текста
GeneralPath gp = new GeneralPath (GeneralPath.WIND_EVEN_ODD);
gp.moveTo (insets.left, getHeight () - insets.bottom);
for (int i = 0; i < getWidth () - insets.right - insets.left; i += 3)
{
gp.lineTo (insets.left + i,
getHeight () - insets.bottom - ((i / 3) % 2 == 1 ? 2: 0));
}

// Отрисовываем её красным цветом
g2d.setPaint (Color.RED);
g2d.draw (gp);
}
}
};

Наличие содержимого перепроверяется при печати и потере фокуса полем. Переключившись на другой компонент мы увидим как отрисовывается наше дополнение к JTextField"у:

Полный код примера можно взять .

Таким образом можно расширить любой доступный компонент быстро переопределив и дополнив метод отрисовки, не прибегая к написанию отдельных громоздких UI или полноценных компонентов. Плюс данный пример можно достаточно легко вынести в отдельный класс и использовать как готовый элемент для своего интерфейса.
Еще одним плюсом данного способа является то, что Вы получаете независимый от текущего установленного приложению/компоненту LaF/UI - он будет работать всегда. Естественно, для некоторых специфичных UI может понадобиться немного иная отрисовка - поддерживать её или нет - Вам решать.

Хотя зачастую и хочется сделать интерфейс ярче и привлекательней, не всегда требуется для этого изобретать новые компоненты - можно воспользоваться стандартными средствами. Приведу пример кастомизации простейшего чекбокса анимированной сменой состояний и фона. В этот раз я опять (да, можете бить меня палками) воспользуюсь заготовленными изображениями из умелых рук дизайнера.

За основу берутся 8 изображений 16х16 - 4 состояния фона чекбокса и 4 состояния галки (5 на самом деле, но 5ое мы добавим програмно):

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

public static List BG_STATES = new ArrayList ();
public static List CHECK_STATES = new ArrayList ();

static
{
// Иконки состояния фона
for (int i = 1; i <= 4; i++)
{
BG_STATES.add (new ImageIcon (
MyCheckBox.class .getResource ("icons/states/" + i + ".png" )));
}

// Дополнительное "пустое" состояние выделения

new BufferedImage (16, 16, BufferedImage.TYPE_INT_ARGB)));

// Состояния выделения
for (int i = 1; i <= 4; i++)
{
CHECK_STATES.add (new ImageIcon (
MyCheckBox.class .getResource ("icons/states/c" + i + ".png" )));
}
}

private Map iconsCache = new HashMap ();

private synchronized void updateIcon ()
{
// Обновляем иконку чекбокса
final String key = bgIcon + "," + checkIcon;
if (iconsCache.containsKey (key))
{
// Необходимая иконка уже была ранее использована
setIcon (iconsCache.get (key));
}
else
{
// Создаем новую иконку совмещающую в себе фон и состояние поверх
BufferedImage b = new BufferedImage (BG_STATES.get (0).getIconWidth (),
BG_STATES.get (0).getIconHeight (), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = b.createGraphics ();
g2d.drawImage (BG_STATES.get (bgIcon).getImage (), 0, 0,
BG_STATES.get (bgIcon).getImageObserver ());
g2d.drawImage (CHECK_STATES.get (checkIcon).getImage (), 0, 0,
CHECK_STATES.get (checkIcon).getImageObserver ());
g2d.dispose ();

ImageIcon icon = new ImageIcon (b);
iconsCache.put (key, icon);
setIcon (icon);
}
}

Остается добавить несколько обработчиков переходов состояний и мы получим анимированный переход между ними:

Получившийся компонент очень легко будет видоизменить к примеру в радиобаттон, или же добавить в него больше состояний перехода для более плавной анимации и т.п.
Собственно, полный рабочий код и изображения выложил отдельно .

Таким образом путей кастомизации элементов есть достаточно много (даже я, наверно, не знаю/не догадываюсь о некоторых из них). Какой способ выбирать - зависит от ситуации - что необходимо получить, какие изменения возможно сделать в существующем компоненте и т.д.

В завершение данной главы, приведу еще один пример полностью видоизмененного UI кнопки с анимацией, возможностью загругления отдельных углов, возможностью настройки стиля и некоторыми другими улучшениями. Вот несколько скринов с итоговым внешним видом (конечно, анимация тут не видна):


Не буду врать, на данный UI кнопки ушло достаточно много времени, но он создавался без помощи и подсказок дизайнера чистыми средствами Graphics2D и Swing. можно скачать и изучить полный сурс и демо данного UI, если Вам стало интересно. В нем использован достаточно большой спектр возможностей Graphics2D и применены некоторые хитрости, которые могут зачастую оказаться полезными.

Итак, думаю достаточно разговоров о графике - о ней более подробно я расскажу в будущих топиках, а сейчас приведу немного интересного материала, который я наработал за достаточно долгое время «общения» со Swing и Graphics2D.

DnD и GlassPane

Думаю первое - Вам всем более чем известно, как и проблемы с ним связанные. Насчет второго - вероятно Вы вскользь слышали о GlassPane или может даже видели это старинное изображение (которое до сих пор актуально, между прочем) об устройстве различных слоев стандартных фреймов. Что же тут такого и зачем я вспомнил об этом? И тем более, как связаны DnD и GlassPane спросите Вы? Вот именно о том, как их связать и что из этого может выйти я и хочу рассказать в этой главе.

Чтож, начнем по порядку - что мы знаем о DnD?
У некоторых Swing-компонентов есть готовые реализации для драга (JTree и JList к примеру) - для других можно достаточно легко дописать свою. Чтобы не бросаться словами на ветер - приведу небольшой пример DnD стринга из лэйбла:

JLabel label = new JLabel ("Небольшой текст для DnD" );
label.setTransferHandler (new TransferHandler()
{
public int getSourceActions (JComponent c)
{
return TransferHandler.COPY;
}

public boolean canImport (TransferSupport support)
{
return false ;
}

protected Transferable createTransferable (JComponent c)
{
return new StringSelection (((JLabel) c).getText ());
}
});
label.addMouseListener (new MouseAdapter()
{
public void mousePressed (MouseEvent e)
{
if (SwingUtilities.isLeftMouseButton (e))
{
JComponent c = (JComponent) e.getSource ();
TransferHandler handler = c.getTransferHandler ();
handler.exportAsDrag (c, e, TransferHandler.COPY);
}
}
});

Теперь возможно перетащить текст данного лэйбла прямо из интерфейса в любое другое место, куда возможно вставить текст через дроп.
Фактически - TransferHandler овечает за то, какие данные при драге отдает компонент и как компонент использует входящие данные при драге на него.

Но что делать, если необходимо отследить последовательность действий пользователя при самом перетаскивании?
Для этого есть отдельная возможность повесить слушатель:

DragSourceAdapter dsa = new DragSourceAdapter()
{
public void dragEnter (DragSourceDragEvent dsde)
{
// При входе драга в область какого-либо компонента
}

public void dragExit (DragSourceEvent dse)
{
// При выходе драга в область какого-либо компонента
}

public void dropActionChanged (DragSourceDragEvent dsde)
{
// При смене действия драга
}

public void dragOver (DragSourceDragEvent dsde)
{
// При возможности корректного завершения драга
}

public void dragMouseMoved (DragSourceDragEvent dsde)
{
// При передвижении драга
}

public void dragDropEnd (DragSourceDropEvent dsde)
{
// При завершении или отмене драга
}
};
DragSource.getDefaultDragSource ().addDragSourceListener (dsa);
DragSource.getDefaultDragSource ().addDragSourceMotionListener (dsa);

Остался последний момент - обозначить роль GlassPane. GlassPane, фактически, позволяет располагать/отрисовывать на себе компоненты, как и любой другой контейнер, но его особенность в том, что он лежит поверх всех Swing-компонентов, когда виден. Т.е. если мы что-либо отрисуем на нем, то оно накроет весь находящийся под ним интерфейс. Это позволяет размещать компоненты независимо от основного контейнера в любом месте, создавать любые визуальные эффекты и делать другие занятные вещи.

Приведу для большего понимания небольшой пример подобного «эффекта» - фрейм с несколькими Swing-компонентами на нем. При клике в любой части окна будет появляться эффект «распозающегося» круга, который виден поверх всех элементов. Что самое интересное - подобный эффект не съедает ресурсов и не требует большой груды кода. Не верите? - посмотрите демо и загляните в исходник, вложенный в jar.

Кстати говоря, есть достаточно интересная библиотека на эту тему, заодно предоставляющая дополнительный скролл-функционал и несколько других вкусностей - JXLayer (офф сайт) (описание #1 описание #2 описание #3). К сожалению проекты хостящиеся на сайте java сейчас находтся не в лучшем состоянии, поэтому приходится ссылаться на отдельные ресурсы.

Итак теперь объединим всё что я уже описал в данной главе и сделаем, наконец, что-то полноценное. К примеру - отображение драга панели с компонентами внутри окна:

При драге панели за лэйбл появляется полупрозрачная копия панели показывающая где именно будет размещена панель при завершении драга. Также клавишей ESC можно отменить перемещение.
Рабочий пример и исходный код можно взять .

Конечно, для реализации именно данной функциональности не стоило бы прибегать к использованию DnD - есть более короткие пути. Впрочем, всё зависит от ситуации. Данный вариант позволяет отрисовывать перетаскивание независимо от других компонентов в окне. Также можно на основе него, к примеру, реализовать драг панели между окнами приложения.

AWTUtilities

Так как уже достаточно давно в JDK6 включили некоторые будущие нововведения из 7ки, не могу обойти их стороной, так как с их помощью возможно много чего сделать приложив при этом гораздо меньшие усилия.
Итак, нас интересует несколько методов из AWTUtilities:
  1. AWTUtilities.setWindowShape(Window, Shape) - позволяет установить любому окну определенную форму (будь то круг или хитрый полигон). Для корректной установки формы окно не должно быть декорировано нативным стилем (setUndecorated(true)).
  2. AWTUtilities.setWindowOpacity (Window, float) – позволяет задать прозрачность окна от 0 (полностью прозрачно) до 1 (непрозрачно). Окно при этом может быть декорировано нативным стилем.
  3. AWTUtilities.setWindowOpaque (Window, boolean) – позволяет полностью спрятать отображение фона и оформления окна, но при этом любой размещенный на нем компонент будет виден. Для корректной установки данного параметра окно также как и в п.1 не должно быть декорировано нативным стилем.

Что же нам это дает? На самом деле - достаточно широкий спектр возможностей. Окнам своего приложения можно задавать любые хитрые формы какие Вам только потребуются, делать «дырки» посреди приложения, создавать кастомные тени под окно, делать приятные на вид попапы и пр. и пр.

Если переходить к конкретике - setWindowShape на деле я никогда не использую, так как задаваемая окну форма строго обрезается по краю и не очень приятно выглядит. На помощь приходит setWindowOpaque - спрятав оформление и фон окна можно с помощью контейнера с кастомным отрисованным фоном создавать абсолютно любые окна. Приведу небольшой пример использования (в нем также есть также использованы некоторые приемы из предыдущих глав поста):

можно взять работающий jar с исходным кодом. Честно скажу, что потратил на данный пример не более десяти минут (из них - минут пять прикидывал как расположить элементы внутри диалога:). Естественно это лишь один из вариантов примемения этих новых возможностей - на самом деле их куда больше.

Единственная неприятная мелочь в использовании AWTUtilities – нестабильная работа на Linux-системах. Т.е. Не везде и не всегда корректно отрабатывает прозрачность окон. Не уверен, проблемы ли это текущей JDK или же ОС.

Создание своих интерактивных компонентов

Я уже поверхностно рассказал о том, как создавать компоненты, UI и некоторые «навороты» для интерфейса своего приложения, но что же делать, если нам нужно добавить функциональную часть к компоненту или создать свой совершенно новый компонент с некоей функциональностью и стилизацией? Стилизовать стандартные компоненты и делать из них отдельные части нового компонента достаточно долгое и нудное занятие, тем более что при малейшем изменении в одном из компонентов вся схема может поехать. Именно в таких случаях стоит сделать свой компонент «с нуля».

Итак, за основу лучше лучше всего взять JComponent и используя paint-методы отрисовать его содержимое. Фактически JСomponent сам по себе - чистый холст с некоторыми зашитыми улучшениями для отрисовками и готовыми стандартными методами setEnabled/setFont/setForeground/setBackground и т.п. Как использовать (и использовать ли их) решать Вам. Все, что Вы будете добавлять в методы отрисовки станет частью компонента и будет отображаться при добавлении его в какой-либо контейнер.

Кстати, небольшое отступление, раз уж зашла речь о контейнерах, - любой наследник и сам JComponent являются контейнерами, т.е. могут содержать в себе другие компоненты, которые будет расположены в зависимости от установленного компоненту лэйаута. Что же творится с отрисовкой чайлд-компонентов, лежащих в данном и как она связана с отрисовкой данного компонента? Ранее я не вдавался подробно в то, как устроены и связаны paint-методы Jcomponent"а, теперь же подробно опишу это…

Фактически, paint() метод содержит в себе вызовы 3ех отдельных методов - paintComponent, paintBorder и paintChildren. Конечно же дополнительно он обрабатывает некоторые «особенные» случаи отрисовки как, например печать или перерисовку отдельной области. Эти три метода всегда вызываются в показанной на изображении выше последовательности. Таким образом сперва идет отрисовка самого компонента, затем поверх рисуется бордер и далее вызывается отрисовка чайлд-компонентов, которые в свою очередь также вызывают свой paint() метод и т.д. Естественно есть еще и различные оптимизации, предотвращающие лишние отрисовки, но об этом подробнее я напишу потом.

Компонент отрисован, но статичен и представляет собой лишь изображение. Нам необходимо обработать возможность управления им мышью и различными хоткеями.
Для этого, во-первых, необходимо добавить соответствующие слушатели (MouseListener/MouseMotionListener/KeyListener) к самому компоненту и обрабатывать отдельные действия.

Чтобы не объяснять все на пальцах, приведу пример компонента, позволяющего визуально ресайзить переданный ему ImageIcon:

можно взять рабочий пример с исходным кодом внутри.

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

  1. Определяемся с функционалом и внешним видом компонента - в данном случае это область с размещенным на ней изображением, бордером вокруг изображения и 4мя ресайзерами по углам. Каждый из ресайзеров позволяет менять размер изображения. Также есть возможность передвигать изображение по области, «схватив» его за центр.
  2. Определяем все необходимые для работы компонента параметры - в данном случае это само изображение и его «опорные» точки (верхний левый и правый нижний углы). Также есть ряд переменных, которые понадобятся при реализации ресайза и драга изображения.
  3. Накидываем заготовку для компонента (желательно отдельный класс, если Вы собираетесь его использовать более раза) - в данном случае создаем класс ImageResizeComponent, определяем все необходимые для отрисовки параметры, переопределяем метод paintComponent() и отрисовываем содержимое. Также переопределяем метод getPreferredSize(), чтобы компонент сам мог определить свой «желаемый» размер.
  4. Реализовываем функциональную часть компонента - в данном случае нам будет достаточно своего MouseAdapter"а для реализации ресайза и передвижения. При нажатии мыши в области проверяем координаты и сверяем имх с координатами углов и самого изображения - если нажатие произошло в районе некого угла - запоминаем его и при драге изменяем его координату, ежели нажатие пришлось на изображение - запоминаем начальные его координаты и при перетаскивании меняем их. И наконец, последний штрих - в mouseMoved() меняем курсор в зависимости от контрола под мышью.
Ничего сложного, правда? С реализацией «кнопочных» частей других компонентов всё еще проще - достаточно проверять, что нажатие пришлось в область кнопки. Параллельно с отслеживанием событий можно также визуально менять отображение компонента (как сделано в данном примере на ресайзерах). В общем сделать можно всё, на что хватит фантазии.

Конечно, я просто описал свои шаги при создании компонентов - они не являются чем-то обязательным и могут быть легко расширены и дополнены.

Важно помнить

Есть несколько вещей, которые стоит при любой работе с итерфейсными элементами в Swing – приведу их в этой отдельной небольшой главе.
  1. Любые операции вызова перерисовки/обновления компонентов должны производится внутри Event Dispatch потока для избежания визуальных «подвисаний» интерфейса Вашего приложения. Выполнить любой код в Event Dispatch потоке достаточно легко:
    SwingUtilities.invokeLater (new Runnable()
    {
    public void run ()
    {
    // Здесь располагаете исполняемый код
    }
    });
    Важно также помнить, что события, вызываемые из различных listener"ов стандартных компонентов (ActionListener/MouseListener и пр.) исходно вызываются из этого потока и не требуют дополнительной обработки.
  2. Из любых paint-методов ни в коем случае нельзя влиять на интерфейс, так как это может привести к зацикливаниям или дедлокам в приложении.
  3. Представьте ситуацию - из метода paint Вы изменяете состояние кнопки, скажем, через setEnabled(enabled). Вызов этого метода заставляет компонент перерисоваться и заново вызвать метод paint и мы опять попадаем на этот злополучный метод. Отрисовка кнопки зациклиться и интерфейс приложения будет попросту висеть в ожидании завершения отрисовки (или как минимум съест добрую часть процессора). Это самый простой вариант подобной ошибки.
  4. Не стоит производить тяжёлые вычисления внутри Event Dispatch потока. Вы можете произвести эти вычисления в отдельном обычном потоке и затем только вызвать обновление через SwingUtilities.invokeLater().
  5. Не стоит, также, производить тяжёлые вычисления внутри методов отрисовки. По возможности стоит выносить их отдельно и кэшировать/запоминать. Это позволит ускорить отрисовку компонентов, улучшить общую отзывчиввость приложения и снизить нагрузку на Event Dispatch поток.
  6. Для обвноления внешнего вида компонентов используйте метод repaint() (или же repaint(Rectangle) – если Вам известна точная область для обновления), сам метод repaint необходимо исполнять в Event Dispatch потоке. Для обновления же расположения элементов в лэйауте используйте метод revalidate() (его нет необходимости исполнять в Event Dispatch потоке). Метод updateUI() может помочь в некоторых случаях для полного обновления элемента (например смене данных таблицы), но его нужно использовать аккуратно, так как он также отменит установленный Вами UI и возьмет UI предоставляемый текущим LaF"ом.
  7. Полная установка LaF всему приложению отменит любые ваши вручную заданные в ходе работы UI компонентов и установит поверх них те UI, которые предлагает устанавливаемый LaF. Поэтому лучше производить установку LaF при загрузке приложения до открытия каких-либо окон и показа визуальных элементов.
Следовние этим простым пунктам позволит Вам не беспокоиться о возникновении непредвиденных «тормозов» или дедлоков в интерфейсе приложения.
Думаю, что этот список можно дополнить еще несколькими пунктами, но они уже будут весьма специфичны/необязательны.

Итоги

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

Еще хотелось бы добавить, что возможно, название статьи может показаться Вам слишком громким и не полностью раскрытым - действительно, я все же не дизайнер и не юзабилити-специалист. Я лишь предлагаю Вам инструменты/способы, которые позволят расширить и улучшить интерфейс приложения. Непосредственно бОльшая (не обязательно вся) часть работы по созданию графики для слайдера/кнопки или определению общего стиля компонентов и самого приложения всегда остается за дизайнером - от этого никуда не уйти.

Все примеры статьи в едином «флаконе». Из начального окна можно выбрать желаемый пример:

  • interface
  • dnd
  • customization
  • интерфейс
  • кастомизация
  • Добавить метки

     

    Пожалуйста, поделитесь этим материалом в социальных сетях, если он оказался полезен!