Числа Коллатца на иной лад: графика в QtE5

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

Да, я знаю, что есть такой инструмент как Wine, но это решение является провальным: если ставить Wine, то придется ставить и компилятор dmd под Windows, и еще неизвестно, насколько хорошо скомпилируется dformlib в Wine и будет ли оно работать.

К счастью, внезапно, нашлось другое решение — для рисования последовательности Коллатца можно использовать Qt5!

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

Начнем, как обычно, с создания основного окна, для чего осуществим создание класса MainForm с наследованием от QWidget (хотя можно наследоваться и от QMainForm). Также определим закрытыми ряд графических элементов (+ некоторые служебные вещи), а также одну логическую переменную, которая нам потребуется чуть позже для управления процессом рисования:

alias WindowType = QtE.WindowType;

class MainForm : QWidget
{
	private
	{
		QHBoxLayout horizontalBox;
		QVBoxLayout verticalBox;
		QPushButton drawButton, clearButton;
		QLabel label;
		QSpinBox number;
		QAction action1, action2;
		QWidget drawArea;

		bool startDrawing;
	}

	this(QWidget parent, WindowType windowType) 
	{
		super(parent, windowType); 
		resize(1030, 530); 
		setWindowTitle("QtE Calculator");
		setStyleSheet("background : white");

		horizontalBox = new QHBoxLayout;
		verticalBox = new QVBoxLayout;
	}
}

Как видите, здесь все в точности также, как было описано в предыдущих статьях, только horizontalBox и verticalBox, мы уже создали, а элементы интерфейса убрали в закрытый (private) блок (напоминаю,в D по умолчанию, модификатор доступа для полей класса/структуры — public).

И вот теперь, можно начать заполнение формы элементами…

Первый элемент, который мы объявим, это область, в которой будем рисовать. Честно говоря, мне не удалось в QtE5 найти аналог PictureBox из DFL2/dformlib, однако, можно с помощью своеобразной хитрости проэмулировать этот элемент GUI. Qt5 позволяет рисовать на практически любом виджете, поэтому, мы поступим следующим образом, создадим пустой виджет и зададим для него нужный стиль отображения. Например, это можно сделать так:

with (drawArea = new QWidget(null))
{
		setToolTip("<span style="color: black;">Область рисования графики</span>");
		setStyleSheet("background : white");
}

Все на редкость просто: помещаем куда надо объект типа QWidget, а затем просто устанавливаем для него подсказку, показываемую при наведении на него мыши (с помощью метода setToolTip) и белый цвет фона. Важная деталь здесь состоит в том, что обязательно надо поставить, какое-нибудь свойство для QWidget, иначе при запуске скомпилированного приложения из консоли получим ряд интересных ругательств от Qt5.

После этого, можно описать еще один интересный элемент нашего интерфейса — текстовое поле с возможностью ограничения вводимых значений. Этот элемент GUI в Qt5 называется QSpinBox (поле со счетчиком) и имеет ряд интересных методов: setPrefix — для установки подписи перед вводимыми данными, setMinimum — для установки минимального значения и setMaximum — для установки максимального значения.

Определим QSpinBox таким образом, чтобы наименьшее значение было равно 2, а наибольшее равнялось 1000:

with (number = new QSpinBox(this))
{
	setStyleSheet("font-size: 16pt;");
	setPrefix("Начало последовательности: ");
	setMinimum(2);
	setMaximum(1000);
}

А теперь опишем надпись и кнопки, а также осуществим связывание кнопок с соответствующими обработчиками, используя механизм «сигнал-слот» (создавая объекты QAction и используя функцию connects):

drawButton = new QPushButton("Draw", this);
clearButton = new QPushButton("Clear", this);

action1 = new QAction(null, &onDrawButton, aThis);
action2 = new QAction(null, &onClearButton, aThis);

connects(drawButton, "clicked()", action1, "Slot()");
connects(clearButton, "clicked()", action2, "Slot()");

Размещаем созданные элементы GUI внутри уже существующих сайзеров, которые помещаем на текущую форму, а также сделаем еще одно — определим свою процедуру отрисовки для элемента drawArea (внимание! это нужно проделать именно в конце всех определений, иначе не сработает!):

verticalBox
	.addWidget(number)
			.addWidget(drawButton)
			.addWidget(clearButton)
			.addWidget(label);

	horizontalBox
			.addLayout(verticalBox)
			.addWidget(drawArea);

	setLayout(horizontalBox);

drawArea.setPaintEvent(&onPaint1, aThis);

Однако, нет самих событий еще нет в наличии, как в общем и их обработчиков. Но это не проблема, и мы можем определить наши обработчики вот так:

extern (C)
{
	void onPaint1(MainForm* mainFormPointer, void* eventPointer, void* painterPointer) 
	{ 
		(*mainFormPointer).runPaint(eventPointer, painterPointer);
	}

	void onDrawButton(MainForm* mainFormPointer) {
		(*mainFormPointer).runDrawButton;
	}

	void onClearButton(MainForm* mainFormPointer) {
		(*mainFormPointer).runClearButton;
	}
}

Обработчик onPaint1 будет обрабатывать процедуру перерисовки для всего окна, onDrawButton будет обрабатывать нажатие кнопки «Draw», а onClearButton — нажатие кнопки «Clear».

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

Лучше всего начать с реализации метода runPaint, который весьма «прост» в своей реализации:

void runPaint(void* eventPointer, void* painterPointer) 
{

	QPainter painter = new QPainter('+', painterPointer); 

	QColor color = new QColor;
	color.setRgb(200, 200, 200, 250);

	QPen pen = new QPen;
	pen.setColor(color);

	painter.setPen(pen);

	for (int i = 0; i < 510; i += 10)
	{
		painter.drawLine(0, i, 500, i);
		painter.drawLine(i, 0, i, 500);
	}

	if (startDrawing)
	{
		startDrawing = false;
		auto N = cast(int) number.value;
		painter.drawCollatzSequence(N);
	}

	painter.end;
}

Метод принимает два указателя — на событие и событие отрисовки (так принято, хотя я уверен, что есть способ проще), после чего внутри метода происходит «захват» указателя на событие перерисовки и передача его в объект QPainter, через который и происходит рисование каких-либо объектов. Далее создаем объект типа QColor, чтобы задать цвет который нам потребуется. Однако, конструктор объекта QColor пустой, а это значит, что сами компоненты цвета в формате RGB придется задать другим способом — через использование метода setRgb объекта QColor.

Сама по себе установка цвета не приведет ни к каким изменениям, поскольку не задан объект при помощи которого будет осуществляться рисование. Именно поэтому следующий наш шаг — это создание карандаша (объект типа QPen), а затем следует передача ему только что созданного цвета (метод setColor) и установка карандаша как элемента, осуществляющего всю отрисовку внутри некоторой области рисования (метод setPen объекта painter).

Объясняя механизм работы, я забыл упомянуть о том, что цвет который мы установили описывается в формате RGBA, т.е. в виде RGB с каналом прозрачности, и при этом описывает цвет линий координатной сетки (хотя она не будет подписана).

Метод painter.end является лишь указанием для Qt5 того факта, что рисование окончено (дело в том, что в Qt5 есть буферизация, как раз end указывает ее границу. Без указания painter.end отрисовка даже не начнется!).

Но, самое интересное идет после отрисовки сетки: если параметр startDrawing будет иметь значение true, то это значит, что нужно нарисовать последовательность Коллатца для заданного числа (в противном случае, просто рисуется сетка), получив его из QSpinBox (это можно провернуть, воспользовавшись свойством value для QSpinBox и приведя его к целому числу) и передав результат в функцию drawCollatzeSequence, которую можно описать так:

void drawCollatzSequence(QPainter painter, int number)
{
	auto doubleX(int x)
	{
		if ((x % 2) == 0)
		{
			return x / 2;
		}
		else
		{
			return 3 * x + 1;
		}
	}

	
	QColor color = new QColor;
	color.setRgb(0, 0, 192, 128);
	
	QPen pen = new QPen;
	pen.setColor(color);
	pen.setWidth(2);
	
	painter.setPen(pen);

	auto collatzSequence = doTimes!doubleX(number, 112);
	
	auto firstX = 0; 
	auto firstY = collatzSequence.front;
	
	foreach (elem; collatzSequence.enumerate(0)) 
	{
		painter.drawLine(firstX, firstY, elem[0] * 4, 250 - (elem[1] / 40));
		firstX = elem[0] * 4;
		firstY = 250 - (elem[1] / 40);
	}
}

Небольшое пояснение: в этой функции отрисовки все точно также, как и в статье про числа Коллатца, но в этот раз, используется QPainter и число-параметр, который определяет для какого числа рисовать последовательность. Также, перед непосредственным получением числа из поля со счетчиком, происходит установка startDrawing в false, которая закрывает повторную прорисовку последовательности и которая позволяет осуществлять рисование только по необходимости. Я решил опустить реализацию doTimes, поскольку она ее использовал без изменений (можног найти в уже упомянутой статье), а также слегка изменил название производящей функции на doubleX. В остальном, все также, и эта функция не является методом и определена отдельно от класса MainForm.

А теперь, остается только сделать так, чтобы после ввода числа и нажатия на кнопку «Draw», последовательность Коллатца для введенного в QSpinBox была построена, для чего в класс MainForm вводим метод runDrawButton:

void runDrawButton() 
{
	startDrawing = true;
	update();
}

В этом методе все очень просто: мы выставляем startDrawing в true, для того, чтобы перерисовывание области drawArea привело к отображению не только сетки (которая будет нарисована в любом случае), но и к отображению заданной последовательности Коллатцаю После этого, для успешного отображения, надо вызвать принудительное обновление всей формы (иначе результат не будет виден), что делается методом update.

Очистку области рисования сделать еще проще, поскольку startDrawing уже заблаговременно установлена в false — мы просто вызываем перерисовку:

void runClearButton()
{
	drawArea.update();
}

Теперь остается только разместить код doTimes в файле functors.d и собрать весь код из статьи в файл app.d, добавив туда необходимые секции импорта и переработанную функцию main (смотри предыдущие статьи про QtE) и скопилировать командой:

dmd app.d functors.d qte5.d

После чего наблюдаем вот такую картинку:

Полный код примера.

    module app;
     
    import core.runtime;
     
    import std.conv;
    import std.random;
    import std.range;
     
     
    import qte5;
     
    import functors;
     
    extern (C)
    {
    	void onPaint1(MainForm* mainFormPointer, void* eventPointer, void* painterPointer) 
    	{ 
    		(*mainFormPointer).runPaint(eventPointer, painterPointer);
    	}
     
    	void onDrawButton(MainForm* mainFormPointer) {
    		(*mainFormPointer).runDrawButton;
    	}
     
    	void onClearButton(MainForm* mainFormPointer) {
    		(*mainFormPointer).runClearButton;
    	}
    }
     
     
    void drawCollatzSequence(QPainter painter, int number)
    {
    	auto doubleX(int x)
    	{
    		if ((x % 2) == 0)
    		{
    			return x / 2;
    		}
    		else
    		{
    			return 3 * x + 1;
    		}
    	}
     
    	
    	QColor color = new QColor;
    	color.setRgb(0, 0, 192, 128);
    	
    	QPen pen = new QPen;
    	pen.setColor(color);
    	pen.setWidth(2);
    	
    	painter.setPen(pen);
     
    	auto collatzSequence = doTimes!doubleX(number, 112);
    	
    	auto firstX = 0; 
    	auto firstY = collatzSequence.front;
    	
    	foreach (elem; collatzSequence.enumerate(0)) 
    	{
    		painter.drawLine(firstX, firstY, elem[0] * 4, 250 - (elem[1] / 40));
    		firstX = elem[0] * 4;
    		firstY = 250 - (elem[1] / 40);
    	}
    }
     
    alias WindowType = QtE.WindowType;
     
    class MainForm : QWidget
    {
    	private
    	{
    		QHBoxLayout horizontalBox;
    		QVBoxLayout verticalBox;
    		QPushButton drawButton, clearButton;
    		QLabel label;
    		QSpinBox number;
    		QAction action1, action2;
    		QWidget drawArea;
     
    		bool startDrawing;
    	}
     
    	this(QWidget parent, WindowType windowType) 
    	{
    		super(parent, windowType); 
    		resize(1030, 530); 
    		setWindowTitle("Qt5 Collatz");
    		setStyleSheet("background : white");
     
    		horizontalBox = new QHBoxLayout;
    		verticalBox = new QVBoxLayout;
     
    		with (drawArea = new QWidget(null))
    		{
    			setToolTip("<font color=black>Область рисования графики</font>");
    			setStyleSheet("background : white");
    		}
     
    		with (number = new QSpinBox(this))
    		{
    			setStyleSheet("font-size: 16pt;");
    			setPrefix("Начало последовательности: ");
    			setMinimum(2);
    			setMaximum(1000);
    		}
     
    		label = new QLabel(this);
    		label.setText("<h3>Программа для рисования чисел-градин</h3><p>Этот демо пример разработали Мохов Г.В. и Бахарев О.Ю.</p>");
     
    		
    		drawButton = new QPushButton("Draw", this);
    		clearButton = new QPushButton("Clear", this);
     
    		action1 = new QAction(null, &onDrawButton, aThis);
    		action2 = new QAction(null, &onClearButton, aThis);
     
    		connects(drawButton, "clicked()", action1, "Slot()");
    		connects(clearButton, "clicked()", action2, "Slot()");
     
    		verticalBox
    			.addWidget(number)
    				.addWidget(drawButton)
    				.addWidget(clearButton)
    				.addWidget(label);
     
    		horizontalBox
    			.addLayout(verticalBox)
    				.addWidget(drawArea);
     
    		setLayout(horizontalBox);
     
    		drawArea.setPaintEvent(&onPaint1, aThis);
    	}
     
    	void runPaint(void* eventPointer, void* painterPointer) 
    	{
     
    		QPainter painter = new QPainter('+', painterPointer); 
     
    		QColor color = new QColor;
    		color.setRgb(200, 200, 200, 250);
     
    		QPen pen = new QPen;
    		pen.setColor(color);
     
    		painter.setPen(pen);
     
    		for (int i = 0; i < 510; i += 10)
    		{
    			painter.drawLine(0, i, 500, i);
    			painter.drawLine(i, 0, i, 500);
    		}
     
    		if (startDrawing)
    		{
    			startDrawing = false;
    			auto N = cast(int) number.value;
    			painter.drawCollatzSequence(N);
    		}
     
    		painter.end;
    	}
     
    	void runDrawButton() 
    	{
    		startDrawing = true;
    		update();
    	}
     
    	void runClearButton()
    	{
    		drawArea.update();
    	}
    }
     
     
    int main(string[] args) 
    {
    	alias normalWindow = 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;
    }


P.S : Автор благодарит Мохова Геннадия Владимировича за помощь в разработке кода примера, а также пояснения относительно работы разных частей QtE. Кроме того, этот пример является одним из демо-примеров в репозитории QtE

Добавить комментарий