Sur cette page

Exemple Scribble

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

Nous réimplémentons les gestionnaires d'événements de la souris pour mettre en œuvre le dessin, le gestionnaire d'événements de la peinture pour mettre à jour l'application et le gestionnaire d'événements du redimensionnement pour optimiser l'apparence de l'application. En outre, nous réimplémentons le gestionnaire d'événements de fermeture pour intercepter les événements de fermeture avant de terminer l'application.

L'exemple montre également comment utiliser QPainter pour dessiner une image en temps réel, ainsi que pour repeindre les widgets.

Capture d'écran de l'exemple Scribble

L'application Scribble permet aux utilisateurs de dessiner une image. Le menu File permet 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. En outre, le menu Help fournit aux utilisateurs des informations sur l'exemple Scribble en particulier, et sur Qtt en général.

L'exemple se compose de deux classes :

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

Nous commencerons par examiner la classe ScribbleArea. Nous examinerons ensuite 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 la classe QWidget. Nous réimplémentons les fonctions mousePressEvent(), mouseMoveEvent() et mouseReleaseEvent() pour mettre en œuvre le dessin. Nous réimplémentons la fonction paintEvent() pour mettre à jour la zone de gribouillage et la fonction resizeEvent() pour nous assurer que la zone QImage sur laquelle nous dessinons est au moins aussi grande que le widget à tout moment.

Nous avons besoin de plusieurs fonctions publiques : openImage() charge une image à partir d'un fichier dans la zone de gribouillage, ce qui permet à l'utilisateur de modifier l'image ; save() écrit l'image actuellement affichée dans le fichier ; clearImage() slot efface l'image affichée dans la zone de gribouillage. Nous avons besoin de la fonction privée drawLineTo() pour effectuer le dessin, et de resizeImage() pour modifier la taille d'un fichier QImage. Le slot print() s'occupe de l'impression.

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

  • modified est true si des modifications non enregistrées sont apportées à l'image affichée dans la zone de gribouillage.
  • scribbling is true lorsque l'utilisateur appuie sur le bouton gauche de la souris dans la zone de gribouillage.
  • penWidth et penColor contiennent 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 de la dernière pression ou du dernier déplacement de la souris.

Mise en œuvre 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 pour le widget, indiquant que le contenu du widget est ancré dans le coin supérieur gauche et ne change pas lorsque le widget est redimensionné. Qt utilise cet attribut pour optimiser les événements de peinture lors des redimensionnements. Il s'agit d'une optimisation pure et simple 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 donnée. Ensuite, nous redimensionnons l'image chargée QImage pour qu'elle soit au moins aussi grande que le widget dans les deux sens à l'aide de la fonction privée resizeImage() et nous définissons la variable membre image comme étant l'image chargée. À la fin, nous appelons QWidget::update() pour programmer un repeint.

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 actuelle 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 gribouillage 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 opérations de dessin à venir.

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 gribouillage. Nous remplissons simplement toute l'image avec du blanc, qui correspond à la valeur RVB (255, 255, 255). Comme d'habitude, lorsque nous modifions l'image, nous remplaçons modified par true et programmons une nouvelle peinture.

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 pression et de relâchement de la souris, nous utilisons la fonction QMouseEvent::button() pour savoir quel bouton a provoqué l'événement. Pour les événements de déplacement de la souris, nous utilisons la fonction QMouseEvent::buttons() pour savoir quels boutons sont actuellement maintenus enfoncés (sous la forme d'une combinaison OR).

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 et de relâchement de la souris est toujours précédé d'un événement de pression de la souris sur le même widget).

Si l'utilisateur déplace la souris avec 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 une QPainter pour la zone de gribouillage et 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 le QImage sur l'écran dans paintEvent(). Il y a au moins trois bonnes raisons à cela :

  • Le système de fenêtres exige que nous puissions redessiner le widget à tout moment. Par exemple, si la fenêtre est réduite et restaurée, le système de fenêtres peut avoir oublié le contenu du widget et nous envoyer un événement de peinture. En d'autres termes, nous ne pouvons pas compter sur le système de fenêtres pour se souvenir de notre image.
  • Qt ne nous permet normalement pas de peindre en dehors de paintEvent(). En particulier, nous ne pouvons pas peindre à partir des gestionnaires d'événements de la souris. (Ce comportement peut être modifié à l'aide de l'attribut Qt::WA_PaintOnScreen widget).
  • S'il est initialisé correctement, un site QImage est assuré d'utiliser 8 bits pour chaque canal de couleur (rouge, vert, bleu et alpha), alors qu'un site QWidget peut avoir une profondeur de couleur inférieure, en fonction de la configuration du moniteur. Cela signifie que si nous chargeons une image 24 bits ou 32 bits et que nous la peignons sur un site QWidget, puis que nous copions à nouveau le site QWidget dans un site 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 démarre l'application Scribble, un événement de redimensionnement est généré et une image est créée et affichée dans la zone de gribouillage. Cette image initiale est légèrement plus grande que la fenêtre principale et la zone de gribouillage de l'application, afin d'éviter de toujours redimensionner l'image lorsque l'utilisateur redimensionne la fenêtre principale (ce qui serait très inefficace). Mais lorsque la fenêtre principale dépasse 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 de la dernière pression ou du dernier déplacement de la souris, nous attribuons la valeur true à modified, nous générons un événement repaint et nous mettons à jour lastPoint de sorte que la prochaine fois que drawLineTo() est appelée, nous reprenons le dessin là où nous l'avons laissé.

Nous pourrions appeler la fonction update() sans paramètre, mais pour une optimisation facile, nous passons un QRect qui spécifie le rectangle à l'intérieur du gribouillis qui doit être mis à jour, afin d'éviter de repeindre complètement le 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 n'a pas d'API pour redimensionner une image. Il existe une fonction QImage::copy() qui pourrait faire l'affaire, mais lorsqu'elle est utilisée pour agrandir une image, elle remplit les nouvelles zones de noir, alors que nous voulons du blanc.

L'astuce consiste donc à créer un tout nouveau site QImage de la bonne taille, à le remplir de blanc et à y dessiner l'ancienne image à l'aide de QPainter. La nouvelle image reçoit le format QImage::Format_RGB32, ce qui signifie que chaque pixel est stocké sous la forme 0xffRRGGBB (où RR, GG et BB sont les canaux de couleur rouge, vert et bleu, ff étant la valeur hexadécimale 255).

L'impression est gérée par l'emplacement print():

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

    QPrintDialog printDialog(&printer, this);

Nous construisons un objet QPrinter haute résolution pour le format de sortie requis, en utilisant un QPrintDialog pour demander à l'utilisateur de spécifier une taille de page et d'indiquer comment la sortie doit être formatée sur la page.

Si la boîte de dialogue est acceptée, nous procédons à l'impression sur le périphérique de peinture :

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

L'impression d'une image dans un fichier de cette manière revient simplement à peindre sur l'imprimante QPrinter. Nous mettons l'image à l'échelle pour qu'elle tienne dans l'espace disponible sur la page avant de la peindre sur le périphérique de peinture.

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 emplacements open(), save(), penColor() et penWidth() correspondent aux entrées de menu. En outre, 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é d'enregistrer ces modifications. 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 gribouillage.

Mise en œuvre 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 gribouillage qui devient le 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 de la barre de titre X. 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 sauvegardées. La logique de cette demande se trouve dans la fonction maybeSave(). Si maybeSave() renvoie vrai, il n'y a pas de modifications ou l'utilisateur les a sauvegardées avec succès, et nous acceptons l'événement. L'application peut alors se terminer normalement. Si maybeSave() renvoie false, l'utilisateur a cliqué sur Cancel, nous "ignorons" donc l'événement, ce qui n'affecte pas l'application.

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

Dans l'emplacement open(), nous donnons d'abord à l'utilisateur la possibilité d'enregistrer toute modification apportée à l'image actuellement affichée, avant qu'une nouvelle image ne soit chargée dans la zone de gribouillage. Ensuite, nous demandons à l'utilisateur de choisir un fichier et nous chargeons le fichier dans le slot ScribbleArea.

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

L'emplacement save() est appelé lorsque l'utilisateur choisit l'entrée de menu Save As, puis une entrée du menu de format. La première chose à faire est de déterminer quelle action a envoyé le signal à l'aide de QObject::sender(). Cette fonction renvoie l'expéditeur sous la forme d'un pointeur QObject. Comme nous savons que l'expéditeur est un objet action, nous pouvons en toute sécurité convertir le QObject. Nous aurions pu utiliser une conversion de style C ou un static_cast<>() C++, mais comme technique de programmation défensive, nous utilisons un qobject_cast(). L'avantage est que si l'objet n'a pas le bon type, un pointeur nul est renvoyé. Les plantages dus à des pointeurs nuls sont beaucoup plus faciles à diagnostiquer que les plantages dus à des castings non sûrs.

Une fois que nous avons l'action, nous extrayons le format choisi en utilisant QAction::data(). (Lorsque les actions sont créées, nous utilisons QAction::setData() pour définir nos propres données personnalisées attachées à l'action, en tant que QVariant. Nous reviendrons sur ce point lorsque nous examinerons 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 l'emplacement penColor() pour demander à l'utilisateur d'indiquer une nouvelle couleur à l'aide de la fonction QColorDialog. Si l'utilisateur choisit une nouvelle couleur, nous en faisons la couleur de la zone de gribouillage.

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 l'emplacement penWidth(), nous utilisons QInputDialog. La classe QInputDialog fournit un dialogue de commodité simple pour obtenir une valeur unique de l'utilisateur. Nous utilisons la fonction statique QInputDialog::getInt(), qui combine une QLabel et une QSpinBox. La QSpinBox est initialisée avec la largeur du stylo de la zone de gribouillage, ce qui permet une plage de 1 à 50, avec un pas de 1 (ce qui signifie que les flèches vers le haut et vers le bas incrémentent ou décrémentent la valeur de 1).

La variable booléenne ok sera définie sur true si l'utilisateur a cliqué sur OK et sur false si l'utilisateur 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 mettons en œuvre l'emplacement about() pour créer une boîte de message décrivant ce que l'exemple est censé montrer.

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 représentant les entrées de menu et les connectons aux emplacements appropriés. Nous créons notamment les actions du sous-menu Save As. Nous utilisons QImageWriter::supportedImageFormats() pour obtenir une liste des formats pris en charge (sous la forme QList<QByteArray>).

Nous parcourons ensuite la liste, en créant une action pour chaque format. Nous appelons QAction::setData() avec le format du fichier, afin de pouvoir le récupérer plus tard sous QAction::data(). Nous aurions également pu déduire le format de fichier du texte de l'action, en tronquant le "...", mais cela aurait été inélégant.

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 créées précédemment au menu saveAsMenu. Nous ajoutons ensuite le reste des actions ainsi que le sous-menu saveAsMenu aux menus File, Options et Help.

La classe QMenu fournit un widget de menu à utiliser dans les barres de menu, les menus contextuels et autres menus contextuels. La classe QMenuBar fournit une barre de menu horizontale avec une liste de menus déroulants QMenus. À la fin, nous plaçons les menus File et Options dans la barre de menu 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 fournit une série de messages différents 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 de sauvegarder, nous appelons la fonction privée saveFile(). Pour simplifier, nous utilisons PNG comme format de fichier ; l'utilisateur peut toujours appuyer 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 ouvrons une boîte de dialogue de fichier avec une suggestion de nom de fichier. La fonction statique QFileDialog::getSaveFileName() renvoie un nom de fichier sélectionné par l'utilisateur. Le fichier ne doit pas nécessairement exister.

Exemple de projet @ 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.