Revision control

Copy as Markdown

// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
/*!
\title Document Viewer
\ingroup examples-widgets
\example demos/documentviewer
\examplecategory {Application Examples}
\meta {tag} {demo,widgets,mainwindow}
\brief A Widgets application to display and print JSON, text, and PDF files.
\image documentviewer_open.png
{Document viewer UI showing a 'Select Open Mode' pop-up}
\e{Document Viewer} demonstrates how to use a QMainWindow with static
and dynamic toolbars, menus, and actions. Additionally, it demonstrates
the following features in widget-based applications:
\list
\li Using QSettings to query and save user preferences,
and managing previously opened file history.
\li Controlling cursor behavior when hovering over widgets.
\li Creating dynamically loaded plugins.
\li Localizing the UI to different languages.
\endlist
\include examples-run.qdocinc
\section1 Creating an application and the main window
The application and its main window is constructed in \c main.cpp.
The main() function uses QCommandLineParser to process command line
arguments---\e help, \e version, and an optional positional
argument, \e file. If the user provided a path to a file when
launching the application, the main window opens it:
\quotefromfile demos/documentviewer/app/main.cpp
\skipto int main
\printuntil exec
\printline }
\section1 MainWindow class
The \c MainWindow class provides an application screen with menus,
actions, and a toolbar. It can open a file, automatically detecting its
content type. It also maintains a list of previously opened files, using
QSettings to store and reload settings when launched. The MainWindow
creates a suitable \e viewer for the opened file, based on its content type,
and provides support for printing a document.
MainWindow's constructor initializes the user interface created in Qt
Designer. The \c mainwindow.ui file provides a QTabWidget on the left,
showing bookmarks and thumbnails. On the right, there is a QScrollArea for
viewing file content.
\section1 ViewerFactory class
The \c ViewerFactory class manages viewers for known file types. These viewers
are implemented as plugins. When an instance of a ViewerFactory is created,
pointers to the view area and the main window are passed to the constructor:
\code
m_factory.reset(new ViewerFactory(ui->viewArea, this));
\endcode
ViewerFactory loads all available plugins on construction. It provides
a public API to query the loaded plugins, their names, and supported MIME
types:
\quotefromfile demos/documentviewer/app/viewerfactory.h
\skipto ViewerList
\printuntil QStringList supportedMimeTypes() const;
The \c viewer() function returns a pointer to the plugin suitable to open
the QFile passed as an argument:
\code
m_viewer = m_factory->viewer(file);
\endcode
If the application settings contain a section for the viewer, it's passed
to the viewer's virtual \c restoreState() function:
\quotefromfile demos/documentviewer/app/mainwindow.cpp
\skipto MainWindow::restoreViewerSettings
\printuntil restoreState
\printline }
Then, the standard UI assets are passed to the viewer and the main scroll
area is set to show the viewer's display widget:
\quotefromfile demos/documentviewer/app/mainwindow.cpp
\skipuntil bool MainWindow::openFile
\skipto m_viewer->initViewer
\printuntil }
\section1 AbstractViewer class
\c AbstractViewer provides a generalized API to view, save, and print a
document. Properties of both the document and the viewer can be queried:
\list
\li Does the document have content?
\li Has it been modified?
\li Is an overview (thumbnails or bookmarks) supported?
\endlist
AbstractViewer provides protected methods for derived classes to create
actions and menus on the main window. In order to display these
assets on the main window, they are parented to it. AbstractViewer is
responsible for removing and destroying the UI assets it creates. It
inherits from QObject to implement signals and slots.
\section2 Signals
\c {void uiInitialized();}
This signal is emitted after a viewer receives all necessary information
about UI assets on the main window.
\c {void printingEnabledChanged(bool enabled);}
This signal is emitted when document printing is either enabled or
disabled. This happens after a new document was successfully loaded,
or, for example, all content was removed.
\c {void showMessage(const QString &message, int timeout = 8000);}
This signal is emitted to display a status message to the user.
\c {void documentLoaded(const QString &fileName);}
This signal notifies the application that a document was successfully
loaded.
\section1 TxtViewer class
\c TxtViewer is a simple text viewer, inheriting from AbstractViewer.
It supports editing text files, copy/cut and paste, printing, and
saving changes.
\section2 Class Definition
\snippet demos/documentviewer/plugins/txtviewer/txtviewer.h interfacing
The class definition starts with the \c Q_OBJECT macro, which handles
signals and slots. It is followed by the \c Q_PLUGIN_METADATA and
\c Q_INTERFACES macros which are necessary to register the plugin.
The class inherits from \c ViewerInterface, which inherits from
\c AbstractViewer. The \c ViewerInterface class is used to provide an
interface between the main window application and the plugin.
\c QPluginLoader also requires the file txtviewer.json, which has to contain
the plugin's key:
\code
{ "Keys": [ "txtviewer" ] }
\endcode
The class defines no constructor, which means that only a standard
constructor without arguments is available. All other functions, including
the destructor, re-implement virtual functions of \c ViewerInterface. They
are used to exchange data, information, and instructions with the main
application.
\snippet demos/documentviewer/plugins/txtviewer/txtviewer.h classDefinition
\section2 TxtViewer Class Implementation
\snippet demos/documentviewer/plugins/txtviewer/txtviewer.cpp init
We start by including the header files necessary to access all classes used
by \c TxtViewer. We also include \c txtviewer.h.
\c QPrinter and \c QPrintDialog are only included if print support is
enabled on the compilation system.
Note that these headers are not included directly in \c mainwindow.h.
Including large headers in other header files can impact build performance.
In this case, it would not cause issues, but it is best practice to include
only the necessary headers to keep dependencies minimal.
The implementation starts with an empty destructor. It could be completely
omitted. It's good practice to implement it empty in order to point out to
code readers that nothing needs to be done in the destructor.
The destructor is followed by an initialization function, taking three
arguments:
\list
\li \c file, the pointer to the file to be opened and displayed.
\li \c parent, pointing to the \c QWidget inside which the editor shall be placed.
\li \c mainWindow, pointing to the application's main window, where menus and menu bars
are handled.
\endlist
The function calls the base init function of \c AbstractViewer. A new
QPlainTextEdit widget is created, which will display the file's contents.
Then, \c TxtViewer's setup function is connected to the base class'
\c uiInitialized signal.
The next function returns the list of mime types, which the text viewer
supports. Only plain text is supported.
The last initialization function adds viewer specific UI components like
menus, icons, buttons, and tooltips. It uses functionality provided by
\c AbstractViewer to make sure that these components are removed from the
application's main window, once another file is displayed with another
viewer plugin.
\snippet demos/documentviewer/plugins/txtviewer/txtviewer.cpp open
\c openFile opens a file, transfers its contents into the QPlainTextEdit,
and prints a status message for the user, depending on whether or not the
opening was successful.
\snippet demos/documentviewer/plugins/txtviewer/txtviewer.cpp infoPrintAndSave
The next re-implemented function tells the main application whether or not
the viewer plugin is actually displaying content.
If printing is supported on the compiling system, the next section
implements it.
The last two re-implementations provide functionality to save the current
file or to save it under a new name.
\section1 ImageViewer class
\c ImageViewer displays images as supported by QImageReader, using
a QLabel.
In the constructor, we increase the allocation limit of QImageReader to
allow for larger photos:
\snippet demos/documentviewer/plugins/imageviewer/imageviewer.cpp init
In the openFile() function, we load the image and determine its size.
If it is larger than the screen, we downscale it to screen size,
maintaining the aspect ratio. This calculation has to be done in native
pixels, and the device pixel ratio needs to be set on the resulting pixmap
for it to appear crisp:
\snippet demos/documentviewer/plugins/imageviewer/imageviewer.cpp open
\section1 JsonViewer class
\c JsonViewer displays a JSON file in a QTreeView. Internally, it loads
the contents of a file into a QJsonDocument and uses it to populate a
custom tree model with \c JsonItemModel.
The JSON viewer plugin demonstrates how to implement a custom item model
inherited from QAbstractItemModel. The \c JsonTreeItem class provides a
basic API for manipulating JSON data and propagating it back to the
underlying QJsonDocument.
JsonViewer uses the top-level objects of the document as bookmarks for
navigation. Other nodes (keys and values) can be added as additional
bookmarks, or removed from the bookmark list.
\section1 PdfViewer class
The \c PdfViewer class (and plugin) is a fork of the \l {PDF Viewer
Widget Example}. It demonstrates the use of QScroller to smoothly
flick through a document.
\section1 Other relevant classes
\section2 HoverWatcher class
The \c HoverWatcher class sets an override cursor when hovering the
mouse over a widget, restoring it upon departure. To prevent multiple
HoverWatcher instances being created for the same widget, it is
implemented as a singleton per widget.
HoverWatcher inherits from QObject and takes the QWidget it watches
as the instance's parent. It installs an event filter to intercept hover
events without consuming them:
\quotefromfile demos/documentviewer/plugins/pdfviewer/hoverwatcher.cpp
\skipto HoverWatcher::HoverWatcher
\printuntil }
The \c HoverAction enum lists the actions that HoverWatcher reacts to:
\quotefromfile demos/documentviewer/plugins/pdfviewer/hoverwatcher.h
\skipto enum HoverAction
\printuntil };
Static functions create watchers, check their existence for a specific
QWidget, or dismiss a watcher:
\quotefromfile demos/documentviewer/plugins/pdfviewer/hoverwatcher.h
\skipto static HoverWatcher
\printuntil static void dismiss
A cursor shape can be set or unset for each HoverAction. If there is
no associated cursor shape, the application's override cursor is
restored when the action is triggered.
\quotefromfile demos/documentviewer/plugins/pdfviewer/hoverwatcher.h
\skipto public slots
\printuntil void unSetCursorShape
The \c mouseButtons property holds the mouse buttons to consider for a
\c MousePress action:
\quotefromfile demos/documentviewer/plugins/pdfviewer/hoverwatcher.h
\skipuntil public slots
\skipto setMouseButtons
\printuntil setMouseButton(
Action-specific signals are emitted after processing an action:
\quotefromfile demos/documentviewer/plugins/pdfviewer/hoverwatcher.h
\skipto signals
\printuntil left();
A general signal is emitted which passes the processed action as an
argument:
\code
void hoverAction(HoverAction action);
\endcode
\section2 RecentFiles class
\c RecentFiles is a QStringList that is specialized to manage a list of
recently opened files.
RecentFiles has slots to add either a single file or multiple files in one
go. An entry is added to the list of recent files if the path points to a
file that exists and can be opened. If a file is already in the list, it
is removed from its original position and added to the top.
\quotefromfile demos/documentviewer/app/recentfiles.h
\skipto public slots
\printuntil addFiles
Files are removed from the list either by name or by index:
\quotefromfile demos/documentviewer/app/recentfiles.h
\skipuntil public slots
\skipto removeFile
\printuntil qsizetype index
Slots that implement saving and restoring from QSettings:
\quotefromfile demos/documentviewer/app/recentfiles.h
\skipuntil public slots
\skipto saveSettings
\printuntil restoreFromSettings
When restoring settings, nonexistent files are ignored. The \c maxFiles
property holds the maximum amount of recent files to store (default is
10).
\code
qsizetype maxFiles();
void setMaxFiles(qsizetype maxFiles);
\endcode
\c {RecentFiles} verifies that a file can be read before
accepting it.
\section2 RecentFileMenu class
\c {RecentFileMenu} is a QMenu, specialized to display a
\l{RecentFiles class}{RecentFiles} object as a submenu.
Its constructor takes a pointer to a parent QObject and a pointer to a
RecentFiles object, the content of which it will visualize.
Its \c fileOpened() signal, triggered when the user selects a recent file
from the list, passes the absolute path to the file as an argument.
\note \c {RecentFileMenu} is destroyed either by its parent widget, or by the
\c {RecentFiles} object passed to its constructor.
\quotefromfile demos/documentviewer/app/recentfilemenu.h
\skipto class RecentFileMenu
\printuntil void fileOpened
\dots
\skipuntil RecentFiles
\printline }
\section1 Translations
The application's user interface is available in English and German.
The default language is auto-selected by Qt: German if the system
language is German; otherwise, English. Also, the user can switch
the language in the \uicontrol Help > \uicontrol Language menu.
Each plugin, as well as the main application, is independently
responsible for loading its own translations during runtime.
\section2 CMake integration
The top-level CMakeLists.txt declares the shipped languages.
\quotefromfile demos/documentviewer/CMakeLists.txt
\skipto qt_standard_project_setup
\printuntil )
The \c documentviewer target defines the main application. It
stores and loads the localized strings for the target in
docviewer_de.ts and docviewer_en.ts files. Furthermore,
it merges the respective \c{qtbase translations} provided by
Qt into the generated translation files, so that
also Qt dialogs like the print dialog are properly translated:
\quotefromfile demos/documentviewer/app/CMakeLists.txt
\skipto qt_add_translations(documentviewer
\printuntil )
Each plugin level \c CMakeLists.txt invokes \c qt_add_translations only
on that plugin's source files (\c SOURCE_TARGETS).
Scoping the translation files to the plugin target prevents re-scanning
and retranslating the sources of the main application and other plugins:
\quotefromfile demos/documentviewer/plugins/txtviewer/CMakeLists.txt
\skipto qt_add_translations(txtviewer
\printuntil )
\section2 Translator class
The \c Translator class is a wrapper around Qt's \l QTranslator that manages
internationalization for both the main application and each plugin. Each
component (main application and plugins) has its own Translator instance,
enabling coordinated language switching across the entire application.
At startup or when the user selects a new language, \c Translator::install()
is called. This method uses \l QTranslator::load() to load translation files
based on the QLocale::uiLanguages() and the basename in the Qt resource system.
If no matching translation is found, it falls back to English.
\quotefromfile demos/documentviewer/app/translator.cpp
\skipto void Translator::install
\printuntil /^\}/
\section2 Plugin support
The \c AbstractViewer base class provides translation support so each plugin
can manage its own translations autonomously:
\list
\li \c AbstractViewer::setTranslationBaseName(): initializes a \c Translator
object, sets its basename, and installs it to load the default translations.
\quotefromfile demos/documentviewer/app/abstractviewer.cpp
\skipto setTranslationBaseName
\printuntil }
\li \c AbstractViewer::eventFilter(): installed on the main window in
\c init(), this method listens for \l QEvent::LanguageChange events. When the
locale has changed, it reinstalls the plugin's translator for the new language
and calls \c retranslate() to refresh all text.
\quotefromfile demos/documentviewer/app/abstractviewer.cpp
\skipto eventFilter
\printuntil /^\}/
\li \c AbstractViewer::retranslate(): a virtual method that each plugin implements
to retranslate its own UI texts. For instance, as reimplemented in ImageViewer:
\quotefromfile demos/documentviewer/plugins/imageviewer/imageviewer.cpp
\skipto retranslate
\printuntil }
\endlist
Widgets that are owned by a plugin and contain translatable strings can
handle \l QEvent::LanguageChange directly. For example, \c ZoomSelector
overrides \c changeEvent() to retranslate its combo box items:
\quotefromfile demos/documentviewer/plugins/pdfviewer/zoomselector.cpp
\skipto ZoomSelector::changeEvent
\printuntil /^\}/
\section2 Application startup
\list
\li \e{Main application}: in \c main.cpp we load the application's translation
before showing the window:
\quotefromfile demos/documentviewer/app/main.cpp
\skipto Translator mainTranslator;
\printuntil mainTranslator.install();
\li \e Plugins: each plugin calls \c AbstractViewer::setTranslationBaseName()
in its \c init() function to initialize a \l{Translator class}{Translator} with
their translation file name and install the translations of the current language.
\quotefromfile demos/documentviewer/plugins/imageviewer/imageviewer.cpp
\skipto ImageViewer::init(
\printuntil init
\dots
\skipto setTranslation
\printline setTranslation
\dots
\endlist
\section2 Runtime language switching
Runtime language switching can be triggered in two ways:
\list 1
\li Switching the language of the whole system at runtime:
The operating system sends a \l QEvent::LocaleChange event to the
application.
\li Using the menu \uicontrol Help > \uicontrol Language:
Clicking on the \c QMenu item triggers \c MainWindow::onActionSwitchLanguage(),
which sets the default locale and posts a \l QEvent::LocaleChange event,
to enter the same flow as a system locale change:
\quotefromfile demos/documentviewer/app/mainwindow.cpp
\skipto void MainWindow::onActionSwitchLanguage
\printuntil }
\endlist
In both cases, \c MainWindow::changeEvent() handles the events.
\l QEvent::LocaleChange installs the new translator, and the resulting
\l QEvent::LanguageChange retranslates the main window UI.
Plugins react via their event filters.
\quotefromfile demos/documentviewer/app/mainwindow.cpp
\skipto void MainWindow::changeEvent
\printuntil /^\}/
\section1 Source files
\sa {All Qt examples}
*/