Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

QAbstractItemModel with QtQuick: Column is always 0 in the index

I'm quite confused by QML. Since a few weeks, I try to implement a timeline for annotation stuff in videos with QML and I can't really get it worked, since I'm quite new to QML.

I try to work you through my problem. This is an example, how the timeline should look like: Timeline example I got different tracks in which I store different annotations which simply represents, that from a start to an end point the video contains an annotations of the given track. For example if I annotate all scenes in a video which contain sunny images, every annotation box marks the scenes where the video has sunny images.

I plan on saving and getting this information through XML files for example. A possibly example would be:

<root length="800" filename="tralala.mp4">
  <track name="sunny">
    <annotation start="20" end="50"/>
    <annotation start="70" end="120"/>
    ...
  </track>
  <track name="cloudy">
    ...
  </track>
</root>

To get the data into a model I could later use, I parse the file with a method like this:

readModelFromXML():

QFile xmlFile(_filename);
xmlFile.open(QIODevice::ReadOnly);
xml.setDevice(&xmlFile);

TrackItem* root;
while(!xml.atEnd() && !xml.hasError())
{
    QXmlStreamReader::TokenType token = xml.readNext();
    if(token == QXmlStreamReader::StartDocument)
            continue;

    if(token == QXmlStreamReader::StartElement)
    {
       if(xml.name() == "root")
       {
           QMap<QString, QVariant> itemData;
           itemData["length"] = xml.attributes().value("length").toInt();
           itemData["filename"] = xml.attributes().value("filename").toString();
           root = new TrackItem(itemData);
       }
       else if(xml.name() == "track")
       {
           QMap<QString, QVariant> itemData;
           itemData["name"] = xml.attributes().value("name").toString();
           TrackItem* track = new TrackItem(itemData, root);
           root->insertChildren(root->childCount(), track);
       }
       else if(xml.name() == "annotation")
       {
           QMap<QString, QVariant> itemData;
           itemData["start"] = xml.attributes().value("start").toInt();
           itemData["end"] = xml.attributes().value("end").toInt();
           TrackItem* parent = root->child(root->childCount() - 1);
           TrackItem* annotation = new TrackItem(itemData, parent);
           parent->insertChildren(parent->childCount(), annotation);
       }
   }
}

Where a TrackItem holds a QList with its children, a QMap with the stored data and possibly a parent form type TrackItem. So my data than looks much like a tree with a root TrackItem object which has no parent, as data it has stored the length and the filename and as its child objects it has the TrackItems for the different tracks. The track TrackItems than have the root object as their parent and only stores the name of the track. Each track than has the annotations with a start and end point stored as itemData as its childs.

TrackItem.h:

public:
explicit TrackItem(QMap<QString, QVariant> &data, TrackItem *parent = 0);
~TrackItem();

some functions for getting childs, inserting childs and so on

private:
QList<TrackItem*> childItems;
QMap<QString, QVariant> itemData;
TrackItem *parentItem;

So now we're getting closer to my problem. I made my own QAbstractItemModel implementation for the communication to my QtQuick view. My own QAbstractItemModel currently has the following roles.

roleNames():

QHash<int, QByteArray> roles;
roles[NameRole] = "name";
roles[StartFrameRole] = "startFrame";
roles[EndFrameRole] = "endFrame";
return roles;

The data functions looks as follows.

data(const QModelIndex &index, int role):

if (!index.isValid())
    return QVariant();

TrackItem *item = getItem(index);

if (role == NameRole)
    return item->data("name");
else if (role == StartFrameRole)
    return item->data("start");
else if (role == EndFrameRole)
    return item->data("end");

return QVariant();

with getItem(const QModelIndex &index):

if (index.isValid()) {
    TrackItem *item = static_cast<TrackItem*>(index.internalPointer());
    if (item)
        return item;
}
return rootItem;

and TrackItem::data(QString key) returning the data stored in the QMap itemData of the TrackItem.

index(int row, int column, const QModelIndex &parent):

if (parent.isValid() && parent.column() != 0)
    return QModelIndex();

TrackItem *parentItem = getItem(parent);
TrackItem *childItem = parentItem->child(row);

if (childItem)
    return createIndex(row, column, childItem);
else
    return QModelIndex();

So in index I try to create the indexes of the TrackItems.

That much to the C++ side. Now I will cover a bit my QML code and follow with my problem. So on the QML side of the programm, I have a QML file called timeline, which I set in the constructor of my own QWidget, which represents the look of the example above.

TimelineWidget Constructor derived from QWidget:

sharedEngine_ = new QQmlEngine(this);
quickWidget_ = new QQuickWidget(sharedEngine_, this);

QQmlContext *context = quickWidget_->rootContext();
context->setContextProperty("timeline", this);

model_ = new TrackModel(":/resources/example.txt", context);

context->setContextProperty("trackmodel", model_);

quickWidget_->setSource(QUrl::fromLocalFile("qml/timeline.qml"));
ui->layout->addWidget(quickWidget_);

As you can see, at this point I also currently create the QAbstractItemModel and set it as a contect property to the QML context, so I could use my model in QML.

So essentially my timeline QML file is a rectangle containing two columns. In the first one, I create the track heads with the name of the track via a repeater over the context property "trackmodel" from above.

Repeater {
  id: headerRepeater
   model: trackmodel
    TrackHead {
       label: model.name
        width: headerWidth
        height: 50
        selected: false
    }
}

In the second column I essentially than create each of my tracks in a scrollView:

Item {
  width: tracksContainer.width + headerWidth
  height: headers.height + 30
  Column {
    id: tracksContainer
    Repeater {
      id: tracksRepeater
      model: trackDelegateModel
    }
  }
}

Here I use a DelegateModel in which I try to build the individual tracks.

DelegateModel {
  id: trackDelegateModel
  model: trackmodel
  Track {
    model: trackmodel
    trackId: index
    height: 50
    width: timelineLength
    ...
    also here are some "slots"  
  }
}

So now to the Track QML file. Each track is also simply a rectangle in which I try to create new Items, that should represent the annotations. Here I also try to use a delegate.

Item {
  Repeater { id: annotationRepeater; model: trackModel }
}

with this DelegateModel:

DelegateModel {
  id: trackModel
  Annotation {
    myModel: model
    trackIndex: trackId
    height: 15
    width: model.endFrame - model.startFrame
    x: model.startFrame
    y: 17.5
    ...
    like before here are also some "slots"
  }
}

So at this point I try to grab information from the QAbstractItemModel via the startFrame and endFrame roles to calculate the length of each annotation. The Annotation than simply is another Rectangle with some manipulation possibilities to move them to another frame in the track or to a whole other track, trimming them and some other things.

Now finally to my problem. I could build the timeline like in the example above. The yellow boxes in the example where painted over in gimp. I can't get the annotations to show up, because I don't understand how the QModelIndex is created. Everytime I step into the data function of the QAbstractItemModel, I could only get TrackItems from the layer after the root, so just the track layer. In what way does QtQuick communicate with the QAbstractItemModel? I thought that I get a row and column in the index function and could create unique indexes for each TrackItem, so that I could grab the proper TrackItem with the getItem function in my data function. Somehow column is always 0 in the index function. How do I tell my model on which layer (root, track or annotation) I am, so that in QML I could grab the right data in the delegates? I hope my problem is clear enough. This is my first post here, so I may apologies if it is to long or out of form. I really hope, someone could help me with this problem.

like image 509
Fabian Pawlowski Avatar asked Nov 09 '22 01:11

Fabian Pawlowski


1 Answers

I stumbled on the same problem when reimplementing the example "Editable Tree model". I have 2 colums in my model. When I clicked at the second column selection.currentIndex.column always returned 0, so did styleData.index.column property. But styleData.column gave me 1 as I clicked at the second column. So I constructed the index I needed explicitely in QML.

var currentIndex = myTreeModel.index(styleData.index.row, styleData.column, styleData.index.parent)
var success = myTreeModel.setData( currentIndex , loaderEditor.item.text, 2 )

After that I got my setData function to do what it should do - change a value in the second column of the model. It is a kind of hack but still it is working. I hope it could be useful for somebody.

P.S. The only significant difference between the editable model example and my implementation is that they are using the model from C++ side by

  view->setModel(model)

I'm using my model through qmlRegisterType. It is the only difference I can think of.

like image 151
Alex Avatar answered Nov 14 '22 21:11

Alex