Доброго времени суток, дорогие читатели.
Недавно столкнулся с проблемой создания панели меню — стандартной панели, которая присутствует почти в каждом мало-мальски функциональном UI. И не с той проблемой, что ее сложно создать, напротив, в Swing создание UI занятие довольно нетривиальное, большинство элементов интерфейса создаются в пару строк кода (это в равной степени касается и меню). А с проблемой создания многофункционального, сложного меню, которое выливается в невероятное количество кода, который в последствии не то чтобы сопровождать, а порой даже и читать невозможно.
Для хранения всего процесса генерации меню любой сложности, решил я прибегнуть к силе под названием XML. С его помощью можно описать меню, как мы хотим его видеть, а благодаря классу-утилите, который не приминем создать, наш XML будет распарсен и в итоге можно будет получить то, ради чего все это и затевается — меню любой сложности. Если вас это заинтересовало, добро пожаловать под хаброкат...
Итак, для начала опишем меню, которое мы хотим привязать к нашему UI. В качестве примера я решил использовать немного урезанное меню, в основу которого легло меню, которое многие из нас наблюдали в Google Docs (да простит меня Google):
- File -> New -> Список документов для создания
- File -> Open
- File -> Download as -> варианты закачки
- File -> Print
- Edit -> Undo
- Edit -> Redo
- Edit -> Cut
- Edit -> Copy
- Edit -> Paste
- Format -> Paragraph Styles -> Normal Text -> варианты формата обычного текста
- Format -> Paragraph Styles -> Heading -> варианты формата заголовка
- Format -> Paragraph Styles -> Options -> разные настройки стилей
- Help -> Report an issue
- Help -> Report abuse
- Help -> Keyboard shortcuts
Помимо структуры, у некоторых из описанных элементов будет картинка, клавиатурное сокращение и мнемоника. Вот такое незамысловатое меню и попробуем создать.
Описание элементов меню в формате XML
Перед началом создания XML файла, описывающее наше, частично заимствованное (только в качестве примера), красивое меню, определимся какие элементы в нем присутствуют.
В первую очередь это JMenuBar, панель на которой будут располагаться описанные ниже элементы. И вот эту панель мы и будем вытаскивать после обработки XML.
Далее у нас следуют представители JMenu — меню и подменю собственной персоной (File, Edit, Paragraph Styles и т.д.). А также JMenuItem — элементы всех меню и подменю (Open, Copy, Paste, Report abuse и т.д.). И конечно же разделители (сепараторы) — горизонтальные линии, с помощью которых элементы меню объединяются в логические группы.
У каждого элемента есть еще ряд свойств, таких как — имя элемента (по нему мы сможем обратиться к интересующему нас элементу из кода), текст, наличие картинки возле текста, комбинация клавиш, по которой можно сфокусироваться на элементе с помощью клавиатуры (т.н. мнемоники — отображаются в виде подчеркнутого элемента в имени и вызываются комбинацией alt + выделенный_символ_в_имени. Например, alt+F, для открытия меню File) и клавиатурное сокращение (т.н. акселератор — отображается в виде комбинации клавиш рядом с элементом меню для быстрого его использования, без вхождения в само меню. Например, Ctrl+O для открытия файла). Также элемент может быть активным и неактивным (Edit -> Cut, если не выделен текст).
С кирпичиками, из которых состоит меню, ознакомились. Осталось рассмотреть XML теги и имена свойств, которые будут соответствовать элементам, описанным выше.
Теги: JMenuBar — , JMenu — , JMenuItem — , разделитель — <separator/>
Свойства: имя — name, текст — text, картинка — image, мнемоника — mnemonic, акселератор — accelerator, доступность элемента — isEnabled.
Знакомьтесь, XML, описывающий меню!
Определившись с XML элементами нашего меню, можно незамедлительно приступить к описанию меню, которое мы хотим в итоге получить:
<?xml version="1.0" encoding="utf-8"?>
<menubar name="ourMenu">
<!--File-->
<menu name="file" text="File" mnemonic="F">
<menu name="new" text="New" mnemonic="N">
<menuitem name="doc" text="Document" image="./data/MenuXML/img1.jpg"/>
</menu>
<menuitem name="open" text="Open..." mnemonic="O" accelerator="control O"/>
<separator/>
<menu name="download" text="Download as">
<menuitem name="msword" text="Microsoft Word (.docx)"/>
</menu>
<separator/>
<menuitem name="print" text="Print" mnemonic="P" accelerator="control P"/>
</menu>
<!--Edit-->
<menu name="edit" text="Edit" mnemonic="d">
<menuitem name="undo" text="Undo" image="./data/img1.jpg" accelerator="control Z" isEnabled="false"/>
<menuitem name="redo" text="Redo" image="./data/img1.jpg" accelerator="control Y" isEnabled="false"/>
<separator/>
<menuitem name="cut" text="Cut" image="./data/img1.jpg" accelerator="control X" isEnabled="false"/>
<menuitem name="copy" text="Copy" image="./data/img1.jpg" accelerator="control C" isEnabled="false"/>
<menuitem name="paste" text="Paste" image="./data/img1.jpg" accelerator="control V" isEnabled="false"/>
</menu>
<!--Format-->
<menu name="format" text="Format" mnemonic="o">
<menu name="styles" text="Paragraph styles">
<menu name="normalText" text="Normal text">
<menuitem name="applyNormText" text="Apply normal text" accelerator="control alt 0"/>
</menu>
<menu name="heading" text="Heading">
<menuitem name="applyHeading" text="Apply heading" accelerator="control alt 1"/>
</menu>
<separator/>
<menu name="options" text="Options" image="./data/img1.jpg">
<menuitem name="saveOptions" text="Save options"/>
</menu>
</menu>
</menu>
<!--Help-->
<menu name="help" text="Help" mnemonic="H">
<menuitem name="issue" text="Report an issue"/>
<menuitem name="abuse" text="Repor abuse"/>
<separator/>
<menuitem name="shortcuts" text="Keyboard shortcuts" accelerator="control SLASH"/>
</menu>
</menubar>
XML-обработчик
Теперь, когда мы описали наше меню в рамках XML, осталось его только распарсить. Для работы с нашей XML воспользуемся Java пакетом org.xml.sax (с API можно ознакомиться здесь и здесь). За обработку элементов нашей XML структуры будет отвечать класс XMLMenuHandler, расширяющий класс DefaultHandler, который в свою очередь реализует интерфейс ContentHandler и ряд других, не менее полезных интерфейсов:
public class XMLMenuHandler extends DefaultHandler {
private Map menuMap = new HashMap();
private JMenuBar menuBar;
private LinkedList menuList = new LinkedList();
@Override
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
//Определяем стартовый элемент (menu, menubar, menuitem или separator)
if(qName.equals("menu")) {
parseMenu(atts);
} else if(qName.equals("menubar")) {
parseMenubar(atts);
} else if(qName.equals("menuitem")) {
parseMenuitem(atts);
} else if(qName.equals("separator")) {
parseSeparator();
}
}
//Определяем закрывающий тег </menu> для перехода к следующему элементу меню.
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if(qName.equals("menu")) {
menuList.removeFirst();
}
}
private void parseSeparator() {
//Добавляем разделитель в текущее меню
((JMenu)menuList.getFirst()).addSeparator();
}
private void parseMenuitem(Attributes atts) {
//Создаем пункт меню
JMenuItem menuItem = new JMenuItem();
String menuItemName = atts.getValue("name");
//Свойства пункта меню
parseAttributes(menuItem, atts);
menuMap.put(menuItemName, menuItem);
//Добавляем к текущему выпадающему меню
((JMenu)menuList.getFirst()).add(menuItem);
}
private void parseMenubar(Attributes atts) {
//Создаем строку меню
JMenuBar tempMenuBar = new JMenuBar();
String menuBarName = atts.getValue("name");
menuMap.put(menuBarName, tempMenuBar);
menuBar = tempMenuBar;
}
private void parseMenu(Attributes atts) {
//Создаем меню
JMenu menu = new JMenu();
String menuName = atts.getValue("name");
//Свойства меню
parseAttributes(menu, atts);
menuMap.put(menuName, menu);
//Создаем меню, если нет ни одного меню-элемента.
if (menuList.size() == 0) {
menuBar.add(menu);
} else { //Добавляем подменю
((JMenu)menuList.getFirst()).add(menu);
}
//Добавляем меню к остальным меню-элементам
menuList.addFirst(menu);
}
private void parseAttributes(JMenuItem item, Attributes atts) {
//Получаем аттрибуты
String text = atts.getValue("text");
String image = atts.getValue("image");
String mnemonic = atts.getValue("mnemonic");
String accelerator = atts.getValue("accelerator");
String isEnabled = atts.getValue("isEnabled");
//Настраиваем свойства меню в соответствие с описанными аттрибутами в XML
item.setText(text);
if(image != null) {
item.setIcon(new ImageIcon(image));
}
if(mnemonic != null) {
item.setMnemonic(mnemonic.charAt(0));
}
if(accelerator != null) {
item.setAccelerator(KeyStroke.getKeyStroke(accelerator));
}
if(isEnabled != null) {
item.setEnabled(Boolean.parseBoolean(isEnabled));
}
}
public Map getMenuMap() {
return menuMap;
}
Опишем класс, который будет отвечать за загрузку XML и активно использовать наш обработчик:
public class XMLMenuParser {
//XML, который будем вычитывать
private InputSource inputSource;
//XML парсер
private SAXParser saxParser;
//Наш XML обработчик
private XMLMenuHandler xmlMenuHandler;
public XMLMenuParser(InputStream inputStream) {
//Загружаем XML файл и инициализируем XML обработчик и парсер.
try {
Reader reader = new InputStreamReader(inputStream);
inputSource = new InputSource(reader);
saxParser = SAXParserFactory.newInstance().newSAXParser();
xmlMenuHandler = new XMLMenuHandler();
} catch (Exception e) {
System.out.println("Something went wrong during SAXParser initialization: " + e.getMessage());
throw new RuntimeException(e);
}
}
public void parseXML() throws IOException, SAXException {
saxParser.parse(inputSource, xmlMenuHandler);
}
public JMenuBar getMenuBar(String name) {
return (JMenuBar)xmlMenuHandler.getMenuMap().get(name);
}
}
А теперь посмотрим на проделанную работу в деле — применим описанное в XML меню к простенькому UI:
public class XMLMenuCreator extends JFrame {
public XMLMenuCreator() {
super("XML Menu Creator");
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
try {
FileInputStream in = new FileInputStream("./data/MenuXML/menu.xml");
XMLMenuParser xmlParser = new XMLMenuParser(in);
xmlParser.parseXML();
//Вытаскиваем, сформированное из XML файла меню, по его имени (тег <menubar>),
//и прикрепляем к нашему UI
setJMenuBar(xmlParser.getMenuBar("ourMenu"));
} catch (Exception e) {
System.out.println("Exception: " + e.getMessage());
}
setSize(200, 200);
setVisible(true);
}
}
Запустив наше приложение, мы получим примерно следующее окошко с меню (сруктура которого такая же, как было описанно выше):
Заключение
Теперь, если появится необходимость в построении ветвистого меню, все что необходимо будет сделать, так это создать соответствующий XML файл, в котором будут описаны все элементы строки меню с их свойствами, а также подключить к проекту два класса XMLMenuHandler и XMLMenuParser (можно даже их объединить, сделав XMLMenuHandler внутренним классом) и использовать этот суповой набор, как было показано выше в примере.
Спасибо за внимание и поздравляю с прошедшим днем программиста.
Исходники проекта можно взять тут: http://www.google.com/url?q=https%3A%2F%2Fdocs.google.com%2Fopen%3Fid%3D0B1I02TclzMD_RTVtNVFNVkkxU1U
Автор: Ramaloke