\documentclass[../j-spec.tex]{subfiles} \begin{document} \section{GUI: Графический интерфейс пользователя} \begin{longtable}{|p{35mm}|p{135mm}|} \hline Экран & Слова \\ \hline \endhead Титул & Здравствуйте, добро пожаловать на курс посвящённый инструментарию разработчика на джава \\ \hline Отбивка & и сегодня на повестке дня у нас интерфейсы, но не те интерфейсы, что программные, а те, что графические. \\ \hline На предыдущем курсе & На предыдущем курсе были рассмотрены механизмы работы знакомых концепций на примере языка Java, такие как базовое процедурное программирование, ООП, исключения. Было рассмотрено устройство языка Java и сопутствующих технологических решений, платформы для создания и запуска приложений на JVM-языках (Groovy, Kotlin, Scala, и др). Также рассмотрены некоторые базовые средства ввода-вывода, позволяющие манипулировать данными за пределами программы. Получены знания принципов работы платформы Java, понимание того, как язык выражает принципы программирования, его объектную природу. Мы научились писать базовые терминальные приложения и утилиты, решать задачи (в том числе алгоритмические, не требующие сложных программных решений) с использованием языка Java и с учётом его особенностей. \\ \hline На этом уроке & Сегодня, наконец-то всё самое интересное начинается. Сегодня будем знакомиться со всеми любимыми окошками. Интерфейс - это чрезвычайно важно, поскольку это именно то, что видят наши пользователи. Мы с вами поигрались в консольке, научились делать интересные и полезные (но немного неполноценные) приложения, и именно сегодня пришла пора обернуть нашу логику в красивую обёртку, добавить свистелки-тарахтели и выпускать в жизнь. Сегодня, пожалуй, самое сложное занятие начального этапа, но не потому что оно содержит какую-то очень сложную информацию, а потому что заставит вас необычным образом взглянуть на те принципы ООП, которые вы уже знаете. Сегодня поговорим о создании окна, менеджерах размещений, элементах графического интерфейса, и обработчиках событий. \\ \hline 06-01 & Сразу хочу закрыть вопрос всех людей, которые когда-либо начинали смотреть эту лекцию: почему именно фреймфорк Swing? Нет, не потому что разработчики Swing платят мне за рекламу, нет, не потому что это модный и современный фреймворк. Скорее всего даже не потому что он пригодится любому программисту на джава. Мы будем пользоваться им потому, что он поможет нам лучше понять ООП. Как работают композиции из объектов, как заставить объекты обмениваться информацией между собой, чем нам помогает ссылочная природа данных в джаве, как удерживать в голове базовые взаимосвязи объектов. И чтобы не выдумывать какие-то искусственные примеры, мы напишем простую игру, например, крестики-нолики с графическим интерфейсом. Почему не джаваФХ? потому что он перестал быть стандартным начиная с джава9, почему не либГДХ? потому что он в итоге всё равно на свинге написан. Ну и взгляните ещё раз внимательно на интеллиж идею, которая почти наверняка у вас открыта - она написана на свинге.\\ \hline отбивка окно JFrame & Ну что, приобщаемся к прекрасному по порядку, создадим первое окошко? для этого нам понадобится то, что в терминах свинга называется джейфрейм.\\ \hline 06-02 & Прежде чем мы начнём, ещё раз хочу вас успокоить: мы не изучаем фреймворк свинг, поэтому не надо зазубривать названия вообще всех классов и компонентов. Мы изучаем программирование и ООП, на примере и с использованием свинга, поэтому сосредоточьтесь, пожалуйста, на объектах, их свойствах и взаимосвязях. Создадим новый класс для нашей программы, и поскольку интереснее всего писать игры, а не какие-то сложные корпоративные программы, то мы назовём наш класс GameWindow, и приступим. Окошки мы будем рисовать при помощи библиотеки swing, основной для рисования в Java. %% Для того чтобы начать, надо получить доступ к методам, содержащимся в библиотеке, а для этого, наш класс игрового окна должен быть наследником класса JFrame. %% Сразу на будущее - все модификаторы видимости будем делать публичными и дефолтными, потому что будем сегодня работать в одном пакете и не очень думать об инкапсуляции. Создадим конструктор. %% А в мэйн классе просто создадим новый объект нашего новоиспечённого класса с окошком. Пока ничего значительно отличающегося от котиков и пёсиков мы не сделали, страшно быть не должно. Если прямо сейчас запустить приложение, оно должно запуститься, и не подав никаких внешних признаков жизни завершиться, это признак того, что мы всё сделали верно, главное, что нет ошибок, а что именно произошло - пойдём выяснять.\\ \hline 06-03 & Самое интересное это то, каким будет создано наше окно и где. большая часть свойств окна неизменна и задаётся в конструкторе, то есть окно при создании будет наделено какими-то свойствами, которые в некоторых случаях можно будет поменять во время работы этого окна. Самое не очевидное на первый взгляд, но очень логичное, когда окон в программе больше одного - это то, что в библиотеке свинг при нажатии на крестик в углу окна программа не завершается. По умолчанию, все создаваемые окна, как вы могли заметить, если запустили программу с нашим пустым окном, невидимые. Это сделано потому что мы можем создавать сколько угодно окон для нашего приложения, и, согласитесь, было бы не слишком приятно, если бы наша программа закрывалась при закрытии первого попавшегося всплывающего окна, да и окна постоянно лезли бы в глаза пользователю не в нужные моменты, а в момент создания в программе. Для того, чтобы программа всё-же когда-нибудь закрылась, нужно придумать для неё основное окошко, и в нашем случае, на данный момент, оно будет единственным. Чтобы программа завершалась при закрытии этого только что созданного игрового окна (и это первое, что нужно делать при создании одно оконных приложений) мы установим нашему экземпляру JFrame такое свойство, как setDefaultCloseOperation, то есть установим, что нужно сделать, когда это окно закроется. Мы передадим туда константу EXIT_ON_CLOSE, чтобы программа завершалась вместе с закрытием окна. А если этого не сделать, то по умолчанию, окно просто снова сделается невидимым, и приложение не завершится. \\ \hline 06-04 & Естественно, теперь очень сильно хочется, чтобы окно стало видимым, но не спешите, сначала мы припомним, почему мы можем просто брать и вызывать методы, которые никогда не видели из конструктора нашего окна не обращаясь даже ни к каким объектам через точку? из-за устройства фреймворка Swing, из-за наследования от JFrame, из-за импорта классов Swing ...30 сек ждём... конечно же это происходит из-за наследования, наше окно теперь не просто использует жфрейм, а само является жфреймом. это и есть то самое применение ООП в жизни.\\ \hline 06-05 & Быстро добавим всяких констант для нашего окошка. Как мы помним, считается хорошим тоном не держать в коде никаких магических цифр, чтобы не думать, что они значат и почему они именно такие, и что будет если их поменять, поэтому объявим ширину и высоту окошка в виде вынесенной константы. Я уже говорил, что принято константы писать большими буквами, или оставил вас в жестоком неведении? пишем высота = 555, ширина = 507, положение окна по оси икс = 800, положение окна по оси игрек = 300. %% соответственно и настроим размеры окна в конструкторе, настроим позицию на экране стандартными методами. Ну и давайте наконец на него взглянем. По-умолчанию наше окошко невидимое, а мы сделаем его видимым setVisible true. %% Короткая справка, пока мы наслаждаемся видом нашего первого окна. Окно -- это всегда отдельный поток программы, и окно крутится в бесконечном цикле, в этом бесконечном цикле есть очередь сообщений, которые цикл опрашивает и выполняет. \\ \hline 06-06 & Чтобы чуть получше начать понимать про многопоточность - давайте напишем sout(method main is over), запустим нашу программу и внимательно смотрите, видим, окошко создалось, в консоли видим, что работа метода мэйн закончилась, а наше окошко всё равно выполняется, его при желании можно подвигать, изменить его размер и всё такое. Это и есть наглядная демонстрация многопоточности. Но подробнее о том как она работает вы будете узнавать чуть позже на этом курсе. Вот и получается, что когда мы создаём новое окно - нам не нужно его ни в какой контейнер помещать, ни думать, как оно там будет взаимодействовать с пользователем, оно создастся и будет жить своей жизнью. Инкапсуляция. Если мы захотим что-то ещё писать в мэйне, никто не запретил, и потоки будут выполняться параллельно, асинхронно. \\ \hline 06-07 & Подведём некоторые промежуточные итоги. Отвечайте первое, что приходит вам в голову. Чтобы создать пустое окно в программе нужно 1. импортировать библиотеку Swing 2. создать класс MainWindow 3. создать класс-наследник JFrame ...30сек... конечно, создать класс-наследник жфрейма, как мы это сделали только что для нашего приложения. Свойства окна, такие как размер и заголовок возможно задать 1. написанием методов в классе-наследнике 2. вызовом методов в конструкторе 3. созданием констант в первых строках класса ...30сек... вызовом методов в конструкторе окна, потому что наше окно теперь наследует все свойства и методы окна в библиотеке \\ \hline отбивка Компоненты и менеджеры размещений & Пустое окно - это скучно, поэтому на него обязательно надо что-то добавить и как-то это расположить. поговорим об этом. \\ \hline & \\ \hline & \\ \hline & \\ \hline & \\ \hline & \\ \hline \end{longtable} \end{document} ***** Давайте начнём сразу писать графическую оболочку для игры крестики-нолики, для этого напишем заголовок setTitle, запретим пользователю изменять размеры нашего окна, для крестиков-ноликов это будет важно, чтобы всё красиво отображалось и никуда не уплывало пишем setResizeable. И начнём говорить об элементах графического интерфейса. Элементы это всем нам знакомые кнопочки, текстовые поля, лейблы, и всякое остальное. Начнём с кнопки «новая игра». За кнопки отвечает класс JButton, его экземпляр мы и создадим, JButton btnNewGame = new JButton(); можно сразу в конструкторе задать надпись, которая будет отображаться на кнопке пишем надпись «New Game». и сразу напишем ещё одну кнопку, например, «выход». Давайте одну из них добавим на окошко. для этого воспользуемся методом add(), который просит в сигнатуре передать ему какой-то компонент. Все кнопки-лейблы - это наследники класса Component. Можем проследить по иерархии, дойдём до класса Компонент. Итак вот мы в конструкторе её добавили, и можем запустить, увидеть, что наша кнопка заняла всё окно нашего приложения. давайте уберём вызов метода setResizeable, и увидим, что если мы решим поменять размер окна, размер кнопки также будет меняться. Такой, адаптивный дизайн получается. Теперь попробуем добавить вторую кнопку, и видим, что вторая кнопка полностью перекрыла первую. ***** Пришла пора поговорить о компоновщиках, или как их ещё называют, менеджерах размещений. Очень рекомендую погуглить какого нибудь дополнительного материала на эту тему, она не очень простая =======рисовать=========== По умолчанию в библиотеке swing используется компоновщик borderLayout. Он располагает всё, что вы ему передаёте в центре, но также у него есть ещё четыре положения. маленькие области по краям. Поэтому, если мы хотим какой-то наш компонент расположить где-то не в центре, мы должны это явно указать при добавлении. Тут немного неочевидно, поэтому запомните, пожалуйста, что при добавлении надо указать ещё один параметр, константу BorderLayout.SOUTH. вот так неожиданно сторона света приплелась сюда, юг. Давайте попробуем поиграться, добавим какой-то другой лэйаут, например, setLayout(new FlowLayout()); он будет располагать элементы друг за другом в том порядке, как мы пишем, слева направо, сверху вниз. Ну и так далее всякие ГридЛэйауты и прочие, некоторые рассмотрены в методичке, некоторые можно увидеть на вышеупомянутых вебинарах, давайте лучше сегодня побольше поговорим о коде нашего приложения. Если каких-то принципиальных вопросов по компоновщикам нет, давайте продолжим. Основная идея, которую надо понять, что в свинге вся работа происходит через компоновщики - вот эти вот самые лэйауты, которые как-то по-своему располагают элементы в окошке. ***** Одними лэйаутами сыт не будешь, так что джависты придумали использовать не только компоненты сами по себе, но ещё и группы элементов, группы элементов складывают на так называемые JPanel. И внутри каждой панели мы можем использовать свой лэйаут. (рисуем окно и две панельки, большую и под ней маленькую) Внутри нижней панели у нас будет грид лэйаут, а на верхней мы будем ручками рисовать наше поле. Ну и от слов к делу, создадим ещё один класс, назовём его Map, и он у нас в программе будет отвечать за поле для игры, унаследуем его от JPanel, опишем конструктор, и пока что её просто сделаем чёрной, чтобы увидеть её на окне. для этого воспользуемся вот такой константой setBackground(Color.BLACK). ну и создадим для неё метод startNewGame(), который на вход должен что то принимать. А что принимать, давайте подумаем: у нас будут два режима игры, компьютер против игрока и игрок против игрока, первый мы напишем, второй будет домашним заданием, но пишем, int mode, дальше, что естественно, размер поля, и давайте сразу не будем привязываться к квадратному полю, поэтому сделаем int field_size_x, int field_size_y. и соответственно для нашей логики из третьего занятия, нам понадобится выигрышная длина. давайте сейчас на всякий случай поставим здесь так называемые заглушки, чтобы мы знали, что метод вызывается и всё у него хорошо, напишем sout(mode, sizes, win_len) ***** Сразу давайте всю архитектуру опишем, наше приложение будет работать в двух окнах, первое будет стартовое, где мы зададим настройки поля и выберем режим игры, и основное, где будет происходить собственно игра. Основное окно у нас есть, и при его закрытии мы выйдем из программы, давайте опишем стартовое окно, для этого создадим ещё один класс, назовём его StartNewGameWindow, унаследуем от JFrame, делаем конструктор. в нашем GameWindow нам понадобится две константы, одна класса StartNewGameWindow чтобы мы могли это окошко показывать когда захотим и вторая это наша панелька для Map. Сразу скажу, что нашему вспомогательному окошку понадобится основное, поэтому в конструктор мы его передадим, ну чтобы красиво отцентрировать его относительно основного, а не лишь-бы куда пихнуть. в основном окне вызовем startNewGameWindow = new SNGW(this), вот, кстати, ещё один способ применять this, когда нам надо передать методу объект, который вызывает этот метод. Теперь создадим панельку для кнопочек в основном окне, для панелей у нас есть класс Jpanel, пишем JPanel pBottom = new JPanel(); в ней как мы помним из чудо картинки мы можем поставить любой лэйаут, и будем использовать setLayout(newGridLayout); который на вход будет у нас требовать два параметра, количество строк и количество столбцов на которые он будет стараться всё поделить. для нас это будет 1 и 2. и на неё мы добавим наши кнопки. pBottom.add(btnNewGame); и pBottom.add(btnExitGame); Создадим нашу карту, map = new Map; и добавим все эти чудные элементы управления на окошко add(map, borderLayout.CENTER), add(pBottom, BL,SOUTH); и вот после всего того что мы описали наше приветственное второе окно должно стать видимым, чтоб мы выбрали с вами параметры для нашей новой игры, startNewGameWindow.setVisible(true); ***** Переварили? теперь возьмёмся за описание последовательности выполнения нашей программы. в основном окне нам понадобится метод, инициализирующий новую карту, помните мы там для панельки конструктор писали? вот вызывать мы его будем отсюда. копипастим void startNewGame() из класса мэп, и вызываем из метода основного класса метод из панельки map.startNewGame. Зачем мы так делаем, казалось бы усложняем, но нет, смотрите, панелька находится на основном окне. а кнопка начала игры будет находиться на другом окошке, которое вообще не в курсе, какие у нас там панельки, или может там вообще дальше нет никакого интерфейса. В этом и есть суть ООП, когда один объект максимально отделён от другого, и каждому из них вообще наплевать, как реализован другой. Соответсвенно, мы в стартовом окошке нажали кнопку начать игру, и оно главному окну говорит - давай, начинай новую игру, а главное окно уже знает, что оно разделено на панельки, и говорит мэпу, мол, давай, пора начинать. понимаете для чего вот этот промежуточный метод? Чтобы не делать лишних связей между классами. это логично с точки зрения абстракций. Одно окно не должно никак управлять панелькой на другом окне. И таким образом мы уже прям почти закончили с классом GameWindow. осталось только поговорить здесь о последней на сегодня теме - об обработчиках событий. Тут на данный момент для вас будет магия. Потому что чтобы полноценно объяснить как повесить листнер, который будет отслеживать нажатия кнопки, для этого нужно как минимум объяснить интерфейсы и анонимные классы, а это мягко говоря не на пять минут. Так вот, давайте определимся, что это магия и это надо просто запомнить. ***** Итак, как повесить листнер и заставить нашу кнопку реагировать на нажатия? Давайте опишем кнопку выхода, она попроще, у кнопок есть такой метод как addActionListener() и ему на вход нужно передать переменную типа экшнЛистнер. Которая требует на вход интерфейсы, а их мы на J1 не проходим, поэтому просто запомните конструкцию, пожалуйста, и не забивайте себе пока что голову. пишем в метод new ActionListener, жмём энтер и среда сама вставляет вам вот такой страшный шаблон, в который надо писать что должно происходить при нажатии на кнопку. Здесь мы будем пользоваться методом exit класса System, которому передадим стандартное для корректного завершения программы число 0. Систем.Экзит полностью грохнет нашу программу и освободит от неё все ресурсы, так что это самый простой и гарантированный способ убить процес. Что называется, топором. Так вот создание обработчиков - это единственная магия, которая есть на J1. Если разобраться в ситуации - то мы создаём экземпляр анонимного класса, который реализует интерфейс. Вот оно вам сейчас надо, в подробностях? по моему для первого уровня сложновато. Кнопка newGame оживляется таким же образом. пишем новый листнер, и внутри мы делаем ни что иное, как показываем наше моднное второе окошко с настройками новой игры. startNewGameWindow.setVisible(true); тут то нам и пригодится то, что закрытие окна не убивает программу, а уходит в невидимость вот на этом таки создание нашего класса основного окна заканчивается. ***** Давайте пилить стартовое окно. Объявим константы, заботливо подобранные заранее, ширина окна 350 высота 230, минимальная выигрышная длина 3, минимальный размер поля 3, и максимальные размеры 10, 10. эти значения нам нужны будут для создания слайдеров. И давайте сразу объявим переменную типа GameWindow gameWindow, и зададим ей значение из конструктора. this.gameWindow = gameWindow. установим ширину и длину. Теперь давайте, чтобы сделать красиво и ровно посередине, воспользуемся ещё одним новым на сегодня классом, Rectangle, который представляет собой абстрактный прямоугольник. назовём его gameWindowBounds и создадим, = gameWindow.getBounds(), сразу передав в него значения границ нашего окна. у окон есть и такой метод, getBounds(), да. В общем это просто удобный класс для работы с прямоугольниками, то есть с окнами например. И теперь делаем немного математики, задаём переменную int pos_x = (int)gameWindowBounds.getCenterX() - WIN_WIDTH / 2; int pos_y = (int)gameWindowBounds.getCenterY() - WIN_HEIGHT / 2; setLocation(pos_x, pos_y); setResizeable(false); setBorder(new EmptyBorder(3, 3, 6, 3)); Сделаем заголовок, СОЗДАНИЕ НОВОЙ ИГРЫ. расположение сделаем сеткой setLayout(new GridLayout(10, 1)); Дальше нам надо добавить сюда много-много разных управляшек, и эти однотипные по сути действия мы для удобства вынесем в отдельный метод void addGameControlsMode(){}; Для начала добавим надпись, для этого существует класс JLabel, пишем add(new JLabel(“Choose gaming mode”)); в конструктор можем передать собственно надпись, а можем ничего не передавать и задать её потом, но для этого надо будет поместить надпись в какой-то именованный контейнер, для нас сейчас это, понятно, лишнее. Дальше нужны будут две радиокнопки, которые пригодятся не только в этом методе, поэтому объявление вынесем вверх, а присваивание значений сделаем в нашем методе, который всё добавляет. выносим private JRadioButton humVSAI; private JRadioButton humVShum; и присвоим humVSAI = new JRadioButton(“Play VS Computer”); с надписью переданной в конструктор, и вторую также. добавим на наше окно и увидим что всё неправильно работает. add(hva); add(hvh); Чтобы всё заработало как мы ожидаем есть класс который связывает радиобатоны в группы, что видно из его названия ButtonGroup gameMode = newBG(); gmode.add(hvm); gmode.add(hvh); и саму радиогруппу никуда класть не надо, она просто объединит. и чтобы одна из них была выбрана по умолчанию, используем перегруженный конструктор радио кнопки, (“play vs comp”, true); ***** Слайдеры пишем следующий метод addGameControlsFieldWinLen(){} в котором будем описывать наши слайдеры, то есть понятно, что это всё ещё конструктор, просто мы делаем его чисто внешне менее жирным. Ну и чтобы логически разграничить, потому что код уже получается достаточно непростой. Напишем некий подзаголовок add(new JLabel(“Выбери длину”)); и пишем новенький лейбл на котором будем писать выигрышную длину JLabel lbl_win_len = new JLabel(“WIN_LEN” + WIN_LEN)); add(lbl_win_len); этот видите мы создаём в два действия, потому что цифру на нём мы будем менять, а не задавать из программы. Давайте объявим слайдеры где-то рядом с JRadioButton'ами. private JSlider sField_size; JSlider sWin_len; и в методе также создадим. в конструкторе слайдеру можно передать три значения, мин, макс и текущее пишем sWin_len = new Slider(MIN_WIN, MIN_FIELD, MIN_WIN); на слайдер мы тоже вешаем слушателя изменений, только тут он называется ChangeListener пишем sWin_len.addChangeListener(new ChangeListener)); и в нём делаем изменение текста для нашего лейбла лбл_вин_лен.сетТекст(“WIN_LEN” + sWin_len.getValue); и добавим его на окно после лейбла. add(sWin_len); дальше создаём такой-же лейбл с размерами поля и аналогично ставим ему текст, добавим на окно. Теперь поинтереснее, создаём слайдер для поля, тоже аналогично, пишем чендж листнер и начинаем прикалываться, введём ещё одну переменную, current_field_size = sField_size.getValue() и будем использовать его в двух местах, во первых нам надо значения на лейбле менять, а во вторых, максимальное значение выигрышной длины тоже менять. лейбл.сетТекст(ФИЛД + каррент); и сВинЛен.сетМаксимум(каррент); и давайте аккуратно по порядку добавим слайдеры и надписи. Вот теперь вы никак не сможете ошибиться и сделать выигрышную длину больше чем размер поля. ***** И осталась нам только кнопочка “начать игру” в конце конструктора пишем JButton btnStartGame = new JButton(“New Game”); add(btnSG); вот у нас и получился один столбец и десять строчек. Осталось только оживить кнопочку, для этого напишем последний на сегодня метод назову его btnStart_onClick(), в кнопке пишем btnSG.addActionListener(new ActionListener()); и внутри любезно предоставленного шаблона просто вызовем наш метод, чтобы много не писать. А обработчик очень интересный, в нём нам надо вызвать метод startNewGame из основного окошка, но чтобы сделать это нам надо наполнить сигнатуру разными параметрами, которые мы возьмём с наших управляшек. Давайте в классе Map объявим пару констант, раз мы не изучали с вами перечисления. GAME_MODE_H_V_A = 0, GAME_MODE_H_V_H = 1; и в классе в котором работаем создадим новую переменную инт game_mode; пишем if(rb_hva.isSelected()) значит если кнопка выделена - game_mode = Map.HVA иначе rb_hvh.isSelected() game_mode = Map.HVH; ну и совсем иначе генерим исключение. мало-ли мы через пол года захотим ещё режимов надобавлять, батон добавим, а сюда забудем дописать, или пользователю не отобразится радиобатон, throw new RuntimeException(“No Buttons Selected”); и программа нам подскажет где искать. также с размером поля, которое пока что у нас будет квадратным, хоть мы и архитектурно заложили, что оно может быть не квадратным. эти показания мы просто снимем со слайдеров и передадим в конструктор slider.getValue(); ну и перед тем как закончить обработчик нам надо наше окошко спрятать от глаз пользователя до следующего раза, setVisible(false); ну вот мы выполнили план на сегодня, хоть мы и немного отошли от методички, но мы сделали хороший задел для нашего последнего занятия, нам там надо будет только написать логику, и расчертить полянку ***** Домашняя работа 1. Полностью разобраться с кодом (попробовать полностью самостоятельно переписать одно из окон) 2. Составить список вопросов и приложить в виде комментария к домашней работе 3. * Раcчертить панель Map на поле для игры, согласно fieldSize ***** К сложному и интересному: метод paintComponent() вызывается фреймворком когда что-то происходит, например, когда мы перекрываем наше окно каким-то другим, или двигаем само окно, но в любом случае вызывается он гораздо реже, чем нам нужно. Но важно помнить, что мы не должны непосредственно этот метод вызывать из кода. этот метод должен вызываться только фреймворком, но сейчас нам важно не это, сейчас нам важно отделить этот метод от собственно нашего рендеринга. давайте создадим ещё один метод void render(Graphics g) и будем его вызывать из нашего paintComponent(); Итак чтобы нарисовать сеточку нам понадобится ширина и высота нашего поля в пикселях, как это сделать: у панельки есть свойства ширина и высота, ими и воспользуемся, создав две дополнительные переменные int panelWidth = getWidth(); int panelHeight = getHeight(); можем вывести в консоль для отладочных нужд, и заодно посмотреть сколько раз и когда вызовется метод перерисовки sout(w, h); помимо этого нам понадобятся переменные, в которых мы будем хранить высоту и ширину каждой ячейки. размеры каждой ячейки нам пригодятся, так что заведём две классовые переменные cellW, cellH = panelW/fsx, panelH/fsy; и в методе который рендерит разлинуем. идём циклом от 0 до <= fieldSizeY и рисуем горизонтальные линии, пишем int y = i * cellH; g.drawLine(0, y, panelWidth, y); и у многих сейчас не полностью нарисовалось, это происходит потому, что перерисовалась только часть компонента, так уж устроена отрисовка в свинге, чтобы не жрать много ресурсов. мы должны заставить наш компонент полностью перерисоваться. сделаем это вызвав метод repaint(); из метода startNewGame; то есть в методе СНГ мы все переменные инициализировали и попросили панель перерисоваться после этого. Опять же если копнуть немного вглубь, мы сказали фреймворку что надо перерисоваться, он поставил метод пэйнтКомпонент куда-то в очередь сообщений окна, и когда очередь дошла - выполнил его. то есть это действует асинхронно и не напрямую. Отвлеклись, продолжим. отрисуем вертикальные линии fori(<=fsX){int x=i*cellW; g.drawLine(x, 0, x, panelHeight)}. ***** Теперь давайте сделаем ещё одну магию, добавим слушателя на мышку, в конструкторе пишем addMouseListener(new MouseAdapter() {}) нам понадобится заоверрайдить метод mouseReleased(), то есть нам важно когда пользователь отпустит кнопку (метод touchUp()) и снова чтобы не заполнять конструктор огромным количеством кода, создадим отдельный метод update(MouseEvent e) и вызовем его из обработчика мышки. и тут мы тоже принудительно вызовем репэйнт, вот собственно и получается наш игровой цикл, клик мыши, отрисовка, клик-отрисовка. На самом деле это делается ещё и для того чтобы мы нашу логику могли портировать куда-то ещё. не обязательно же мышка должна вызывать у нас изменения игрового поля, а например тап по тачскрину. Теперь мы в нашем методе update из класса MouseEvent вытаскиваем координаты клика, делим на размер ячейки и тем самым получаем номер ячейки, в которую мы кликнули. пишем: int cellX = e.getX/cellW; int cellY = e.getY/cellH. для верности можем их sout чтобы посмотреть как это работает и работает-ли. ***** берём наш код от крестиков-ноликов с третьего занятия свой я вам выкладывал, так что если не готовы воспользоваться своим - возьмите мой. и копипастим его от мэйна до метода humanTurn(). всё, где логика и нет работы с консолью. вот вам пример необходимости писать методы, которые ничего лишнего не выводят. код не переписываем, а приложение уже совсем другое. Ну и видим, что нам не хватает кое каких констант. объявим private static final int EMPTY_DOT = 0, HUMAN_DOT = 1, AI_DOT = 2; убираем статики из методов, и чиним наш проект дальше. В методе checkLine меняем чары на инты, для того чтобы переименовать все переменные в данной области видимости надо навести на неё курсор и нажать шифт-ф6. переименуем на dot. в checkWin() делаем точно также. для aiTurn() после того как убрали статик видим, что не хватает только рандомайзера, создадим классовую переменную pr fin Random random = new R(); статики мы тогда создавали потому что не знали ООП, и не создавали объекты, и не знали, кто такие поля объектов. Напомню, что в статические методы нельзя передавать ссылки на объекты, и из статических методов можно обращаться только к статическим переменным и методам. ***** Ну что, нашли мы кликнутую ячейку, давайте в неё ходить, только нам для начала надо проверить, валидная-ли ячейка, и можно-ли туда ходить, вот и проверим: (!isValid(cellX, cellY) || !isEmptyCell(cellX, cellY)) return; игнорим клик, выходим из метода ну а если всё хорошо - просто ходим филд[селХ][селУ] = hum_dot; И добавить в наш метод отрисовки ещё немного логики. сделаем двойной массив по всем ячейкам нашего поля фори(филдСайзУ) фори(филдСайзХ) и внутри пишем if(isEmptyCell(ж, и)) continue; то есть если ячейка пустая - просто идём дальше, ничего не делаем, дальше пишем маленькую заготовку: if(field[i][j] == H_D) {} else if (field[i][j] == A_D) {} else throw new RTE (не пойму что в ячейке + и,ж) мало ли что мы там перекодили и допилили, может решили сделать баттл для трёх игроков? И теперь пришли к отрисовке. хотите, можете сюда картинку вставлять, хотите закрашенные квадратики, хотите рисуйте крестики-нолики, я для простоты буду пользоваться методом g.fillOval() и буду рисовать кружки. ему в сигнатуре передаётся левая верхняя координата прямоугольника, в который потом будет вписан овал, и его ширина и высота соответственно. тем, кто хоть раз рисовал в пэинте - знакома эта техника рисования кружков. ну и чтобы придать ему цвета - перед тем как рисовать изменим цвет объекта graphics, g.setColor(Color.BLUE); или задать интами от 0 до 255 в формате РГБ new Color(RGB); Итак я предлагаю для человека рисовать синие кружки, а для компа красные. и после проверки собственно его рисовать, в проверках пишем setColor(); а после проверок пишем g.fillOval(j*CellW, i*CellH, CW, CH); и чтобы сделать небольшой отступ сделаем следующее, заведём классовую константу DOTS_PAGGING = 5 пикселя. и первые координаты смещаем на + пэддинга вторые на - 2 пэддинга. Так посимпатичнее. ***** Давайте завязывать с этим безумием объявим константы которые будут содержать исходы игры, псфи DRAW = 0; HUM_WIN = 1; AI_WIN = 2; и нужна переменная stateGameOver в которой будет храниться статус. Значит теперь в методе апдейт мы поставили точку, перерисовали и жедаем по старому сценарию if(checkWin(H_D)) stGO = H_W; return; if (isMapFull()) stGO = DR; return; aiTurn(); repaint(); if(checkWin(A_D)) stGO = A_W; return; if (isMapFull()) stGO = DR; ***** дальше в принципе нам только и надо что нарисовать все красотульки. значит идём в метод рендер и думаем там. как только мы вывели поле, и если игра закончилась нам надо вывести сообщение с одним из вариантов исхода. так и запишем if (gameOver) showOverMessage(g); вот просто перевели наши слова на английский. теперь смотрим, чего у нас для этого не хватает в коде, как минимум метода showOverMessage() и переменной признака конца игры. да не вопрос, мыжпрограммисты, напишем войд sOW(Graphics g), private boolean gameOver. и поехали наполнять эти штуки смыслом: в методе начала новой игры нам, очевидно, надо сказать что геймовер = фолс. и закончим на этом метод СНГ. идём в метод апдейт и в самом начале пишем, что if(gameover) return; очевидно, что если геймовер случился - надо игнорировать вообще все клики. ну а если кто-то выиграл или ничья - геймовер также становится тру. и на этом закончим с методом апдейт и останется только дописать собственно метод показывающий, что у нас закончилась игра. Ну и для того чтобы нарисовать в классе графики какой-то текст есть метод g.drawString() с выводимой строкой и координатами по х и по у, внимание, координата по у это координата нижней части букв, как строчка в тетради. Ну и для того чтобы этот текст кастомизировать да и вообще для работы со шрифтами есть класс не поверите Font. создадим классовый private final Font который назовём не поверите font = new Font( и на вход конструктор внезапно принимает название шрифта, стиль и размер “Times new roman”, Font.BOLD, 48); будьте осторожны при работе с такими неочевидными классами. и теперь наш метод с отрисовкой строки немного дополняем, setColor(); setFont(font). вот так он и выводится. Давайте создадим классовые константы, в которых будем хранить наши сообщения о победах. psfS MSG_DRAW= “DRAW”; MSG_HW; MSG_AW; ***** в showGameOver пишем свич (стейтГеймОвер) и кейсы ДРО, Х_В, А_В. дефолт throw new RTE(unexpected GO message); теперь если дро, g.drawString(MSG_DRAW, 180, getHeight/2); если H_W, g.drawString(HW, 70, gH/2); если A_W (20); и перед ним пишем setColor(YELLOW) setFont(font); ну и чтобы было виднее, я предлагаю рисовать тёмно серый прямоугольник в качестве фона работает он также как fillOval только рисует прямоугольник пишем fillRect(0, 200, getWidth, 70); естественно надо все магические цифры либо вывести в константы, либо как-то динамически считать). и БУМ! всё классно, мы закончили. **** 3: Крестики-нолики ***** public class TicTacToe { private static final char HUMAN_DOT = 'X'; private static final char AI_DOT = 'O'; private static final char EMPTY_DOT = '.'; private static final Scanner SCANNER = new Scanner(System.in); private static final Random RANDOM = new Random(); private static int fieldSizeY; private static int fieldSizeX; private static char[][] field; ***** Инициализация игрового поля private static void initMap() { fieldSizeY = 3; fieldSizeX = 3; field = new char[fieldSizeY][fieldSizeX]; for (int i = 0; i < fieldSizeY; i++) { for (int j = 0; j < fieldSizeX; j++) { field[i][j] = EMPTY_DOT; } } } ***** Вывели поле с любыми украшениями вокруг него private static void printMap() { System.out.print("+"); for (int i = 0; i < fieldSizeX * 2 + 1; i++) System.out.print((i % 2 == 0) ? "-" : i / 2 + 1); System.out.println(); for (int i = 0; i < fieldSizeY; i++) { System.out.print(i + 1 + "|"); for (int j = 0; j < fieldSizeX; j++) System.out.print(field[i][j] + "|"); System.out.println(); } for (int i = 0; i <= fieldSizeX * 2 + 1; i++) System.out.print("-"); System.out.println(); } ***** Ход игрока private static void humanTurn() { int x, y; do { System.out.print("Введите координаты X и Y через пробел>> "); x = SCANNER.nextInt() - 1; y = SCANNER.nextInt() - 1; } while (!isValidCell(x, y) || !isEmptyCell(x, y)); field[y][x] = HUMAN_DOT; } ***** ячейка-то вообще правильная? private static boolean isValidCell(int x, int y) { return x >= 0 && x < fieldSizeX && y >= 0 && y < fieldSizeY; } ***** а пустая? private static boolean isEmptyCell(int x, int y) { return field[y][x] == EMPTY_DOT; } ***** Ход компьютера private static void aiTurn() { int x, y; do { x = RANDOM.nextInt(fieldSizeX); y = RANDOM.nextInt(fieldSizeY); } while (!isEmptyCell(x, y)); field[y][x] = AI_DOT; } ***** проверка на победу private static boolean checkWin(char c) { if (field[0][0]==c && field[0][1]==c && field[0][2]==c) return true; if (field[1][0]==c && field[1][1]==c && field[1][2]==c) return true; if (field[2][0]==c && field[2][1]==c && field[2][2]==c) return true; //....... } ***** ничья? private static boolean isMapFull() { for (int i = 0; i < fieldSizeY; i++) { for (int j = 0; j < fieldSizeX; j++) { if (field[i][j] == EMPTY_DOT) return false; } } return true; } ***** Игровой цикл public static void main(String[] args) { initMap(7, 5, 4); printMap(); while (true) { humanTurn(); printMap(); if (checkWin(HUMAN_DOT)) { System.out.println("Выиграл игрок!!!"); break; } if (isMapFull()) { System.out.println("Ничья!!!"); break; } aiTurn(); printMap(); if (checkWin(AI_DOT)) { System.out.println("Выиграл компьютер!!!"); break; } if (isMapFull()) { System.out.println("Ничья!!!"); break; } } SCANNER.close(); } ***** Домашнее задание 1. Полностью разобраться с кодом; 2. Переделать проверку победы, чтобы она не была реализована просто набором условий. 3. * Попробовать переписать логику проверки победы, чтобы она работала для поля 5х5 и количества фишек 4. 4. *** Доработать искусственный интеллект, чтобы он мог блокировать ходы игрока, и пытаться выиграть сам.