Нещодавно на форумі постало досить цікаве питання, про те, як можна зробити візуалізацію формул у Qt. На жаль я не мав можливості тривалий час займатися сайтом та форумом, але вирішив, що уявити своє можливе бачення проблеми навіть через деякий час буде корисним.
Питання полягає в тому, щоб візуалізувати формулу, яка написана у вигляді рядка.
Наприклад, ви друкуєте в деякому полі введення sqrt(5), а в якомусь віджеті буде виведено графічне відображення квадратного кореня з 5, тобто не результат, а сама формула. Подібний функціонал реалізований у Latex та LibreOffice.
Виглядатиме це таким чином.
Саме по собі завдання написання подібного функціоналу на мій погляд може виявитися досить важким і потребує значних витрат часу, якщо мова зайде про реалізацію функціоналу з вкладеними формулами і т.д. Тому зупинюся на тій концепції, яка спала на думку для реалізації відображення найпростіших варіантів формул, тобто. без вкладених підформул.
Основні положення
Для реалізації такого функціоналу потрібно:
- Використовуючи регулярні вирази виділити запис формули
- Відобразити формулу на віджеті
Вважаю, що для цього завдання використання регулярних виразів буде цілком доречним, оскільки потрібно правильно розпарсувати рядок і виділити екземпляри формул. Для створення графічного відображення формули напишемо спеціальний клас FormulaWidget , який міститиме регулярний вираз для пошуку в рядку формули, а також метод, який малюватиме формулу у зазначеній точці на віджеті з використанням об'єкта QPainter, який надаватиметься самим віджетом. Даний метод повинен буде повернути нове положення для відображення наступної формули, щоб вони не накладалися один на одного, якщо рядок буде передано кілька формул.
Для малювання формул створимо клас, успадкований від QWidget і перевизначимо його метод paintEvent(), який відповідає за відображення вмісту цього віджету.
Головне вікно програми
Як головне вікно програми буде використовуватися клас, успадкований від QWidget. Ви вибираєте його під час створення проекту. Також він матиме файл графічної форми, в якому потрібно буде розташувати віджети, що нас цікавлять:
- QLineEdit - куди записуватимемо формулу
- FormulaWidget - де відображатиметься формула
У графічному редакторі необхідно буде додати об'єкт звичайного віджету та за допомогою контекстного меню перетворити його на створений попередньо клас FormulaWidget . У контекстному меню є для цього пункт "Перетворити на..." або в англійському варіанті "Promote to...".
Стосуватися вмісту файлу main.cpp не буду, оскільки все створюється за замовчуванням.
віджет.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.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); } }
В результаті роботи даного коду буде отримано програму, представлену на скріншоті на початку статті.
Також додаю код проекту. Завантажити