Qt + MVP + QThread. Строим свой велосипед

в 8:53, , рубрики: c++, mvp, qt, QThread, Программирование

image
Всем добрый день!

Недавно передо мной встала довольно интересная задача, сделать интерфейс пользователя для общения с одной железкой. Связь с ней осуществлялась посредством COM-порта. Так как работа предполагалась с системой реального времени (эта самая железка) в операционной системе Windows, для того, чтобы GUI не тормозил, было решено работу с COM-портом вынести в отдельный поток. Так как требования к самой системе постоянно менялись, для минимизации и ускорения исправлений также было решено писать используя паттерн проектирования MVP. В качестве среды разработки был выбран Qt. Просто потому, что мне нравится данная среда и ее возможности. Так и получилась связка Qt+MVP+Qthread. Кому интересно, что из этого всего вышло и по каким граблям я ходил, прошу под кат.

Планирование

У нас есть некое устройство, с которым мы хотим общаться через COM-порт. Причем, немного усложним себе задачу, мы хотим общаться как с помощью команд, вводимых с клавиатуры, «аля-консоль», так и в автоматическом режиме, когда запросы устройству отправляются по таймеру. Также нам нужно принимать ответ от устройства, обрабатывать его и выводить на форму. Вся работа по приему и отправке сообщений в COM-порт должна выполнятся в отдельном потоке. Плюс к этому должна быть возможность отправлять сообщения на устройство, подключенное к компьютеру из разных мест программы. И желательно еще иметь возможность написать тесты (привет TDD).

На Хабре уже была статья про связку Qt+MVP [1]. Также было несколько статей про использование потоков в Qt [2], [3]. Если говорить кратко, то суть MVP в том, что логика приложения отделяется от его внешнего вида, что позволяет эту логику тестировать отдельно от остальной части программы, с помощью Unit-тестов, использовать в других приложениях, а также быстрее вносить изменения, под меняющиеся требования.

Немного о терминах:
View – отвечает за внешний вид программы, это будет наша форма.
Model – отвечает за логику работы программы, из нее будут отправляться наши сообщения в COM-порт, и в ней же будет разбираться пришедший ответный пакет.
Presenter – это связующее звено между View и Model.
Кратко на этом все, более подробно можно почитать тут и тут. Пора приступать к коду.
Мы напишем небольшое приложение (ссылка на готовый проект в конце) и по ходу написания будем реализовывать то, что вынесено в заголовок статьи.

Model

Я люблю начинать писать программы с логики, а не с интерфейса.
Итак, работу начинаем с написания класса ModelComPort.
Для начала реализуем отправку сообщений в COM-порт.

Наш класс должен:

  1. Автоматически определять имеющиеся COM-порты в системе.
  2. Устанавливать соединение с указанным COM-портом на указанной скорости.
  3. Отправлять сообщения в COM-порт.
  4. Расшифровывать принятое сообщение из COM-порта.

Вот как он будет выглядеть:

ModelComPort.h
class ModelComPort
{
public:
    ModelComPort();
    ~ModelComPort();

    // Соединение с COM-портом
    void connectToComPort();

    // Наименование порта
    void setPortName(QString portName);
    QString getPortName() const;

    // Скорость порта
    void setBaudrate(int baudrate);
    int getBaudrate() const;

    // Получение списка COM-портов
    QList<QString> getListNamePorts() const;

    // Получение состояния порта
    bool isConnect() const;

    // Запись в COM-порт
    void onCommand(QString command);

    // Прием ответа из COM-порта
    void response(QByteArray msg);

private:
    // Поиск существующих COM-портов в системе
    void searchComPorts();
    // Отправка команды на контроллер
    void sendCommand(int command);

private:	
    bool m_connected;                  // Есть ли соединение с COM-портом
    QString m_portName;             // Имя COM-порта
    QList<QString> m_listPorts;   // Список COM-портов в системе
    // Настройки связи
    int m_baudrate;
    int m_dataBits;
    int m_parity;
    int m_stopBits;
    int m_flowControl;

    QByteArray m_inBuf;         // Входной буффер
    
    ComPortThread thread;     // Поток для работы с портом
};

Как видно, для тех свойств, которые подлежат изменению мы устанавливает get и set методы. Пока не обращайте внимание на объект типа ComPortThread, о нем будет рассказано ниже.

Файл ModelComPort.cpp я полностью приводить не буду, остановлюсь только на некоторых нюансах:

Конструктор

ModelComPort::ModelComPort() :
    m_portName(""),
    m_baudrate(QSerialPort::Baud9600),
    m_dataBits(QSerialPort::Data8),
    m_parity(QSerialPort::NoParity),
    m_stopBits(QSerialPort::OneStop),
    m_flowControl(QSerialPort::NoFlowControl),
    m_connected(false)
{    		
    searchComPorts();    
}

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

Метод определения имеющихся COM-портов довольно прост:

void ModelComPort::searchComPorts()
{
    foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts())
    {        
        m_listPorts.append(info.portName());
    }
}

Далее рассмотрим метод, с помощью которого мы создаем подключение:

connectToComPort

void ModelComPort::connectToComPort()
{
    if (!m_connected)
    {
        if (m_portName == "")
	{
	    return;
	}

        if (!thread->isRunning())
	{
            thread.connectCom(m_portName, m_baudrate, m_dataBits, m_dataBits, m_stopBits, m_flowControl);
            thread.wait(500);

            // В случае успешного подключения
            if (thread.isConnect())
	    {
	        m_connected = true;
	    }
	}
    }
    else
    {
        if (thread.isConnect())
	{
            thread.disconnectCom();
	}
	m_connected = false;
    }
}

Тут все просто. Сначала мы определяем, имеется ли у нас уже подключение или нет. Сделано это для того, чтобы при нажатии на одну и ту же кнопку, пользователь мог как подключиться к порту, так и отключится от него. То есть, например, при загрузке мы отключены. Первое нажатие на кнопку подключения подключает нас, второе нажатие на кнопку подключения отключает нас. И так по кругу.
Далее мы определяем, знаем ли мы имя COM-порта, к которому подключаемся. Потом смотрим, запущен ли у нас поток, который будет осуществлять работу с портом. Если поток не запущен, то создаем его, запускаем и он уже подключается к COM-порту. Тут, наверное, стоит остановиться поподробнее. Дело в том, что, чтобы работа с COM-портом осуществлялась в отдельном потоке, этот самый поток и должен создавать подключение во время своей работы. Поэтому мы в классе ModelComPort не создаем подключение сами, а говорим потоку, что хотим создать подключение и передаем ему параметры, с которым хотели бы подключится.
Далее даем потоку время на создание подключения и проверяем удалось ли его создать. Если все хорошо, устанавливаем флаг, что мы подключены.

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

Код очень прост

void ModelComPort::setPortName(QString portName)
{
    m_portName = portName;
}

QString ModelComPort::getPortName() const
{
    return m_portName;
}

void ModelComPort::setBaudrate(int baudrate)
{
    m_baudrate = baudrate;
}

int ModelComPort::getBaudrate() const
{
    return m_baudrate;
}

bool ModelComPort::isConnect() const
{
    return m_connected;
}

Так как одним из условий было то, что команды могут отправляться как автоматически по таймеру, так и с консоли, значит нам нужен метод, который будет принимать текстовую команду с консоли, расшифровывать ее и отправлять в COM-порт.
Метод на вход получает строку с консоли и отправляет соответствующую команду:

void ModelComPort::onCommand(QString command)
{ 
    if (command == "On")
    {
        sendСommand(ON);
    }
    else if (command == "Off")
    {
        sendСommand(OFF);
    }
    
....
    
}

Все команды у нас будут находится в отдельном файле и иметь трехзначный код:

Enum Commands
{
	ON = 101,
	OFF = 102
....
}

Ну а метод sendСommand будет формировать пакет и отдавать его потоку на отправку:

sendCommand

void ModelComPort::sendCommand(int command)
{
    QByteArray buffer;    
    quint8 checkSumm = 0;

    buffer[0] = '#';
    buffer[1] = '<';
    buffer[2] = 0;    
    checkSumm ^= buffer[2];
    buffer[3] = command;
    checkSumm ^= buffer[3];
    buffer[4] = checkSumm;

    thread.transaction(buffer, 250);
}

Число 250 в строке thread.transaction(buffer, 250); это время ожидания в мс на отправку нашего пакета. Если в течении этого времени пакет отправить не удалось, предполагаем, что связи с устройством у нас нет и выводим ошибку.
С классом ModelComPort у нас все, теперь переходим к созданию класса PresenterComPort.

Presenter

Как говорилось ранее, Presenter у нас является промежуточным звеном между View и Model. То есть на него возложена двойная функция. С одной стороны этот класс должен реагировать на все действия пользователя производимые с GUI. С другой стороны, он должен обеспечивать синхронизацию всех наших View и Model. То есть, если у нас имеется несколько View, общая информация, отображаемая на них должна быть одинакова. Это, во-первых, а во-вторых, данные, которые введены на нашем (наших) View должны быть синхронизированы с данными, с которыми работает наша Model.
Итак, посмотрим на наш Presenter.

PresenterComPort

class PresenterComPort : public QObject
{
    Q_OBJECT
public:
    explicit PresenterComPort(QObject *parent = 0);
    ~PresenterComPort();

    void appendView(IViewComPort *view);

private slots:
    // Подключение к Com-порту
    void processConnect();
    // Изменение имени Com-порта
    void processNameComPortChanged(QString portName);
    // Изменение скорости Com-порта
    void processBaudratePortChanged(int baudrate);
    // Отправка команды в COM-порт
    void onCommand(QString command);
    // Получение ответа из COM-порта
    void response(const QByteArray& msg);

private:
    void refreshView() const;
    void refreshView(IViewComPort *view) const;
    void setPortInfo() const;
    void setPortInfo(IViewComPort *view) const;

private:
    ModelComPort *m_model;
    QList<IViewComPort*> m_viewList;
    ComPortThread thread;
};

Как видно, публичный метод тут только один, он используется для привязки нашего (наших) View к Presenter. Для того, чтобы мы могли работать с любым View как с одним объектом, все наши View, должны наследоваться от одного интерфейса. В данном случае я использую один View и наследую его от интерфейса IViewComPort. Подробности данной реализации можно посмотреть тут [1]. Рассмотрим подробнее метод appendView().

appendView

void PresenterComPort::appendView(IViewComPort *view)
{
    // Проверяем наличие данного вида в списке
    if (m_viewList.contains(view))
    {
        return;
    }

    m_viewList.append(view);

    QObject *view_obj = dynamic_cast<QObject*>(view);
    // Подключение к COM-порту
    QObject::connect(view_obj, SIGNAL(processConnect()), this, SLOT(processConnect()));
    // Изменение имени COM-порта
    QObject::connect(view_obj, SIGNAL(processNameComPortChanged(QString)), this, SLOT(processNameComPortChanged(QString)));
    // Изменение скорости подключения
    QObject::connect(view_obj, SIGNAL(processBaudratePortChanged(int)), this, SLOT(processBaudratePortChanged(int)));
    // Отправка команды в COM-порт
    QObject::connect(view_obj, SIGNAL(onCommand(QString)), this, SLOT(onCommand(QString)));

    refreshView(view);
    setPortInfo(view);
}

В нем переданный View заносится в список, а наш Presenter подключается к сигналам, которые могут придти от данного View. Сделано это как раз для того, чтобы обо всех изменениях на форме знал наш Presenter.

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

setPortInfo

void PresenterComPort::setPortInfo(IViewComPort *view) const
{
    // Список всех COM-портов в системе
    QList<QString> tempList = m_model->getListNamePorts();

    // Заносим на вид все COM-порты в системе
    for (int i = 0; i < tempList.count(); i++)
    {
        view->addPortName(tempList.at(i));
    }

    // Заносим на вид возможные скорости подключения
    view->addBaudrate(9600);
    view->addBaudrate(34800);
    view->addBaudrate(115200);
}

Как видно по нему, мы запрашиваем у нашей Model список всех COM-портов, а далее заносим эту информацию на нашу форму.
Возможные скорости подключения я прописал жестко, в своей работе чаще всего использую 9600, но на всякий случай добавил еще пару.
Остальной код можно посмотреть в проекте, выложенном в конце статьи, а то, итак, уже растянулась сильно, а обсудить еще много чего сталось.Переходим к нашему View.

View

На форме у нас будут 2 comboBox для установки настроек подключения, одна кнопка, которая будет отвечать за подключение/отключение к COM-порту. Также у нас будет консоль, в которой мы будем писать команды. И еще будет светодиод, который будет отображать текущее состояние подключения. Если мы подключены будет гореть зеленым.

Итоговый вид формы можно увидеть ниже.
image

Сам код формы особого интереса не представляет, мы просто отправляем сигналы при изменении выбранного пункта в каждом из ComboBox, сигнал при нажатии кнопки подключения, а также испускаем сигнал, если в консоли был нажат Enter.
Все эти сигналы перехватывает наш Presenter, и передает данные в нашу Model для дальнейшей обработки

Пора перейти к реализации нашего потока, который будет отвечать за работу с COM-портом.
Есть несколько мнений, как лучше организовать работу с потоками в Qt. Кто-то создает поток и помещает в него данные, кто-то наследуется от Qthread и переопределяет метод run(). У каждого способа есть свои достоинства и недостатки. В данном случае мы пойдем по второму пути, будем наследоваться от Qthread.

ComPortThread

Итак, рассмотрим на наш класс ComPortThread:

ComPortThread.h

class ComPortThread : public QThread
{
	Q_OBJECT

public:
    ComPortThread(QObject *parent = 0);
    ~ComPortThread();
    
    // Главный цикл
    void run();
    // Отправка сообщения в COM-порт
    void transaction(const QByteArray& request, int waitTimeout);
    // Соединение с COM-портом
    void connectCom(QString namePort, int baudRate, int m_dataBits, int m_parity, int m_stopBits, int m_flowControl);
    // Отключение от COM-порта
    void disconnectCom();
    // Текущее состояние подключения
    bool isConnect();   

signals:
    // Пришел ответ
    void responseMsg(const QByteArray &s);
    // Ошибка подключения к COM-порту
    void error(const QString &s);
    // Истекло время ожидания ответа
    void timeout(const QString &s);

private:	
    int m_waitTimeout;    // Время ожидания ответа и время подключения к COM-порту
    QMutex mutex;
    QWaitCondition cond;	

    // Настройки COM-порта
    QString m_portName;
    int m_baudrate;
    int m_dataBits;
    int m_parity;
    int m_stopBits;
    int m_flowControl;

    // Ответ из COM-порта
    QByteArray m_request;

    // Флаги состояния
    bool m_isConnect;             // Подключены
    bool m_isDisconnecting;   // Хотим отключится
    bool m_isConnecting;       // Хотим подключится
    bool m_isQuit;                  // Хотим выйти из потока
};

Как видно, в нем мы имеет настройки подключения с COM-порту, которые будут нам передаваться из Model, текущее состояние COM-порта (подключен или нет) и стадия (подключение/отключение).

Переходим к реализации.

Констуктор

ComPortThread::ComPortThread(QObject *parent)
    : QThread(parent),
	  m_waitTimeout(0),
	  m_isQuit(false),
	  m_isConnect(false),
	  m_isDisconnecting(false),
	  m_isConnecting(false)
{
	QMutexLocker locker(&mutex);
}

Здесь я думаю ничего нового для тех, кто когда-нибудь работал с синхронными потоками, кто не знаком с ними, советую обратиться к документации Qt.
Переходим к методу подключения:

connectCom

void ComPortThread::connectCom(QString namePort, int baudRate, int dataBits, int parity, int stopBits, int flowControl)
{
	m_portName = namePort;
	m_baudrate = baudRate;
	m_dataBits = dataBits;
	m_parity = parity;
	m_stopBits = stopBits;
	m_flowControl = flowControl;

	// Если поток не запущен - запускаем его
	if (!isRunning())
	{
		m_isConnecting = true;
		start();
		m_isQuit = false;
	}
	else
	{
		// Если поток запущен, будим его
		cond.wakeOne();
	}
}

Как видно, здесь мы не создаем подключения как такового, здесь мы только проверяем, есть ли у нас рабочий поток, если нет, то создаем новый поток и устанавливаем флаг намерения, что хотим создать подключение.С отключением от COM-порта то же самое, выставляем намерение, что мы хотим отключиться. Вся работа, которая будет производится потоком, будет в методе run(), который мы переопределим.

disconnectCom

void ComPortThread::disconnectCom()
{
	mutex.lock();
	m_isDisconnecting = true;
	mutex.unlock();
	cond.wakeOne();
}

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

Переходим к главному методу, в котором совершается вся полезная работа

run

void ComPortThread::run()
{
	QSerialPort serial;

	// Блокируем поток для инициализации переменных потока
	mutex.lock();
	// Имя текущего COM-порта
	QString currentPortName = m_portName;

	// Время ожидания ответа
	int currentWaitTimeout = m_waitTimeout;

	// Информация, отправляемая в COM-порт
	QByteArray currentRequest = m_request;
	mutex.unlock();	

	while (!m_isQuit)
	{
		// Если было изменение имени COM-порта
		if (m_isConnecting)
		{
			// Устанавливаем имя COM-порта
			serial.setPortName(currentPortName);			

			// Открываем COM-порт
			if (serial.open(QIODevice::ReadWrite))
			{
				// Выставляем настройки
				if ((serial.setBaudRate(m_baudrate)
						&& serial.setDataBits((QSerialPort::DataBits)m_dataBits)
						&& serial.setParity((QSerialPort::Parity)m_parity)
						&& serial.setStopBits((QSerialPort::StopBits)m_stopBits)
						&& serial.setFlowControl((QSerialPort::FlowControl)m_flowControl)))
				{
					m_isConnect = true;
					m_isConnecting = false;
				}
				else
				{
					m_isConnect = false;
					m_isConnecting = false;

					emit error(tr("Can't open %1, error code %2")
							   .arg(m_portName)
							   .arg(serial.error()));
					return;
				}
			}
			else
			{
				m_isConnect = false;
				m_isConnecting = false;

				emit error(tr("Can't open %1, error code %2")
						   .arg(m_portName)
						   .arg(serial.error()));
				return;
			}
		}
		else if (m_isDisconnecting)
		{
			serial.close();
			m_isDisconnecting = false;
			m_request.clear();
			m_isQuit = true;
		}
		else
		{
			// Отправляем в COM-порт команду
			if (!currentRequest.isEmpty())
			{				
				serial.write(currentRequest);

				// Даем время на отправку
				if (serial.waitForBytesWritten(m_waitTimeout))
				{
					// Даем время на получение ответа
					if (serial.waitForReadyRead(currentWaitTimeout))
					{
						// Читаем ответ
						QByteArray responseFromPort = serial.readAll();

						while (serial.waitForReadyRead(10))
						{
							responseFromPort += serial.readAll();
						}						

						// Отправляем сигнал о том, что ответ получен
						emit responseMsg(responseFromPort);
					}
					else
					{						
						// Ошибка по таймауту ожидания ответа
						emit timeout(tr("Wait read response timeout %1")
									 .arg(QTime::currentTime().toString()));
					}
				}
				else
				{					
					// Ошибка по таймауту ожидания передачи запроса
                    emit timeout(tr("Wait write request timeout %1")
                                 .arg(QTime::currentTime().toString()));
				}

				// Очищаем текущую команду
				currentRequest.clear();
			}
			else
			{
				mutex.lock();
				// Засыпаем до следующей отправки
				cond.wait(&mutex);
				currentWaitTimeout = m_waitTimeout;
				currentRequest = m_request;
				mutex.unlock();
			}
		}
	}
}

В первую очередь создаем локальные переменные, в них мы будем заносить информацию, которая может изменится во время работы потока. Далее заходим в бесконечный цикл, в котором будет крутиться наш поток до тех пор, пока мы не установим флаг, что хотим выйти.
Крутясь в потоке мы смотрим флаги и согласно им производим те или иные действия. Этакая машина состояний. То есть, если стоит флаг, означающий, что мы хотим подключиться к COM-порту, мы подключаемся, сбрасываем этот флаг и засыпаем до тех пор, пока не последует другой какой-либо команды. Далее, если приходит команда на отправку сообщения в COM-порт, поток просыпается, берет сообщение, которое необходимо передать, и далее пытается его передать в течении указанного времени. Если передать не удалось, то поток отправляет сигнал, на который может подписаться любой внешний объект и таким образом узнать, что передача не удалась.
Если передача прошла успешно, поток заданное время ждет ответа. Если ответ не пришел, поток выдает сигнал, на который опять же может подписаться любой объект, таким образом мы может узнать, что наша железка не отвечает и разбираться уже с ней.
Если же ответ получен, то поток опять испускает сигнал о том, что данные готовы, их можно забирать и обрабатывать.

Грабли

В общем, на словах звучит довольно легко, но есть нюансы. Дело в том, что наша Model не может принимать сигналы. То есть, пакет нам пришел, но Model об этом не знает. С другой стороны, сигналы может принимать Presenter (так как он унаследован от Qobject), но, Presenter не имеет доступа к потоку, который работает с COM-портом. Есть два варианта решения (может и больше, кто знает, напишите в комментариях), вариант первый, работу с потоком вынести в Presenter. Мне показалось это не очень хорошей идеей, так как тогда придется работу по упаковке/распаковки сообщений выносить тоже в Presenter, то есть часть логики программы будет у нас находится уже не в нашей Model, а в Presenter. Эту идею я откинул. Второй вариант – это сделать класс ComPortThread Singleton’ом. И подписать на его сигналы наш Presenter, а всю обработку вести в Model.Для этого класс ComPortThread необходимо немного переделать:

ComPortThread

class ComPortThread : public QThread
{
	Q_OBJECT

public:
    static ComPortThread* getInstance()
    {
        static QMutex mutex;

        if (!m_instance)
        {
            mutex.lock();

            if (!m_instance)
            {
                m_instance = new ComPortThread;
            }

            m_refCount++;
            mutex.unlock();
        }

        return m_instance;
    }	

	void run();
	void transaction(const QByteArray& request, int waitTimeout);
	void connectCom(QString namePort, int baudRate, int m_dataBits, int m_parity, int m_stopBits, int m_flowControl);
	void disconnectCom();
	bool isConnect();
    void free();

signals:
	void responseMsg(const QByteArray &s);
	void error(const QString &s);
	void timeout(const QString &s);

private:
    ComPortThread(QObject *parent = 0);
    ~ComPortThread();
    ComPortThread(const ComPortThread&);
    ComPortThread& operator=(const ComPortThread&);

private:	
	int m_waitTimeout;
	QMutex mutex;
	QWaitCondition cond;	

	QString m_portName;
	int m_baudrate;
	int m_dataBits;
	int m_parity;
	int m_stopBits;
	int m_flowControl;

	QByteArray m_request;

	bool m_isConnect;
	bool m_isDisconnecting;
	bool m_isConnecting;
	bool m_isQuit;

    static ComPortThread* m_instance;
    static int m_refCount;
};

Прячем конструкторы и деструктор от внешнего доступа, реализуем метод получения ссылки, а в классах ModelComPort и PresenterComPort в констукторы добавляем строчку:

thread = ComPortThread::getInstance();

Не забываем в деструкторы этих классов добавить строки:

if (thread)
{
    thread->free();
    thread = 0;
}

Метод free() объекта thread ведет подсчет ссылок на самого себя, и как только их станет ноль, разрешит свое удаление. Сделано это для защиты от удаления объекта, на который возможны висячие ссылки. Соответственно, во всех классах, где у нас использовался объект ComPortThread меняем объявление объекта на объявление указателя и работаем с потоком уже через указатель. Более подробно можно посмотреть в исходниках.

Ну и наконец собираем все воедино, файл main.cpp

main.cpp

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    CopterGUI w = new CopterGUI;

    PresenterComPort *presenterComPort = new PresenterComPort();
    presenterComPort->appendView(&w);

    w.show();

    return a.exec();
}

Заключение

Ну вот, в общем, и все.

Интересны ваши отзывы, предложения, критика.

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

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

Автор: Genoik

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js