[Java] Paint
Давайте сделаем простую версию Paint - пусть будет можно рисовать:
-
сначала кружки в местах где накликали мышкой
-
а затем и линии (там где провели курсором при зажатой кнопке)
1) Создаем окно
Раньше мы действовали так:
-
создавали свой
class MyPanel extends JPanel
-
затем в main-функции создавали объект окно:
JFrame frame = new JFrame;
-
и добавляли объект нашей панели в окно:
frame.add(new MyPanel());
Но почему бы не создать класс который сам по себе является наследником не панели (JPanel
), а сам по себе является окном?
Давайте так и сделаем (это будет важно для двойной буферизации в самом конце).
Создаем класс MainFrame
, который является окном, а значит должен быть отнаследован от JFrame
(т.е. class MainFrame extends JFrame {
).
В конструкторе MainFrame
нам достаточно указать какой-нибудь разумный размер окна, сказать что мы хотим завершать исполнение программы по нажатию на крестик и сделать окно видимым (все это аналогично тому, что мы делали раньше - только теперь мы вызываем эти методы сами у себя через this.названиеМетода();
).
Наконец, добавим точку входа (main
-функцию), и наше окошко уже появляется при запуске:
public static void main(String[] args) {
MainFrame frame = new MainFrame();
}
2) Рисуем примитивы
Чтобы что-нибудь нарисовать - надо переопределить метод определяющий то, как JFrame
отрисовывается - это метод void paint(Graphics g)
(в отличие от того что было при наследовании от панели, ведь там был метод paintComponent
):
@Override
public void paint(Graphics g) {
}
Теперь внутри этого метода обращаясь к Graphics g
можно рисовать различные примитивы как мы уже привыкли.
Заметьте, что теперь фон окна иногда не чисто серого цвета, а как бы прозрачный - т.е. показывает то, что было на его месте до того как он отрисовался. Это вызвано тем, что раньше 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
(т.е. надо сообщить о том, что окно требуется перерисовать. А делать это надо в тот момент, когда мы это поняли - т.е. в методе обработки клика мышью).
Раньше мы этот метод repaint вызывали из вечного цикла while(true)
, т.е. избыточно нагружали наш компьютер, но в данном случае перерисовка требуется только когда пользователь кликает.
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) Дополнительные идеи
Сделайте так чтобы когда пользователь рисовал очередную загогулину - она выбирала себе случайный цвет и случайную толщину.
Еще вы можете добавить сохранение нарисованного холста в текстовый файл, или даже сделать вашу рисовалку многопользовательской! Для этого изучите материалы в следующей статье.
9) Частые проблемы
Ситуация:
Окошко маленькое
Либо вы указали маленький размер окна в пикселях (нормальный размер - это например 640x480), либо что-то упустили в условии.
Ситуация:
Окно прозрачное/за ним мусор/остается мусор с предыдущих отрисовок
Вы упустили в условии вызов родительской реализации paint()
.
Ситуация:
Окно мерцает
Это из-за отсутствия двойной буферизации. Меня устраивает если окно мигает в процессе рисования мышью или в процессе изменения размера окна, но не в состоянии “окно не двигается и изображение статично”.
Это можно пофиксить - 7 пункт
условия.