Давайте сделаем простую версию Paint:

  • рисование кружков по клику мышки

  • рисование линий (там где провели курсором мышки при зажатой кнопке)

  • добавим каждой линии случайный цвет

  • добавим панель для выбора цвета

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

Раньше мы действовали так:

  • создавали свой class MyPanel extends JPanel

  • затем в main-функции создавали объект окно: JFrame frame = new JFrame;

  • и добавляли объект нашей панели в окно: frame.add(new MyPanel());

Т.е. мы наследовались от панели чтобы поменять ее логику отрисовки, и тогда окно при отрисовке провоцировало отрисоваться нашу панель - которая делала то что мы ее попросили.

Но можно ведь вместо того чтобы наследоваться от панели (JPanel) и менять логику ее отрисовки - наследоваться от самого окна (JFrame) и менять отрисовку самого окна.

Давайте так и сделаем, существенная разница в том что это позволит нам избавиться от “мигания” отрисовки с помощью двойной буферизации в самом конце этого задания.

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

Давайте настроим наше окно не в main-функции как мы делали это раньше, а в конструкторе MyFrame - т.е. пусть окно при своем конструировании само сразу себя настроит. Нам достаточно:

  • 1.2) указать какой-нибудь разумный размер окна (вызывая свой же метод setSize(...)),
  • 1.3) указать что при закрытии окна нажатием на крестик - программа должна завершать свою работу (вызывая свой же метод setDefaultCloseOperation(...)),
  • 1.4) сделать окно видимым (все это аналогично тому, что мы делали раньше - только теперь мы вызываем эти методы сами у себя через this.названиеМетода();).

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

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

2) Проверяем что в окне можно рисовать

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

    @Override
    public void paint(Graphics g) {
    }

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

2.1) Нарисуйте какой-нибудь овал, запустите программу и убедитесь что овал нарисован.

2.2) Попробуйте изменить размер окна - нравится ли вам то что вы увидели? Почему так?

2.3) Потому что обычное окно JFrame в своем методе paint(...) зарисовывает себя одним серым цветом.

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

2.5) Чтобы добавить в метод то старое что было в JFrame в методе paint(...) - достаточно перед тем как отрисовать овал вызвать “старую версию” этого метода - через super.paint(...). По сути мы здесь делаем то же что и через this.someMethod(...), но используя ключевое слово super вместо this мы обращаемся к зову предков - вызываем реализацию метода из родительского класса - из базового JFrame.

2.6) Проверьте что после добавления super-вызова окно при изменении цвета выглядит красиво. При этом убедитесь что овал все еще на месте.

3) Рисуем овалы

Давайте по каждому клику мышки рисовать овалы. Для этого нам нужно сделать три вещи:

  • хранить в нашем окне в поле список овалов (т.е. координаты совершенных кликов мышки)

  • при каждом клике мышки добавлять в этот список овал с координатами совершенного клика мышки

  • при каждой отрисовке окна пробегать в цикле по перечню овалов и рисовать каждый из них

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

3.2) Создайте список (ArrayList<Oval>) овалов - т.е. нужно сделать поле для хранения всех кликов мышки. Храниться они будут в окне, поэтому поле стоит создавать внутри MyFrame. Не забудьте что чтобы не случилось NullPointerException - нужно это поле инициализировать через new ... (в конструкторе окна). Т.е. нужно создать пустой список.

3.3) Чтобы мы смогли реагировать на клики мышкой - нужно чтобы наше окно получило “сертификат Слушателя Событий Мышки”. По аналогии с тем как панель брала на себя обязательство быть KeyEventDispatcher (implements KeyEventDispatcher), так и наше окно должно заявить себя “являющимся MouseListener”. Т.е. вам нужно добавить implements MouseListener в декларации класса MyFrame.

3.4) Чтобы выполнить это наше обязательство - мы должны реализовать методы которые от нас требует это почетное звание “Слушателя Событий Мышки”. Поэтому нажмите на покрасневшую строку объявления окна - и нажмите Alt+Enter->Implement methods. Это создаст вам 5 методов навроде mouseClicked(...), mousePressed(...) и прочих. Добавьте в них разные System.out.println("Mouse ...") чтобы чуть позже разобраться кто из них когда срабатывает.

3.5) Теперь надо зарегистрировать себя как слушатель событий мышки - добавьте в конструктор MyFrame вызов addMouseListener(this); (что зарегистрирует нас как обработчика событий мышки в рамках этого окна - т.е. в рамках нас самих).

3.6) Запустите и проверьте что если кликать мышкой в окне и прочее - то в консоли появляются ваши сообщения из шага 3.4). Выясните про каждый из пяти методов - что же они делают.

3.7) Добавьте в список овалов при клике мышкой новый овал - его координаты должны указывать на место где произошел клик мышкой. Заметьте что все пять методов-событий про мышку получают единственный аргумент - объект MouseEvent e - быть может у него есть методы которые сообщат нам нужные коородинаты \(x\) и \(y\)?

3.8) Наконец, в отрисовке окна пробегайте в цикле по перечню овалов и отрисовывайте каждый из них.

3.9) Запустите и проверьте рисуются ли овалы? А если после кликов мышкой изменить размер окна? Если только тогда овалы появляются - значит надо например добавить while (true) { frame.repaint(); } в main-функцию - чтобы окно постоянно перерисовывалось. Если после этого появлиось мерцание и мигание - мы с этим позже разберемся с помощью двойной буферизации. Это вызвано тем что мы рисуем овалы один за другим подряд. Но при каждой отрисовке это начинается с чистого холста. В результате пользователь видит то все овалы, то только часть, то ни одного. Это и выглядит как мерцание.

3.10) Проверьте все ли вам нравится - если что-то не нравится - обязательно исправьте это. Если не знаете как - спросите меня. Например ОБЯЗАТЕЛЬНО сделайте так чтобы овалы рисовались вокруг места где кликнула мышка - красиво и симметрично.

3.11) Сделайте мир красочнее - пусть в момент клика созданному овалу назначается случайный цвет (и сохраняется в поле овала через его конструктор). Чтобы учесть этот цвет при отрисовке - перед вызовом очередного draw... достаточно вызвать g.setColor(new Color(255, 0, 0)); (только вместо \(255\), \(0\) и \(0\) должны быть другие числа от \(0\) до \(255\) - это яркость каналов Red, Green, Blue).

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

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

4) Рисуем мышкой

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

public class MyFrame extends JFrame implements MouseListener, MouseMotionListener {

4.2) И естественно нам нужно “исполнять контракт” - реализовать методы этого интерфейса - Alt+Enter и добавьте реализацию двух методов - mouseDragged(...) и mouseMoved(...).

4.3) Зарегистрируйте окно слушателем движений мышки (addMouseMotionListener(this)) так же как это было сделано для обычных событий мышки.

4.4) Выясните кто из двух методов-событий про движения мышки - когда вызывается. И добавьте список хранящий “росчерки мышки”. Каждый росчерк - это объект вашего класса, например можете назвать его кривая - Curve. В нем нужно хранить координаты - точки по которым прошла мышка (т.е. в каждой кривой-росчерке как минимум есть поле - ArrayList<Point> - хранящий координаты промежуточных точек.

4.5) Добавьте в отрисовке окна отрисовку этих кривуль-росчерков - соединяйте отрезками все эти точки.

5) Избавляемся от мигания окна

Когда вы добавили while (true) { frame.repaint(); } - появлиось мерцание и мигание. Это вызвано тем что мы рисуем разные фигуры одну за другой - подряд, а не одновременно. При каждой отрисовке этот процесс начинается с чистого холста. В результате пользователь видит то все овалы, то только часть, то ни одного и т.п.. Это и выглядит как мерцание.

Чтобы проблема ушла - обычно используется двойная буферизация. Идея очень простая - у нас есть два холста. Один показывается пользователю, а другой используется для рисования следующего кадра.

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

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

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

5.2) Удалите из paint окна в самом начале вызов super.paint(g) - мы сами сделаем всю темную магию по отрисовке

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

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

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

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

6) Дополнительные идеи

Давайте добавим в наш Paint палитру цветов - возьмите любую небольшую картинку похожую на палитру, например одну из этих:

Палитра из Paint

Палитра из гугл-картинок по запросу "Palette"

6.1) Загрузите ее в поле BufferedImage как картинку как это было в прошлом задании.

6.2) Добавьте ее отрисовку в верхнем левом угле окна.

6.3) Когда происходит клик мышки - проверьте куда он попал, если он в рамках картинки - значит произошел “выбор цвета” (текущий цвет надо хранить где-то тоже в одном из полей).

6.4) Как узнать какой цвет в нужном пикселе картинки? (т.е. там куда попал клик) Просто загуглите java get pixel color from bufferedimage. Окажется что вам нужно что-то вроде:

int color = paletteImage.getRGB(x, y);
int red =   (color & 0x00ff0000) >> 16;
int green = (color & 0x0000ff00) >> 8;
int blue =   color & 0x000000ff;

Не обращайте внимание на это - главное что мы вытащили red, green, blue цвета пикселя. В целом здесь происходит следующее - вытаскивается цвет ввиде числа, а дальше разбивается битовыми операциями на части. Все число - это четыре байта, три байта выделено на каждый из каналов цвета (а каждый байт - это значения от 0 до 255).

6.5) И теперь при создании нового штриха или овала - назначайте ему цветом то что сейчас является последним выбранным цветом.

6.6) Перед отрисовкой каждого объекта - выставляйте его цвет (сохраненный в поле этого объекта).

6.7) Добавьте рядом с палитрой отрисовку текущего выбранного цвета (большим одноцветным прямоугольником).

6.8) Проверьте что палитру нельзя случайно зарисовать :) просто нужно рисовать ее после всех остальных объектов - почти в самом конце метода paint (но перед g.dispose();).

6.9) Еще текущий выбранный цвет можно рисовать временным овалом вокруг текущего положения мышки. Эта идея хороша если дальше вы захотите еще менять размер штриха-отрисовки (например колесиком мышки через реализацию интерфейса MouseWheelListener).