5

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.

  • 2
    Your question is very well formated and has a lot of info, but the title is bad. It makes it easier for people to answer and find your post later if your title is more to do with your problem, something like: "Column is always 0 in the index function" this is just a suggestion, your post is good anyway. Hope you get your answer – paulonogueira Feb 16 '17 at 11:03
  • 1
    Yes, you are right: QML only uses the first column of a model. You might however provide a model with one role per track, that contains an object with the data. Another idea would be, to pass your model through a [`ProxyModel`](http://doc.qt.io/qt-5/qabstractproxymodel.html) that reorders the columns, so you have the column of your desire always to be the first. – derM - not here for BOT dreams Feb 16 '17 at 12:08
  • Even the [`TableModel`](http://doc.qt.io/qt-5/qml-qtquick-controls-tableview.html) of QML makes use of only one column of a model. It uses one `role` in the first model column per view column. – derM - not here for BOT dreams Feb 16 '17 at 12:23
  • So I'm right to asume, that it isn't possible to use a mode in tree form like from the [editable tree model example](http://doc.qt.io/qt-5/qtwidgets-itemviews-editabletreemodel-example.html) with QML? I based my model off of this example, because I though it would be a good idea to have the whole timeline organised in a tree, where the childs of the root are the tracks and the childs of the tracks are the annotations. – Fabian Pawlowski Feb 17 '17 at 20:30
  • If you look at the [Simple Tree Model Example](http://doc.qt.io/qt-5/qtwidgets-itemviews-simpletreemodel-example.html) you can see, that column isn't always 0 in the index function. So there has to be some way, to access other column in my model or am I totally wrong? How does the TreeView QML Type does this? – Fabian Pawlowski Feb 18 '17 at 09:30
  • Uhm...Can you check [this](http://stackoverflow.com/a/27655785/2538363) answer out? Maybe it can help. – BaCaRoZzo Feb 20 '17 at 20:59
  • This question is simply too long. I'd love to help with C++ models in QML, but please: try reducing the question to the minimal working example. – Jonas G. Drange Feb 20 '17 at 22:49
  • Hey, sorry I think I can't narrow it down. I tried another approach now, which worked for me. The short version how I made myself a timeline widget with QML is, that now, I'm mostly using only QML and throw away the model part in C++ (making my own QAbstractItemModel etc). Now, I got a QML Ui Form which translates roughly into a part which creates the Track Heads using a repeater and another part which uses a Repeater und creates Tracks using a new QML Ui Form for this. – Fabian Pawlowski Feb 24 '17 at 19:44
  • I simply use two ListModels now. One for the tracks and just one for the annotations. I now "paint" the annotations simply over the whole area where the tracks are painted and use the Drag of the MouseArea of each Annotation for controlling where you could drag and drop the annotations in the tracks. If someone needs a clearer explanation how I build this for inspiration or such, feel free to ask for that. I will than gonna try to explain it in more detail. – Fabian Pawlowski Feb 24 '17 at 19:45

1 Answers1

1

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.

Alex
  • 43
  • 5