Рисование в QT
QT — технология для разработки кросс платформенных приложений, реализуемых на языке C++. Компания Nokia выпустила отличную технологию и отличную IDE к ней, которые, на мой взгляд, являются удачным сочетанием простоты написания программ (вам достаточно знать ООП и C++) и одновременной «контролируемости» (доступны все библиотеки для их просмотра). В результате получается оптимизированное приложение, в котором подключено, только то, что необходимо. Конечно в нем нет некоторых готовых реализаций, например события получения фокуса, однако есть все для его реализации.
При изучении QT можно натолкнуться на глобальную проблему реализации графики. А если попробуете искать в Google, то можно найти форум, на котором сидят гуру, и «посылают» в тот же Google, но конечно есть исключения. Макс Шлее пишет в своей замечательной книге об этом, но это больше похоже на нервные наброски. QT Assistant — отличная вещь, но нет готового примера, чтоб просто разобраться. В общем я разобрался благодаря тому, что использовал все три источника и знание ООП. Конечно можно сказать, что при желании можно разобраться легко в этом, но я хочу чтоб человек зашел в Google, набрал «Рисование в QT» и вышел на статью, в которой будет все описано рассказано на примере.
Идеология
canvas()<>
void paintEvent(QPaintEvent*); //вызов метода, который собственно и будет рисовать то, что вы запрограммируете далее
bool par; /* если вы захотите рисовать по событию, например нажатие кнопки изменит значение этого параметра. А потом Вы можете сделать какое-нибудь условие на этот параметр, при выполнении которого будет что-нибудь рисоваться. */
Qt как рисовать в отдельном окне
Статья «нулевого уровня», но пару примеров кода содержит 🙂
1. Общие принципы
- QPainter – «рисовальщик» QT, класс-исполнитель команд рисования;
- QPaintEngine – «движок» рисования, обычно не используемый из кода непосредственно. Он бывает нужен программисту лишь при создании собственных контекстов рисования, отсутствующих в системе;
- QPaintDevice – контекст рисования (канва), который можно понимать как графическое «полотно», состоящее из пикселов.
В обычных случаях программе достаточно «захватить» адрес объекта контекста, отрисовать нужные графические примитивы и освободить контекст:
Здесь рисуется заштрихованный прямоугольник на канве текущего виджета. Существенно, что этот код должен выполняться из метода paintEvent, который может быть переопределён в любом пользовательском виджете, являющемся наследником имеющего канву класса (см.п.2).
- Перо QPen – для рисования контуров;
- Кисть QBrush – для заполнения контуров цветом.
Непосредственно отрисовкой занимаются методы класса QPainter с названиями на draw (drawLine, drawRect, drawPolygon, drawEllipse) или fill (fillRect, fillPath). Кроме настроек пера и кисти, этим методам часто требуется информация о координатах точек, размерах прямоугольников, текущих цветах и т.п. Для хранения всей этой информации в QT предусмотрен ряд геометрических классов, которые ничего не рисуют, но хранят описания размеров или расположений графических объектов. Большинство геометрических классов реализованы «дважды», например, класс QPoint работает с целочисленными координатами точек на плоскости, а QPointF – с вещественными:
Целочисленные и вещественные координаты точки на плоскости
. x (), . y () – получение координат;
. setX (), . setY () – установка координат.
В классах определены сравнения, арифметические действия над координатами
Размер, то есть, совокупность ширины и высоты
. width (), . height () – получить размеры;
. setWidth (), . setHeight () – установить размеры;
. scale () – масштабировать размер.
Прямоугольник, фактически, это " точка+размер "
См. QPoint , QSize
Отрезки на плоскости
. x 1(), . y 1(), . x 2(), . y 2() – координаты начала и конца отрезка;
. dx (), . dy () – проекции на оси 0 X и 0 Y
Многоугольник на плоскости, фактически, массив точек (координат вершин)
<< добавить точку QPoint
. point ( i ) – вернуть i — ую точку
Цвет в модели RGB (или HSV )
. setRgb ( r,g,b ) установить интенсивности цветов;
. red (), . green , . blue (), . alpha () – получить интенсивности цветов и прозрачность
- translate – сдвинуть начало координат в указанную точку;
- scale – масштабировать систему координат;
- rotate – повернуть систему координат;
- shear – исказить систему координат (выполнить скос);
- save, restore – сохранить/восстановить состояние рисовальщика.
В большинстве случаев при отрисовке объектов удобнее пользоваться этими методами, чем высчитывать всё в «абсолютных координатах» канвы. Например, мы не меняли геометрию треугольника, определённого в функции drawTriangle и не высчитывали при каждой отрисовке новых координат его вершин:
2. Работа с графической канвой
Создадим проект на основе класса QWidget (добавились файлы main.cpp, widget.h, widget.cpp). Так как отрисовка выполняется по событию paintEvent, в заголовочном файле widget.h нам понадобится добавить прототип виртуального метода paintEvent, предусмотренного в каждом виджете. Чтобы виджет мог рисовать на канве, он должен переопределить этот метод:
В файле widget.cpp нам остается написать реализацию метода. Покажем простейшие действия с кистью, пером и строкой текста:
Теперь класс Widget может рисовать на своей канве. Так как событие paintEvent происходит, в том числе, при изменении размеров окна виджета, картинка всё время будет соответствовать окну формы.
3. Работа с изображениями
- Format_Invalid — формат неверен;
- Format_Mono – монохромное изображение с 1 битом на пиксел;
- Format_Index8 – данные представляют собой 8-битные индексы цветовой палитры;
- Format_RGB32 – каждый пиксел представлен 32 битами (интенсивности красного, зелёного и синего, плюс значение альфа-канала, всегда равное 0xFF, то есть, прозрачность не поддерживается);
- Format_ARGB32 – 32 бита на пиксел с поддержкой альфа-канала прозрачности.
Для создания изображения в конструктор класса достаточно передать его размеры в пикселах и формат:
Загрузить изображение можно, передав конструктору путь к нужному файлу:
или воспользовавшись методом load:
Как видно из кода, отобразить изображение можно методом drawImage класса QPainter. Показать часть изображения можно с помощью дополнительных параметров метода.
Папка, которая является текущей, в общем случае зависит от проекта. Обычно это та папка, где находится файл Makefile проекта.
Сохранить изображение может метод save:
Сохранение произойдёт в ту папку, которая является текущей в вашей конфигурации проекта.
Для чтения отдельных пикселов удобен метод pixel:
Записать пикселы можно с помощью метода setPixel:
Показанный код рисует линейный градиент в окне формы, для работы нужны директивы
- invertPixels – позволяет инвертировать цвета пикселов и/или альфа-канал;
- scaled – позволяет масштабировать изображение;
- mirrored – выполняет зеркальное отражение картинки по горизонтали и/или вертикали.
Класс контекстно-зависимого представления изображений QPixmap также унаследован от QPaintDevice. Его использование целесообразно там, где нужен промежуточный буфер для рисования или критична скорость отрисовки графических объектов. Существует также отдельный класс-потомок QBitmap для работы с монохромными изображениями.
4. Построение графиков и диаграмм
Хорошее введение в тему даёт эта статья.
5. «Продвинутый» пример рисования мышью в QT
По всем правилам создаём графическую сцену, таймер и пример простейшей «рисовалки» нажатой левой кнопкой мыши.
Qt как рисовать в отдельном окне
На этом занятии мы научимся писать событийные графические программы с анимированной 2D графикой, используя модуль QtGui библиотеки Qt
Содержание
Следуйте инструкциям. В конце выполните задание, указанное в тексте.
Окно с нарисованным кругом на Qt
Наша первая цель — получить окно, в котором нарисовано статичное изображение круга. Рисование будет выполнятся средствами Qt с программным рендерингом (то есть без участия видеокарты).

Создание проекта
Откройте Visual Studio 2017 и создайте новый проект типа Qt Gui Application (для этого у вас должны быть установлены Qt SDK и Qt Visual Studio Add-In). Укажите название проекта AnimatedBall. Выберите путь размещения проекта так, чтобы в нём не было пробелов и кириллических символов (некоторые инструменты Qt плохо поддерживают Unicode в путях).

В процессе вы можете получить следующую ошибку:

Проблема в том, что Qt SDK может быть установлен в произвольный каталог на диске и Qt Add-in (плагин для Visual Studio) не может его найти. Можно зарегистрировать установленный Qt SDK вручную.
Перейдите к меню “Qt VS Tools” и выберите действие “Qt Options”

В появившемся диалоге вам нужно добавить версию Qt (кнопка Add)

В диалоге выбора пути нужно выбрать путь к каталогу версии Qt (это должен быть каталог на уровень выше каталога bin)

После добавления версии Qt вы можете снова попробовать создать проект!
Чистим проект и добавляем main
В созданном проекте удалите все файлы, кроме “main.cpp”. В файле “main.cpp” удалите всё содержимое. Мы будем писать проект с нуля.
Ниже приведён пример простейшей программы, отображающей окно и состоящей из одной функции main. Для удобства предлагаемый пример подробно прокомментирован. Вам нужно переписать код, не переписывая комментарии
Хорошие комментарии в коде уточняют детали, объясняют странные решения или как-то иначе дополняют код. Если код говорит сам за себя, комментарий к нему не нужен. Комментарии, приведённые в примерах, просто повторяют то, что уже написано в документации. Они не нужны в реальном коде.
Соберите программу и запустите ё в отладчике на пошаговое выполнение. Найдите ответ на вопрос: показывается ли окно программы сразу при вызове window.show() ?
Разобраться со сборкой вам поможет шпаргалка по горячим клавишам Visual Studio:
| hotkey | действие |
|---|---|
| F7 | сборка всех проектов в solution |
| F5 | запуск программы в отладчике |
| F10 | переход через инструкцию в отладчике |
Запущенная программа будет выглядеть примерно так:

Заменяем обычный QWindow на RasterWindow
Наша цель осталась прежней: нарисовать в окне круг. Обычный класс QWindow не содержит кода рисования круга — он вообще ничего не рисует.
В таких случаях в Qt принято наследоваться от класса QWindow, реализуя свой класс окна. Так мы и сделаем: создайте в проекте заголовочный файл “RasterWindow.h” и поместите в него код, описанный ниже, избегая комментариев:
Теперь создайте файл “RasterWindow.cpp”. Прежде всего, в этот файл надо добавить заголовок “RasterWindow.h”.
Затем реализуем в файле “RasterWindow.cpp” конструктор класса. Перепишите этот код (кроме комментария):
Опишем метод event: этот метод вызывается при каждом событии окна, поэтому при его написании следует действовать аккуратно
- если мы не обработали событие, мы должны вызвать метод родительского класса: QWindow::event(event) , иначе механизм обработки событий Qt будет сломан
- мы должны обработать только событие UpdateRequest, которое уведомляет о запросе на изменение окна
- изменение размера должно приводить к перерисовке окна
Опишем метод exposeEvent: этот метод класса QWindow библиотека Qt вызывает при фактическом показе окна.
На примере события QExposeEvent покажем, как именно происходит обработка событий в Qt. В первую очередь, любое событие добавляется в очередь и обрабатывается позднее, когда до него дойдёт очередь. Затем событие проходит через виртуальный (то есть полиморфный) метод event() . Реализация event() по умолчанию перенаправит событие в метод exposeEvent . Мы можем обработать это событие в нашем классе, чтобы выполнить действие сразу после отображения окна.

Опишем метод resizeEvent: этот метод класса QWindow библиотека Qt вызывает при изменении размера окна.
Метод “renderLater” будет добавлять в очередь событий Qt событие обновления экрана (UpdateRequest), тем самым форсируя перерисовку кадра в ближайшем будущем.
Теперь реализуем самый сложный метод класса — renderNow. Метод будет запускать рисование на буфере кадра, очищать буфер, а потом готовый буфер выводить на экран.
Вывод сначала в буфер, а затем в окно известен как двойная буферизация (double buffering) — это общепринятый способ борьбы с мерцанием (flickering) изображения. Представьте, что двойной буферизации не будет. Тогда мы будем рисовать сразу в буфере кадра дисплея, при этом операционная система будет рисовать содержимое дисплея с частотой 60Гц, а наша программа по каким-то причинам будет рисовать каждый кадр вчетверо дольше. В этом случае на экране будут проявляться недорисованные фрагменты картинки:
Поскольку в обычно время рисования мало отличается от времени вывода кадра, в реальности вы увидите лишь высокочастотное мерцание картинки над фоном.
Чтобы избежать этого, мы будем рисовать во вспомогательный буфер. Чтобы убрать следы старого кадра, будем заливать буфер белым цветом путём вызова painter.fillRect . Перепишите код, предложенный ниже:
Остался всего один метод “render”, в котором мы установим параметры кисти рисования и нарисуем эллипс.
Отметим, что перед рисованием мы установили режим устранения ступенчатости. Ступенчатость — это эффект, при котором фигура, нарисованная ровно по решётке пикселей, выглядит угловатой из-за необходимости рисовать линии приближённо. Ступенчатость (aliasing) устраняется специальными алгоритмами рисования, которые частично закрашивают пиксели на границах фигур и линий. При взгляде издалека частичная заливка создаёт для глаза иллюзию прямых контуров.
На иллюстрации слева изображена ступенчатая линия (в большом увеличении), а справа сглаженная линия.

Теперь перейдите в функцию main и замените тип переменной “window” с QWindow на RasterWindow. Не забудьте подключить заголовок “RasterWindow.h” в файле “main.cpp”.
Соберите программу, запусти её. Убедитесь, что окно программы выглядит примерно так:

Исследуем программу в отладчике
Остановитесь и проверьте следующее
- код с предыдущих этапов у вас работает
- все ваши товарищи закончили те же этапы
После этого вместе с преподавателем посмотрите, как с помощью отладчика проинспектировать работу программы. Разберите вопросы:
- почему exposeEvent вызывается уже после вызова window.show() ?
- почему при запуске программы вызывается resizeEvent?
- что показывает call stack, что такое стек и как стек используется для вызов функций?
- где находятся локальные переменные в памяти программы и как посмотреть их в отладчике?
Добавляем анимацию
Наша следующая цель — добавить в окно анимацию движения шарика.
Теоретический минимум: цикл рисования и обработки событий
В любой операционной системе оконные приложения рисуют содержимое окон непрерывно и циклически. В каждую секунду в системе происходит множество событий:
-
мониторы обновляются с частотой
В таких условиях операционной системе (ОС) приходится ловко манипулировать множеством событий и потоков данных. Например, около 60 раз в секунду система собирает изображения всех окон, рисует их поверх друг друга в одну картинку рабочего стола и выводит на экран.
Чтобы ОС могла выполнить свою задачу, программа должна ей помогать: вести общение с системой в режиме интерактивного диалога, реагируя на переданные окну события ввода и рисуя новые кадры со скоростью до 60 кадров в секунду.
Графические библиотеки делают это путём реализации событий рисования (paint events) в цикле событий (event loop). Qt также реализует цикл событий, запуск которого происходит внутри метода QGuiApplication::exec() , в этом цикле событий есть события рисования.
Однако, по умолчанию Qt старается сэкономить ресурсы и рисовать как можно реже. Чтобы реализовать анимацию, нам потребуется форсировать постоянное рисование новых кадров с частотой до 60 кадров в секунду.
Добавляем окну свойство animating
Добавьте в класс RasterWindow новые публичные методы “isAnimating” и “setAnimating”:
Затем добавьте булево поле для хранения данных этого свойства
Мы используем термины “поле класса” и “свойство класса”. Разница между ними в том, что термин “поле” выражает хранение данных в области памяти класса, а термин “свойство” выражает наблюдаемое извне свойство объекта. Свойство может быть реализовано с помощью поля, а может быть вычислено на лету: например, мы можем вычислить площадь прямоугольника, а не хранить её постоянно, если мы знаем ширину и высоту прямоугольника. Если у класса Rectangle есть метод area() , то внешний наблюдатель не заметит разницы между хранением поля и повторным вычислением. Поэтому “свойство” и “поле” — это разные вещи.
Реализация методов будет простой, единственный нюанс заключается в планировании (scheduling) выполнения метода render путём вызова метода renderLater:
Самый важный трюк мы разместим в методе “renderNow”. Если окно зациклено на анимацию, то после рисования кадра мы должны нарисовать новый кадр. Делать это немедленно нельзя: мы уйдём в бесконечную рекурсию, что приведёт к переполнению стека программы.
Поэтому мы будем лишь планировать перерисовку путём добавления события в очередь событий. Фактически перерисовка состоится тогда, когда на то будет воля Qt. Тем не менее, мы рассчитываем на частоту порядка 60Гц (благодаря вертикальной синхронизации и её обработке в Qt).
Перейдите к методу “RasterWindow::renderNow()” и добавьте в конец метода планирование перерисовки:
Перейдите в функцию main и добавьте вызов метода setAnimating:
Соберите программу и запустите. Не видите анимации? Ну конечно, ведь её же нет! Программа рисует порядка 60 кадров в секунду, но кадры пока ещё не отличаются друг от друга.
Добавляем класс сцены и анимацию шарика
Чтобы анимировать шарик, надо на каждом кадре изменять его местоположение. Для этого надо как минимум где-то хранить позицию шарика.
Добавлять позицию шарика в поле класса RasterWindow не стоит: во-первых класс и так стал объёмным, во-вторых хранение позиции шарика явно выбивается из абстракции, которую задаёт термин “окно”.
Каждый класс реализует один слой абстракции. Каждая функция реализует одно действие на одном слое абстракции. Если вы чувствуете, что пишете в одной функции код с разных слоёв абстракции — значит, пора выделить новую функцию. Если вы хотите добавить в класс поле или метод с другого уровня абстракции — возможно, пора выделить новый класс
Создайте файлы “PoolTableScene.h” и “PoolTableScene.cpp”. В новом заголовке поместите объявление класса PoolTableScene. На этот раз комментарий лучше сохранить, т.к. он отражает предназначение класса:
Реализация PoolTableScene имеет всего три нюанса
- мы используем constexpr для определения констант
- мы используем анонимные пространства имён, чтобы при компоновке с другими файлами исходного кода не возникало конфликтов имён
- т.е. другой файл в большой программе тоже может содержать константу BALL_SIZE, но с другим значением
Связываем классы окна и сцены
В языке C++ работает идиома владения: каждым объектом, размещённым в динамической памяти, должен кто-то владеть — либо другой объект, либо какая-то функция.
- если объектом уникально владеет другой объект, то объект-хозяин должен создавать этот объект и удалять его самостоятельно
- удаление должно произойти в любом варианте удаления хозяина: нельзя рассчитывать, что перед удалением хозяина будет вызван какой-либо метод, кроме деструктора
- удаление должно произойти даже при выбросе исключения
В Qt соблюсти эти принципы позволяет идиома parent-child. В современном C++ без Qt для той же цели служит класс unique_ptr и функция “make_unique”. В данном случае у нас нет необходимости наследовать класс сцены от классов Qt, поэтому мы будем использовать идиомы C++.
Кроме сцены нам потребуется таймер, способный замерять время, прошедшее с предыдущего кадра. Скорость работы системы может быть изменчивой, и нельзя заранее предсказать, сколько времени пройдёт с предыдущего кадра в реальных условиях. Однако, для плавной и физически корректной анимации движения нам при каждом обновлении сцен надо знать промежуток времени, прошедший с предыдущего обновления.
Для измерения времени мы будем использовать QElapsedTimer, который применяется следующим способом:
Итак, добавьте перед определением класса RasterWindow включение заголовка “PoolTableScene.h”, заголовка “memory”, в котором определён умный указатель unique_ptr, и заголовка “QtCore/QElapsedTimer”, в котором определён класс таймера:
Затем добавьте в класс RasterWindow два новых поля и два новых приватных метода, а затем удалите метод “render”:
Метод updateScene будет содержать получение времени у таймера, перезапуск таймера и обновление сцены. В случае, если кадры рисуются слишком быстро и время, прошедшее с предыдущего кадра, оказалось меньше 1 миллисекунды, обновление сцены будет пропущено.
Метод renderScene будет содержать подготовку объекта QPainter, заполнение буфера кадра белым цветом и затем вызов метода redraw() у класса сцены.
Также мы изменим два существующих метода: конструктор и метод renderNow. В конструкторе мы будем создавать новый объект сцены, связывая его отношением parent-child с классом окна путём передачи указателя this:
Метод renderNow станет намного проще благодаря выделению кода в методы renderScene и updateScene:
Соберите программу и запустите её. У вас должно появиться окно, в котором будет виден движущийся шарик.
Задание cg2.1: используем векторную алгебру
Векторная алгебра позволяет писать код выразительнее: вместо раздельных расчётов для каждой координаты вы можете складывать, умножать или обрабатывать иным путём две координаты в 2D пространстве, 3 координаты в 3D и 4 координаты в однородном представлении в 3D графике. Кроме того, современные процессоры и видеокарты (особенно видеокарты) получают ускорение за счёт использования векторных операций и векторизации. Впрочем, о векторизации вашего кода позаботится компилятор (если сможет), а вам нужно заботиться о выразительности кода.
Задание: перепишите код в PoolTableScene, чтобы вместо float использовалась структура “Vector2f”.
- хранить позицию удобнее в виде 2D вектора
- скорость лучше задать с помощью константного 2D вектора
- сдвиг шарика за один кадр равен произведению вектора скорости на прошедшее время, значит, пригодится оператор умножения Vector2f на float.
Определение структуры “Vector2f” показано ниже. Обратите внимание на оператор умножения скаляра на вектор, определённый в виде свободной функции.
- операция умножения вектора на скаляр является коммутативной, то есть vector * scale = scale * vector , поэтому определены два оператора: Vector2f operator*(Vector2f, float) и Vector2f operator*(float, Vector2f)
- оба оператора определены вне класса в виде свободных функций, т.к. по крайней мере один из них нельзя сделать методом (мы не можем объявлять методы для типа float)
- к обоим операторам добавлен inline , чтобы избежать нарушения One Definition Rule
Численный метод против аналитического
В компьютерной анимации есть всего несколько общих принципов, используемых практически во всех ситуациях. Сегодня мы поработаем с одним из методов анимации: расчёт по неким правилам движения (например, при симуляции законов физики). Мы разберём разницу между численным и аналитическим методом расчёта величие.
Численный метод: реализуем отталкивание от стенок
Чтобы шар отталкивался от стенок, мы будем проверять, что (левая, правая, верхняя, нижняя) граница шарика столкнулась с соответствующей границей окна. При столкновении мы будем менять скорость по одной из координат на обратную (симулируя отражение).
Сначала подготовимся: добавим структуру RectF, в которой будем хранить границы области, в которой двигается шарик. Создайте заголовочный файл “RectF.h” и перенесите в него реализацию структуры:
Теперь изменим определение класса PoolTableScene: добавим поля для хранения скорости шарика (которая теперь будет изменяться динамически), размера шарика (неизменного), границ сцены (неизменных).
Все величины будем инициализировать в конструкторе.
Чтобы передать размеры сцены при конструировании, изменим конструктор RasterWindow и выделим константы, хранящие размер окна:
Метод “PoolTableScene::update” станет сложнее: после пересчёта координат, хранимых в m_ballPosition , мы должны проверить два случая
- левая граница шарика левее левой границы поля
- правая граница шарика правее правой границы поля
Наконец, мы изменим константы скорости шарика, чтобы быстрее увидеть эффект отталкивания:
Аналогичные проверки потребуются для верхней и нижней границ. Добавьте их, и будьте внимательны при замене переменных. Запустите программу и проверьте: шарик должен отталкиваться от всех 4 стенок
Данный метод расчёта является численным (или итеративным): новая позиция шарика вычисляется на основе только предыдущей позиции и скорости, без учёта начальных значений величин.
Аналитический метод: реализуем волновое движение
При использовании аналитического метода позиция шарика будет функцией от начальных значений величин и времени, прошедшего с начального момента. Изменим код так, чтобы вместо прямолинейного движения с отталкиванием от стенок шар двигался по синусоиде.
Удалите весь код из “PoolTableScene::update”. Затем удалите поле “m_ballSpeed”. Теперь добавим в PoolTableScene возможность учёта времени, прошедшего с начального момента.
- добавьте классу поле float m_time = 0;
- в начале метода “update” добавьте инструкцию m_time += deltaSeconds;
Внешний вид синусоиды показан ниже. Рисунок подсказывает простой способ анимации движения по синусоиде: возьмём время за t, тогда в каждый момент времени:
- x = speedX·t , где
- speedX — скорость по оси X
- ampY — амплитуда (максимальное отклонение по оси Y)
- period — период, через который повторяется один шаг движения

Важный навык программиста — умение запрограммировать формулу, выраженную математически. Сейчас мы прокачаем этот навык!
Введём новые константы, параметризующие движение шарика, и уберём константу BALL_SPEED_Y :
Добавим в конструктор PoolTableScene инициализацию “m_ballPosition”:
Используя универсальное конструирование и перегруженные операторы класса Vector2f, мы можем описать обновление координат одним выражением:
Соберите и запустите программу. Шарик должен двигаться с невысокой скоростью по синусоиде:
Задание cg2.2: волновое движение с отталкиванием
Задание: совместите отталкивание от стенок и волновое движение так, чтобы шарик двигался по синусоиде и отталкивался от левой и правой стенок.
Подсказка: реализовать симуляцию движения в данных условиях можно тремя способами — число аналитическим методом (используя деление по модулю), чисто численным (используя производные) и смешанным численно-аналитическим методом.