© EVILEG 2015-2018
Рекомендует хостинг
TIMEWEB

Блокирующий последовательный порт. QSerialPort + QThread.

Qt, QSerialPort, QThread

Мне представилась задача написать ПО для управления излучателем рентгена. А именно: реализовать протокол передачи данных между ПК и излучателем рентгена и создать пользовательские функции "Установить параметры", "Включить рентген", "Выключить рентген".

Управление излучателем рентгена осуществляется благодаря передаче данных по последовательному порту, который еще называют COM-портом, но применимо это только в отношении ОС Windows.

Qt имеет класс QSerialPort, который предоставляет функции для доступа к последовательному порту.

Хочу продемонстрировать вам свою реализацию данной задачи.

Структура проекта


Emitter.pro - профайл проекта

Headers:

  • emitter.h - заголовочный файл класса излучателя.
  • mainwindow.h - заголовочный файл основного окна программы.

Sources:

  • emitter.cpp - файл реализации класса излучателя.
  • main.cpp
  • mainwindow.cpp - файл реализации основного окна программы.

Forms:

  • mainwindow.ui - файл формы основного окна программы

Класс излучателя

В профайл проекта добавляем serialport для возможности использовать модуль QtSerialPort.

QT       += core gui serialport

emitter.hpp

#ifndef EMITTER_H
#define EMITTER_H

#include <QSerialPort>
#include <QTimer>

class Emitter : public QObject
{
    Q_OBJECT
public:
    explicit Emitter(QObject *parent = 0);
    // Функция проверки соединения.
    bool isConnected() const;

public slots:
    // Слот установки параметров.
    void setFeatures(int voltage, int current, int workTime, int coolTime);
    // Слот включения рентгена.
    void turnOnXRay();
    // Слот выключения рентгена.
    void turnOffXRay();

private:
    enum class Command: quint8; // Объявление перечисления
    // Функция подключения.
    void connectToEmitter();
    // Важнейшая функция, реализующая протокол обмена с излучателем.
    QByteArray writeAndRead(Command com, quint8 y1 = 0, quint8 y2 = 0, quint8 *m = nullptr);
    // Команда излучателя: Статус излучателя.
    bool commandS();
    // Команда излучателя: Установка параметров.
    void commandP(quint16 volt, quint16 curr, quint16 workTime, quint16 coolTime);
    // Команда излучателя: Включить излучатель.
    void commandN();
    // Команда излучателя: Выключить излучатель.
    void commandF();

    const quint8 STARTBYTE = 0x40;  // Начало посылки.
    const quint8 DEV = 0;  // ID - излучателя.

    // Доступные нам команды, отправляемые излучателю. Отправляется третьим байтом в посылке.
    enum class Command : quint8
    {
        S = 0x53, // Команда запрос статуса.
        P = 0x50, // Команда установки параметров.
        N = 0x4e, // Команда включения рентгена.
        F = 0x46  // Команда выключения рентгена.
    };

    // Сообщения излучателя.
    enum class MessageCommandS : quint8
    {
        OK = 0x00, // Все ок.
        XRAY_ON = 0x01, // Излучение включено.
        XRAY_STARTING = 0x02, // Выход излучателя на режим.
        XRAY_TRAIN = 0x03, // Идет тренировка.
        COOLING = 0x04 // Охлаждение.
    };

    // Ошибки излучателя.
    enum class ErrorCommandS : quint8
    {
        NO_ERROR = 0x00, // Нет ошибок.
        XRAY_CONTROL_ERROR = 0x01, // Ошибка управления рентгеновской трубкой.
        MODE_ERROR = 0x02, // Ошибка установки заданного режима.
        VOLTAGE_ERROR = 0x03, // Превышение порога напряжения.
        CURRENT_ERROR = 0x04, // Превышение порога по току.
        PROTECTIVE_BOX_ERROR = 0x05, // Защитный бокс открыт.
        LOW_SUPPLY_VOLTAGE = 0x06, // Низкое питающее напряжение.
        DISCONNECTION = 0x07, // Отсутствие подтверждения соединения (более 1 с)ю
        OVERHEAT = 0x08 // Перегрев.
    };

    QSerialPort *m_pSerialPort;
    bool m_isConnected;

    QTimer *m_pTimerCheckConnection;
};

#endif // EMITTER_H

"Общение" с излучателем происходит по определенному протоколу.

Краткое описание протокола излучателя :

  1. Ведущим является компьютер. Он всегда первым посылает команду. При приеме каждой посылки, микропроцессор аппарата должен послать назад соответствующую команду ответа.
  2. Каждая посылка выглядит следующим образом:
    @ dev com y1 y2 m1 ... mn CRC
    @ - стартовый байт, признак начала передачи (всегда равен 0x40).
    dev - ID устройства. ID - контроллера и компьютера должны отличаться.
    com - команда.
    y1 - младший байт слова, определяющего число посылаемых байт данных.
    y2 - старший байт слова. определяющего число посылаемых байт данных.
    m1 - первый байт данных.
    mn - последний байт данных.
    CRC - байт контрольной суммы. Контрольная сумма считается как сумма всех байт, начиная с первого байта команды и заканчивая последним байтом данных.

Работа с последовательным портом будет в синхронном режиме, т.е. используя функции write и waitForBytesWritten для записи и waitForReadyRead и readAll для чтения.

emitter.cpp

#include "emitter.h"

#include <QDebug>
#include <QThread>

Emitter::Emitter(QObject *parent) : QObject(parent)
{
    // Инициализация последовательного порта.
    m_pSerialPort = new QSerialPort(this);
    m_pSerialPort->setPortName("COM3");
    // Скорость передачи данных. 19200 бит/с.
    m_pSerialPort->setBaudRate(QSerialPort::Baud19200);
    m_pSerialPort->setDataBits(QSerialPort::Data8);
    m_pSerialPort->setParity(QSerialPort::NoParity);
    m_pSerialPort->setStopBits(QSerialPort::OneStop);
    m_pSerialPort->setFlowControl(QSerialPort::NoFlowControl);

    /* В данном излучателе при включенном рентгене нужно обязательно, хотя бы раз в секунду,
    посылать команду на запрос статуса, чтобы подтвердить, что соединение не разорвано.
    Иначе рентген будет выключен. Для этого создаем таймер с интервалом в 1 секунду. */
    m_pTimerCheckConnection = new QTimer(this);
    m_pTimerCheckConnection->setInterval(1000);

    /* По истечении времени 1 с вызывается команда запроса статуса.
    Здесь используется именно лямбда-функция, чтобы не создавать слот.
    Можно было бы сделать commandS private slot, но в этом случае при connect
    через старую форму записи (на макросах SIGNAL, SLOT)
    этот слот был бы доступен внешним объектам. */
    connect(m_pTimerCheckConnection, &QTimer::timeout, [this](){
        commandS();
    });

    // Подключаем последовательный порт.
    connectToEmitter();
}

bool Emitter::isConnected() const
{
    return m_isConnected;
}

void Emitter::setFeatures(int voltage, int current, int workTime, int coolTime)
{
    if (isConnected())
    {
        commandP(voltage, current, workTime, coolTime);
    }
}

void Emitter::turnOnXRay()
{
    if (isConnected())
    {
        commandN();
    }
}

void Emitter::turnOffXRay()
{
    if (isConnected())
    {
        commandF();
    }
}

void Emitter::connectToEmitter()
{
    if (m_pSerialPort->open(QSerialPort::ReadWrite))
    {
        // Убеждаемся, что в последовательный порт подключен именно излучатель рентгена.
        m_isConnected = commandS();
        if (m_isConnected)
        {
            qDebug() << "Излучатель рентгена подключен.";
        }
        else
        {
            qDebug() << "В последовательный порт излучателя рентгена подключено другое устройство!";
        }
    }
    else
    {
        qDebug() << "Последовательный порт не подключен.";
        m_isConnected = false;
    }
}

QByteArray Emitter::writeAndRead(Command com, quint8 y1, quint8 y2, quint8 *m)
{
    quint16 y = y2 * 256 + y1;

    // Данные, посылаемые в последовательный порт.
    QByteArray sentData;
    sentData.resize(6 + y);
    sentData[0] = STARTBYTE;
    sentData[1] = DEV;

    // Контрольная сумма = сумма всех байтов, начиная с байта команды.
    quint8 crc = 0;
    crc += sentData[2] = static_cast<quint8>(com);
    crc += sentData[3] = y1;
    crc += sentData[4] = y2;

    // Если есть байты данных, то их тоже включаем в контрольную сумму.
    if (y && m)
    {
        for (int i = 0; i < y; ++i)
        {
            crc += sentData[5 + i] = m[i];
        }
    }

    // Последний байт и есть контрольная сумма.
    sentData[5 + y] = crc;

    // Записываем в последовательный порт и ждем до 100 мс, пока запись не будет произведена.
    m_pSerialPort->write(sentData);
    m_pSerialPort->waitForBytesWritten(100);

    // Засыпаем, ожидая, пока микроконтроллер излучателя обработает данные и ответит.
    this->thread()->msleep(50);
    // Читаем данные от излучателя.
    m_pSerialPort->waitForReadyRead(50);
    return m_pSerialPort->readAll();
}

bool Emitter::commandS() // Команда запрос статуса.
{
    // Отправляем излучателю.
    QByteArray receivedData = writeAndRead(Command::S);

    /* Должно придти именно 8 байт.
    Ответ должен быть таким: @ dev com 2 0 s e CRC.
    Иначе ответ пришел не от излучателя. */
    if (receivedData.size() == 8)
    {
        quint8 crc = 0;
        for (int i = 1; i < receivedData.size() - 1; ++i)
        {
            crc += receivedData[i];
        }
        if (crc != static_cast<quint8>(receivedData[receivedData.size() - 1]))
            qDebug() << "CRC не совпадает!" << crc << static_cast<quint8>(receivedData[receivedData.size() - 1]);

        // Если условие верно, то нам ответил наш излучатель рентгена.
        if (receivedData[0] == STARTBYTE &&
            static_cast<Command>(receivedData.at(2)) == Command::S &&
            receivedData[3] == 2 &&
            receivedData[4] == 0)
        {
            MessageCommandS message = static_cast<MessageCommandS>(receivedData.at(5));
            switch (message)
            {
            case MessageCommandS::OK:
                qDebug() << "Все в порядке.";
                break;
            case MessageCommandS::XRAY_ON:
                qDebug() << "Излучение включено.";
                break;
            case MessageCommandS::XRAY_STARTING:
                qDebug() << "Выход излучения на режим.";
                break;
            case MessageCommandS::XRAY_TRAIN:
                qDebug() << "Идет тренировка.";
                break;
            case MessageCommandS::COOLING:
                qDebug() << "Охлаждение.";
                break;
            }
            ErrorCommandS error = static_cast<ErrorCommandS>(receivedData.at(6));
            switch (error)
            {
            case ErrorCommandS::NO_ERROR:
                qDebug() << "Нет ошибок";
                break;
            case ErrorCommandS::XRAY_CONTROL_ERROR:
                qDebug() << "Ошибка управления рентгеновской трубкой.";
                break;
            case ErrorCommandS::MODE_ERROR:
                qDebug() << "Невозможно установить заданный режим на трубке.";
                break;
            case ErrorCommandS::VOLTAGE_ERROR:
                qDebug() << "Превышен порог напряжения.";
                break;
            case ErrorCommandS::CURRENT_ERROR:
                qDebug() << "Превышен порог по току.";
                break;
            case ErrorCommandS::PROTECTIVE_BOX_ERROR:
                qDebug() << "Защитный бокс открыт.";
                break;
            case ErrorCommandS::LOW_SUPPLY_VOLTAGE:
                qDebug() << "Низкое питающее напряжение.";
                break;
            case ErrorCommandS::DISCONNECTION:
                qDebug() << "Отсутствие связи с хостом (более 1с).";
                break;
            case ErrorCommandS::OVERHEAT:
                qDebug() << "Перегрев.";
                break;
            }
            // Возвращаем true, т.к. ответ пришел от излучателя. 
            return true;
        }
    }
    else
    {
        qDebug() << "Ошибка. Ответ не соответствует ожиданиям.";
    }
    return false;
}

// Команда установить параметры.
void Emitter::commandP(quint16 volt, quint16 curr, quint16 workTime, quint16 coolTime)
{
    quint8 m[8];

    // 8 байт данных, 4 слова. Первый младший байт слова, второй старший байт слова.
    m[0] = volt - (volt/256) * 256;
    m[1] = volt/256;
    m[2] = curr - (curr/256) * 256;
    m[3] = curr/256;
    m[4] = workTime - (workTime/256) * 256;
    m[5] = workTime/256;
    m[6] = coolTime - (coolTime/256) * 256;
    m[7] = coolTime/256;
    QByteArray receivedData = writeAndRead(Command::P, 8, 0, m);

    /* Должно придти именно 14 байт.
    Ответ должен быть таким: @ dev com 8 0 volt curr workTime coolTime CRC.
    volt, curr, workTime, coolTime - слова в 2 байта.
    Иначе ответ пришел не от излучателя. */
    if (receivedData.size() == 14)
    {
        quint8 crc = 0;
        for (int i = 1; i < receivedData.size() - 1; ++i)
        {
            crc += receivedData[i];
        }
        if (crc != static_cast<quint8>(receivedData[receivedData.size() - 1]))
            qDebug() << "CRC не совпадает!" << crc << static_cast<quint8>(receivedData[receivedData.size() - 1]);

        // Ответом от излучателя являются текущие значения параметров.
        if (receivedData[0] == STARTBYTE &&
            static_cast<Command>(receivedData.at(2)) == Command::P &&
            receivedData[3] == 8 &&
            receivedData[4] == 0)
        {
            quint16 receivedVolt = receivedData[5] + receivedData[6] * 256;
            quint16 receivedCurr = receivedData[7] + receivedData[8] * 256;
            quint16 receivedWorkTime = receivedData[9] + receivedData[10] * 256;
            quint16 receivedCoolTime = receivedData[11] + receivedData[12] * 256;
            qDebug() << "Данные : " << receivedVolt << receivedCurr << receivedWorkTime << receivedCoolTime;
        }
    }
    else
    {
        qDebug() << "Ошибка. Ответ не соответствует ожиданиям.";
    }
}

// Команда выключения рентгена.
void Emitter::commandN()
{
    QByteArray receivedData = writeAndRead(Command::N);

    /* Должно придти именно 6 байт.
    Ответ должен быть таким: @ dev com 0 0 CRC.
    Иначе ответ пришел не от излучателя. */
    if (receivedData.size() == 6)
    {
        quint8 crc = 0;
        for (int i = 1; i < receivedData.size() - 1; ++i)
        {
            crc += receivedData[i];
        }
        if (crc != static_cast<quint8>(receivedData[receivedData.size() - 1]))
            qDebug() << "CRC не совпадает!" << crc << static_cast<quint8>(receivedData[receivedData.size() - 1]);

        // Включаем таймер подверждения соединения, если рентген включен.
        if (receivedData[0] == STARTBYTE &&
            static_cast<Command>(receivedData.at(2)) == Command::N &&
            receivedData[3] == 0 &&
            receivedData[4] == 0)
        {
            qDebug() << "Рентген включен.";
            m_pTimerCheckConnection->start();
        }
    }
    else
    {
        qDebug() << "Ошибка. Ответ не соответствует ожиданиям.";
    }
}

void Emitter::commandF() //Выключить рентген.
{
    QByteArray receivedData = writeAndRead(Command::F);

    /* Должно придти именно 6 байт.
    Ответ должен быть таким: @ dev com 0 0 CRC.
    Иначе ответ пришел не от излучателя. */
    if (receivedData.size() == 6)
    {
        quint8 crc = 0;
        for (int i = 1; i < receivedData.size() - 1; ++i)
        {
            crc += receivedData[i];
        }
        if (crc != static_cast<quint8>(receivedData[receivedData.size() - 1]))
            qDebug() << "CRC не совпадает!" << crc << static_cast<quint8>(receivedData[receivedData.size() - 1]);

        // Выключаем таймер подтверждения соединения.
        if (receivedData[0] == STARTBYTE &&
            static_cast<Command>(receivedData.at(2)) == Command::F &&
            receivedData[3] == 0 &&
            receivedData[4] == 0)
        {
            qDebug() << "Рентген выключен.";
            m_pTimerCheckConnection->stop();
        }
    }
    else
    {
        qDebug() << "Ошибка. Ответ не соответствует ожиданиям.";
    }
}

mainwindow.h

Здесь объявляется один сигнал для передачи параметров, и создается пара объект-излучатель - объект-поток.

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QThread>

#include "emitter.h"

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

signals:
    // Этот сигнал будет высылаться при нажатии на кнопку "Задать", чтобы передать параметры в излучатель.
    void setFeaturesRequested(int voltage, int current, int workTime, int coolTime);

private:
    Ui::MainWindow *ui;

    // Излучатель и отдельный поток, в котором он будет работать.
    QThread *m_pThread;
    Emitter *m_pEmitter;
};

#endif // MAINWINDOW_H

mainwindow.ui

Примитивный дизайн. Но лучшего для демонстрации работы излучателя и не требуется.
Виджеты spinBox, spinBox_2, spinBox_3, spinBox_4 переименованы в spinBoxVoltage, spinBoxCurrent, spinBoxWorkTime, spinBoxCoolTime соответственно. Кнопки pushButton, pushButton_2, pushButton_3 переименованы в pushButtonSet, pushButtonTurnOn, pushButtonTurnOff соответственно. Сделано это для удобства обращения к данным объектам в файле реализации mainwindow.cpp.


mainwindow.cpp

Здесь объект излучателя перемещается в отдельный поток. Также описана работа пользовательского интерфейса.

#include "mainwindow.h"
#include "ui_mainwindow.h"

#include <QMessageBox>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    m_pThread = new QThread(this);
    // Указывать родителя нет необходимости. Родителем станет поток, когда переместим в него наш объект излучателя.
    m_pEmitter = new Emitter;
    
    /* Перемещаем объект излучателя в отдельный поток, чтобы синхронные ожидающие операции не блокировали
    основной GUI-поток. Создаем соединение: Удаляем объект излучателя при окончании работы потока.
    Запускаем поток.*/
    m_pEmitter->moveToThread(m_pThread);
    connect(m_pThread, SIGNAL(finished()), m_pEmitter, SLOT(deleteLater()));
    m_pThread->start();
    
    // Проверка соединения.
    if (m_pEmitter->isConnected())
    {
        ui->pushButtonTurnOff->setEnabled(false);
    }
    else
    {
        ui->pushButtonSet->setEnabled(false);
        ui->pushButtonTurnOn->setEnabled(false);
        ui->pushButtonTurnOff->setEnabled(false);
        QMessageBox::critical(this, "Ошибка подключения", "Подключите излучатель рентгена в последовательный порт COM3"
                              " и перезапустите программу.", QMessageBox::Ok);
    }

    // При нажатии на кнопку "Включить" включать рентген и переключать состояние кнопок.
    connect(ui->pushButtonTurnOn, &QPushButton::clicked, m_pEmitter, &Emitter::turnOnXRay);
    connect(ui->pushButtonTurnOn, &QPushButton::clicked, [this](){
        ui->pushButtonTurnOn->setEnabled(false);
        ui->pushButtonTurnOff->setEnabled(true);
    });
    
    // При нажатии на кнопку "Выключить" выключать рентген и переключать состояние кнопок.
    connect(ui->pushButtonTurnOff, &QPushButton::clicked, m_pEmitter, &Emitter::turnOffXRay);
    connect(ui->pushButtonTurnOff, &QPushButton::clicked, [this](){
        ui->pushButtonTurnOn->setEnabled(true);
        ui->pushButtonTurnOff->setEnabled(false);
    });

    // При нажатии на кнопку "Задать", передаются все соответствующие параметры выставленные в spinBox'ах.
    connect(ui->pushButtonSet, &QPushButton::clicked, [this](){
        emit setFeaturesRequested(ui->spinBoxVoltage->value(),
                                  ui->spinBoxCurrent->value(),
                                  ui->spinBoxWorkTime->value(),
                                  ui->spinBoxCoolTime->value());
    });
    connect(this, &MainWindow::setFeaturesRequested, m_pEmitter, &Emitter::setFeatures);
}

MainWindow::~MainWindow()
{
    // Перед удалением основного окна, дожидаемся завершения работы потока.
    m_pThread->quit();
    m_pThread->wait(1000);

    delete ui;
}

Итог

Как результат: Создан класс излучателя, где соблюдена инкапсуляция. Есть 4 публичных функции, 3 из которых слоты. Есть внутренние функции излучателя, работа которых реализована в соответствие с описанным синхронным протоколом. Показано использование объекта излучателя в паре с объектом потока, что дает возможность не блокировать основной поток даже если операции чтения-записи требуют задержек.

    Комментарии

    3 мая 2018 г. 10:49

    Nice article, in my opinion it's much clearer than the official example at http://doc.qt.io/qt-5/qtserialport-blockingmaster-example.html.
    I have the following question: what would be the most elegant way to programmatically set the port name, given that the application may offer a QComboBox that lists every serial port reachable by the host device? How would you do it?

    3 мая 2018 г. 15:19

    Thanks for reply. This is not hard to do. This described in "Terminal" example by Qt http://doc.qt.io/qt-5/qtserialport-terminal-example.html ,  in SettingsDialog http://doc.qt.io/qt-5/qtserialport-terminal-settingsdialog-cpp.html

    void SettingsDialog::fillPortsInfo()
    {
        m_ui->serialPortInfoListBox->clear();
        QString description;
        QString manufacturer;
        QString serialNumber;
        const auto infos = QSerialPortInfo::availablePorts();
        for (const QSerialPortInfo &info : infos) {
            QStringList list;
            description = info.description();
            manufacturer = info.manufacturer();
            serialNumber = info.serialNumber();
            list << info.portName()
                 << (!description.isEmpty() ? description : blankString)
                 << (!manufacturer.isEmpty() ? manufacturer : blankString)
                 << (!serialNumber.isEmpty() ? serialNumber : blankString)
                 << info.systemLocation()
                 << (info.vendorIdentifier() ? QString::number(info.vendorIdentifier(), 16) : blankString)
                 << (info.productIdentifier() ? QString::number(info.productIdentifier(), 16) : blankString);
    
            m_ui->serialPortInfoListBox->addItem(list.first(), list);
        }
    
        m_ui->serialPortInfoListBox->addItem(tr("Custom"));
    }
    3 мая 2018 г. 15:34

    Thanks for the answer. But I meant to ask a different thing. I already knew about QSerialPortInfo, but I wanted to know what would be the best way of setting the current portname in the Emitter class. I'd like the update to be thread-safe, since Emiter extends QThread. What I'm asking is if something like the following is the right thing to do:

    // emitter.h
    void Emitter::setPortName(const QString& portName) {
        m_pSerialPort->setPortName(portName);
    }
    
    // inside some view file with a pointer to an instance of Emitter
    Emitter* emitter = ...
    QComboBox* portNamesComboBox = ... // assume it contains a list of portNames extracted from QSerialPortInfo
    QPushButton* changePortBtn = new QPushButton(tr("Change"));
    
    connect(changePortBtn , QPushButton::clicked, this, [this,portNamesComboBox]() {
        auto newPortName  = portNamesComboBox.currentText();
        emitter->setPortName(newPortName);
    });
    4 мая 2018 г. 6:36

    Class Emitter does not extend QThread. They work in pair. They do not inherit each other.

    In the article i do so:

    connect(ui->pushButtonTurnOn, &QPushButton::clicked, m_pEmitter, &Emitter::turnOnXRay);
    connect(ui->pushButtonTurnOn, &QPushButton::clicked, [this](){
            ui->pushButtonTurnOn->setEnabled(false);
            ui->pushButtonTurnOff->setEnabled(true);
    });
    This works.

    Комментарии

    Только авторизованные пользователи могут оставлять комментарии.
    Пожалуйста, Авторизуйтесь или Зарегистрируйтесь
    22 июля 2018 г. 20:56
    Тарас

    C++ - Тест 001. Первая программа и типы данных

    • Результат 73баллов,
    • Очки рейтинга1
    22 июля 2018 г. 18:29
    Kaptn

    C++ - Тест 003. Условия и циклы

    • Результат 100баллов,
    • Очки рейтинга10
    22 июля 2018 г. 7:48
    Kaptn

    C++ - Тест 003. Условия и циклы

    • Результат 64баллов,
    • Очки рейтинга-1
    Последние комментарии
    18 июля 2018 г. 12:45
    plgrm44

    Qt/C++ - Урок 050. Логирование событий Qt приложения в текстовый файл

    А что мешает сохранить адрес дефолтного обработчика и после вывода в файл вызывать и его?
    17 июля 2018 г. 13:34
    Arrow

    Qt/C++ - Урок 049. QTranslator - динамический перевод мультиязычного приложения на Qt

    Работает так: find_package (Qt5LinguistTools)file (GLOB TS_FILES ${SOURCE_DIR}/translations/*.ts)qt5_add_translation (QM_FILES ${TS_FILES})add_custom_target (translations ALL DEPE...
    17 июля 2018 г. 9:53
    Илья Чичак

    Django - Урок 035. Различные шаблоны для рендеринга разных типов контента в поисковой выдаче

    тут все упирается в то, что вы хотите дать поисковым роботам. был у меня опыт проектирования страницы для роботов - сделал точки входа - со статикой для роботов и АПИ для JS клиента=) а отлавл...
    17 июля 2018 г. 9:43
    Евгений Легоцкой

    Django - Урок 035. Различные шаблоны для рендеринга разных типов контента в поисковой выдаче

    Думаю, что это всё равно стоит оставить для индексирующих роботов поисковых систем, которые испоьлзуют простые GET запросы. Они же AJAX не используют. Так что полностью уйти от этого не получи...
    Сейчас обсуждают на форуме
    23 июля 2018 г. 11:24
    Arrow

    QComboBox делегат для QTableView

    И можно еще один маленький вопрос: Как изменить значение в ячейке QTableView?
    23 июля 2018 г. 8:56
    Arrow

    Получение прав пользователей на таблицу базы данных

    Да, только самый основной прикол в том что для возможности редактирования таблицы пользователю одних прав на таблицу мало, нужны еще и на последовательность: GRANT USAGE O...
    23 июля 2018 г. 8:52
    Евгений Легоцкой

    Перестал работать Qt Maintenance Tool

    В настройках есть параметры прокси, возможно, через них сможете завести, через какой-нибудь прокси сервер. По поводу списка репозиториев. сам не в курсе. Спросил на официальном форум...
    23 июля 2018 г. 7:59
    Евгений Легоцкой

    Qt Android

    Так, по поводу ошибок, там в main.cpp ещё одна ошибка у вас Надо так написать QQmlApplicationEngine engine;engine.load(QUrl(QStringLiteral("qrc:/main.qml")));DBase myClas...
    23 июля 2018 г. 6:44
    Евгений Легоцкой

    как проверить состояние у динамически созданного CheckBox в qml

    Тогда можно расширить эту модель, наследоваться от неё. И добавить в качестве роли ещё и состояние чекбокса, для выбора. Также можно переопределить метод setData, чтобы можно было ус...

    Рекомендуемые страницы