Search code examples
c++qtqwidgetqlistviewqstyleditemdelegate

QListView only showing a single item in the view


I am utilizing the Model View Delegate framework used by Qt for displaying lists of objects with custom 'views' or layouts.

Background:

I require showing a country flag, country name, city name and an optional 'premium' rating star in a list, which can be selected by a user.

To achieve this, I use the following components:

  • Model - a QStandardItemModel, see doc page, which holds all the data of the QListView and interaction concerns, doc page

  • Delegate - For drawing/displaying the data in a custom layout fashion, I use a QStyledItemDelegate, see doc page.

  • View - For the view, a simple QListView will suffice, as this is a single column list containing a collection of objects with a custom layout.

Tutorials and Help:

Using the following tutorial(s), 1. a messageviewer application showing how to implement a detailed model-delegate-view concept in accordance with, 2. the basics of a simple messaging menu system for Nokia smartphones, I have been able to create, with relative ease, the desired layout for my QListView.

Problem:

I am required to add QStandardItem items to my model, which will be added to my view. I do so, and it is confirmed by the delegate where each item is drawn by the paint override method.

However, during runtime, the QListView only displays 1 item. But I can use my up/down arrow keys to select various other items in the list.

Please see code below:

Setting up MVC(delegate):

mainwindow.h

//...
QStandardItemModel *modelServers;
ServerDelegate* serverDelegate;
//...

mainwindow.cpp

modelServers = new QStandardItemModel(0);
serverDelegate = new ServerDelegate(0);

ui->listServers->setModel(modelServers);
ui->listServers->setItemDelegate(serverDelegate);

and adding items (QStandardItem's) to list:

for (int i = 0; i < someList->length(); ++i) {
    Server server = someList->value(i);
    QStandardItem *item = new QStandardItem();
    item->setData(server.getCountryName,         item->setData(QPixmap("", "PNG"), ServerDelegate::DataRole::CountryFlag);
    item->setData(server.getCountry(), ServerDelegate::DataRole::CountryText);
    item->setData(server.getCity(), ServerDelegate::DataRole::CityText);
    item->setData(i, ServerDelegate::ListIndex);
    //...
    modelServer->appendRow(item)
}

The finally the list displays the data, however the list is only populated with items, but only the first has visible text and images.

note: when scrolling down, only the top item is visible, see images below for an example.

e.g.

Initial Loaded list:

enter image description here

One scroll down

enter image description here

Selecting an item with no text/images:

enter image description here

Additional Code below:

ServerDelegate class handling the custom layout

ServerDelegate.h

#ifndef SERVERDELEGATE_H
#define SERVERDELEGATE_H

#include <QApplication>
#include <QtGui>
#include <QStyledItemDelegate>
#include <QtWidgets>
#include <qglobal.h>

#include "global.h"

class ServerDelegate : public QStyledItemDelegate
{
    Q_OBJECT
public:
    ServerDelegate(QStyledItemDelegate* parent = 0);
    virtual ~ServerDelegate();

    enum DataRole{
        CountryText = Qt::UserRole + 100,
        CityText = Qt::UserRole+101,
        CountryFlag = Qt::UserRole+102,
        SideIconFlag = Qt::UserRole+103,
        ListIndex = Qt::UserRole+105
    };

    void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;

    QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;

private:
    QFont fontCountry, fontCity;
};

#endif // SERVERDELEGATE_H

ServerDelegate.cpp

#include "serverdelegate.h"

ServerDelegate::ServerDelegate(QStyledItemDelegate *parent)
    : QStyledItemDelegate(parent)
{
    fontCountry = QApplication::font();
    fontCountry.setBold(true);
    fontCountry.setPointSize(QApplication::font().pointSize() + 3);

    fontCity = QApplication::font();
    fontCity.setItalic(true);
    fontCity.setPointSize(QApplication::font().pointSize() - 1);
}

ServerDelegate::~ServerDelegate(){
}

QSize ServerDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const{
    Q_UNUSED(index)
    QSize totalCountrySize = QPixmap("","").size();
    QSize totalSideIcon = QPixmap(":/res/images/premium", "PNG").size();

    QFontMetrics fmCountry(fontCountry);
    QFontMetrics fmCity(fontCity);

    int fontHeight = (2 * 2) + (2 * 5) + fmCountry.height() + fmCity.height();
    int iconHeight = (2 * 2) + (totalCountrySize.height() > totalSideIcon.height() ? totalCountrySize.height() : totalSideIcon.height());
    int height = (fontHeight > iconHeight) ? fontHeight : iconHeight;

    int width = option.rect.width();
    QSize size = QSize(width, height);

    return size;
}

void ServerDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const{
    QStyledItemDelegate::paint(painter, option, index);

    QRect rec = option.rect;

    painter->save();
    painter->setClipRect(rec);

    QString countryText = index.data(DataRole::CountryText).toString();
    QString cityText = index.data(DataRole::CityText).toString();
    QPixmap countryFlag = QPixmap(qvariant_cast<QPixmap>(index.data(DataRole::CountryFlag)));
    QPixmap sideIcon = qvariant_cast<QPixmap>(index.data(DataRole::SideIconFlag));

    // Get a rectangle by size x, y.
    // With cooridinates [0,0]; [x,0]; [x,y]; [0,y]
    QRect topLine = option.rect,
            bottomLine = option.rect;

    // create top 'seperator' of X px's width, green in color;
    topLine.setTop(0);
    topLine.setLeft(0);
    topLine.setRight(option.rect.width());
    topLine.setBottom(2); // 1px down

    painter->setPen(QColor(116, 183, 151));
    painter->fillRect(topLine, QColor(116, 183, 151));
    painter->drawRect(topLine);

    // create bottom 'seperator' of X px's width, green in color;
    bottomLine.setTop(option.rect.height() - 2);
    bottomLine.setLeft(0);
    bottomLine.setRight(option.rect.width());
    bottomLine.setBottom(option.rect.height()); // 1px down

    painter->setPen(QColor(116, 183, 151));
    painter->fillRect(bottomLine, QColor(116, 183, 151));
    painter->drawRect(bottomLine);

    // create background rectangle
    QRect content(0, topLine.bottom(), option.rect.width(), bottomLine.top());

    painter->setPen(QColor(116, 183, 151));
    painter->fillRect(content, QColor(116, 183, 151));
    painter->drawRect(content);

    // create content rectangles from content container.

    QRect rectCountryFlag = content,
            rectSideIcon = content;

    //    create country icon rectangle
    QSize countryFlagSize = countryFlag.size();
    int cFPos = (rectCountryFlag.height() / 2) - (countryFlagSize.height() / 2) - 8;
    rectCountryFlag.setTop(cFPos);
    rectCountryFlag.setBottom(content.height() - cFPos);
    rectCountryFlag.setLeft(20 - 8);
    rectCountryFlag.setRight(20 + 16 + countryFlagSize.width());

    painter->drawPixmap(rectCountryFlag, countryFlag);

    //    create side icon rectangle
    QSize sideIconSize = sideIcon.size();
    int siPos = (rectSideIcon.height() / 2) - (sideIconSize.height() / 2) - 4;
    rectSideIcon.setTop(siPos);
    rectSideIcon.setBottom(content.height() - siPos);
    rectSideIcon.setLeft(rec.width() - (10 + 8 + sideIconSize.width()));
    rectSideIcon.setRight(rec.width() - 10);

    painter->drawPixmap(rectSideIcon, sideIcon);

    const QRect textContent(rectCountryFlag.right() + 5 + 20, content.top() + 5,
                            rectSideIcon.left() - 5, content.bottom() - 5);

    // create country text rectangle

    QRect rectCountryText = content,
            rectCityText = content;

    rectCountryText.setLeft(textContent.left());
    rectCountryText.setTop(textContent.top());
    rectCountryText.setRight(textContent.right());
    rectCountryText.setBottom(qRound(textContent.height() * 0.6) - 5);

    painter->setPen(QColor(26, 26, 26));
    painter->setFont(fontCountry);
    painter->drawText(rectCountryText, countryText);

    // create city text rectangle

    rectCityText.setLeft(textContent.left() + ( 2 * 5));
    rectCityText.setTop(rectCountryText.bottom() + 5);
    rectCityText.setRight(textContent.right());
    rectCityText.setBottom(textContent.height());

    painter->setPen(QColor(77, 77, 77));
    painter->setFont(fontCity);
    painter->drawText(rectCityText, cityText);

    // restore painter
    painter->restore();
}

Solution

  • Before I post my answer, a big shoutout to scopchanov for his assistance and example provided, also hinting to the fact that an offset occurs on each call to the paint, something that I was blissfully unaware of.

    Problem Explained:

    The offset values provided by option.rect is the same QSize which is returned by the sizeHint method. In my case, my concern was with the y offset which had a value of 56.

    My Understanding

    Using the concept of the iterative offset value, I modified my code (which had previously the desired output), where I assumed that the paint method was drawn from an origin of [0,0], where infact I had to account for the offset aswel, i.e. I was not provided with a container which was placed at the next QListView item's location, instead I was given the location of the next QListView's item start point, and am required to build the item from there.

    The solution:

    In my case, I had the desired item layout, but items were drawn ontop of each other, due to the [0,0] origin, causing the single item effect. After changing a few values, and building a dependency hierachy of sorts, I was presented with a list of items having the desired layout.

    The Code

    Updated ServerDelegate.cpp

    #include "serverdelegate.h"
    
    ServerDelegate::ServerDelegate(QStyledItemDelegate *parent)
        : QStyledItemDelegate(parent)
    {
        fontCountry = QApplication::font();
        fontCountry.setBold(true);
        fontCountry.setPointSize(QApplication::font().pointSize() + 3);
    
        fontCity = QApplication::font();
        fontCity.setItalic(true);
        fontCity.setPointSize(QApplication::font().pointSize() - 1);
    }
    
    ServerDelegate::~ServerDelegate(){
    }
    
    QSize ServerDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const{
        Q_UNUSED(index)
        QSize totalCountrySize = Global::getCountryFlagFromCache(index.data(DataRole::CountryText).toString()).size();
        QSize totalSideIcon = QPixmap(":/res/images/premium", "PNG").size();
    
        QFontMetrics fmCountry(fontCountry);
        QFontMetrics fmCity(fontCity);
    
        int fontHeight = (2 * AppGlobal::Style_List_Seperator_Width) + (2 * AppGlobal::Style_List_Text_Item_Margin) + fmCountry.height() + fmCity.height();
        int iconHeight = (2 * AppGlobal::Style_List_Seperator_Width) + (totalCountrySize.height() > totalSideIcon.height() ? totalCountrySize.height() : totalSideIcon.height());
        int height = (fontHeight > iconHeight) ? fontHeight : iconHeight;
    
        int width = option.rect.width();
        QSize size = QSize(width, height);
    
        return size;
    }
    
    void ServerDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const{
        QStyledItemDelegate::paint(painter, option, index);
    
        QFontMetrics fmCountry(fontCountry);
        QFontMetrics fmCity(fontCity);
    
        QRect rec = option.rect;
    
        painter->save();
        painter->setClipRect(rec);
    
        QString countryText = index.data(DataRole::CountryText).toString();
        QString cityText = index.data(DataRole::CityText).toString();
        QPixmap countryFlag = QPixmap(qvariant_cast<QPixmap>(index.data(DataRole::CountryFlag)));
        QPixmap sideIcon = qvariant_cast<QPixmap>(index.data(DataRole::SideIconFlag));
    
        // Get a rectangle by size x, y.
        // With cooridinates [0,0]; [x,0]; [x,y]; [0,y]
        QRect topLine = option.rect,
                bottomLine = option.rect;
    
        // create top 'seperator' of X px's width, green in color;
        topLine.setTop(rec.top());
        topLine.setLeft(rec.left());
        topLine.setRight(rec.right());
        topLine.setBottom(rec.top() + AppGlobal::Style_List_Seperator_Width); // 1px down
    
        painter->setPen(AppGlobal::Style_List_Seperator_Color);
        painter->fillRect(topLine, AppGlobal::Style_List_Seperator_Color);
        painter->drawRect(topLine);
    
        // create bottom 'seperator' of X px's width, green in color;
        bottomLine.setTop(rec.bottom() - AppGlobal::Style_List_Seperator_Width);
        bottomLine.setLeft(rec.left());
        bottomLine.setRight(rec.right());
        bottomLine.setBottom(rec.bottom()); // 1px down
    
        painter->setPen(AppGlobal::Style_List_Seperator_Color);
        painter->fillRect(bottomLine, AppGlobal::Style_List_Seperator_Color);
        painter->drawRect(bottomLine);
    
        // create background rectangle
        QRect content(rec.left(), topLine.bottom(), (rec.right() - rec.left()), (bottomLine.top() - topLine.bottom()));
    
        painter->setPen(AppGlobal::Style_List_Background_Color);
        painter->fillRect(content, ((option.state & QStyle::State_MouseOver) ? AppGlobal::Style_List_Hover_Color : AppGlobal::Style_List_Background_Color ));
        painter->drawRect(content);
    
        // create content rectangles from content container.
    
        QRect rectCountryFlag = content,
                rectSideIcon = content;
    
        //    create country icon rectangle
        QSize countryFlagSize = countryFlag.size();
        int cFPos = ((rectCountryFlag.bottom() - rectCountryFlag.top()) / 2) - (countryFlagSize.height() / 2) - 8;
        rectCountryFlag.setTop(rectCountryFlag.top() + cFPos);
        rectCountryFlag.setBottom(content.bottom() - cFPos);
        rectCountryFlag.setLeft(AppGlobal::Style_List_Left_Item_Margin - 8);
        rectCountryFlag.setRight(AppGlobal::Style_List_Left_Item_Margin + 16 + countryFlagSize.width());
    
        painter->drawPixmap(rectCountryFlag, countryFlag);
    
        //    create side icon rectangle
        QSize sideIconSize = sideIcon.size();
        int siPos = ((rectSideIcon.bottom() - rectSideIcon.top()) / 2) - (sideIconSize.height() / 2) - 4;
        rectSideIcon.setTop(rectSideIcon.top() + siPos);
        rectSideIcon.setBottom(content.bottom() - siPos);
        rectSideIcon.setLeft(rec.width() - (AppGlobal::Style_List_Right_Item_Margin + 8 + sideIconSize.width()));
        rectSideIcon.setRight(rec.width() - AppGlobal::Style_List_Right_Item_Margin);
    
        painter->drawPixmap(rectSideIcon, sideIcon);
    
        int textContentLeft = rectCountryFlag.right() + AppGlobal::Style_List_Text_Item_Margin + AppGlobal::Style_List_Left_Item_Margin,
                textContentTop = content.top() + AppGlobal::Style_List_Text_Item_Margin;
    
        const QRect textContent( textContentLeft , textContentTop,
                                (rectSideIcon.left() - AppGlobal::Style_List_Text_Item_Margin) - textContentLeft, (content.bottom() - AppGlobal::Style_List_Text_Item_Margin) - textContentTop);
    
        // create country text rectangle
    
        QRect rectCountryText = content,
                rectCityText = content;
    
        rectCountryText.setLeft(textContent.left());
        rectCountryText.setTop(textContent.top());
        rectCountryText.setRight(textContent.right());
        rectCountryText.setBottom(textContent.top() + fmCountry.height());
    
        painter->setPen(AppGlobal::Style_Heading_Color);
        painter->setFont(fontCountry);
        painter->drawText(rectCountryText, countryText);
    
        // create city text rectangle
    
        rectCityText.setLeft(textContent.left() + ( 2 * AppGlobal::Style_List_Text_Item_Margin));
        rectCityText.setTop(rectCountryText.bottom());
        rectCityText.setRight(textContent.right());
        rectCityText.setBottom(textContent.bottom() + fmCity.height());
    
        painter->setPen(AppGlobal::Style_SubText_Color);
        painter->setFont(fontCity);
        painter->drawText(rectCityText, cityText);
    
        // restore painter
        painter->restore();
    }