이 페이지에서

문서 뷰어

JSON, 텍스트 및 PDF 파일을 표시하고 인쇄하는 위젯 애플리케이션입니다.

'열기 모드 선택' 팝업이 표시된 문서 뷰어 UI

문서 뷰어는 정적 및 동적 툴바, 메뉴, 동작을 갖춘 QMainWindow 의 사용 방법을 보여줍니다. 또한 위젯 기반 애플리케이션에서 다음과 같은 기능을 시연합니다:

  • QSettings 를 사용하여 사용자 기본 설정을 조회 및 저장하고, 이전에 열었던 파일 기록을 관리하는 방법.
  • 위젯 위에 마우스를 올렸을 때 커서 동작 제어.
  • 동적으로 로드되는 플러그인 생성.
  • UI를 다양한 언어로 현지화하는 방법.

예제 실행

다음 위치에서 예제를 실행할 수 있습니다.

애플리케이션 및 메인 창 만들기

애플리케이션과 메인 창은 ` main.cpp`에서 생성됩니다. `main()` 함수는 ` QCommandLineParser `을 사용하여 명령줄 인수(도움말, 버전, 선택적 위치 인수인 ` file`)를 처리합니다. 사용자가 애플리케이션을 실행할 때 파일 경로를 지정한 경우, 메인 창에서 해당 파일을 엽니다:

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QCoreApplication::setOrganizationName("QtProject"_L1);
    QCoreApplication::setApplicationName("DocumentViewer"_L1);
    QCoreApplication::setApplicationVersion("1.0"_L1);

    Translator mainTranslator;
    mainTranslator.setBaseName("docviewer"_L1);
    mainTranslator.install();

    QCommandLineParser parser;
    parser.setApplicationDescription(Tr::tr("A viewer for JSON, PDF and text files"));
    parser.addHelpOption();
    parser.addVersionOption();
    parser.addPositionalArgument("File"_L1, Tr::tr("JSON, PDF or text file to open"));
    parser.process(app);

    const QStringList &positionalArguments = parser.positionalArguments();
    const QString &fileName = (positionalArguments.count() > 0) ? positionalArguments.at(0)
                                                                : QString();

    MainWindow w(mainTranslator);

    // Start application only if plugins are available
    if (!w.hasPlugins()) {
        QMessageBox::critical(nullptr,
                              Tr::tr("No viewer plugins found"),
                              Tr::tr("Unable to load viewer plugins. Exiting application."));
        return 1;
    }

    w.show();
    if (!fileName.isEmpty())
        w.openFile(fileName);

    return app.exec();
}

MainWindow 클래스

MainWindow 클래스는 메뉴, 동작 및 툴바가 포함된 애플리케이션 화면을 제공합니다. 이 클래스는 파일의 콘텐츠 유형을 자동으로 감지하여 파일을 열 수 있습니다. 또한 QSettings 을 사용하여 이전에 열었던 파일 목록을 관리하며, 애플리케이션 실행 시 설정을 저장하고 다시 불러옵니다. MainWindow는 열린 파일의 콘텐츠 유형에 따라 적절한 뷰어를 생성하고, 문서 인쇄 기능을 지원합니다.

MainWindow의 생성자는 Qt Designer 에서 생성된 사용자 인터페이스를 초기화합니다. mainwindow.ui 파일은 왼쪽에 북마크와 썸네일을 표시하는 QTabWidget 를 제공합니다. 오른쪽에는 파일 내용을 볼 수 있는 QScrollArea 가 있습니다.

ViewerFactory 클래스

ViewerFactory 클래스는 알려진 파일 유형에 대한 뷰어를 관리합니다. 이러한 뷰어는 플러그인으로 구현됩니다. ViewerFactory 인스턴스가 생성될 때, 뷰 영역과 메인 윈도우에 대한 포인터가 생성자에 전달됩니다:

m_factory.reset(new ViewerFactory(ui->viewArea, this));

ViewerFactory는 생성 시 사용 가능한 모든 플러그인을 로드합니다. 이 클래스는 로드된 플러그인, 플러그인 이름 및 지원되는 MIME 유형을 조회할 수 있는 공개 API를 제공합니다:

    using ViewerList = QList<AbstractViewer *>;
    QStringList viewerNames(bool showDefault = false) const;
    ViewerList viewers() const;
    AbstractViewer *findViewer(const QString &viewerName) const;
    AbstractViewer *defaultViewer() const;
    QStringList supportedMimeTypes() const;

viewer() 함수는 인수로 전달된 ` QFile `을 여는 데 적합한 플러그인에 대한 포인터를 반환합니다:

m_viewer = m_factory->viewer(file);

애플리케이션 설정에 뷰어 관련 섹션이 포함되어 있는 경우, 해당 설정은 뷰어의 가상 restoreState() 함수로 전달됩니다:

void MainWindow::restoreViewerSettings()
{
    if (!m_viewer)
        return;

    QSettings settings;
    settings.beginGroup(settingsViewers);
    QByteArray viewerSettings = settings.value(m_viewer->viewerName(), QByteArray()).toByteArray();
    settings.endGroup();
    if (!viewerSettings.isEmpty())
        m_viewer->restoreState(viewerSettings);
}

그런 다음 표준 UI 리소스가 뷰어로 전달되고, 메인 스크롤 영역이 뷰어의 디스플레이 위젯을 표시하도록 설정됩니다:

    m_viewer->initViewer(ui->actionBack, ui->actionForward, ui->menuHelp->menuAction(), ui->tabWidget);
    restoreViewerSettings();
    ui->scrollArea->setWidget(m_viewer->widget());
    return true;
}

AbstractViewer 클래스

AbstractViewer 는 문서를 보고, 저장하고, 인쇄하기 위한 일반화된 API를 제공합니다. 문서와 뷰어의 속성을 모두 조회할 수 있습니다:

  • 문서에 내용이 포함되어 있습니까?
  • 수정된 적이 있습니까?
  • 개요(썸네일 또는 북마크)가 지원되나요?

AbstractViewer는 파생 클래스가 메인 창에 액션과 메뉴를 생성할 수 있도록 protected 메서드를 제공합니다. 이러한 요소를 메인 창에 표시하기 위해, 해당 요소들은 메인 창을 부모로 설정됩니다. AbstractViewer는 자신이 생성한 UI 요소를 제거하고 소멸시키는 역할을 담당합니다. 이 클래스는 신호와 슬롯을 구현하기 위해 ` QObject `을 상속받습니다.

신호

void uiInitialized();

이 신호는 뷰어가 메인 창의 UI 자산에 대한 모든 필수 정보를 수신한 후에 발생합니다.

void printingEnabledChanged(bool enabled);

이 신호는 문서 인쇄가 활성화되거나 비활성화될 때 발생합니다. 이는 새 문서가 성공적으로 로드된 후, 또는 예를 들어 모든 콘텐츠가 제거된 후에 발생합니다.

void showMessage(const QString &message, int timeout = 8000);

이 시그널은 사용자에게 상태 메시지를 표시하기 위해 발송됩니다.

void documentLoaded(const QString &fileName);

이 신호는 문서가 성공적으로 로드되었음을 애플리케이션에 알립니다.

TxtViewer 클래스

TxtViewer 는 AbstractViewer를 상속받은 간단한 텍스트 뷰어입니다. 이 클래스는 텍스트 파일 편집, 복사/잘라내기 및 붙여넣기, 인쇄, 변경 사항 저장을 지원합니다.

클래스 정의
class TxtViewer : public ViewerInterface
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.DocumentViewer.ViewerInterface" FILE "txtviewer.json")
    Q_INTERFACES(ViewerInterface)

클래스 정의는 신호와 슬롯을 처리하는 ` Q_OBJECT ` 매크로로 시작됩니다. 그 다음에는 플러그인을 등록하는 데 필요한 ` Q_PLUGIN_METADATA ` 및 ` Q_INTERFACES ` 매크로가 이어집니다.

이 클래스는 ViewerInterface 을 상속받으며, 은 AbstractViewer 을 상속받습니다. ViewerInterface 클래스는 메인 윈도우 애플리케이션과 플러그인 간의 인터페이스를 제공하는 데 사용됩니다.

QPluginLoader 또한 txtviewer.json 파일이 필요하며, 이 파일에는 플러그인의 키가 포함되어야 합니다:

{ "Keys": [ "txtviewer" ] }

이 클래스는 생성자를 정의하지 않으므로, 인자가 없는 표준 생성자만 사용할 수 있습니다. 소멸자를 포함한 다른 모든 함수는 ` ViewerInterface`의 가상 함수를 재구현한 것입니다. 이들은 메인 애플리케이션과 데이터, 정보 및 명령을 교환하는 데 사용됩니다.

public:
    TxtViewer();
    ~TxtViewer() override;
    void init(QFile *file, QWidget *parent, QMainWindow *mainWindow) override;
    QString viewerName() const override { return QLatin1StringView(staticMetaObject.className()); };
    QStringList supportedMimeTypes() const override;
    bool saveDocument() override { return saveFile(m_file.get()); };
    bool saveDocumentAs() override;
    bool hasContent() const override;
    QByteArray saveState() const override { return {}; }
    bool restoreState(QByteArray &) override { return true; }
    bool supportsOverview() const override { return false; }

#ifdef DOCUMENTVIEWER_PRINTSUPPORT
protected:
    void printDocument(QPrinter *printer) const override;
#endif // DOCUMENTVIEWER_PRINTSUPPORT

private slots:
    void setupTxtUi();

private:
    void retranslate() override;
    void openFile();
    bool saveFile (QFile *file);

    QPlainTextEdit *m_textEdit;
    QAction *m_cutAct = nullptr;
    QAction *m_copyAct = nullptr;
    QAction *m_pasteAct = nullptr;
};
TxtViewer 클래스 구현
#include "txtviewer.h"

#include <QFileDialog>
#include <QMainWindow>
#include <QMenu>
#include <QMenuBar>
#include <QPlainTextEdit>
#include <QScrollBar>
#include <QToolBar>

#include <QGuiApplication>
#include <QPainter>
#include <QTextDocument>

#include <QDir>

#ifdef DOCUMENTVIEWER_PRINTSUPPORT
#include <QPrinter>
#include <QPrintDialog>
#endif

using namespace Qt::StringLiterals;

TxtViewer::TxtViewer()
    : m_cutAct(new QAction(this)),
      m_copyAct(new QAction(this)),
      m_pasteAct(new QAction(this))
{
    connect(this, &AbstractViewer::uiInitialized, this, &TxtViewer::setupTxtUi);

    const QIcon cutIcon = QIcon::fromTheme(QIcon::ThemeIcon::EditCut,
                                           QIcon(":/demos/documentviewer/images/cut.png"_L1));
    m_cutAct->setIcon(cutIcon);
    m_cutAct->setShortcuts(QKeySequence::Cut);

    const QIcon copyIcon = QIcon::fromTheme(QIcon::ThemeIcon::EditCopy,
                                            QIcon(":/demos/documentviewer/images/copy.png"_L1));
    m_copyAct->setIcon(copyIcon);
    m_copyAct->setShortcuts(QKeySequence::Copy);

    const QIcon pasteIcon = QIcon::fromTheme(QIcon::ThemeIcon::EditPaste,
                                             QIcon(":/demos/documentviewer/images/paste.png"_L1));
    m_pasteAct->setIcon(pasteIcon);
    m_pasteAct->setShortcuts(QKeySequence::Paste);
}

TxtViewer::~TxtViewer() = default;

void TxtViewer::init(QFile *file, QWidget *parent, QMainWindow *mainWindow)
{
    AbstractViewer::init(file, new QPlainTextEdit(parent), mainWindow);
    m_textEdit = qobject_cast<QPlainTextEdit *>(widget());
    setTranslationBaseName("txtviewer"_L1);
}

QStringList TxtViewer::supportedMimeTypes() const
{
    return {"text/plain"_L1};
}

void TxtViewer::setupTxtUi()
{
    QMenu *editMenu = addMenu();
    QToolBar *editToolBar = addToolBar();
#if QT_CONFIG(clipboard)
    connect(m_cutAct, &QAction::triggered, m_textEdit, &QPlainTextEdit::cut);
    editMenu->addAction(m_cutAct);
    editToolBar->addAction(m_cutAct);

    connect(m_copyAct, &QAction::triggered, m_textEdit, &QPlainTextEdit::copy);
    editMenu->addAction(m_copyAct);
    editToolBar->addAction(m_copyAct);

    connect(m_pasteAct, &QAction::triggered, m_textEdit, &QPlainTextEdit::paste);
    editMenu->addAction(m_pasteAct);
    editToolBar->addAction(m_pasteAct);

    menuBar()->addSeparator();

    m_cutAct->setEnabled(false);
    m_copyAct->setEnabled(false);
    connect(m_textEdit, &QPlainTextEdit::copyAvailable, m_cutAct, &QAction::setEnabled);
    connect(m_textEdit, &QPlainTextEdit::copyAvailable, m_copyAct, &QAction::setEnabled);
#endif // QT_CONFIG(clipboard)

    openFile();

    connect(m_textEdit, &QPlainTextEdit::textChanged, this, [&](){
        maybeSetPrintingEnabled(hasContent());
    });

    connect(m_uiAssets.back, &QAction::triggered, m_textEdit, [&](){
        auto *bar = m_textEdit->verticalScrollBar();
        if (bar->value() > bar->minimum())
            bar->setValue(bar->value() - 1);
    });

    connect(m_uiAssets.forward, &QAction::triggered, m_textEdit, [&](){
        auto *bar = m_textEdit->verticalScrollBar();
        if (bar->value() < bar->maximum())
            bar->setValue(bar->value() + 1);
    });

    retranslate();
}

먼저, ` TxtViewer`에서 사용하는 모든 클래스에 접근하는 데 필요한 헤더 파일을 포함합니다. 또한 ` txtviewer.h`도 포함합니다.

QPrinter QPrintDialog 은 컴파일 시스템에서 인쇄 기능이 활성화된 경우에만 포함됩니다.

이 헤더 파일들은 mainwindow.h 에 직접 포함되지 않는다는 점에 유의하십시오. 다른 헤더 파일에 대용량 헤더 파일을 포함하면 빌드 성능에 영향을 줄 수 있습니다. 이 경우 문제가 발생하지는 않겠지만, 의존성을 최소화하기 위해 필요한 헤더만 포함하는 것이 모범 사례입니다.

구현은 빈 소멸자로 시작합니다. 이 소멸자는 완전히 생략할 수도 있습니다. 소멸자에서 수행할 작업이 없음을 코드 독자에게 명확히 알리기 위해 빈 소멸자를 구현하는 것이 좋은 관행입니다.

소멸자 다음에는 세 개의 인자를 받는 초기화 함수가 나옵니다:

  • file, 열어서 표시할 파일의 포인터.
  • parent, 편집기가 배치될 QWidget 을 가리킵니다.
  • mainWindow, 메뉴와 메뉴 바가 처리되는 애플리케이션의 메인 윈도우를 가리킵니다.

이 함수는 ` AbstractViewer`의 기본 초기화 함수를 호출합니다. 파일 내용을 표시할 새로운 ` QPlainTextEdit ` 위젯이 생성됩니다. 그런 다음, ` TxtViewer`의 설정 함수가 기본 클래스의 ` uiInitialized ` 신호에 연결됩니다.

다음 함수는 텍스트 뷰어가 지원하는 MIME 유형 목록을 반환합니다. 일반 텍스트만 지원됩니다.

마지막 초기화 함수는 메뉴, 아이콘, 버튼, 툴팁과 같은 뷰어 전용 UI 구성 요소를 추가합니다. 이 함수는 AbstractViewer 에서 제공하는 기능을 사용하여, 다른 뷰어 플러그인으로 다른 파일이 표시되면 이러한 구성 요소가 애플리케이션의 메인 창에서 제거되도록 합니다.

void TxtViewer::openFile()
{
    const QString type = tr("open");
    if (!m_file->open(QFile::ReadOnly | QFile::Text)) {
        statusMessage(tr("Cannot read file %1:\n%2.")
                      .arg(QDir::toNativeSeparators(m_file->fileName()),
                           m_file->errorString()), type);
        return;
    }

    QTextStream in(m_file.get());
#if QT_CONFIG(cursor)
    QGuiApplication::setOverrideCursor(Qt::WaitCursor);
#endif
    if (!m_textEdit->toPlainText().isEmpty()) {
        m_textEdit->clear();
        disablePrinting();
    }
    m_textEdit->setPlainText(in.readAll());
#if QT_CONFIG(cursor)
    QGuiApplication::restoreOverrideCursor();
#endif

    statusMessage(tr("File %1 loaded.")
                  .arg(QDir::toNativeSeparators(m_file->fileName())), type);
    maybeEnablePrinting();
}

openFile 파일을 열고, 그 내용을 QPlainTextEdit 로 전송하며, 파일 열기가 성공했는지 여부에 따라 사용자에게 상태 메시지를 출력합니다.

bool TxtViewer::hasContent() const
{
    return (!m_textEdit->toPlainText().isEmpty());
}

#ifdef DOCUMENTVIEWER_PRINTSUPPORT
void TxtViewer::printDocument(QPrinter *printer) const
{
    if (!hasContent())
        return;

    m_textEdit->print(printer);
}
#endif // DOCUMENTVIEWER_PRINTSUPPORT

bool TxtViewer::saveFile(QFile *file)
{
    QString errorMessage;

    QGuiApplication::setOverrideCursor(Qt::WaitCursor);
    if (file->open(QFile::WriteOnly | QFile::Text)) {
        QTextStream out(file);
        out << m_textEdit->toPlainText();
    } else {
        errorMessage = tr("Cannot open file %1 for writing:\n%2.")
                       .arg(QDir::toNativeSeparators(file->fileName())),
                            file->errorString();
    }
    QGuiApplication::restoreOverrideCursor();

    if (!errorMessage.isEmpty()) {
        statusMessage(errorMessage);
        return false;
    }

    statusMessage(tr("File %1 saved")
                  .arg(QDir::toNativeSeparators(file->fileName())));
    return true;
}

bool TxtViewer::saveDocumentAs()
{
    QFileDialog dialog(mainWindow());
    dialog.setWindowModality(Qt::WindowModal);
    dialog.setAcceptMode(QFileDialog::AcceptSave);
    if (dialog.exec() != QDialog::Accepted)
        return false;

    const QStringList &files = dialog.selectedFiles();
    if (files.isEmpty())
        return false;

    //newFile();
    m_file->setFileName(files.first());
    return saveDocument();
}

void TxtViewer::retranslate()
{
    if (toolBars().isEmpty())
        return;
    menus().at(0)->setTitle(tr("&Edit"));
    toolBars().at(0)->setWindowTitle(tr("Edit"));

    m_cutAct->setText(tr("Cu&t"));
    m_cutAct->setStatusTip(tr("Cut the current selection's contents to the clipboard"));
    m_copyAct->setText(tr("&Copy"));
    m_copyAct->setStatusTip(tr("Copy the current selection's contents to the clipboard"));
    m_pasteAct->setText(tr("&Paste"));
    m_pasteAct->setStatusTip(tr("Paste the clipboard's contents into the current selection"));
}

다음으로 재구현된 함수는 뷰어 플러그인이 실제로 콘텐츠를 표시하고 있는지 여부를 메인 애플리케이션에 알립니다.

컴파일 시스템에서 인쇄가 지원되는 경우, 다음 섹션에서 이를 구현합니다.

마지막 두 가지 재구현은 현재 파일을 저장하거나 새 이름으로 저장하는 기능을 제공합니다.

ImageViewer 클래스

ImageViewer 은 ` QLabel`을 사용하여 ` QImageReader`에서 지원하는 방식으로 이미지를 표시합니다.

생성자에서 더 큰 사진을 처리할 수 있도록 QImageReader 의 할당 한도를 늘립니다:

ImageViewer::ImageViewer()
    : m_zoomInAct(new QAction(this)),
      m_zoomOutAct(new QAction(this)),
      m_resetZoomAct(new QAction(this)),
      m_formats(imageFormats())
{
    connect(this, &AbstractViewer::uiInitialized, this, &ImageViewer::setupImageUi);
    QImageReader::setAllocationLimit(1024); // MB

    m_zoomInAct->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::ZoomIn));
    m_zoomInAct->setShortcut(QKeySequence::ZoomIn);
    connect(m_zoomInAct, &QAction::triggered, this, &ImageViewer::zoomIn);

    m_zoomOutAct->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::ZoomOut));
    m_zoomOutAct->setShortcut(QKeySequence::ZoomOut);
    connect(m_zoomOutAct, &QAction::triggered, this, &ImageViewer::zoomOut);

    m_resetZoomAct->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::ZoomFitBest));
    m_resetZoomAct->setShortcut(QKeySequence(Qt::ControlModifier | Qt::Key_0));
    connect(m_resetZoomAct, &QAction::triggered, this, &ImageViewer::resetZoom);
}

openFile() 함수에서는 이미지를 불러와 크기를 확인합니다. 화면보다 큰 경우, 종횡비를 유지하면서 화면 크기로 축소합니다. 이 계산은 네이티브 픽셀 단위로 수행되어야 하며, 결과 픽스맵에 장치 픽셀 비율을 설정해야 선명하게 표시됩니다:

void ImageViewer::openFile()
{
#if QT_CONFIG(cursor)
    QGuiApplication::setOverrideCursor(Qt::WaitCursor);
#endif
    const QString name = m_file->fileName();
    QImageReader reader(name);
    const QImage origImage = reader.read();
    if (origImage.isNull()) {
        statusMessage(tr("Cannot read file %1:\n%2.")
                      .arg(QDir::toNativeSeparators(name),
                           reader.errorString()), tr("open"));
        disablePrinting();
#if QT_CONFIG(cursor)
        QGuiApplication::restoreOverrideCursor();
#endif
        return;
    }

    clear();

    QImage image = origImage.colorSpace().isValid()
        ? origImage.convertedToColorSpace(QColorSpace::SRgb)
        : origImage;

    const auto devicePixelRatio = m_imageLabel->devicePixelRatioF();
    m_imageSize = QSizeF(image.size()) / devicePixelRatio;

    QPixmap pixmap = QPixmap::fromImage(image);
    pixmap.setDevicePixelRatio(devicePixelRatio);
    m_imageLabel->setPixmap(pixmap);

    const QSizeF targetSize = m_imageLabel->parentWidget()->size();
    if (m_imageSize.width() > targetSize.width()
        || m_imageSize.height() > targetSize.height()) {
        m_initialScaleFactor = qMin(targetSize.width() / m_imageSize.width(),
                                    targetSize.height() / m_imageSize.height());
    }
    m_maxScaleFactor = 3 * m_initialScaleFactor;
    m_minScaleFactor = m_initialScaleFactor / 3;
    doSetScaleFactor(m_initialScaleFactor);

    statusMessage(msgOpen(name, origImage));
#if QT_CONFIG(cursor)
    QGuiApplication::restoreOverrideCursor();
#endif

    maybeEnablePrinting();
}

JsonViewer 클래스

JsonViewer 는 JSON 파일을 ` QTreeView`에 표시합니다. 내부적으로는 파일 내용을 ` QJsonDocument `에 불러온 다음, 이를 사용하여 ` JsonItemModel`을 통해 사용자 정의 트리 모델을 채웁니다.

이 JSON 뷰어 플러그인은 ` QAbstractItemModel`을 상속받은 사용자 정의 항목 모델을 구현하는 방법을 보여줍니다. ` JsonTreeItem ` 클래스는 JSON 데이터를 조작하고 이를 기본 ` QJsonDocument`로 다시 전달하기 위한 기본 API를 제공합니다.

JsonViewer는 문서의 최상위 객체를 탐색용 북마크로 사용합니다. 다른 노드(키 및 값)는 추가 북마크로 추가하거나 북마크 목록에서 제거할 수 있습니다.

PdfViewer 클래스

PdfViewer 클래스(및 플러그인)는 PDF 뷰어 위젯 예제의 포크입니다. 이 예제는 QScroller 를 사용하여 문서를 부드럽게 넘겨 볼 수 있는 방법을 보여줍니다.

기타 관련 클래스

HoverWatcher 클래스

HoverWatcher 클래스는 마우스가 위젯 위로 호버할 때 커서를 재정의하고, 마우스를 떼면 원래 상태로 복원합니다. 동일한 위젯에 대해 여러 개의 HoverWatcher 인스턴스가 생성되는 것을 방지하기 위해, 위젯당 싱글톤으로 구현되었습니다.

HoverWatcher는 QObject 를 상속받으며, 감시 대상인 QWidget 를 인스턴스의 부모로 지정합니다. 이 클래스는 호버 이벤트를 소모하지 않고 가로채기 위해 이벤트 필터를 설치합니다:

HoverWatcher::HoverWatcher(QWidget *watched)
    : QObject(watched), m_watched(watched)
{
    Q_ASSERT(watched);
    m_cursorShapes[Entered].emplace(Qt::OpenHandCursor);
    m_cursorShapes[MousePress].emplace(Qt::ClosedHandCursor);
    m_cursorShapes[MouseRelease].emplace(Qt::OpenHandCursor);
    // no default for Left => restore override cursor
    m_watched->installEventFilter(this);
}

HoverAction 열거형에는 HoverWatcher가 반응하는 동작들이 나열되어 있습니다:

    enum HoverAction {
        Entered,
        MousePress,
        MouseRelease,
        Left,
        Ignore
    };

정적 함수는 워처를 생성하거나, 특정 QWidget 에 대한 워처의 존재 여부를 확인하거나, 워처를 해제합니다:

    static HoverWatcher *watcher(QWidget *watched);
    static const HoverWatcher *watcher(const QWidget *watched);
    static bool hasWatcher(QWidget *widget);
    static void dismiss(QWidget *watched);

각 HoverAction에 대해 커서 모양을 설정하거나 해제할 수 있습니다. 관련 커서 모양이 없는 경우, 액션이 트리거되면 애플리케이션의 오버라이드 커서가 복원됩니다.

public slots:
    void setCursorShape(HoverAction type, Qt::CursorShape shape);
    void unSetCursorShape(HoverAction type);

mouseButtons 속성은 MousePress 액션에서 고려할 마우스 버튼을 포함합니다:

    void setMouseButtons(Qt::MouseButtons buttons);
    void setMouseButton(Qt::MouseButton button, bool enable);

액션 처리 후에는 액션별 신호가 발송됩니다:

signals:
    void entered();
    void mousePressed();
    void mouseReleased();
    void left();

처리된 액션을 인수로 전달하는 일반 신호가 발송됩니다:

void hoverAction(HoverAction action);
RecentFiles 클래스

RecentFiles 은 최근에 열린 파일 목록을 관리하도록 특화된 ` QStringList `입니다.

RecentFiles에는 단일 파일을 추가하거나 한 번에 여러 파일을 추가할 수 있는 슬롯이 있습니다. 경로가 존재하며 열 수 있는 파일을 가리키는 경우, 해당 항목이 최근 파일 목록에 추가됩니다. 파일이 이미 목록에 있는 경우, 원래 위치에서 제거되고 목록 맨 위로 추가됩니다.

public slots:
    void addFile(const QString &fileName) { addFile(fileName, EmitPolicy::EmitWhenChanged); }
    void addFiles(const QStringList &fileNames);

파일은 이름이나 인덱스를 통해 목록에서 제거됩니다:

    void removeFile(const QString &fileName) { removeFile(m_files.indexOf(fileName)); }
    void removeFile(qsizetype index) {removeFile(index, RemoveReason::Other); }

QSettings 에서 저장 및 복원 기능을 구현하는 슬롯:

    void saveSettings(QSettings &settings, const QString &key) const;
    bool restoreFromSettings(QSettings &settings, const QString &key);

설정을 복원할 때, 존재하지 않는 파일은 무시됩니다. ` maxFiles ` 속성은 저장할 최근 파일의 최대 개수를 지정합니다(기본값은 10개입니다).

qsizetype maxFiles();
void setMaxFiles(qsizetype maxFiles);

RecentFiles 파일을 수락하기 전에 해당 파일을 읽을 수 있는지 확인합니다.

RecentFileMenu 클래스

RecentFileMenu 는 ` QMenu`의 파생 클래스로, ` RecentFiles ` 객체를 하위 메뉴로 표시하도록 특화되어 있습니다.

이 클래스의 생성자는 상위 ` QObject `에 대한 포인터와, 그 내용을 시각화할 `RecentFiles` 객체에 대한 포인터를 인수로 받습니다. 사용자가 목록에서 최근 파일을 선택할 때 트리거되는 ` fileOpened() ` 신호는 해당 파일의 절대 경로를 인수로 전달합니다.

참고: ` RecentFileMenu `는 부모 위젯이나 생성자에 전달된 ` RecentFiles ` 객체에 의해 소멸됩니다.

class RecentFileMenu : public QMenu
{
    Q_OBJECT

public:
    explicit RecentFileMenu(QWidget *parent, RecentFiles *recent);

signals:
    void fileOpened(const QString &fileName);
    ...
};

번역

응용 프로그램의 사용자 인터페이스는 영어와 독일어로 제공됩니다. 기본 언어는 Qt에 의해 자동으로 선택됩니다. 시스템 언어가 독일어인 경우 독일어가, 그렇지 않은 경우 영어가 선택됩니다. 또한 사용자는 Help > Language 메뉴에서 언어를 전환할 수 있습니다. 각 플러그인과 메인 응용 프로그램은 런타임 중에 자체 번역을 불러오는 책임을 각각 독립적으로 집니다.

CMake 통합

최상위 수준의 CMakeLists.txt 파일에는 포함된 언어들이 선언되어 있습니다.

qt_standard_project_setup(REQUIRES 6.8
    I18N_SOURCE_LANGUAGE en
    I18N_TRANSLATED_LANGUAGES de
)

documentviewer 타깃은 메인 애플리케이션을 정의합니다. 이 타깃은 docviewer_de.ts 및 docviewer_en.ts 파일에 해당 타깃의 현지화된 문자열을 저장하고 불러옵니다. 또한, Qt에서 제공하는 각 qtbase translations 를 생성된 번역 파일에 병합하여, 인쇄 대화 상자와 같은 Qt 대화 상자도 올바르게 번역되도록 합니다:

qt_add_translations(documentviewer
    SOURCE_TARGETS documentviewer abstractviewer
    TS_FILE_BASE docviewer
    MERGE_QT_TRANSLATIONS
    QT_TRANSLATION_CATALOGS qtbase
)

각 플러그인 레벨의 ` CMakeLists.txt `은 해당 플러그인의 소스 파일(SOURCE_TARGETS)에 대해서만 ` qt_add_translations `을 호출합니다. 번역 파일의 범위를 플러그인 타겟으로 제한함으로써 메인 애플리케이션 및 다른 플러그인의 소스 파일이 재스캔되거나 재번역되는 것을 방지합니다:

qt_add_translations(txtviewer
    SOURCE_TARGETS txtviewer
    TS_FILE_BASE txtviewer
)
Translator 클래스

Translator 클래스는 Qt의 QTranslator 를 감싸는 래퍼로, 메인 애플리케이션과 각 플러그인의 국제화를 모두 관리합니다. 각 구성 요소(메인 애플리케이션 및 플러그인)는 고유한 Translator 인스턴스를 가지며, 이를 통해 애플리케이션 전체에서 언어 전환이 조율됩니다. 시작 시 또는 사용자가 새 언어를 선택할 때 ` Translator::install() `가 호출됩니다. 이 메서드는 ` QTranslator::load()`를 사용하여 ` QLocale::uiLanguages()` 및 Qt 리소스 시스템의 기본 이름(basename)을 기반으로 번역 파일을 불러옵니다. 일치하는 번역이 발견되지 않으면 영어로 대체됩니다.

void Translator::install()
{
    if (m_baseName.isEmpty()) {
        qWarning() << "The basename of the translation is not set. Ignoring.";
       return;
    }
    if (!m_translator.isEmpty())
        qApp->removeTranslator(&m_translator);

   if (m_translator.load(m_trLocale, m_baseName, "_"_L1, ":/i18n/"_L1)
        && qApp->installTranslator(&m_translator)) {
        qInfo() << "Loaded translation" << m_translator.filePath();
    } else {
        if (m_trLocale.language() != QLocale::English) {
            qWarning() << "Failed to load translation" << m_baseName <<
                   "로케일" << m_trLocale.name() << ". 영어 번역으로 대체합니다";
            setLanguage(QLocale::English);
        }
    }
}
플러그인 지원

AbstractViewer 기본 클래스는 번역 기능을 제공하므로, 각 플러그인은 자체 번역을 자율적으로 관리할 수 있습니다:

  • AbstractViewer::setTranslationBaseName(): Translator 객체를 초기화하고, 기본 이름을 설정한 후, 기본 번역을 불러오도록 설치합니다.
    void AbstractViewer::setTranslationBaseName(const QString &baseName)
    {
        m_translator.reset(new Translator);
        m_translator->setBaseName(baseName);
        m_translator->install();
    }
  • AbstractViewer::eventFilter(): ` init()`의 메인 창에 설치되는 이 메서드는 ` QEvent::LanguageChange ` 이벤트를 수신합니다. 로케일이 변경되면 새로운 언어에 맞게 플러그인의 번역기를 재설치하고, ` retranslate() `를 호출하여 모든 텍스트를 새로 고칩니다.
    bool AbstractViewer::eventFilter(QObject *, QEvent *event)
    {
        if (event->type() != QEvent::LanguageChange)
            return false;
    
        const QLocale locale;
        if (locale != m_currentLocale) {
            m_currentLocale = locale;
            if (m_translator) {
                m_translator->setLanguage(locale.language());
                m_translator->install();
            }
            retranslate();
        }
        return false;
    }
  • AbstractViewer::retranslate(): 각 플러그인이 자체 UI 텍스트를 다시 번역하기 위해 구현하는 가상 메서드입니다. 예를 들어, ImageViewer에서 재구현된 방식은 다음과 같습니다:
        retranslate();
    }

플러그인이 소유하고 번역 가능한 문자열을 포함하는 위젯은 ` QEvent::LanguageChange `를 직접 처리할 수 있습니다. 예를 들어, ` ZoomSelector `은 콤보 박스 항목을 다시 번역하기 위해 ` changeEvent() `을 재정의합니다:

void ZoomSelector::changeEvent(QEvent *event)
{
    if (event->type() == QEvent::LanguageChange)
        retranslate();
    QComboBox::changeEvent(event);
}
애플리케이션 시작 시
  • 메인 애플리케이션: main.cpp 에서 창을 표시하기 전에 애플리케이션의 번역 데이터를 불러옵니다:
        Translator mainTranslator;
        mainTranslator.setBaseName("docviewer"_L1);
        mainTranslator.install();
  • 플러그인: 각 플러그인은 init() 함수 내에서 AbstractViewer::setTranslationBaseName() 를 호출하여, 해당 번역 파일 이름으로 Translator를 초기화하고 현재 언어의 번역을 적용합니다.
    void ImageViewer::init(QFile *file, QWidget *parent, QMainWindow *mainWindow)
        ...
        setTranslationBaseName("imgviewer"_L1);
        ...
실행 시 언어 전환

실행 시 언어 전환은 다음 두 가지 방법으로 트리거할 수 있습니다:

  1. 실행 시 전체 시스템의 언어 변경: 운영 체제가 애플리케이션에 ‘ QEvent::LocaleChange ’ 이벤트를 전송합니다.
  2. 메뉴 [ Help > Language] 사용: [ QMenu ] 항목을 클릭하면 [ MainWindow::onActionSwitchLanguage()]가 트리거되며, 이는 기본 로케일을 설정하고 [ QEvent::LocaleChange ] 이벤트를 발생시켜 시스템 로케일 변경과 동일한 흐름을 따르게 합니다:
    void MainWindow::onActionSwitchLanguage(QLocale::Language lang)
    {
        QLocale::setDefault(QLocale(lang));
        QEvent event(QEvent::LocaleChange);
        QCoreApplication::sendEvent(this, &event);
    }

두 경우 모두 MainWindow::changeEvent() 가 이벤트를 처리합니다. QEvent::LocaleChange 는 새로운 번역기를 설치하고, 그 결과 생성된 QEvent::LanguageChange 는 메인 창 UI를 다시 번역합니다. 플러그인은 각자의 이벤트 필터를 통해 이에 반응합니다.

void MainWindow::changeEvent(QEvent *event)
{
    switch (event->type()) {
    case QEvent::LanguageChange:
        ui->retranslateUi(this);
        statusBar()->clearMessage();
        break;
    case QEvent::LocaleChange:
        m_translator.setLanguage(QLocale().language());
        m_translator.install();
        break;
    default:
        break;
    }

    QMainWindow::changeEvent(event);
}

소스 파일

code.qt.io의 예제 프로젝트

참조: 모든 Qt 예제.

© 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.