На прикладі реалізації нестандартного для 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.

Працюючий приклад можна побачити тут

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*