
Привет, коллеги и доброжелательные критики! Сегодня я решил отвлечься от своей громоздкой работы, чтобы написать что-то простое, но с изюминкой — калькулятор с графическим интерфейсом на C++20 и SFML. Этот проект — не претензия на что-то грандиозное, а скорее лёгкий эксперимент, чтобы вспомнить, как приятно писать код, который сразу видно на экране. Заодно я поделюсь с вами своими мыслями, подходами и парой советов. Давайте разберём, как я это закрутил и почему выбрал именно SFML.
Почему калькулятор? И почему SFML?
Калькулятор — это классика программирования. Помню, как в начале карьеры, ещё в нулевых, писал такие на Pascal для курсовых, потом переделывал их на C с самописным парсером для зачётов. Это как «Hello, World», только с кнопками и математикой — отличный способ проверить свои навыки. Сейчас, конечно, можно было бы взять что-то посерьёзнее: Qt для полноценного GUI, SDL для низкоуровневого контроля или даже Unreal Engine, если уж совсем размахнуться. Но я остановился на SFML, а конкретно на версии 2.6.1. Почему именно она? Это последняя стабильная версия на март 2025 года, и она отлично дружит с современными компиляторами — GCC 12, MSVC 2022 и даже Clang 17. В ней есть мелкие улучшения рендеринга, поддержка C++17/20 из коробки и никаких сюрпризов с совместимостью, что для меня как человека без дополнительного запаса времени очень важно — не люблю тратить своё драгоценное время на борьбу с зависимостями.
SFML я выбрал не просто так. Это лёгкая библиотека для 2D-графики, которая не заставляет тебя писать тонны boilerplate-кода, как Qt, или возиться с низкоуровневыми деталями, как SDL. Сравните с другими системами: Qt — это тяжеловес с кучей возможностей, но для калькулятора его функционал избыточен, как если бы вы использовали танк для поездки в магазин. SDL даёт больше контроля, но требует больше ручной работы — например, самому рисовать текстуры или управлять контекстом OpenGL. SFML же сразу предлагает готовые примитивы вроде RectangleShape, Text и Sprite, что идеально для простого GUI. Её плюсы: минимализм (быстро настраивается), производительность (для 2D почти не жрёт ресурсов), кроссплатформенность (Windows, Linux, macOS без лишних телодвижений). Почему не SFML 3.1, потому — что синтаксис поменялся и нужно потратить время, чтобы вникнуть, а времени увы очень мало. Может в будущем я и буду писать код на SFML 3.1, но точно не сейчас. Да и багов хватает всегда с выходом чегото нового.
Я использую SFML для небольших экспериментов, где нужна быстрая визуализация: прототипы интерфейсов, простые инструменты для отладки, визуализации данных или даже мелкие игрушки для души. Например, пару лет назад я делал простой шутер — «Кощей», рендерил тысячи клеток в реальном времени, и она справилась без лагов. Для больших проектов я бы взял Qt или Unity, но тут задача была другая — сделать что-то рабочее за пару часов, без оверхеда и с удовольствием.
Архитектура: как я это разложил
Проект у меня получился из трёх основных классов плюс точка входа. Я старался держать всё просто, но с учётом современных практик — никаких сырых указателей, никаких C-style массивов. Вот что вышло:
Button — класс для кнопок. Простая обёртка над прямоугольником и текстом, но с умными указателями для управления памятью.
Calculator — главный класс, который собирает интерфейс, обрабатывает клики и связывает всё с вычислениями.
ExpressionEvaluator — рекурсивный парсер выражений. Без него это был бы просто красивый блокнот с кнопками.
main.cpp — минимальный код для запуска окна и цикла событий.
Я сразу решил использовать std::unique_ptr вместо сырых указателей — в 2025 году это уже стандарт, и возиться с new/delete нет никакого смысла. Также заменил все массивы на std::vector — меньше багов, больше читаемости. Давайте разберём каждый кусок подробнее.
Кнопки (Button.h)
#pragma once #include <string> #include <memory> #include <SFML/Graphics.hpp> class Button : public sf::Drawable, public sf::Transformable { public: Button(std::string text, sf::Font& font, unsigned int characterSize, sf::Vector2f position, sf::Vector2f size) : m_rect{std::make_unique<sf::RectangleShape>(size)}, // Создаем прямоугольник кнопки m_text{std::make_unique<sf::Text>(text, font, characterSize)} { // Создаем текст на кнопке m_rect->setPosition(position); // Задаем позицию кнопки m_rect->setFillColor(sf::Color(200, 200, 200)); // Серый фон m_rect->setOutlineColor(sf::Color::Black); // Черная обводка m_rect->setOutlineThickness(2); // Толщина обводки m_text->setPosition(position.x + 20, position.y + 20); // Текст с отступом внутри кнопки m_text->setFillColor(sf::Color::White); // Белый цвет текста } void pressEffect() { // Эффект нажатия — меняем цвет m_rect->setFillColor(sf::Color(150, 150, 150)); m_rect->setOutlineColor(sf::Color(150, 150, 150)); } void releaseEffect() { // Эффект отпускания — возвращаем исходный цвет m_rect->setFillColor(sf::Color(200, 200, 200)); m_rect->setOutlineColor(sf::Color::Black); } std::string getText() const { // Получаем текст кнопки return m_text->getString(); } sf::FloatRect getGlobalBounds() const { // Границы кнопки с учетом трансформаций return getTransform().transformRect(m_rect->getGlobalBounds()); } private: void draw(sf::RenderTarget& target, sf::RenderStates states) const override { // Отрисовка кнопки states.transform *= getTransform(); target.draw(*m_rect, states); // Рисуем прямоугольник target.draw(*m_text, states); // Рисуем текст } std::unique_ptr<sf::RectangleShape> m_rect; // Умный указатель на прямоугольник std::unique_ptr<sf::Text> m_text; // Умный указатель на текст };
Класс кнопки — это мой первый шаг к интерфейсу. Он простой, но функциональный: прямоугольник с текстом, который реагирует на клики. Использовал std::unique_ptr, чтобы памятью управляла сама программа — никаких утечек, никакого ручного delete. Добавил визуальную обратную связь через pressEffect и releaseEffect — кнопка темнеет при нажатии, что делает UI живым. Цвета выбрал на глаз: серый фон и белый текст — классика, но в реальном проекте я бы вынес их в константы или конфиг-файл, чтобы дизайнеры могли играться. SFML тут хорош тем, что сразу даёт RectangleShape и Text — не надо самому писать шейдеры или возиться с текстурами, как в SDL. Ещё я подумал центрировать текст поумнее, но отступ в 20 пикселей для демки сгодился.
Калькулятор (Calculator.h)
#pragma once #include <string> #include <vector> #include <memory> #include <SFML/Graphics.hpp> #include "Button.h" #include "ExpressionEvaluator.h" class Calculator : public sf::Drawable, public sf::Transformable { public: explicit Calculator(sf::Font& font) { // Конструктор принимает шрифт display = std::make_unique<sf::RectangleShape>(sf::Vector2f(360, 50)); // Создаем дисплей display->setPosition(20, 20); // Позиция дисплея display->setFillColor(sf::Color(173, 216, 230)); // Голубой фон display->setOutlineColor(sf::Color::Black); // Черная обводка display->setOutlineThickness(2); // Толщина обводки displayText = std::make_unique<sf::Text>("", font, 30); // Текст на дисплее displayText->setPosition(30, 30); // Позиция текста displayText->setFillColor(sf::Color::Black); // Черный цвет текста windowBackground = std::make_unique<sf::RectangleShape>(sf::Vector2f(400, 600)); // Фон окна windowBackground->setFillColor(sf::Color::White); // Белый цвет фона const std::vector<std::string> labels = { // Список меток для кнопок "7", "8", "9", "/", "4", "5", "6", "*", "1", "2", "3", "-", "0", "C", "=", "+", "(", ")", "<<<" }; buttons.reserve(labels.size()); // Резервируем место под кнопки for (size_t i = 0; i < labels.size(); ++i) { // Создаем кнопки в цикле buttons.emplace_back(std::make_unique<Button>( labels[i], font, 24, sf::Vector2f(20 + (i % 4) * 90, 100 + (i / 4) * 90), sf::Vector2f(80, 80) )); } } void handleEvent(const sf::Event& event, sf::RenderWindow& window) { // Обработка событий if (event.type == sf::Event::MouseButtonPressed && event.mouseButton.button == sf::Mouse::Left) { for (const auto& button : buttons) { if (button->getGlobalBounds().contains( static_cast<sf::Vector2f>(sf::Mouse::getPosition(window)))) { button->pressEffect(); // Анимация нажатия processInput(button->getText()); // Обрабатываем ввод displayText->setString(input); // Обновляем дисплей } } } if (event.type == sf::Event::MouseButtonReleased && event.mouseButton.button == sf::Mouse::Left) { for (const auto& button : buttons) { button->releaseEffect(); // Возвращаем цвет } } } private: void draw(sf::RenderTarget& target, sf::RenderStates states) const override { // Отрисовка калькулятора states.transform *= getTransform(); target.draw(*windowBackground, states); // Рисуем фон target.draw(*display, states); // Рисуем дисплей target.draw(*displayText, states); // Рисуем текст for (const auto& button : buttons) { target.draw(*button, states); // Рисуем все кнопки } } void processInput(const std::string& text) { // Логика обработки ввода using namespace std::string_literals; if (text == "C"s) { // Очистка input.clear(); } else if (text == "="s) { // Вычисление результата try { double result = ExpressionEvaluator::evaluate(input); input = std::to_string(result); if (input.ends_with(".000000")) { // Убираем лишние нули input = input.substr(0, input.find('.')); } } catch (const std::exception&) { input = "Error"s; // Ошибка при вычислении } } else if (text == "<<<"s) { // Удаление последнего символа if (!input.empty()) { input.pop_back(); } } else if (text == "+"s || text == "-"s || text == "*"s || text == "/"s) { // Операторы std::string_view ops = "+-*/"; if (!input.empty() && ops.find(input.back()) == std::string_view::npos && input.length() < 19) { input += text; } } else if (input.length() < 19) { // Добавляем символ, если не превышен лимит input += text; } displayText->setString(input); // Обновляем текст на дисплее } std::unique_ptr<sf::RectangleShape> windowBackground; // Фон окна std::unique_ptr<sf::RectangleShape> display; // Дисплей std::unique_ptr<sf::Text> displayText; // Текст дисплея std::vector<std::unique_ptr<Button>> buttons; // Вектор кнопок std::string input; // Текущий ввод };
Это ядро всего проекта. Я долго думал, как организовать кнопки, и решил остановиться на сетке 4×5 — это стандартная раскладка, как на старых калькуляторах Casio, только с парой дополнительных кнопок вроде скобок и «backspace». Размеры окна (400×600) и кнопок (80×80) подбирал вручную, чтобы всё аккуратно влезло, а отступы в 20 пикселей между элементами добавил для читаемости. Дисплей сделал голубым — просто захотелось чего-то яркого на белом фоне. В processInput вся логика ввода: защита от двойных операторов (чтобы не вводились «++» или «*/»), лимит в 19 символов (SFML начинает обрезать текст, если больше), плюс обработка ошибок вроде деления на ноль. Использовал ends_with из C++20 — мелочь, но избавляет от ручной проверки концов строки. SFML тут хорош своей простотой: метод draw сам рендерит всё в нужном порядке, и мне не пришлось писать сложную логику обновления.
Ещё я заметил, что при быстрых кликах SFML немного подтормаживает — это не баг самой библиотеки, а особенность того, как я обрабатываю события. В реальном проекте я бы добавил дебаунсинг или перерисовывал только изменённые элементы, но для демки оставил как есть.
Парсер (ExpressionEvaluator.h)
#pragma once #include <string> #include <string_view> #include <stdexcept> #include <charconv> class ExpressionEvaluator { public: static double evaluate(std::string_view expression) { // Вычисляем выражение size_t pos = 0; return parseExpression(expression, pos); } private: static double parseExpression(std::string_view expr, size_t& pos) { // Разбираем выражение (+, -) double result = parseTerm(expr, pos); while (pos < expr.length()) { char op = expr[pos]; if (op != '+' && op != '-') break; pos++; double term = parseTerm(expr, pos); result = (op == '+') ? result + term : result - term; } return result; } static double parseTerm(std::string_view expr, size_t& pos) { // Разбираем члены (*, /) double result = parseFactor(expr, pos); while (pos < expr.length()) { char op = expr[pos]; if (op != '*' && op != '/') break; pos++; double factor = parseFactor(expr, pos); if (op == '*') result *= factor; else if (factor == 0) throw std::invalid_argument("Деление на ноль!"); else result /= factor; } return result; } static double parseFactor(std::string_view expr, size_t& pos) { // Разбираем множители (числа, скобки) skipWhitespace(expr, pos); if (pos >= expr.length()) throw std::invalid_argument("Некорректное выражение"); if (expr[pos] == '(') { // Обработка скобок pos++; double result = parseExpression(expr, pos); skipWhitespace(expr, pos); if (pos >= expr.length() || expr[pos] != ')') throw std::invalid_argument("Нет закрывающей скобки"); pos++; return result; } double result{}; auto [ptr, ec] = std::from_chars(expr.data() + pos, // Парсим число expr.data() + expr.length(), result); if (ec != std::errc()) throw std::invalid_argument("Некорректное число"); pos = ptr - expr.data(); return result; } static void skipWhitespace(std::string_view expr, size_t& pos) { // Пропускаем пробелы while (pos < expr.length() && std::isspace(expr[pos])) pos++; } };
Парсер — это, пожалуй, самая интересная часть. Я решил сделать рекурсивный спуск. Алгоритм простой, но правильный: сначала парсим скобки и числа (через parseFactor), потом умножение и деление (в parseTerm), и только потом сложение с вычитанием (в parseExpression). Это автоматически учитывает приоритет операций, так что «2 + 3 * 4» даст 14, а не 20. Использовал std::from_chars вместо stringstream — это быстрее и меньше тянет за собой STL-оверхеда. Плюс, std::string_view экономит копирования строк, что для парсера мелочь, но приятная.
Я добавил поддержку пробелов через skipWhitespace, хотя в этом интерфейсе она не особо нужна — просто привычка писать код с запасом на будущее. Ещё парсер выбрасывает исключения при ошибках вроде деления на ноль или незакрытых скобок — в реальном проекте я бы добавил нормальное логирование, но тут просто вывожу «Error» на дисплей. SFML тут не участвует, но без парсера калькулятор был бы просто красивой оболочкой.
Точка входа (main.cpp)
#include <SFML/Graphics.hpp> #include <iostream> #include "Calculator.h" int main() { sf::RenderWindow window(sf::VideoMode(400, 600), L"SFML Калькулятор"); // Создаем окно auto font = std::make_unique<sf::Font>(); // Загружаем шрифт if (!font->loadFromFile("arialmt.ttf")) { std::cerr << "Не удалось загрузить шрифт!\n"; return -1; } Calculator calculator(*font); // Создаем калькулятор while (window.isOpen()) { // Главный цикл sf::Event event; while (window.pollEvent(event)) { if (event.type == sf::Event::Closed) window.close(); // Закрытие окна calculator.handleEvent(event, window); // Обработка событий } window.clear(); // Очистка экрана window.draw(calculator); // Отрисовка калькулятора window.display(); // Показываем результат } return 0; }
Точка входа — это стандартный цикл SFML. Окно 400×600 выбрал как компромисс между компактностью и читаемостью. Шрифт взял Arial, потому что он универсален и не требует возни с лицензиями — в продакшене я бы добавил fallback на системный шрифт через sf::Font::loadFromMemory или проверку через std::filesystem. SFML тут стабильно рендерит всё без сюрпризов, хотя я заметил, что при частых кликах FPS может проседать — это не баг библиотеки, а моя реализация без оптимизаций.

Рефлексия: что получилось и что можно лучше
Проект занял у меня пару часов в субботу вечером, и результат меня приятно удивил. Калькулятор считает выражения вроде «2 + 3 * (4 — 1)» (правильно выдаёт 11), ловит ошибки вроде деления на ноль или незакрытых скобок, и выглядит аккуратно. SFML показала себя с лучшей стороны: рендеринг быстрый, API интуитивный, никаких глюков с памятью или шрифтами. C++20 тоже не подвёл: std::string_view экономит копирования, ends_with упрощает обработку строк, а std::from_chars делает парсинг чисел шустрым.
Нет предела совершенству, что можно улучшить:
Десятичные числа. Сейчас парсер понимает только целые — надо добавить поддержку точек и, возможно, научные форматы вроде «1.23e-4».
Производительность. Для коротких выражений мой рекурсивный спуск норм, но на длинных строках (например, 100 операторов) лучше взять стековый алгоритм или подключить Boost.Spirit. Я даже прикинул, как это сделать, но для демки оставил как есть.
UI/UX. Клавиатурный ввод был бы логичным дополнением — сейчас только мышью тыкать. Ещё можно добавить масштабируемость окна, тёмную тему или анимации переходов между состояниями.
Тестирование. Я писал на коленке, но в реальном проекте нужны юнит-тесты для парсера — хотя бы на базовые случаи вроде «2+2», «1/0» и «(2+3)*4».
Логирование. Ошибки сейчас просто пишут «Error» на дисплей. В продакшене я бы вывел их в файл или консоль с деталями: где упало, что ввели.
Оптимизация рендеринга. SFML немного подтормаживает при частых кликах — можно добавить дебаунсинг на события или перерисовывать только изменённые элементы через dirty rectangles.
Ещё я подумал про локализацию — например, заменить точку на запятую для регионов, где так принято, но это уже избыточно для такого проекта. В целом, SFML 2.6.1 дала мне ровно то, что я хотел: быстрый старт и минимум головной боли.
Рекомендации новичкам и не только
Если вы только начинаете, вот что я бы посоветовал:
-
Не бойтесь библиотек вроде SFML — это не так страшно, как кажется. Она проще, чем Qt, и учит основам работы с графикой.
-
Осваивайте современный C++ — умные указатели вроде unique_ptr и контейнеры вроде vector спасут вас от кучи багов с памятью.
-
Попробуйте написать парсер вручную хотя бы раз — это отличное упражнение для понимания алгоритмов и структур данных.
-
Делайте визуальную обратную связь — даже простая смена цвета кнопки делает UI живым и дружелюбным.
-
Экспериментируйте с версиями — SFML 2.6.1 стабильна, но если вам нужны новые фичи, следите за веткой разработки на GitHub, уже доступна версия 3.1 .
Для проффи совет другой: возьмите такую простую задачу и доведите её до идеала. Добавьте тесты, профилирование, конфиги, обработку граничных случаев. Это хороший способ не закиснуть на рабочих рутинах и вспомнить, почему мы вообще любим кодить. Я, например, после этого проекта задумался, как бы переписать парсер на концепты C++20 или прикрутить многопоточность для рендеринга — просто ради интереса. Спасибо за внимание и всем хорошего времени суток.
Творите и любите своё творение, будьте добры один к другому!!!
ссылка на оригинал статьи https://habr.com/ru/articles/891354/
Добавить комментарий