Давайте сделаем простую версию 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 пункт условия.