文档查看器
一款用于显示和打印 JSON、文本和 PDF 文件的 Widgets 应用程序。
文档查看器演示了如何使用带有静态和动态工具栏、菜单和操作的QMainWindow 。此外,它还演示了基于 widget 的应用程序的以下功能:
- 使用QSettings 查询和保存用户首选项,并管理之前打开的文件历史记录。
- 控制鼠标悬停在 widget 上时的光标行为。
- 创建动态加载的插件。
- 将用户界面本地化为不同语言。
运行示例
要从 Qt Creator,打开Welcome 模式,然后从Examples 中选择示例。更多信息,请参阅Qt Creator: 教程:构建并运行。
创建应用程序和主窗口
应用程序及其主窗口在main.cpp
中构建。main()函数使用QCommandLineParser 处理命令行参数--help、version 和一个可选的位置参数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
类提供了一个带有菜单、操作和工具栏的应用程序屏幕。它可以打开文件,并自动检测其内容类型。它还能维护以前打开过的文件列表,使用QSettings 来存储并在启动时重新加载设置。MainWindow 会根据打开文件的内容类型为其创建合适的阅读器,并支持打印文档。
MainWindow 的构造函数初始化在Qt Designer 中创建的用户界面。mainwindow.ui
文件左侧提供了一个QTabWidget ,显示书签和缩略图。右侧是QScrollArea ,用于查看文件内容。
查看器工厂类
ViewerFactory
类管理已知文件类型的查看器。这些查看器是作为插件实现的。创建 ViewerFactory 实例时,会将视图区域和主窗口的指针传递给构造函数:
m_factory.reset(new ViewerFactory(ui->viewArea, this));
ViewerFactory 会在构建时加载所有可用的插件。它提供了一个公共 API 来查询已加载的插件、它们的名称和支持的 MIME 类型:
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); }
然后,标准用户界面资产被传递给查看器,主滚动区域被设置为显示查看器的显示部件:
m_viewer->initViewer(ui->actionBack, ui->actionForward, ui->menuHelp->menuAction(), ui->tabWidget); restoreViewerSettings(); ui->scrollArea->setWidget(m_viewer->widget()); return true; }
抽象查看器类
AbstractViewer
提供了查看、保存和打印文档的通用应用程序接口。文档和查看器的属性都可以查询:
- 文档有内容吗?
- 是否被修改过?
- 是否支持概览(缩略图或书签)?
AbstractViewer 为派生类提供了受保护的方法,以便在主窗口上创建操作和菜单。要在主窗口上显示这些资产,它们必须是主窗口的父类。AbstractViewer 负责移除和销毁其创建的用户界面资产。它继承自QObject 以实现信号和槽。
信号
void uiInitialized();
该信号在查看器接收到有关主窗口上用户界面资产的所有必要信息后发出。
void printingEnabledChanged(bool enabled);
启用或禁用文档打印时发出该信号。这发生在成功加载新文档或删除所有内容之后。
void showMessage(const QString &message, int timeout = 8000);
向用户显示状态信息时发出该信号。
void documentLoaded(const QString &fileName);
该信号通知应用程序已成功加载文档。
TxtViewer 类
TxtViewer
是一个简单的文本查看器,继承自 AbstractViewer。它支持编辑文本文件、复制/剪切和粘贴、打印以及保存更改。
ImageViewer 类
ImageViewer
使用QLabel 显示QImageReader 支持的图像。
在构造函数中,我们增加了QImageReader 的分配限制,以容纳更大的照片:
ImageViewer::ImageViewer() : m_formats(imageFormats()) { connect(this, &AbstractViewer::uiInitialized, this, &ImageViewer::setupImageUi); QImageReader::setAllocationLimit(1024); // MB }
在 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
在QTreeView 中显示一个 JSON 文件。在内部,它将文件内容加载到QJsonDocument 中,并用JsonItemModel
填充自定义树模型。
JSON viewer 插件演示了如何实现从QAbstractItemModel 继承的自定义项目模型。JsonTreeItem
类提供了一个基本的 API,用于操作 JSON 数据并将其传播回底层的QJsonDocument 。
JsonViewer 将文档的顶层对象用作导航书签。其他节点(键和值)可作为附加书签添加或从书签列表中删除。
PdfViewer 类
PdfViewer
类(和插件)是PDF Viewer Widget 示例的分叉。它演示了如何使用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 对象的指针,它将显示 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
只在该插件的源文件上调用qt_add_translations
(SOURCE_TARGETS
)。将翻译文件的范围设为插件目标,可以防止重新扫描和重新翻译主程序和其他插件的源文件:
qt_add_translations(txtviewer SOURCE_TARGETS txtviewer TS_FILE_BASE txtviewer )
翻译类
Translator
类是 Qt 的QTranslator 的包装器,用于管理主程序和每个插件的国际化。每个组件(主程序和插件)都有自己的 Translator 实例,从而在整个程序中实现协调的语言切换。启动时或用户选择新语言时,Translator::install()
。该方法使用QTranslator::load() 根据QLocale::uiLanguages() 和 Qt XML 资源系统中的 basename 加载翻译文件。如果找不到匹配的翻译,就会返回英语。
voidTranslator::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 << "for locale"<<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::updateTranslation()
:在现有翻译器上调用install()
安装新翻译,然后调用retranslate()
刷新所有文本。void AbstractViewer::updateTranslation(QLocale::Language lang) { if (m_translator) { m_translator->setLanguage(lang); m_translator->install(); retranslate(); } }
AbstractViewer::retranslate()
虚拟方法:这是每个插件实现的虚拟方法,用于重新翻译自己的用户界面文本。例如,在 ImageViewer 中重新实现:void ImageViewer::retranslate() { m_toolBar->setWindowTitle(tr("Images")); m_zoomInAct->setText(tr("Zoom &In")); m_zoomOutAct->setText(tr("Zoom &Out")); m_resetZoomAct->setText(tr("Reset Zoom")); }
应用程序启动
- 主应用程序:在
main.cpp
中,我们会在显示窗口之前加载应用程序的翻译:Translator mainTranslator; mainTranslator.setBaseName("docviewer"_L1); mainTranslator.install();
- 插件:每个插件都会在其
init()
函数中调用AbstractViewer::setTranslationBaseName()
,用其翻译文件名初始化翻译器,并安装当前语言的翻译。void ImageViewer::init(QFile *file, QWidget *parent, QMainWindow *mainWindow) ... setTranslationBaseName("imgviewer"_L1); ...
运行时语言切换
运行时语言切换有两种方式:
- 使用菜单Help >Language :点击
QMenu
项目会触发MainWindow::onActionSwitchLanguage()
,安装新语言并重新翻译主程序和插件:void MainWindow::onActionSwitchLanguage(QLocale::Language lang) { m_translator.setLanguage(lang); m_translator.install(); ui->retranslateUi(this); const auto viewerList = m_factory->viewers(); for (AbstractViewer *viewer : viewerList) viewer->updateTranslation(lang); statusBar()->clearMessage(); }
- 在运行时切换整个系统的语言:应用程序可以通过监听事件并在
QEvent::LocaleChange
事件上调用MainWindow::onActionSwitchLanguage()
来对此做出反应。void MainWindow::changeEvent(QEvent *event) { if (event->type() == QEvent::LocaleChange) onActionSwitchLanguage(QLocale::system().language()); QMainWindow::changeEvent(event); }
源文件
另请参阅 所有 Qt 示例。
© 2025 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.