gb-java-devel/scenarios/jtd1-06b.tex

210 lines
64 KiB
TeX
Raw Normal View History

\documentclass[../j-spec.tex]{subfiles}
2023-02-05 01:34:09 +03:00
\usepackage{tikz}
\usepackage{aeguill}
\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.
2023-02-05 01:34:09 +03:00
%% 2
Для того чтобы начать, надо получить доступ к методам, содержащимся в библиотеке, а для этого, наш класс игрового окна должен быть наследником класса JFrame.
2023-02-05 01:34:09 +03:00
%% 3
Сразу на будущее - все модификаторы видимости будем делать публичными и дефолтными, потому что будем сегодня работать в одном пакете и не очень думать об инкапсуляции. Создадим конструктор.
2023-02-05 01:34:09 +03:00
%% 4
А в мэйн классе просто создадим новый объект нашего новоиспечённого класса с окошком. Пока ничего значительно отличающегося от котиков и пёсиков мы не сделали, страшно быть не должно. Если прямо сейчас запустить приложение, оно должно запуститься, и не подав никаких внешних признаков жизни завершиться, это признак того, что мы всё сделали верно, главное, что нет ошибок, а что именно произошло - пойдём выяснять.\\ \hline
06-03 & Самое интересное это то, каким будет создано наше окно и где. большая часть свойств окна неизменна и задаётся в конструкторе, то есть окно при создании будет наделено какими-то свойствами, которые в некоторых случаях можно будет поменять во время работы этого окна. Самое не очевидное на первый взгляд, но очень логичное, когда окон в программе больше одного - это то, что в библиотеке свинг при нажатии на крестик в углу окна программа не завершается. По умолчанию, все создаваемые окна, как вы могли заметить, если запустили программу с нашим пустым окном, невидимые. Это сделано потому что мы можем создавать сколько угодно окон для нашего приложения, и, согласитесь, было бы не слишком приятно, если бы наша программа закрывалась при закрытии первого попавшегося всплывающего окна, да и окна постоянно лезли бы в глаза пользователю не в нужные моменты, а в момент создания в программе.
2023-02-05 01:34:09 +03:00
Для того, чтобы программа всё-же когда-нибудь закрылась, нужно придумать для неё основное окошко, и в нашем случае, на данный момент, оно будет единственным. Чтобы программа завершалась при закрытии этого только что созданного игрового окна (и это первое, что нужно делать при создании одно оконных приложений) мы установим нашему экземпляру JFrame такое свойство, как setDefaultCloseOperation, то есть установим, что нужно сделать, когда это окно закроется. Мы передадим туда константу EXIT\_ON\_CLOSE, чтобы программа завершалась вместе с закрытием окна. А если этого не сделать, то по умолчанию, окно просто снова сделается невидимым, и приложение не завершится. \\ \hline
06-04 & Естественно, теперь очень сильно хочется, чтобы окно стало видимым, но не спешите, сначала мы припомним, почему мы можем просто брать и вызывать методы, которые никогда не видели из конструктора нашего окна не обращаясь даже ни к каким объектам через точку? из-за устройства фреймворка Swing, из-за наследования от JFrame, из-за импорта классов Swing
...30 сек ждём...
конечно же это происходит из-за наследования, наше окно теперь не просто использует жфрейм, а само является жфреймом. это и есть то самое применение ООП в жизни.\\ \hline
06-05 & Быстро добавим всяких констант для нашего окошка. Как мы помним, считается хорошим тоном не держать в коде никаких магических цифр, чтобы не думать, что они значат и почему они именно такие, и что будет если их поменять, поэтому объявим ширину и высоту окошка в виде вынесенной константы. Я уже говорил, что принято константы писать большими буквами, или оставил вас в жестоком неведении? пишем высота = 555, ширина = 507, положение окна по оси икс = 800, положение окна по оси игрек = 300.
2023-02-05 01:34:09 +03:00
%% 2
соответственно и настроим размеры окна в конструкторе, настроим позицию на экране стандартными методами. Ну и давайте наконец на него взглянем. По-умолчанию наше окошко невидимое, а мы сделаем его видимым setVisible true.
2023-02-05 01:34:09 +03:00
%% 3
Короткая справка, пока мы наслаждаемся видом нашего первого окна. Окно -- это всегда отдельный поток программы, и окно крутится в бесконечном цикле, в этом бесконечном цикле есть очередь сообщений, которые цикл опрашивает и выполняет. \\ \hline
06-06 & Чтобы чуть получше начать понимать про многопоточность - давайте напишем sout(method main is over), запустим нашу программу и внимательно смотрите, видим, окошко создалось, в консоли видим, что работа метода мэйн закончилась, а наше окошко всё равно выполняется, его при желании можно подвигать, изменить его размер и всё такое. Это и есть наглядная демонстрация многопоточности. Но подробнее о том как она работает вы будете узнавать чуть позже на этом курсе. Вот и получается, что когда мы создаём новое окно - нам не нужно его ни в какой контейнер помещать, ни думать, как оно там будет взаимодействовать с пользователем, оно создастся и будет жить своей жизнью. Инкапсуляция. Если мы захотим что-то ещё писать в мэйне, никто не запретил, и потоки будут выполняться параллельно, асинхронно. \\ \hline
06-07 & Подведём некоторые промежуточные итоги. Отвечайте первое, что приходит вам в голову. Чтобы создать пустое окно в программе нужно 1. импортировать библиотеку Swing 2. создать класс MainWindow 3. создать класс-наследник JFrame
...30сек...
конечно, создать класс-наследник жфрейма, как мы это сделали только что для нашего приложения. Свойства окна, такие как размер и заголовок возможно задать 1. написанием методов в классе-наследнике 2. вызовом методов в конструкторе 3. созданием констант в первых строках класса
2023-01-23 15:07:01 +03:00
...30сек...
2023-01-23 15:07:01 +03:00
вызовом методов в конструкторе окна, потому что наше окно теперь наследует все свойства и методы окна в библиотеке \\ \hline
2023-01-23 15:07:01 +03:00
2023-02-05 01:34:09 +03:00
отбивка Компоненты и менеджеры размещений & Пустое окно - это скучно, поэтому на него обязательно надо что-то добавить и как-то это расположить. поговорим об этом, начнём сразу писать графическую оболочку для игры крестики-нолики. \\ \hline
2023-01-23 15:07:01 +03:00
2023-02-05 01:34:09 +03:00
06-08 & для того, чтобы точно ничего не перепутать в процессе разработки, напишем заголовок setTitle, запретим пользователю изменять размеры нашего окна, для крестиков-ноликов это будет важно, чтобы всё красиво отображалось и никуда не уплывало пишем setResizeable. И начнём говорить об элементах графического интерфейса. Элементы это всем нам знакомые кнопочки, текстовые поля, лейблы, и всякое остальное. Начнём с кнопки «новая игра».
%% 2
2023-01-23 15:07:01 +03:00
2023-02-05 01:34:09 +03:00
За кнопки отвечает класс JButton, его экземпляр мы и создадим, JButton btnStart = new JButton(); можно сразу в конструкторе задать надпись, которая будет отображаться на кнопке пишем надпись «New Game». и сразу напишем ещё одну кнопку, например, «выход».
%% 3
2023-01-23 15:07:01 +03:00
2023-02-05 01:34:09 +03:00
Одну из них добавим на окошко. для этого воспользуемся внутри конструктора методом с достаточно понятным для окна названием add(), который просит в сигнатуре передать ему какой-то компонент. Все кнопки-лейблы - это наследники класса Component. Можем взять любой компонент и проследить по иерархии, дойдём до класса Компонент.
%% 4
2023-02-05 01:34:09 +03:00
Итак вот мы в конструкторе добавили кнопку, и можем запустить, увидеть, что она заняла всё окно нашего приложения. Если убрать вызов метода setResizeable, то увидим, что если мы решим поменять размер окна, размер кнопки также будет меняться. Такой, адаптивный дизайн получается. \\ \hline
2023-02-05 01:34:09 +03:00
06-09 & При попытке добавить вторую кнопку на это окно, увидим, что вторая кнопка полностью перекрыла первую. Тут приходит пора поговорить о компоновщиках, или, как их ещё называют, менеджерах размещений. Менеджеры размещений нужны для того, чтобы не думать каждый раз о том, как изменится размер и координаты конкретного элемента, скажем, при изменении окна и не писать сложное поведение вложенных компонентов чтобы достаточно просто отобразить то, что привычно пользователю. Компоновщики активно используются в любом программировании графических интерфейсов в любых языках программирования, от С++ до джаваскрипта, просто потому что это достаточно удобный механизм, берущий на себя значительный пласт работы. \\ \hline
06-10 & Менеджер размещения или компоновщик, как следует из названия -- это специальный объект, который помещается на некоторые компоненты и осуществляет автоматическую расстановку добавляемых к нему, компоненту, согласно каким-то прописанным внутри данного менеджера размещения правилам. Чуть забегая вперёд можно сказать, что это контейнеры, реализующие программный интерфейс под названием RootPaneContainer, но это так, шестерёнки, которые заучивать не нужно. Важно понимать, что именно делают компоновщики -- размещают компоненты. Список менеджеров можно увидеть на слайде. Все их можно создать, так сказать пощупать, попробовать в работе и это достаточно сильно зависит от задачи -- какой именно выбрать для использования на том или ином экране. О трёх самых популярных сейчас поговорим.\\ \hline
2024-05-12 20:23:51 +03:00
06-11 & По умолчанию в библиотеке swing используется компоновщик borderLayout. Он располагает всё, что вы ему передаёте в центре, но также у него есть ещё четыре положения. маленькие области по краям. Если мы не занимаем какую-то область компонентом, она схломывается до нулевых размеров, оставляя место другим компонентам. Поэтому, если мы хотим какой-то наш компонент расположить где-то не в центре, мы должны это явно указать при добавлении. Тут немного неочевидно, поэтому запомните, пожалуйста, что при добавлении надо указать ещё один параметр, константу, например, BorderLayout.SOUTH. вот так неожиданно сюда сторона света приплелась, юг. Или вот, например, FlowLayout() будет располагать элементы друг за другом в том порядке, как мы пишем, слева направо, сверху вниз, как мы привыкли писать рукой на бумаге. Компоновщик-сетка при создании принимает на вход число строк и столбцов и располагает компоненты в получившейся сетке. Ну и так далее всякие ГридБэгЛэйауты и прочие. Ещё раз, основная идея, которую надо понять, это не названия компоновщиков, а то, что в свинге вся работа происходит через компоновщики - вот эти вот самые лэйауты, которые как-то по-своему располагают элементы в окошке. Кстати, если это ещё не стало очевидным, кнопки расположились одна поверх другой потому что по умолчанию к фрейму применён БодэЛэйаут, который всё располагает по умолчанию в центре. \\ \hline
2023-02-05 01:34:09 +03:00
06-12 & Одними лэйаутами сыт не будешь, так что разработчики джавы придумали использовать не только компоненты сами по себе, но ещё и группы элементов, группы элементов складывают на так называемые JPanel. И внутри каждой панели мы можем использовать свой лэйаут. Jpanel - это по умолчанию невидимый прямоугольник, основным свойством которого для нас в данный момент является то, что на нём может находиться собственный компоновщик, что открывает для нас близкие к бесконечным возможности по расстановке компонентов на экране. Например, мы можем создать для нашего окна некую панель с кнопками внизу (или вверху, не важно), а всё остальное пространство оставить под другие важные вещи. На слайде можно увидеть код панели, добавление её в нижнюю часть основного экрана, расположение внутри панели компоновщика и двух кнопок. И, соответственно, результат. Обратите особенное внимание на 25ю строку, на экран мы добавляем не кнопки по отдельности, а компонент, на который предварительно добавили кнопки. \\ \hline
06-13 & ЖПэнел настолько хорош и полезен, что мы не можем посвятить ему всего один слайд. Основые графические интерактивности мы будем делать именно на панели, поэтому надо как минимум создать класс с основной игровой панелью. Назовём её как-нибудь хорошо, например, Мэп, потому что это будет карта поля сражения в крестики-нолики. Опишем конструктор, и пока что нашу новенькую панельку просто сделаем чёрной, чтобы увидеть её на белом окне. Для этого в конструкторе воспользуемся сеттером цвета фона и цветовой константой setBackground(Color.BLACK).
%% 2
Естественно, чтобы панелька не висела в воздухе, нужно создать её объект как на 22й строке и поместить куда-нибудь на основной экран, как на 28й строке. и вот она, нарядная, чёрная, как мы и задумали, заняла вообще всё место на окне, кроме юга, где уютно расположилась панель с кнопками. \\ \hline
06-14 & Внутри панели, кстати, раз уж это поле боя, сразу предлагаю написать метод, который будет начинать этот самый бой, назовём этот метод как-нибудь необычно, например, startNewGame(). А что метод будет принимать, давайте подумаем: например, у нас будут два режима игры, компьютер против игрока и игрок против игрока, первый мы напишем сразу, второй на семинаре, значит нужен int mode, дальше, что естественно, размер поля, и давайте сразу не будем привязываться к квадратному полю 3х3, поэтому сделаем int fieldsizex, int fieldsizey. и соответственно для полей больше, чем 3х3 нам понадобится выигрышная длина, то есть число крестиков или ноликов, расположенных подряд на одной прямой для победы той или иной стороны, да что я вам объясняю, знаете сами. сейчас, на всякий случай, поставим здесь так называемые заглушки, чтобы мы знали, что метод вызывается и всё у него хорошо, напишем sout(mode, sizes, winlen). \\ \hline
06-15 & Сразу давайте всю архитектуру опишем, чтобы потом было несложно её расширять и наполнять. наше приложение будет работать в двух окнах, первое будет стартовое, где мы зададим настройки поля и выберем режим игры, и основное, где будет происходить собственно игра. Основное окно у нас есть, и при его закрытии мы выйдем из программы, давайте опишем стартовое окно, для этого создадим ещё один класс, назовём его как-нибудь умно, SettingsWindow, унаследуем от JFrame, делаем конструктор, который, смотрите-ка принимает экземпляр игрового окна. Вспомогательному окошку понадобится основное, чтобы красиво отцентрировать его относительно основного, а не лишь-бы куда пихнуть.
%% 02
в нашем GameWindow нам понадобится две константы, одна класса StartNewGameWindow чтобы мы могли это окошко показывать когда захотим и вторая это наша панелька для Map. В основном окне вызовем сеттингсВиндоу = new сеттингсвиндоу(this), обратите внимание, вот ещё один способ применять this, когда нам надо передать методу объект, который вызывает этот метод, фактически, основное окно передаёт себя. ну и сразу для удобства разработки сделаем окно настроек видимым.
%% 03
На слайде мы видим то, что можно с очень большой натяжкой назвать диаграммой классов, моей целью было не создание идеальной диаграммы, а более-менее понятное объяснение того, что мы тут напрограммировали. Буквами Ф обозначены экземпляры ЖФрейм, буквой П ЖПанель, а А это эппликейшн, то есть приложение. Итак у нас есть самый скучный в мире Мейн, который ничего не делает, а только создаёт новое игровое окно. окно на котором лежит мэп с игрой и которое время от времени будет обращаться к сеттингсам за настройками новой игры. \\ \hline
06-16 & окно с настройками игры. у меня было очень большое желание сделать тут кучу компонентов как-нибудь интересно связать их, но я понял, что в этом случае чрезвычайно скучно будет сидеть на семинаре, поэтому окно настроек игры у нас будет представлено одной кнопкой старта игры, вызывающей метод старта игры с одним зафиксированным набором настроек -- игра с компом, поле 3х3, чтобы выиграть надо собрать 3 крестика (или нолика) подряд. О веселье на семинаре подумали, давайте придумаем что-то, чтобы было не скучно прямо сейчас и сделаем относительное позиционирование. В данный момент окно создаётся в координатах 0-0 и имеет размер 0-0, то есть в левом верхнем углу экрана мы просто увидим кнопки свернуть-развернуть-закрыть. нас это не очень устраивает, тем более, что мы передали этому окну настроек ссылку на объект игрового окна, поверх которого мы и хотим отобразить окно настроек. Поэтому создадим нашему окну какие-нибудь размеры и скажем, что его местоположение должно быть относительным главному окну. Уже знакомым способом бросим на всё окно кнопку подтверждения правильности настроек и старта игры. \\ \hline
06-17 & Дальше совсем никак не получится продолжить беседу без небольшого количества магии. Нам нужно оживить кнопки на окнах, это делается специальной конструкцией, синтаксис которой мы будем понимать уже на следующем уроке, но пока что вынуждены просто запомнить. Есть хорошая новость, идея нам хорошо подсказывает какие скобочки где должны стоять, чтобы всё работало.
%% 2
Хорошая новость, кстати в том, что синтаксически вы уже можете понять что именно здесь написано -- у объекта кнопки батонЕксит вызывается метод добавления к этому объекту некоторого слушателя действия. а какое может у кнопки быть самое очевидное действие? на неё нажали. в аргумент метода добавления передаётся некоторый новый объект класса слушатель действия, у которого переопределяется метод действие произошло. как видите, все кусочки этого паззла вам по отдельности знакомы.
%% 3
добавим действие при нажатии на кнопку выхода - заставим приложение выйти и создадим аналогичный слушатель для кнопки старта новой игры. Эта кнопка будет вызывать наше стильное окно с настройками, которое пока что без настроек, как мы помним. ну, как вызывать... просто делать его видимым, потому что оно уже создано и только этого и ждёт. тут то нам и пригодится то, что закрытие окна не убивает программу, а только окно уходит в невидимость \\ \hline
06-18 & теперь возьмёмся за описание последовательности выполнения нашей программы. в основном окне нам понадобится метод, инициализирующий новую карту, то есть мы не будем напрямую вызывать по кнопке старта новой игры на окне настроек метод в панели основного окна. Зачем мы так делаем, казалось бы усложняем, но нет, смотрите, панелька находится на основном окне. а кнопка начала игры будет находиться на другом окошке, которое вообще не в курсе, какие у нас там панельки, или может там вообще дальше нет никакого интерфейса. В этом и есть суть ООП, когда один объект максимально отделён от другого, и каждому из них вообще наплевать, как реализован другой. Соответсвенно, мы в настроечном окошке нажали кнопку начать игру, и оно главному окну говорит - давай, начинай новую игру, а главное окно уже знает, что оно разделено на панельки, и говорит мэпу, мол, давай, пора начинать. понимаете для чего вот этот промежуточный метод? Чтобы не делать лишних связей между классами. это логично с точки зрения абстракций. Одно окно не должно никак управлять панелькой на другом окне.
% 02
В окне настроек, пишем обработчик для единственной кнопки, и из этого обработчика вызываем единственный доступный нам метод старта новой игры - на основном окне. и спрячем окно настроек вызвав метод сетВизибл с аргументом фолс. а startNewGame() класса мэп, вызываем из метода основного класса через map.startNewGame. Ещё раз цепочка вызовов - основное окно делает окно настроек видимым, окно настроек говорит основному, что пора начинать игру, основное окно уже в свою очередь знает, как именно надо эту игру начинать и просит панельку стартовать. Если всё сделано верно, видим в терминале вывод из нашей заглушки на панели мэп. \\ \hline
06-19 & Значительная часть сложностей сегодняшней лекции позади, давайте попробуем вспомнить, о чём мы вообще тут говорили. Менеджер размещений - это 1. сотрудник, занимающийся разработкой интерфейса 2. объект, выполняющий расстановку компонентов на окне приложения 3. механизм, проверяющий возможность отображения окна в ОС
... 30 сек ...
Конечно же второе, объект, выполняющий расстановку компонентов на окне приложения. Экземпляр JPanel позволяет 1. применять комбинации из компоновщиков 2. добавить к интерфейсу больше компонентов 3. создавать группы компонентов 4. всё вышеперечисленное
... 30 сек ...
и да, все ответы верны, панель - это вообще довольно интересное изобретение, дальше мы вообще будем на ней рисовать. Для выполнения кода по нажатию кнопки на интерфейсе нужно 1. создать обработчик кнопки и вписать код в него 2. переопределить метод нажатия у компонента кнопки 3. использовать специальный класс “слушателя” кнопок
... 30 сек ...
и здесь правильным ответом будет первый, естественно, сам компонент нам переопределять не нужно, у нас инкапсуляция, да и никаких общих слушателей вообще всех кнопок не существует. \\ \hline
06-20 & К сложному и интересному. мы же хотим, чтобы было очень красиво, а не просто как-то средне, поэтому будем учиться рисовать. да и лекция всё таки про графический интерфейс, совсем не сказать про рисование графики было бы неправильно. \\ \hline
06-21 & Итак, всё что будет происходить дальше - будет происходить на панели с картой. первое, что мы хотим с ней сделать - это перестать раскрашивать её в чёрный. Нам надо научиться на этой самой панели рисовать. для этого фреймворком свинг определён метод панели paintComponent(). он вызывается фреймворком когда что-то происходит, например, когда мы перекрываем наше окно каким-то другим, или двигаем само окно на другой экран, или, например сворачиваем-разворачиваем окно, но в любом случае вызывается он гораздо реже, чем нам нужно. А нужно нам перерисовывать компонент по каждому клику мышкой, например.
Важно помнить, что мы не должны непосредственно этот метод вызывать из кода. этот метод должен вызываться только фреймворком, для того чтобы очень сильно попросить фреймворк тоже есть специальный метод, но сейчас нам важно не это, сейчас нам важно отделить стандартный метод рисования компонента от собственно нашего рисования на этом компоненте, так сказать, бизнес-логику. давайте создадим ещё один метод void render(Graphics g) и будем его вызывать из переопределённого paintComponent(); из самого пэинткомпонента супер вызов удалять не будем, потому что наверняка там происходит что-то очень важное, а мы точно не хотим ничего сломать. Опять же если копнуть немного вглубь, мы когда-нибудь скажем фреймворку что надо перерисовать панельку, он поставит метод пэйнтКомпонент куда-то в очередь сообщений окна, и когда очередь до этого метода дойдёт - окно выполнит перерисовку. то есть действие полностью асинхронно и не напрямую зависит от наших вызовов \\ \hline
06-22 & Итак чтобы рисовать нужен как ни странно объект графики, который умеет, внезапно, рисовать всякие геометрические фигуры, линии и прочие штуки. Вот например линия из координат 0-0 в координаты 100-100, длиной, получается, примерно в 141,5 пиксель, теорему пифагора же все помнят? А чтобы нарисовать сеточку нам понадобится не только прямая линия, но также ширина и высота нашего поля в пикселях. Как их узнать? у панельки есть свойства ширина и высота, ими и воспользуемся, создав две дополнительные переменные int panelWidth = getWidth(); int panelHeight = getHeight();
%% 2
всё, что связано с размерами, наверное, лучше вынести в переменные объекта, просто потому, что они нам, скорее всего, понадобятся в каких-нибудь ещё методах, не находить же их заново. А помимо ширины и высоты нам понадобятся переменные, в которых мы будем хранить высоту и ширину каждой ячейки. размеры каждой ячейки пригодятся, например, для создания отступа одной линии от другой, так что заведём две переменные cellW, cellH. на семинаре мы будем их формировать как-то умно, а пока что мы возьмём ширину и высоту панели и поделим на три;
%% 3
и в методе который рендерит разлинуем. идём циклом от 0 до меньше либо равно 3 потому что высота поля равна трём ячейкам и рисуем горизонтальные линии, пишем (y = i * cellH; g.drawLine от 0, y до panelWidth, y). аналогичный цикл делаем по ширине от 0 до 3 и рисуем аналогично вертикальные линии
у многих после запуска этого кода не полностью рисуется или вовсе не рисуется разлиновка, это происходит из-за асинхронности рисования, упомянутой парой минут ранее, так уж устроена отрисовка в свинге, чтобы не жрать много ресурсов, скорее всего наш метод с линиями отработал уже после того, как свинг нарисовал панельку. мы должны заставить наш компонент полностью перерисоваться. сделаем это вызвав метод repaint(); из метода startNewGame; то есть в методе старта игры мы в перспективе все переменные инициализировали и попросили панель перерисоваться. \\ \hline
06-23 & Теперь сделаем ещё одну магию, которая будет очень похожа на ту, что мы уже изучили, добавим слушателя на мышку. В конструкторе панели пишем addMouseListener(new MouseAdapter() {}) нам понадобится переопределить метод mouseReleased(), то есть нам важно когда пользователь отпустит кнопку (для сенсорных экранов андроида это метод touchUp()) и снова чтобы не заполнять конструктор огромным количеством кода, создадим отдельный метод update(MouseEvent e) и вызовем его из обработчика мышки. В нем мы тоже принудительно вызовем репэйнт, чтобы получился некий игровой цикл, старт - отрисовка, клик мыши, отрисовка, клик-отрисовка. На самом деле это делается ещё и для того чтобы мы саму логику, например, могли портировать куда-то ещё. не обязательно же мышка должна вызывать у нас изменения игрового поля, а например тап по тачскрину или вызов по сети.
%% 2
Теперь в методе update из класса MouseEvent вытаскиваем координаты клика, делим на размер ячейки и тем самым получаем номер ячейки, в которую мы кликнули. пишем: int cellX = e.getX/cellW; int cellY = e.getY/cellH. для верности можем их вывести на экран, чтобы посмотреть как это работает и работает-ли. на слайде видно, как я прокликал каждую ячейку слева направо сверху вниз. \\ \hline
06-24 & далее мы очень быстро пробежимся по логике игры в крестики-нолики просто потому что лекция всё таки больше про графические интерфейсы и ООП, чем про алгоритм игры. \\ \hline
06-25 & Итак нам понадобится генератор псевдослучайных чисел, потому что мы не будем на лекции создавать очень умного противника, символы, которыми мы будем обозначать на поле игрока, компьютер и пустую ячейку, собственно поле и его размеры. Размеры - на будущее.
%% 2
2023-02-05 23:50:05 +03:00
Метод инициализации поля - просто создаём новый массив и заполняем его пустотой. Его вызов будет логично сразу разместить в метод старта новой игры.
2023-02-05 01:34:09 +03:00
%% 3
Когда кто-то, мы или компьютер, будет совершать ход, нам будет важно, попал ли игрок ввобще в какую-то ячейку поля и пустота ли там, потому что, насколько я помню, нельзя ставить крестик поверх нолика и наоборот
%% 4
Компьютер, как я и сказал, будем делать очень примитивный, он просто будет делать ход в случайные места на карте. Перед тем, как я покажу следующий слайд я прошу слабонервных удалиться от экранов и напоминаю, что мы делаем программу в учебных целях
%% 5
2023-02-05 23:50:05 +03:00
как вы видите, учебные цели предполагают не только демонстрацию того, как надо делать, но также и демонстрацию того как делать не надо. все трюки выполнены профессионалами, ни один тимлид после ревью кода не пострадал. на работе никогда так не делайте, всегда пишите с помощью циклов, потому что стоит нам захотеть поиграть на поле 4х4 или 5х5 - этот метод будет расти в геометрической прогрессии. метод принимает на вход символ, который нужно проверить и проверяет - не победил ли он.
2023-02-05 01:34:09 +03:00
%% 6
2023-02-05 23:50:05 +03:00
а вот, например, достаточно хороший метод проверки поля на состояние ничьей. напомню, ничья в крестиках ноликах - это когда никто не победил и пустых клеток не осталось. \\ \hline
2023-02-05 01:34:09 +03:00
2023-02-05 23:50:05 +03:00
06-26 & Итак, теперь наша программа не просто умеет быть красивым окошком, но и может понять ряд интересных вещей, таких как, верный ли сделан ход, наступила ли ничья или кто-то победил. Всё, что мы будем делать дальше, будет сконцентрировано на методах обновления игрового состояния и отрисовки игрового поля. Ну что, нашли мы кликнутую ячейку, давайте в неё ходить, только нам для начала надо проверить, валидная-ли ячейка, и можно-ли туда ходить, вот и проверим: (если не валидная ячейка по координатам (cellX, cellY) или не пустая ячейка по тем же координатам) return; то есть если что-то пошло не так, игнорим клик, выходим из метода, ну а если всё хорошо - просто ходим филд[селХ][селУ] = humandot;
%% 2
И добавим в метод отрисовки ещё немного логики. сделаем обход всех ячеек поля и внутри пишем, если ячейка пустая - просто идём дальше, ничего не делаем, дальше пишем маленькую заготовку: если в ячейке крестик - что-то сделаем, если нолик - сделаем что-то другое и в противном случае выбросим новый рантайм эксепшн с текстом (не пойму что в ячейке + и,ж), мало ли что мы там перекодили и допилили, может решили сделать баттл для трёх игроков?
%% 3
И теперь пришли к отрисовке. хотите, можете сюда картинку вставлять, хотите закрашенные квадратики, хотите рисуйте крестики-нолики, я для простоты буду пользоваться методом объекта графики g.fillOval() и буду рисовать кружки. ему в сигнатуре передаётся левая верхняя координата прямоугольника, в который потом будет вписан овал, и его ширина и высота соответственно. тем, кто хоть раз рисовал в пэинте - знакома эта техника рисования кружков. ну и чтобы придать ему цвета - перед тем как рисовать изменим цвет объекта graphics, g.setColor(Color.BLUE); Итак я предлагаю для человека рисовать синие кружки, а для компа красные. и чтобы сделать небольшой отступ от границ ячейки сделаем следующее, заведём константу DOTSPADDING = 5 пикселя. и первые координаты смещаем на + пэддинг вторые на - 2 пэддинга. Так посимпатичнее.
%% 4
по сути, нам осталось сделать две вещи - прикрутить так называемую бизнес логику, то есть в правильном порядке повызывать методы с логикой игры, избавиться от пары выпадающих исключений и нарисовать сообщение об окончании игры. начнём с конца. с конца игры. для того чтобы вывести результат, мы не будем придумывать ещё одно окно, это не слишком интересно, лучше напишем поверх игрового поля сообщение, а для этого нам надо как-то передать информацию о том, что игра завершена, для этого придумаем состояние окончания игры и опишем его вариации. \\ \hline
06-27 & Получается, теперь осталось это только оживить и нарисовать. Идём в метод апдейт и уточняем, что когда пользователь поставил точку, надо проверить состояние поля на наличие победы или ничьей, дать возможность компу поставить точку и сделать тоже самое
%% 2
дальше в принципе нам только и надо что нарисовать все красотульки. значит идём в метод рендер и думаем там. как только мы вывели поле, и если игра закончилась нам надо вывести сообщение с одним из вариантов исхода. так и запишем if (gameOver) шоуМессаджГеймовер(g); вот просто перевели наши слова на английский. теперь смотрим, чего у нас для этого не хватает в коде, как минимум метода шоумессаджгеймовер() и переменной признака конца игры. да не вопрос, мыжпрограммисты, напишем войд шоумессаджгеймовер(Graphics g), и где-то там вверху private boolean окончена ли игра. и поехали наполнять эти штуки смыслом. метод окончания игры рисует тёмно серый прямоугольник с жёлтой надписью о победе одного из игроков или ничьей в зависимости от геймоверстейта. если вдруг не поняли какой стейт у геймовера - кинули исключение.
%% 3
Осталось куда-то деть булево с окончанием игры и понять что за исключение у нас возникает при старте приложения. Почитав текст исключения мы понимаем, что оно возникает, когда программа не может что-то поделить на ноль. А что и когда она не может поделить на ноль? размеры поля до их инициализации, поэтому нам понадобиться ещё одна булева переменная - инициализирована ли игра.
В конструкторе панели, очевидно, поле ещё не инициализировано
В апдейте, очевидно, что нет смысла обрабатывать клики по неинициализированному полю или полю на котором закончилась игра
В старте новой игры, очевидно, игра перестаёт быть законченой, а поле становится инициализированным
Рендерить на неинициализированном поле нам тоже ничего не нужно.
А вот там, где мы проверяли не победил ли кто, нужно добавить истинность булевой с геймовером. \\ \hline
06-28 & Результат наших трудов вы можете видеть на экране, мне кажется, что получилось неплохо, да и с графическими интерфейсами короткое знакомство провели. \\ \hline
06-29 & На этой лекции мы познакомились с тем, как создавать окна, поговорили о менеджерах размещений, элементах графического интерфейса, и обработчиках событий. Многое осталось за границей этой лекции, во многом из-за того, что мы ещё не изучили такие интересные темы, как многопоточность и программные интерфейсы, но обязательно изучим. Изучение графических фреймворков не должно становиться для вас целью, если не хотите связать своё будущее с фронтенд разработкой, а раз уж вы здесь, думаю, вы уже сделали свой выбор. Разработка графического интерфейса подобного рода должна стать утилитарной задачей, то есть, решаться по мере поступления каких-то вопросов, поэтому мы оставим вопрос разработки графических интерфейсов на будущее самостоятельное изучение или даже лучше - просто периодически будем к ним возвращаться и делать наши приложения более привлекательными визуально. \\ \hline
2023-02-14 23:57:22 +03:00
06-30 & В качестве домашнего задания именно по графическим интерфейсам нужно будет полностью и досконально разобраться с кодом. Не бойтесь записывать свои вопросы и задавать их на семинарах и наставникам. А чтобы попрактиковаться в программировании нужно переделать метод проверки победы, чтобы на него не было больно смотреть. Будет классно, если получится сделать метод проверки победы для поля любого размера и любого числа крестиков-ноликов для победы, то есть, например, чтобы победить на поле 5х5 нужно собрать линию из 4х крестиков. Ну и если останется время, я отметил это задание аж двумя звёздочками, подумайте, как сделать компьютер немного умнее, чем просто случайным. специально отмечу в формулировке задания слово примитивно, потому что никто не ожидает от вас каких-то разборов графовых алгоритмов, минимакса и прочей теории игр. \\ \hline
2023-02-05 01:34:09 +03:00
2023-02-14 23:57:22 +03:00
06-31 & На сегодня это всё. Напоследок соглашусь с Иоганном Вольфгангом фон Гёте, если не очень нравится что-то делать, научиться этому будет довольно трудно. а если трудно, то правильно ли выбрано направление? Любите то, чему учитесь, знания будут сами собой оставаться в голове, без усилий. \\ \hline
2023-02-05 01:34:09 +03:00
\end{longtable}
\end{document}