Использование XML для генерации панели меню в Swing

в 8:09, , рубрики: java, sax, swing, XML, Программирование, метки: , , ,

Доброго времени суток, дорогие читатели.
Недавно столкнулся с проблемой создания панели меню — стандартной панели, которая присутствует почти в каждом мало-мальски функциональном 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);
    }
}

Запустив наше приложение, мы получим примерно следующее окошко с меню (сруктура которого такая же, как было описанно выше):
image

Заключение

Теперь, если появится необходимость в построении ветвистого меню, все что необходимо будет сделать, так это создать соответствующий XML файл, в котором будут описаны все элементы строки меню с их свойствами, а также подключить к проекту два класса XMLMenuHandler и XMLMenuParser (можно даже их объединить, сделав XMLMenuHandler внутренним классом) и использовать этот суповой набор, как было показано выше в примере.

Спасибо за внимание и поздравляю с прошедшим днем программиста.

Исходники проекта можно взять тут: http://www.google.com/url?q=https%3A%2F%2Fdocs.google.com%2Fopen%3Fid%3D0B1I02TclzMD_RTVtNVFNVkkxU1U

Автор: Ramaloke

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js