Auf dieser Seite

Scribble-Beispiel

Das „Scribble“-Beispiel zeigt, wie einige der Ereignisbehandler von „ QWidget “ neu implementiert werden, um die für die Widgets der Anwendung generierten Ereignisse zu empfangen.

Wir implementieren die Maus-Ereignisbehandler neu, um das Zeichnen zu ermöglichen, den „Paint“-Ereignisbehandler, um die Anwendung zu aktualisieren, und den „Resize“-Ereignisbehandler, um das Erscheinungsbild der Anwendung zu optimieren. Darüber hinaus implementieren wir den „Close“-Ereignisbehandler neu, um die Schließereignisse abzufangen, bevor die Anwendung beendet wird.

Das Beispiel veranschaulicht zudem, wie man mit „ QPainter “ ein Bild in Echtzeit zeichnen sowie Widgets neu zeichnen kann.

Screenshot des „Scribble“-Beispiels

Mit der Anwendung „Scribble“ können die Benutzer ein Bild zeichnen. Über das Menü „ File “ haben die Benutzer die Möglichkeit, eine vorhandene Bilddatei zu öffnen und zu bearbeiten, ein Bild zu speichern und die Anwendung zu beenden. Während des Zeichnens können die Benutzer über das Menü „ Options “ die Stiftfarbe und -breite auswählen sowie den Bildschirm löschen. Darüber hinaus bietet das Menü „ Help “ den Benutzern Informationen speziell zum Scribble-Beispiel sowie allgemein zu Qt.

Das Beispiel besteht aus zwei Klassen:

  • ScribbleArea ist ein benutzerdefiniertes Widget, das eine „ QImage “ anzeigt und es dem Benutzer ermöglicht, darauf zu zeichnen.
  • MainWindow stellt ein Menü oberhalb des „ ScribbleArea “ bereit.

Zunächst werden wir uns die Klasse „ ScribbleArea “ ansehen. Anschließend werden wir die Klasse „ MainWindow “ betrachten, die „ ScribbleArea “ verwendet.

Definition der Klasse „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;
};

Die Klasse „ ScribbleArea “ erbt von „ QWidget “. Wir implementieren die Funktionen „ mousePressEvent() “, „ mouseMoveEvent() “ und „ mouseReleaseEvent() “ neu, um das Zeichnen zu realisieren. Wir implementieren die Funktion „ paintEvent() “ neu, um den Zeichenbereich zu aktualisieren, sowie die Funktion „ resizeEvent() “, um sicherzustellen, dass der „ QImage “, auf dem wir zeichnen, zu jedem Zeitpunkt mindestens so groß ist wie das Widget.

Wir benötigen mehrere öffentliche Funktionen: „ openImage() “ lädt ein Bild aus einer Datei in den Zeichenbereich, sodass der Benutzer das Bild bearbeiten kann; „ save() “ schreibt das aktuell angezeigte Bild in eine Datei; der Slot „ clearImage() “ löscht das im Zeichenbereich angezeigte Bild. Wir benötigen die private Funktion „ drawLineTo() “, um das Zeichnen tatsächlich durchzuführen, und „ resizeImage() “, um die Größe von „ QImage “ zu ändern. Der Slot „ print() “ übernimmt den Druckvorgang.

Außerdem benötigen wir die folgenden privaten Variablen:

  • modified ist „ true “, wenn es ungespeicherte Änderungen am Bild gibt, das im Skizzenbereich angezeigt wird.
  • scribbling ist „ true “, solange der Benutzer die linke Maustaste innerhalb des Skizzenbereichs gedrückt hält.
  • penWidth und „ penColor “ enthalten die aktuell eingestellte Breite und Farbe des in der Anwendung verwendeten Stifts.
  • image speichert das vom Benutzer gezeichnete Bild.
  • lastPoint Enthält die Position des Cursors zum Zeitpunkt des letzten Mausklick- oder Mausbewegungsereignisses.

Implementierung der Klasse „ScribbleArea“

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

Im Konstruktor setzen wir das Attribut „ Qt::WA_StaticContents “ für das Widget und geben damit an, dass der Inhalt des Widgets an der oberen linken Ecke verankert ist und sich bei einer Größenänderung des Widgets nicht verschiebt. Qt nutzt dieses Attribut, um Zeichenvorgänge bei Größenänderungen zu optimieren. Dies dient ausschließlich der Optimierung und sollte nur für Widgets verwendet werden, deren Inhalt statisch ist und an der oberen linken Ecke verankert ist.

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

In der Funktion „ openImage() “ laden wir das angegebene Bild. Anschließend passen wir die Größe des geladenen „ QImage “ mithilfe der privaten Funktion „ resizeImage() “ so an, dass es in beiden Richtungen mindestens so groß wie das Widget ist, und setzen die Membervariable „ image “ auf das geladene Bild. Am Ende rufen wir „ QWidget::update()“ auf, um ein Neuzeichnen anzufordern.

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

Die Funktion „ saveImage() “ erstellt ein „ QImage “-Objekt, das nur den sichtbaren Bereich des eigentlichen „ image “ abdeckt, und speichert es mit „ QImage::save()“. Wenn das Bild erfolgreich gespeichert wurde, setzen wir die Variable „ modified “ des Kritzelbereichs auf „ false “, da keine ungespeicherten Daten vorhanden sind.

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

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

Die Funktionen „ setPenColor() “ und „ setPenWidth() “ legen die aktuelle Stiftfarbe und -breite fest. Diese Werte werden für zukünftige Zeichenvorgänge verwendet.

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

Der öffentliche Slot „ clearImage() “ löscht das im Skizzenbereich angezeigte Bild. Wir füllen einfach das gesamte Bild mit Weiß, was dem RGB-Wert (255, 255, 255) entspricht. Wie üblich, wenn wir das Bild ändern, setzen wir „ modified “ auf „ true “ und planen eine Neuzeichnung ein.

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

Für Mausklick- und Mausloslass-Ereignisse verwenden wir die Funktion ` QMouseEvent::button()` (Mausklick-Ereignis), um herauszufinden, welche Taste das Ereignis ausgelöst hat. Für Mausbewegungsereignisse verwenden wir ` QMouseEvent::buttons()` (Mausbewegungsereignis), um festzustellen, welche Tasten derzeit gedrückt gehalten werden (als ODER-Kombination).

Wenn der Benutzer die linke Maustaste drückt, speichern wir die Position des Mauszeigers in ` lastPoint`. Außerdem vermerken wir, dass der Benutzer gerade etwas kritzelt. (Die Variable ` scribbling ` ist notwendig, da wir nicht davon ausgehen können, dass einem Mausbewegungs- und Mausloslass-Ereignis immer ein Mausklick-Ereignis auf demselben Widget vorausgeht.)

Wenn der Benutzer die Maus bei gedrückter linker Taste bewegt oder die Taste loslässt, rufen wir die private Funktion ` drawLineTo() ` auf, um zu zeichnen.

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

In der Neuimplementierung der Funktion ` paintEvent()` erstellen wir einfach ein ` QPainter ` für den Kritzelbereich und zeichnen das Bild.

An dieser Stelle fragen Sie sich vielleicht, warum wir nicht einfach direkt auf das Widget zeichnen, anstatt in einem ` QImage ` zu zeichnen und den ` QImage ` in ` paintEvent()` auf den Bildschirm zu kopieren. Dafür gibt es mindestens drei gute Gründe:

  • Das Fenstersystem verlangt, dass wir das Widget jederzeit neu zeichnen können. Wenn das Fenster beispielsweise minimiert und wiederhergestellt wird, hat das Fenstersystem möglicherweise den Inhalt des Widgets vergessen und sendet uns ein „paint“-Ereignis. Mit anderen Worten: Wir können uns nicht darauf verlassen, dass das Fenstersystem sich an unser Bild erinnert.
  • Qt erlaubt es uns normalerweise nicht, außerhalb von ` paintEvent()` zu zeichnen. Insbesondere können wir nicht aus den Mausereignis-Handlern heraus zeichnen. (Dieses Verhalten lässt sich jedoch mithilfe des Widget-Attributs ` Qt::WA_PaintOnScreen ` ändern.)
  • Bei korrekter Initialisierung verwendet ein „ QImage “ garantiert 8 Bit für jeden Farbkanal (Rot, Grün, Blau und Alpha), während ein „ QWidget “ je nach Monitorkonfiguration eine geringere Farbtiefe aufweisen kann. Das bedeutet: Wenn wir ein 24-Bit- oder 32-Bit-Bild laden und es auf ein ` QWidget` zeichnen und anschließend das ` QWidget ` wieder in ein ` QImage ` kopieren, gehen möglicherweise einige Informationen verloren.
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);
}

Wenn der Benutzer die Scribble-Anwendung startet, wird ein Resize-Ereignis ausgelöst, und ein Bild wird erstellt und im Scribble-Bereich angezeigt. Wir gestalten dieses Anfangsbild etwas größer als das Hauptfenster der Anwendung und den Scribble-Bereich, um zu vermeiden, dass das Bild jedes Mal neu skaliert werden muss, wenn der Benutzer die Größe des Hauptfensters ändert (was sehr ineffizient wäre). Wenn das Hauptfenster jedoch größer als diese Anfangsgröße wird, muss das Bild neu skaliert werden.

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

In ` drawLineTo()` zeichnen wir eine Linie von dem Punkt aus, an dem sich die Maus zum Zeitpunkt des letzten Mausklicks oder der letzten Mausbewegung befand, setzen ` modified ` auf `true`, lösen ein Repaint-Ereignis aus und aktualisieren ` lastPoint `, sodass wir beim nächsten Aufruf von ` drawLineTo() ` an der Stelle weiterzeichnen, an der wir aufgehört haben.

Wir könnten die Funktion ` update() ` ohne Parameter aufrufen, aber als einfache Optimierung übergeben wir einen ` QRect `, der das Rechteck innerhalb des Scribble-Bereichs angibt, das aktualisiert werden muss, um ein vollständiges Neuzeichnen des Widgets zu vermeiden.

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 bietet keine elegante API zum Ändern der Bildgröße. Es gibt zwar eine Funktion „ QImage::copy()“, die diesen Zweck erfüllen könnte, doch wenn man sie zum Vergrößern eines Bildes verwendet, füllt sie die neuen Bereiche mit Schwarz, während wir Weiß wünschen.

Der Trick besteht also darin, ein brandneues ` QImage ` mit der richtigen Größe zu erstellen, es mit Weiß zu füllen und das alte Bild mithilfe von ` QPainter` darauf zu zeichnen. Das neue Bild erhält das Format „ QImage::Format_RGB32 “, was bedeutet, dass jedes Pixel als 0xffRRGGBB gespeichert wird (wobei RR, GG und BB die Farbkanäle Rot, Grün und Blau sind und ff den Hexadezimalwert 255 darstellt).

Das Drucken wird über den Slot „ print() “ abgewickelt:

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

    QPrintDialog printDialog(&printer, this);

Wir erstellen ein hochauflösendes QPrinter-Objekt für das gewünschte Ausgabeformat und verwenden einen QPrintDialog, um den Benutzer aufzufordern, eine Seitengröße anzugeben und festzulegen, wie die Ausgabe auf der Seite formatiert werden soll.

Wird der Dialog bestätigt, führen wir den Druckvorgang auf dem Zeichen-Device durch:

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

Das Drucken eines Bildes in eine Datei auf diese Weise ist lediglich eine Frage des Zeichnens auf den QPrinter. Wir skalieren das Bild so, dass es in den verfügbaren Platz auf der Seite passt, bevor wir es auf das Zeichen-Gerät zeichnen.

Definition der Klasse „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;
};

Die Klasse „ MainWindow “ erbt von „ QMainWindow “. Wir implementieren den Handler „ closeEvent()“ aus „ QWidget “ neu. Die Slots „ open() “, „ save() “, „ penColor() “ und „ penWidth() “ entsprechen den Menüeinträgen. Darüber hinaus definieren wir vier private Funktionen.

Wir verwenden die boolesche Funktion „ maybeSave() “, um zu prüfen, ob ungespeicherte Änderungen vorliegen. Sind dies der Fall, geben wir dem Benutzer die Möglichkeit, diese Änderungen zu speichern. Die Funktion gibt „ false “ zurück, wenn der Benutzer auf „ Cancel “ klickt. Wir verwenden die Funktion „ saveFile() “, um dem Benutzer das Speichern des aktuell im Skizzenbereich angezeigten Bildes zu ermöglichen.

Implementierung der Klasse „MainWindow“

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

    createActions();
    createMenus();

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

Im Konstruktor erstellen wir einen Skizzenbereich, den wir zum zentralen Widget des „ MainWindow “-Widgets machen. Anschließend erstellen wir die zugehörigen Aktionen und Menüs.

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

Schließen-Ereignisse werden an Widgets gesendet, die der Benutzer schließen möchte, in der Regel durch Klicken auf „ File|Exit “ oder auf die Schaltfläche „ X “ in der Titelleiste. Durch die Neuimplementierung des Ereignishandlers können wir Versuche, die Anwendung zu schließen, abfangen.

In diesem Beispiel nutzen wir das Schließen-Ereignis, um den Benutzer aufzufordern, noch nicht gespeicherte Änderungen zu speichern. Die Logik hierfür befindet sich in der Funktion „ maybeSave() “. Wenn „ maybeSave() “ den Wert „true“ zurückgibt, liegen keine Änderungen vor oder der Benutzer hat sie erfolgreich gespeichert, und wir akzeptieren das Ereignis. Die Anwendung kann dann normal beendet werden. Wenn „ maybeSave() “ „false“ zurückgibt, hat der Benutzer auf „ Cancel “ geklickt, sodass wir das Ereignis „ignorieren“ und die Anwendung davon unberührt bleibt.

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

Im Slot „ open() “ geben wir dem Benutzer zunächst die Möglichkeit, etwaige Änderungen am aktuell angezeigten Bild zu speichern, bevor ein neues Bild in den Zeichenbereich geladen wird. Anschließend fordern wir den Benutzer auf, eine Datei auszuwählen, und laden die Datei über „ ScribbleArea “ ein.

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

Der Slot „ save() “ wird aufgerufen, wenn der Benutzer den Menüeintrag „ Save As “ auswählt und anschließend einen Eintrag aus dem Formatmenü wählt. Als Erstes müssen wir mithilfe von „ QObject::sender()“ herausfinden, welche Aktion das Signal gesendet hat. Diese Funktion gibt den Absender als „ QObject “-Zeiger zurück. Da wir wissen, dass es sich bei dem Absender um ein Aktionsobjekt handelt, können wir den „ QObject “ sicher umwandeln. Wir hätten einen Cast im C-Stil oder einen C++- static_cast<>() verwenden können, aber als defensive Programmiertechnik nutzen wir „ qobject_cast()“. Der Vorteil besteht darin, dass ein Nullzeiger zurückgegeben wird, wenn das Objekt den falschen Typ hat. Abstürze aufgrund von Nullzeigern lassen sich viel leichter diagnostizieren als Abstürze aufgrund unsicherer Casts.

Sobald wir die Aktion haben, extrahieren wir das gewählte Format mit ` QAction::data()`. (Beim Erstellen der Aktionen verwenden wir ` QAction::setData()`, um unsere eigenen benutzerdefinierten Daten als „ QVariant “ an die Aktion anzuhängen. Mehr dazu, wenn wir „ createActions() “ besprechen.)

Da wir nun das Format kennen, rufen wir die private Funktion ` saveFile() ` auf, um das aktuell angezeigte Bild zu speichern.

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

Wir nutzen den Slot „ penColor() “, um mit „ QColorDialog “ eine neue Farbe vom Benutzer abzufragen. Wählt der Benutzer eine neue Farbe aus, legen wir diese als Farbe für den Zeichenbereich fest.

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

Um im Slot „ penWidth() “ eine neue Stiftbreite abzurufen, verwenden wir „ QInputDialog “. Die Klasse „ QInputDialog “ stellt einen einfachen Komfortdialog bereit, um einen einzelnen Wert vom Benutzer abzurufen. Wir verwenden die statische Funktion „ QInputDialog::getInt()“, die „ QLabel “ und „ QSpinBox “ kombiniert. „ QSpinBox “ wird mit der Stiftbreite des Skizzenbereichs initialisiert und erlaubt einen Bereich von 1 bis 50 mit einer Schrittweite von 1 (das bedeutet, dass die Aufwärts- und Abwärtspfeile den Wert um 1 erhöhen bzw. verringern).

Die boolesche Variable „ ok “ wird auf „ true “ gesetzt, wenn der Benutzer auf „ OK “ geklickt hat, und auf „ false “, wenn der Benutzer „ Cancel “ gedrückt hat.

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

Wir implementieren den Slot „ about() “, um ein Meldungsfeld zu erstellen, das beschreibt, was das Beispiel veranschaulichen soll.

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

In der Funktion „ createAction() “ erstellen wir die Aktionen, die die Menüeinträge darstellen, und verbinden sie mit den entsprechenden Slots. Insbesondere erstellen wir die Aktionen, die im Untermenü „ Save As “ zu finden sind. Wir verwenden „ QImageWriter::supportedImageFormats()“, um eine Liste der unterstützten Formate abzurufen (als „ QList<QByteArray>“).

Anschließend durchlaufen wir die Liste und erstellen für jedes Format eine Aktion. Wir rufen ` QAction::setData()` mit dem Dateiformat auf, damit wir es später als ` QAction::data()` abrufen können. Wir hätten das Dateiformat auch aus dem Text der Aktion ableiten können, indem wir das „...“ abgeschnitten hätten, aber das wäre unelegant gewesen.

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

In der Funktion ` createMenu() ` fügen wir die zuvor erstellten Format-Aktionen zu ` saveAsMenu` hinzu. Anschließend fügen wir die restlichen Aktionen sowie das Untermenü „ saveAsMenu “ zu den Menüs ` File`, ` Options ` und ` Help ` hinzu.

Die Klasse „ QMenu “ stellt ein Menü-Widget zur Verwendung in Menüleisten, Kontextmenüs und anderen Popup-Menüs bereit. Die Klasse „ QMenuBar “ stellt eine horizontale Menüleiste mit einer Liste von ausklappbaren „ QMenus“ bereit. Zum Schluss fügen wir die Menüs „ File “ und „ Options “ in die Menüleiste von „ MainWindow “ ein, die wir mithilfe der Funktion „ QMainWindow::menuBar()“ abrufen.

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

In mayBeSave() prüfen wir, ob ungespeicherte Änderungen vorliegen. Ist dies der Fall, verwenden wir QMessageBox, um den Benutzer darauf hinzuweisen, dass das Bild geändert wurde, und ihm die Möglichkeit zu geben, die Änderungen zu speichern.

Wie bei „ QColorDialog “ und „ QFileDialog “ ist die einfachste Möglichkeit, eine „ QMessageBox “-Meldung zu erstellen, die Verwendung ihrer statischen Funktionen. „ QMessageBox “ bietet eine Reihe verschiedener Meldungen, die entlang zweier Achsen angeordnet sind: Schweregrad (Frage, Information, Warnung und kritisch) und Komplexität (die Anzahl der erforderlichen Antwortschaltflächen). Hier verwenden wir die Funktion „ warning() “, da die Meldung ziemlich wichtig ist.

Wenn der Benutzer sich für das Speichern entscheidet, rufen wir die private Funktion ` saveFile() ` auf. Der Einfachheit halber verwenden wir PNG als Dateiformat; der Benutzer kann jederzeit auf Cancel klicken und die Datei in einem anderen Format speichern.

Die Funktion „ maybeSave() “ gibt „ false “ zurück, wenn der Benutzer auf „ Cancel “ klickt; andernfalls gibt sie „ true “ zurück.

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

In „ saveFile() “ öffnen wir einen Dateidialog mit einem Dateinamensvorschlag. Die statische Funktion „ QFileDialog::getSaveFileName()“ gibt den vom Benutzer ausgewählten Dateinamen zurück. Die Datei muss nicht vorhanden sein.

Beispielprojekt @ 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.