How to implement Open Recent Files in Qt
This article shows how to implement Open Recent Files functionality in Qt. This will be demonstrated on a minimalistic implementation of an image viewer. The situation is illustrated in the figure below:
To implement Open Recent we need to introduce the following objects and functions:
- a submenu QMenu* recentFilesMenu which will appear after we click on Open Recent
- a slot openRecent() that is called anytime we choose a file from recentFilesMenu
- a list of QActions QList<QAction*> recentFileActionList. This list of actions represents the recently opened files
- const int maxFileNr representing the maximum number of files that can be held in the recentFilesMenu
- QString currentFilePath that stores the path of the recently opened file
- function loadFile(QString) and two helper functions adjustForCurrentFile(QString) and updateRecentActionList(QString)
There are two important points to stress about the current implementation:
- The recently opened files are represented by a list of QActions. The number of actions in the menu remains constant (i.e. 4 in our case). We do not create actions every time there is a change in the recently opened files. We create a constant number of QActions at the beginning and then update their text, data, and visibility whenever the user opens another file. At the start, all actions are invisible.
- The names and paths of the recently opened files are stored in the configuration file that can be accessed via QSettings. They are not stored as member variables of the MainWindow! This enables other instances of the program to access recently opened files, as well.
Implementation of MainWindow
Below you can see the declaration of the MainWindow with the above-mentioned requirements. You can read about the details of these functions below. Please also notice the QLabel *imageLabel that will hold the image that we load from a file.
class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = Q_NULLPTR); private slots: void openRecent(); void open(); private: QMenu* fileMenu; QMenu* recentFilesMenu; QAction* openAction; QListrecentFileActionList; const int maxFileNr; QString currentFilePath; QLabel *imageLabel; void createActionsAndConnections(); void createMenus(); void loadFile(const QString& filePath); void adjustForCurrentFile(const QString& filePath); void updateRecentActionList(); };
In the MainWindow constructor we initialize the value of maxFileNr and create the actions, menus and connections.
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), maxFileNr(4) { imageLabel = new QLabel; setCentralWidget(imageLabel); createActionsAndConnections(); createMenus(); resize(350, 250); }
In createActionsAndConnections() we initialize the standard openAction and 4 QActions associated with the recentFileActionList. We also connect the openAction to the slot open() and each of the QActions in the recentFileActionList to the slot openRecent().
void MainWindow::createActionsAndConnections(){ openAction = new QAction(tr("&Open..."), this); openAction->setShortcuts(QKeySequence::Open); QObject::connect(openAction, &QAction::triggered, this, MainWindow::open); QAction* recentFileAction = 0; for(auto i = 0; i < maxFileNr; ++i){ recentFileAction = new QAction(this); recentFileAction->setVisible(false); QObject::connect(recentFileAction, &QAction::triggered, this, MainWindow::openRecent); recentFileActionList.append(recentFileAction); } }
In createMenus() we create the fileMenu and append openAction. We also create the recentFilesMenu in association with the fileMenu and append the actions from the recentFileActionList. At the end, we call updateRecentActionList(). The details of updateRecentActionList() will be discussed later.
void MainWindow::createMenus(){ fileMenu = menuBar()->addMenu(tr("&File")); fileMenu->addAction(openAction); recentFilesMenu = fileMenu->addMenu(tr("Open Recent")); for(auto i = 0; i < maxFileNr; ++i) recentFilesMenu->addAction(recentFileActionList.at(i)); updateRecentActionList(); }
How to implement open() and openRecent()
In open() we first open a QFileDialog. If the user makes a choice, we call the function loadFile(filePath). Similarly, in openRecentFile() we first identify the QAction that called the slot and then load the corresponding file. The entire file path is stored in action->data(), the name of the file without the path is stored in action->text().
void MainWindow::open(){ QString filePath = QFileDialog::getOpenFileName( this, tr("Open File"), "", tr("Images (*.png *.xpm *.jpg *.gif)")); if (!filePath.isEmpty()) loadFile(filePath); } void MainWindow::openRecent(){ QAction *action = qobject_cast<QAction *>(sender()); if (action) loadFile(action->data().toString()); }
How to implement loadFile(QString)
The private function loadFile(QString) is called both by the slot open() and openRecent(). The responsibility of loadFile(QString) is:
- to issue a warning in case the file is not present at the original path.
- to load the image from the filePath and issue information about possible issues.
- to call the function adjustForCurrentFile(QString) which updates the value of private variable currentFilePath and the list of recently opened files in the configuration file.
You can see its implementation below:
void MainWindow::loadFile(const QString &filePath){ QFile file(filePath); if (!file.open(QFile::ReadOnly)) { QMessageBox::warning(this, tr("Recent Files Example"), tr("This file could not be found:\n%1.") .arg(filePath)); return; } QPixmap pMap(filePath); if (pMap.isNull()) { QMessageBox::information(this, tr("Recent Files Example"), tr("Cannot load:\n%1.") .arg(filePath)); return; } imageLabel->setPixmap(pMap); imageLabel->setAlignment(Qt::AlignCenter); adjustForCurrentFile(filePath); }
How to implement adjustForCurrentFile(QString)
As you can see from the figure above, the function adjustForCurrentFile(QString) is called by the function loadFile(QString). The responsibility of adjustForCurrentFile(QString) is:
- to update the value of the private variable currentFilePath.
- to update the list of recently opened files in the configuration file. This is accomplished via QSettings.
- to call updateRecentActionList() which retrieves the list of recently opened files from the configuration file and changes the text, data and visibility of the QActions in the recentFileActionList.
If you have not worked with configuration files and QSettings before, read this article on QSettings first. In our case the configuration file is created and identified based on the information that we provided in the main.cpp through setOrganizationName() and setApplicationName(). You do not have to look for the configuration file or know how it looks like since we are going to retrieve the necessary information from the very same location via QSettings in updateRecentActionList(). Notice that before adding the newly opened file to the top of the recentFilePaths list in the configuration file, we have to first remove all its previous occurrences with removeAll().
void MainWindow::adjustForCurrentFile(const QString &filePath){ currentFilePath = filePath; setWindowFilePath(currentFilePath); QSettings settings; QStringList recentFilePaths = settings.value("recentFiles").toStringList(); recentFilePaths.removeAll(filePath); recentFilePaths.prepend(filePath); while (recentFilePaths.size() > maxFileNr) recentFilePaths.removeLast(); settings.setValue("recentFiles", recentFilePaths); // see note updateRecentActionList(); }
Note: In a normal application you would also have several other actions such as New, Save, Save As (such as in this official example). In particular, in the case of New you would probably create a new instance of MainWindow (i.e. you would have several instances of MainWindow present). In such a case you would like to see that an update in the recent files menu of one window causes an update in the recent files menu of all other windows. This is why you should call updateRecentActionList() on all top-level windows. This is shown below.
foreach (QWidget *widget, QApplication::topLevelWidgets()) { MainWindow *mainwindow = qobject_cast<MainWindow*>(widget); if (mainwindow) mainwindow->updateRecentActionList(); }
Beware, however, that if you run several instances of the program (i.e. multiple processes), the code above won't help you.
Implementation of updateRecentActionList()
The function updateRecentActionList() is called whenever we are loading (or saving) a file. The function updates the recentFileActionList according to the newly updated configuration file. If the list of recently opened files contains fewer files than the allowed number (maxFileNr), we make the remaining actions invisible. Notice that we have to change both the text and the data of the QActions to account for the name and the path of the newly opened file.
void MainWindow::updateRecentActionList(){ QSettings settings; QStringList recentFilePaths = settings.value("recentFiles").toStringList(); auto itEnd = 0u; if(recentFilePaths.size() <= maxFileNr) itEnd = recentFilePaths.size(); else itEnd = maxFileNr; for (auto i = 0u; i < itEnd; ++i) { QString strippedName = QFileInfo(recentFilePaths.at(i)).fileName(); recentFileActionList.at(i)->setText(strippedName); recentFileActionList.at(i)->setData(recentFilePaths.at(i)); recentFileActionList.at(i)->setVisible(true); } for (auto i = itEnd; i < maxFileNr; ++i) recentFileActionList.at(i)->setVisible(false); }
That's it. To try out the example you need to open a couple of files with Open. When you do that, their names will appear under Open recent. Try to add more than 4 files or add a file that already appears in the list. You will see that these cases are well taken care of!
Tagged: Qt