Задание 39. Простой программа рисования
Это задание является первым из трех частей мультиплеерного 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 пункт
условия (не обязательный).