Расширение функциональности элементов пользовательского интерфейса в Java
На примере реализации нестандартного для Swing поведения меню.
Автор: 22 сентября 2006 года
Довольно часто при создании приложений с GUI (stand alone приложений или
апплетов) приходится сталкиваться с необходимостью несколько изменить внешний
вид и поведение стандартных компонентов пользовательского интерфейса. Иногда
этого хочет заказчик. Иногда этого требует дизайнер интерфейсов. Так или иначе
время от времени такая задача возникает. И не всегда представляется возможным
создать комбинированный из нескольких других элемент пользовательского
интерфейса. Например добавить кнопку закрытия в закладку компонента
JTabbedPane. Или реализовать меню, которое может отображать пиктограмму
над названием пункта меню и кроме этого позволит использовать JMenuBar
непосредственно как контейнер для пунктов меню. Вот такое меню мы и реализуем.
Практически каждый кто писал на java приложения с GUI, использовал в своей
работе классы имплементирующие меню. И наверняка каждый знает, что при создании
меню есть определенные правила, все это хорошо описано в сановском туториал,
книгах и статьях. И каждый знает об ограничениях, которые накладывает этот
подход – чтобы использовать JMenuItem, необходимо создать экземпляр
JMenu – контейнер для нашего итема, и уже только этот контейнер мы можем
поместить в JMenuBar. Но ведь во многих windows-приложениях пункт меню
может быть виден и использоваться прямо из панели меню. К сожалению разработчики
Swing не заложили такой возможности в библиотеку. На самом деле это не сложно
изменить. Для того чтобы проделать такой фокус нам нужно будет расширить класс
JMenu – должен заметить, что мне не удалось реализовать прослушивание
событий как это принято, пришлось пойти на ухищрения и изменить эту схему. Если
кто то знает как это можно сделать, буду благодарен за комментарии и дополнения.
Итак, задача: в создаваемом приложении реализовать пункт меню, который будет
находиться непосредственно в панели меню, иметь возможность использовать
картинки и текст, и его поведение будет соответствовать стандартному пункту меню
– если какой то пункт выбран – можно перемещаться с помощью клавиш курсора
влево-вправо и реагировать на клавиши пробел и Enter. Выглядеть это будет
примерно так:
При этом наше меню должно отображать пиктограмму и название итема –
пиктограмма сверху а текст снизу, при этом корректно реагировать на ситуации
когда пиктограмма или текст отсутствуют, и иметь возможность помещать как
отдельные элементы, так и элементы-контейнеры.
Таким образом задача разбивается на следующие подзадачи:
расширить функциональность классов которые создают меню чтобы их поведение
удовлетворяло поставленным требованиям
корректно отрисовать наше меню.
Начнем реализацию со второй подзадачи.
Так как правильно и однотипно в панели меню отображаться должны и
элементы-контейнеры(JMenu) и элементы-итемы (JMenuItem)
напрашивается решение унаследовать наш класс от JMenu. Так мы и сделаем.
Все что нужно будет изменить в нашем классе – это определить размеры нашего
пункта меню и отрисовать его.
В классе обьявлено несколько констант для отступов от границ пунктов меню и
между пиктограммой и текстом, а также значения размера для пункта меню по
умолчанию, если ни текст ни пиктограмма не были заданы. Также обьявлены
экземплярные переменные icon и title для реализации заявленной
функциональности – отображения пиктограммы и надписи.
public class PictMenu extends JMenu {
static final long serialVersionUID = -1;
protected final int DEFAULT_WIDTH = 50;
protected final int DEFAULT_HEIGHT = 24;
protected final int TOP_DROP = 7;
protected final int PICT_DROP = 10;
protected final int BOTTOM_DROP = 7;
protected final int STR_DROP = 10;
protected final int BETWEEN_DROP = 0;
protected int px = 0;
protected int py = 0;
protected int sx = 0;
protected int sy = 0;
protected ImageIcon icon = null;
protected String title = "";
В классе имплементированы два метода и конструктор с параметрами. В
конструкторе инициализируем экземплярные переменные и определяем соответствующие
размеры пункта меню.
public PictMenu(String title, ImageIcon pict) {
super("");
this.title = title;
this.icon = pict;
Dimension dim = computeSize();
setPreferredSize(dim);
setMinimumSize(dim);
}
protected Dimension computeSize() {
int w = DEFAULT_WIDTH;
int h = DEFAULT_HEIGHT;
int separator = BETWEEN_DROP;
int strWidth = 0;
int strHeight = 0;
int pictWidth = 0;
int pictHeight = 0;
if ((title == null || title.length() == 0 ) && icon == null) {
return new Dimension(w, h);
}
else {
if (title == null || title.length() == 0) {
separator = 0;
}
else {
Font font = getFont();
FontMetrics fm = getFontMetrics(font);
strWidth = fm.stringWidth(title);
strHeight = fm.getHeight();
}
if (icon == null) {
separator = 0;
}
else {
pictWidth = icon.getIconWidth();
pictHeight = icon.getIconHeight();
py = TOP_DROP;
}
w = (pictWidth >= strWidth) ?
(pictWidth + PICT_DROP * 2) :
(strWidth + STR_DROP * 2);
px = (w - pictWidth) / 2;
sx = (w - strWidth) / 2;
}
h = TOP_DROP + pictHeight + separator + strHeight + BOTTOM_DROP;
sy = h - BOTTOM_DROP;
return new Dimension(w, h);
}
Второй метод ответственнен за отображение. Вообще, чтобы нарисовать на
Swing-компоненте то что нам хочется, достаточно переопределить метод
paintComponent(Graphics). Метод обьявлен в классе JСomponent и
служит для вызова делегированного paint() метода соответствующего
ComponentUI обьекта. Метод isSelected() родительского класса
JMenu возвращает true если наш пункт меню был выбран. В этом
случае мы добавляем прорисовку рамки чтобы визуально выделить выбранный пункт и
сдвигаем картинку и надпись на 1 пиксел вниз-вправо.
Вторая часть нашей задачи – сделать возможным добавление итема в контейнер
JMenuBar. Казалось бы очевидное решение – изменить поведение контейнера
чтобы он корректно обрабатывал добавление итемов. Однако на самом деле это не
будет самым простым решением – придется переписать довольно много кода и
придется создать класс, который будет наследовать JMenuItem и
прорисовывать итем аналогично тому как мы сделали в первой части задачи. Можно
пойти другим путем. У нас уже есть класс который умеет себя правильно и
однотипно прорисовывать в контейнере. Контейнер корректно размещает инстансы
этого класса и передает сообщения о действиях пользователя. Что нам нужно – это
как то спрятать выпадающий список включенных элементов и дать возможность
программисту реализовывать реакцию на действия с этим пунктом меню.
Необходимость прятать выпадающий список появляется из-за того, что даже если
мы не поместим ни одного итема в контейнер, то класс JMenu прорисовывает
рамку этого списка, такой квадратик 2х2 пиксела. Выглядит не очень красиво. Этот
выпадающий список реализован как popup-menu, координаты для отображения которого
рассчитываются контейнером. Доступ обьявлен как private, напрямую
добраться к нему нельзя. Но есть public - метод getPopupMenu(), и
мы установим для внутреннего popup-menu размер 0х0 пикселов. Все, больше мы его
не увидим.
Итак, создаем наследника от ранее нами созданного класса PictMenu.
В конструкторе делаем вызов getPopupMenu().setPreferredSize(new
Dimension(0, 0));
Еще мы должны диспетчеризовать события клавиатуры, поэтому наш класс будет
реализовывать интерфейс KeyEventDispatcher и мы зарегистрируем его
в DefaultKeyboardFocusManager.
public class SimplePictMenu extends PictMenu implements KeyEventDispatcher {
static final long serialVersionUID = -1;
public SimplePictMenu(String title, ImageIcon pict) {
super(title, pict);
getPopupMenu().setPreferredSize(new Dimension(0, 0));
DefaultKeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(this);
addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent me) {
sendCancelKeyEvent();
}
public void mouseClicked(MouseEvent me) {
sendCancelKeyEvent();
doAction();
}
});
}
Интерфейс KeyEventDispatcher обьявляет единственный метод
dispatchKeyEvent в который передается событие клавиатуры. События
клавиатуры, которые необходимо обработать – нажатие enter или пробел – вызов
метода-обработчика пункта меню, нажатие курсорных клавиш и клавиши esc уже умеет
обрабатывать наш суперкласс JMenu.
Поэтому мы обьявим метод с пустой реализацией doAction() который будет
переопределяться и в котором будет код, реализующий действия при выборе пункта
меню.
public void doAction() {
}
После вызова doAction() необходимо снять выделение с пункта меню. Для
этого мы генерируем событие, как будто пользователь нажал клавишу esc и передаем
его для обработки вызовом processKeyEvent().
protected final void sendCancelKeyEvent() {
KeyEvent kee = new KeyEvent((Component)this, 401, 0l, 0, 27, (char)27);
dispatchKeyEvent(kee);
}
public boolean dispatchKeyEvent(KeyEvent e) {
KeyEvent kee = new KeyEvent((Component)this, e.getID(), e.getWhen(),
e.getModifiers(), e.getKeyCode(),e.getKeyChar());
if (isSelected()) {
if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_SPACE) {
kee = new KeyEvent((Component)this, e.getID(), e.getWhen(),
e.getModifiers(), 27, (char)27);
doAction();
}
}
processKeyEvent(kee);
return true;
}
Чтобы пункт меню так же реагировал на действия мыши регистрируем
MouseListener и имплементируем методы mouseReleased() и
mouseClicked().
В результате, создав два совсем простых класса мы получили значительное
изменение функциональности меню стандартного пользовательского интерфейса.
Java 5 обладает некоторыми полезными возможностями. В данной статье мы рассмотрим их и узнаем, как можно извлечь из них выгоду. В этой части мы рассмотрим auto-boxing foreach... подробнее
В первой части мы обсудили новые возможности Java 5 относительно функции auto-boxing и цикла foreach. В данной части мы обсудим поддержку функций с переменным числом аргументов и статическое импортирование (static import). Поскольку другие функции, такие как enum, annotation, и generics, заслуживают отдельной статьи, мы их не будет демонстрировать в данной... подробнее
Довольно часто при создании приложений с GUI (stand alone приложений или апплетов) приходится сталкиваться с необходимостью несколько изменить внешний вид и поведение стандартных компонентов пользовательского интерфейса... подробнее
Что такое сервлет ? Это класс порожденный от класса HttpServlet с переопреденными методами doGet и doPost (управление приходит в один из этих методов в зависимости от того какого типа был запрос. Надеюсь у Вас есть некоторый опыт в cgi-программировании... подробнее
Технология Java Server Pages (JSP) является составной частью единой технологии создания бизнес-приложений J2EE. JSP - это альтернативная методика разработки приложений... подробнее