Недавно на форуме появился довольно интересный вопрос, о том, как можно сделать визуализацию формул в Qt. К сожалению я не имел возможности длительное время заниматься сайтом и форумом, но решил, что представить своё возможное видение проблемы даже спустя некоторое время, будет полезным.
Вопрос состоит в том, чтобы визуализировать формулу, которая написана в виде некоторой строки.
Например, вы печатаете в некотором поле ввода sqrt(5), а в каком-то виджете будет выведено графическое отображение корня квадратного из 5, то есть не результат, а сама формула. Подобный функционал реализован в Latex и LibreOffice.
Выглядеть это будет следующим образом.
Сама по себе задача написания подобного функционала на мой взгляд может оказаться довольно тяжёлой и требующей значительных затрат времени, особенно если речь зайдёт о реализации функционала со вложенными формулами и т.д. Поэтому остановлюсь на той концепции, которая пришла мне в голову для реализации отображения простейших вариантов формул, т.е. без вложенных подформул.
Основные положения
Для реализации подобного функционала требуется:
- Используя регулярные выражения выделить запись формулы
- Отобразить формулу на виджете
Полагаю, что для данной задачи использование регулярных выражений будет вполне уместным, поскольку нужно правильно распарсить строку и выделить экземпляры формул. Для создания графического отбражения формулы напишем специальный класс FormulaWidget , который будет содержать регулярное выражение для поиска в строке формулы, а также метод, который будет отрисовывать формулу в указанной точке на виджете с использованием объекта QPainter, который будет предоставляться самим виджетом. Данный метод должен будет вернуть новое положение для отрисовки следующей формулы, чтобы они не накладывались друг на друга, если в строке будет передано несколько формул.
Для отрисовки формул создадим класс, наследованный от QWidget и переопределим его метод paintEvent(), который отвечает за отрисовку содержимого данного виджета.
Главное окно приложения
В качестве главного окна приложения будет использоваться класс, наследованный от QWidget. Вы выбираете его при создании проекта. Также он будет иметь файл графической формы, в котором нужно будет расположить интересующие нас виджеты:
- QLineEdit - куда будем записывать формулу
- FormulaWidget - где будет отображаться формула
В графическом редакторе необходимо будет добавить объект обычного виджета и с помощью контекстного меню преобразовать его в созданный предварительно класс FormulaWidget . В контекстном меню имеется для этого пункт "Преобразовать в..." или в английском варианте "Promote to...".
Касаться содержимого файла main.cpp не буду, поскольку всё создаётся по умолчанию.
widget.h
- #ifndef WIDGET_H
- #define WIDGET_H
- #include <QWidget>
- namespace Ui {
- class Widget;
- }
- class Widget : public QWidget
- {
- Q_OBJECT
- public:
- explicit Widget(QWidget *parent = 0);
- ~Widget();
- private:
- Ui::Widget *ui;
- };
- #endif // WIDGET_H
widget.cpp
Для передачи набранного текста в виджеты формулы воспользуемся подключением сигнала textChanged у QLineEdit к слоту setFormula, который создадим в классе FormulaWidget .
- #include "widget.h"
- #include "ui_widget.h"
- Widget::Widget(QWidget *parent) :
- QWidget(parent),
- ui(new Ui::Widget)
- {
- ui->setupUi(this);
- connect(ui->lineEdit, &QLineEdit::textChanged, ui->formulaWidget, &FormulaWidget::setFormula);
- }
- Widget::~Widget()
- {
- delete ui;
- }
Классы для отрисовки формул
А теперь рассмотрим сам класс для визуализации формул и класс самой формулы. Оба этих класса будут расположены в одном заголовочном файле ( FormulaWidget.h ) и файле реализации ( FormulaWidget.cpp ).
FormulaItem
FormulaWidget.h
- // Класс визуализации формулы
- class FormulaItem
- {
- public:
- explicit FormulaItem(QString value) : m_value(value){}
- static const QString REGULAR_EXPRESSION; // Строка регулярного выражения для поиска формулы
- // Метод отрисовки формулы
- QPoint draw(const QPoint& pos, QPainter& p) const;
- private:
- QString m_value; // Значение формулы
- };
FormulaWidget.cpp
- const QString FormulaItem::REGULAR_EXPRESSION = "sqrt\\((?<value>\\d+)\\)";
- QPoint FormulaItem::draw(const QPoint& pos, QPainter& p) const
- {
- int valueWidth = p.fontMetrics().width(m_value);
- int valueHeight = p.fontMetrics().height();
- p.drawLine(pos.x(), 4 + valueHeight / 2, pos.x() + 5, 4 + valueHeight);
- p.drawLine(pos.x() + 5, 4 + valueHeight, pos.x() + 10, pos.y() + 1);
- p.drawLine(pos.x() + 10, pos.y() + 1, pos.x() + 14 + valueWidth, pos.y() + 1);
- p.drawText(QRect(pos.x() + 12, pos.y() + 4, pos.x() + 12 + valueWidth, pos.y() + 4 + valueHeight), m_value);
- return QPoint(pos.x() + valueWidth + 20, pos.y());
- }
Статическая постоянная переменная REGULAR_EXPRESSION является строкой, которая содержит регулярное выражение для поиска вхождения формулы.
В Qt существует два класса для работы с регулярными выражениями:
- QRegExp - я использовал его в статье по написанию подсветки синтаксиса html разметки.
- QRegularExpression - по нему я статей ещё не писал, и это будет первой статьёй с его использованием
Первый класс был введён раньше, второй был введён только в Qt 5.0. Первый, как я понимаю, это собственная реализация регулярных выражений компании Qt. А второй класс - это уже реализация регулярных выражений с поддержкой синтаксиса исключительно языка Perl . На данный момент в документации рекомендуется ипользовать QRegularExpression , а для изучения синтаксиса регулярных выражений изучать документацию на Perl .
Что касается страшного метода draw , то его реализация отвечает за красивую отрисовку формулы с установкой положения значения внутрь квадратного корня. Там достаточно псевдоматематики для попиксельной отрисовки всех линий и чисел с наличием магических чисел. В реальных проектах старайтесь избегать такого и использовать именованные константы наподобие PADDING_BOTTOM, OFFSET и т.д.
Одним из важных моментов в методе draw является то, что объект QPainter должен передаваться по ссылке в качестве аргумента, но при этом ссылка не может быть константной, поскольку данный объект будет модифицироваться. Так что здесь всё в порядке. По своему опыту скажу, что это нормально для работы с отрисовкой кастомных виджетов, когда их QPainter передаётся по неконстантной ссылке в другие методы.
В приведённом регулярном выражении есть именованное захваченное значение, по которому мы можем извлечь значение, находящееся в корне, чтобы в дальнейшем отрисовывать его.
"sqrt\((?
FormulaWidget
FormulaWidget.h
- // Класс для отрисовки всех формул
- class FormulaWidget : public QWidget
- {
- Q_OBJECT
- using BaseClass = QWidget;
- public:
- explicit FormulaWidget(QWidget* parent = nullptr);
- public slots:
- // Слот для установки формулы
- void setFormula(const QString& formula);
- protected:
- virtual void paintEvent(QPaintEvent* event) override;
- private:
- QList<FormulaItem> m_items;
- };
В классе присутствует два важных метода. Первый для установки строки, из которой будем извлекать формулы, а второй переопределённый метод paintEvent(). Переопределённый метод отвечает за отрисовку виджета, в нём будет также будем отрисовывать все формулы. Данный метод вызывается в стеке методов обработки событий в Qt. Напрямую данный метод никогда не вызывают, он вызывается при возникновении определённых событий, но чтобы его вызвать достаточно вызвать метод update().
FormulaWidget.cpp
- FormulaWidget::FormulaWidget(QWidget* parent) :
- BaseClass(parent)
- {
- // Установим цвет фона виджета, по умолчанию он такой же, как в системном оформлении ОС
- QPalette pal = palette();
- pal.setColor(QPalette::Background, Qt::white);
- setAutoFillBackground(true);
- setPalette(pal);
- }
- void FormulaWidget::setFormula(const QString& formula)
- {
- // Очищаем все формулы
- m_items.clear();
- // Создаём объект регулярного выражения для поиска формулы
- QRegularExpression sqrt_value(FormulaItem::REGULAR_EXPRESSION);
- // Ищем все вхождения формулы
- QRegularExpressionMatchIterator i = sqrt_value.globalMatch(formula);
- // создаём все объекты формул
- while (i.hasNext())
- {
- QRegularExpressionMatch match = i.next();
- if (match.hasMatch())
- {
- m_items.append(FormulaItem(match.captured("value")));
- }
- }
- // Запускаем перерисовку
- update();
- }
- void FormulaWidget::paintEvent(QPaintEvent* event)
- {
- // Для перерисовки используется объект QPainter,
- // который обязательно должен получить объект за отрисовку которого он отвечает
- QPainter p(this);
- p.setRenderHint(QPainter::Antialiasing);
- p.setPen(Qt::black);
- QPoint formulaPos(2, 2);
- // Производим отрисовку всех формул, которые удалось найти
- for (const FormulaItem& item : m_items)
- {
- formulaPos = item.draw(formulaPos, p);
- }
- }
В результате работы данного кода будет получено приложение, представленное на скриншоте в начале статьи.
Также прилагаю код проекта. Скачать