Displaying the selected album with AlbumWidget

This widget will display the data of the selected album from AlbumListWidget. Some buttons will allow us to interact with this album.

Here is the layout of the AlbumWidget.ui file:

The top frame, albumInfoFrame, with a horizontal layout, contains:

  • albumName: This object displays the album's name (Lorem ipsum in the designer)
  • addPicturesButton: This object allows the user to add pictures selecting files
  • editButton: This object is used to rename the album
  • deleteButton: This object is used to delete the album

The bottom element, thumbnailListView, is a QListView. This list view represents items from PictureModel. By default, QListView is able to display a picture next to text requesting Qt::DisplayRole and Qt::DecorationRole from the model.

Take a look at the header AlbumWidget.h file:

#include <QWidget> 
#include <QModelIndex> 
 
namespace Ui { 
class AlbumWidget; 
} 
 
class AlbumModel; 
class PictureModel; 
class QItemSelectionModel; 
class ThumbnailProxyModel; 
 
class AlbumWidget : public QWidget 
{ 
    Q_OBJECT 
 
public: 
    explicit AlbumWidget(QWidget *parent = 0); 
    ~AlbumWidget(); 
 
    void setAlbumModel(AlbumModel* albumModel); 
    void setAlbumSelectionModel(QItemSelectionModel* albumSelectionModel); 
    void setPictureModel(ThumbnailProxyModel* pictureModel); 
    void setPictureSelectionModel(QItemSelectionModel* selectionModel); 
 
signals: 
    void pictureActivated(const QModelIndex& index); 
 
private slots: 
    void deleteAlbum(); 
    void editAlbum(); 
    void addPictures(); 
 
private: 
    void clearUi(); 
    void loadAlbum(const QModelIndex& albumIndex); 
 
private: 
    Ui::AlbumWidget* ui; 
    AlbumModel* mAlbumModel; 
    QItemSelectionModel* mAlbumSelectionModel; 
 
    ThumbnailProxyModel* mPictureModel; 
    QItemSelectionModel* mPictureSelectionModel; 
}; 

As this widget needs to deal with Album and Picture data, this class has AlbumModel and ThumbnailProxyModel setters. We also want to know and share the model selection with other widgets and views (that is, AlbumListWidget). That is why we also have Album and Picture model selection setters.

The signal pictureActivated() will be triggered when the user double-clicks on a thumbnail. We will see later how MainWindow will connect to this signal to display the picture at full size.

The private slots, deleteAlbum()editAlbum(), and addPictures(), will be called when the user clicks on one of these buttons.

Finally, the loadAlbum() function will be called to update the UI for a specific album. The clearUi()function will be useful to clear all information displayed by this widget UI.

Take a look at the beginning of the implementation in the AlbumWidget.cpp file:

#include "AlbumWidget.h" 
#include "ui_AlbumWidget.h" 
 
#include <QInputDialog> 
#include <QFileDialog> 
 
#include "AlbumModel.h" 
#include "PictureModel.h" 
 
AlbumWidget::AlbumWidget(QWidget *parent) : 
    QWidget(parent), 
    ui(new Ui::AlbumWidget), 
    mAlbumModel(nullptr), 
    mAlbumSelectionModel(nullptr), 
    mPictureModel(nullptr), 
    mPictureSelectionModel(nullptr) 
{ 
    ui->setupUi(this); 
    clearUi(); 
 
    ui->thumbnailListView->setSpacing(5); 
    ui->thumbnailListView->setResizeMode(QListView::Adjust); 
    ui->thumbnailListView->setFlow(QListView::LeftToRight); 
    ui->thumbnailListView->setWrapping(true); 
 
    connect(ui->thumbnailListView, &QListView::doubleClicked, 
            this, &AlbumWidget::pictureActivated); 
 
    connect(ui->deleteButton, &QPushButton::clicked, 
            this, &AlbumWidget::deleteAlbum); 
 
    connect(ui->editButton, &QPushButton::clicked, 
            this, &AlbumWidget::editAlbum); 
 
    connect(ui->addPicturesButton, &QPushButton::clicked, 
            this, &AlbumWidget::addPictures); 
} 
 
AlbumWidget::~AlbumWidget() 
{ 
    delete ui; 
} 

The constructor configures thumbnailListView, our QListView that will display thumbnails of the current selected album. We set here various parameters:

  • setSpacing(): In this parameter, by default items are glued to each other. You can add spacing between them.
  • setResizeMode(): This parameter dynamically lays out items when the view is resized. By default, items keep their original placement even if the view is resized.
  • setFlow(): This parameter specifies the list direction. Here we want to display items from left to right. By default, the direction is TopToBottom.
  • setWrapping(): This parameter allows an item to wrap when there is not enough space to display it in the visible area. By default, wrapping is not allowed and scrollbars will be displayed.

The end of the constructor performs all the signal connections related to the UI. The first one is a good example of signal relaying. We connect the QListView::doubleClicked signal to our class signal, AlbumWidget::pictureActivated. Other connections are common; we want to call a specific slot when the user clicks on a button. As always in the Qt Designer Form Class, the destructor will delete the member variable ui.

Let's see the AlbumModel setter implementation:

void AlbumWidget::setAlbumModel(AlbumModel* albumModel) 
{ 
    mAlbumModel = albumModel; 
 
    connect(mAlbumModel, &QAbstractItemModel::dataChanged, 
        [this] (const QModelIndex &topLeft) { 
            if (topLeft == mAlbumSelectionModel->currentIndex()) { 
                loadAlbum(topLeft); 
            } 
    }); 
} 
 
void AlbumWidget::setAlbumSelectionModel(QItemSelectionModel* albumSelectionModel) 
{ 
    mAlbumSelectionModel = albumSelectionModel; 
 
    connect(mAlbumSelectionModel, 
            &QItemSelectionModel::selectionChanged, 
            [this] (const QItemSelection &selected) { 
                if (selected.isEmpty()) { 
                    clearUi(); 
                    return; 
                } 
                loadAlbum(selected.indexes().first()); 
    }); 
} 

If the selected album's data changed, we need to update the UI with the loadAlbum() function. A test is performed to ensure that the updated album is the currently selected one. Notice that the QAbstractItemModel::dataChanged() function has three parameters but the lambda slot syntax allows us to omit unused parameters.

Our AlbumWidget component must update its UI according to the currently selected album. As we share the same selection model, each time the user selects an album from AlbumListWidget, the signal QItemSelectionModel::selectionChanged is triggered. In this case, we update the UI by calling the loadAlbum() function. As we do not support album multi-selection, we can restrict the process to the first selected element. If the selection is empty, we simply clear the UI.

It is now the turn of the PictureModel setter implementation:

void AlbumWidget::setPictureModel(PictureModel* pictureModel) 
{ 
    mPictureModel = pictureModel; 
    ui->thumbnailListView->setModel(mPictureModel); 
} 
 
void AlbumWidget::setPictureSelectionModel(QItemSelectionModel* selectionModel) 
{ 
    ui->thumbnailListView->setSelectionModel(selectionModel); 
} 

It is very simple here. We set the model and the selection model of thumbnailListView, our QListView that will display the selected album's thumbnails. We also keep the picture model to manipulate the data later on.

We can now cover the features one by one. Let's start with album deletion:

void AlbumWidget::deleteAlbum() 
{ 
    if (mAlbumSelectionModel->selectedIndexes().isEmpty()) { 
        return; 
    } 
    int row = mAlbumSelectionModel->currentIndex().row(); 
    mAlbumModel->removeRow(row); 
 
    // Try to select the previous album 
    QModelIndex previousModelIndex = mAlbumModel->index(row - 1, 
        0); 
    if(previousModelIndex.isValid()) { 
        mAlbumSelectionModel->setCurrentIndex(previousModelIndex, 
             QItemSelectionModel::SelectCurrent); 
        return; 
    } 
 
    // Try to select the next album 
    QModelIndex nextModelIndex = mAlbumModel->index(row, 0); 
    if(nextModelIndex.isValid()) { 
        mAlbumSelectionModel->setCurrentIndex(nextModelIndex, 
            QItemSelectionModel::SelectCurrent); 
        return; 
    } 
} 

The most important task in the deleteAlbum() function is to retrieve the current row index from mAlbumSelectionModel. Then, we can request mAlbumModel to delete this row. The rest of the function will only try to automatically select the previous or the next album. Once again, as we shared the same selection model, AlbumListWidget will automatically update its album selection.

The following snippet shows the album rename feature:

void AlbumWidget::editAlbum() 
{ 
    if (mAlbumSelectionModel->selectedIndexes().isEmpty()) { 
        return; 
    } 
 
    QModelIndex currentAlbumIndex =  
        mAlbumSelectionModel->selectedIndexes().first(); 
 
    QString oldAlbumName = mAlbumModel->data(currentAlbumIndex, 
        AlbumModel::Roles::NameRole).toString(); 
 
    bool ok; 
    QString newName = QInputDialog::getText(this, 
                                            "Album's name", 
                                            "Change Album name", 
                                            QLineEdit::Normal, 
                                            oldAlbumName, 
                                            &ok); 
 
    if (ok && !newName.isEmpty()) { 
        mAlbumModel->setData(currentAlbumIndex, 
                             newName, 
                             AlbumModel::Roles::NameRole); 
    } 
} 

Here, again the QInputDialog class will help us to implement a feature. You should be confident with its behavior now. This function performs three steps:

  1. Retrieve the current name from album model.
  2. Generate a great input dialog.
  3. Request the album model to update the name

As you can see, the generic functions data() and setData() from the models are very powerful when combined with ItemDataRole. As already explained, we do not directly update our UI; this will be automatically performed because setData() emits a signal, dataChanged(), which AlbumWidget handles.

The last feature allows us to add some new picture files in the current album:

void AlbumWidget::addPictures() 
{ 
    QStringList filenames = 
        QFileDialog::getOpenFileNames(this, 
            "Add pictures", 
             QDir::homePath(), 
            "Picture files (*.jpg *.png)"); 
 
    if (!filenames.isEmpty()) { 
        QModelIndex lastModelIndex; 
        for (auto filename : filenames) { 
            Picture picture(filename); 
            lastModelIndex = mPictureModel->pictureModel()->addPicture(picture); 
        } 
        ui->thumbnailListView->setCurrentIndex(lastModelIndex); 
    } 
} 

The QFileDialog class is used here to help the user select several picture files. For each filename, we create a Picture data holder, like we have already seen in this chapter for album creation. Then we can request mPictureModel to add this picture in the current album. Note that, because mPictureModel is a ThumbnailProxyModel class, we have to retrieve the real PictureModel using the helper function, pictureModel(). As the function addPicture() returns us the corresponding QModelIndex, we finally select the most recently added picture in thumbnailListView.

Let's complete AlbumWidget.cpp:

void AlbumWidget::clearUi() 
{ 
    ui->albumName->setText(""); 
    ui->deleteButton->setVisible(false); 
    ui->editButton->setVisible(false); 
    ui->addPicturesButton->setVisible(false); 
} 
 
void AlbumWidget::loadAlbum(const QModelIndex& albumIndex) 
{ 
    mPictureModel->pictureModel()->setAlbumId(mAlbumModel->data(albumIndex, 
        AlbumModel::Roles::IdRole).toInt()); 
 
    ui->albumName->setText(mAlbumModel->data(albumIndex, 
        Qt::DisplayRole).toString()); 
 
    ui->deleteButton->setVisible(true); 
    ui->editButton->setVisible(true); 
    ui->addPicturesButton->setVisible(true); 
} 

The clearUi()function clears the album's name and hides the buttons, while the loadAlbum() function retrieves the Qt::DisplayRole (the album's name) and displays the buttons.