Application Example
应用程序示例演示了如何实现带有菜单、工具栏和状态栏的标准GUI应用程序。
该示例本身是一个围绕QPlainTextEdit构建的简单文本编辑器程序。
应用程序示例的几乎所有代码都在MainWindow类中,该类继承QMainWindow。QMainWindow为具有菜单、工具栏、停靠窗口和状态栏的窗口提供了框架。应用程序在菜单栏中提供文件、编辑和帮助条目,并提供以下弹出菜单:
主窗口底部的状态栏显示光标下菜单项或工具栏按钮的说明。
为了简化示例,最近打开的文件不会显示在“File”菜单中,尽管90%的应用程序都需要此功能。此外,此示例一次只能加载一个文件。SDI和MDI示例演示了如何解除这些限制以及如何实现最近打开的文件处理。
QAction 介绍
QAction是表示一个用户操作的对象,例如保存文件或调用对话框。操作可以放在QMenu或QToolBar中,或者两者都放,或者放在任何其他重新实现QWidget::actionEvent() 的小部件中。
一个动作有一个显示在菜单中的文本、一个图标、一个快捷键、一个工具提示、一个状态提示(显示在状态栏中)、一个“这是什么?”文本等。每当用户调用该操作时(例如,通过单击关联的菜单项或工具栏按钮),它都会发出一个triggered() 信号。
QAction的实例可以通过传递父QObject或使用QMenu、QMenuBar或QToolBar的便利函数之一来创建。我们在菜单中以及窗口上作为父对象的工具栏中创建操作,以防止所有权问题。对于只在菜单中的操作,我们使用方便函数QMenu::addAction() ,它允许我们传递文本、图标和目标对象及其slot成员函数。
创建工具栏与创建菜单非常相似。我们在菜单中放置的相同操作可以在工具栏中重用。创建操作后,我们使用QToolBar::addAction() 将其添加到工具栏。
示例代码还包含一个必须解释的习惯用法。对于某些操作,我们指定一个图标作为QAction构造函数的QIcon。我们使用QIcon::fromTheme() 从底层窗口系统获取正确的标准图标。如果由于平台不支持而失败,我们将传递一个文件名作为回退。在这里,文件名以 : 开头,这样的文件名不是普通的文件名,而是可执行文件存储资源中的路径。我们将在回顾application.qrc 作为项目一部分的文件。
只有当QPlainTextEdit包含选定的文本时,“编辑|剪切”和“编辑|复制”动作才可用。我们在默认情况下禁用它们,并将QPlainTextEdit::copyAvailable()信号连接到QAction::setEnabled()插槽,确保在文本编辑器没有选择时禁用这些操作。
在创建帮助菜单之前,我们调用QMenuBar::addSeparator()。这对大多数小部件样式(例如,Windows和macOS样式)没有影响,但对于某些样式,这确保帮助被推到菜单栏的右侧。
main.cpp
#include <QApplication>
#include <QCommandLineParser>
#include <QCommandLineOption>
#include "mainwindow.h"
int main(int argc, char *argv[])
{
// 用指定的基名称初始化.qrc文件指定的资源。
// 通常,当资源作为应用程序的一部分构建时,资源会在启动时自动加载。
// 在某些平台上,对于存储在静态库中的资源,Q_INIT_RESOURCE()宏是必需的。
Q_INIT_RESOURCE(application); // 一堆图片资源
#ifdef Q_OS_ANDROID
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
QApplication app(argc, argv); // 定义 QApplication
// 设置此应用程序的组织的名称
QCoreApplication::setOrganizationName("QtProjectZSH");
// 设置此应用程序的名称
QCoreApplication::setApplicationName("Application Example");
// 设置此应用程序的版本
QCoreApplication::setApplicationVersion(QT_VERSION_STR);
QCommandLineParser parser; // 定义命令行解析器
parser.setApplicationDescription(QCoreApplication::applicationName());
parser.addHelpOption(); // -h, --help and -?
parser.addVersionOption(); // -v / --version
parser.addPositionalArgument("file", "The file to open."); // 附加自定义的file参数
parser.process(app); // 处理命令行,命令行是从QCoreApplication实例app获得的
MainWindow mainWin;
if (!parser.positionalArguments().isEmpty()) // 获取附加参数的列表不为空--file字符串不为空
mainWin.loadFile(parser.positionalArguments().first()); // 加载获取文件路径
mainWin.show();
return app.exec();
}
MainWindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
# include <QMainWindow>
class QSessionManager;
class QPlainTextEdit;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow();
void loadFile(const QString &fileName);
protected:
// 重新实现QWidget::closeEvent(),以检测用户何时尝试关闭窗口,并警告用户未保存的更改。
void closeEvent(QCloseEvent *event) override;
private slots:
void newFile();
void open();
bool save();
bool saveAs();
void about();
void documentWasModified();
#ifndef QT_NO_SESSIONMANAGER
void commitData(QSessionManager &);
#endif
private:
void createActions();
void createStatusBar();
void readSettings();
void writeSettings();
bool maybeSave();
bool saveFile(const QString &fileName);
void setCurrentFile(const QString &fileName);
// QString strippedName(const QString &fullFileName);
QPlainTextEdit *textEdit;
QString curFile;
};
#endif
MainWindow.cpp
/* 我们首先包括<QtWidgets>,一个头文件,其中包含Qt核心、Qt GUI和Qt小部件模块中所有类的定义。
这使我们不必把每个类都单独包含进去。我们还包括mainwindow.h。
* 你可能想知道为什么我们不在mainwindow.h中包含<QtWidgets>,然后就不用它了。原因是,包含来
自另一个头文件的如此大的头文件可能会迅速降低性能。在这里,它不会造成任何伤害,但一般来说,
只包含另一个头文件中严格必需的头文件仍然是一个好主意。*/
#include <QtWidgets>
#include "mainwindow.h"
/*在构造函数中,首先创建一个QPlainTextEdit小部件作为主窗口(this对象)的子窗口。
然后调用QMainWindow::setCentralWidget(),告诉您这将是一个小部件,它占据了主窗口的中心区域,位于工具栏和状态栏之间。*/
MainWindow::MainWindow() : textEdit(new QPlainTextEdit)
{
// QMainWindow::setCentralWidget 将获取小部件指针的所有权,并在适当的时候将其删除。
setCentralWidget(textEdit); // 设置中心控件
createActions(); // 创建操作
createStatusBar(); // 创建状态栏
readSettings(); // 读取并恢复用户的设置
// 在QPlainTextEdit的document对象和documentWasModified()槽之间建立一个信号槽连接。
// 每当用户修改QPlainTextEdit中的文本时,更新标题栏以显示文件已被修改。
connect(textEdit->document(), &QTextDocument::contentsChanged,
this, &MainWindow::documentWasModified); // 关联文档被修改的槽函数
#ifndef QT_NO_SESSIONMANAGER
// 设置QGUI应用程序是否启用回退会话管理。
QGuiApplication::setFallbackSessionManagementEnabled(false);
connect(qApp, &QGuiApplication::commitDataRequest,
this, &MainWindow::commitData); // 连接到commitData
#endif
setCurrentFile(QString()); // 设置当前文件
// // 窗口是否使用macOS上的统一标题和工具栏外观,不建议使用
// setUnifiedTitleAndToolBarOnMac(true);
}
/* 重新实现QWidget::closeEvent(),以检测用户何时尝试关闭窗口,并警告用户未保存的更改 */
void MainWindow::closeEvent(QCloseEvent *event)
{
if (maybeSave()) { // 用户希望关闭应用程序
writeSettings(); // 首选项保存到磁盘
event->accept(); // 接受close事件
} else { // 用户不希望关闭应用程序
event->ignore(); // 忽略close事件 应用程序将保持运行
}
}
/* 当用户从菜单中选择File | New时,将调用newFile()槽 */
void MainWindow::newFile()
{
if (maybeSave()) { // 用户希望关闭应用程序
textEdit->clear(); // 清除QPlainTextEdit
setCurrentFile(QString()); // setCurrentFile() 更新窗口标题并清除windowModified标志
}
}
/* 用户单击File | open时, 将调用open() 槽。*/
void MainWindow::open()
{
if (maybeSave()) {
QString fileName = QFileDialog::getOpenFileName(this); // 弹出一个QFileDialog,要求用户选择一个文件
if (!fileName.isEmpty()) // 用户选择一个文件(即文件名不是空字符串)
loadFile(fileName); // 函数loadFile() 来实际加载该文件
}
}
/*用户单击File | save时, 将调用save() 槽 */
bool MainWindow::save()
{
if (curFile.isEmpty()) { // 用户尚未提供文件名
return saveAs(); // 调用saveAs()
} else { // 用户尚已提供文件名
return saveFile(curFile); // 调用私有函数saveFile() 来实际保存文件
}
}
/* saveAs() 函数 */
bool MainWindow::saveAs()
{
QFileDialog dialog(this);
dialog.setWindowModality(Qt::WindowModal); // 对话框模态
dialog.setAcceptMode(QFileDialog::AcceptSave); // 设置文件保存模态
if (dialog.exec() != QDialog::Accepted) // 首先弹出一个QFileDialog,要求用户提供一个名称。
return false; // 如果用户单击Cancel,则返回的文件名为空,我们什么也不做。
return saveFile(dialog.selectedFiles().first()); // 否则,调用saveFile() 函数
}
/* 应用程序的About框是使用QMessageBox::About() 静态函数并依赖于它对HTML子集的支持。
* 围绕文本字符串的tr() 调用标记要翻译的字符串。在所有用户可见的字符串上调用tr()是一个好习惯,
以防以后决定将应用程序翻译成其他语言。Qt国际化概述更详细地介绍了tr()。*/
void MainWindow::about()
{
QMessageBox::about(this, tr("About Application"),
tr("The <b>Application</b> example demonstrates how to "
"write modern GUI applications using Qt, with a menu bar, "
"toolbars, and a status bar."));
}
/* 用户编辑而更改QPlainTextEdit中的文本时,都会调用documentWasModified()槽 */
void MainWindow::documentWasModified()
{
// 调用QWidget::setWindowModified()使标题栏显示文件已被修改。如何做到这一点在每个平台上有所不同。
setWindowModified(textEdit->document()->isModified());
}
/* 私有函数 createActions() 私有函数创建QActions并填充菜单和两个工具栏。
* 代码非常重复,因此我们只显示与File | New、File | Open和Help | About Qt对应的操作。*/
void MainWindow::createActions()
{
QMenu *fileMenu = menuBar()->addMenu(tr("&File")); // 文件菜单
QToolBar *fileToolBar = addToolBar(tr("File")); // 添加工具栏菜单
const QIcon newIcon = QIcon::fromTheme("document-new", QIcon(":/images/new.png"));
QAction *newAct = new QAction(newIcon, tr("&New"), this); // 操作-新建
newAct->setShortcuts(QKeySequence::New);
newAct->setStatusTip(tr("Create a new file"));
connect(newAct, &QAction::triggered, this, &MainWindow::newFile);
fileMenu->addAction(newAct);
fileToolBar->addAction(newAct);
const QIcon openIcon = QIcon::fromTheme("document-open", QIcon(":/images/open.png"));
QAction *openAct = new QAction(openIcon, tr("&Open..."), this); // 操作-打开
openAct->setShortcuts(QKeySequence::Open);
openAct->setStatusTip(tr("Open an existing file"));
connect(openAct, &QAction::triggered, this, &MainWindow::open);
fileMenu->addAction(openAct);
fileToolBar->addAction(openAct);
const QIcon saveIcon = QIcon::fromTheme("document-save", QIcon(":/images/save.png"));
QAction *saveAct = new QAction(saveIcon, tr("&Save"), this); // 操作-保存
saveAct->setShortcuts(QKeySequence::Save);
saveAct->setStatusTip(tr("Save the document to disk"));
connect(saveAct, &QAction::triggered, this, &MainWindow::save);
fileMenu->addAction(saveAct);
fileToolBar->addAction(saveAct);
const QIcon saveAsIcon = QIcon::fromTheme("document-save-as");
QAction *saveAsAct = fileMenu->addAction(saveAsIcon, tr("Save &As..."), this, &MainWindow::saveAs); // 操作-另存
saveAsAct->setShortcuts(QKeySequence::SaveAs);
saveAsAct->setStatusTip(tr("Save the document under a new name"));
fileMenu->addSeparator();
const QIcon exitIcon = QIcon::fromTheme("application-exit");
QAction *exitAct = fileMenu->addAction(exitIcon, tr("E&xit"), this, &QWidget::close); // 操作-退出
// exitAct->setShortcuts(QKeySequence::Quit); // windows没有为Quit设置快捷键
exitAct->setShortcuts(QKeySequence::listFromString ("Ctrl+Q"));
exitAct->setStatusTip(tr("Exit the application"));
QMenu *editMenu = menuBar()->addMenu(tr("&Edit")); // 添加编辑菜单
QToolBar *editToolBar = addToolBar(tr("Edit")); // 再添加编辑工具栏
#ifndef QT_NO_CLIPBOARD
const QIcon cutIcon = QIcon::fromTheme("edit-cut", QIcon(":/images/cut.png"));
QAction *cutAct = new QAction(cutIcon, tr("Cu&t"), this); // 操作-剪切
cutAct->setShortcuts(QKeySequence::Cut);
cutAct->setStatusTip(tr("Cut the current selection's contents to the clipboard"));
connect(cutAct, &QAction::triggered, textEdit, &QPlainTextEdit::cut);
editMenu->addAction(cutAct);
editToolBar->addAction(cutAct);
const QIcon copyIcon = QIcon::fromTheme("edit-copy", QIcon(":/images/copy.png"));
QAction *copyAct = new QAction(copyIcon, tr("&Copy"), this); // 操作-复制
copyAct->setShortcuts(QKeySequence::Copy);
copyAct->setStatusTip(tr("Copy the current selection's contents to the clipboard"));
connect(copyAct, &QAction::triggered, textEdit, &QPlainTextEdit::copy);
editMenu->addAction(copyAct);
editToolBar->addAction(copyAct);
const QIcon pasteIcon = QIcon::fromTheme("edit-paste", QIcon(":/images/paste.png"));
QAction *pasteAct = new QAction(pasteIcon, tr("&Paste"), this); // 操作-粘贴
pasteAct->setShortcuts(QKeySequence::Paste);
pasteAct->setStatusTip(tr("Paste the clipboard's contents into the current selection"));
connect(pasteAct, &QAction::triggered, textEdit, &QPlainTextEdit::paste);
editMenu->addAction(pasteAct);
editToolBar->addAction(pasteAct);
menuBar()->addSeparator();
#endif // !QT_NO_CLIPBOARD
QMenu *helpMenu = menuBar()->addMenu(tr("&Help")); // 菜单-帮助
QAction *aboutAct = helpMenu->addAction(tr("&About"), this, &MainWindow::about); // 操作-帮助
aboutAct->setStatusTip(tr("Show the application's About box"));
QAction *aboutQtAct = helpMenu->addAction(tr("About &Qt"), qApp, &QApplication::aboutQt); // 操作-Qt帮助
aboutQtAct->setStatusTip(tr("Show the Qt library's About box"));
#ifndef QT_NO_CLIPBOARD
cutAct->setEnabled(false); // 禁用 操作-剪切
copyAct->setEnabled(false); // 禁用 操作-复制
connect(textEdit, &QPlainTextEdit::copyAvailable, cutAct, &QAction::setEnabled);
connect(textEdit, &QPlainTextEdit::copyAvailable, copyAct, &QAction::setEnabled);
#endif // !QT_NO_CLIPBOARD
}
/* QMainWindow::statusBar()返回一个指向主窗口QStatusBar小部件的指针。
* 与QMainWindow::menuBar()类似,小部件在函数第一次被调用时自动创建。*/
void MainWindow::createStatusBar()
{
/* 隐藏正常状态指示并以指定的毫秒数(超时)显示给定消息。
如果timeout为0(默认值),则消息将一直显示,直到clearMessage()槽被调用,
或者直到再次调用showMessage()槽来更改消息。
* 请注意,调用showMessage()是为了显示工具提示文本的临时解释,因此传递0的超时时间不足以显示永久消息。*/
statusBar()->showMessage(tr("Ready"));
}
/* 从构造函数调用readSettings()函数来加载用户的首选项和其他应用程序设置。
* QSettings类提供了一个高级接口,用于将设置永久存储在磁盘上。在Windows上,它使用(in)著名的Windows注册表;
* 在macOS上,它使用原生的基于xml的CFPreferences API;在Unix/X11上,它使用文本文件。*/
void MainWindow::readSettings()
{
/* QSettings构造函数接受标识公司和产品名称的参数。这确保了不同应用程序的设置是分开的。*/
QSettings settings(QCoreApplication::organizationName(), QCoreApplication::applicationName());
/* 我们使用QSettings::value()来提取几何设置的值。QSettings::value()的第二个参数是可选的,如果不存在,则为设置
指定一个默认值。此值在应用程序第一次运行时使用。*/
const QByteArray geometry = settings.value("geometry", QByteArray()).toByteArray();
if (geometry.isEmpty()) { // 如果没有
const QRect availableGeometry = screen()->availableGeometry();
resize(availableGeometry.width() / 3, availableGeometry.height() / 2); // 调整大小
move((availableGeometry.width() - width()) / 2, // 移动位置
(availableGeometry.height() - height()) / 2);
} else {
// 使用QWidget::saveGeometry()和Widget::restoreGeometry()来保存位置。
// 它们使用不透明的QByteArray来存储屏幕编号、几何形状和窗口状态。
restoreGeometry(geometry); // 恢复设置
}
}
/* writessettings()函数是从closeEvent()调用的。
* 写入设置与读取设置类似,只是更简单一些。
* 构造函数QSettings的参数必须与readSettings()中的参数相同。*/
void MainWindow::writeSettings()
{
// 计算机\HKEY_USERS\S-1-5-21-1785755811-1963137699-1591789978-500\SOFTWARE\QtProjectZSH\Application Example
QSettings settings(QCoreApplication::organizationName(), QCoreApplication::applicationName());
settings.setValue("geometry", saveGeometry());
}
/*调用maybeSave()函数来保存挂起的更改。
* 如果有挂起的更改,它会弹出一个QMessageBox,让用户保存文档。
* 选项有:QMessageBox::Yes、QMessageBox::No、QMessageBox::Cancel。
* 使用QMessageBox:: default标志将Yes按钮设置为默认按钮(当用户按回车键时调用的按钮);
* 使用QMessageBox:: escape标志将Cancel按钮变为escape按钮(用户按Esc时调用的按钮)。
* maybeSave()函数在所有情况下都会返回true,除非用户单击Cancel或保存文件失败。如果返回值为false,调用者必须检查返回值,
并停止正在进行的操作
*/
bool MainWindow::maybeSave()
{
if (!textEdit->document()->isModified())
return true;
const QMessageBox::StandardButton ret
= QMessageBox::warning(this, tr("Application"),
tr("The document has been modified.\n"
"Do you want to save your changes?"),
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
switch (ret) {
case QMessageBox::Save:
return save();
case QMessageBox::Cancel:
return false;
default:
break;
}
return true;
}
/* 在loadFile()中,我们使用QFile和QTextStream来读取数据。QFile对象提供对存储在文件中的字节的访问。*/
void MainWindow::loadFile(const QString &fileName)
{
QFile file(fileName);
/* 我们首先以只读模式打开文件。QFile::Text标志表示该文件是文本文件,不是二进制文件。
在Unix和macOS上,这没有区别,但在Windows上,它确保了在读取时,“\r\n”行尾序列被转换为“\n”。*/
if (!file.open(QFile::ReadOnly | QFile::Text)) {
QMessageBox::warning(this, tr("Application"),
tr("Cannot read file %1:\n%2.")
.arg(QDir::toNativeSeparators(fileName), file.errorString()));
return;
}
/* 如果成功打开文件,则使用QTextStream对象读取数据。QTextStream自动将8位数据转换为Unicode QString
并支持各种编码。如果没有指定编码,QTextStream假设文件使用系统默认的8位编码(例如,Latin-1;
参见QTextCodec::codecForLocale()获取详细信息)。*/
QTextStream in(&file);
/* 由于对QTextStream::readAll()的调用可能需要一些时间,所以在整个应用程序继续运行时,我们将游标设置
为Qt::WaitCursor。*/
#ifndef QT_NO_CURSOR
QGuiApplication::setOverrideCursor(Qt::WaitCursor);
#endif
textEdit->setPlainText(in.readAll());
#ifndef QT_NO_CURSOR
QGuiApplication::restoreOverrideCursor();
#endif
setCurrentFile(fileName);
// 最后,我们调用私有的setCurrentFile()函数(稍后将介绍这个函数),并在状态栏中显示字符串“File loaded”2秒(2000毫秒)。
statusBar()->showMessage(tr("File loaded"), 2000);
}
/* 保存文件与加载文件类似。我们使用QSaveFile来确保所有数据都被安全写入,并且如果写入失败,现有的文件不会被损坏。
* 我们使用QFile::Text标志来确保在Windows上,“\n”被转换为“\r\n”,以符合Windows的约定。*/
bool MainWindow::saveFile(const QString &fileName)
{
QString errorMessage;
QGuiApplication::setOverrideCursor(Qt::WaitCursor);
QSaveFile file(fileName);
if (file.open(QFile::WriteOnly | QFile::Text)) {
QTextStream out(&file);
out << textEdit->toPlainText();
if (!file.commit()) {
errorMessage = tr("Cannot write file %1:\n%2.")
.arg(QDir::toNativeSeparators(fileName), file.errorString());
}
} else {
errorMessage = tr("Cannot open file %1 for writing:\n%2.")
.arg(QDir::toNativeSeparators(fileName), file.errorString());
}
QGuiApplication::restoreOverrideCursor();
if (!errorMessage.isEmpty()) {
QMessageBox::warning(this, tr("Application"), errorMessage);
return false;
}
setCurrentFile(fileName);
statusBar()->showMessage(tr("File saved"), 2000);
return true;
}
/* 当加载或保存文件时,或当用户开始编辑新文件时(在这种情况下fileName为空),调用setCurrentFile()函数来
重置几个变量的状态。我们更新curFile变量,清除QTextDocument::modified标志和相关的QWidget:windowModified标志,
并更新窗口标题以包含新的文件名(或untitled.txt)。*/
void MainWindow::setCurrentFile(const QString &fileName)
{
curFile = fileName;
textEdit->document()->setModified(false);
setWindowModified(false);
QString shownName = curFile;
if (curFile.isEmpty())
shownName = "untitled.txt";
setWindowFilePath(shownName);
}
/* 在QWidget::setWindowTitle()调用中,围绕curFile的strippedName()函数调用缩短文件名以排除路径。函数如下:*/
//QString MainWindow::strippedName(const QString &fullFileName)
//{
// return QFileInfo(fullFileName).fileName();
//}
#ifndef QT_NO_SESSIONMANAGER
void MainWindow::commitData(QSessionManager &manager)
{
qDebug() << "commitData";
if (manager.allowsInteraction()) {
if (!maybeSave())
manager.cancel();
} else {
// Non-interactive: save without asking
if (textEdit->document()->isModified())
save();
}
}
#endif
application.qrc
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file>images/copy.png</file>
<file>images/cut.png</file>
<file>images/new.png</file>
<file>images/open.png</file>
<file>images/paste.png</file>
<file>images/save.png</file>
</qresource>
</RCC>