Sur cette page

Exemple Scribble

L'exemple « Scribble » montre comment réimplémenter certains gestionnaires d'événements de QWidget afin de recevoir les événements générés pour les widgets de l'application.

Nous réimplémentons les gestionnaires d’événements de souris pour mettre en œuvre le dessin, le gestionnaire d’événements « paint » pour actualiser l’application et le gestionnaire d’événements « resize » pour optimiser l’apparence de l’application. De plus, nous réimplémentons le gestionnaire d’événements « close » pour intercepter les événements de fermeture avant de quitter l’application.

Cet exemple montre également comment utiliser QPainter pour dessiner une image en temps réel, ainsi que pour rafraîchir les widgets.

Capture d'écran de l'exemple « Scribble »

L’application Scribble permet aux utilisateurs de dessiner une image. Le menu « File » permet aux utilisateurs d’ouvrir et de modifier un fichier image existant, d’enregistrer une image et de quitter l’application. Pendant le dessin, le menu « Options » permet aux utilisateurs de choisir la couleur et la largeur du stylo, ainsi que d’effacer l’écran. De plus, le menu « Help » fournit aux utilisateurs des informations sur l’exemple Scribble en particulier, et sur Qt en général.

L'exemple se compose de deux classes :

  • ScribbleArea est un widget personnalisé qui affiche une « QImage » et permet à l’utilisateur de dessiner dessus.
  • MainWindow fournit un menu au-dessus de l’ ScribbleArea.

Nous commencerons par examiner la classe ` ScribbleArea `. Ensuite, nous examinerons la classe ` MainWindow `, qui utilise ` ScribbleArea`.

Définition de la classe ScribbleArea

class ScribbleArea : public QWidget
{
    Q_OBJECT

public:
    ScribbleArea(QWidget *parent = nullptr);

    bool openImage(const QString &fileName);
    bool saveImage(const QString &fileName, const char *fileFormat);
    void setPenColor(const QColor &newColor);
    void setPenWidth(int newWidth);

    bool isModified() const { return modified; }
    QColor penColor() const { return myPenColor; }
    int penWidth() const { return myPenWidth; }

public slots:
    void clearImage();
    void print();

protected:
    void mousePressEvent(QMouseEvent *event) override;
    void mouseMoveEvent(QMouseEvent *event) override;
    void mouseReleaseEvent(QMouseEvent *event) override;
    void paintEvent(QPaintEvent *event) override;
    void resizeEvent(QResizeEvent *event) override;

private:
    void drawLineTo(const QPoint &endPoint);
    void resizeImage(QImage *image, const QSize &newSize);

    bool modified = false;
    bool scribbling = false;
    int myPenWidth = 1;
    QColor myPenColor = Qt::blue;
    QImage image;
    QPoint lastPoint;
};

La classe ` ScribbleArea ` hérite de ` QWidget`. Nous réimplémentons les fonctions ` mousePressEvent()`, ` mouseMoveEvent() ` et ` mouseReleaseEvent() ` pour implémenter le dessin. Nous réimplémentons la fonction ` paintEvent() ` pour mettre à jour la zone de dessin, et la fonction ` resizeEvent() ` pour garantir que l’ QImage e sur laquelle nous dessinons soit à tout moment au moins aussi grande que le widget.

Nous avons besoin de plusieurs fonctions publiques : openImage() charge une image depuis un fichier dans la zone de griffonnage, permettant ainsi à l’utilisateur de modifier l’image ; save() enregistre l’image actuellement affichée dans un fichier ; le slot clearImage() efface l’image affichée dans la zone de griffonnage. Nous avons besoin de la fonction privée drawLineTo() pour effectuer le dessin, et de resizeImage() pour modifier la taille d’un QImage. Le slot print() gère l’impression.

Nous avons également besoin des variables privées suivantes :

  • modified est true s’il y a des modifications non enregistrées sur l’image affichée dans la zone de dessin.
  • scribbling est true tant que l’utilisateur maintient le bouton gauche de la souris enfoncé dans la zone de dessin.
  • penWidth et penColor contiennent respectivement la largeur et la couleur actuellement définies pour le stylo utilisé dans l’application.
  • image stocke l'image dessinée par l'utilisateur.
  • lastPoint Contient la position du curseur lors du dernier événement de clic ou de déplacement de la souris.

Implémentation de la classe ScribbleArea

ScribbleArea::ScribbleArea(QWidget *parent)
    : QWidget(parent)
{
    setAttribute(Qt::WA_StaticContents);
}

Dans le constructeur, nous définissons l’attribut ` Qt::WA_StaticContents ` du widget, indiquant que le contenu du widget est ancré au coin supérieur gauche et ne change pas lorsque le widget est redimensionné. Qt utilise cet attribut pour optimiser les événements de dessin lors des redimensionnements. Il s’agit d’une simple optimisation qui ne doit être utilisée que pour les widgets dont le contenu est statique et ancré dans le coin supérieur gauche.

bool ScribbleArea::openImage(const QString &fileName)
{
    QImage loadedImage;
    if (!loadedImage.load(fileName))
        return false;

    QSize newSize = loadedImage.size().expandedTo(size());
    resizeImage(&loadedImage, newSize);
    image = loadedImage;
    modified = false;
    update();
    return true;
}

Dans la fonction ` openImage() `, nous chargeons l’image fournie. Ensuite, nous redimensionnons l’ QImage e chargée pour qu’elle soit au moins aussi grande que le widget dans les deux directions à l’aide de la fonction privée ` resizeImage() `, puis nous définissons la variable membre ` image ` sur l’image chargée. À la fin, nous appelons ` QWidget::update()` pour planifier un rafraîchissement de l’affichage.

bool ScribbleArea::saveImage(const QString &fileName, const char *fileFormat)
{
    QImage visibleImage = image;
    resizeImage(&visibleImage, size());

    if (visibleImage.save(fileName, fileFormat)) {
        modified = false;
        return true;
    }
    return false;
}

La fonction ` saveImage() ` crée un objet ` QImage ` qui ne couvre que la partie visible de l’image ` image ` et l’enregistre à l’aide de ` QImage::save()`. Si l’image est enregistrée avec succès, nous définissons la variable ` modified ` de la zone de griffonnage sur ` false`, car il n’y a pas de données non enregistrées.

void ScribbleArea::setPenColor(const QColor &newColor)
{
    myPenColor = newColor;
}

void ScribbleArea::setPenWidth(int newWidth)
{
    myPenWidth = newWidth;
}

Les fonctions setPenColor() et setPenWidth() définissent la couleur et la largeur actuelles du stylo. Ces valeurs seront utilisées pour les futures opérations de dessin.

void ScribbleArea::clearImage()
{
    image.fill(qRgb(255, 255, 255));
    modified = true;
    update();
}

Le slot public clearImage() efface l’image affichée dans la zone de griffonnage. Nous remplissons simplement l’intégralité de l’image en blanc, ce qui correspond à la valeur RVB (255, 255, 255). Comme d’habitude lorsque nous modifions l’image, nous définissons modified sur true et planifions un rafraîchissement.

void ScribbleArea::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        lastPoint = event->position().toPoint();
        scribbling = true;
    }
}

void ScribbleArea::mouseMoveEvent(QMouseEvent *event)
{
    if ((event->buttons() & Qt::LeftButton) && scribbling)
        drawLineTo(event->position().toPoint());
}

void ScribbleArea::mouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton && scribbling) {
        drawLineTo(event->position().toPoint());
        scribbling = false;
    }
}

Pour les événements de clic et de relâchement de la souris, nous utilisons la fonction QMouseEvent::button() afin de déterminer quel bouton a déclenché l’événement. Pour les événements de déplacement de la souris, nous utilisons QMouseEvent::buttons() afin de déterminer quels boutons sont actuellement enfoncés (sous forme de combinaison OU).

Si l’utilisateur appuie sur le bouton gauche de la souris, nous enregistrons la position du curseur de la souris dans ` lastPoint`. Nous notons également que l’utilisateur est en train de griffonner. (La variable ` scribbling ` est nécessaire car nous ne pouvons pas supposer qu’un événement de déplacement ou de relâchement de la souris est toujours précédé d’un événement de clic sur le même widget.)

Si l’utilisateur déplace la souris en maintenant le bouton gauche enfoncé ou relâche le bouton, nous appelons la fonction privée ` drawLineTo() ` pour dessiner.

void ScribbleArea::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);
    QRect dirtyRect = event->rect();
    painter.drawImage(dirtyRect, image, dirtyRect);
}

Dans la réimplémentation de la fonction paintEvent(), nous créons simplement un QPainter pour la zone de griffonnage, puis nous dessinons l’image.

À ce stade, vous vous demandez peut-être pourquoi nous ne dessinons pas directement sur le widget au lieu de dessiner dans un ` QImage ` et de copier l’ QImage à l’écran dans ` paintEvent()`. Il y a au moins trois bonnes raisons à cela :

  • Le système de fenêtrage exige que nous soyons en mesure de redessiner le widget à tout moment. Par exemple, si la fenêtre est minimisée puis restaurée, le système de fenêtrage peut avoir oublié le contenu du widget et nous envoyer un événement de dessin. En d’autres termes, nous ne pouvons pas compter sur le système de fenêtrage pour mémoriser notre image.
  • Qt ne nous autorise normalement pas à dessiner en dehors de ` paintEvent()`. En particulier, nous ne pouvons pas dessiner à partir des gestionnaires d’événements de la souris. (Ce comportement peut toutefois être modifié à l’aide de l’attribut ` Qt::WA_PaintOnScreen ` du widget.)
  • S’il est correctement initialisé, un ` QImage ` utilise systématiquement 8 bits pour chaque canal de couleur (rouge, vert, bleu et alpha), tandis qu’un ` QWidget ` peut présenter une profondeur de couleur inférieure, en fonction de la configuration du moniteur. Cela signifie que si nous chargeons une image de 24 ou 32 bits et que nous la dessinons sur une « QWidget », puis que nous copions à nouveau l’ QWidget e dans une « QImage », nous risquons de perdre certaines informations.
void ScribbleArea::resizeEvent(QResizeEvent *event)
{
    if (width() > image.width() || height() > image.height()) {
        int newWidth = qMax(width() + 128, image.width());
        int newHeight = qMax(height() + 128, image.height());
        resizeImage(&image, QSize(newWidth, newHeight));
        update();
    }
    QWidget::resizeEvent(event);
}

Lorsque l'utilisateur lance l'application Scribble, un événement de redimensionnement est généré et une image est créée puis affichée dans la zone de dessin. Nous rendons cette image initiale légèrement plus grande que la fenêtre principale de l’application et la zone de griffonnage, afin d’éviter de devoir redimensionner l’image à chaque fois que l’utilisateur redimensionne la fenêtre principale (ce qui serait très inefficace). Mais lorsque la fenêtre principale devient plus grande que cette taille initiale, l’image doit être redimensionnée.

void ScribbleArea::drawLineTo(const QPoint &endPoint)
{
    QPainter painter(&image);
    painter.setPen(QPen(myPenColor, myPenWidth, Qt::SolidLine, Qt::RoundCap,
                        Qt::RoundJoin));
    painter.drawLine(lastPoint, endPoint);
    modified = true;

    int rad = (myPenWidth / 2) + 2;
    update(QRect(lastPoint, endPoint).normalized()
                                     .adjusted(-rad, -rad, +rad, +rad));
    lastPoint = endPoint;
}

Dans ` drawLineTo()`, nous traçons une ligne à partir du point où se trouvait la souris lors du dernier clic ou déplacement de la souris, nous définissons ` modified ` sur `true`, nous générons un événement de rafraîchissement et nous mettons à jour ` lastPoint ` afin que, la prochaine fois que ` drawLineTo() ` sera appelée, nous puissions continuer à dessiner là où nous nous étions arrêtés.

Nous pourrions appeler la fonction update() sans paramètre, mais pour optimiser le code, nous lui transmettons un paramètre QRect qui spécifie le rectangle à l’intérieur de la zone de dessin devant être mis à jour, afin d’éviter un rafraîchissement complet du widget.

void ScribbleArea::resizeImage(QImage *image, const QSize &newSize)
{
    if (image->size() == newSize)
        return;

    QImage newImage(newSize, QImage::Format_RGB32);
    newImage.fill(qRgb(255, 255, 255));
    QPainter painter(&newImage);
    painter.drawImage(QPoint(0, 0), *image);
    *image = newImage;
}

QImage ne dispose pas d’une API pratique pour redimensionner une image. Il existe une fonction ` QImage::copy()` qui pourrait faire l’affaire, mais lorsqu’on l’utilise pour agrandir une image, elle remplit les nouvelles zones de noir, alors que nous voulons du blanc.

L'astuce consiste donc à créer une toute nouvelle image ` QImage ` de la bonne taille, à la remplir de blanc, puis à y dessiner l'ancienne image à l'aide de ` QPainter`. La nouvelle image est définie au format « QImage::Format_RGB32 », ce qui signifie que chaque pixel est stocké sous la forme 0xffRRGGBB (où RR, GG et BB correspondent respectivement aux canaux de couleur rouge, vert et bleu, et ff à la valeur hexadécimale 255).

L’impression est gérée par le slot ` print() ` :

void ScribbleArea::print()
{
#if defined(QT_PRINTSUPPORT_LIB) && QT_CONFIG(printdialog)
    QPrinter printer(QPrinter::HighResolution);

    QPrintDialog printDialog(&printer, this);

Nous créons un objet QPrinter haute résolution pour le format de sortie requis, en utilisant une boîte de dialogue QPrintDialog pour demander à l'utilisateur de spécifier un format de page et d'indiquer comment la sortie doit être mise en page.

Si la boîte de dialogue est acceptée, nous effectuons l'impression sur le périphérique de dessin :

    if (printDialog.exec() == QDialog::Accepted) {
        QPainter painter(&printer);
        QRect rect = painter.viewport();
        QSize size = image.size();
        size.scale(rect.size(), Qt::KeepAspectRatio);
        painter.setViewport(rect.x(), rect.y(), size.width(), size.height());
        painter.setWindow(image.rect());
        painter.drawImage(0, 0, image);
    }
#endif // QT_CONFIG(printdialog)
}

Imprimer une image dans un fichier de cette manière revient simplement à la dessiner sur le QPrinter. Nous redimensionnons l’image pour qu’elle s’adapte à l’espace disponible sur la page avant de la dessiner sur le périphérique de sortie.

Définition de la classe MainWindow

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);

protected:
    void closeEvent(QCloseEvent *event) override;

private slots:
    void open();
    void save();
    void penColor();
    void penWidth();
    void about();

private:
    void createActions();
    void createMenus();
    bool maybeSave();
    bool saveFile(const QByteArray &fileFormat);

    ScribbleArea *scribbleArea;

    QMenu *saveAsMenu;
    QMenu *fileMenu;
    QMenu *optionMenu;
    QMenu *helpMenu;

    QAction *openAct;
    QList<QAction *> saveAsActs;
    QAction *exitAct;
    QAction *penColorAct;
    QAction *penWidthAct;
    QAction *printAct;
    QAction *clearScreenAct;
    QAction *aboutAct;
    QAction *aboutQtAct;
};

La classe ` MainWindow ` hérite de ` QMainWindow`. Nous réimplémentons le gestionnaire ` closeEvent()` à partir de ` QWidget`. Les slots ` open()`, ` save()`, ` penColor() ` et ` penWidth() ` correspondent aux entrées du menu. De plus, nous créons quatre fonctions privées.

Nous utilisons la fonction booléenne ` maybeSave() ` pour vérifier s’il existe des modifications non enregistrées. Si c’est le cas, nous donnons à l’utilisateur la possibilité de les enregistrer. La fonction renvoie ` false ` si l’utilisateur clique sur ` Cancel`. Nous utilisons la fonction ` saveFile() ` pour permettre à l’utilisateur d’enregistrer l’image actuellement affichée dans la zone de griffonnage.

Implémentation de la classe MainWindow

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent), scribbleArea(new ScribbleArea(this))
{
    setCentralWidget(scribbleArea);

    createActions();
    createMenus();

    setWindowTitle(tr("Scribble"));
    resize(500, 500);
}

Dans le constructeur, nous créons une zone de griffonnage que nous désignons comme widget central du widget ` MainWindow `. Nous créons ensuite les actions et les menus associés.

void MainWindow::closeEvent(QCloseEvent *event)
{
    if (maybeSave())
        event->accept();
    else
        event->ignore();
}

Les événements de fermeture sont envoyés aux widgets que les utilisateurs souhaitent fermer, généralement en cliquant sur « File|Exit » ou sur le bouton « X » de la barre de titre. En réimplémentant le gestionnaire d’événements, nous pouvons intercepter les tentatives de fermeture de l’application.

Dans cet exemple, nous utilisons l’événement de fermeture pour demander à l’utilisateur d’enregistrer les modifications non enregistrées. La logique correspondante se trouve dans la fonction ` maybeSave() `. Si ` maybeSave() ` renvoie `true`, cela signifie qu’il n’y a pas de modifications ou que l’utilisateur les a enregistrées avec succès, et nous acceptons alors l’événement. L’application peut alors se fermer normalement. Si la fonction maybeSave() renvoie « false », cela signifie que l’utilisateur a cliqué sur « Cancel » ; nous « ignorons » donc l’événement, ce qui n’affecte pas le fonctionnement de l’application.

void MainWindow::open()
{
    if (maybeSave()) {
        QString fileName = QFileDialog::getOpenFileName(this,
                                   tr("Open File"), QDir::currentPath());
        if (!fileName.isEmpty())
            scribbleArea->openImage(fileName);
    }
}

Dans le slot open(), nous donnons d’abord à l’utilisateur la possibilité d’enregistrer les modifications apportées à l’image actuellement affichée, avant qu’une nouvelle image ne soit chargée dans la zone de dessin. Ensuite, nous demandons à l’utilisateur de choisir un fichier et nous chargeons ce fichier dans le slot ScribbleArea.

void MainWindow::save()
{
    QAction *action = qobject_cast<QAction *>(sender());
    QByteArray fileFormat = action->data().toByteArray();
    saveFile(fileFormat);
}

Le slot « save() » est appelé lorsque l’utilisateur sélectionne l’entrée de menu « Save As », puis choisit une option dans le menu « Format ». La première chose à faire est de déterminer quelle action a envoyé le signal à l’aide de la fonction « QObject::sender() ». Cette fonction renvoie l’émetteur sous la forme d’un pointeur « QObject ». Comme nous savons que l’émetteur est un objet « action », nous pouvons le convertir en toute sécurité en « QObject ». Nous aurions pu utiliser un transtypage de style C ou une instruction C++ ` static_cast<>()`, mais par souci de programmation défensive, nous utilisons ` qobject_cast()`. L’avantage est que si l’objet est d’un type incorrect, un pointeur nul est renvoyé. Les plantages dus à des pointeurs nuls sont bien plus faciles à diagnostiquer que ceux dus à des transtypages non sécurisés.

Une fois l’action obtenue, nous extrayons le format choisi à l’aide de ` QAction::data()`. (Lors de la création des actions, nous utilisons ` QAction::setData()` pour définir nos propres données personnalisées associées à l’action, sous la forme d’un ` QVariant`. Nous reviendrons plus en détail sur ce point lorsque nous aborderons ` createActions()`.)

Maintenant que nous connaissons le format, nous appelons la fonction privée saveFile() pour enregistrer l’image actuellement affichée.

void MainWindow::penColor()
{
    QColor newColor = QColorDialog::getColor(scribbleArea->penColor());
    if (newColor.isValid())
        scribbleArea->setPenColor(newColor);
}

Nous utilisons le slot penColor() pour récupérer une nouvelle couleur auprès de l’utilisateur via QColorDialog. Si l’utilisateur choisit une nouvelle couleur, nous l’appliquons à la zone de griffonnage.

void MainWindow::penWidth()
{
    bool ok;
    int newWidth = QInputDialog::getInt(this, tr("Scribble"),
                                        tr("Select pen width:"),
                                        scribbleArea->penWidth(),
                                        1, 50, 1, &ok);
    if (ok)
        scribbleArea->setPenWidth(newWidth);
}

Pour récupérer une nouvelle largeur de stylo dans le slot « penWidth() », nous utilisons ` QInputDialog`. La classe ` QInputDialog ` fournit une boîte de dialogue simple et pratique permettant d’obtenir une valeur unique de la part de l’utilisateur. Nous utilisons la fonction statique QInputDialog::getInt(), qui combine un QLabel et un QSpinBox. L’ QSpinBox est initialisé avec la largeur du stylo de la zone de griffonnage ; il autorise une plage comprise entre 1 et 50, par incréments de 1 (ce qui signifie que les flèches haut et bas augmentent ou diminuent la valeur de 1).

La variable booléenne ok sera définie sur true si l’utilisateur a cliqué sur OK et sur false s’il a appuyé sur Cancel.

void MainWindow::about()
{
    QMessageBox::about(this, tr("About Scribble"),
            tr("<p>The <b>Scribble</b> example shows how to use QMainWindow as the "
               "base widget for an application, and how to reimplement some of "
               "QWidget's event handlers to receive the events generated for "
               "the application's widgets:</p><p> We reimplement the mouse event "
               "handlers to facilitate drawing, the paint event handler to "
               "update the application and the resize event handler to optimize "
               "the application's appearance. In addition we reimplement the "
               "close event handler to intercept the close events before "
               "terminating the application.</p><p> The example also demonstrates "
               "how to use QPainter to draw an image in real time, as well as "
               "to repaint widgets.</p>"));
}

Nous implémentons le slot about() pour créer une boîte de message décrivant ce que l’exemple est censé afficher.

void MainWindow::createActions()
{
    openAct = new QAction(tr("&Open..."), this);
    openAct->setShortcuts(QKeySequence::Open);
    connect(openAct, &QAction::triggered, this, &MainWindow::open);

    const QList<QByteArray> imageFormats = QImageWriter::supportedImageFormats();
    for (const QByteArray &format : imageFormats) {
        QString text = tr("%1...").arg(QString::fromLatin1(format).toUpper());

        QAction *action = new QAction(text, this);
        action->setData(format);
        connect(action, &QAction::triggered, this, &MainWindow::save);
        saveAsActs.append(action);
    }

    printAct = new QAction(tr("&Print..."), this);
    connect(printAct, &QAction::triggered, scribbleArea, &ScribbleArea::print);

    exitAct = new QAction(tr("E&xit"), this);
    exitAct->setShortcuts(QKeySequence::Quit);
    connect(exitAct, &QAction::triggered, this, &MainWindow::close);

    penColorAct = new QAction(tr("&Pen Color..."), this);
    connect(penColorAct, &QAction::triggered, this, &MainWindow::penColor);

    penWidthAct = new QAction(tr("Pen &Width..."), this);
    connect(penWidthAct, &QAction::triggered, this, &MainWindow::penWidth);

    clearScreenAct = new QAction(tr("&Clear Screen"), this);
    clearScreenAct->setShortcut(tr("Ctrl+L"));
    connect(clearScreenAct, &QAction::triggered,
            scribbleArea, &ScribbleArea::clearImage);

    aboutAct = new QAction(tr("&About"), this);
    connect(aboutAct, &QAction::triggered, this, &MainWindow::about);

    aboutQtAct = new QAction(tr("About &Qt"), this);
    connect(aboutQtAct, &QAction::triggered, qApp, &QApplication::aboutQt);
}

Dans la fonction ` createAction() `, nous créons les actions correspondant aux entrées du menu et les relions aux slots appropriés. Nous créons notamment les actions du sous-menu « Save As ». Nous utilisons ` QImageWriter::supportedImageFormats()` pour obtenir la liste des formats pris en charge (sous la forme ` QList<QByteArray>`).

Ensuite, nous parcourons la liste en créant une action pour chaque format. Nous appelons ` QAction::setData()` avec le format de fichier, afin de pouvoir le récupérer ultérieurement via ` QAction::data()`. Nous aurions également pu déduire le format de fichier à partir du texte de l’action, en tronquant les « … », mais cela aurait manqué d’élégance.

void MainWindow::createMenus()
{
    saveAsMenu = new QMenu(tr("&Save As"), this);
    for (QAction *action : std::as_const(saveAsActs))
        saveAsMenu->addAction(action);

    fileMenu = new QMenu(tr("&File"), this);
    fileMenu->addAction(openAct);
    fileMenu->addMenu(saveAsMenu);
    fileMenu->addAction(printAct);
    fileMenu->addSeparator();
    fileMenu->addAction(exitAct);

    optionMenu = new QMenu(tr("&Options"), this);
    optionMenu->addAction(penColorAct);
    optionMenu->addAction(penWidthAct);
    optionMenu->addSeparator();
    optionMenu->addAction(clearScreenAct);

    helpMenu = new QMenu(tr("&Help"), this);
    helpMenu->addAction(aboutAct);
    helpMenu->addAction(aboutQtAct);

    menuBar()->addMenu(fileMenu);
    menuBar()->addMenu(optionMenu);
    menuBar()->addMenu(helpMenu);
}

Dans la fonction ` createMenu() `, nous ajoutons les actions de format précédemment créées à ` saveAsMenu`. Ensuite, nous ajoutons le reste des actions ainsi que le sous-menu « saveAsMenu » aux menus « File », « Options » et « Help ».

La classe ` QMenu ` fournit un widget de menu destiné à être utilisé dans les barres de menus, les menus contextuels et autres menus contextuels. La classe ` QMenuBar ` fournit une barre de menus horizontale comportant une liste de menus déroulants ` QMenu`. Pour finir, nous plaçons les menus ` File ` et ` Options ` dans la barre de menus de ` MainWindow`, que nous récupérons à l’aide de la fonction ` QMainWindow::menuBar()`.

bool MainWindow::maybeSave()
{
    if (scribbleArea->isModified()) {
       QMessageBox::StandardButton ret;
       ret = QMessageBox::warning(this, tr("Scribble"),
                          tr("The image has been modified.\n"
                             "Do you want to save your changes?"),
                          QMessageBox::Save | QMessageBox::Discard
                          | QMessageBox::Cancel);
        if (ret == QMessageBox::Save)
            return saveFile("png");
        else if (ret == QMessageBox::Cancel)
            return false;
    }
    return true;
}

Dans mayBeSave(), nous vérifions s’il y a des modifications non enregistrées. Si c’est le cas, nous utilisons QMessageBox pour avertir l’utilisateur que l’image a été modifiée et lui donner la possibilité d’enregistrer les modifications.

Comme pour QColorDialog et QFileDialog, la manière la plus simple de créer un QMessageBox est d’utiliser ses fonctions statiques. QMessageBox propose toute une gamme de messages classés selon deux axes : la gravité (question, information, avertissement et critique) et la complexité (le nombre de boutons de réponse nécessaires). Ici, nous utilisons la fonction warning(), car le message est assez important.

Si l’utilisateur choisit d’enregistrer, nous appelons la fonction privée saveFile(). Par souci de simplicité, nous utilisons le format PNG ; l’utilisateur peut à tout moment cliquer sur Cancel et enregistrer le fichier dans un autre format.

La fonction maybeSave() renvoie false si l’utilisateur clique sur Cancel; sinon, elle renvoie true.

bool MainWindow::saveFile(const QByteArray &fileFormat)
{
    QString initialPath = QDir::currentPath() + "/untitled." + fileFormat;

    QString fileName = QFileDialog::getSaveFileName(this, tr("Save As"),
                               initialPath,
                               tr("%1 Files (*.%2);;All Files (*)")
                               .arg(QString::fromLatin1(fileFormat.toUpper()))
                               .arg(QString::fromLatin1(fileFormat)));
    if (fileName.isEmpty())
        return false;
    return scribbleArea->saveImage(fileName, fileFormat.constData());
}

Dans saveFile(), nous affichons une boîte de dialogue de sélection de fichier avec une suggestion de nom de fichier. La fonction statique QFileDialog::getSaveFileName() renvoie le nom de fichier sélectionné par l’utilisateur. Le fichier n’a pas besoin d’exister.

Exemple de projet sur code.qt.io

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.