이 페이지에서

Scribble 예제

스크리블 예제는 애플리케이션의 위젯에서 생성된 이벤트를 수신하기 위해 ` QWidget`의 일부 이벤트 핸들러를 재구현하는 방법을 보여줍니다.

그리기 기능을 구현하기 위해 마우스 이벤트 핸들러를, 애플리케이션을 업데이트하기 위해 페인트 이벤트 핸들러를, 애플리케이션의 외관을 최적화하기 위해 크기 조정 이벤트 핸들러를 재구현합니다. 또한, 애플리케이션을 종료하기 전에 닫기 이벤트를 가로채기 위해 닫기 이벤트 핸들러를 재구현합니다.

이 예제는 또한 QPainter 를 사용하여 실시간으로 이미지를 그리는 방법과 위젯을 다시 그리기(repaint)하는 방법을 보여줍니다.

Scribble 예제의 스크린샷

사용자는 Scribble 애플리케이션을 통해 이미지를 그릴 수 있습니다. File 메뉴를 통해 사용자는 기존 이미지 파일을 열고 편집하거나, 이미지를 저장하고, 애플리케이션을 종료할 수 있습니다. 그리는 동안 Options 메뉴를 통해 사용자는 펜 색상과 펜 두께를 선택하거나 화면을 지울 수 있습니다. 또한 Help 메뉴는 Scribble 예제에 대한 구체적인 정보와 Qt에 대한 일반적인 정보를 사용자에게 제공합니다.

이 예제는 두 개의 클래스로 구성되어 있습니다:

  • ScribbleArea 는 QImage 을 표시하고 사용자가 그 위에 그림을 그릴 수 있도록 하는 사용자 정의 위젯입니다.
  • MainWindow 는 ScribbleArea 상단에 메뉴를 제공합니다.

먼저 ScribbleArea 클래스를 살펴보겠습니다. 그 다음에는 ScribbleArea 를 사용하는 MainWindow 클래스를 살펴보겠습니다.

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

ScribbleArea 클래스는 QWidget 을 상속받습니다. 그리기 기능을 구현하기 위해 mousePressEvent(), mouseMoveEvent() 및 mouseReleaseEvent() 함수를 재구현합니다. 또한 스크리블 영역을 업데이트하기 위해 paintEvent() 함수를 재구현하고, 그리기 대상인 QImage 가 언제든지 위젯만큼은 커지도록 보장하기 위해 resizeEvent() 함수를 재구현합니다.

몇 가지 공용 함수가 필요합니다. ` openImage() `는 파일에서 이미지를 스크리블 영역으로 불러와 사용자가 이미지를 편집할 수 있게 하며, ` save() `는 현재 표시된 이미지를 파일로 저장하고, ` clearImage() ` 슬롯은 스크리블 영역에 표시된 이미지를 지웁니다. 실제로 그리기 작업을 수행하기 위해서는 비공개 함수 ` drawLineTo() `가 필요하며, ` QImage`의 크기를 변경하기 위해서는 ` resizeImage() `가 필요합니다. ` print() ` 슬롯은 출력 처리를 담당합니다.

또한 다음과 같은 비공개 변수가 필요합니다:

  • modified 스크리블 영역에 표시된 이미지에 저장되지 않은 변경 사항이 있는 경우 ` true `입니다.
  • scribbling 사용자가 스크리블 영역 내에서 마우스 왼쪽 버튼을 누르고 있는 동안 ` true `입니다.
  • penWidth penColor 는 애플리케이션에서 사용되는 펜의 현재 설정된 너비와 색상을 저장합니다.
  • image 사용자가 그린 이미지를 저장합니다.
  • lastPoint 마지막 마우스 클릭 또는 마우스 이동 이벤트 발생 시점의 커서 위치를 저장합니다.

ScribbleArea 클래스 구현

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

생성자에서 위젯의 Qt::WA_StaticContents 속성을 설정하여, 위젯의 내용이 왼쪽 상단 모서리를 기준으로 고정되어 있으며 위젯의 크기가 조정되어도 변경되지 않음을 나타냅니다. Qt Widgets는 이 속성을 사용하여 크기 조정 시 페인트 이벤트를 최적화합니다. 이는 순전히 최적화를 위한 것이며, 내용이 정적이고 왼쪽 상단 모서리에 고정된 위젯에만 사용해야 합니다.

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

openImage() 함수에서는 주어진 이미지를 로드합니다. 그런 다음 비공개 함수인 resizeImage() 를 사용하여 로드된 QImage 의 크기를 양방향 모두에서 위젯 크기 이상으로 조정하고, image 멤버 변수를 로드된 이미지로 설정합니다. 마지막으로 QWidget::update()을 호출하여 다시 그리기를 예약합니다.

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

saveImage() 함수는 실제 image 의 보이는 부분만을 덮는 QImage 객체를 생성하고, QImage::save()을 사용하여 이를 저장합니다. 이미지가 성공적으로 저장되면, 저장되지 않은 데이터가 없으므로 낙서 영역의 modified 변수를 false 으로 설정합니다.

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

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

setPenColor() 및 setPenWidth() 함수는 현재 펜 색상과 너비를 설정합니다. 이 값들은 향후 그리기 작업에 사용될 것입니다.

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

public clearImage() 슬롯은 낙서 영역에 표시된 이미지를 지웁니다. 단순히 전체 이미지를 RGB 값 (255, 255, 255)에 해당하는 흰색으로 채웁니다. 평소와 마찬가지로 이미지를 수정할 때는 modified 을 true 로 설정하고 다시 그리기를 예약합니다.

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

마우스 클릭 및 마우스 릴리스 이벤트의 경우, ` QMouseEvent::button()` 함수를 사용하여 어떤 버튼이 이벤트를 발생시켰는지 확인합니다. 마우스 이동 이벤트의 경우, ` QMouseEvent::buttons()`을 사용하여 현재 누르고 있는 버튼(OR 조합으로)을 확인합니다.

사용자가 마우스 왼쪽 버튼을 누르면, 마우스 커서의 위치를 lastPoint 에 저장합니다. 또한 사용자가 현재 낙서를 하고 있다는 사실도 기록해 둡니다. ( scribbling 변수가 필요한 이유는, 마우스 이동 및 마우스 릴리스 이벤트가 항상 동일한 위젯에서 마우스 클릭 이벤트에 이어 발생한다고 가정할 수 없기 때문입니다.)

사용자가 왼쪽 버튼을 누른 상태에서 마우스를 이동하거나 버튼을 놓으면, 비공개 함수인 ` drawLineTo() `를 호출하여 그림을 그립니다.

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

paintEvent() 함수를 재구현할 때, 낙서 영역에 대한 QPainter 를 생성하고 이미지를 그립니다.

이 시점에서, 왜 ` QImage `에서 그리는 대신 위젯에 직접 그리지 않고, ` paintEvent()`에서 ` QImage `을 화면으로 복사하는지 궁금할 수 있습니다. 이에 대한 타당한 이유는 적어도 세 가지가 있습니다:

  • 창 시스템은 우리가 언제든지 위젯을 다시 그릴 수 있어야 한다고 요구합니다. 예를 들어, 창이 최소화되었다가 복원되면 창 시스템이 위젯의 내용을 잊어버리고 우리에게 페인트 이벤트를 보낼 수 있습니다. 즉, 창 시스템이 우리의 이미지를 기억해 줄 것이라고 기대할 수 없습니다.
  • Qt는 일반적으로 ` paintEvent()` 외부에서 페인트를 허용하지 않습니다. 특히, 마우스 이벤트 핸들러 내에서 페인트를 수행할 수 없습니다. (다만, ` Qt::WA_PaintOnScreen ` 위젯 속성을 사용하여 이 동작을 변경할 수는 있습니다.)
  • 올바르게 초기화된 경우, ` QImage `는 각 색상 채널(빨강, 녹색, 파랑, 알파)에 대해 8비트를 사용하도록 보장되지만, ` QWidget `는 모니터 구성에 따라 더 낮은 색상 심도를 가질 수 있습니다. 즉, 24비트 또는 32비트 이미지를 불러와 QWidget 에 그린 다음, QWidget 를 다시 QImage 로 복사하면 일부 정보가 손실될 수 있습니다.
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);
}

사용자가 Scribble 애플리케이션을 시작하면 크기 조정 이벤트가 발생하고, 이미지가 생성되어 낙서 영역에 표시됩니다. 사용자가 메인 창 크기를 조정할 때마다 이미지를 매번 재조정해야 하는 상황(이는 매우 비효율적일 것입니다)을 피하기 위해, 이 초기 이미지를 애플리케이션의 메인 창과 스크리블 영역보다 약간 더 크게 만듭니다. 하지만 메인 창이 이 초기 크기보다 커지면 이미지의 크기도 조정해야 합니다.

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

drawLineTo() 에서, 마지막 마우스 클릭 또는 마우스 이동이 발생했을 때 마우스가 위치했던 지점에서 선을 그리고, modified 를 true로 설정하며, repaint 이벤트를 생성하고, lastPoint 를 업데이트하여 다음에 drawLineTo() 가 호출될 때 중단된 지점부터 계속 그리도록 합니다.

update() 함수를 매개변수 없이 호출할 수도 있지만, 간단한 최적화 방안으로 QRect 를 전달하여 낙서 영역 내의 업데이트가 필요한 사각형을 지정함으로써 위젯의 전체적인 다시 그리기를 피합니다.

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 에는 이미지 크기를 조정하는 편리한 API가 없습니다. QImage::copy() 함수를 사용하면 이 작업을 수행할 수 있지만, 이미지를 확대할 때 이 함수는 새로운 영역을 검은색으로 채우는 반면, 우리는 흰색을 원합니다.

따라서 해결책은 올바른 크기의 완전히 새로운 ` QImage `을 생성하여 이를 흰색으로 채운 다음, ` QPainter`을 사용하여 기존 이미지를 그 위에 그리는 것입니다. 새 이미지는 QImage::Format_RGB32 형식을 가지며, 이는 각 픽셀이 0xffRRGGBB(여기서 RR, GG, BB는 각각 빨강, 녹색, 파랑 색상 채널이고, ff는 16진수 값 255를 나타냄)로 저장됨을 의미합니다.

인쇄 처리는 ` print() ` 슬롯에서 처리됩니다:

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

    QPrintDialog printDialog(&printer, this);

필요한 출력 형식에 맞는 고해상도 QPrinter 객체를 생성하고, QPrintDialog를 사용하여 사용자에게 페이지 크기를 지정하고 페이지 내 출력 형식을 설정하도록 요청합니다.

대화 상자가 승인되면, 페인트 장치로 인쇄 작업을 수행합니다:

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

이러한 방식으로 이미지를 파일에 인쇄하는 것은 단순히 QPrinter에 그리는 작업에 불과합니다. 이미지를 페인트 장치에 그리기 전에, 페이지의 사용 가능한 공간에 맞도록 이미지를 크기를 조정합니다.

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

MainWindow 클래스는 QMainWindow 를 상속받습니다. 우리는 QWidget 에서 closeEvent() 핸들러를 재구현합니다. open(), save(), penColor() 및 penWidth() 슬롯은 메뉴 항목에 해당합니다. 또한 네 개의 비공개 함수를 생성합니다.

부울 값을 반환하는 ` maybeSave() ` 함수를 사용하여 저장되지 않은 변경 사항이 있는지 확인합니다. 저장되지 않은 변경 사항이 있는 경우, 사용자에게 해당 변경 사항을 저장할 기회를 제공합니다. 사용자가 ` Cancel`을 클릭하면 이 함수는 ` false `을 반환합니다. ` saveFile() ` 함수를 사용하여 사용자가 스크리블 영역에 현재 표시된 이미지를 저장할 수 있도록 합니다.

MainWindow 클래스 구현

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

    createActions();
    createMenus();

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

생성자에서 스크리블 영역을 생성하고, 이를 MainWindow 위젯의 중심 위젯으로 설정합니다. 그런 다음 관련 동작과 메뉴를 생성합니다.

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

닫기 이벤트는 사용자가 닫고자 하는 위젯으로 전송되며, 일반적으로 ‘ File|Exit ’을 클릭하거나 ‘ X ’ 제목 표시줄 버튼을 클릭하여 발생합니다. 이벤트 핸들러를 재구현함으로써 애플리케이션을 닫으려는 시도를 가로챌 수 있습니다.

이 예제에서는 닫기 이벤트를 사용하여 사용자에게 저장되지 않은 변경 사항이 있는지 확인하고 저장하도록 요청합니다. 이에 대한 로직은 ` maybeSave() ` 함수에 있습니다. ` maybeSave() `가 `true`를 반환하면 변경 사항이 없거나 사용자가 성공적으로 저장한 것이므로 이벤트를 수락합니다. 그러면 애플리케이션은 정상적으로 종료됩니다. maybeSave() 가 false를 반환하면, 사용자가 Cancel 를 클릭한 것이므로 이벤트를 “무시”하여 애플리케이션이 이에 영향을 받지 않도록 합니다.

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

open() 슬롯에서는 먼저 사용자에게 현재 표시된 이미지의 수정 내용을 저장할 기회를 준 다음, 새 이미지를 낙서 영역에 불러옵니다. 그런 다음 사용자에게 파일을 선택하도록 요청하고, ScribbleArea 에서 해당 파일을 불러옵니다.

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

save() 슬롯은 사용자가 Save As 메뉴 항목을 선택한 후, 포맷 메뉴에서 항목을 선택할 때 호출됩니다. 가장 먼저 해야 할 일은 QObject::sender()를 사용하여 어떤 액션이 신호를 보냈는지 파악하는 것입니다. 이 함수는 발신자를 QObject 포인터로 반환합니다. 발신자가 액션 객체임을 알고 있으므로, QObject 로 안전하게 형변환할 수 있습니다. C 스타일의 형 변환이나 C++의 ` static_cast<>()`을 사용할 수도 있었겠지만, 방어적 프로그래밍 기법으로서 ` qobject_cast()`를 사용합니다. 이 방법의 장점은 객체의 유형이 잘못되었을 경우 null 포인터가 반환된다는 점입니다. null 포인터로 인한 크래시는 안전하지 않은 형 변환으로 인한 크래시보다 진단하기가 훨씬 쉽습니다.

액션을 확보한 후에는 QAction::data()를 사용하여 선택된 형식을 추출합니다. (액션이 생성될 때, QAction::setData()를 사용하여 액션에 첨부된 사용자 정의 데이터를 QVariant 로 설정합니다. 이에 대한 자세한 내용은 createActions() 를 검토할 때 다루겠습니다.)

이제 형식을 알았으니, 비공개 함수인 saveFile() 를 호출하여 현재 표시된 이미지를 저장합니다.

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

penColor() 슬롯을 사용하여 QColorDialog 을 통해 사용자로부터 새로운 색상을 가져옵니다. 사용자가 새로운 색상을 선택하면, 이를 낙서 영역의 색상으로 설정합니다.

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

penWidth() 슬롯에서 새로운 펜 너비를 가져오기 위해 QInputDialog 를 사용합니다. QInputDialog 클래스는 사용자로부터 단일 값을 가져올 수 있는 간단한 편의 대화 상자를 제공합니다. 우리는 ` QLabel `과 ` QSpinBox`을 결합한 정적 함수 ` QInputDialog::getInt()`를 사용합니다. ` QSpinBox `는 낙서 영역의 펜 너비로 초기화되며, 1부터 50까지의 범위를 허용하고, 단계는 1입니다(즉, 위쪽 및 아래쪽 화살표 키를 누르면 값이 1씩 증가하거나 감소합니다).

부울 변수 ` ok `는 사용자가 ` OK `를 클릭하면 ` true `로, ` Cancel`를 누르면 ` false `로 설정됩니다.

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

about() 슬롯을 구현하여 이 예제가 무엇을 보여주기 위해 설계되었는지를 설명하는 메시지 상자를 표시합니다.

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

createAction() 함수에서는 메뉴 항목을 나타내는 액션을 생성하고 이를 적절한 슬롯에 연결합니다. 특히 Save As 하위 메뉴에 있는 액션들을 생성합니다. QImageWriter::supportedImageFormats()을 사용하여 지원되는 형식 목록을 가져옵니다( QList<QByteArray>).

그런 다음 목록을 순회하며 각 형식에 대한 액션을 생성합니다. 파일 형식을 매개변수로 전달하여 ` QAction::setData()`를 호출함으로써, 나중에 ` QAction::data()`를 통해 이를 불러올 수 있게 합니다. 액션의 텍스트에서 “...”를 잘라내어 파일 형식을 추론할 수도 있었겠지만, 그 방법은 우아하지 않았을 것입니다.

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

createMenu() 함수 내에서, 앞서 생성한 형식 액션들을 saveAsMenu 에 추가합니다. 그런 다음 나머지 액션들과 saveAsMenu 하위 메뉴를 File, Options 및 Help 메뉴에 추가합니다.

QMenu 클래스는 메뉴 바, 컨텍스트 메뉴 및 기타 팝업 메뉴에서 사용할 수 있는 메뉴 위젯을 제공합니다. QMenuBar 클래스는 풀다운 QMenu목록이 포함된 수평 메뉴 바를 제공합니다. 마지막으로 File 및 Options 메뉴를 MainWindow 의 메뉴 바에 배치하며, 이 메뉴 바는 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;
}

mayBeSave() 에서는 저장되지 않은 변경 사항이 있는지 확인합니다. 변경 사항이 있는 경우, QMessageBox 를 사용하여 사용자에게 이미지가 수정되었음을 경고하고, 변경 사항을 저장할 기회를 제공합니다.

QColorDialog 및 QFileDialog 와 마찬가지로, QMessageBox 를 생성하는 가장 쉬운 방법은 정적 함수를 사용하는 것입니다. QMessageBox 는 심각도(질문, 정보, 경고, 중요)와 복잡도(필요한 응답 버튼의 수)라는 두 가지 축에 따라 분류된 다양한 메시지를 제공합니다. 여기서는 메시지가 상당히 중요하므로 warning() 함수를 사용합니다.

사용자가 저장을 선택하면, 비공개 함수인 saveFile() 를 호출합니다. 간편성을 위해 파일 형식으로 PNG를 사용하지만, 사용자는 언제든지 Cancel 를 눌러 다른 형식으로 파일을 저장할 수 있습니다.

maybeSave() 함수는 사용자가 Cancel 를 클릭하면 false 를 반환하고, 그렇지 않으면 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());
}

saveFile() 에서는 파일 이름 제안이 포함된 파일 대화 상자를 표시합니다. 정적 함수 QFileDialog::getSaveFileName()는 사용자가 선택한 파일 이름을 반환합니다. 해당 파일이 실제로 존재하지 않아도 됩니다.

예제 프로젝트 @ 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.