Инструменты пользователя

Инструменты сайта


qt:notes

Qt Notes

Cкачал себе версию Qt с компилятором MinGW (x64) и спустя почти год считаю, что это был не самый удачный выбор. Потому, что:

  • Qt Creator собран с помощью компилятора от Microsoft. В моем случае это MSVC2019 (посмотреть можно в "Help" - "About Qt Creator…"). Если собирать свои кастомные Widget-ты для Qt Designer, то QDesigner не может подключить dll собранную MinGW, соответственно свои виджеты не отображаются. Плагины (dll) надо собирать тем-же компилятором, которым был собран Qt Creator и его запчасть Qt Designer! Пруф - Tutorial, YouTube от Льва Алексеевского. (Следуя за авторам по пути сборки своего виджета, в конце видео мы узнаем, что новый виджет к QDesigner не подключился и в качестве решения предлагается пересобрать весь Qt Creator компилятором MinGW из исходных кодов. В этот момент я захотел поставить компилятор MSVC2019 и решил что это проще… ха)
  • Многие сторонние библиотеки собраны и тестируются именно под MSVC компилятор. Например gRPC, Protobuf. Компилятор MinGW эти библиотеки тоже собирает, но с кучей ворнингов. Windows 10+ x86, x64 Visual Studio 2017+ Officially Supported . Protobuf в итоге правда и с MinGW заработал.

Судя по всему, разработчики библиотек ориентируются в первую очередь на компилятор автора операционной системы, т.е. Microsoft. В общем-то это резонно или же Microsoft этому как-то способствует. Мой выбор MinGW для Qt был обусловлен тем, что:

  • MinGW шел в самой поставке Qt, ставить отдельно его не требовалось.
  • Была надежда, что для сборки тех-же проектов под Linux будет меньше проблем из-за различий в компиляторах, т.к. MinGW это GCC для Windows.
  • Чтобы Qt заработал с MSVC2019 необходимо было поставить Visual Studio 2019, а ставить лишнего не хотелось. (Потом узнал, что можно было поставить пакет Build Tools. Microsoft исторически, то разрешала ставить компилятор без студии, то запрещала, и вот теперь снова можно. Но для установки Build Tools надо скачать инсталлятор, предварительно залогинившись в аккаунте Microsoft. А в 2023 году перейти на скачивание сайт Microsoft не дает, говорит, что ошибка с аккаунтом. И не поймешь, толи санкции, толи ретроградный меркурий… В общем нашел возможность только поставить целиком Visual Studio 2019. Скачать отдельно Build Tools 2019 не получилось. Такая вот исповедь рукожопа…)

Установив 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

QObject, QThread, ...

QSerialPort в отдельном потоке

…лядский порт

Впечатления:

Qt - это событийно ориентированный фреймворк. Большинство взаимодействий между объектами обрабатывается через очередь сообщений (EventLoop), которая своя у каждого потока Qthread. Например, если в одном потоке некий объект-наследник QObject генерит сигнал, то этот сигнал ставится в очереди событий тех потоков, в которых обслуживаются объекты-наследники QObject с подключенными к сигналу слотами.

  • Наследование от QObject необходимо чтобы работали сигналы и слоты!
  • Но при наследовании от 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() позволяет вычитать эти данные.

  • Если не вызывать waitForReadyRead() перед bytesAvailable() и read(), то внутренний буфер не заполняется принятой информацией и кажется что принятых данных нет! Тогда поток сможет реагировать на посылаемые ему сигналы.

При работе с портом оказалось, что я упустил вызов функции waitForBytesWritten(), но связь с портом вполне успешно работала. waitForBytesWritten() ожидалось будет аналогом waitForReadyRead(), но для записи данных. Вероятно она отрабатывает передачу передаваемых данных из внутреннего буфера QSerialPort в драйвер порта. Исходя из того, что отсутствие вызова waitForBytesWritten() не сломало обмен, то это намекает на то, что и запись в драйвер и чтение из драйвера отрабатывается в waitForReadyRead().

Если посмотреть со стороны микроконтроллера, который подключен к COM порту, то оказалось что действительно без вызова waitForReadyRead() нет входящих данных от РС. Если вместо waitForReadyRead() вызывать waitForBytesWritten() то данные от РС поступают в микроконтроллер.

  • Функция waitForReadyRead() обрабатывает и запись, и чтение внутренних буферов с драйвером.
  • Если нужна только запись в порт, то необходимо вызывать waitForBytesWritten(). waitForBytesWritten() не заменяет waitForReadyRead(), т.к. отрабатывает только запись в порт со стороны компьютера.

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

// Функции 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() вызывается непрерывно в потоке чтобы обрабатывать обмен данными. В потоке есть две задержки:

  • QThread::msleep(m_closed_sleep_ms) - задержка на несколько секунд, чтобы проверять не появился ли с в системе нужный COM порт. Проверка наличия порта и его открытие/закрытие происходит в функции reopenTarget().
  • QThread::msleep(m_open_sleep_ms) - задержка на ожидание новых данных вызывается если функции чтения и записи отработали в холостую, т.е. никаких новых данных нет.

Мне показалось разумным вынести функционал обработки потока данных в отдельный класс TargetTransfer, чтобы его уже можно было использовать в потоке любым способом: Перекрытием run() или через moveToThread().

Вариант 1: Перекрытие функции QThread::run()

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;
}

Вариант 2: Worker Object

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 на максимальной скорости, зависящей от скорости выполнения потока. Получились следующий замеры максимальной скорости передачи данных "в попугаях":

  • вариант 1, run(): 420
  • вариант 1, worker: 400

"Попугаи" здесь должны быть "количество байт в миллисекунду", но возможно это не так. Мне важно было убедиться, что трафик через QSerialPort с микроконтроллером работает на максимальной скорости без сбоев. Для изучения работы потоков в Qt требуется гораздо больше времени, которого как всегда нет. Поэтому данные цифры получены только в первом приближении, и вполне возможно тест составлен не достаточно корректно. Но по крайней мере, эти цифры согласуются с ожиданием того, что цикл обработки событий должен отжирать некоторое время у потока.

UPDATE: Скорее всего быстродействие обоих реализаций практически одинаково. При серии запусков оказалось, что каждая реализации дает максимальное быстродействие то 400, то 420 "попугаев". Предположительно это зависит от распределения ресурсов в системе Windows при исполнении высоконагруженного приложения. Иногда цикл выполняется быстрее, иногда чуть медленнее. Возможно это зависит и от того, на каком ядре/ядрах ОС исполняет этот цикл.

Однопоточный QSerialPort

Во многих источниках разработчики QSerialPort не рекомендуют использовать этот объект в отдельном потоке. Наоборот, они рекомендуют использовать его прямо в основном потоке программы. Видимо это потому, что QSerialPort работает на событиях помещаемых в цикл обработки EventLoop. Нет надобности самостоятельно вызывать waitForReadyRead() и waitForBytesWritten(), эти обработчики (или подобные им) ставятся в цикл обработки автоматически и вызываются незаметно внутри основного потока. Разработчику же достаточно вызвать write() для записи, а приходящие данные вычитывать через readAll() по сигналу readyRead().

В случае, когда работа идет из одного потока это удобно. Но при управлении сложным прибором часто требуется многопоточность - одновременно идет исполнение какого-то алгоритма, съем телеметрии, подстройка параметров оператором и прочее. В данном случае, привязка QSerialPort к одному потоку очень мешает, а все навороты с сигналами и обслуживанием через EventLoop, или необходимостью вызовов waitForReadyRead()/waitForBytesWritten() скорее обескураживают.

QRunnable сигналы и слоты

QRunnable - это как бы запуск отдельной задачи в потоке. Задача реализуется в методе QRunnable::run(), что очень похоже на то, как это делается при перекрытии функции run() в потоке QThread. Но задача QRunnable не запускается сама по себе как поток, она добавляется в пул потоков QThreadPool. В этом пуле разработчик указывает сколько может быть запущено потоков QThread для обработки всех поставленных на обработку задач.

Создание потока в операционной системе может быть ресурсоемкой задачей, поэтому резонно на старте приложения создать сразу несколько потоков, а потом лишь ставить им задачи QRunnable на исполнение. При таком подходе не будет тратиться время на создание/уничтожение потоков при запуске каждой задачи.

  • Если требуется запустить задачу (обработчик), которая работает непрерывно все время жизни приложения, то следует использовать QThread. Например - работа с QSerialPort.
  • Если требуется запустить задачу, которая имеет окончание, то рекомендуется запускать ее через QRunnable в QThreadPool. Когда задача заканчивается, например досчитывается какая-то долгая математика, и происходит выход из функции run(), то QThreadPool удаляет данную задачу из стека задач и больше эта задача не выполняется. Таким образом QRunnable имеет конечное время исполнения.
  • Если какую-то задачу можно распараллелить для исполнения на нескольких ядрах процессора, то предлагается использовать QtConcurrent. Наглядный пример - большая картинка разбивается на столбцы, каждый из столбцов обрабатывается одним и тем-же алгоритмом (обработчиком), но исполняется в отдельных потоках на отдельных ядрах, что дает выигрыш в скорости обработки изображения.

Есть возможность сделать так, чтобы QThreadPool не удалял задачу при окончании исполнения QRunnable::run(), а вызывал функцию run() заново. Для этого необходимо вызвать QRunnable::setAutoDelete(false), чтобы отключить авто удаление, которое включено по умолчанию.

При работе с QRunnable есть одна проблема, он не поддерживает слоты и сигналы! И если надо передать что-либо в исполняемую задачу, или получить из нее например статус исполнения, то надо как-то выкручиваться!

Простейшим решением является использование наследника от QObject как поле объекта-наследника QRunnable. Для теста обмена по QSerialPort я написал объект-тестер Tester_TargetTransf наследуемый от QRunnable. В перекрытой функции run() этого объекта в СОМ порт посылаются случайные числа, затем вычитываются и проверяются на соответствие записанным. Если данные не совпадают, что счетчик ошибок увеличивается, и если число ошибок превышает некий максимальный порог, то функция run() прерывается, что приводит к удалению моего объекта Tester_TargetTransf:QRunnable из QThreadPool. Если ошибок нет, то тест выполняется до тех пор пока пользователь его не остановит.

Чтобы организовать взаимодействие с объектом-тестером мне потребовались сигналы:

  • report(QString) - отображать текущую информацию, сколько слов передано и сравнено, количество ошибок и т.д.
  • completed() - передать сигнал, когда тест завершится
  • connected(QString mess) - передать информацию о СОМ порте к которому подключился тест
  • connected(QString mess) - передать информацию о СОМ порте от которого отключился тест

Слот 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;
}

Qt и FTDI

Как считать серийный номер устройства 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

Два способа вставить 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);

QStringList IndexOf CaseInsensitive

    #if CFG_STR_COMPARE_INSENSITIVE
            QRegularExpression re("^" + searchText, QRegularExpression::CaseInsensitiveOption);
            int strInd= strList->indexOf(re);
    #else
            int strInd = strList->indexOf(searchText);
    #endif
  • "^" - означает что текст должен начинаться с "searchText"
  • ".+" - означает что после "searchText" далее может быть любой текст
  • "\" - экранирование спец-символа

Закрыть окно при потере фокуса

При создании окна необходимо передать ему фокус, а в самом окне обработать событие 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() предпочтительнее, но это не точно.

Tagged Widgets (Old)

В 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;
    ...
}

Tagged Widgets (New)

Использовать "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, а сразу указатель на какую-нибудь структуру требующуюся обработчику.

"Getting the most of signal/slot connections in Qt"

Hex Value Show

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);

windeployqt, копирование dll для exe

При динамической линковке приложения, написанного на 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. Тогда:

  1. Создаю директорию BoardCtrl_Exe в которой будет лежать программа со всеми DLL.
  2. Копирую BoardCtrl.exe из директории build (где Qt Creator создает BoardCtrl.exe) в новую директорию BoardCtrl_Exe
  3. открываю командную строку и в перехожу в ней в директорию BoardCtrl_Exe
  4. запускаю в командной строке windeployqt.exe BoardCtrl.exe
  5. готово, windeployqt копирует все необходимое для запуска BoardCtrl.exe в текущую директорию
  > 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 для работы с сетью и т.д.

Update by F5

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
}   

qFloatDistance вместо qFuzzyCompare

#define COMP_EXP        5
#define COMP_PRECISION (1 << COMP_EXP)

bool changedScale = qFloatDistance(m_resc, resc_n) > COMP_PRECISION;

Обработка клавиш курсора, Up-Right-Down-Left

Возникла необходимость управлять моторами подвижки 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;
}

При таком подходе мы будем обрабатывать только первое нажатие на кнопку.

Создание Floating окна

При программном создании дополнительного окна 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.

Побочка

Подозрения, непроверенные, но удручающие:

  • Если окно унаследовано от QDialog, то оно запускается как модальное методом exec() , вместо show() для варианта с наследованием от QWidget. Возможно из-за этого не удалось создать висящее окно над Dialog. (Над компонентом QSpinBox при двойном клике у меня создается слайдер со шкалой значений. Так вот над Dialog слайдера не видно, хотя везде в обычных окнах QWidget слайдер отображается.)
  • При использовании флага QWidget(parent, Qt::Dialog), обнаружилось что форма не автосайзится по высоте. Почему-то окно не становится меньше некоторой высоты. Пришлось авторесайзить вручную:
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()); // масштабируем вручную
}

Terminate приложения в конструкторе MainWindow

Конструктор 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()));
    }
}

Uncheck All RadioButtons

for (int i = 0 ; i < items.count(); ++i) {
    items[i].rButton->setAutoExclusive(false);
    items[i].rButton->setChecked(false);
    items[i].rButton->setAutoExclusive(false);
}

QTextEdit AutoScroll и сохранение в файл

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();
        }
    }
}

ComboBoxDelegate applyValue при переключении активного значения

Когда QComboBox или другой виджет используется в QModel в качестве Editor, то значение Editor применяется при его закрытии. Т.е. на примере QTableModel:

  • при дабле-клике на ячейку в таблице, в ячейке отображается DelegateEditor - QComboBox
  • при переключении списка QComboBox метод setModelData() не вызывается. Apply не происходит!
  • при вводе Enter или при переключении фокуса на другую ячейку, DelegateEditor исчезает при этом вызывается setModelData(), а в ячейке показывается DisplayValue из модели.

Чтобы происходило применение нового значения при переключении 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);
}
qt/notes.txt · Последнее изменение: 2024/04/24 16:35 — vasco