Logs
Consultez les logs.
OK
Liste des données
Consultez la liste des données.
OK
Loading...
Formulaire
Saisissez vos données.
Enregistrer
Annuler

Apprendre Qt

Vues
380

Introduction


C++ est un langage de programmation orientée objet. Il offre la possibilité d'avoir le contrôle total sur la gestion de la mémoire.


### Travailler avec Qt


Qt est un gestionnaire d'interfaces homme machine développé en C++ et étendu à plusieurs langages de programmation. Il permet de créer des interfaces homme machine multiplateformes bureau ou mobile sans changer la base de code. 


Changer le style d'apparence des fenêtres en C++ sous Qt


Qt est une librairie de création d'interfaces homme machine prenant en charge le changement dynamique de style d'apparence des fenêtres d'une application. QStyleFactory peut être utilisé pour lister les styles d'apparence disponible sur le système. Alors que, QApplication peut être utilisé pour appliquer dynamiquement un style d'apparence à l'ensemble des fenêtres d'une application. 


Gestion du programme principal 


// main.cpp (Editer le programme principal)
...
#include "cMainWindow.h"
#include <QApplication>

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    cMainWindow *oMainWindow = new cMainWindow;
    oMainWindow->resize(400, 200);
    oMainWindow->show();
    return app.exec();
}
...

Gestion de la fenêtre principale


// cMainWindow.h (Editer la fenêtre principale)
...
#pragma once

#include <QMainWindow>
#include "cActions.h"

class QLabel;

namespace Ui
{
    class cMainWindow;
}

class cMainWindow : public QMainWindow
{
    Q_OBJECT

public:
    using sAction = cActions::sAction;

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

private slots:
    void on_actionHomePage_triggered();
    void on_actionThemePage_triggered();
    void onAction(const sAction &_action);

private:
    Ui::cMainWindow *ui;
    QLabel *m_themeStatus;
};
...

// cMainWindow.cpp (Editer la fenêtre principale)
...
#include "cMainWindow.h"
#include "ui_cMainWindow.h"
#include <QStyleFactory>
#include <QMessageBox>
#include <QLabel>

const QString ACTION_APPLY_STYLE = "ACTION_APPLY_STYLE";

cMainWindow::cMainWindow(QWidget *parent)
    : QMainWindow(parent),
      ui(new Ui::cMainWindow)
{
    ui->setupUi(this);
    ui->stackedWidget->setCurrentWidget(ui->pageHome);
    ui->tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
    ui->tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);

    QStringList oThemes = QStyleFactory::keys();
    for (const auto &oTheme : oThemes)
    {
        qDebug() << "La detection du style d'apparence a reussi."
                 << "|theme=" << oTheme;

        ui->tableWidget->insertRow(ui->tableWidget->rowCount());
        QTableWidgetItem *oTableWidgetItem = new QTableWidgetItem(oTheme);
        ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, 0, oTableWidgetItem);

        cActions *oActions = new cActions;
        oActions->addAction(oTheme, ACTION_APPLY_STYLE, "Appliquer le style");
        oActions->initActions();
        connect(oActions, &cActions::emitAction, this, &cMainWindow::onAction);

        ui->tableWidget->setCellWidget(ui->tableWidget->rowCount() - 1, 1, oActions);
    }

    qDebug() << "La detection du style en cours est termine."
             << "|style=" << qApp->style()->name();

    m_themeStatus = new QLabel;
    statusBar()->addWidget(m_themeStatus);
    m_themeStatus->setText(tr("Thème : %1").arg(qApp->style()->name()));
}

cMainWindow::~cMainWindow()
{
    delete ui;
}

void cMainWindow::on_actionHomePage_triggered()
{
    ui->stackedWidget->setCurrentWidget(ui->pageHome);
}

void cMainWindow::on_actionThemePage_triggered()
{
    ui->stackedWidget->setCurrentWidget(ui->pageTheme);
}

void cMainWindow::onAction(const sAction &_action)
{
    qDebug() << "L'action est en cours d'application."
             << "|data=" << _action.data
             << "|key=" << _action.key
             << "|text=" << _action.text;
    if (_action.key == ACTION_APPLY_STYLE)
    {
        qApp->setStyle(_action.data);
        m_themeStatus->setText(tr("Thème : %1").arg(qApp->style()->name()));
    }
}
...

// cMainWindow.ui (Editer le menu général)
image.png

// cMainWindow.ui (Editer la liste des styles)
image.png

Gestion du bouton des actions


// cActions.h (Editer le bouton des actions)
...
#pragma once

#include <QWidget>
#include <QVector>
#include <QMap>

namespace Ui
{
    class cActions;
}

class cActions : public QWidget
{
    Q_OBJECT

public:
    struct sAction
    {
        QString data;
        QString key;
        QString text;
    };

public:
    explicit cActions(QWidget *parent = 0);
    ~cActions();
    void addAction(const QString &_data, const QString &_key, const QString &_text);
    void initActions();

private:
    bool isActive(const QString &_key) const;
    bool loadAction(const QString &_key, sAction &_action) const;

private slots:
    void onAction();

signals:
    void emitAction(sAction);

private:
    Ui::cActions *ui;
    QVector<sAction> m_actions;
    QMap<QAction *, QString> m_keys;
};
...

// cActions.cpp (Editer le bouton des actions)
...
#include "cActions.h"
#include "ui_cActions.h"
#include <QMenu>

cActions::cActions(QWidget *parent)
    : QWidget(parent),
      ui(new Ui::cActions)
{
    ui->setupUi(this);
    ui->layActions->setAlignment(Qt::AlignCenter);
}

cActions::~cActions()
{
    delete ui;
}

void cActions::addAction(const QString &_data, const QString &_key, const QString &_text)
{
    if (!isActive(_key))
    {
        sAction oAction{_data, _key, _text};
        m_actions.push_back(oAction);
    }
    else
    {
        qDebug() << "L'action a ete deja ajoutee a la liste."
                 << "|data=" << _data
                 << "|key=" << _key
                 << "|text=" << _text;
    }
}

void cActions::initActions()
{
    if (m_actions.isEmpty())
    {
        qDebug() << "Aucune action n'a ete ajoutee à liste";
        return;
    }

    QMenu *oMenu = new QMenu(this);

    for (const auto &oAction : m_actions)
    {
        QAction *iAction = new QAction(oAction.text);
        iAction->setData(oAction.data);
        connect(iAction, &QAction::triggered, this, &cActions::onAction);
        m_keys[iAction] = oAction.key;
        oMenu->addAction(iAction);
    }

    ui->btnActions->setMenu(oMenu);
}

bool cActions::isActive(const QString &_key) const
{
    for (const auto &oAction : m_actions)
    {
        if (oAction.key == _key)
        {
            return true;
        }
    }
    return false;
}

bool cActions::loadAction(const QString &_key, sAction &_action) const
{
    for (const auto &oAction : m_actions)
    {
        if (oAction.key == _key)
        {
            _action = oAction;
            return true;
        }
    }
    return false;
}

void cActions::onAction()
{
    QAction *oAction = qobject_cast<QAction *>(sender());
    QString oKey = m_keys[oAction];
    sAction iAction;
    if (loadAction(oKey, iAction))
    {
        emit emitAction(iAction);
    }
}
...

// cActions.ui (Editer le bouton des actions)
image.png

Gestion du fichier CMake


// CMakeLists.txt (Editer le fichier CMake)
...
cmake_minimum_required(VERSION 3.10.0)
project(rdvcpp VERSION 0.1.0 LANGUAGES C CXX)

set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)

set(CMAKE_PREFIX_PATH "C:/Qt/6.5.2/msvc2019_64/lib/cmake")

find_package(Qt6 REQUIRED COMPONENTS
    Widgets
)

qt_wrap_ui(UI_FILES
    cMainWindow.ui
    cActions.ui
)

add_executable(rdvcpp
    main.cpp
    cMainWindow.cpp
    cActions.cpp
    resources.qrc
    ${UI_FILES}
)

target_link_libraries (rdvcpp
    Qt6::Widgets
)
...

Gestion du fichier ressource


// resources.qrc (Editer le fichier ressource)
...
<RCC>
  <qresource prefix="/img">
    <file alias="logo.png">data/img/logo.png</file>
  </qresource>
</RCC>
...


Exécuter le projet


// Terminal (Exécuter le projet)
...
rdvcpp.exe
...

// Application (Fiche du menu à propos)
image.png

// Application (Apparence du style [WindowsVista])
image.png

// Application (Apparence du style [Windows])
image.png

// Application (Apparence du style [Fusion])
image.png

Démo de l'application


https://www.youtube.com/watch?v=MAE01f4tfo0


Afficher une fenêtre d'aide HTML en C++ sous Qt


Qt est une librairie de création d'interfaces homme machine prenant en charge l'affichage de texte HTML dans une fenêtre de navigation web. QTextBrowser peut être utilisé pour afficher le contenu d'un fichier d'aide au format HTML avec la possibilité d'y insérer des liens de navigation interne dans le document.


Gestion du programme principal


// main.cpp (Editer le programme principal)
...
#include "cMainWindow.h"
#include <QApplication>
#include <QFontDatabase>

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

    QString oFontFile(":/font/fontawesome-webfont.ttf");
    int id = QFontDatabase::addApplicationFont(oFontFile);
    if (id == -1)
    {
        qDebug() << "Le chargement de la police a echoue."
                 << "|fontFile=" << oFontFile;
    }
    else
    {
        QStringList oFontFamilies = QFontDatabase::applicationFontFamilies(id);
        qDebug() << "Le chargement de la police a reussi."
                 << "|fontFile=" << oFontFile
                 << "|fontFamilies=" << oFontFamilies;
    }

    cMainWindow *oMainWindow = new cMainWindow;
    oMainWindow->resize(400, 200);
    oMainWindow->show();
    return app.exec();
}
...

Gestion de la fenêtre principale


// cMainWindow.h (Editer la fenêtre principale)
...
#pragma once

#include <QMainWindow>

class cHelpWindow;

namespace Ui
{
    class cMainWindow;
}

class cMainWindow : public QMainWindow
{
    Q_OBJECT

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

private slots:
    void on_actionHelpApp_triggered();
    void on_actionAboutApp_triggered();
    void on_actionAboutQt_triggered();

private:
    Ui::cMainWindow *ui;
    cHelpWindow *m_helpWindow;
};
...

// cMainWindow.cpp (Editer la fenêtre principale)
...
#include "cMainWindow.h"
#include "ui_cMainWindow.h"
#include "cHelpWindow.h"
#include <QMessageBox>
#include <QFile>
#include <QFontDatabase>
#include <QDebug>

cMainWindow::cMainWindow(QWidget *parent)
    : QMainWindow(parent),
      ui(new Ui::cMainWindow)
{
    ui->setupUi(this);
    QFile oFile(":/help/fr.html");
    if (!oFile.open(QIODevice::ReadOnly | QIODevice::Text))
    {
        qDebug() << "Le fichier d'aide est introuvable."
                 << "filename=" << oFile.fileName();
        return;
    }
    QByteArray oFileData = oFile.readAll();
    m_helpWindow = new cHelpWindow(QString(oFileData), this);
}

cMainWindow::~cMainWindow()
{
    delete ui;
}

void cMainWindow::on_actionHelpApp_triggered()
{
    m_helpWindow->exec();
}

void cMainWindow::on_actionAboutApp_triggered()
{
    QMessageBox::about(this, "A propos de l'application ",
                       "<b>ReadySCOPE - v1.0</b><br>"
                       "Oscilloscope numérique<br><br>"
                       "Produit par <a href='https://readydev.ovh/home'>ReadyDEV</a><br>"
                       "Plateforme de Développement Continu");
}

void cMainWindow::on_actionAboutQt_triggered()
{
    QMessageBox::aboutQt(this);
}
...

// cMainWindow.ui (Editer le menu à propos)
image.png

Gestion de la fenêtre d'aide de l'application


// cHelpWindow.h (Editer la fenêtre d'aide)
...
#pragma once

#include <QDialog>

class cFloatButton;

namespace Ui
{
    class cHelpWindow;
}

class cHelpWindow : public QDialog
{
    Q_OBJECT

public:
    explicit cHelpWindow(const QString &_message, QWidget *parent = 0);
    ~cHelpWindow();

protected:
    void resizeEvent(QResizeEvent *event);

private slots:
    void onOpenTopLink();
    void onOpenBottomLink();

private:
    Ui::cHelpWindow *ui;
    QString m_message;
    cFloatButton *m_topLink;
    cFloatButton *m_bottomLink;
};
...

// cHelpWindow.cpp (Editer la fenêtre d'aide)
...
#include "cHelpWindow.h"
#include "ui_cHelpWindow.h"
#include "cFloatButton.h"
#include <QLabel>
#include <QResizeEvent>
#include <QScrollBar>

using namespace std::chrono_literals;

cHelpWindow::cHelpWindow(const QString &_message, QWidget *parent)
    : QDialog(parent),
      ui(new Ui::cHelpWindow),
      m_message(_message)
{
    ui->setupUi(this);
    ui->txtHelp->setHtml(m_message);
    resize(600, 300);
    m_bottomLink = new cFloatButton(ICON_FA_CHEVRON_DOWN, this);
    connect(m_bottomLink, &cFloatButton::emitClicked, this, &cHelpWindow::onOpenBottomLink);
    m_topLink = new cFloatButton(ICON_FA_CHEVRON_UP, this);
    connect(m_topLink, &cFloatButton::emitClicked, this, &cHelpWindow::onOpenTopLink);
}

cHelpWindow::~cHelpWindow()
{
    delete ui;
}

void cHelpWindow::resizeEvent(QResizeEvent *event)
{
    m_bottomLink->move(width() - 75 - 50, height() - 85);
    m_topLink->move(width() - 75, height() - 85);
}

void cHelpWindow::onOpenTopLink()
{
    QScrollBar *oScrollBar = ui->txtHelp->verticalScrollBar();
    oScrollBar->setValue(0);
}

void cHelpWindow::onOpenBottomLink()
{
    QScrollBar *oScrollBar = ui->txtHelp->verticalScrollBar();
    oScrollBar->setValue(oScrollBar->maximum());
}
...

// cHelpWindow.ui (Editer la fenêtre d'aide)
image.png

Gestion du bouton flottant


// cFloatButton.h (Editer le bouton flotant)
...
#pragma once

#include <QWidget>
#include "IconsFontAwesome4.h"

namespace Ui
{
    class cFloatButton;
}

class cFloatButton : public QWidget
{
    Q_OBJECT

public:
    explicit cFloatButton(const QString &_iconLabel, QWidget *parent = 0);
    ~cFloatButton();

protected:
    bool eventFilter(QObject *object, QEvent *event);

signals:
    void emitClicked();

private:
    Ui::cFloatButton *ui;
    QString m_iconLabel;
};
...

// cFloatButton.cpp (Editer le bouton flotant)
...
#include "cFloatButton.h"
#include "ui_cFloatButton.h"
#include <QMouseEvent>

using namespace std::chrono_literals;

cFloatButton::cFloatButton(const QString &_iconLabel, QWidget *parent)
    : QWidget(parent),
      ui(new Ui::cFloatButton),
      m_iconLabel(_iconLabel)
{
    ui->setupUi(this);
    adjustSize();

    QFont font;
    font.setFamily("FontAwesome");
    font.setPixelSize(20);

    ui->lblButton->setFont(font);
    ui->lblButton->setText(m_iconLabel);
    ui->lblButton->installEventFilter(this);
}

cFloatButton::~cFloatButton()
{
    delete ui;
}

bool cFloatButton::eventFilter(QObject *object, QEvent *event)
{
    if (object == ui->lblButton && event->type() == QEvent::MouseButtonRelease)
    {
        QMouseEvent *oMouseEvent = static_cast<QMouseEvent *>(event);
        if (oMouseEvent->button() == Qt::LeftButton)
        {
            emit emitClicked();
            return true;
        }
    }
    return false;
}
...

// cFloatButton.ui (Editer le bouton flotant)
image.png

Gestion du fichier CMake


// CMakeLists.txt (Editer le fichier CMake)
...
cmake_minimum_required(VERSION 3.10.0)
project(rdvcpp VERSION 0.1.0 LANGUAGES C CXX)

set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)

set(CMAKE_PREFIX_PATH "C:/Qt/6.5.2/msvc2019_64/lib/cmake")

find_package(Qt6 REQUIRED COMPONENTS
    Widgets
)

qt_wrap_ui(UI_FILES
    cMainWindow.ui
    cHelpWindow.ui
    cFloatButton.ui
)

add_executable(rdvcpp
    main.cpp
    cMainWindow.cpp
    cHelpWindow.cpp
    cFloatButton.cpp
    resources.qrc
    ${UI_FILES}
)

target_link_libraries (rdvcpp
    Qt6::Widgets
)
...

Gestion du fichier ressource


// resources.qrc (Editer le fichier ressource)
...
<RCC>
  <qresource prefix="/img">
    <file alias="logo.png">data/img/logo.png</file>
  </qresource>
  <qresource prefix="/help">
    <file alias="fr.html">data/help/fr.html</file>
  </qresource>
  <qresource prefix="/font">
    <file alias="fontawesome-webfont.ttf">data/font/fontawesome-webfont.ttf</file>
  </qresource>
</RCC>
...

Exécution du projet


// Terminal (Exécuter le projet)
...
rdvcpp.exe
...

// Application (Fiche du menu à propos)
image.png

// Application (Fiche de l'aide de l'application)
image.png

// Application (Fiche à propos de l'application)
image.png

// Application (Fiche à propos de Qt)
image.png

Démo de l'application


https://www.youtube.com/watch?v=Qctn_INIvPg


Gérer une communication port série en C++ sous Qt


Qt est une librairie de création d'interfaces homme machine prenant en charge la gestion de la communication port série. QSerialPort peut être utilisé pour concevoir en toute simplicité une interface de contrôle d'un système via le port série. Dans ce tutoriel, nous utilisons QSerialPort pour établir 2 connexions à partir d'une paire de ports série afin de mettre en place un système chat.


Gestion du programme principal


// main.cpp (Editer le programme principal)
...
#include "cMainWindow.h"
#include <QApplication>

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    cMainWindow *oMainWindow = new cMainWindow;
    oMainWindow->resize(600, 400);
    oMainWindow->show();
    return app.exec();
}
...

Gestion de la fenêtre principale


// cMainWindow.h (Editer la fenêtre principale)
...
#pragma once

#include <QMainWindow>
#include <QtSerialPort/QSerialPort>
#include <QVector>
#include <QMap>

namespace Ui
{
    class cMainWindow;
}

class cMainWindow : public QMainWindow
{
    Q_OBJECT

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

private:
    bool isActive(const QString &_portName) const;
    void addConnection(const QString &_portName);
    void addPortToTab(const QString &_portName);
    bool loadConnection(const QString &_portName, QSerialPort **_serialPort) const;
    bool loadPortToTab(const QString &_portName, QWidget **_tabWidget) const;
    bool loadPortToTab(QWidget *_oTabWidget, QString &_portName) const;
    bool loadPortToTab(const QString &_portName, QLayout **_layout) const;
    void showSendMessageTab(const QString &_portName) const;
    void clearOpenConnection();
    void clearSendMessagePort1();
    void clearSendMessagePort2();

private slots:
    void on_actionOpenConnection_triggered();
    void on_actionDisplayConnection_triggered();
    void on_btnOpenConnection_clicked();
    void on_btnSendMessagePort1_clicked();
    void on_btnSendMessagePort2_clicked();
    void onReadyData();
    void onErrorOccurred(QSerialPort::SerialPortError error);
    void onSendMessage(QAction *_action);

private:
    Ui::cMainWindow *ui;
    QVector<QSerialPort *> m_connections;
    QMap<QString, QWidget *> m_portToTab;
};
...

// cMainWindow.cpp (Editer la fenêtre principale)
...
#include "cMainWindow.h"
#include "ui_cMainWindow.h"
#include "cPortAction.h"
#include "cMessageSend.h"
#include "cMessageRecv.h"
#include <QMessageBox>
#include <QScrollBar>

cMainWindow::cMainWindow(QWidget *parent)
    : QMainWindow(parent),
      ui(new Ui::cMainWindow)
{
    ui->setupUi(this);
    ui->stackedWidget->setCurrentWidget(ui->pageHome);
    ui->tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
    ui->tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
    ui->tabWidget->setCurrentWidget(ui->tabSendMessagePort1);
    ui->messageLayoutPort1->setAlignment(Qt::AlignTop);
    ui->messageLayoutPort2->setAlignment(Qt::AlignTop);
}

cMainWindow::~cMainWindow()
{
    delete ui;
}

bool cMainWindow::isActive(const QString &_portName) const
{
    for (QSerialPort *oSerialPort : m_connections)
    {
        if (oSerialPort->portName() == _portName)
        {
            if (oSerialPort->isOpen())
                return true;
        }
    }
    return false;
}

void cMainWindow::addConnection(const QString &_portName)
{
    ui->tableWidget->insertRow(ui->tableWidget->rowCount());
    QTableWidgetItem *oTableWidgetItem = new QTableWidgetItem(_portName);
    ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, 0, oTableWidgetItem);

    cPortAction *oPortAction = new cPortAction(_portName);
    connect(oPortAction, &cPortAction::emitSendMessage, this, &cMainWindow::onSendMessage);

    ui->tableWidget->setCellWidget(ui->tableWidget->rowCount() - 1, 1, oPortAction);
}

void cMainWindow::addPortToTab(const QString &_portName)
{
    switch (m_portToTab.size())
    {
    case 0:
        m_portToTab[_portName] = ui->tabSendMessagePort1;
        ui->tabWidget->setTabText(ui->tabWidget->indexOf(ui->tabSendMessagePort1), _portName);
        break;
    case 1:
        m_portToTab[_portName] = ui->tabSendMessagePort2;
        ui->tabWidget->setTabText(ui->tabWidget->indexOf(ui->tabSendMessagePort2), _portName);
        break;
    default:
        break;
    }
}

bool cMainWindow::loadConnection(const QString &_portName, QSerialPort **_serialPort) const
{
    for (QSerialPort *oSerialPort : m_connections)
    {
        if (oSerialPort->portName() == _portName)
        {
            if (oSerialPort->isOpen())
            {
                (*_serialPort) = oSerialPort;
                return true;
            }
        }
    }
    return false;
}

bool cMainWindow::loadPortToTab(const QString &_portName, QWidget **_tabWidget) const
{
    QMap<QString, QWidget *>::const_iterator it;
    for (it = m_portToTab.begin(); it != m_portToTab.end(); ++it)
    {
        if (it.key() == _portName)
        {
            (*_tabWidget) = it.value();
            return true;
        }
    }
    return false;
}

bool cMainWindow::loadPortToTab(QWidget *_oTabWidget, QString &_portName) const
{
    QMap<QString, QWidget *>::const_iterator it;
    for (it = m_portToTab.begin(); it != m_portToTab.end(); ++it)
    {
        if (it.value() == _oTabWidget)
        {
            _portName = it.key();
            return true;
        }
    }
    return false;
}

bool cMainWindow::loadPortToTab(const QString &_portName, QLayout **_layout) const
{
    QWidget *oTabWidget;
    if (!loadPortToTab(_portName, &oTabWidget))
        return false;

    if (oTabWidget == ui->tabSendMessagePort1)
    {
        (*_layout) = ui->messageLayoutPort1;
        return true;
    }
    if (oTabWidget == ui->tabSendMessagePort2)
    {
        (*_layout) = ui->messageLayoutPort2;
        return true;
    }

    return false;
}

void cMainWindow::showSendMessageTab(const QString &_portName) const
{
    QWidget *oTabWidget;
    if (!loadPortToTab(_portName, &oTabWidget))
        return;
    ui->tabWidget->setTabVisible(ui->tabWidget->indexOf(ui->tabSendMessagePort1), (oTabWidget == ui->tabSendMessagePort1));
    ui->tabWidget->setTabVisible(ui->tabWidget->indexOf(ui->tabSendMessagePort2), (oTabWidget == ui->tabSendMessagePort2));
}

void cMainWindow::clearOpenConnection()
{
    ui->edtPortName->clear();
}

void cMainWindow::clearSendMessagePort1()
{
    ui->edtSendMessagePort1->clear();
}

void cMainWindow::clearSendMessagePort2()
{
    ui->edtSendMessagePort2->clear();
}

void cMainWindow::on_actionOpenConnection_triggered()
{
    if (m_connections.size() == 2)
    {
        QMessageBox::critical(this, tr("Messages d'erreurs"),
                              tr("Le nombre maximal de connexions est atteint."));
        return;
    }

    ui->stackedWidget->setCurrentWidget(ui->pageOpenConnection);
}

void cMainWindow::on_actionDisplayConnection_triggered()
{
    ui->stackedWidget->setCurrentWidget(ui->pageDisplayConnection);
}

void cMainWindow::on_btnOpenConnection_clicked()
{
    QString oPortName = ui->edtPortName->text();

    if (oPortName.isEmpty())
    {
        QMessageBox::critical(this, tr("Messages d'erreurs"),
                              tr("Le nom du port est obligatoire."));
        return;
    }

    if (isActive(oPortName))
    {
        QMessageBox::critical(this, tr("Messages d'erreurs"),
                              tr("Le port série est déjà ouvert."));
        return;
    }

    QSerialPort *oSerialPort = new QSerialPort(this);
    connect(oSerialPort, &QSerialPort::readyRead, this, &cMainWindow::onReadyData);
    connect(oSerialPort, &QSerialPort::errorOccurred, this, &cMainWindow::onErrorOccurred);

    oSerialPort->setPortName(oPortName);
    oSerialPort->setBaudRate(QSerialPort::Baud9600);
    oSerialPort->setDataBits(QSerialPort::Data8);
    oSerialPort->setFlowControl(QSerialPort::NoFlowControl);
    oSerialPort->setParity(QSerialPort::NoParity);
    oSerialPort->setStopBits(QSerialPort::OneStop);
    oSerialPort->open(QIODevice::ReadWrite);

    if (!oSerialPort->isOpen())
    {
        QMessageBox::critical(this, tr("Messages d'erreurs"),
                              tr("La connexion au port série a échoué."));

        return;
    }

    QMessageBox::information(this, tr("Messages d'informations"),
                             tr("La connexion au port série a réussi."));

    m_connections.push_back(oSerialPort);
    addConnection(oPortName);
    addPortToTab(oPortName);
    clearOpenConnection();
    ui->stackedWidget->setCurrentWidget(ui->pageHome);
}

void cMainWindow::on_btnSendMessagePort1_clicked()
{
    QString oMessage = ui->edtSendMessagePort1->toPlainText();

    if (oMessage.isEmpty())
    {
        QMessageBox::critical(this, tr("Messages d'erreurs"),
                              tr("Le message est obligatoire."));

        return;
    }

    QString oPortName;

    if (!loadPortToTab(ui->tabSendMessagePort1, oPortName))
    {
        QMessageBox::critical(this, tr("Messages d'erreurs"),
                              tr("Le nom du port est inconnu."));

        return;
    }

    QSerialPort *oSerialPort;
    if (!loadConnection(oPortName, &oSerialPort))
    {
        QMessageBox::critical(this, tr("Messages d'erreurs"),
                              tr("La connexion port série est introuvable."));
        return;
    }

    cMessageSend *oMessageSend = new cMessageSend(oMessage);
    ui->messageLayoutPort1->addWidget(oMessageSend);
    ui->scrollAreaPort1->widget()->adjustSize();
    qApp->processEvents();
    ui->scrollAreaPort1->verticalScrollBar()->setValue(ui->scrollAreaPort1->verticalScrollBar()->maximum());

    oSerialPort->write(oMessage.toUtf8());

    clearSendMessagePort1();

    qDebug() << "L'envoi de message est termine."
             << "|portName=" << oSerialPort->portName()
             << "|message=" << oMessage.left(50);
}

void cMainWindow::on_btnSendMessagePort2_clicked()
{
    QString oMessage = ui->edtSendMessagePort2->toPlainText();

    if (oMessage.isEmpty())
    {
        QMessageBox::critical(this, tr("Messages d'erreurs"),
                              tr("Le message est obligatoire."));
        return;
    }

    QString oPortName;

    if (!loadPortToTab(ui->tabSendMessagePort2, oPortName))
    {
        QMessageBox::critical(this, tr("Messages d'erreurs"),
                              tr("Le nom du port est inconnu."));

        return;
    }

    QSerialPort *oSerialPort;
    if (!loadConnection(oPortName, &oSerialPort))
    {
        QMessageBox::critical(this, tr("Messages d'erreurs"),
                              tr("La connexion port série est introuvable."));
        return;
    }

    cMessageSend *oMessageSend = new cMessageSend(oMessage);
    ui->messageLayoutPort2->addWidget(oMessageSend);
    ui->scrollAreaPort2->widget()->adjustSize();
    qApp->processEvents();
    ui->scrollAreaPort2->verticalScrollBar()->setValue(ui->scrollAreaPort2->verticalScrollBar()->maximum());

    oSerialPort->write(oMessage.toUtf8());

    clearSendMessagePort2();

    qDebug()
        << "L'envoi de message est termine."
        << "|portName=" << oSerialPort->portName()
        << "|message=" << oMessage.left(50);
}

void cMainWindow::onReadyData()
{
    QSerialPort *oSerialPort = qobject_cast<QSerialPort *>(sender());
    QByteArray oMessage = oSerialPort->readAll();
    QString oPortName = oSerialPort->portName();
    QLayout *oMessageLayout;

    if (!loadPortToTab(oSerialPort->portName(), &oMessageLayout))
        return;

    cMessageRecv *oMessageRecv = new cMessageRecv(oMessage);
    oMessageLayout->addWidget(oMessageRecv);

    qDebug() << "La reception de message est termine."
             << "|portName=" << oSerialPort->portName()
             << "|message=" << oMessage.left(50);
}

void cMainWindow::onErrorOccurred(QSerialPort::SerialPortError error)
{
    if (error != QSerialPort::NoError)
    {
        QSerialPort *oSerialPort = qobject_cast<QSerialPort *>(sender());
        qDebug() << "La connexion au port serie a echoue."
                 << "|errorCode=" << error
                 << "|portName=" << oSerialPort->portName();
    }
}

void cMainWindow::onSendMessage(QAction *_action)
{
    QString oPortName = _action->data().toString();
    QSerialPort *oSerialPort;
    if (loadConnection(oPortName, &oSerialPort))
    {
        ui->stackedWidget->setCurrentWidget(ui->pageSendMessage);
        showSendMessageTab(oPortName);
        qDebug() << "L'envoi de message a demarre."
                 << "|portName=" << oSerialPort->portName();
    }
}
...

// cMainWindow.ui (Editer le menu)
image.png

// cMainWindow.ui (Editer la liste des ports série)
image.png

// cMainWindow.ui (Editer l'ouverture d'un port série)
image.png

// cMainWindow.ui (Editer l'envoi des messages sur le port série 1)
image.png

// cMainWindow.ui (Editer l'envoi des messages sur le port série 2)
image.png

Gestion des actions associées à un port série


// cPortAction.h (Editer les actions associées à un port série)
...
#pragma once

#include <QWidget>

namespace Ui
{
    class cPortAction;
}

class cPortAction : public QWidget
{
    Q_OBJECT

public:
    explicit cPortAction(const QString &_portName, QWidget *parent = 0);
    ~cPortAction();

private:
    void initActions();

private slots:
    void onSendMessage();

signals:
    void emitSendMessage(QAction *);

private:
    Ui::cPortAction *ui;
    QString m_portName;
};
...

// cPortAction.cpp (Editer les actions associées à un port série)
...
#include "cPortAction.h"
#include "ui_cPortAction.h"
#include <QMenu>

cPortAction::cPortAction(const QString &_portName, QWidget *parent)
    : QWidget(parent),
      ui(new Ui::cPortAction),
      m_portName(_portName)
{
    ui->setupUi(this);
    ui->layActions->setAlignment(Qt::AlignCenter);
    initActions();
}

cPortAction::~cPortAction()
{
    delete ui;
}

void cPortAction::initActions()
{
    QMenu *oMenu = new QMenu(this);

    QAction *oActions = new QAction("Envoyer un message");
    oActions->setData(m_portName);
    oMenu->addAction(oActions);
    connect(oActions, &QAction::triggered, this, &cPortAction::onSendMessage);

    ui->btnActions->setMenu(oMenu);
}

void cPortAction::onSendMessage()
{
    QAction *oActions = qobject_cast<QAction *>(sender());
    emit emitSendMessage(oActions);
}
...

// cPortAction.ui (Editer les actions)
image.png

Gestion de l'envoi de message


// cMessageSend.h (Editer l'envoi des messages)
...
#pragma once

#include <QWidget>

namespace Ui
{
    class cMessageSend;
}

class cMessageSend : public QWidget
{
    Q_OBJECT

public:
    explicit cMessageSend(const QString &_message, QWidget *parent = 0);
    ~cMessageSend();

private:
    Ui::cMessageSend *ui;
    QString m_message;
};
...

// cMessageSend.h (Editer l'envoi des messages)
...
#include "cMessageSend.h"
#include "ui_cMessageSend.h"

using namespace std::chrono_literals;

cMessageSend::cMessageSend(const QString &_message, QWidget *parent)
    : QWidget(parent),
      ui(new Ui::cMessageSend),
      m_message(_message)
{
    ui->setupUi(this);
    ui->layLogo->setAlignment(Qt::AlignTop);
    ui->lblMessage->setText(m_message);
}

cMessageSend::~cMessageSend()
{
    delete ui;
}
...

// cMessageSend.ui (Editer l'envoi des messages)
image.png

Gestion de la réception des messages


// cMessageRecv.h (Editer la réception des messages)
...
#pragma once

#include <QWidget>

namespace Ui
{
    class cMessageRecv;
}

class cMessageRecv : public QWidget
{
    Q_OBJECT

public:
    explicit cMessageRecv(const QString &_message, QWidget *parent = 0);
    ~cMessageRecv();

private:
    Ui::cMessageRecv *ui;
    QString m_message;
};
...

// cMessageRecv.cpp (Editer la réception des messages)
...
#include "cMessageRecv.h"
#include "ui_cMessageRecv.h"

using namespace std::chrono_literals;

cMessageRecv::cMessageRecv(const QString &_message, QWidget *parent)
    : QWidget(parent),
      ui(new Ui::cMessageRecv),
      m_message(_message)
{
    ui->setupUi(this);
    ui->layLogo->setAlignment(Qt::AlignTop);
    ui->lblMessage->setText(m_message);
}

cMessageRecv::~cMessageRecv()
{
    delete ui;
}
...

// cMessageRecv.ui (Editer la réception des messages)
image.png

Gestion du fichier CMake


// CMakeLists.txt (Editer le fichier CMake)
...
cmake_minimum_required(VERSION 3.10.0)
project(rdvcpp VERSION 0.1.0 LANGUAGES C CXX)

set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)

set(CMAKE_PREFIX_PATH "C:/Qt/6.5.2/msvc2019_64/lib/cmake")

find_package(Qt6 REQUIRED COMPONENTS
    Widgets
    SerialPort
)

qt_wrap_ui(UI_FILES
    cMainWindow.ui
    cPortAction.ui
    cMessageSend.ui
    cMessageRecv.ui
)

add_executable(rdvcpp
    main.cpp
    cMainWindow.cpp
    cPortAction.cpp
    cMessageSend.cpp
    cMessageRecv.cpp
    resources.qrc
    ${UI_FILES}
)

target_link_libraries (rdvcpp
    Qt6::Widgets
    Qt6::SerialPort
)
...

Gestion du fichier ressource


// resources.qrc (Editer le fichier ressource)
...
<RCC>
  <qresource prefix="/img">
    <file alias="logo.png">data/img/logo.png</file>
  </qresource>
</RCC>
...

Exécution du projet


// Terminal (Exécuter le projet)
...
rdvcpp.exe
...

image.png

image.png

image.png

Démo de l'application


https://www.youtube.com/watch?v=zHWRMOeXmqQ


Gérer des configurations utilisateur en C++ sous Qt


Qt est une librairie de création d'interfaces homme machine prenant en charge la gestion des configurations utilisateur. QSettings peut être utilisé pour sauvegarder de manière permanente toutes les configurations utilisateur associées à une application. Dans ce tutoriel, nous utilisons QSettings pour sauvegarder les paramètres d'une application de gestion d'oscilloscope numérique.


Gestion du programme principal


// main.cpp (Editer le programme principal)
...
#include "cMainWindow.h"
#include <QApplication>

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    cMainWindow *oMainWindow = new cMainWindow;
    oMainWindow->resize(400, 500);
    oMainWindow->show();
    return app.exec();
}
...

Gestion de la fenêtre principale


// cMainWindow.h (Editer la fenêtre principale)
...
#pragma once

#include <QMainWindow>

class QLabel;
class QSettings;

namespace Ui
{
    class cMainWindow;
}

class cMainWindow : public QMainWindow
{
    Q_OBJECT

public:
    struct sSettingsDTO
    {
        // General
        QString inputDevice;
        int bufferSize;
        int nanValue;
        bool isAfterGlow;
        int refreshRate;
        QString settingsPath;
        // Format
        QString displayFormat;
        QString binaryFormat;
        // Sacling
        double scalingA;
        double scalingB;
        bool isScalingEnabled;
    };

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

private:
    void loadDefaultValue(sSettingsDTO &_settingsDTO);
    void readSettings(sSettingsDTO &_settingsDTO);
    void writeSettings(const sSettingsDTO &_settingsDTO);
    void saveSettings(QSettings &_settings, const sSettingsDTO &_settingsDTO);
    void loadSettings(QSettings &_settings, sSettingsDTO &_settingsDTO);

protected:
    void closeEvent(QCloseEvent *event);

private slots:
    void on_actionSaveSettings_triggered();
    void on_actionLoadSettings_triggered();
    void on_actionSettingsPage_triggered();
    void on_btnSettingsOK_clicked();

private:
    Ui::cMainWindow *ui;
    QString m_settingsPath;
};
...

// cMainWindow.cpp (Editer la fenêtre principale)
...
#include "cMainWindow.h"
#include "ui_cMainWindow.h"
#include <QFileDialog>
#include <QSettings>
#include <QDir>
#include <QFileInfo>
#include <QMessageBox>
#include <QCloseEvent>

// Organization
static const QString DEF_ORGANIZATION_NAME = "ReadyDEV";
static const QString DEF_APP_NAME = "ReadySCOPE";
// Settings : General
static const QString DEF_INPUT_DEVICE = "InputDevice";
static const QString DEF_BUFFER_SIZE = "BufferSize";
static const QString DEF_NAN_VALUE = "NanValue";
static const QString DEF_AFTER_GLOW = "AfterGlow";
static const QString DEF_REFRESH_RATE = "RefreshRate";
static const QString DEF_SETTINGS_PATH = "SettingsPath";
// Settings : Format
static const QString DEF_SETTINGS_FORMAT = "Format";
static const QString DEF_DISPLAY_FORMAT = "DisplayFormat";
static const QString DEF_BINARY_FORMAT = "BinaryFormat";
// Settings : Scaling
static const QString DEF_SETTINGS_SCALING = "Scaling";
static const QString DEF_SCALING_A = "ScalingA";
static const QString DEF_SCALING_B = "ScalingB";
static const QString DEF_SCALING_ENABLED = "ScalingEnabled";

cMainWindow::cMainWindow(QWidget *parent)
    : QMainWindow(parent),
      ui(new Ui::cMainWindow)
{
    ui->setupUi(this);
    ui->stackedWidget->setCurrentWidget(ui->pageHome);
    QSettings oSettings(DEF_ORGANIZATION_NAME, DEF_APP_NAME);
    sSettingsDTO oSettingsDTO;
    loadSettings(oSettings, oSettingsDTO);
    writeSettings(oSettingsDTO);
}

cMainWindow::~cMainWindow()
{
    delete ui;
}

void cMainWindow::loadDefaultValue(sSettingsDTO &_settingsDTO)
{
    // General
    _settingsDTO.inputDevice = "[input-device]";
    _settingsDTO.bufferSize = 100000;
    _settingsDTO.nanValue = 0;
    _settingsDTO.isAfterGlow = false;
    _settingsDTO.refreshRate = 10;
    _settingsDTO.settingsPath = QDir::homePath();
    // Format
    _settingsDTO.displayFormat = "[display-format]";
    _settingsDTO.binaryFormat = "[binary-format]";
    // Scaling
    _settingsDTO.scalingA = 1.0;
    _settingsDTO.scalingB = 0.0;
    _settingsDTO.isScalingEnabled = false;
}

void cMainWindow::readSettings(sSettingsDTO &_settingsDTO)
{
    // General
    _settingsDTO.inputDevice = ui->edtInputDevice->text();
    _settingsDTO.bufferSize = ui->edtBufferSize->value();
    _settingsDTO.nanValue = ui->edtNanValue->value();
    _settingsDTO.isAfterGlow = ui->radAfterGlowYes->isChecked();
    _settingsDTO.refreshRate = ui->edtRefreshRate->value();
    _settingsDTO.settingsPath = m_settingsPath;
    // Format
    _settingsDTO.displayFormat = ui->edtDisplayFormat->text();
    _settingsDTO.binaryFormat = ui->edtBinaryFormat->text();
    // Scaling
    _settingsDTO.scalingA = ui->edtScalingA->value();
    _settingsDTO.scalingB = ui->edtScalingB->value();
    _settingsDTO.isScalingEnabled = ui->chbScalingEnabled->isChecked();
}

void cMainWindow::writeSettings(const sSettingsDTO &_settingsDTO)
{
    // General
    ui->edtInputDevice->setText(_settingsDTO.inputDevice);
    ui->edtBufferSize->setValue(_settingsDTO.bufferSize);
    ui->edtNanValue->setValue(_settingsDTO.nanValue);
    ui->radAfterGlowYes->setChecked(_settingsDTO.isAfterGlow);
    ui->radAfterGlowNo->setChecked(!_settingsDTO.isAfterGlow);
    ui->edtRefreshRate->setValue(_settingsDTO.refreshRate);
    m_settingsPath = _settingsDTO.settingsPath;
    // Format
    ui->edtDisplayFormat->setText(_settingsDTO.displayFormat);
    ui->edtBinaryFormat->setText(_settingsDTO.binaryFormat);
    // Scaling
    ui->edtScalingA->setValue(_settingsDTO.scalingA);
    ui->edtScalingB->setValue(_settingsDTO.scalingB);
    ui->chbScalingEnabled->setChecked(_settingsDTO.isScalingEnabled);
}

void cMainWindow::saveSettings(QSettings &_settings, const sSettingsDTO &_settingsDTO)
{
    // General
    _settings.setValue(DEF_INPUT_DEVICE, _settingsDTO.inputDevice);
    _settings.setValue(DEF_BUFFER_SIZE, _settingsDTO.bufferSize);
    _settings.setValue(DEF_NAN_VALUE, _settingsDTO.nanValue);
    _settings.setValue(DEF_AFTER_GLOW, _settingsDTO.isAfterGlow);
    _settings.setValue(DEF_REFRESH_RATE, _settingsDTO.refreshRate);
    _settings.setValue(DEF_SETTINGS_PATH, _settingsDTO.settingsPath);
    // Format
    _settings.beginGroup(DEF_SETTINGS_FORMAT);
    _settings.setValue(DEF_DISPLAY_FORMAT, _settingsDTO.displayFormat);
    _settings.setValue(DEF_BINARY_FORMAT, _settingsDTO.binaryFormat);
    _settings.endGroup();
    // Scaling
    _settings.beginGroup(DEF_SETTINGS_SCALING);
    _settings.setValue(DEF_SCALING_A, _settingsDTO.scalingA);
    _settings.setValue(DEF_SCALING_B, _settingsDTO.scalingB);
    _settings.setValue(DEF_SCALING_ENABLED, _settingsDTO.isScalingEnabled);
    _settings.endGroup();
}

void cMainWindow::loadSettings(QSettings &_settings, sSettingsDTO &_settingsDTO)
{
    sSettingsDTO oSettingsDTO;
    loadDefaultValue(oSettingsDTO);

    // General
    _settingsDTO.inputDevice = _settings.value(DEF_INPUT_DEVICE, oSettingsDTO.inputDevice).toString();
    _settingsDTO.bufferSize = _settings.value(DEF_BUFFER_SIZE, oSettingsDTO.bufferSize).toInt();
    _settingsDTO.nanValue = _settings.value(DEF_NAN_VALUE, oSettingsDTO.nanValue).toInt();
    _settingsDTO.isAfterGlow = _settings.value(DEF_AFTER_GLOW, oSettingsDTO.isAfterGlow).toBool();
    _settingsDTO.refreshRate = _settings.value(DEF_REFRESH_RATE, oSettingsDTO.refreshRate).toInt();
    _settingsDTO.settingsPath = _settings.value(DEF_SETTINGS_PATH, oSettingsDTO.settingsPath).toString();
    // Format
    _settings.beginGroup(DEF_SETTINGS_FORMAT);
    _settingsDTO.displayFormat = _settings.value(DEF_DISPLAY_FORMAT, oSettingsDTO.displayFormat).toString();
    _settingsDTO.binaryFormat = _settings.value(DEF_BINARY_FORMAT, oSettingsDTO.binaryFormat).toString();
    _settings.endGroup();
    // Scaling
    _settings.beginGroup(DEF_SETTINGS_SCALING);
    _settingsDTO.scalingA = _settings.value(DEF_SCALING_A, oSettingsDTO.scalingA).toDouble();
    _settingsDTO.scalingB = _settings.value(DEF_SCALING_B, oSettingsDTO.scalingB).toDouble();
    _settingsDTO.isScalingEnabled = _settings.value(DEF_SCALING_ENABLED, oSettingsDTO.isScalingEnabled).toBool();
    _settings.endGroup();
}

void cMainWindow::closeEvent(QCloseEvent *event)
{
    sSettingsDTO oSettingsDTO;
    readSettings(oSettingsDTO);
    QSettings oSettings(DEF_ORGANIZATION_NAME, DEF_APP_NAME);
    saveSettings(oSettings, oSettingsDTO);
}

void cMainWindow::on_actionSaveSettings_triggered()
{

    QString oFilename = QFileDialog::getSaveFileName(this, tr("Sauvegarder les configurations"), m_settingsPath, tr("Settings (*.ini)"));
    if (oFilename.isEmpty())
    {
        qDebug() << "L'enregistrement des configurations a ete annule.";
        return;
    }

    m_settingsPath = QFileInfo(oFilename).absolutePath();

    sSettingsDTO oSettingsDTO;
    readSettings(oSettingsDTO);
    QSettings oSettings(oFilename, QSettings::IniFormat);
    saveSettings(oSettings, oSettingsDTO);

    qDebug() << "L'enregistrement des configurations s'est bien deroule."
             << "|filename=" << oFilename;
}

void cMainWindow::on_actionLoadSettings_triggered()
{
    QString oFilename = QFileDialog::getOpenFileName(this, tr("Charger les configurations"), m_settingsPath, tr("Settings (*.ini)"));
    if (oFilename.isEmpty())
    {
        qDebug() << "Le chargement des configurations a ete annule.";
        return;
    }

    m_settingsPath = QFileInfo(oFilename).absolutePath();

    sSettingsDTO oSettingsDTO;
    QSettings oSettings(oFilename, QSettings::IniFormat);
    loadSettings(oSettings, oSettingsDTO);
    writeSettings(oSettingsDTO);

    qDebug() << "Le chargement des configurations s'est bien deroule."
             << "|filename=" << oFilename;
}

void cMainWindow::on_actionSettingsPage_triggered()
{
    ui->stackedWidget->setCurrentWidget(ui->pageSettings);
}

void cMainWindow::on_btnSettingsOK_clicked()
{
    ui->stackedWidget->setCurrentWidget(ui->pageHome);
}
...

// cMainWindow.ui (Editer le menu général)
image.png

// cMainWindow.ui (Editer la fiche des configurations)
image.png

Gestion du fichier CMake


// CMakeLists.txt (Editer le fichier CMake)
...
cmake_minimum_required(VERSION 3.10.0)
project(rdvcpp VERSION 0.1.0 LANGUAGES C CXX)

set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)

set(CMAKE_PREFIX_PATH "C:/Qt/6.5.2/msvc2019_64/lib/cmake")

find_package(Qt6 REQUIRED COMPONENTS
    Widgets
)

qt_wrap_ui(UI_FILES
    cMainWindow.ui
)

add_executable(rdvcpp
    main.cpp
    cMainWindow.cpp
    resources.qrc
    ${UI_FILES}
)

target_link_libraries (rdvcpp
    Qt6::Widgets
)
...

Gestion du fichier ressource


// resources.qrc (Editer le fichier ressource)
...
<RCC>
  <qresource prefix="/img">
    <file alias="logo.png">data/img/logo.png</file>
  </qresource>
</RCC>
...

Exécution du projet


// Terminal (Exécuter le projet)
...
rdvcpp.exe
...

// Application (Fiche du menu général)
image.png

// Application (Fiche des configurations)
image.png

// Application (Registre des configurations utilisateur sous Windows)
image.png

// Application (Registre des configurations utilisateur sous Windows)
image.png

// Application (Registre des configurations utilisateur sous Windows)
image.png

// Application (Sauvegarde des configuration dans un fichier)
image.png

Démo de l'application


https://www.youtube.com/watch?v=aHYklSnqhI0


Tracer des données de mesures scientifiques en C++ sous Qt


Qt est une librairie de création d'interfaces homme machine prenant en charge la gestion de dessins graphiques. Le module de dessin QPainter peut être utilisé pour créer un oscilloscope numérique. Dans ce tutoriel, nous utilisons QPainter pour tracer des données de mesures scientifiques provenant de différentes sources de données capteur en répartissant l'affichage dans différentes sections d'une image de type QImage.


Gestion du programme principal


// main.cpp (Editer le programme principal)
...
#include "cMainWindow.h"
#include <QApplication>

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    cMainWindow *oMainWindow = new cMainWindow;
    oMainWindow->resize(640, 480);
    oMainWindow->show();
    return app.exec();
}
...

Gestion de la fenêtre principale


// cMainWindow.h (Editer la fenêtre principale)
...
#pragma once

#include <QMainWindow>
#include "cPlot.h"

namespace Ui
{
    class cMainWindow;
}

class cMainWindow : public QMainWindow
{
    Q_OBJECT

public:
    using sPlot = cPlot::sPlot;
    using sPlots = cPlot::sPlots;

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

private:
    void addManagePlot(const sPlot &_plot);
    void updateManagePlot();

private slots:
    void on_actionChangeBackgroundColor_triggered();
    void on_actionAddPlot_triggered();
    void on_actionPlotPage_triggered();
    void on_actionManagePlotPage_triggered();
    void on_plot_emitResizePlot();

protected:
    void resizeEvent(QResizeEvent *event);
    void timerEvent(QTimerEvent *event);

private:
    Ui::cMainWindow *ui;
    QVector<QImage> m_images;
    int m_refreshTime;
    int m_refreshTimerId;
};
...

// cMainWindow.cpp (Editer la fenêtre principale)
...
#include "cMainWindow.h"
#include "ui_cMainWindow.h"
#include <QColorDialog>
#include <QResizeEvent>
#include <QMessageBox>

static const int DEF_REFRESH_TIME = 100; // 100ms : 10 Hz

cMainWindow::cMainWindow(QWidget *parent)
    : QMainWindow(parent),
      ui(new Ui::cMainWindow),
      m_refreshTime(DEF_REFRESH_TIME)
{
    ui->setupUi(this);
    ui->stackedWidget->setCurrentWidget(ui->pagePlot);
    ui->tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
    ui->tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
    m_refreshTimerId = startTimer(m_refreshTime);
}

cMainWindow::~cMainWindow()
{
    delete ui;
}

void cMainWindow::addManagePlot(const sPlot &_plot)
{
    ui->tableWidget->insertRow(ui->tableWidget->rowCount());
    int iCol = 0;

    QTableWidgetItem *oId = new QTableWidgetItem(QString("%1").arg(_plot.id));
    QTableWidgetItem *oX = new QTableWidgetItem(QString("%1").arg(_plot.x));
    QTableWidgetItem *oY = new QTableWidgetItem(QString("%1").arg(_plot.y));
    QTableWidgetItem *oWidth = new QTableWidgetItem(QString("%1").arg(_plot.width));
    QTableWidgetItem *oHeight = new QTableWidgetItem(QString("%1").arg(_plot.height));
    QTableWidgetItem *oDivision = new QTableWidgetItem(QString("%1").arg(_plot.nDivision));

    ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, iCol++, oId);
    ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, iCol++, oX);
    ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, iCol++, oY);
    ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, iCol++, oWidth);
    ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, iCol++, oHeight);
    ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, iCol++, oDivision);
}

void cMainWindow::updateManagePlot()
{
    if (ui->plot->getSize())
    {
        ui->tableWidget->setRowCount(0);

        for (const auto &oPlot : ui->plot->getPlots())
        {
            addManagePlot(oPlot);
        }
    }
}

void cMainWindow::on_actionChangeBackgroundColor_triggered()
{
    QColor oldColor = ui->plot->getBackgroundColor();
    QColor newColor = QColorDialog::getColor(oldColor, this, tr("Sélectionner une couleur"));
    if (!newColor.isValid())
    {
        qDebug() << "Aucune couleur n'a ete selectionne.";
        return;
    }
    if (newColor != oldColor)
    {
        ui->plot->setBackgroundColor(newColor);
    }
}

void cMainWindow::on_actionAddPlot_triggered()
{
    if (ui->plot->isMax())
    {
        QMessageBox::critical(this, tr("Messages d'erreurs"),
                              tr("Le nombre maximal d'oscilloscopes est atteint."));
        return;
    }

    ui->plot->addPlot();
    updateManagePlot();
}

void cMainWindow::on_actionPlotPage_triggered()
{
    ui->stackedWidget->setCurrentWidget(ui->pagePlot);
}

void cMainWindow::on_actionManagePlotPage_triggered()
{
    ui->stackedWidget->setCurrentWidget(ui->pageManagePlot);
}

void cMainWindow::on_plot_emitResizePlot()
{
    updateManagePlot();
}

void cMainWindow::resizeEvent(QResizeEvent *event)
{
    QWidget *oCurrentWidget = ui->stackedWidget->currentWidget();
    if (oCurrentWidget != ui->pagePlot)
    {
        ui->stackedWidget->setCurrentWidget(ui->pagePlot);
        ui->stackedWidget->setCurrentWidget(oCurrentWidget);
    }
}

void cMainWindow::timerEvent(QTimerEvent *event)
{
    ui->plot->render();
    ui->plot->repaint();
}
...

// cMainWindow.ui (Editer le menu général)
image.png

// cMainWindow.ui (Editer la fiche de gestion des oscilloscopes)
image.png

// cMainWindow.ui (Editer la fiche d'affichage des oscilloscopes)
image.png

Gestion des oscilloscopes numériques


// cPlot.h (Editer les oscilloscopes numériques)
...
#pragma once

#include <QWidget>
#include <QImage>
#include <QVector>
#include <QMap>

class QMenu;

class cPlot : public QWidget
{
    Q_OBJECT

public:
    struct sPlot
    {
        int id;
        int x;
        int y;
        int width;
        int height;
        int nDivision;
    };

    using sPlots = QVector<sPlot>;
    using sData = QVector<QPointF>;
    using sImages = QMap<int, QImage>;
    using sDatas = QMap<int, sData>;
    using sXShiftValues = QMap<int, double>;
    using sAnimatePlots = QMap<int, bool>;
    using sOffsets = QMap<int, QPointF>;

public:
    explicit cPlot(QWidget *parent = nullptr);
    ~cPlot();
    void render();
    void setBackgroundColor(const QColor _color);
    void addPlot();
    bool isMax() const;

    const QColor &getBackgroundColor() const { return m_backgroundColor; }
    sPlots &getPlots() { return m_plots; }
    const sPlots &getPlots() const { return m_plots; }
    int getSize() const { return m_plots.size(); }

private:
    void addPlotImage(int _plotId, int _width, int _height);
    void addPlotXShiftValue(int _plotId);
    void addPlotOffset(int _plotId);
    //
    void initScreenSize();
    void createPlotMenu();
    bool loadAllPlots(sPlots &_plots) const;
    bool loadPlot(int _x, int _y, sPlot &_plot) const;
    bool isActivePlot(const QPoint &_pos) const;
    bool isActivePlot(const QPoint &_pos, int &_plotId) const;
    bool hasPlotImage(int _plotId) const;
    bool hasPlotData(int _plotId) const;
    bool hasPlotXShiftValue(int _plotId) const;
    bool hasPlotOffset(int _plotId) const;
    bool isAnimatePlot(int _plotId) const;
    void generatePlotData(sData &_datas, double _xShiftValue);
    QPointF pointToPlot(const QPointF &_point) const;
    void drawText(QPaintDevice *_image, int _x, int _y, int _width, int _height, int _fontSize, bool _isBold, const QColor &_color, int _align, const QString &_text);
    //
    void drawPlotBackgroundColor();
    void drawPlotsBackgroundColor();
    void drawPlotsDefaultBorder();
    void drawPlotsBorder();
    void drawTextNoPlots();
    void drawActivePlots();
    void drawPlotsGrids();
    void drawPlotsGridsDivs();
    void drawPlotsGridsTicks();
    void drawPlotsGridsTicksDivs();
    void drawPlotsGridsDivsValues();
    void drawAnimatePlots();
    void drawPlotsDatas();
    void drawPlots();

protected:
    void paintEvent(QPaintEvent *event);
    void resizeEvent(QResizeEvent *event);
    void mousePressEvent(QMouseEvent *event);
    void wheelEvent(QWheelEvent *event);

private:
    void onResizePlot(const QSize &_size);
    void onPlotMenu(const QPoint &_pos);
    void onAddPlotData();
    void onAnimatePlotData();
    void onPlotClick(const QPoint &_pos);

signals:
    void emitResizePlot(QSize _size);
    void emitPlotMenu(QPoint _pos);
    void emitPlotClick(QPoint _pos);

private:
    sPlots m_plots;
    sImages m_images;
    sDatas m_datas;
    sXShiftValues m_xShiftValues;
    sAnimatePlots m_isAnimatePlots;
    sOffsets m_offsets;
    //
    QSize m_screenSize;
    QImage m_image;
    QMenu *m_plotMenu;
    QColor m_backgroundColor;
    //
    double m_scaleFactor;
};
...

// cPlot.cpp (Editer les oscilloscopes numériques)
...
#include "cPlot.h"
#include "cMainWindow.h"
#include <QPainter>
#include <QPaintEvent>
#include <QMenu>
#include <QApplication>
#include <QScreen>
#include <QMessageBox>

// debug
static const bool DEF_DEBUG_ON = false;

// plot
static const int DEF_PLOT_MAX = 9;
static const int DEF_PLOT_MARGIN_LEFT = 70;
static const int DEF_PLOT_MARGIN_BOTTOM = 50;
static const int DEF_PLOT_MARGIN_TOP = 30;
static const int DEF_PLOT_MARGIN_RIGHT = 10;
static const int DEF_PLOT_PIXEL_PER_DIV = 40;
static const int DEF_PLOT_TEXT_FONT_SIZE = 10;
static const int DEF_PLOT_DATA_NUMBER = 1024;
static const double DEF_PLOT_X_MIN = -45;
static const double DEF_PLOT_X_MAX = 2000;
static const double DEF_PLOT_X_SHIT_VALUE = 45;
static const double DEF_PLOT_X_SHIT_PER_TIME = 5;
static const QPointF DEF_PLOT_OFFSET = QPointF(-5, -3);

static const double DEF_PLOT_X_PER_DIV = 90.0 / 2.0;
static const double DEF_PLOT_Y_PER_DIV = 1.0 / 2.0;

static const int DEF_PLOT_DEFAULT_BORDER_THICKNESS = 1;
static const int DEF_PLOT_BORDER_THICKNESS = 2;
static const int DEF_PLOT_DATA_THICKNESS = 2;

static const QColor DEF_PLOT_BACKGROUND_COLOR = "#FF000000";
static const QColor DEF_PLOT_DEFAULT_BORDER_COLOR = "#FF404040";
static const QColor DEF_PLOT_BORDER_COLOR = "#FFAA0000";
static const QColor DEF_PLOT_TEXT_COLOR = "#FFAA0000";
static const QColor DEF_NO_PLOT_TEXT_COLOR = "#FF808080";
static const QColor DEF_NO_PLOT_BACKGROUND_COLOR = "#80808080";
static const QColor DEF_PLOT_DATA_COLOR = "#FFAA00AA";

static const QString DEF_PLOT_FONT_FAMILY = "Arial";

static const QString DEF_NO_PLOT_TEXT = "[NO-PLOT]";
static const QString DEF_NO_PLOT_ITEM_TEXT = "NO-PLOT-[%1]";
static const QString DEF_PLOT_ITEM_TEXT = "PLOT-[%1]";

// axis
static const int DEF_AXIS_DIV_PER_GRID = 2;

static const int DEF_AXIS_THICKNESS = 2;
static const int DEF_AXIS_GRID_THICKNESS = 1;
static const int DEF_AXIS_TICK_THICKNESS = 1;
static const int DEF_AXIS_TICK_DIV_THICKNESS = 2;
static const int DEF_AXIS_DIV_PER_GRID_THICKNESS = 2;

static const int DEF_AXIS_TICK_SIZE = 5;
static const int DEF_AXIS_TICK_GRID_SIZE = 10;
static const int DEF_AXIS_DIV_VALUE_FONT_SIZE = 10;

static const double DEF_AXIS_TICK_PER_DIV = 5.0;
static const double DEF_AXIS_TICK_DIV_PER_GRID = 2.0;

static const QColor DEF_AXIS_COLOR = "#FF808080";
static const QColor DEF_AXIS_TICK_COLOR = "#FFAA0000";
static const QColor DEF_AXIS_TICK_VALUE_COLOR = "#FFAA0000";
static const QColor DEF_AXIS_GRID_COLOR = "#FF333333";
static const QColor DEF_AXIS_DIV_PER_GRID_COLOR = "#FF222222";

cPlot::cPlot(QWidget *parent)
    : QWidget(parent),
      m_image(parent->width(), parent->height(), QImage::Format_ARGB32),
      m_scaleFactor(1.0),
      m_backgroundColor(DEF_PLOT_BACKGROUND_COLOR)
{
    initScreenSize();
    createPlotMenu();
    connect(this, &cPlot::emitResizePlot, this, &cPlot::onResizePlot);
    connect(this, &cPlot::emitPlotMenu, this, &cPlot::onPlotMenu);
    connect(this, &cPlot::emitPlotClick, this, &cPlot::onPlotClick);
}

cPlot::~cPlot()
{
}

void cPlot::render()
{
    drawPlotBackgroundColor();
    drawPlotsBackgroundColor();
    drawPlotsDefaultBorder();
    drawTextNoPlots();
    drawActivePlots();
    drawPlotsGrids();
    drawPlotsGridsDivs();
    drawPlotsGridsTicks();
    drawPlotsGridsTicksDivs();
    drawPlotsGridsDivsValues();
    drawAnimatePlots();
    drawPlotsDatas();
    drawPlotsBorder();
    drawPlots();
}

void cPlot::setBackgroundColor(const QColor _color)
{
    if (m_backgroundColor != _color)
    {
        m_backgroundColor = _color;
    }
}

void cPlot::addPlot()
{
    if (isMax())
        return;

    m_plots.push_back(sPlot());

    int oSzie = m_plots.size();
    double oSqrt = sqrt((double)oSzie);
    int oCeil = (int)ceil(oSqrt);
    int oDivision = oCeil;
    int oWidth = width() / oDivision;
    int oHeight = height() / oDivision;

    for (int iCount = 0; iCount < m_plots.size(); iCount++)
    {
        int oId = iCount;
        int iX = iCount % oDivision;
        int iY = iCount / oDivision;
        int oX = iX * oWidth;
        int oY = iY * oHeight;

        sPlot &oPlot = m_plots[iCount];
        oPlot.id = oId;
        oPlot.x = oX;
        oPlot.y = oY;
        oPlot.width = oWidth;
        oPlot.height = oHeight;
        oPlot.nDivision = oDivision;

        addPlotImage(oPlot.id, oWidth, oHeight);
        addPlotXShiftValue(oPlot.id);
        addPlotOffset(oPlot.id);
    }

    if (m_plots.size() && DEF_DEBUG_ON)
    {
        qDebug() << "\nLe calcul des parametres de l'oscillosocpe.";

        for (const auto &oPlot : m_plots)
        {
            qDebug() << "|id=" << oPlot.id
                     << "|x=" << oPlot.x
                     << "|y=" << oPlot.y
                     << "|width=" << oPlot.width
                     << "|height=" << oPlot.height
                     << "|WIDTH=" << width()
                     << "|HEIGHT=" << height()
                     << "|nSCOPE=" << oPlot.nDivision * oPlot.nDivision;
        }
    }
}

bool cPlot::isMax() const
{
    return (m_plots.size() >= DEF_PLOT_MAX);
}

void cPlot::addPlotImage(int _plotId, int _width, int _height)
{
    if (hasPlotImage(_plotId))
    {
        QImage &oImage = m_images[_plotId];
        oImage = oImage.scaled(QSize(_width, _height) * m_scaleFactor);
    }
    else
    {
        m_images[_plotId] = QImage(QSize(_width, _height) * m_scaleFactor, QImage::Format_ARGB32);
    }
}

void cPlot::addPlotXShiftValue(int _plotId)
{
    if (!hasPlotXShiftValue(_plotId))
    {
        m_xShiftValues[_plotId] = _plotId * DEF_PLOT_X_SHIT_VALUE;
    }
}

void cPlot::addPlotOffset(int _plotId)
{
    if (!hasPlotOffset(_plotId))
    {
        m_offsets[_plotId] = DEF_PLOT_OFFSET;
    }
}

void cPlot::initScreenSize()
{
    m_screenSize = QApplication::primaryScreen()->geometry().size();
    qDebug() << Q_FUNC_INFO
             << "|screenSize=" << m_screenSize;
}

void cPlot::createPlotMenu()
{
    m_plotMenu = new QMenu(this);
    QAction *oAction;

    oAction = new QAction("Ajouter des données");
    connect(oAction, &QAction::triggered, this, &cPlot::onAddPlotData);
    m_plotMenu->addAction(oAction);

    oAction = new QAction("Animer les données");
    connect(oAction, &QAction::triggered, this, &cPlot::onAnimatePlotData);
    m_plotMenu->addAction(oAction);
}

bool cPlot::loadAllPlots(sPlots &_plots) const
{
    if (m_plots.isEmpty())
        return false;

    const sPlot &oPlot = m_plots[0];
    int oDivision = oPlot.nDivision;
    int oWidth = oPlot.width;
    int oHeight = oPlot.height;
    int oSize = oDivision * oDivision;

    for (int iCount = 0; iCount < oSize; iCount++)
    {
        int oId = iCount;
        int iX = iCount % oDivision;
        int iY = iCount / oDivision;
        int oX = iX * oWidth;
        int oY = iY * oHeight;

        sPlot iPlot;
        iPlot.id = oId;
        iPlot.x = oX;
        iPlot.y = oY;
        iPlot.width = oWidth;
        iPlot.height = oHeight;
        iPlot.nDivision = oDivision;

        _plots.push_back(iPlot);
    }

    if (_plots.size() && DEF_DEBUG_ON)
    {
        qDebug() << "\nLe calcul des parametres de l'oscillosocpe [par défaut].";

        for (const auto &iPlot : _plots)
        {
            qDebug() << "|id=" << iPlot.id
                     << "|x=" << iPlot.x
                     << "|y=" << iPlot.y
                     << "|width=" << iPlot.width
                     << "|height=" << iPlot.height
                     << "|WIDTH=" << width()
                     << "|HEIGHT=" << height()
                     << "|nSCOPE=" << iPlot.nDivision * iPlot.nDivision;
        }
    }

    return true;
}

bool cPlot::loadPlot(int _x, int _y, sPlot &_plot) const
{
    if (m_plots.size())
    {
        sPlots oPlots;
        if (loadAllPlots(oPlots))
        {
            for (const auto &oPlot : oPlots)
            {
                QRegion oRegion(oPlot.x, oPlot.y, oPlot.width, oPlot.height);
                if (oRegion.contains(QPoint(_x, _y)))
                {
                    _plot = oPlot;
                    return true;
                }
            }
        }
    }
    return false;
}

bool cPlot::isActivePlot(const QPoint &_pos) const
{
    for (const auto &oPlot : m_plots)
    {
        QRegion oRegion(oPlot.x, oPlot.y, oPlot.width, oPlot.height);
        if (oRegion.contains(_pos))
        {
            return true;
        }
    }
    return false;
}

bool cPlot::isActivePlot(const QPoint &_pos, int &_plotId) const
{
    for (const auto &oPlot : m_plots)
    {
        QRegion oRegion(oPlot.x, oPlot.y, oPlot.width, oPlot.height);
        if (oRegion.contains(_pos))
        {
            _plotId = oPlot.id;
            return true;
        }
    }
    return false;
}

bool cPlot::hasPlotImage(int _plotId) const
{
    for (const auto &oPlotId : m_images.keys())
    {
        if (oPlotId == _plotId)
        {
            return true;
        }
    }
    return false;
}

bool cPlot::hasPlotData(int _plotId) const
{
    for (const auto &oPlotId : m_datas.keys())
    {
        if (oPlotId == _plotId)
        {
            return true;
        }
    }
    return false;
}

bool cPlot::hasPlotXShiftValue(int _plotId) const
{
    for (const auto &oPlotId : m_xShiftValues.keys())
    {
        if (oPlotId == _plotId)
        {
            return true;
        }
    }
    return false;
}

bool cPlot::hasPlotOffset(int _plotId) const
{
    for (const auto &oPlotId : m_offsets.keys())
    {
        if (oPlotId == _plotId)
        {
            return true;
        }
    }
    return false;
}

bool cPlot::isAnimatePlot(int _plotId) const
{
    for (const auto &oPlotId : m_isAnimatePlots.keys())
    {
        if (oPlotId == _plotId)
        {
            return true;
        }
    }
    return false;
}

void cPlot::generatePlotData(sData &_datas, double _xShiftValue)
{
    for (int iX = 0; iX < DEF_PLOT_DATA_NUMBER; iX++)
    {
        double oX = DEF_PLOT_X_MIN + ((DEF_PLOT_X_MAX - DEF_PLOT_X_MIN) * iX) / DEF_PLOT_DATA_NUMBER;
        double oY = 2 + qSin(qDegreesToRadians(oX));
        oX = oX - _xShiftValue;
        QPointF oData(oX, oY);
        _datas.push_back(oData);
    }
}

QPointF cPlot::pointToPlot(const QPointF &_point) const
{
    double oX = _point.x() * DEF_PLOT_PIXEL_PER_DIV / DEF_PLOT_X_PER_DIV;
    double oY = _point.y() * DEF_PLOT_PIXEL_PER_DIV / DEF_PLOT_Y_PER_DIV;
    return QPointF(oX, oY);
}

void cPlot::drawText(QPaintDevice *_image, int _x, int _y, int _width, int _height, int _fontSize, bool _isBold, const QColor &_color, int _align, const QString &_text)
{
    QPainter oPainter;
    oPainter.begin(_image);
    oPainter.setRenderHint(QPainter::TextAntialiasing);
    oPainter.setPen(_color);

    QFont oFont = oPainter.font();
    oFont.setPointSize(_fontSize);
    oFont.setStyleHint(QFont::Helvetica, QFont::PreferAntialias);
    oFont.setBold(_isBold);
    oFont.setFamily(DEF_PLOT_FONT_FAMILY);

    oPainter.setFont(oFont);
    oPainter.drawText(QRect(_x, _y, _width, _height), _align, _text);
    oPainter.end();
}

void cPlot::drawPlotBackgroundColor()
{
    QPainter oPainter;
    oPainter.begin(&m_image);
    oPainter.fillRect(0, 0, m_image.width(), m_image.height(), m_backgroundColor);
    oPainter.end();
}

void cPlot::drawPlotsBackgroundColor()
{
    for (sPlot &oPlot : m_plots)
    {
        if (hasPlotImage(oPlot.id))
        {
            QImage &oImage = m_images[oPlot.id];
            QPainter oPainter;
            oPainter.begin(&oImage);
            oPainter.fillRect(0, 0, oPlot.width, oPlot.height, m_backgroundColor);
            oPainter.end();
        }
    }
}

void cPlot::drawPlotsDefaultBorder()
{
    sPlots oPlots;
    if (loadAllPlots(oPlots))
    {
        for (int iCount = 0; iCount < oPlots.size(); iCount++)
        {
            if (iCount < m_plots.size())
            {
                sPlot &oPlot = m_plots[iCount];
                if (hasPlotImage(oPlot.id))
                {
                    QImage &oImage = m_images[oPlot.id];
                    QPainter oPainter;
                    oPainter.begin(&oImage);
                    oPainter.setPen(QPen(DEF_PLOT_DEFAULT_BORDER_COLOR, DEF_PLOT_DEFAULT_BORDER_THICKNESS, Qt::DashLine));
                    oPainter.drawRect(0, 0, oPlot.width, oPlot.height);
                    oPainter.end();
                }
            }
            else
            {
                sPlot &oPlot = oPlots[iCount];
                QPainter oPainter;
                oPainter.begin(&m_image);
                oPainter.setPen(QPen(DEF_PLOT_DEFAULT_BORDER_COLOR, DEF_PLOT_DEFAULT_BORDER_THICKNESS, Qt::DashLine));
                oPainter.drawRect(oPlot.x, oPlot.y, oPlot.width, oPlot.height);
                oPainter.end();
            }
        }
    }
}

void cPlot::drawPlotsBorder()
{
    for (auto &oPlot : m_plots)
    {
        if (hasPlotImage(oPlot.id))
        {
            QImage &oImage = m_images[oPlot.id];
            int oX = DEF_PLOT_MARGIN_LEFT;
            int oY = DEF_PLOT_MARGIN_TOP;
            int oWidth = oPlot.width - (DEF_PLOT_MARGIN_LEFT + DEF_PLOT_MARGIN_RIGHT);
            int oHeight = oPlot.height - (DEF_PLOT_MARGIN_TOP + DEF_PLOT_MARGIN_BOTTOM);

            QPainter oPainter;
            oPainter.begin(&oImage);
            oPainter.setPen(QPen(DEF_PLOT_BORDER_COLOR, DEF_PLOT_BORDER_THICKNESS));
            oPainter.drawRect(oX, oY, oWidth, oHeight);
            oPainter.end();
        }
    }
}

void cPlot::drawTextNoPlots()
{
    if (m_plots.isEmpty())
    {
        drawText(&m_image, 0, 0, width(), height(), DEF_PLOT_TEXT_FONT_SIZE, false,
                 DEF_NO_PLOT_TEXT_COLOR, Qt::AlignCenter, DEF_NO_PLOT_TEXT);
        return;
    }

    sPlots oPlots;
    if (loadAllPlots(oPlots))
    {
        for (int iCount = 0; iCount < oPlots.size(); iCount++)
        {
            QString oText;

            if (iCount < m_plots.size())
            {
                sPlot &oPlot = m_plots[iCount];
                if (hasPlotImage(oPlot.id))
                {
                    QImage &oImage = m_images[oPlot.id];
                    oText = QString(DEF_PLOT_ITEM_TEXT).arg(oPlot.id);
                    drawText(&oImage, 0, 0, oPlot.width, DEF_PLOT_MARGIN_TOP, DEF_PLOT_TEXT_FONT_SIZE, false,
                             DEF_PLOT_TEXT_COLOR, Qt::AlignCenter, oText);
                }
            }
            else
            {
                sPlot &oPlot = oPlots[iCount];
                oText = QString(DEF_NO_PLOT_ITEM_TEXT).arg(oPlot.id);
                drawText(&m_image, oPlot.x, oPlot.y, oPlot.width, oPlot.height, DEF_PLOT_TEXT_FONT_SIZE, false,
                         DEF_NO_PLOT_TEXT_COLOR, Qt::AlignCenter, oText);
            }
        }
    }
}

void cPlot::drawActivePlots()
{
    if (m_plots.size())
    {
        QRegion oRegion(0, 0, width(), height());
        for (const auto &oPlot : m_plots)
        {
            QRegion iRegion(oPlot.x, oPlot.y, oPlot.width, oPlot.height);
            oRegion = oRegion.subtracted(iRegion);
        }
        QPainter oPainter;
        oPainter.begin(&m_image);
        oPainter.setClipRegion(oRegion);
        oPainter.fillRect(0, 0, width(), height(), DEF_NO_PLOT_BACKGROUND_COLOR);
        oPainter.end();
    }
}

void cPlot::drawPlotsGrids()
{
    for (auto &oPlot : m_plots)
    {
        if (hasPlotImage(oPlot.id))
        {
            QImage &oImage = m_images[oPlot.id];
            const double &xShiftValue = m_xShiftValues[oPlot.id];

            int oXmax = oPlot.width - (DEF_PLOT_MARGIN_LEFT + DEF_PLOT_MARGIN_RIGHT);
            int oYmax = oPlot.height - (DEF_PLOT_MARGIN_TOP + DEF_PLOT_MARGIN_BOTTOM);
            QRect oRect(DEF_PLOT_MARGIN_LEFT, DEF_PLOT_MARGIN_TOP, oXmax, oYmax);

            for (int iX = 0; iX < 3 * oXmax; iX += DEF_PLOT_PIXEL_PER_DIV)
            {
                QPainter oPainter;
                oPainter.begin(&oImage);
                oPainter.setClipRect(oRect);
                oPainter.translate(oRect.bottomLeft());
                oPainter.scale(1.0, -1.0);

                int oDx = iX - xShiftValue;

                oPainter.setPen(QPen(DEF_AXIS_GRID_COLOR, DEF_AXIS_GRID_THICKNESS, Qt::DotLine));
                oPainter.drawLine(oDx, 0, oDx, oYmax);
                oPainter.end();
            }

            for (int iY = 0; iY < oYmax; iY += DEF_PLOT_PIXEL_PER_DIV)
            {
                QPainter oPainter;
                oPainter.begin(&oImage);
                oPainter.setClipRect(oRect);
                oPainter.translate(oRect.bottomLeft());
                oPainter.scale(1.0, -1.0);

                oPainter.setPen(QPen(DEF_AXIS_GRID_COLOR, DEF_AXIS_GRID_THICKNESS, Qt::DotLine));
                oPainter.drawLine(0, iY, oXmax, iY);
                oPainter.end();
            }
        }
    }
}

void cPlot::drawPlotsGridsDivs()
{
    for (auto &oPlot : m_plots)
    {
        if (hasPlotImage(oPlot.id))
        {
            QImage &oImage = m_images[oPlot.id];
            const double &xShiftValue = m_xShiftValues[oPlot.id];

            int oXmax = oPlot.width - (DEF_PLOT_MARGIN_LEFT + DEF_PLOT_MARGIN_RIGHT);
            int oYmax = oPlot.height - (DEF_PLOT_MARGIN_TOP + DEF_PLOT_MARGIN_BOTTOM);
            QRect oRect(DEF_PLOT_MARGIN_LEFT, DEF_PLOT_MARGIN_TOP, oXmax, oYmax);
            int oDiv = DEF_AXIS_DIV_PER_GRID * DEF_PLOT_PIXEL_PER_DIV;

            for (int iX = 0; iX < 3 * oXmax; iX += oDiv)
            {
                QPainter oPainter;
                oPainter.begin(&oImage);
                oPainter.setClipRect(oRect);
                oPainter.translate(oRect.bottomLeft());
                oPainter.scale(1.0, -1.0);

                int oDx = iX - xShiftValue;

                oPainter.setPen(QPen(DEF_AXIS_DIV_PER_GRID_COLOR, DEF_AXIS_DIV_PER_GRID_THICKNESS));
                oPainter.drawLine(oDx, 0, oDx, oYmax);
                oPainter.end();
            }

            for (int iY = 0; iY < oYmax; iY += oDiv)
            {
                QPainter oPainter;
                oPainter.begin(&oImage);
                oPainter.setClipRect(oRect);
                oPainter.translate(oRect.bottomLeft());
                oPainter.scale(1.0, -1.0);

                oPainter.setPen(QPen(DEF_AXIS_DIV_PER_GRID_COLOR, DEF_AXIS_DIV_PER_GRID_THICKNESS));
                oPainter.drawLine(0, iY, oXmax, iY);
                oPainter.end();
            }
        }
    }
}

void cPlot::drawPlotsGridsTicks()
{
    for (auto &oPlot : m_plots)
    {
        if (hasPlotImage(oPlot.id))
        {
            QImage &oImage = m_images[oPlot.id];
            const double &xShiftValue = m_xShiftValues[oPlot.id];

            int oXmax = oPlot.width - (DEF_PLOT_MARGIN_LEFT + DEF_PLOT_MARGIN_RIGHT);
            int oYmax = oPlot.height - (DEF_PLOT_MARGIN_TOP + DEF_PLOT_MARGIN_BOTTOM);
            QRect oRect(DEF_PLOT_MARGIN_LEFT, DEF_PLOT_MARGIN_TOP, oXmax, oYmax);
            int oTick = DEF_PLOT_PIXEL_PER_DIV / DEF_AXIS_TICK_PER_DIV;

            for (int iX = 0; iX < 3 * oXmax; iX += oTick)
            {
                QPainter oPainter;
                oPainter.begin(&oImage);
                oPainter.setClipRect(oRect);
                oPainter.translate(oRect.bottomLeft());
                oPainter.scale(1.0, -1.0);

                int oDx = iX - xShiftValue;

                oPainter.setPen(QPen(DEF_AXIS_TICK_COLOR, DEF_AXIS_TICK_THICKNESS, Qt::SolidLine));
                oPainter.drawLine(oDx, -DEF_AXIS_TICK_SIZE, oDx, DEF_AXIS_TICK_SIZE);
                oPainter.end();
            }

            for (int iY = 0; iY < oYmax; iY += oTick)
            {
                QPainter oPainter;
                oPainter.begin(&oImage);
                oPainter.setClipRect(oRect);
                oPainter.translate(oRect.bottomLeft());
                oPainter.scale(1.0, -1.0);

                oPainter.setPen(QPen(DEF_AXIS_TICK_COLOR, DEF_AXIS_TICK_THICKNESS, Qt::SolidLine));
                oPainter.drawLine(-DEF_AXIS_TICK_SIZE, iY, DEF_AXIS_TICK_SIZE, iY);
                oPainter.end();
            }
        }
    }
}

void cPlot::drawPlotsGridsTicksDivs()
{
    for (auto &oPlot : m_plots)
    {
        if (hasPlotImage(oPlot.id))
        {
            QImage &oImage = m_images[oPlot.id];
            const double &xShiftValue = m_xShiftValues[oPlot.id];

            int oXmax = oPlot.width - (DEF_PLOT_MARGIN_LEFT + DEF_PLOT_MARGIN_RIGHT);
            int oYmax = oPlot.height - (DEF_PLOT_MARGIN_TOP + DEF_PLOT_MARGIN_BOTTOM);
            QRect oRect(DEF_PLOT_MARGIN_LEFT, DEF_PLOT_MARGIN_TOP, oXmax, oYmax);
            int oDiv = DEF_PLOT_PIXEL_PER_DIV * DEF_AXIS_TICK_DIV_PER_GRID;

            for (int iX = 0; iX < 3 * oXmax; iX += oDiv)
            {
                QPainter oPainter;
                oPainter.begin(&oImage);
                oPainter.setClipRect(oRect);
                oPainter.translate(oRect.bottomLeft());
                oPainter.scale(1.0, -1.0);

                int oDx = iX - xShiftValue;

                oPainter.setPen(QPen(DEF_AXIS_TICK_COLOR, DEF_AXIS_TICK_DIV_THICKNESS, Qt::SolidLine));
                oPainter.drawLine(oDx, -DEF_AXIS_TICK_GRID_SIZE, oDx, DEF_AXIS_TICK_GRID_SIZE);
                oPainter.end();
            }

            oDiv = DEF_PLOT_PIXEL_PER_DIV;
            for (int iY = 0; iY < oYmax; iY += oDiv)
            {
                QPainter oPainter;
                oPainter.begin(&oImage);
                oPainter.setClipRect(oRect);
                oPainter.translate(oRect.bottomLeft());
                oPainter.scale(1.0, -1.0);

                oPainter.setPen(QPen(DEF_AXIS_TICK_COLOR, DEF_AXIS_TICK_DIV_THICKNESS, Qt::SolidLine));
                oPainter.drawLine(-DEF_AXIS_TICK_GRID_SIZE, iY, DEF_AXIS_TICK_GRID_SIZE, iY);
                oPainter.end();
            }
        }
    }
}

void cPlot::drawPlotsGridsDivsValues()
{
    for (auto &oPlot : m_plots)
    {
        if (hasPlotImage(oPlot.id))
        {
            QImage &oImage = m_images[oPlot.id];
            const double &xShiftValue = m_xShiftValues[oPlot.id];

            int oXmax = oPlot.width - (DEF_PLOT_MARGIN_LEFT + DEF_PLOT_MARGIN_RIGHT);
            int oYmax = oPlot.height - (DEF_PLOT_MARGIN_TOP + DEF_PLOT_MARGIN_BOTTOM);
            QRect oRect(DEF_PLOT_MARGIN_LEFT, DEF_PLOT_MARGIN_TOP, oXmax, oYmax);
            QRect oRect2(DEF_PLOT_MARGIN_LEFT - 10, DEF_PLOT_MARGIN_TOP, oXmax + DEF_PLOT_MARGIN_RIGHT, oYmax + DEF_PLOT_MARGIN_BOTTOM);
            int oDiv = DEF_PLOT_PIXEL_PER_DIV * DEF_AXIS_TICK_DIV_PER_GRID;

            for (int iX = 0; iX < 3 * oXmax; iX += oDiv)
            {
                double oX = iX * DEF_PLOT_X_PER_DIV / DEF_PLOT_PIXEL_PER_DIV;

                QString oValue = QString("%1").arg(oX);

                QPainter oPainter;
                oPainter.begin(&oImage);
                oPainter.setClipRect(oRect2);
                oPainter.translate(oRect.bottomLeft());
                oPainter.scale(1.0, 1.0);
                oPainter.setPen(DEF_AXIS_TICK_VALUE_COLOR);

                int oDx = iX - xShiftValue;

                QFont oFont = oPainter.font();
                oFont.setPointSize(DEF_AXIS_DIV_VALUE_FONT_SIZE);
                oFont.setStyleHint(QFont::Helvetica, QFont::PreferAntialias);
                oFont.setFamily(DEF_PLOT_FONT_FAMILY);
                oPainter.setFont(oFont);

                oPainter.drawText(oDx - 40, 10, 80, 25, Qt::AlignHCenter | Qt::AlignTop, oValue);
                oPainter.end();
            }

            oDiv = DEF_PLOT_PIXEL_PER_DIV;
            for (int iY = 0; iY < oYmax; iY += oDiv)
            {
                double oY = iY * DEF_PLOT_Y_PER_DIV / DEF_PLOT_PIXEL_PER_DIV;

                QString oValue = QString("%1").arg(oY);

                QPainter oPainter;
                oPainter.begin(&oImage);
                oPainter.translate(oRect.bottomLeft());
                oPainter.scale(1.0, 1.0);
                oPainter.setPen(DEF_AXIS_TICK_VALUE_COLOR);

                QFont oFont = oPainter.font();
                oFont.setPointSize(DEF_AXIS_DIV_VALUE_FONT_SIZE);
                oFont.setStyleHint(QFont::Helvetica, QFont::PreferAntialias);
                oFont.setFamily(DEF_PLOT_FONT_FAMILY);
                oPainter.setFont(oFont);

                oPainter.drawText(-80 - 15, -iY - 20, 80, 40, Qt::AlignVCenter | Qt::AlignRight, oValue);
                oPainter.end();
            }
        }
    }
}

void cPlot::drawAnimatePlots()
{
    for (const auto &oPlotId : m_isAnimatePlots.keys())
    {
        const bool &isAnimatePlot = m_isAnimatePlots[oPlotId];

        if (hasPlotXShiftValue(oPlotId) && isAnimatePlot)
        {
            sData oData;
            double &xShiftValues = m_xShiftValues[oPlotId];
            xShiftValues += DEF_PLOT_X_SHIT_PER_TIME;
            generatePlotData(oData, xShiftValues);
            m_datas[oPlotId] = oData;
        }
    }
}

void cPlot::drawPlotsDatas()
{
    for (const auto &oPlotId : m_datas.keys())
    {
        const sData &oDatas = m_datas[oPlotId];
        const sPlot &oPlot = m_plots[oPlotId];
        QImage &oImage = m_images[oPlot.id];

        int oXmax = oPlot.width - (DEF_PLOT_MARGIN_LEFT + DEF_PLOT_MARGIN_RIGHT);
        int oYmax = oPlot.height - (DEF_PLOT_MARGIN_TOP + DEF_PLOT_MARGIN_BOTTOM);
        QRect oRect(DEF_PLOT_MARGIN_LEFT, DEF_PLOT_MARGIN_TOP, oXmax, oYmax);

        for (int iX = 0; iX < oDatas.size() - 1; iX++)
        {
            QPointF oPoint1 = pointToPlot(oDatas[iX]);
            QPointF oPoint2 = pointToPlot(oDatas[iX + 1]);

            QPainter oPainter;
            oPainter.begin(&oImage);
            oPainter.setClipRect(oRect);
            oPainter.translate(oRect.bottomLeft());
            oPainter.scale(1.0, -1.0);

            oPainter.setPen(QPen(DEF_PLOT_DATA_COLOR, DEF_PLOT_DATA_THICKNESS, Qt::SolidLine));
            oPainter.drawLine(oPoint1, oPoint2);
            oPainter.end();
        }
    }
}

void cPlot::drawPlots()
{
    for (auto &oPlot : m_plots)
    {
        if (hasPlotImage(oPlot.id))
        {
            QImage &oImage = m_images[oPlot.id];
            QPainter oPainter;
            oPainter.begin(&m_image);
            oPainter.drawImage(oPlot.x, oPlot.y, oImage.scaled(QSize(oPlot.width, oPlot.height) * m_scaleFactor));
            oPainter.end();
        }
    }
}

void cPlot::paintEvent(QPaintEvent *event)
{
    QPainter oPainter(this);
    QRect oRect = rect() & event->rect();
    oPainter.setClipRect(oRect);
    oPainter.drawImage(0, 0, m_image.scaled(size() * m_scaleFactor));
}

void cPlot::resizeEvent(QResizeEvent *event)
{
    m_image = m_image.scaled(event->size() * m_scaleFactor);
    emit emitResizePlot(event->size());
}

void cPlot::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton)
    {
        emit emitPlotClick(event->pos());
    }
    else if (event->button() == Qt::RightButton)
    {
        emit emitPlotMenu(event->pos());
    }
}

void cPlot::wheelEvent(QWheelEvent *event)
{
    qDebug() << "cPlot::wheelEvent()...";
}

void cPlot::onResizePlot(const QSize &_size)
{
    if (m_plots.size())
    {
        const sPlot &oPlot = m_plots[0];
        int oDivision = oPlot.nDivision;
        int oWidth = _size.width() / oDivision;
        int oHeight = _size.height() / oDivision;

        for (int iCount = 0; iCount < m_plots.size(); iCount++)
        {
            sPlot &iPlot = m_plots[iCount];

            int oId = iCount;
            int iX = iCount % oDivision;
            int iY = iCount / oDivision;
            int oX = iX * oWidth;
            int oY = iY * oHeight;

            iPlot.id = oId;
            iPlot.x = oX;
            iPlot.y = oY;
            iPlot.width = oWidth;
            iPlot.height = oHeight;
            iPlot.nDivision = oDivision;

            if (hasPlotImage(iPlot.id))
            {
                QImage &oImage = m_images[iPlot.id];
                oImage = oImage.scaled(QSize(oWidth, oHeight) * m_scaleFactor);
            }
        }
    }

    if (m_plots.size() && DEF_DEBUG_ON)
    {
        qDebug() << "\nLe calcul des parametres de l'oscillosocpe.";

        for (const auto &oPlot : m_plots)
        {
            qDebug() << "|id=" << oPlot.id
                     << "|x=" << oPlot.x
                     << "|y=" << oPlot.y
                     << "|width=" << oPlot.width
                     << "|height=" << oPlot.height
                     << "|WIDTH=" << _size.width()
                     << "|HEIGHT=" << _size.height()
                     << "|nSCOPE=" << oPlot.nDivision * oPlot.nDivision;
        }
    }
}

void cPlot::onPlotMenu(const QPoint &_pos)
{
    if (isActivePlot(_pos))
    {
        m_plotMenu->exec(mapToGlobal(_pos));
    }
}

void cPlot::onAddPlotData()
{
    QPoint oPos = mapFromGlobal(m_plotMenu->pos());
    int oPlotId;
    if (isActivePlot(oPos, oPlotId))
    {
        if (hasPlotData(oPlotId))
        {
            QMessageBox::critical(this, tr("Messages d'erreurs"),
                                  tr("L'oscilloscope [%1] contient déjà des données.").arg(oPlotId));
            return;
        }

        if (hasPlotXShiftValue(oPlotId))
        {
            sData oData;
            const double &oXShiftValues = m_xShiftValues[oPlotId];
            generatePlotData(oData, oXShiftValues);
            m_datas[oPlotId] = oData;
        }
    }
}

void cPlot::onAnimatePlotData()
{
    QPoint oPos = mapFromGlobal(m_plotMenu->pos());
    int oPlotId;
    if (isActivePlot(oPos, oPlotId))
    {
        if (!hasPlotData(oPlotId))
        {
            QMessageBox::critical(this, tr("Messages d'erreurs"),
                                  tr("L'oscilloscope [%1] doit contenir des données.").arg(oPlotId));
            return;
        }
        if (isAnimatePlot(oPlotId))
        {
            bool &isAnimatePlot = m_isAnimatePlots[oPlotId];
            isAnimatePlot = !isAnimatePlot;
        }
        else
        {
            m_isAnimatePlots[oPlotId] = true;
        }
    }
}

void cPlot::onPlotClick(const QPoint &_pos)
{
    sPlot oPlot;
    if (loadPlot(_pos.x(), _pos.y(), oPlot))
    {
        qDebug() << "cPlot::mousePressEvent()...\n"
                 << "|x=" << _pos.x()
                 << "|y=" << _pos.y()
                 << "|oPlot.id=" << oPlot.id
                 << "|oPlot.x=" << oPlot.x
                 << "|oPlot.y=" << oPlot.y
                 << "|oPlot.width=" << oPlot.width
                 << "|oPlot.height=" << oPlot.height
                 << "|m_image.bytesPerLine=" << m_image.bytesPerLine()
                 << "|m_image.width=" << m_image.width()
                 << "|m_image.bits=" << m_image.bits()
                 << "|m_image.bits=" << m_image.depth();
    }
}
...

Gestion du fichier CMake


// CMakeLists.txt (Editer le fichier CMake)
...
cmake_minimum_required(VERSION 3.10.0)
project(rdvcpp VERSION 0.1.0 LANGUAGES C CXX)

set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)

set(CMAKE_PREFIX_PATH "C:/Qt/6.5.2/msvc2019_64/lib/cmake")

find_package(Qt6 REQUIRED COMPONENTS
    Widgets
)

qt_wrap_ui(UI_FILES
    cMainWindow.ui
)

add_executable(rdvcpp
    main.cpp
    cMainWindow.cpp
    cPlot.cpp
    resources.qrc
    ${UI_FILES}
)

target_link_libraries (rdvcpp
    Qt6::Widgets
)
...

Gestion du fichier ressource


// resources.qrc (Editer le fichier ressource)
...
<RCC>
  <qresource prefix="/img">
    <file alias="logo.png">data/img/logo.png</file>
  </qresource>
</RCC>
...

Exécution du projet


// Terminal (Exécuter le projet)
...
rdvcpp.exe
...

// Application (Fiche du menu général)
image.png

// Application (Fiche du menu ajout des données)
image.png

// Application (Fiche du menu animation temps réel)
image.png

// Application (Fiche d'affichage de plusieurs écrans)
image.png

// Application (Fiche de masquage de plusieurs écrans)  
image.png

Démo de l'application


https://www.youtube.com/watch?v=blf4bniIpuk