Справочник функций

Ваш аккаунт

Войти через: 
Забыли пароль?
Регистрация
Информацию о новых материалах можно получать и без регистрации:

Почтовая рассылка

Подписчиков: -1
Последний выпуск: 19.06.2015

Расширение функциональности элементов пользовательского интерфейса в Java

На примере реализации нестандартного для Swing поведения меню.

Автор: Виктор Вареников
22 сентября 2006 года

Довольно часто при создании приложений с GUI (stand alone приложений или апплетов) приходится сталкиваться с необходимостью несколько изменить внешний вид и поведение стандартных компонентов пользовательского интерфейса. Иногда этого хочет заказчик. Иногда этого требует дизайнер интерфейсов. Так или иначе время от времени такая задача возникает. И не всегда представляется возможным создать комбинированный из нескольких других элемент пользовательского интерфейса. Например добавить кнопку закрытия в закладку компонента JTabbedPane. Или реализовать меню, которое может отображать пиктограмму над названием пункта меню и кроме этого позволит использовать JMenuBar непосредственно как контейнер для пунктов меню. Вот такое меню мы и реализуем.

Практически каждый кто писал на java приложения с GUI, использовал в своей работе классы имплементирующие меню. И наверняка каждый знает, что при создании меню есть определенные правила, все это хорошо описано в сановском туториал, книгах и статьях. И каждый знает об ограничениях, которые накладывает этот подход – чтобы использовать JMenuItem, необходимо создать экземпляр JMenu – контейнер для нашего итема, и уже только этот контейнер мы можем поместить в JMenuBar. Но ведь во многих windows-приложениях пункт меню может быть виден и использоваться прямо из панели меню. К сожалению разработчики Swing не заложили такой возможности в библиотеку. На самом деле это не сложно изменить. Для того чтобы проделать такой фокус нам нужно будет расширить класс JMenu – должен заметить, что мне не удалось реализовать прослушивание событий как это принято, пришлось пойти на ухищрения и изменить эту схему. Если кто то знает как это можно сделать, буду благодарен за комментарии и дополнения.

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

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

    Таким образом задача разбивается на следующие подзадачи:
  1. расширить функциональность классов которые создают меню чтобы их поведение удовлетворяло поставленным требованиям
  2. корректно отрисовать наше меню.

Начнем реализацию со второй подзадачи.

Так как правильно и однотипно в панели меню отображаться должны и элементы-контейнеры(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 пиксел вниз-вправо.

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

    if (isSelected()) {
        g.setColor(new Color(244, 244, 244));
        g.fillRect(0, 0, getWidth(), getHeight());
                    
        g.setColor(Color.WHITE);
        g.drawLine(0, 0, getWidth() - 1, 0);
        g.drawLine(0, 1, getWidth() - 2, 1);
        g.drawLine(0, 2, getWidth() - 3, 2);
                    
        g.drawLine(0, 0, 0, getHeight() - 1);
        g.drawLine(1, 0, 1, getHeight() - 2);
        g.drawLine(2, 0, 2, getHeight() - 3);
                    
        g.setColor(Color.BLACK);
        g.drawLine(1, getHeight() - 1, getWidth() - 1, getHeight() - 1);
        g.drawLine(getWidth() - 1, 1, getWidth() - 1, getHeight() - 1);
                    
        g.setColor(new Color(204, 204, 204));
        g.drawLine(2, getHeight() - 2, getWidth() - 2, getHeight() - 2);
        g.drawLine(getWidth() - 2, 2, getWidth() - 2, getHeight() - 2);
                 
        g.setColor(new Color(224, 224, 224));
        g.drawLine(3, getHeight() - 3, getWidth() - 3, getHeight() - 3);
        g.drawLine(getWidth() - 3, 3, getWidth() - 3, getHeight() - 3);
                  
        g.setColor(Color.BLACK);
        if (icon != null) g.drawImage(icon.getImage(), px + 1, py + 1, this);
        if (title != null && title.length() > 0) g.drawString(title, sx + 1, sy + 1);
    }
    else {
        if (icon != null) g.drawImage(icon.getImage(), px, py, this);
        if (title != null && title.length() > 0) g.drawString(title, sx, sy);
    }
    }

Вторая часть нашей задачи – сделать возможным добавление итема в контейнер 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().

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

Полный код классов реализующих такое меню: PictMenu.java и SimplePictMenu.java.

Работающий пример можно увидеть здесь

Оставить комментарий

Комментарий:
можно использовать BB-коды
Максимальная длина комментария - 4000 символов.
 

Комментарии

1.
93K
04 апреля 2014 года
Сергей Иванович
0 / / 04.04.2014
Мне нравитсяМне не нравится
4 апреля 2014, 21:04:02
за статью спасибо, а ссылка умерла
2.
61K
26 мая 2010 года
SGluk
0 / / 26.05.2010
Мне нравитсяМне не нравится
26 мая 2010, 08:10:58
Спасибо Виктору за идею)))
Попробовал все упаковать в один файл. Вот что получилось.
Реклама на сайте | Обмен ссылками | Ссылки | Экспорт (RSS) | Контакты
Добавить статью | Добавить исходник | Добавить хостинг-провайдера | Добавить сайт в каталог