Evgenii Legotckoi
22 апреля 2018 г. 2:18

Qt/C++ - Урок 076. Визуализация математических формул на Qt

Недавно на форуме появился довольно интересный вопрос, о том, как можно сделать визуализацию формул в Qt. К сожалению я не имел возможности длительное время заниматься сайтом и форумом, но решил, что представить своё возможное видение проблемы даже спустя некоторое время, будет полезным.

Вопрос состоит в том, чтобы визуализировать формулу, которая написана в виде некоторой строки.

Например, вы печатаете в некотором поле ввода sqrt(5), а в каком-то виджете будет выведено графическое отображение корня квадратного из 5, то есть не результат, а сама формула. Подобный функционал реализован в Latex и LibreOffice.

Выглядеть это будет следующим образом.

Сама по себе задача написания подобного функционала на мой взгляд может оказаться довольно тяжёлой и требующей значительных затрат времени, особенно если речь зайдёт о реализации функционала со вложенными формулами и т.д. Поэтому остановлюсь на той концепции, которая пришла мне в голову для реализации отображения простейших вариантов формул, т.е. без вложенных подформул.


Основные положения

Для реализации подобного функционала требуется:

  • Используя регулярные выражения выделить запись формулы
  • Отобразить формулу на виджете

Полагаю, что для данной задачи использование регулярных выражений будет вполне уместным, поскольку нужно правильно распарсить строку и выделить экземпляры формул. Для создания графического отбражения формулы напишем специальный класс FormulaWidget , который будет содержать регулярное выражение для поиска в строке формулы, а также метод, который будет отрисовывать формулу в указанной точке на виджете с использованием объекта QPainter, который будет предоставляться самим виджетом. Данный метод должен будет вернуть новое положение для отрисовки следующей формулы, чтобы они не накладывались друг на друга, если в строке будет передано несколько формул.

Для отрисовки формул создадим класс, наследованный от QWidget и переопределим его метод paintEvent(), который отвечает за отрисовку содержимого данного виджета.

Главное окно приложения

В качестве главного окна приложения будет использоваться класс, наследованный от QWidget. Вы выбираете его при создании проекта. Также он будет иметь файл графической формы, в котором нужно будет расположить интересующие нас виджеты:

  • QLineEdit - куда будем записывать формулу
  • FormulaWidget - где будет отображаться формула

В графическом редакторе необходимо будет добавить объект обычного виджета и с помощью контекстного меню преобразовать его в созданный предварительно класс FormulaWidget . В контекстном меню имеется для этого пункт "Преобразовать в..." или в английском варианте "Promote to...".

Касаться содержимого файла main.cpp не буду, поскольку всё создаётся по умолчанию.

widget.h

  1. #ifndef WIDGET_H
  2. #define WIDGET_H
  3.  
  4. #include <QWidget>
  5.  
  6. namespace Ui {
  7. class Widget;
  8. }
  9.  
  10. class Widget : public QWidget
  11. {
  12. Q_OBJECT
  13.  
  14. public:
  15. explicit Widget(QWidget *parent = 0);
  16. ~Widget();
  17.  
  18. private:
  19. Ui::Widget *ui;
  20. };
  21.  
  22. #endif // WIDGET_H

widget.cpp

Для передачи набранного текста в виджеты формулы воспользуемся подключением сигнала textChanged у QLineEdit к слоту setFormula, который создадим в классе FormulaWidget .

  1. #include "widget.h"
  2. #include "ui_widget.h"
  3.  
  4. Widget::Widget(QWidget *parent) :
  5. QWidget(parent),
  6. ui(new Ui::Widget)
  7. {
  8. ui->setupUi(this);
  9. connect(ui->lineEdit, &QLineEdit::textChanged, ui->formulaWidget, &FormulaWidget::setFormula);
  10. }
  11.  
  12. Widget::~Widget()
  13. {
  14. delete ui;
  15. }

Классы для отрисовки формул

А теперь рассмотрим сам класс для визуализации формул и класс самой формулы. Оба этих класса будут расположены в одном заголовочном файле ( FormulaWidget.h ) и файле реализации ( FormulaWidget.cpp ).

FormulaItem

FormulaWidget.h

  1. // Класс визуализации формулы
  2. class FormulaItem
  3. {
  4. public:
  5. explicit FormulaItem(QString value) : m_value(value){}
  6.  
  7. static const QString REGULAR_EXPRESSION; // Строка регулярного выражения для поиска формулы
  8. // Метод отрисовки формулы
  9. QPoint draw(const QPoint& pos, QPainter& p) const;
  10.  
  11. private:
  12. QString m_value; // Значение формулы
  13. };

FormulaWidget.cpp

  1. const QString FormulaItem::REGULAR_EXPRESSION = "sqrt\\((?<value>\\d+)\\)";
  2.  
  3. QPoint FormulaItem::draw(const QPoint& pos, QPainter& p) const
  4. {
  5. int valueWidth = p.fontMetrics().width(m_value);
  6. int valueHeight = p.fontMetrics().height();
  7.  
  8. p.drawLine(pos.x(), 4 + valueHeight / 2, pos.x() + 5, 4 + valueHeight);
  9. p.drawLine(pos.x() + 5, 4 + valueHeight, pos.x() + 10, pos.y() + 1);
  10. p.drawLine(pos.x() + 10, pos.y() + 1, pos.x() + 14 + valueWidth, pos.y() + 1);
  11.  
  12. p.drawText(QRect(pos.x() + 12, pos.y() + 4, pos.x() + 12 + valueWidth, pos.y() + 4 + valueHeight), m_value);
  13. return QPoint(pos.x() + valueWidth + 20, pos.y());
  14. }

Статическая постоянная переменная REGULAR_EXPRESSION является строкой, которая содержит регулярное выражение для поиска вхождения формулы.

В Qt существует два класса для работы с регулярными выражениями:

  1. QRegExp - я использовал его в статье по написанию подсветки синтаксиса html разметки.
  2. QRegularExpression - по нему я статей ещё не писал, и это будет первой статьёй с его использованием

Первый класс был введён раньше, второй был введён только в Qt 5.0. Первый, как я понимаю, это собственная реализация регулярных выражений компании Qt. А второй класс - это уже реализация регулярных выражений с поддержкой синтаксиса исключительно языка Perl . На данный момент в документации рекомендуется ипользовать QRegularExpression , а для изучения синтаксиса регулярных выражений изучать документацию на Perl .

Что касается страшного метода draw , то его реализация отвечает за красивую отрисовку формулы с установкой положения значения внутрь квадратного корня. Там достаточно псевдоматематики для попиксельной отрисовки всех линий и чисел с наличием магических чисел. В реальных проектах старайтесь избегать такого и использовать именованные константы наподобие PADDING_BOTTOM, OFFSET и т.д.

Одним из важных моментов в методе draw является то, что объект QPainter должен передаваться по ссылке в качестве аргумента, но при этом ссылка не может быть константной, поскольку данный объект будет модифицироваться. Так что здесь всё в порядке. По своему опыту скажу, что это нормально для работы с отрисовкой кастомных виджетов, когда их QPainter передаётся по неконстантной ссылке в другие методы.

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

"sqrt\((? \d+)\)" - в данном регулярном выражении таким именем является value .

FormulaWidget

FormulaWidget.h

  1. // Класс для отрисовки всех формул
  2. class FormulaWidget : public QWidget
  3. {
  4. Q_OBJECT
  5. using BaseClass = QWidget;
  6. public:
  7. explicit FormulaWidget(QWidget* parent = nullptr);
  8.  
  9. public slots:
  10. // Слот для установки формулы
  11. void setFormula(const QString& formula);
  12.  
  13. protected:
  14. virtual void paintEvent(QPaintEvent* event) override;
  15.  
  16. private:
  17. QList<FormulaItem> m_items;
  18. };

В классе присутствует два важных метода. Первый для установки строки, из которой будем извлекать формулы, а второй переопределённый метод paintEvent(). Переопределённый метод отвечает за отрисовку виджета, в нём будет также будем отрисовывать все формулы. Данный метод вызывается в стеке методов обработки событий в Qt. Напрямую данный метод никогда не вызывают, он вызывается при возникновении определённых событий, но чтобы его вызвать достаточно вызвать метод update().

FormulaWidget.cpp

  1. FormulaWidget::FormulaWidget(QWidget* parent) :
  2. BaseClass(parent)
  3. {
  4. // Установим цвет фона виджета, по умолчанию он такой же, как в системном оформлении ОС
  5. QPalette pal = palette();
  6. pal.setColor(QPalette::Background, Qt::white);
  7. setAutoFillBackground(true);
  8. setPalette(pal);
  9. }
  10.  
  11. void FormulaWidget::setFormula(const QString& formula)
  12. {
  13. // Очищаем все формулы
  14. m_items.clear();
  15.  
  16. // Создаём объект регулярного выражения для поиска формулы
  17. QRegularExpression sqrt_value(FormulaItem::REGULAR_EXPRESSION);
  18. // Ищем все вхождения формулы
  19. QRegularExpressionMatchIterator i = sqrt_value.globalMatch(formula);
  20.  
  21. // создаём все объекты формул
  22. while (i.hasNext())
  23. {
  24. QRegularExpressionMatch match = i.next();
  25. if (match.hasMatch())
  26. {
  27. m_items.append(FormulaItem(match.captured("value")));
  28. }
  29. }
  30.  
  31. // Запускаем перерисовку
  32. update();
  33. }
  34.  
  35. void FormulaWidget::paintEvent(QPaintEvent* event)
  36. {
  37. // Для перерисовки используется объект QPainter,
  38. // который обязательно должен получить объект за отрисовку которого он отвечает
  39. QPainter p(this);
  40. p.setRenderHint(QPainter::Antialiasing);
  41. p.setPen(Qt::black);
  42.  
  43. QPoint formulaPos(2, 2);
  44.  
  45. // Производим отрисовку всех формул, которые удалось найти
  46. for (const FormulaItem& item : m_items)
  47. {
  48. formulaPos = item.draw(formulaPos, p);
  49. }
  50. }

В результате работы данного кода будет получено приложение, представленное на скриншоте в начале статьи.

Также прилагаю код проекта. Скачать

Вам это нравится? Поделитесь в социальных сетях!

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь