Cкачал себе версию Qt с компилятором MinGW (x64) и спустя почти год считаю, что это был не самый удачный выбор. Потому, что:
Судя по всему, разработчики библиотек ориентируются в первую очередь на компилятор автора операционной системы, т.е. Microsoft. В общем-то это резонно или же Microsoft этому как-то способствует. Мой выбор MinGW для Qt был обусловлен тем, что:
Установив Visual Studio 2019 в менеджере "Qt Manage Kits…" наконец-то появилась возможность собирать проект компилятором MSVC2019.
Но подключить свои виджеты так и не удалось. Даже если взять штатный пример Custom Widget Plugin Example. Судя по подсказкам необходимо еще и чтобы версия фреймворка Qt совпадала с той, с которой собирался Qt Creator. Т.е. если "версия Creator, поставляемая с Qt6.3, построена на Qt6.2.3, то перекомпиляция плагинов с 6.2.3 действительно решает проблему".
Получается, что пересобрать Qt Creator, как предлагается в видео Льва Алексеевского, это единственный правильный путь. Кстати, может быть проблема и не в компиляторе была. Не верится, что dll собранные разными компиляторами настолько не совместимы. По крайней мере, при подсовывании dll собранных разными компиляторами, ошибка в Qt Designer одинаковая - проблема с метаинформацией.
QT_TRANSLATE_NOOP(context, sourceText) / QT_TR_NOOP(sourceText)
static const char *greeting_strings[] = { QT_TRANSLATE_NOOP("FriendlyConversation", "Hello"), QT_TRANSLATE_NOOP("FriendlyConversation", "Goodbye") }; QString FriendlyConversation::greeting(int type) { return tr(greeting_strings[type]); } QString global_greeting(int type) { return qApp->translate("FriendlyConversation", greeting_strings[type]); }
examples - terminal - settingsdialog.cpp
static const char blankString[] = QT_TRANSLATE_NOOP("SettingsDialog", "N/A"); QStringList list; list << (!description.isEmpty() ? description : blankString);
Недостающая статья о многопоточности Qt в C++
QReadWriteLock lock; void ReaderThread::run() { ... lock.lockForRead(); read_file(); lock.unlock(); ... } void WriterThread::run() { ... lock.lockForWrite(); write_file(); lock.unlock(); ... }
QReadWriteLock - This type of lock is useful if you want to allow multiple threads to have simultaneous read-only access, but as soon as one thread wants to write to the resource, all other threads must be blocked until the writing is complete.
EVILEG: Асинхронные API в Qt 6 - QFuture и QPromise
…лядский порт
Впечатления:
Qt - это событийно ориентированный фреймворк. Большинство взаимодействий между объектами обрабатывается через очередь сообщений (EventLoop), которая своя у каждого потока Qthread. Например, если в одном потоке некий объект-наследник QObject генерит сигнал, то этот сигнал ставится в очереди событий тех потоков, в которых обслуживаются объекты-наследники QObject с подключенными к сигналу слотами.
Подобный проброс сигнала через EventLoop между потоками это, Queued Connection соединение между сигналом и слотом. Но объект с сигналом и объект со слотом могут быть и в одном потоке. Более того, это может быть один и тот же объект. При испускании сигнала этот объект ставит в очередь событий некий event, который обработает своим-же слотом когда цикл обработки событий дойдет до этого event.
Отвлекаясь от многопоточности, если оба объекта находятся в одном потоке, то испускание сигнала может привести к непосредственному вызову слота - это Direct Connection соединение сигнала со слотом. Подробнее - Threads and QObjects
Примеры работы с QSerialPort в блокирующем режиме, предлагаемые тут Qt Serial Port Examples работают через наследование от QThread и перекрытии функции Run().
Всё, что исполняется в функции Run(), исполняется в отдельном потоке. Поэтому все переменные, которые будут объявлены внутри этой функции будут аллоцированы в стеке нового потока. Это избавляет от необходимости контролировать в каком потоке находятся те или иные данные. Ведь сама переменная объекта наследника QThread осталась в том потоке, который ее создал, как и сами поля этого объекта.
Но перекрыв функцию Run() поток лишается цикла обработчика событий, т.к. этот EventLoop запускается вызовом exec() внутри Run(). С этого момента объект-потомок QThread не сможет обрабатывать посылаемые ему сигналы, т.к. цикл обработки не крутится, не обрабатывает сигналы и не вызывает для них слоты! Но сам объект все еще может отправлять сигналы в другие потоки, что в примерах используется для передачи событий response/error/timeout для отображения в widget рабочего окна.
Функция exec() - это непрерывный цикл вызовов функции processEvents(). Если exec() не использовать, то можно вручную, в своей реализации run(), периодически вызывать processEvents() для обработки накопившихся событий.
Теперь про сам QSerialPort. Этот объект, судя по всему, посылает сам себе события через очередь сообщений для обработки чтения и записи COM порта. И когда очереди сообщений у нас нет из-за перекрытия Run(), то абсолютно необходимо вызывать напрямую функции waitForReadyRead() и waitForBytesWritten(), как говорит об этом писание. (см. розовые вставки)
Т.е. функции QSerialPort::write() и QSerialPort::read() обмениваются данными с некими внутренними буферами, которые кстати переаллоцируются в случае переполнения. Но необходимо так-же данные из этих буферов писать и читать в само устройство COM порта. Которое на большинстве ОС открывается как файл с некими атрибутами и в этот файл происходит запись и чтение, например Serial-Communication-in-Windows.
При вызове waitForReadyRead(timeout) необходимо указать таймаут на чтение, на это время функция заблокирует исполнение если не будет принято новых данных. Это применимо когда известно сколько данных придет в COM порт в ответ на какой-то запрос. Но не вполне применимо когда данные идут из устройства потоком, или идут эпизодически. Если же ставить некий таймаут, то все это время, пока будет ожидаться пришествие данных, нельзя будет писать в COM порт! Вызываться функции read() и write() должны по очереди в одном потоке. ДА, вызывать функции QSerialPort можно только из того потока в котором он создан или куда был перенесен через moveToThread! Из другого потока функции вызвать можно, но работать они не будут ибо выделено синим:
Note: The serial port is always opened with exclusive access (that is, no other process or thread can access an already opened serial port).
Оказалось, что можно выставить в m_serial→waitForReadyRead(timeout) значение timeout в 0. Тогда эта функция проверяет наличие данных в драйвере, вычитывает их в свой внутренний буфер QSerialPort если они есть, и не висит в таймауте. Тогда функция bytesAvailable() возвращает сколько данных принято во внутреннем буфере, а функция m_serial→read() позволяет вычитать эти данные.
При работе с портом оказалось, что я упустил вызов функции waitForBytesWritten(), но связь с портом вполне успешно работала. waitForBytesWritten() ожидалось будет аналогом waitForReadyRead(), но для записи данных. Вероятно она отрабатывает передачу передаваемых данных из внутреннего буфера QSerialPort в драйвер порта. Исходя из того, что отсутствие вызова waitForBytesWritten() не сломало обмен, то это намекает на то, что и запись в драйвер и чтение из драйвера отрабатывается в waitForReadyRead().
Если посмотреть со стороны микроконтроллера, который подключен к COM порту, то оказалось что действительно без вызова waitForReadyRead() нет входящих данных от РС. Если вместо waitForReadyRead() вызывать waitForBytesWritten() то данные от РС поступают в микроконтроллер.
В итоге, чтобы поток данных передавался в и читался из прибора достаточно вызывать по очереди функции подобные этим:
// Функции write() и read() вызываются из разных потоков, данные буферизируются в FIFO, // реализованные как циклический буфер с мютексом. size_t TargetTransfer::write(void *data, size_t len) { return m_buff_tx.write(data, len); } size_t TargetTransfer::read(CircBuffer::Index &rd_ind, void *data, size_t len, bool &isOver) { return m_buff_rx.read(rd_ind, data, len, isOver); } // Приватные методы writeToTarget()/readFromTarget() пишут/читают эти FIFO в/из QSerialPort. bool TargetTransfer::readFromTarget() { m_serial->waitForReadyRead(0); size_t rx_cnt = m_serial->bytesAvailable(); if (rx_cnt) { CircBuffer::LinMemPtr_wr mem_ptr; bool locked = m_buff_rx.tryLockLinMem_BeforeWrite(mem_ptr); if (locked) { size_t transf_cnt = std::min(rx_cnt, mem_ptr.len); size_t read_cnt = m_serial->read(static_cast<char*>(mem_ptr.data), transf_cnt); m_buff_rx.unlockLinMem_AfterWrite(read_cnt); } } return rx_cnt > 0; } bool TargetTransfer::writeToTarget() { bool do_write = !m_buff_tx.isEmpty(m_buff_tx_ind_rd); if (do_write) { CircBuffer::LinMemPtr_rd mem_ptr; bool locked = m_buff_tx.tryLockLinMem_BeforeRead(m_buff_tx_ind_rd, mem_ptr); if (locked) { size_t written_cnt = m_serial->write(static_cast<char*>(mem_ptr.data), mem_ptr.len); // m_serial->waitForReadyRead(0); - не нужен, если вызывается waitForReadyRead() m_buff_tx.unlockLinMem_AfterRead(m_buff_tx_ind_rd, written_cnt); } } return do_write; } bool TargetTransfer::processCompleted() { if (m_cfg_seq != m_new_seq) reopenTarget(); if (m_is_open) { bool emptyTx = !writeToTarget(); bool emptyRx = !readFromTarget(); if (emptyTx && emptyRx) { QThread::msleep(m_open_sleep_ms); } } else { QThread::msleep(m_closed_sleep_ms); } return m_completed; }
Функция processCompleted() вызывается непрерывно в потоке чтобы обрабатывать обмен данными. В потоке есть две задержки:
Мне показалось разумным вынести функционал обработки потока данных в отдельный класс TargetTransfer, чтобы его уже можно было использовать в потоке любым способом: Перекрытием run() или через moveToThread().
ThreadTargetTransfRun::ThreadTargetTransfRun(TargetTransfer::transf_cfg cfg, QObject *parent) : QThread(parent), m_cfg{cfg} { start(); } ThreadTargetTransfRun::~ThreadTargetTransfRun() { if (m_transf) m_transf->stop(); wait(); } void ThreadTargetTransfRun::run() { m_transf = new TargetTransfer(m_cfg); while (!m_transf->processCompleted()) { delete m_transf; } // Доступ к TargetTransfer.write() и TargetTransfer.read() из прочих потоков. TargetTransfer *ThreadTargetTransfRun::transf() const { return m_transf; }
ThreadTargetTransf::ThreadTargetTransf(TargetTransfer::transf_cfg cfg, QObject *parent) : QObject(parent) { m_transf = new TargetTransfer(cfg); m_transf->moveToThread(&m_thread); connect(&m_thread, &QThread::finished, m_transf, &QObject::deleteLater); connect(this, &ThreadTargetTransf::do_start, m_transf, &TargetTransfer::processThread); m_thread.start(); } ThreadTargetTransf::~ThreadTargetTransf() { if (m_transf) m_transf->stop(); m_thread.quit(); m_thread.wait(); } TargetTransfer *ThreadTargetTransf::transf() const { return m_transf; } void ThreadTargetTransf::start() { emit do_start(); }
В первом варианте, с переопределенной функцией run(), в потоке не запускается цикл обработки сообщений QEventLoop. Во втором варианте QEventLoop запускается и в ответ на получение сигнала на запуск Worker выполняется слот из объекта Worker. В моем случае сигнал do_start() запускает слот TargetTransfer::processThread в потоке m_thread, т.е. запускается цикл обслуживания трансфера через QSerialPort. При этом цикл обработки сообщений внутри потока не прекращается и продолжает работать. Теоретически второй вариант должен работать медленнее, так как небольшая часть времени будет уходить на обработку цикла событий, даже если этот цикл пустой.
Для проверки быстродействия обоих вариантов я убрал обращения к QSerialPort, а вместо этого данные из FIFO_TX копирую сразу в FIFO_RX. Получился Loop режим (или Эхо), без участия СОМ порта. Без работы с СОМ портом данные копируются из одного циклического буфера TX в другой циклический буфет RX на максимальной скорости, зависящей от скорости выполнения потока. Получились следующий замеры максимальной скорости передачи данных "в попугаях":
"Попугаи" здесь должны быть "количество байт в миллисекунду", но возможно это не так. Мне важно было убедиться, что трафик через QSerialPort с микроконтроллером работает на максимальной скорости без сбоев. Для изучения работы потоков в Qt требуется гораздо больше времени, которого как всегда нет. Поэтому данные цифры получены только в первом приближении, и вполне возможно тест составлен не достаточно корректно. Но по крайней мере, эти цифры согласуются с ожиданием того, что цикл обработки событий должен отжирать некоторое время у потока.
Во многих источниках разработчики QSerialPort не рекомендуют использовать этот объект в отдельном потоке. Наоборот, они рекомендуют использовать его прямо в основном потоке программы. Видимо это потому, что QSerialPort работает на событиях помещаемых в цикл обработки EventLoop. Нет надобности самостоятельно вызывать waitForReadyRead() и waitForBytesWritten(), эти обработчики (или подобные им) ставятся в цикл обработки автоматически и вызываются незаметно внутри основного потока. Разработчику же достаточно вызвать write() для записи, а приходящие данные вычитывать через readAll() по сигналу readyRead().
В случае, когда работа идет из одного потока это удобно. Но при управлении сложным прибором часто требуется многопоточность - одновременно идет исполнение какого-то алгоритма, съем телеметрии, подстройка параметров оператором и прочее. В данном случае, привязка QSerialPort к одному потоку очень мешает, а все навороты с сигналами и обслуживанием через EventLoop, или необходимостью вызовов waitForReadyRead()/waitForBytesWritten() скорее обескураживают.
QRunnable - это как бы запуск отдельной задачи в потоке. Задача реализуется в методе QRunnable::run(), что очень похоже на то, как это делается при перекрытии функции run() в потоке QThread. Но задача QRunnable не запускается сама по себе как поток, она добавляется в пул потоков QThreadPool. В этом пуле разработчик указывает сколько может быть запущено потоков QThread для обработки всех поставленных на обработку задач.
Создание потока в операционной системе может быть ресурсоемкой задачей, поэтому резонно на старте приложения создать сразу несколько потоков, а потом лишь ставить им задачи QRunnable на исполнение. При таком подходе не будет тратиться время на создание/уничтожение потоков при запуске каждой задачи.
Есть возможность сделать так, чтобы QThreadPool не удалял задачу при окончании исполнения QRunnable::run(), а вызывал функцию run() заново. Для этого необходимо вызвать QRunnable::setAutoDelete(false), чтобы отключить авто удаление, которое включено по умолчанию.
При работе с QRunnable есть одна проблема, он не поддерживает слоты и сигналы! И если надо передать что-либо в исполняемую задачу, или получить из нее например статус исполнения, то надо как-то выкручиваться!
Простейшим решением является использование наследника от QObject как поле объекта-наследника QRunnable. Для теста обмена по QSerialPort я написал объект-тестер Tester_TargetTransf наследуемый от QRunnable. В перекрытой функции run() этого объекта в СОМ порт посылаются случайные числа, затем вычитываются и проверяются на соответствие записанным. Если данные не совпадают, что счетчик ошибок увеличивается, и если число ошибок превышает некий максимальный порог, то функция run() прерывается, что приводит к удалению моего объекта Tester_TargetTransf:QRunnable из QThreadPool. Если ошибок нет, то тест выполняется до тех пор пока пользователь его не остановит.
Чтобы организовать взаимодействие с объектом-тестером мне потребовались сигналы:
Слот stop() используется чтобы остановить исполнение теста.
Реализуем слот и сигналы в объекте наследнике QObject - Tester_Emitter:
class Tester_Emitter : public QObject { Q_OBJECT public: bool do_stop() const; signals: void report(QString mess); void completed(); void connected(QString mess); void disconnected(QString mess); public slots: void stop(); private: bool m_do_stop; };
Далее создаем в потоковом тестере поле-объект данного класса, m_emitter:
class Tester_TargetTransf : public QRunnable { public: explicit Tester_TargetTransf(TargetTransfer::transf_cfg cfg, const QString &portName, uint buff_len, uint max_err_cnt); void run() override; const Tester_Emitter &emitter() const; private: TargetTransfer::transf_cfg m_cfg; QString m_portName; uint m_buff_len; uint m_max_err_cnt; Tester_Emitter m_emitter; };
Использование объекта-эмиттера с сигналами и слотами примерно следующее:
Tester_TargetTransf::Tester_TargetTransf(TargetTransfer::transf_cfg cfg, const QString &portName, uint buff_len, uint max_err_cnt): m_cfg{cfg}, m_portName{portName}, m_buff_len{buff_len}, m_max_err_cnt{max_err_cnt} { } void Tester_TargetTransf::run() { // Тестируемый объект - Поток с QSerialPort ThreadTargetTransfRun (m_cfg); // Массивы передаваемых/принимаемых данных std::vector<uint8_t> wr_data(m_buff_len); std::vector<uint8_t> rd_data(m_buff_len); std::generate(wr_data.begin(), wr_data.end(), std::rand); // Цикл тестирования while (!m_emitter.do_stop()) { // transf_thread: тестирование write() / read() данных ... // Report if (...) { QString mess = QString("FrameLen: %1, SendCnt: %2, ErrCnt: %3, Rate: %4 ") .arg(frame_len, 4).arg(total_cnt, 10).arg(err_cnt, 4).arg(speed); emit m_emitter.report(mess); } // Exit by Error if (err_cnt >= m_max_err_cnt) { break; } } emit m_emitter.completed(); } const Tester_Emitter &Tester_TargetTransf::emitter() const { return m_emitter; } bool Tester_Emitter::do_stop() const { return m_do_stop; } void Tester_Emitter::stop() { m_do_stop = true; }
Как считать серийный номер устройства FTDI по индексу и открыть его:
DWORD devIndex = 0; //индекс устройства в списке формируемом FT_ListDevices, обычно первое char SerialNumber[64]{0}; m_ftStatus = FT_ListDevices(reinterpret_cast<void *>(devIndex), SerialNumber, FT_LIST_BY_INDEX | FT_OPEN_BY_SERIAL_NUMBER); if (m_ftStatus == FT_OK) { m_cfgSerialNum = QString::fromLocal8Bit(SerialNumber); ... m_ftStatus = FT_OpenEx(static_cast<void *>(m_cfgSerialNum.toLatin1().data()), FT_OPEN_BY_SERIAL_NUMBER, &m_ftHandle); }
При попытке использовать static_cast вместо reinterpret_cast компилятор выдает ошибку. "Habr: Еще раз про приведение типов в языке С++ или расстановка всех точек над cast"
Два способа вставить Vertical Spacer (Пружинку "поджимающую" остальные виджеты в контейнере) в QWidget:
QSpacerItem * verticalSpacer = new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding); ui->frameLeft->layout()->addItem(verticalSpacer);
или
QVBoxLayout *layoutV = qobject_cast<QVBoxLayout *>(ui->frameLeft->layout()); layoutV->addStretch(1);
Здесь frameLeft - это виджет, в которые вставляются "панельки" которые надо поджать пружинкой.
Для вставки новой панельки перед Vertical Spacer, надо использовать метод insertWidget() от QBoxLayout:
BufferControl_frm *frame = new BufferControl_frm(this); QVBoxLayout *layoutV = qobject_cast<QVBoxLayout *>(ui->frameLeft->layout()); layoutV->insertWidget(layoutV->count() - 1, frame);
#if CFG_STR_COMPARE_INSENSITIVE QRegularExpression re("^" + searchText, QRegularExpression::CaseInsensitiveOption); int strInd= strList->indexOf(re); #else int strInd = strList->indexOf(searchText); #endif
При создании окна необходимо передать ему фокус, а в самом окне обработать событие focusOutEvent(QFocusEvent * event). Например в виджете NTDigitalEdit создаем окно слайдера NTDigitalEditSlider:
class NTDigitalEdit : public QFrame { void createSlider(); } void NTDigitalEdit::createSlider() { NT::NTDigitalEditSlider *slider = new NT::NTDigitalEditSlider(...); ... slider->show(); slider->setFocus(); } // Окно слайдера: class NTDigitalEditSlider : public QWidget { ... void focusOutEvent(QFocusEvent * event) override; } void NTDigitalEditSlider::focusOutEvent(QFocusEvent * event) { Q_UNUSED(event); close(); deleteLater(); }
Либо можно не вызывать deleteLater() в обработчике события потери фокуса, а в конструкторе слайдера выставить атрибут - удалять виджет при событии close(): setAttribute(Qt::WA_DeleteOnClose, true). По некоторым рекомендациям на форумах предполагается, что вызов deleteLater() предпочтительнее, но это не точно.
В Delphi у каждого визуального компонента (виджета) есть поле tag, которое разработчик может использовать на свое усмотрение. Подобное можно сделать и в Qt, но самостоятельно.
Это может быть полезно, например, если создается большой набор кнопок с одинаковой логикой обработки. Не разумно писать отдельный слот для каждой кнопки. Можно сохранить индекс кнопки (или ID) в самой кнопке и затем в общем обработчике по индексу узнать для которой кнопки был вызван обработчик.
(В качестве альтернативы, можно в обработчике сравнивать указатели всех кнопок с источником события OQbject::sender(). Но это будет несколько дольше, чем напрямую достать tag из виджета кнопки.)
1 - Делаем необходимых наследников с дополнительным полем tag:
template <class T> class TaggedWidget: public T { public: TaggedWidget(QWidget* parent = 0) : T(parent) {} void setTag(int newTag) { m_tag = newTag; }; int tag() const { return m_tag; } private: int m_tag; }; typedef TaggedWidget<QLineEdit> TagLineEdit; typedef TaggedWidget<QPushButton> TagPushButton; typedef TaggedWidget<QCheckBox> TagCheckBox; ...
2 - Если используется Qt Designer, то необходимые кнопки преобразуем в TagPushButton (Promote to…):
3 - В конструкторе формы назначаем tag и обработчик
// Например есть 5 кнопок TagPushButton ui->btSet1->setTag(0); ui->btSet2->setTag(1); ui->btSet3->setTag(2); ui->btSet4->setTag(3); ui->btSet5->setTag(4); // Назначаем им один обработчик connect(ui->btSet1, &QPushButton::clicked, this, &UI_Form::testApplyClicked); connect(ui->btSet2, &QPushButton::clicked, this, &UI_Form::testApplyClicked); connect(ui->btSet3, &QPushButton::clicked, this, &UI_Form::testApplyClicked); connect(ui->btSet4, &QPushButton::clicked, this, &UI_Form::testApplyClicked); connect(ui->btSet5, &QPushButton::clicked, this, &UI_Form::testApplyClicked);
4 - Используем tag в обработчике:
void UI_Form::testApplyClicked(bool checked) { Q_UNUSED(checked); TagPushButton*bt = static_cast<TagPushButton*>(QObject::sender()); int tag = bt->tag(); // Делаем что-то полезное, например меняем значение в массиве m_enabled[tag] = checked; ... }
Использовать "Promote to" для каждого виджета не удобно, можно сделать проще. Вместо слота можно использовать std::bind - обёртку над callable-объектом (т.е. объектом, который можно вызвать, передав ему необходимое число аргументов)
// h: class UI_FiltersND : public QWidget { Q_OBJECT public: explicit UI_FiltersND(QWidget *parent = nullptr); ~UI_FiltersND(); private: Ui::UI_FiltersND *ui; inline static const int ITEM_CNT = 8; struct FilterItemsUi { QToolButton *btSet; NTDigitalEdit *dePos; QRadioButton *rbSelected; }; struct FilterUi { QLabel *lbActPos; QLabel *lbStatus; FilterItemsUi items[ITEM_CNT]; //... }; // Два набора виджетов, для двух фильтров FilterUi uiF1_; FilterUi uiF2_; } // cpp: void UI_FiltersND::connectFiltersUI() for (int i = 0; i < ITEM_CNT; ++i) { connect(uiF1_.items[i].btSet, &QToolButton::clicked, std::bind(&UI_FiltersND::onSetClicked, this, &uiF1_, i)); connect(uiF2_.items[i].btSet, &QToolButton::clicked, std::bind(&UI_FiltersND::onSetClicked, this, &uiF2_, i)); } } void UI_FiltersND::onSetClicked(FilterUi *uiFx, int itemInd) { if (uiFx == &uiF1_) qDebug() << "F1:" << itemInd; else qDebug() << "F2:" << itemInd; }
При таком подходе можно передать в обработчик не просто идентификатор tag, а сразу указатель на какую-нибудь структуру требующуюся обработчику.
QString("LIM_STATUS: 0x%1").arg(m_lastLimMask, 4, 16, QLatin1Char('0')) QString("LIM_STATUS: 0x%1").arg(m_lastLimMask, 4, 16, QChar('0')) QSettings sett(fileName, QSettings::IniFormat); bool ok; uint16_t vid = sett.value("Vid").toString().toInt(&ok, 16);
При динамической линковке приложения, написанного на Qt, необходима куча dll для запуска его вне среды разработки. Т.е. если просто запустить собранный exe файл, то будут выдаваться сообщения, что не найдена та или иная dll. И пока не скопируешь все необходимые dll рядом с exe файлом, то написанное приложение не запустится.
Можно вручную смотреть в сообщении какой именно библиотеки не хватает, находить ее в директории C:\Qt\6.3.0\mingw_64\bin и копировать рядом к exe файлу. Это долго и муторно. К тому же помимо dll необходимо скопировать некоторые директории с правильной вложенностью. Плюс к этому, с каждой версией Qt меняется набор необходимых DLL и директорий. В общем, руной способ сейчас стал слишком сложным.
К счастью есть утилита windeployqt.exe, которая сама копирует к exe файлу все необходимое для его запуска. Например, у меня есть приложение BoardCtrl.exe собранное в Qt Creator и я хочу запускать его отдельно от IDE. Тогда:
> cd c:/BoardCtrl_Exe > C:\Qt\6.3.0\mingw_64\bin\windeployqt.exe BoardCtrl.exe
Полный путь к windeployqt.exe требуется, если путь к windeployqt.exe не прописан в переменных среды PATH и командная строка ругается что не знает, что такое windeployqt.exe.
Вот сколько всего накопировал windeployqt. (Кроме файла config.ini, этот файл на картинке мой) В зависимости от используемых компонентов меняется и состав необходимых dll. Например, если использовать QML, то потребуются dll для поддержки QML. Если приложение работает с сетью, то потребуются dll для работы с сетью и т.д.
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { QAction* actF5 = new QAction(this); actF5 ->setShortcut(Qt::Key_F5); connect(actF5 , SIGNAL(triggered()), this, SLOT(updatebyF5())); this->addAction(actF5); } void MainWindow::updatebyF5() { // Update something }
или
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { QShortcut * shortcutF5 = new QShortcut(QKeySequence(Qt::Key_F5),this,SLOT(updatebyF5())); shortcutF5->setAutoRepeat(false); } void MainWindow::updatebyF5() { // Update something }
#define COMP_EXP 5 #define COMP_PRECISION (1 << COMP_EXP) bool changedScale = qFloatDistance(m_resc, resc_n) > COMP_PRECISION;
Возникла необходимость управлять моторами подвижки XY с помощью клавиш курсора. Т.е. пока клавиша нажата мотор едет, при отпускании - останавливается. Для этого необходимо обрабатывать сообщения о нажатии и об отпускании клавиши.
По умолчанию в Qt клавиши курсора работают аналогично клавише Tag, т.е. перемещают фокус ввода между компонентами расположенными в окне. Если окно пустое, то обрабатывать нажатие кнопок можно напрямую в перекрытых функциях QWidget:
class UI_CursorSM : public QWidget { Q_OBJECT protected: bool keyPressEvent(QKeyEvent *event) override; bool keyReleaseEvent(QKeyEvent *event) override; };
Но чаще всего окно уже содержит несколько компонентов, поэтому сообщения от клавиш курсора не буду доходить до этих функций окна. Т.к. обработчик перемещения фокуса обработает сообщение QKeyEvent раньше и пометит его как обработанное. Поэтому дальнейшим потребителям сообщение доставлено не будет.
В качестве решения можно повесить фильтр на перехват событий, но сделать это надо на само приложение, а не на текущее окно:
class UI_CursorSM : public QWidget { Q_OBJECT private: bool eventFilter(QObject *obj, QEvent *e); } UI_CursorSM::UI_CursorSM(QWidget *parent) : QWidget(parent, Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint), ui(new Ui::UI_CursorSM) { qApp->installEventFilter(this); } UI_CursorSM::~UI_CursorSM() { qApp->removeEventFilter(this); delete ui; } bool UI_CursorSM::eventFilter(QObject *obj, QEvent *e) { if(qApp->activeWindow() == this) { if (e->type() == QEvent::KeyPress) { QKeyEvent *event= dynamic_cast<QKeyEvent*>(e); if (!keyPressEvent_my(event)) { return qApp->eventFilter(obj, e); } return true; } else if (e->type() == QEvent::KeyRelease) { QKeyEvent *event= dynamic_cast<QKeyEvent*>(e); if (!keyReleaseEvent_my(event)) { return qApp->eventFilter(obj, e); } return true; } } return qApp->eventFilter(obj, e); }
При установке фильтра на текущее окно, события по прежнему будут уходить в обработчик Tab.
В обработчиках событий необходимо проверять автоповтор нажатия кнопки event→isAutoRepeat(). Ведь если любую клавишу держать нажатой, то начинается автоповтор ввода символа, и сообщения о нажатии / отпускании клавиши непрерывно повторяются.
bool UI_CursorSM::keyPressEvent_my(QKeyEvent *event) { switch (event->key()) { case Qt::Key_Up: if (!event->isAutoRepeat()) moveUp(); break; case Qt::Key_Down: if (!event->isAutoRepeat()) moveDown(); break; case Qt::Key_Left: if (!event->isAutoRepeat()) moveLeft(); break; case Qt::Key_Right: if (!event->isAutoRepeat()) moveRight(); break; case Qt::Key_Escape: case Qt::Key_Space: if (!event->isAutoRepeat()) stop(); break; default: return false; } return true; } bool UI_CursorSM::keyReleaseEvent_my(QKeyEvent *event) { switch (event->key()) { case Qt::Key_Up: if (!event->isAutoRepeat()) stop(); break; case Qt::Key_Down: if (!event->isAutoRepeat()) stop(); break; case Qt::Key_Left: if (!event->isAutoRepeat()) stop(); break; case Qt::Key_Right: if (!event->isAutoRepeat()) stop(); break; default: return false; } return true; }
При таком подходе мы будем обрабатывать только первое нажатие на кнопку.
При программном создании дополнительного окна QWidget необходимо оставить в конструкторе parent = nullptr чтобы окно было висящим отдельно от основного окна. Но тогда при закрытии основного окна приложение не закрывается, а остается ждать закрытия всех висящих окон. Это не удобно.
Чтобы закрытие основного окна уничтожало все остальные окна, необходимо в конструкторах этих окон указывать в parent-ом основное окно. Но тогда эти окна отображаются на поверхности главного окна, а не создаются висящими.
Чтобы это исправить необходимо в конструктор QWidget передать флаг Qt::Dialog. Тогда окно будет иметь parent-ом главное окно и закрываться вместе с ним, а так-же отображаться как висящее отдельно от основного окна.
UI_CursorSM::UI_CursorSM(QWidget *parent) : QWidget(parent, Qt::Dialog), ui(new Ui::UI_CursorSM) { }
Это решение актуально тогда, когда ранее созданные однооконные приложения с QWidget необходимо интегрировать в общее большое приложение. Если же окно создается сразу как часть большого приложения, то удобнее дополнительные окна сразу создавать на основе QDialog, а не QWidget.
Если висящее окно имеет фиксированный дизайн, то необходимо отключить его "растягивание" мышкой. Для этого дополнительно в конструкторе передается флаг Qt::MSWindowsFixedSizeDialogHint.
Подозрения, непроверенные, но удручающие:
UI_KeysSM::UI_KeysSM(SM_Motor &sm1, SM_Motor* sm2, QWidget *parent) : QWidget(parent, Qt::Dialog), ui(new Ui::UI_KeysSM) , sm_{&sm1, sm2} { ui->setupUi(this); ... resize(width(), minimumSizeHint().height()); // масштабируем вручную }
Конструктор MainWindow выполняется до запуска цикла обработки сообщений (EventLoop).
// main.cpp int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec(); // start EventLoop }
Иногда возникает необходимость закрыть приложение, если какое-то критическое условие не выполняется. Например, если нет какой-либо необходимой библиотеки DLL. Если сделать это в конструкторе MainWindow, то вызов qApp→exec() для закрытия не сработает, т.к. EventLooop еще не запущен.
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); // can't call qApp->quit() while connectToDll // because thread loop not started yet: a.exec() if (!connectToDll()) { QMessageBox::critical(0, tr("Critical Error"), tr("No DLL connected"); qApp->exec(); // IT DOESN'T WORK! } }
В качестве решения, можно проверить подключение к DLL перед запуском EventLoop в функции main().
int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; if (w.connectToDll()) { w.show(); return a.exec(); } }
Либо можно вызвать слот quit() через некоторую задержку, достаточную чтобы цикл сообщений запустился:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); // can't call qApp->quit() while connectToDll // because thread loop not started yet: a.exec() if (!connectToDll()) { QMessageBox::critical(0, tr("Critical Error"), tr("No DLL connected"); QTimer::singleShot(250, qApp, SLOT(quit())); } }
for (int i = 0 ; i < items.count(); ++i) { items[i].rButton->setAutoExclusive(false); items[i].rButton->setChecked(false); items[i].rButton->setAutoExclusive(false); }
void UI_Logger::showMess(QString mess) { ui->edText->append(mess); if (scrollEna_) { QScrollBar *sb = ui->edText->verticalScrollBar(); sb->setValue(sb->maximum()); } } void UI_Logger::saveLog() { QString nomeFile = QFileDialog::getSaveFileName(this, tr("Save Log to File"), "", tr("Text (*.txt)")); if (!nomeFile.isEmpty()) { QFile file(nomeFile); if (file.open(QIODevice::ReadWrite)) { QTextStream stream(&file); stream << ui->edText->toPlainText(); file.flush(); file.close(); } } }
Когда QComboBox или другой виджет используется в QModel в качестве Editor, то значение Editor применяется при его закрытии. Т.е. на примере QTableModel:
Чтобы происходило применение нового значения при переключении QComboBox, надо эмитировать сигнал commitData().
ComboBoxDelegate::ComboBoxDelegate(const QStringList &strItems, QWidget *parent) : QStyledItemDelegate(parent) , m_strItems{strItems} { } QWidget *ComboBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { Q_UNUSED(option); Q_UNUSED(index); QComboBox *editor = new QComboBox(parent); editor->addItems(m_strItems); connect(editor, &QComboBox::currentIndexChanged, this, &ComboBoxDelegate::applyValue); return editor; } void ComboBoxDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { QVariant value = index.model()->data(index, Qt::EditRole); QComboBox *cmbox = static_cast<QComboBox*>(editor); cmbox->setCurrentText(value.toString()); } void ComboBoxDelegate::applyValue(int itmInd) { Q_UNUSED(itmInd); QComboBox *editor = qobject_cast<QComboBox *>(sender()); emit commitData(editor); //emit closeEditor(editor); - если надо автоматически закрыть Editor } void ComboBoxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { QComboBox *cmbox = static_cast<QComboBox*>(editor); int value = cmbox->currentIndex(); if (value >= 0) { model->setData(index, cmbox->currentText(), Qt::EditRole); } } void ComboBoxDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const { Q_UNUSED(index); editor->setGeometry(option.rect); }