
Портянкин И. Swing
.pdfМеню и панели инструментов |
275 |
JComponent, и для вывода на экран его просто делают видимым и добавляют в специально предназначенный для этого слой POPUP_LAYER многослойной панели JLayeredPane. Однако как только всплывающее меню начинает «вылезать» за границы окна, в котором оно должно быть показано, или используется в AWT-приложении, все меняется — всплывающее меню выводится в собственном отдельном тяжеловесном окне без рамки. Для программиста вся эта кухня остается незаметной, что, конечно, облегчает программирование. Впрочем, вы можете включиться в процесс и с помощью метода setLightweightPopupEnabled() рекомендовать для всплывающего меню тип компонента (легковесный или тяжеловесный). Иногда это может быть полезно.
Загрузка меню из файлов XML
Во всех примерах с меню, что мы разобрали, можно было заметить весьма неприятную тенденцию: создание нескольких полноценных пунктов меню (со значками, мнемониками, акселераторами и обработчиками событий) сильно раздувало код наших программ. Можно себе представить, насколько сильно разрастется код программы, обладающей более или менее большими возможностями, и соответственно большой системой меню. Выходом из этой ситуации может стать использование архитектуры Action, встроенной во все кнопки библиотеки, и такое решение будет действительно хорошим, особенно если эти же команды будут использоваться и для создания кнопок на панели инструментов, и для возможных контекстных меню. Но и у этого подхода есть свои недостатки — для каждой команды придется писать довольно объемный класс, и таких классов-близнецов может быть очень много.
С другой стороны, существует множество визуальных инструментов для построения пользовательского интерфейса, и в них почти всегда есть средства для создания меню произвольной сложности. Хотя после их использования качество и чистота кода оставляют желать лучшего, а стоимость поддержки программы возрастает (а иногда вообще невозможна без примененного визуального средства), в случае с меню они существенно сэкономят ваше время.
Наконец, можно просто вспомнить о том, что система меню представляет собой несложную иерархическую структуру, в которой в качестве узлов выступают выпадающие меню JMenu, листьями являются пункты меню, такие как JMenuItem и JRadioButtonMenuItem, а корнем всей системы является строка меню JMenuBar. У каждого члена этой иерархии есть некоторые атрибуты: название, значок, текст и т.п. Не правда ли, описанная структура так и просится в файл формата XML? Именно формат XML предназначен для описания произвольных иерархических структур, узлы которых могут иметь набор некоторых атрибутов. Придумав подходящее описание XML для меню Swing, а затем написав специальный инструмент для загрузки, мы сможем мгновенно загружать самые запутанные и сложные меню. Кстати, подобный подход часто используется во многих графических системах: меню описывается на некотором текстовом языке и помещается в «ресурсы» программы, откуда загрузить его не составляет особого труда.
Прежде всего необходимо определить то, как будет выглядеть файл XML с описанием системы меню. Наиболее логичным смотрится следующий вариант:
<?xml version="1.0" encoding="UTF-8"?>
<menubar name="mainMenu">
<menu name="file" text="Файл" mnemonic="Ф">
<menuitem name="create" text="Создать" mnemonic="Н" accelerator="control N"/>
<menuitem name="open" text="Открыть" mnemonic="О"

276 ГЛАВА 10
enabled="false"/>
<menuitem name="close" text="Закрыть" mnemonic="З" enabled="false"/>
<menuitem name="separator"/>
<menu name="print" text="Печать" mnemonic="П"> <menuitem name="preview"
text="Предварительный просмотр" mnemonic="р"/>
</menu>
<menuitem name="separator"/>
<menuitem name="exit" label="Выход" mnemonic="В" accelerator="alt X"/>
</menu>
</menubar>
После стандартного заголовка файла XML следует описание элементов нашей системы меню. Корнем системы, как и следует ожидать, стал элемент с названием menubar: он будет описывать строку меню, в которой затем разместятся все выпадающие меню и пункты меню. У элемента menubar имеется только один атрибут name (имя), по этому имени мы затем сможем найти его3. Чтобы при поиске элементов меню не возникало ошибок, имена всех элементов должны быть уникальными. К строке меню присоединяются выпа-
дающие меню: для XML это означает наличие элементов-потомков с названием menu. Как правило, для выпадающих меню настраивается два свойства: text (текст меню) и mnemonic
(символ мнемоники). Данные свойства будут задаваться в виде атрибутов элемента menu. Выпадающие меню состоят из пунктов меню и, если это необходимо, из других выпадаю-
щих меню (это позволит организовать сложную иерархическую систему меню). Поэтому потомками элемента menu смогут быть элементы menuitem (пункт меню) или другие элементы menu. Больше всего атрибутов поддерживает элемент menuitem: для него часто задаются не только текст меню и символ мнемоники, но и сочетание клавиш быстрого доступа (accelerator)4, а также то, доступен ли он в данный момент пользователю (enabled). Кроме того, в качестве особых элементов могут выступать разделители меню. Для них мы зарезервировали специальное имя separator. При написании файла XML, описывающего меню, не забывайте о том, что все элементы XML обязательно должны закрываться. Для системы меню это имеет дополнительное значение: пока вы не закроете элемент menu, все следующие за ним пункты меню будут попадать в описываемое им выпадающее меню.
Созданное нами описание системы меню на языке XML интуитивно понятно и позволяет мгновенно описать сложную систему меню, «на лету» задавая наиболее важные свойства пунктов меню. Теперь нам необходимо написать инструмент, распознающий подобный файл XML и создающий на его основе меню Swing. Стандартные библиотеки Java содержат все необходимые инструменты для работы с XML, в том числе и упрощенный интерфейс SAX (Simple API for XML). Именно этот интерфейс мы применим для распознавания системы меню. Вот что у нас получится:
// com/porty/swing/XMLMenuLoader.java
3 Здесь может показаться, что имя для строки меню ни к чему, ведь она в нашем файле описана одна. Однако чуть позже мы увидим, что добавить можно сколь угодно строк меню, так что имя все же нужно.
4 Нам не составит труда распознать строку, описывающую клавиши быстрого доступа: в классе KeyStroke имеется перегруженный метод getKeyStroke(), позволяющий получить клавиатурное сокращение на основе строки, составленной по определенным правилам. Описание этих правил вы сможете найти в интерактивной документации класса KeyStroke.
Меню и панели инструментов |
277 |
// Инструмент для загрузки меню из файла XML package com.porty.swing;
import javax.swing.*; import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler; import java.io.*;
import javax.xml.parsers.*; import java.awt.event.*; import java.util.*;
public class XMLMenuLoader {
//источник данных XML private InputSource source;
//анализатор XML
private SAXParser parser; // обработчик XML
private DefaultHandler documentHandler;
//хранилище для всех частей системы меню private Map<String, JComponent> menuStorage
=new HashMap<String, JComponent>();
//конструктор, требует задать поток данных с меню public XMLMenuLoader(InputStream stream) {
//настраиваем источник данных XML try {
Reader reader = new InputStreamReader(stream, "UTF-8"); source = new InputSource(reader);
parser = SAXParserFactory. newInstance().newSAXParser();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
//создаем обработчик XML
documentHandler = new XMLParser();
}
// считывает XML и создает систему меню public void parse() throws Exception { parser.parse(source, documentHandler);
}

278 ГЛАВА 10
// позволяет получить строку меню
public JMenuBar getMenuBar(String name) { return (JMenuBar) menuStorage.get(name);
}
//позволяет получить выпадающее меню public JMenu getMenu(String name) {
return (JMenu) menuStorage.get(name);
}
//позволяет получить элемент меню
public JMenuItem getMenuItem(String name) { return (JMenuItem) menuStorage.get(name);
}
//удобный метод для быстрого добавления
//слушателя событий
public void addActionListener(String name, ActionListener listener) {
getMenuItem(name).addActionListener(listener);
}
// текущая строка меню
private JMenuBar currentMenuBar;
// список для упорядочения выпадающих меню
private LinkedList<JMenu> menus = new LinkedList<JMenu>();
// обработчик XML
class XMLParser extends DefaultHandler { // новый узел XML
public void startElement(String uri, String localName,
String qName, Attributes attributes) {
// определяем тип узла
if (qName.equals("menubar")) parseMenuBar(attributes);
else if (qName.equals("menu")) parseMenu(attributes);
else if (qName.equals("menuitem")) parseMenuItem(attributes);
}
// конец узла, используется для смены выпадающих меню public void endElement(String uri, String localName,
String qName) {
if (qName.equals("menu")) menus.removeFirst();
}
Меню и панели инструментов |
279 |
// создает новую строку меню
protected void parseMenuBar(Attributes attrs) { JMenuBar menuBar = new JMenuBar();
// определяем имя
String name = attrs.getValue("name"); menuStorage.put(name, menuBar); currentMenuBar = menuBar;
}
// создает новое выпадающее меню
protected void parseMenu(Attributes attrs) { // создаем меню
JMenu menu = new JMenu();
String name = attrs.getValue("name");
//настраиваем общие атрибуты adjustProperties(menu, attrs); menuStorage.put(name, menu);
//добавляем меню к предыдущему выпадающему
//меню или к строке меню
if ( menus.size() != 0 ) { menus.getFirst().add(menu);
} else { currentMenuBar.add(menu);
}
// добавляем в список выпадающих меню menus.addFirst(menu);
}
// новый пункт меню
protected void parseMenuItem(Attributes attrs) {
//проверяем, не разделитель ли это
String name = attrs.getValue("name"); if (name.equals("separator")) {
menus.getFirst().addSeparator(); return;
}
//создаем пункт меню
JMenuItem menuItem = new JMenuItem();
//настраиваем свойства adjustProperties(menuItem, attrs); menuStorage.put(name, menuItem);
//добавляем к текущему выпадающему меню menus.getFirst().add(menuItem);
}
280 |
ГЛАВА 10 |
// настройка общих атрибутов пунктов меню private void adjustProperties(
JMenuItem menuItem, Attributes attrs) {
//получаем поддерживаемые атрибуты
String text = attrs.getValue("text");
String mnemonic = attrs.getValue("mnemonic"); String accelerator = attrs.getValue("accelerator"); String enabled = attrs.getValue("enabled");
//настраиваем свойства меню menuItem.setText(text);
if (mnemonic != null) { menuItem.setMnemonic(mnemonic.charAt(0));
}
if (accelerator != null) { menuItem.setAccelerator(
KeyStroke.getKeyStroke(accelerator));
}
if (enabled != null) { menuItem.setEnabled(Boolean.valueOf(enabled));
}
}
}
}
Наш инструмент для распознавания системы меню разместится в библиотечном пакете com.porty.swing — так вы сможете легко присоединять его к своим программам. Начинается работа с конструктора класса XMLMenuLoader: в качестве параметра конструктора необходимо указывать поток данных (InputStream), откуда будут считываться информация. Поток InputStream прежде всего преобразуется в символьный поток
Reader: если мы этого не сделаем, то неминуемо получим проблемы с кодировками символов, отличных от ASCII. Так как мы применяем XML, инструмент подразумевает, что
данные находятся в кодировке UTF-8. Далее необходимо получить источник данных InputSource, именно с источником данных такого типа работает любой распознаватель
XML. Как видно из программы, сделать это легко: достаточно передать символьный поток Reader в конструктор класса InputSource. После этого нам остается настроить сам распознаватель XML — для этого пригодится фабрика классов SAXParserFactory. В кон-
це конструктора мы создаем обработчик данных XML, именно в этот обработчик будет
поступать вся информация от распознавателя.
После вызова конструктора инструмент XMLMenuLoader готов к работе. Для начала распознавания необходимо вызвать метод parse(). Данный метод очень прост: он «вклю-
чает» распознаватель XML, передавая ему в качестве параметров источник данных и ссылку на обработчик распознанных элементов.
Таким образом, основная работа инструмента выполняется во внутреннем классе XMLParser, который обрабатывает распознанные элементы XML. Обработчик в интер-
фейсе SAX (а мы выбрали для обработки именно этот интерфейс) пассивен: он ожидает,
пока распознаватель не вызовет один из определенных в обработчике методов. В нашем обработчике основной метод — startElement(), он вызывается распознавателем при обна-

Меню и панели инструментов |
281 |
ружении нового элемента XML. В качестве параметров данному методу передается название элемента и набор его атрибутов. Выяснив, какое название у текущего элемента, мы выполняем соответствующие действия.
Для элемента с названием menubar (он описывает строку меню) вызывается метод parseMenuBar(), в качестве параметров этого метода передаются атрибуты элемента. Прежде всего создается новая строка меню JMenuBar. Атрибуты используются для по-
лучения уникального имени элемента (как вы помните, при описании структуры меню на XML все элементы должны иметь уникальное имя в атрибуте name). С помощью уникального имени новая строка меню размещается в ассоциативном массиве menuStorage,
откуда ее всегда можно получить обратно как раз по имени. Строка меню, распознанная последней, также сохраняется в ссылке currentMenuBar: с помощью этой ссылки в строку
меню будут добавляться выпадающие меню.
Выпадающее меню, описанное в элементе с названием menu, создается и настраивается в методе parseMenu(). Для начала создается новый объект JMenu, после чего мы по-
лучаем уникальное имя нового выпадающего меню. Прежде чем разместить новое меню в массиве menuStorage, для него настраиваются общие для всех пунктов меню свойства
(текст, символ мнемоники, клавиши быстрого доступа, доступность). Настройка выполняется в методе adjustProperties(): для всех поддерживаемых свойств мы получаем значе-
ния атрибутов с соответствующими именами, для каждого атрибута выясняем, не равен ли он пустой ссылке null (если атрибут для текущего элемента не описан, то вместо него
будет возвращена пустая ссылка), и если нет, то настраиваем соответствующее свойства пункта меню. Заметьте, что метод adjustProperties() работает одинаково для выпадающих меню JMenu и пунктов меню JMenuItem, и в этом нет ничего удивительного — достаточно вспомнить, что класс JMenu унаследован от JMenuItem.
После настройки свойств остается выяснить, куда необходимо добавить новое выпадающее меню. Все дело в том, что выпадающее меню может быть добавлено как в строку меню, так и в другое выпадающее меню (при сложной иерархической системе меню).
Чтобы поддерживать меню произвольной сложности, мы вводим список выпадающих меню LinkedList с названием menus. Каждое новое меню добавляется в этот список первым (методом addFirst()). Следующее выпадающее меню будет добавлено в то меню, что
находится в списке на первой позиции, и после этого само станет первым, так что следующее за ним меню будет добавлено уже в него. Таким образом мы сможем поддерживать любое количество выпадающих меню. Остается только удалять из списка первое
выпадающее меню, когда в файле XML закрывается элемент menu. Сделать это несложно: при закрытии элемента вызывается метод endElement(), именно в нем мы удаляем первое меню методом removeFirst(), если название элемента нам подходит (menu)5. В том
случае если список выпадающих меню пуст, новое меню добавляется непосредственно в строку меню.
Наконец, обычный пункт меню, описываемый элементом с названием menuitem, обрабатывается в методе parseMenuItem(). Прежде всего мы проверяем, не описывает ли данный элемент разделитель (в таком случае атрибут name будет равен строке separator),
и если это так, просто добавляем разделитель в текущее выпадающее меню (его позволя-
ет получить список выпадающих меню menus). Если же элемент описывает «настоящий» пункт меню, то мы создаем объект JMenuItem, настраиваем его свойства (с помощью все того же метода adjustProperties()), добавляем в массив menuStorage и присоединяем к те-
кущему выпадающему меню. Обратите внимание, что пункты меню присоединяются к выпадающему меню без проверки на его наличие, так что при написании XML следите за тем, чтобы пункты меню всегда находились внутри элементов menu.
5 Обратите внимание, что в данном случае список LinkedList используется фактически в роли стека, благодаря наличию методов getFirst(), addFirst() и removeFirst() (эквивалентных просмотру верхнего элемента стека, «проталкиванию» элемента в стек и «выталкиванию» его оттуда).
282 |
ГЛАВА 10 |
После завершения анализа файла XML система меню создана, все выпадающие меню
и пункты меню присоединены друг к другу, а ссылки на все созданные элементы меню хранятся в ассоциативном массиве menuStorage. Для дальнейшей работы с меню необ-
ходимо предоставить способ получения созданных элементов меню по их уникальному
имени, так чтобы программа смогла добавить в окно строку меню и зарегистрировать слушателей событий для пунктов меню. Для этого в классе XMLMenuLoader имеется несколько удобных методов для получения элементов меню: метод getMenuBar() позволяет получить по имени строку меню, метод getMenu() возвращает выпадающее меню, наконец, метод getMenuItem() возвращает пункт меню. Еще один вспомогательный метод с названием addActionListener() чрезвычайно удобен для быстрого присоединения слушателя ActionListener к пункту меню с именем, указанным в качестве параметра метода. Приме-
няя данный метод, вы сможете еще больше ускорить настройку системы меню. Обратите внимание, что благодаря использованию анализатора SAX наш инструмент
крайне нетребователен к структуре документа XML. Главное, чтобы элементы menubar, menu и menuitem были описаны в подходящем порядке. Используя эту возможность, вы можете описать в одном файле XML и строку меню для нашего инструмента (или даже несколько), и что-то еще, если вам это понадобится.
Ну а теперь остается проверить наш новый инструмент в работе. Теперь попробуем загрузить систему меню, описанную нами еще в начале раздела, в обычное окно JFrame. Описание меню на языке XML будет находиться в файле menu.xml, вы сможете найти его вместе с исходными текстами программы:
//TestXMLMenuLoader.java
//Проверка загрузки системы меню из файла XML import javax.swing.*;
import com.porty.swing.*; import java.io.*;
import java.awt.event.*; import java.awt.*;
public class TestXMLMenuLoader extends JFrame { public TestXMLMenuLoader() {
super("TestXMLMenuLoader"); setDefaultCloseOperation(EXIT_ON_CLOSE); // открываем файл XML с описанием меню try {
InputStream stream =
new FileInputStream("menu.xml");
//загружаем меню
XMLMenuLoader loader =
new XMLMenuLoader(stream); loader.parse();
//устанавливаем строку меню setJMenuBar(loader.getMenuBar("mainMenu"));
//быстрое присоединение слушателя loader.addActionListener("exit",
new ActionListener() {

Меню и панели инструментов |
283 |
public void actionPerformed(ActionEvent e) { System.exit(0);
}
});
}catch (Exception ex) { ex.printStackTrace();
}
// выводим окно на экран setSize(300, 200); setVisible(true);
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new TestXMLMenuLoader(); } });
}
}
Пример чрезвычайно прост: мы наследуем класс своего приложения от окна с рамкой JFrame, и задаем для него небольшой размер. Система меню загружается прямо в конструкторе окна (приходится применять блок обработки исключений try/catch, так
как происходит работа с вводом/выводом и всегда возможно исключение типа IOException): мы открываем файл menu.xml, передаем поток с данными в наш новый инструмент XMLMenuLoader и загружаем систему меню, вызывая метод parse(). После этого все эле-
менты системы меню находятся в хранилище под своими уникальными именами. Далее нам остается получить строку меню (имя ее нам известно из описания меню на языке
XML) и присоединить ее к своему окну. Также демонстрируется удобство метода addActionListener() — он одновременно получает необходимый пункт меню по имени и сразу же
присоединяет к нему слушателя событий.
Запустив пример, вы увидите описанную нами на языке XML систему меню в окне, и оцените скорость, с которой можно создать сложную систему меню, настроенную до мелочей (включая мнемоники, клавиши быстрого доступа и возможность перевода на любой язык в кодировке UTF-8).
284 |
ГЛАВА 10 |
Создание аналогичной системы меню в коде вылилось бы в немалое количество строк кода, которые к тому же были бы трудно поддерживаемыми и не слишком хо-
рошо читаемыми. Вы легко сможете доработать созданный нами инструмент, так чтобы он поддерживал больше свойств (понадобится доработать метод adjustProperties())
и остальные элементы системы меню, такие как меню с флажками. Только не забывайте, что наш инструмент манипулирует компонентами Swing, а именно элементами меню, это значит, что вся работа с ним должна идти из потока рассылки графических событий.
Идея применения декларативного языка, такого как XML, для описания простого интерфейса не нова, и на этой ниве испытывали свои силы многие программисты. Вы можете попробовать использовать довольно известный проект SwiXML, также позволяющий создать компоненты Swing из описания XML. Для системы меню или простых компонентов этот подход работает прекрасно, но вот со сложными интерфейсами поддержка XML вряд ли проще создания интерфейса прямо в коде. К тому же в коде, в хорошем средстве разработки (IDE) у вас будут и автоматическая проверка корректности типов, и автоматическое дополнение имен классов и переменных, и многое другое, что в обычном описании недоступно.
Панели инструментов
Панели инструментов предназначены для вывода на экран набора кнопок (как правило, кнопок особого вида: с краткими надписями или вовсе без них, но с подсказками и с небольшими четко различимыми значками), инициирующих запуск наиболее часто используемых команд приложения. В панелях инструментов также встречаются наиболее востребованные пользователями компоненты, находить которые в меню или диалоговых окнах долго и неудобно. Продуманные панели инструментов значительно повышают привлекательность приложения и «привязывают» к себе пользователя, который мгновенно привыкает к ним.
В Swing панели инструментов представлены компонентом JToolBar. Этот компонент довольно прост в работе и настройке, тем не менее, с его помощью вы сможете создавать любые панели инструментов, способные на многое, а понадобится для этого всего несколько строк кода.
Простые панели инструментов
Создание панели инструментов в Swing не таит в себе никаких трудностей. Вы создаете компонент JToolBar, добавляете в него свои кнопки или другие компоненты (особенно удобно использовать для панелей инструментов «команды» Action, которые позволяют в одном месте указать и параметры внешнего вида кнопки, и описать то, что должно происходить при щелчке на ней) и выводите панель инструментов на экран. Проиллюстрирует сказанное следующий пример:
//SimpleToolbars.java
//Простые панели инструментов import javax.swing.*;
import java.awt.event.*; import java.awt.*;
public class SimpleToolbars extends JFrame { public SimpleToolbars() {
super("SimpleToolbars");