Недавно на форуме появился довольно интересный вопрос, о том, как можно сделать визуализацию формул в 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); } }
В результате работы данного кода будет получено приложение, представленное на скриншоте в начале статьи.
Также прилагаю код проекта. Скачать