После того, как Геннадий Владимирович опубликовал у нас статью, я был весьма заинтригован этой графической библиотекой, однако, все равно несколько колебался перед ее использованием…
Проведя пару экспериментов, а также воспроизведя ряд примеров из статьи про QtE, я решил все-таки попробовать написать что-нибудь самостоятельно просто для того, чтобы заставить себя перейти на новый тулкит, а заодно пощупать его возможности.
Так мне пришла в голову мысль повторить «подвиг», написав калькулятор, но уже на D и Qt5!
Начну с того, что я вообще незнаком с Qt и его концепциями, хоть и когда-то пытался познакомится с ним. Однако, я уверен, что незнание Qt5 не помеха, поскольку во Всемирной Паутине есть куча примеров по этому замечательному тулкиту, а также есть вполне исчерпывающая документация по API, доступная вот здесь.
Первым делом, убеждаемся, что libQtE5Widgets64.so (или QtE5Widgets64.dll) в наличии (предполагается, что у вас, также как и у меня 64-разрядная операционная система) и если это не так, то скачиваем и собираем эти библиотеки.
Мне повезло, в том, что у меня оказалась абсолютно чистая Kubuntu 16.04 и один из наших гостей описал подробную процедуру по сборке QtE5 на 64-разрядном Linux, которой я воспользовался. Помимо этого, перед тем, как выполнить компиляцию пришлось установить кое-что из дополнительного программного обеспечения с помощью команды:
sudo apt-get install qt5-default qtcreator cmake
Далее, пришлось немного помучиться с компиляцией проекта в QtCreator, которая проходит по инструкции Виталия Колыванова, а вот установку скомпилированной libQtE5Widgets64.so.1.0.0 я провел несколько иначе: сначала я ее переименовал в libQtE5Widgets64.so, а затем просто скопировал в папку /lib системы — как это ни странно, но этого действия оказалось чуть более чем достаточно.
Теперь, все что нам нужно — это файл qte5.d из стандартной поставки QtE5 и чистый файл app.d.
Два этих файла мы объединим в один простой проект в среде Monodevelop с MonoD, воспользовавшись опцией создания пустого проекта (как это сделать, я уже описывал в одной из статей) и получим тем самым возможность автоматического дополнения кода и удобный список всех функций, который на настоящий момент содержаться в QtE.
На этом подготовка к разработке калькулятора закончена и мы плавно переходим к процессу его создания.
Для начала определимся какие элементы интерфейса нам потребуется, чтобы воплотить простейший калькулятор, который будет считать только в целых числах (в основном из-за чисто технического ограничения, но это для нас не так уж важно).
Вполне очевидно, что для калькулятора потребуется дисплей, роль которого сыграет элемент QLCDNumber; несколько кнопок под числа и операции, которые можно реализовать с помощью элемента управления QPushButton; а также потребуется некоторое количество так называемых «сайзеров» или «выравнивателей» для размещения внутри них элементов: QVBoxLayout — для вертикального размещения элементов внутри себя и QHBoxLayout — для горизонтального размещения элементов.
Я очень надеюсь, что вы хотя бы просто пробежались глазами по статье «QtE5 — изучаем D и Qt5 в комфортной графической среде», поскольку дизайн приложения в QtE5 очень сложен (по крайней мере, мне так до сих пор кажется), а объяснить его в нескольких словах я вряд ли смогу и кроме того, дальше идет довольно таки сложный код размещения элементов…
Помимо самих элементов интерфейса, нам потребуется такой элемент, внутри которого мы могли бы с уверенностью размещать нужные элементы управления, такие как кнопки и др. К счастью, в Qt5 есть такой элемент и называется он QWidget, и именно от этого элемента нам и нужно осуществить наследование для того, чтобы создать окно со своими собственными элементами GUI:
alias WindowType = QtE.WindowType; enum WHITE = "background : white;"; class MainForm : QWidget { this(QWidget parent, WindowType windowType) { super(parent, windowType); resize(300, 400); setWindowTitle("QtE Calculator"); setStyleSheet(WHITE); // ... } }
Примерно такую заготовку мы будем использовать в нашем калькуляторе.
Вначале мы создаем псевдоним для QtE.WindowType чисто для упрощения кода и для простоты его прочтения, после чего определяем производный от QWidget класс MainForm. В классе MainForm определяем стандартный конструктор, принимающий два параметра — элемент, который является «родителем» (родителем в плане системы вызовов, а не наследования) и тип окна. Внутри конструктора тоже не происходит ничего необычного: класс MainForm обращается к родительскому конструктору посредством super, затем выставляет свой собственный размер (300 пикселей в длину и 400 в высоту), а затем выводит в область заголовка некоторую надпись (в данном случае, этой надписью является надпись — «QtE Calculator»).
А вот следующая инструкция, а именно инструкция setStyleSheet(WHITE) уже является необычной.
Дело в том, что эта инструкция одинакова применима к большинству элементов графического интерфейса QtE и позволяет используя знания HTML и CSS задавать внешний вид некоторого элемента, в данном случае — обычного окна. Перечисление WHITE также является одним из элементов стратегии упрощения кода и задает обычную строковую константу, содержащую инструкцию по окраске фона в белый цвет.
Теперь, мы можем перейти ко внутреннему содержимому класса MainForm и для начала мы определим почти весь набор элементов интерфейса:
alias WindowType = QtE.WindowType; enum WHITE = "background : white;"; class MainForm : QWidget { QVBoxLayout verticalSizer, verticalSizer1, buttonGroup5; QHBoxLayout horizontalSizer, buttonGroup1, buttonGroup2, buttonGroup3, buttonGroup4; QPushButton button0, button1, button2, button3, button4, button5, button6, button7, button8, button9, sign, clear, add, subtract, multiply,divide, equal; this(QWidget parent, WindowType windowType) { super(parent, windowType); resize(300, 400); setWindowTitle("QtE Calculator"); setStyleSheet(WHITE); // ... } }
Почти весь (а это кнопки для чисел, операций и кнопки вычисления), потому что один из элементов интерфейса мы определим как глобальную переменную (да, да, я знаю, что это нехорошо, но иных вариантов не было), а именно — QLCDNumber:
QLCDNumber lcd;
После этого можно настроить элементы управления, согласно нашим нуждам, и начнем мы пожалуй, с LCD-дисплея, стилизовав его под дисплей обычных карманных калькуляторов:
lcd = new QLCDNumber(this); lcd.setMode(QLCDNumber.Mode.Dec); lcd.setStyleSheet("background : lightgreen; color : gray;"); lcd.display(0);
Этот код должен быть размещен непосредственно внутри конструктора класса MainForm, поскольку является кодом инициализации. Здесь требуется немного пояснить, что делает этот набор методов объекта lcd. Метод setMode устанавливает режим отображения значений на LCD-индикаторе и принимает в качестве аргумента константу (или перечисление, точно не скажу), которая размещена прямо в этом же классе. В данном случае, константой QLCDNumber.Mode.Dec устанавливается десятичный режим отображения чисел, хотя ради интереса можно поставить что-нибудь более интригующее, к примеру. шестнадцатеричный режим отображения (константа QLCDNumber.Mode.Hex). После установки правильного режима отображения, мы настраиваем стиль самого дисплея с помощью CSS, после чего используем самый главный для нас метод — метод display, который позволяет вывести на lcd некоторое целое число.
К сожалению, именно из-за display наш калькулятор будет считать только в целых числах.
LCD-дисплей, не единственный элемент интерфейса, и это значит, что необходимо подумать о размещении элементов в окне приложения.
Дело в том, что элементы интерфейса в Qt5 можно разместить в окне приложения с помощью т.н. сайзеров (sizers) или, как их называет разработчик QtE5, выравнивателей.
Сайзеров в QtE5 два типа: QVBoxLayout — вертикальный и QHBoxLayout — горизонтальный выравниватель. В них мы и будем размещать кнопки, используя для этого методы, определенные для обоих типов сайзеров: addWidget, размеющающий элементы управления, и addLayout, размещающий уже сами сайзеры.
Создадим элементы GUI и настроим их свойства, после чего создадим сайзеры и поместим в них один за другим элементы (просто следите за кодом, иначе никак это и не объяснить). Размещение будем проводить, используя замечательную конструкцию языка программирования D под названием with:
alias WindowType = QtE.WindowType; enum WHITE = "background : white;"; class MainForm : QWidget { QVBoxLayout verticalSizer, verticalSizer1, buttonGroup5; QHBoxLayout horizontalSizer, buttonGroup1, buttonGroup2, buttonGroup3, buttonGroup4; QPushButton button0, button1, button2, button3, button4, button5, button6, button7, button8, button9, sign, clear, add, subtract, multiply,divide, equal; this(QWidget parent, WindowType windowType) { super(parent, windowType); resize(300, 400); setWindowTitle("QtE Calculator"); setStyleSheet(WHITE); lcd = new QLCDNumber(this); lcd.setMode(QLCDNumber.Mode.Dec); lcd.setStyleSheet("background: lightgreen; color : gray;"); lcd.display(0); verticalSizer = new QVBoxLayout; verticalSizer1 = new QVBoxLayout; horizontalSizer = new QHBoxLayout; with (buttonGroup1 = new QHBoxLayout) { sign = new QPushButton("+/-", this); button0 = new QPushButton("0", this); clear = new QPushButton("C", this); QAction action0 = new QAction(null, &onButton0, null); connects(button0, "clicked()", action0, "Slot()"); QAction actionSign = new QAction(null, &onSignButton, null); connects(sign, "clicked()", actionSign, "Slot()"); QAction actionClear = new QAction(null, &onClearButton, null); connects(clear, "clicked()", actionClear, "Slot()"); addWidget(sign); addWidget(button0); addWidget(clear); } with (buttonGroup2 = new QHBoxLayout) { button1 = new QPushButton("1", this); button2 = new QPushButton("2", this); button3 = new QPushButton("3", this); QAction action1 = new QAction(null, &onButton1, null); connects(button1, "clicked()", action1, "Slot()"); QAction action2 = new QAction(null, &onButton2, null); connects(button2, "clicked()", action2, "Slot()"); QAction action3 = new QAction(null, &onButton3, null); connects(button3, "clicked()", action3, "Slot()"); addWidget(button1); addWidget(button2); addWidget(button3); } with (buttonGroup3 = new QHBoxLayout) { button4 = new QPushButton("4", this); button5 = new QPushButton("5", this); button6 = new QPushButton("6", this); QAction action4 = new QAction(null, &onButton4, null); connects(button4, "clicked()", action4, "Slot()"); QAction action5 = new QAction(null, &onButton5, null); connects(button5, "clicked()", action5, "Slot()"); QAction action6 = new QAction(null, &onButton6, null); connects(button6, "clicked()", action6, "Slot()"); addWidget(button4); addWidget(button5); addWidget(button6); } with (buttonGroup4 = new QHBoxLayout) { button7 = new QPushButton("7", this); button8 = new QPushButton("8", this); button9 = new QPushButton("9", this); QAction action7 = new QAction(null, &onButton7, null); connects(button7, "clicked()", action7, "Slot()"); QAction action8 = new QAction(null, &onButton8, null); connects(button8, "clicked()", action8, "Slot()"); QAction action9 = new QAction(null, &onButton9, null); connects(button9, "clicked()", action9, "Slot()"); addWidget(button7); addWidget(button8); addWidget(button9); } with (buttonGroup5 = new QVBoxLayout) { add = new QPushButton("+", this); subtract = new QPushButton("-", this); multiply = new QPushButton("*", this); divide = new QPushButton("/", this); QAction actionAdd = new QAction(null, &onAddButton, null); connects(add, "clicked()", actionAdd, "Slot()"); QAction actionSubtract = new QAction(null, &onSubtractButton, null); connects(subtract, "clicked()", actionSubtract, "Slot()"); QAction actionMultiply = new QAction(null, &onMultiplyButton, null); connects(multiply, "clicked()", actionMultiply, "Slot()"); QAction actionDivide = new QAction(null, &onDivideButton, null); connects(divide, "clicked()", actionDivide, "Slot()"); addWidget(add); addWidget(subtract); addWidget(multiply); addWidget(divide); } equal = new QPushButton("=", this); QAction actionEqual = new QAction(null, &onEqualButton, null); connects(equal, "clicked()", actionEqual, "Slot()"); verticalSizer1 .addLayout(buttonGroup4) .addLayout(buttonGroup3) .addLayout(buttonGroup2) .addLayout(buttonGroup1); horizontalSizer .addLayout(verticalSizer1) .addLayout(buttonGroup5); verticalSizer .addWidget(lcd) .addLayout(horizontalSizer) .addWidget(equal); setLayout(verticalSizer); } }
Помимо определения свойств кнопок, мы также осуществим их «привязку» к обработчикам событий с помощью объекта QAction и функции connects, связывающей слоты и сигналы самих событий (они определены внутри самой Qt5, а также в файле qte5, созданном Геннадием Владимировичем Моховым). Подробнее об этом, вы можете прочесть в предыдущих статьях про QtE5.
Кроме этого, после размещения элементов и сайзеров, мы размещаем последний сайзер прямо в главном окне с помощью метода setLayout класса MainForm. По приведенному коду, видно, что размещение и настройка элементов очень просты, как в общем, и их создание: в ходе создания, мы просто передаем нужному объекту указатель на главный элемент (parent, в нашем случае, это форма MainForm) и некоторый параметр-описатель, которым может быть текст кнопки или какой-либо другой подходящий параметр.
Приведенный код еще не является завершенным, поскольку мы еще не описали логику калькулятора и события, связывающие уже размещенные на форме кнопки и LCD-дисплей.
Определим несколько глобальных переменных, которые будут накапливать цифры аргументов операций сложения, вычитания, умножения и деления, а также определим строку, хранящую результат операции (необходимо для реализации некоторого поведения обычного карманного калькулятора) и числовую переменную, которая сохранит итог операции, пригодный для отображения на экране:
int result; string resultRegister, numberRegister; string operationSign;
Теперь опишем реакцию цифровых кнопок и кнопки смены знака числа с помощью событий, определенных следующим образом:
extern(C) { void updateLCD(string number) { numberRegister ~= number; lcd.display(to!int(numberRegister)); lcd.update; } void setOperationSign(string sign) { resultRegister = numberRegister; operationSign = sign; numberRegister = ""; result = 0; } void onButton0(void* button) { updateLCD("0"); } void onButton1(void* button) { updateLCD("1"); } void onButton2(void* button) { updateLCD("2"); } void onButton3(void* button) { updateLCD("3"); } void onButton4(void* button) { updateLCD("4"); } void onButton5(void* button) { updateLCD("5"); } void onButton6(void* button) { updateLCD("6"); } void onButton7(void* button) { updateLCD("7"); } void onButton8(void* button) { updateLCD("8"); } void onButton9(void* button) { updateLCD("9"); } void onAddButton(void* button) { setOperationSign("+"); } void onSubtractButton(void* button) { setOperationSign("-"); } void onMultiplyButton(void* button) { setOperationSign("*"); } void onDivideButton(void* button) { setOperationSign("/"); } void onClearButton(void* button) { numberRegister = "0"; lcd.display(0); lcd.update; } void onSignButton(void* button) { numberRegister = "-" ~ numberRegister; lcd.display(to!int(numberRegister)); lcd.update; } }
Работает это очень просто: мы будем прицеплять цифры к числу, отображенному в данный момент на калькуляторе с помощью оператора конкатенации строк. Вспомогательные функции updateLCD и setOperationSign помогут грамотно вывести на дисплей число-результат и корректно установить знак математической операции.
Помимо этого, функция setOperationSign и соответствующие операции смогут отследить выбранную пользователем математическую операцию (сохраняя знак операции в переменной operationSign) или отследить сброс текущего значения на дисплее.
Теперь осталось только опознать математическую операцию, выбранную пользователем, а также обновить значение на дисплее:
void onEqualButton(void* button) { switch (operationSign) { case "+": result = to!int(resultRegister) + to!int(numberRegister); numberRegister = to!string(result); break; case "-": result = to!int(resultRegister) - to!int(numberRegister); numberRegister = to!string(result); break; case "*": result = to!int(resultRegister) * to!int(numberRegister); numberRegister = to!string(result); break; case "/": result = to!int(resultRegister) / to!int(numberRegister); numberRegister = to!string(result); break; default: numberRegister = resultRegister; break; } lcd.display(result); lcd.update; }
Соответственно, необходимо приведенный обработчик события разместить внутри блока extern(C), для того, чтобы Qt5 смогла правильно среагировать на нажатие кнопки «равно». В этом обработчике просто прооисходит перевод строкового накопителя цифр в целое число, а затем использование его в качестве второго аргумента в математической операции (первый аргумент, как вы уже поняли, это переменная result, уже содержащая первое введенное число), обозначение которой уже было помещено в operationSign с помощью соотвествующих кнопкам процедур обработки.
Следующим шагом будет размешение в файле следующей, немного переработанной функции main:
int main(string[] args) { alias normalWindow = QtE.WindowType.Window; if (LoadQt(dll.QtE5Widgets, true)) { return 1; } QApplication app = new QApplication(&Runtime.cArgs.argc, Runtime.cArgs.argv, 1); MainForm mainForm = new MainForm(null, normalWindow); mainForm .show .saveThis(&mainForm); return app.exec; }
Внутри main мы создаем для удобства чтения кода псевдоним на тип окна QtE.WindowType.Window, а также производим проверку того, насколько успешно подгрузилась Qt5. Также мы создаем экземпляр класса QApplication (экземпляр приложения на Qt, входная точка в приложение), осуществляем захват переданных программе аргументов (в случае, если таковые последуют), после чего осуществляем создание экземпляра MainForm (первый параметр — родительский элемент, которого в данном случае нет, поскольку окно само по себе будет первым в иерархии элементов графического интерфейса; второй параметр — тот самый тип окна).
Затем, мы просто отображаем созданное окно, сохраняем на него указатель для Qt5 и запускаем.
- Полный код калькулятора на QtE5. (спойлер)
-
module app; import core.runtime; import std.conv; import qte5; QLCDNumber lcd; int result; string resultRegister, numberRegister; string operationSign; extern(C) { void updateLCD(string number) { numberRegister ~= number; lcd.display(to!int(numberRegister)); lcd.update; } void setOperationSign(string sign) { resultRegister = numberRegister; operationSign = sign; numberRegister = ""; result = 0; } void onButton0(void* button) { updateLCD("0"); } void onButton1(void* button) { updateLCD("1"); } void onButton2(void* button) { updateLCD("2"); } void onButton3(void* button) { updateLCD("3"); } void onButton4(void* button) { updateLCD("4"); } void onButton5(void* button) { updateLCD("5"); } void onButton6(void* button) { updateLCD("6"); } void onButton7(void* button) { updateLCD("7"); } void onButton8(void* button) { updateLCD("8"); } void onButton9(void* button) { updateLCD("9"); } void onAddButton(void* button) { setOperationSign("+"); } void onSubtractButton(void* button) { setOperationSign("-"); } void onMultiplyButton(void* button) { setOperationSign("*"); } void onDivideButton(void* button) { setOperationSign("/"); } void onClearButton(void* button) { numberRegister = "0"; lcd.display(0); lcd.update; } void onSignButton(void* button) { numberRegister = "-" ~ numberRegister; lcd.display(to!int(numberRegister)); lcd.update; } void onEqualButton(void* button) { switch (operationSign) { case "+": result = to!int(resultRegister) + to!int(numberRegister); numberRegister = to!string(result); break; case "-": result = to!int(resultRegister) - to!int(numberRegister); numberRegister = to!string(result); break; case "*": result = to!int(resultRegister) * to!int(numberRegister); numberRegister = to!string(result); break; case "/": result = to!int(resultRegister) / to!int(numberRegister); numberRegister = to!string(result); break; default: numberRegister = resultRegister; break; } lcd.display(result); lcd.update; } } alias WindowType = QtE.WindowType; enum WHITE = "background : white;"; class MainForm : QWidget { QVBoxLayout verticalSizer, verticalSizer1, buttonGroup5; QHBoxLayout horizontalSizer, buttonGroup1, buttonGroup2, buttonGroup3, buttonGroup4; QPushButton button0, button1, button2, button3, button4, button5, button6, button7, button8, button9, sign, clear, add, subtract, multiply,divide, equal; this(QWidget parent, WindowType windowType) { super(parent, windowType); resize(300, 400); setWindowTitle("QtE Calculator"); setStyleSheet(WHITE); lcd = new QLCDNumber(this); lcd.setMode(QLCDNumber.Mode.Dec); lcd.setStyleSheet("background: lightgreen; color : gray;"); lcd.display(0); verticalSizer = new QVBoxLayout; verticalSizer1 = new QVBoxLayout; horizontalSizer = new QHBoxLayout; with (buttonGroup1 = new QHBoxLayout) { sign = new QPushButton("+/-", this); button0 = new QPushButton("0", this); clear = new QPushButton("C", this); QAction action0 = new QAction(null, &onButton0, null); connects(button0, "clicked()", action0, "Slot()"); QAction actionSign = new QAction(null, &onSignButton, null); connects(sign, "clicked()", actionSign, "Slot()"); QAction actionClear = new QAction(null, &onClearButton, null); connects(clear, "clicked()", actionClear, "Slot()"); addWidget(sign); addWidget(button0); addWidget(clear); } with (buttonGroup2 = new QHBoxLayout) { button1 = new QPushButton("1", this); button2 = new QPushButton("2", this); button3 = new QPushButton("3", this); QAction action1 = new QAction(null, &onButton1, null); connects(button1, "clicked()", action1, "Slot()"); QAction action2 = new QAction(null, &onButton2, null); connects(button2, "clicked()", action2, "Slot()"); QAction action3 = new QAction(null, &onButton3, null); connects(button3, "clicked()", action3, "Slot()"); addWidget(button1); addWidget(button2); addWidget(button3); } with (buttonGroup3 = new QHBoxLayout) { button4 = new QPushButton("4", this); button5 = new QPushButton("5", this); button6 = new QPushButton("6", this); QAction action4 = new QAction(null, &onButton4, null); connects(button4, "clicked()", action4, "Slot()"); QAction action5 = new QAction(null, &onButton5, null); connects(button5, "clicked()", action5, "Slot()"); QAction action6 = new QAction(null, &onButton6, null); connects(button6, "clicked()", action6, "Slot()"); addWidget(button4); addWidget(button5); addWidget(button6); } with (buttonGroup4 = new QHBoxLayout) { button7 = new QPushButton("7", this); button8 = new QPushButton("8", this); button9 = new QPushButton("9", this); QAction action7 = new QAction(null, &onButton7, null); connects(button7, "clicked()", action7, "Slot()"); QAction action8 = new QAction(null, &onButton8, null); connects(button8, "clicked()", action8, "Slot()"); QAction action9 = new QAction(null, &onButton9, null); connects(button9, "clicked()", action9, "Slot()"); addWidget(button7); addWidget(button8); addWidget(button9); } with (buttonGroup5 = new QVBoxLayout) { add = new QPushButton("+", this); subtract = new QPushButton("-", this); multiply = new QPushButton("*", this); divide = new QPushButton("/", this); QAction actionAdd = new QAction(null, &onAddButton, null); connects(add, "clicked()", actionAdd, "Slot()"); QAction actionSubtract = new QAction(null, &onSubtractButton, null); connects(subtract, "clicked()", actionSubtract, "Slot()"); QAction actionMultiply = new QAction(null, &onMultiplyButton, null); connects(multiply, "clicked()", actionMultiply, "Slot()"); QAction actionDivide = new QAction(null, &onDivideButton, null); connects(divide, "clicked()", actionDivide, "Slot()"); addWidget(add); addWidget(subtract); addWidget(multiply); addWidget(divide); } equal = new QPushButton("=", this); QAction actionEqual = new QAction(null, &onEqualButton, null); connects(equal, "clicked()", actionEqual, "Slot()"); verticalSizer1 .addLayout(buttonGroup4) .addLayout(buttonGroup3) .addLayout(buttonGroup2) .addLayout(buttonGroup1); horizontalSizer .addLayout(verticalSizer1) .addLayout(buttonGroup5); verticalSizer .addWidget(lcd) .addLayout(horizontalSizer) .addWidget(equal); setLayout(verticalSizer); } } int main(string[] args) { alias normalWindow = QtE.WindowType.Window; if (LoadQt(dll.QtE5Widgets, true)) { return 1; } QApplication app = new QApplication(&Runtime.cArgs.argc, Runtime.cArgs.argv, 1); MainForm mainForm = new MainForm(null, normalWindow); mainForm .show .saveThis(&mainForm); return app.exec; }
Сохраняем в файл и компилируем, запускаем, получая вот такой результат:
Большое спасибо хочу сказать разработчику этой поистине великолепной привязки к Qt5, Мохову Геннадию Владимировичу, за его помощь при обучении работе с QtE5 и за очень ценные советы, данные в ходе написания этой статьи!
В следующей статье, я и автор QtE5, расскажем кое-что интересное и познавательное о QtE5, показав небольшой практический экзерсис в математической графике.