Пишем калькулятор на C++ с SFML

7a5962f088022ea9816a041d97ad71a2.png

Привет, коллеги и доброжелательные критики! Сегодня я решил отвлечься от своей грамозкой работы, чтобы написать что-то простое, но с изюминкой — калькулятор с графическим интерфейсом на 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 
#include 
#include 

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(size)}, // Создаем прямоугольник кнопки
          m_text{std::make_unique(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 m_rect; // Умный указатель на прямоугольник
    std::unique_ptr m_text; // Умный указатель на текст
};

Класс кнопки — это мой первый шаг к интерфейсу. Он простой, но функциональный: прямоугольник с текстом, который реагирует на клики. Использовал std: unique_ptr, чтобы памятью управляла сама программа — никаких утечек, никакого ручного delete. Добавил визуальную обратную связь через pressEffect и releaseEffect — кнопка темнеет при нажатии, что делает UI живым. Цвета выбрал на глаз: серый фон и белый текст — классика, но в реальном проекте я бы вынес их в константы или конфиг-файл, чтобы дизайнеры могли играться. SFML тут хорош тем, что сразу даёт RectangleShape и Text — не надо самому писать шейдеры или возиться с текстурами, как в SDL. Ещё я подумал центрировать текст поумнее, но отступ в 20 пикселей для демки сгодился.

Калькулятор (Calculator.h)

#pragma once
#include 
#include 
#include 
#include 
#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::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("", font, 30); // Текст на дисплее
        displayText->setPosition(30, 30); // Позиция текста
        displayText->setFillColor(sf::Color::Black); // Черный цвет текста

        windowBackground = std::make_unique(sf::Vector2f(400, 600)); // Фон окна
        windowBackground->setFillColor(sf::Color::White); // Белый цвет фона

        const std::vector 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

Это ядро всего проекта. Я долго думал, как организовать кнопки, и решил остановиться на сетке 4×5 — это стандартная раскладка, как на старых калькуляторах Casio, только с парой дополнительных кнопок вроде скобок и «backspace». Размеры окна (400×600) и кнопок (80×80) подбирал вручную, чтобы всё аккуратно влезло, а отступы в 20 пикселей между элементами добавил для читаемости. Дисплей сделал голубым — просто захотелось чего-то яркого на белом фоне. В processInput вся логика ввода: защита от двойных операторов (чтобы не вводились »++» или »*/»), лимит в 19 символов (SFML начинает обрезать текст, если больше), плюс обработка ошибок вроде деления на ноль. Использовал ends_with из C++20 — мелочь, но избавляет от ручной проверки концов строки. SFML тут хорош своей простотой: метод draw сам рендерит всё в нужном порядке, и мне не пришлось писать сложную логику обновления.

Ещё я заметил, что при быстрых кликах SFML немного подтормаживает — это не баг самой библиотеки, а особенность того, как я обрабатываю события. В реальном проекте я бы добавил дебаунсинг или перерисовывал только изменённые элементы, но для демки оставил как есть.

Парсер (ExpressionEvaluator.h)

#pragma once
#include 
#include 
#include 
#include 

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 
#include 
#include "Calculator.h"

int main() {
    sf::RenderWindow window(sf::VideoMode(400, 600), L"SFML Калькулятор"); // Создаем окно
    auto font = std::make_unique(); // Загружаем шрифт
    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 может проседать — это не баг библиотеки, а моя реализация без оптимизаций.

9f7329a7a72b480d689efbc675e94fa4.jpg

Рефлексия: что получилось и что можно лучше

Проект занял у меня пару часов в субботу вечером, и результат меня приятно удивил. Калькулятор считает выражения вроде »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 дала мне ровно то, что я хотел: быстрый старт и минимум головной боли.

Рекомендации новичкам и не только

Если вы только начинаете, вот что я бы посоветовал:

  1. Не бойтесь библиотек вроде SFML — это не так страшно, как кажется. Она проще, чем Qt, и учит основам работы с графикой.

  2. Осваивайте современный C++ — умные указатели вроде unique_ptr и контейнеры вроде vector спасут вас от кучи багов с памятью.

  3. Попробуйте написать парсер вручную хотя бы раз — это отличное упражнение для понимания алгоритмов и структур данных.

  4. Делайте визуальную обратную связь — даже простая смена цвета кнопки делает UI живым и дружелюбным.

  5. Экспериментируйте с версиями — SFML 2.6.1 стабильна, но если вам нужны новые фичи, следите за веткой разработки на GitHub, уже доступна версия 3.1 .

Для проффи совет другой: возьмите такую простую задачу и доведите её до идеала. Добавьте тесты, профилирование, конфиги, обработку граничных случаев. Это хороший способ не закиснуть на рабочих рутинах и вспомнить, почему мы вообще любим кодить. Я, например, после этого проекта задумался, как бы переписать парсер на концепты C++20 или прикрутить многопоточность для рендеринга — просто ради интереса. Спасибо за внимание и всем хорошего времени суток.

Творите и любите своё творение, будьте добры один к другому!!!

Телеграмм канал — Программирование игр С++

© Habrahabr.ru