Задание 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 пункт условия (не обязательный).