QML C++ integration: View Models for dynamic classes containing arrays of other classes

823 views Asked by At

I'm coming from c++ and I'm having difficulties making my classes work the way I want them to. The program I am building is rather simple. The user creates an album or two and can fill that album with cards. Those albums are created dynamically on run time.

Here is my initial class diagram for the back-end c++ stuff: class diagram

And here is how the UI looks like: QML UI

The crux of the problem: A set of changes to the classes are necessary to make them available to QML.

  • Inheritance: Classes need to inherit from QObject or QAbstractListModel depending on their role.
  • Containers: Dynamically instantiated objects within an object, through pointers or not, need to be placed in a QList or something similar.
  • Root context property: The model needs to be registered so that it can be made available in a Listview for example.

I have had some success setting an instance of an Album class, with some changes made to it, as a root context property. The model worked correctly and the cards in the album displayed correctly in a ListView.

The problem here is that I need to step out of the picture another level, I should set a class that contains the Albums as the root context property and let the user create, and add, Albums to it during run time. I will also need to also display the albums and the cards inside them through different views.

I am wondering if I should implement a table/tree data structure for the albums and the cards and somehow pass the columns and rows to the models or If there is a tidier approach that I am not aware of.

Here are my classes so far:

album.h

#ifndef ALBUM_H
#define ALBUM_H

#include <QObject>
#include <QString>
#include <QAbstractListModel>
#include <QQmlListProperty>
#include "card.h"


class Album : public QAbstractListModel
{
    Q_OBJECT
public:
    enum AlbumRoles {
        CardIDRole = Qt::UserRole + 1,
        NameRole,
        ImageURLRole,
        SubtypeRole,
        SupertypeRole,
        NumberRole,
        ArtistRole,
        RarityRole,
        SeriesRole,
        SetRole,
        SetCodeRole,
        ConditionRole,
        StatusRole
    };

    Album(QObject *parent = 0);
    QString name() const;
    void addCard(const Card &card);

    int rowCount(const QModelIndex & parent = QModelIndex()) const;

    QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const;

protected:
    QHash<int, QByteArray> roleNames() const;

private:
    QList<Card> m_cards;
    QString m_name;
};

album.cpp

#include "album.h"

Album::Album(QObject *parent)
    : QAbstractListModel(parent)
{
}

QString Album::name() const
{
    return m_name;
}

void Album::addCard(const Card &card)
{
    beginInsertRows(QModelIndex(), rowCount(), rowCount());
    m_cards << card;
    endInsertRows();
}

int Album::rowCount(const QModelIndex & parent) const {
    Q_UNUSED(parent);
    return m_cards.count();
}

QVariant Album::data(const QModelIndex & index, int role) const {
    if (index.row() < 0 || index.row() >= m_cards.count())
        return QVariant();

    const Card &card = m_cards[index.row()];
    if (role == CardIDRole)
        return card.cardID();
    else if (role == NameRole)
        return card.name();
    else if (role == ImageURLRole)
        return card.imageURL();
    else if (role == SubtypeRole)
        return card.subtype();
    else if (role == SupertypeRole)
        return card.supertype();
    else if (role == NumberRole)
        return card.number();
    else if (role == ArtistRole)
        return card.artist();
    else if (role == RarityRole)
        return card.rarity();
    else if (role == SeriesRole)
        return card.series();
    else if (role == SetRole)
        return card.set();
    else if (role == SetCodeRole)
        return card.setCode();
    else if (role == ConditionRole)
        return card.condition();
    else if (role == StatusRole)
        return card.status();
    return QVariant();
}

QHash<int, QByteArray> Album::roleNames() const {
    QHash<int, QByteArray> roles;
    roles[CardIDRole] = "cardID";
    roles[NameRole] = "name";
    roles[ImageURLRole] = "imageURL";
    roles[SubtypeRole] = "subtype";
    roles[SupertypeRole] = "supertype";
    roles[NumberRole] = "number";
    roles[ArtistRole] = "artist";
    roles[RarityRole] = "rarity";
    roles[SeriesRole] = "series";
    roles[SetRole] = "set";
    roles[SetCodeRole] = "setCode";
    roles[ConditionRole] = "condition";
    roles[StatusRole] = "status";
    return roles;
}

card.h

#ifndef CARD_H
#define CARD_H
#include <QString>

class Card
{
public:
    // Standard Qt constructor with parent for memory management
    Card(const QString &cardID, const QString &name, const QString &imageURL, const QString &subtype, const QString &supertype, const int &number, const QString &artist, const QString &rarity, const QString &series, const QString &set, const QString &setCode, const QString &condition, const QString &status);

    QString cardID() const;
    QString name() const;
    QString imageURL() const;
    QString subtype() const;
    QString supertype() const;
    int number() const;
    QString artist() const;
    QString rarity() const;
    QString series() const;
    QString set() const;
    QString setCode() const;
    QString condition() const;
    QString status() const;


private:
    // private members
    QString m_cardID;
    QString m_name;
    QString m_imageURL;
    QString m_subtype;
    QString m_supertype;
    int m_number;
    QString m_artist;
    QString m_rarity;
    QString m_series;
    QString m_set;
    QString m_setCode;
    QString m_condition;
    QString m_status;
};
#endif //CARD_H

card.cpp

#include "card.h"

// Standard Qt constructor with parent for memory management
Card::Card(const QString &cardID, const QString &name, const QString &imageURL, const QString &subtype, const QString &supertype, const int &number, const QString &artist, const QString &rarity, const QString &series, const QString &set, const QString &setCode, const QString &condition, const QString &status): m_cardID(cardID), m_name(name), m_imageURL(imageURL), m_subtype(subtype), m_supertype(supertype), m_number(number), m_artist(artist), m_rarity(rarity), m_series(series), m_set(set), m_setCode(setCode), m_condition(condition), m_status(status)
{

}

QString Card::cardID() const
{
    return m_cardID;
}

QString Card::name() const
{
    return m_name;
}

QString Card::imageURL() const
{
    return m_imageURL;
}

QString Card::subtype() const
{
    return m_subtype;
}

QString Card::supertype() const
{
    return m_supertype;
}

int Card::number() const
{
    return m_number;
}

QString Card::artist() const
{
    return m_artist;
}

QString Card::rarity() const
{
    return m_rarity;
}

QString Card::series() const
{
    return m_series;
}

QString Card::set() const
{
    return m_set;
}

QString Card::setCode() const
{
    return m_setCode;
}

QString Card::condition() const
{
    return m_condition;
}

QString Card::status() const
{
    return m_status;
}

main.cpp

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QtQml>
#include "app.h"
#include "album.h"
#include "card.h"

int main(int argc, char *argv[])
{

    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QGuiApplication app(argc, argv);

    qmlRegisterType<Album>("Classes.PokemonApp", 1, 0, "Album");
    qmlRegisterType<Card>("Classes.PokemonApp", 1, 0, "Card");

    App pokeApp;
    Album myAlbumModel;
    Card cardOne("xy7-2","Gloom","http://s3.amazonaws.com/pokemontcg/xy7/2.png","Stage 1","Pokémon",2,"Masakazu Fukuda","Uncommon","XY","Ancient Origins","xy7","Mint","In my collection");
    Card cardTwo("xy7-7","Sceptile-EX","https://s3.amazonaws.com/pokemontcg/xy7/7.png","EX","Pokémon",7,"Eske Yoshinob","Rare Holo EX","XY","Ancient Origins","xy7","Used","Duplicate");

    myAlbumModel.addCard(cardOne);
    myAlbumModel.addCard(cardTwo);
    pokeApp.addAlbum(myAlbumModel);

    QQmlApplicationEngine engine;
    engine.load(QUrl(QLatin1String("qrc:/main.qml")));

    QQmlContext *ctxt = engine.rootContext();
    ctxt->setContextProperty("pokeApp", &pokeApp);

    return app.exec();
}

CardsView.qml

import QtQuick 2.0
import Classes.PokemonApp 1.0
// Cards Deligate

ListView {
    width: 200; height: 250

    model: myAlbumModel
    delegate: Text { text: "Card:"
                           + "\n" + "ID: " + cardID
                           + "\n" + "Name: " + name
                           + "\n" + "Image URL: " + imageURL
                           + "\n" + "Subtype: " + subtype
                           + "\n" + "Supertype: " + supertype
                           + "\n" + "Number: " + number
                           + "\n" + "Artist: " + artist
                           + "\n" + "Rarity: " + rarity
                           + "\n" + "Series: " + series
                           + "\n" + "Set: " + set
                           + "\n" + "Set code: " + setCode
                           + "\n" + "Condition: " + condition
                           + "\n" + "Status: " + status }
}

So to be clear, this is what I need help with: Give the user the ability to create albums during run time, let the user add cards to albums and have models display the albums and the cards in them in different views.

2

There are 2 answers

0
Nizar On BEST ANSWER

I am now finished with the project and I have accomplished all the goals I had and overcome the problems that I have asked for help here and a few other ones that showed up later on.

For those who might find this page while searching for solutions for similar problems they have, you can find all the source code for my complete project on my Github page here: https://github.com/Nizars/PokeApp

I would like to point out a few changes that I have made:

I have switched to an SQL method of storing the cards, the albums and the card images as well.

I have used different models for different views. A proxy model for relational queries used by the table view and two sql models for other functionalities.

I also use a custom made image provider for checking for images requested from the qml side in the database first before requesting the images from an API through https and storing it in the database if it wasn't found there at first.

I have also switched to a borderless window to get rid of the windows default window border and created my own close, minimize and maximize buttons. I have also used a mouse position provider to make dragging the window around possible and seamless.

I have also added a working RSS feed to the homepage and used a page swipe view for the different pages in the app, I used connections to signal messages between the different qml documents so that data can be sent between them.

Final note: You need to include your own OpenSSL library includes for the network requests to work and create the sql tables in your local server as well as connect the app to it in pokeapp.cpp.

Feel free to ask me any questions if you need any help.

0
Kevin Krammer On

Assuming that your App class has a list of Album objects, accessible as a property, adding an album would simply be a slot or invokable method in that class.

Something like

public slots:
    void addAlbum(const QString &name);

That would create an instance of Album, put it in the list and emit the notify signal for the property.

For adding a Card you could add a similar slot to the Album class, something like

pulic slots:
    void addCard(const QString &cardID, const QString &name, const QString &imageURL, const QString &subtype, const QString &supertype, const int &number, const QString &artist, const QString &rarity, const QString &series, const QString &set, const QString &setCode, const QString &condition, const QString &status);

which would simply create a Card add then call the addCard overload.