Что если нам надо добавить кнопки в нашу программу? Например в Paint мы можем хотеть добавить кнопки выбора режима рисования - овалы по кликам или росчерк по движению мыши, или добавить кнопку очистки экрана (чтобы начать рисовать заново).

В играх тоже часто хочется сделать подобное - например добавить кнопку перезапустить уровень или просто сделать возможным кликнуть на персонажа (тоже своего рода кнопка) - скажем чтобы начать диалог с ним. Или кнопкой может быть одна из фраз в диалоге которую может выбрать наш персонаж.

Что требуется от кнопки?

  • Когда по кнопке нажали - должен быть выполнен какой-то произвольный код. По сути это код обрабатывающий событие “кнопка была нажата”.

  • Кнопка должна уметь себя рисовать. Проще всего рисовать просто кнопку-картинку.

  • Дополнительно у кнопки может быть вторая версия - картинка “нажатой кнопки”, когда он как бы прожата вглубину (т.е. пока кнопка мыши нажата но еще не отпущена).

  • Дополнительно у кнопки может быть третья версия - картинка “увеличенной кнопки”, когда мышка нависает над кнопкой (но еще не нажата, кнопка как бы в предвосхищении и просит мышку кликнуть - благодаря этому пользователю очевидно что это кнопка которую можно нажать).

Пример как это может выглядеть:

1) Создаем базовое окно для экспериментов

Давайте начнем с того чтобы сделать базовое окно подобное тому что было сделано в прошлом задании про Paint:

1.1) Создаем класс MyFrame:

import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferStrategy;

public class MyFrame extends JFrame {
    public MyFrame() {
        setSize(640, 480);
        setDefaultCloseOperation(EXIT_ON_CLOSE);

        setVisible(true); // делаем окно видимым только после того как оно полностью готово
    }

    @Override
    public void paint(Graphics g) {
        BufferStrategy bufferStrategy = getBufferStrategy(); // Обращаемся к стратегии буферизации
        if (bufferStrategy == null) { // Если она еще не создана
            createBufferStrategy(2); // то создаем ее
            bufferStrategy = getBufferStrategy(); // и опять обращаемся к уже наверняка созданной стратегии
        }
        g = bufferStrategy.getDrawGraphics(); // Достаем текущую графику (текущий буфер) - это наш холст для рисования (спрятанный от глаз пользователя)
        g.clearRect(0, 0, getWidth(), getHeight()); // Очищаем наш холст (ведь там остался предыдущий кадр)

        // Выполняем рисование:
        g.drawOval(200, 100, 20, 10); // рисуем тестовый овал чтобы убедиться что все работает

        g.dispose();                // Освободить все временные ресурсы графики (после этого в нее уже нельзя рисовать)
        bufferStrategy.show();      // Сказать буферизирующей стратегии отрисовать новый буфер (т.е. поменять показываемый и обновляемый буферы местами)
    }
}

1.2) Создаем окно в main-функции:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyFrame frame = new MyFrame();
        while (true) {
            frame.repaint();

            // давайте отрисовывать окно не чаще чем раз в 10 миллисекунд - т.е. не чаще чем 100 раз в секунду
            Thread.sleep(10); // для этого ждем 10 миллисекунд прежде чем вновь вызвать frame.repaint();
            // это полезно для того чтобы не грузить процессор компьютера слишком сильно
        }
    }
}

2) Создаем класс Button

Нам нужен класс который представляет кнопку, будем считать что кнопка у нас всегда прямоугольная, тогда она состоит из:

  • поля x, y - координаты верхнего левого угла (в пикселях)

  • поля width, height - ширина и высота (в пикселях)

  • поле image - картинка (которую нужно вписать в указанный прямоугольник)

  • метод paint(...) - кнопка должна уметь рисовать себя

  • метод onMouseHit(...) - кнопка должна уметь проверять “попал ли клик мышки по мне” и если мышка попала - реагировать на это (как минимум провоцируя какое-то событие, но может быть еще прожимаясь вглубь под тяжестью нажатия)

2.1) Создайте класс Button с полями x, y, width, height.

2.2) Создайте метод paint(...) который рисует кнопку в переданной извне графике Graphics g (т.е. через аргумент функции paint(...)). Пока что давайте просто нарисуем прямоугольник.

2.3) Создайте метод onMouseHit(int mouseX, int mouseY) который проверяет правда ли что переданные через аргументы координаты клика мышки попали в кнопку. Если попали - то пусть этот метод выведет в консоль “Button hit!”.

3) Создаем объект этой кнопки в окне

Теперь нам надо создать экземпляр этой кнопки. Т.е. создать конкретный объект-кнопку в окне.

3.1) Создайте поле в окне MyFrame которое будет хранить кнопку, т.е. объект только что созданного класса Button.

3.2) Инициализируйте это поле (создав объект через вызов конструктора - ... = new Button(...);) в конструкторе окна по каким-нибудь координатам. Сделайте это до setVisible(true).

3.3) В отрисовке окна (т.е. в методе paint(...) в MyFrame) добавьте отрисовку кнопки - вызвав ее метод отрисовки.

3.4) Запустите программу - убедитесь что кнопка (прямоугольник) рисуется там где вы бы ожидали с учетом ее координат.

4) Добавляем обработку мышки

Теперь надо добавить обработку нажатий мышки. Это делается так же как и раньше, но теперь мы не хотим сразу что-то делать при клике мышки - а хотим просто сообщить об этом кнопке.

И кнопка в свою очередь проверит - а попало ли нажатие мышки в кнопку? Если да, то кнопка на это будет реагировать.

4.1) MyFrame должен заявить себя как “слушатель мышки”, т.е. реализовать интерфейс: implements MouseListener. А значит и реализовать в себе 5 методов - mouseClicked(...) и прочие.

4.2) В методе который обрабатывает событие нажатия мышки оповестите об этом кнопку - т.е. вызовите ее метод onMouseHit(...). Обратите внимание что этому методу надо сообщить где именно мышка нажала - передав два аргумента - координаты клика мышки.

4.3) Окно должно добавить себя в список “почетных слушателей мышки” - в конструкторе добавьте строчку addMouseListener(this);.

4.4) Запустите программу - убедитесь что при клике мышкой в консоли появлется “Button hit!” тогда и только тогда когда мышка попала в кнопку.

5) Добавляем картинку для кнопки

5.1) Вбейте в гугл “gui button image” и найдите картинку которая вам по душе, или например скачайте эту:

Красивая кнопка

5.2) Создайте папку data в проекте рядом с папкой src как мы уже это делали раньше.

5.3) Создайте в классе кнопки поле хранящее картинку кнопки - BufferedImage image; и инициализируйте это поле в конструкторе картинки, т.е. через ImageIO.read(new File("data\\button.png")) (тоже так же как раньше).

5.4) Поправьте рисование кнопки - теперь надо рисовать картинку, т.е. используя g.drawImage(...) (напоминаю что последний аргумент этого метода observer нас не интересует, можно вместо него передать отстутствующий объект - null).

5.5) Запустите программу - убедитесь что кнопка рисуется во всей красе и клики по ней все еще работают.

6) Добавляем более интересную реакцию на кнопку

Что если мы хотим чтобы нажатие на кнопку делало что-то интересное?

Например перезапускало уровень или выбирало персонажа (тоже своего рода кнопка) чтобы начать диалог с ним. Или кнопкой может быть одна из фраз в диалоге которую может выбрать наш персонаж.

Давайте для примера решим что у нас есть некий счетчик - поле в окне которое хранит число, изначально это число равно \(0\). И пусть каждое нажатие на кнопку увеличивает этот счетчик на \(1\) и выводит новое значение в консоль.

6.1) Добавьте в окно поле int counter;, в конструкторе окна инициализируйте это поле нулем.

6.2) Теперь кнопка должна уметь делать произвольное действие при нажатии на нее. По сути “действие” стало новым параметром кнопки, в дополнение к ее ширине и высоте!

6.3) Создайте интерфейс ButtonAction - это делается так же как создание нового класса, но в момент когда вы вводите имя ButtonAction - внизу выберите Interface.

6.4) В этом интерфейсе мы укажем какие методы требуются от любого кто заявит что реализовывает этот интерфейст. От любого кто заявит что он “является ButtonAction”. В нашем случае мы требуем лишь одного - наличия метода void onClick(); - это метод который будет вызываться когда кнопка нажата.

6.5) Теперь в кнопке мы можем добавить новое поле ButtonAction action; которое будет хранить “как реагировать на нажатие”. По сути это объект который может делать ровно одну вещь - выполнить метод onClick(), и кнопка попросит его об этом в момент когда мышка кликнет по кнопке.

6.6) Это поле надо чем-то инициализировать - добавьте новый аргумент в конструктор кнопки (наравне с x, y, width, height), пусть тот кто создает кнопку сам решает какой ButtonAction привязан к этой кнопке. Т.е. по сути - какое действие нужно делать при нажатии на эту кнопку.

6.7) Поправьте метод onMouseHit(...) - теперь нам надо не писать в консоль сообщение, а вызывать наше поле-реакцию, чтобы уже он сделал то что надо в ответ на нажатие, т.е. нам надо теперь вызывать action.onClick();.

6.8) Теперь там где кнопка создавалась (где вызывался ее конструктор через new Button(...) из конструктора окна) - нужно добавить еще один - пятый - аргумент - “действие при нажатии на кнопку”, добавьте запятую сразу после ширины и высоты кнопки и начните печатать new ButtonAction - IDEA предложит вам подсказкой - нажмите Enter, получится что-то навроде:

button = new Button(..., new ButtonAction() { // это создание объекта реализующего интерфейс ButtonAction
    @Override
    public void onClick() { // а значит вам надо прямо тут указать какая реализация у его метода onClick()
        
    }
});

6.9) Добавьте в этом методе onClick() что-то свое, например System.out.println("My onClick()!!!");.

6.10) Запустите программу, убедитесь что кнопка рисуется и если кликнуть в нее - появляется “My onClick()!!!”.

6.11) Давайте вернемся к тому что кнопка должна увеличивать на \(1\) счетчик int counter; который объявлен в окне и выводит его в консоль. Попробуйте это написать в методе onClick() который только что создали.

6.12) Запустите программу, убедитесь что кнопка рисуется и что нажатие на кнопку выводит возрастающие числа от \(1\) и дальше - т.е. значения увеличенного счетчика.

7) Добавим еще одну кнопку

А теперь то ради чего мы мучались! Давайте добавим еще одну кнопку, которая будет увеличивать этот же счетчик counter но на этот раз сразу на \(10\) (и тоже сразу выводить новое значение в консоль).

7.1) Создайте в окне новое поле Button button2;

7.2) Инициализируйте ее в конструкторе окна: button2 = new Button(...);, но с отличиями от первой кнопки - по другим координатам и счетчик теперь нужно увеличивать на \(10\).

7.3) Запустите программу. Появилась ли новая кнопка? Нет? Но мы ведь забыли добавить ее в отрисовку окна - поправьте. Пример результата:

Две кнопки

7.4) Запустите программу. Появилась ли новая кнопка? Срабатывают ли нажатия на нее? Нет? Но мы ведь забыли добавить ее проверку “попала ли мышка” - поправьте, окно при событии “мышка нажала” должно оповещать обе кнопки об этом.

7.5) Запустите программу, все должно работать.

7.6) Заметьте что для будущих добавлений новых кнопок можно избавиться от шага 7.3) и 7.4) если хранить кнопки не отдельными полями - а списком ArrayList<Button> buttons;.

7.7) Тогда в конструкторе окна кнопки вместо создания и сохранения в поле - будут добавляться в этот список кнопок:

buttons = new ArrayList<>();
buttons.add(new Button(200, 100, 250, 50, new ButtonAction() {
    @Override
    public void onClick() {
        counter += 1;
        System.out.println(counter);
    }
}));
buttons.add(new Button(..., new ButtonAction() {
    ...
}));

7.8) А отрисовка всех кнопок будет выглядеть так:

for (Button button : buttons) {
    button.paint(g);
}

7.9) А запуск проверки каждой кнопкой “не попала ли в меня мышка” - так:

for (Button button : buttons) {
    button.onMouseHit(e.getX(), e.getY());
}

7.10) Убедитесь что обе кнопки все еще работают и реагируют на мышку как и раньше.

8) Делаем красиво! Увеличивание кнопок!

Давайте теперь сделаем так чтобы кнопка становилась больше когда мышка над ней нависает.

Чтобы увеличивать кнопку когда мышка сверху, нам достаточно в кнопке хранить флажок “мышка сейчас над нами или нет”, при каждом движении мышки - обновлять этот флажок проверяя “правда ли мышка над нами?” И при отрисовке учитывать этот флаг - если он выставлен в true то нужно рисовать кнопку чуть больше чем обычно.

8.1) Добавляем в кнопку флаг boolean isMouseOver;, в конструкторе мышки выставляем этот флаг в false (в момент создания кнопки мышка не над нею).

8.2) Добавляем в кнопку метод void onMouseMove(int mouseX, int mouseY) - в нем проверяем, если координаты в которых теперь находится мышка - внутри кнопки - то выставляем флажок isMouseOver = true; - иначе выставляем isMouseOver = false;.

8.3) Подправляем логику отрисовки кнопки - если флаг isMouseOver выставлен в true то надо рисовать кнопку чуть большего размера.

8.4) Заявляем что наше окно implements MouseMotionListener - чтобы следить за движениями мышки.

8.5) Реализуем методы mouseDragged и mouseMoved, в обоих методах нам надо оповестить все кнопки о движении мышки:

for (Button button : buttons) {
    button.onMouseMove(e.getX(), e.getY());
}

8.6) Не забываем зарегистрировать окно как “слушателя движений мышки” - в конструкторе надо добавить addMouseMotionListener(this);.

8.7) Проверьте что программа все еще работает как надо, но что теперь кнопки еще и увеличиваются когда мышка над ними!

9) Делаем красиво! Уменьшение кнопок!

Давайте теперь сделаем так чтобы кнопка становилась меньше когда мышка на кнопку нажимает.

Т.е. вместо того чтобы просто оповестить кнопку о нажатии мышкой мы хотим:

  • когда кнопка мышки нажимается (т.е. mousePressed(MouseEvent e)) - сообщаем об этом кнопке чтобы она могла выставить свой флаг “меня нажали но еще не отпустили”.

  • когда кнопка мышки отпускается (т.е. mouseReleased(MouseEvent e)) - сообщаем об этом кнопке чтобы она могла сбросить свой флаг “меня нажали но еще не отпустили”, ведь ее только что отпустили. Но при этом кнопка НЕ ВЫЗЫВАЕТ action.onClick();, ведь обработка кнопки уже проихошла при опускании кнопки мыши.

10) Пример результата