Это задание является первым из трех частей мультиплеерного Paint. В результате получится программа в которой пользователи сервера и клиента рисуют и видят общую картину.

Дедлайн:

  • 9-1: когда-нибудь не скоро
  • 10-1: 21 февраля
  • 11-1: 21 февраля

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

1) Создаем окно

Создаем класс MainFrame, который как и в самом начале задания отрисовки змейки (в задании 32) является окном, а значит должен быть отнаследован от JFrame (т.е. class MainFrame extends JFrame {).

В конструкторе MainFrame нам достаточно указать какой-нибудь разумный размер окна, попросить окно принять этот размер (pack()), сказать что мы хотим завершать исполнение программы по нажатию на крестик и сделать окно видимым (все это аналогично тому, что мы делали в задании 32):

setPreferredSize(new Dimension(800, 600));
pack();
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setVisible(true);

Итак, добавим точку входа (main-функцию), и наше окошко уже появляется при запуске:

public static void main(String[] args) {
    MainFrame frame = new MainFrame();
}

2) Рисуем примитивы

Чтобы что-нибудь нарисовать - надо переопределить метод определяющий то, как JFrame отрисовывается - это метод void paint(Graphics g):

    @Override
    public void paint(Graphics g) {
    }

Теперь внутри этого метода обращаясь к Graphics g можно рисовать различные примитивы.

Например можно нарисовать линию:

g.drawLine(100, 200, 500, 250);

Или овал (drawOval), или прямоугольник (drawRect). Поэкспериментируйте с разными фигурами: чтобы увидеть перечень всех функций которые поддерживает Graphics нужно написать g. и нажать Ctrl+Space - IDE покажет вам перечень функций.

Заметьте, что теперь фон окна иногда не чисто серого цвета, а как бы прозрачный - т.е. показывает то, что было на его месте до того как он отрисовался. Это вызвано тем, что раньше JFrame при отрисовке исполнял свой старый метод paint(...), который в частности заливал серым цветом все окно, теперь же мы переопределили этот метод, а следовательно код очистки окошка перестал исполняться. Но есть возможность не целиком заменить старый метод, а расширить его - для этого достаточно в начале реализации paint(...) добавить вызов super.paint(g). Здесь super это почти как this - только родительская часть текущего объекта, поэтому таким образом можно вызвать родительскую (т.е. JFrame) реализацию paint.

Чтобы изменить цвет - перед вызовом очередного draw... достаточно вызвать g.setColor(new Color(255, 0, 0));.

Сложнота: Чтобы изменить толщину штрихов - нужно преобразовать Graphics к Graphics2D и затем вызвать метод setStroke():

Graphics2D g2d = (Graphics2D) g;
g2d.setStroke(new BasicStroke(10));

3) Обрабатываем клики мышки

Чтобы обрабатывать нажатия мышки надо заявить себя “слушателем мышки”, реализовать методы которые обрабатывают события и зарегистрировать себя как “официального слушателя мышки в этом окне”:

  • Добавить implements MouseListener в объявлении MainFrame, чтобы получилось public class MainFrame extends JFrame implements MouseListener {
  • Кликнуть в подсвеченном красным implements, затем нажать Alt+Enter и выбрать Implement methods
  • Добавить в конструктор MainFrame вызов addMouseListener(this); (что зарегистрирует нас как обработчика событий мышки)

Теперь можно например в mouseClicked обрабатывать клики мышки в окне. Чтобы получить координаты мышки надо у аргумента функции обработки события (void mouseClicked(MouseEvent e)) вызвать метод getX() и getY(), т.е. e.getX() и e.getY().

4) Рисуем овалы в местах кликов мышкой

Давайте заведем списки координат где кликнула мышь. Объявим мы их как поля MainFrame:

ArrayList<Integer> xs = new ArrayList<>();
ArrayList<Integer> ys = new ArrayList<>();

Теперь в методе mouseClicked достаточно добавлять (методом add) координаты очередного клика к этим массивам.

Чтобы отрисовать эти клики - надо в методе paint перебрать все сохраненные на данный момент координаты кликов и отрисовать их, например вот так:

for (int i = 0; i < xs.size(); ++i) {
    g.drawOval(xs.get(i), ys.get(i), 10, 10);
}

Если теперь запустить и протестировать - мышкой рисовать овалы не получается. Почему? Под отладчиком оказывается что метод mouseClicked отрабатывает нажатия, но после этого paint банально не вызывается.

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

5) Обрабатываем движения мышью

Это опять про “великих обработчиков”. Все то же самое что и про обработку кликов мышью в пункте 3, разве что реализовывать надо интерфейс MouseMotionListener. В результате получается что-то подобное:

public class MainFrame extends JFrame implements MouseListener, MouseMotionListener {

А так же:

  • После Alt+Enter добавятся переопределения методов mouseDragged и mouseMoved
  • Регистрация нашего объекта как слушателя движений мышью вызовом addMouseMotionListener(this); в конструкторе MainFrame

6) Задание на пятерку - контуры движений мыши

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

7) Двойная буферизация (добровольное, необязательное)

Когда элементов отрисовки окажется достаточно много - станет заметно мерцание внутри окна. Это вызвано тем, что отрисовка происходит по-элементно, и каждое промежуточное состояние пользователь видит. В результате элементы которые отрисовываются в конце - пользователь наблюдает крайне малое время.

Чтобы это поправить, надо включить двойную буферизацию:

1) Указать число буферов для стратегии буферизации - createBufferStrategy(2); (например вызвав в конце конструирования окна)

2) В методе paint удалить вызов super.paint(g)

3) В самом начале метода paint написать следующий код:

BufferStrategy bufferStrategy = getBufferStrategy();        // Обращаемся к стратегии буферизации
if (bufferStrategy == null) {                               // Если она еще не создана
    createBufferStrategy(2);                                // то создаем ее
    bufferStrategy = getBufferStrategy();                   // и опять обращаемся к уже наверняка созданной стратегии
}
g = bufferStrategy.getDrawGraphics();                       // Достаем текущую графику (текущий буфер)
g.setColor(getBackground());                                // Выставялем цвет в цвет фона
g.fillRect(0, 0, getWidth(), getHeight());                  // Зачищаем все окно фоновым цветом

4) В самом конце метода paint написать следующий код:

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

8) Отправка задания

Отправляйте выполненное задание ввиде zip-архива src папки, и пожалуйста:

  • Тему письма называйте правильно, например: Задание 39 16-1 Полярный Коля
  • Правильно называнный zip-архив (или 7zip), внутри которого папка src с .java файлами, пример названия: 39_16_1_polyarniy_nikolay.zip

9) Частые проблемы

Ситуация: Окошко маленькое

Либо вы указали маленький размер окна в пикселях (нормальный размер - это например 640x480), либо что-то упустили в условии (например место где указывается размер окна или вызов pack()).

Ситуация: Окно прозрачное/за ним мусор/остается мусор с предыдущих отрисовок

Вы упустили в условии вызов родительской реализации paint().

Ситуация: Окно мерцает

Это из-за отсутствия двойной буферизации. Меня устраивает если окно мигает в процессе рисования мышью или в процессе изменения размера окна, но не в состоянии “окно не двигается и изображение статично”. Опционально это можно пофиксить, как это фиксить - 7 пункт условия (не обязательный).