Tuesday, July 19, 2011

Populating SelectionDialog from C++ backend

I have what I would imagine a pretty common need to create a data-set in C++ backend, and use that data to populate the values that can be selected in QML's SelectionDialog.

Since SelectionDialog seems to follow the QML Data model pattern, and it has its own delegate defined that handles the data for the platform, I thought it would be as easy as this:

PageStackWindow {
    id: appWindow

    initialPage: mainPage

    MainPage{id: mainPage}

    ToolBarLayout {
        id: commonTools
        visible: true
        ToolIcon { platformIconId: "toolbar-view-menu";
             anchors.right: parent===undefined ? undefined : parent.right
             onClicked: (myMenu.status == DialogStatus.Closed) ? myMenu.open() : myMenu.close()
        }
    }

    Menu {
        id: myMenu
        visualParent: pageStack
        MenuLayout {
            MenuItem { text: "Sample menu item" }
        }
    }
}

MainPage.qml:

Page {
    id: mainPage
    tools: commonTools

    SelectionDialog {
        id: selectionDialog
        titleText: "Select item"

        model: selectionModel
    }

    Button {
        text: "Select"

        onClicked: selectionDialog.open()
    }
}

main.cpp:

#include <QtGui/QApplication>
#include <QtDeclarative>

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QStringList dataList;
    dataList.append("Item 1");
    dataList.append("Item 2");
    dataList.append("Item 3");
    dataList.append("Item 4");
    dataList.append("Item 5");

    QStringListModel dataListModel(dataList);

    QDeclarativeView view;
    QDeclarativeContext *ctxt = view.rootContext();
    ctxt->setContextProperty("selectionModel", &dataListModel);
    view.setSource(QUrl("qrc:/qml/main.qml"));
    view.showFullScreen();

    return app.exec();
}

In these code samples, only MainPage.qml and main.cpp are interesting. I create a list of items I want to show in C++, assign them to a model so that it can be shown in QML, register the model as id so that it's available in QML and then load and show the QML. When pressing the "Select" button, the items should be shown.

But they don't show, instead I got an empty list. And there is at least two separate reasons why the items are not shown:

  • Qt's models have "roles" which are used to access data in the model. SelectionDialog tries to display data from roles that are not set by QStringListModel. SelectionDialog uses data from role "name", but QStringListModel gives access to the data in the role "display".
  • SelectionDialog's model property does not accept just any model, it expects QML ListModel type. QStringListModel can not be assigned to the QML property, trying to assign it will give an error.

If you want more information and discussion on those issues, see a posting on MeeGo Forum.

These issues, especially the second one, may be temporary problems that will be fixed in future. But for now, I'll show the cleanest way that I could figure out to handle this.

For mixed C++/QML programs I think the best practice is to have the C++ part of the code to be common as much as possible across platforms, and the QML part will be what is customized for each platform as necessary. So the C++ code I represent here hopefully works in MeeGo/Harmattan as well as in Symbian. For the QML part I will only represent the Harmattan part, as that's what I'm interested in and that's what I can test.

In a nut shell, I create in QML an empty ListModel, and signal from C++ to QML side to add elements to the ListModel using JavaScript. In the C++ code I create a wrapper for QStringListModel that sets the displayed role to "name" so that Meego Harmattan and Symbian Qt Component delegates can use it. In Symbian platforms, there should be no need for the "Connections" block in the QML, as you should be able to just assign the "selectionModel" to SelectionDialog's model directly.

There is one more caveat on Meego 1.2 Harmattan API. You need to set SelectionDialog's selectedIndex property to some value. If you do not set the selected index, initially the dialog does not show the first item. So for now displaying a dialog with this code is not possible without having a default selection.

I'll represent the code here, included with comments on the parts that need explaining. If you've figured a cleaner way to do this please post in the comments.

main.cpp:

#include <QtGui/QApplication>
#include <QtDeclarative>

#include "qstringlistmodelforqtcomponent.h"

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QStringList dataList;
    QStringListModelForQtComponent dataListModel;

    dataList.append("Item 1");
    dataList.append("Item 2");
    dataList.append("Item 3");
    dataList.append("Item 4");
    dataList.append("Item 5");

    QDeclarativeView view;
    QDeclarativeContext *ctxt = view.rootContext();
    ctxt->setContextProperty("selectionModel", &dataListModel);
    view.setSource(QUrl("qrc:/qml/main.qml"));

    // The displayed elements must be set after the QML has been
    // loaded, as on MeeGo/Harmattan adding the elements to 
    // lists is done by emitting signals. The signal handlers 
    // are not instantiated before the QML has been loaded.
    // Doing it this way should also work on Symbian  
    // Qt Components and the stock ListView because QML models 
    // are updated on the fly if they are changed.
    dataListModel.setStringList(dataList);

    view.showFullScreen();

    dataListModel.setStringList(dataList);

    return app.exec();
}
qstringlistmodelforqtcomponent.h:
#ifndef Q_STRING_LIST_MODEL_FOR_QT_COMPONENT_H
#define Q_STRING_LIST_MODEL_FOR_QT_COMPONENT_H

#include <QObject>
#include <QStringListModel>

class QStringListModelForQtComponent : public QStringListModel
{
    Q_OBJECT

public:
    // Contrary to normal QStringListModel, this class does not
    // allow giving the strings in the model in constructor. 
    // The reason is that for this model to work also on 
    // MeeGo/Harmattan, the strings must be set after the
    // QML template has been loaded. Otherwise the signal 
    // handlers in QML have not been registered. The default 
    // role name used to display the roles is "name", which is 
    // used both by Symbian and Harmattan Qt Compontents.
    QStringListModelForQtComponent(const QByteArray &displayRoleName = "name", QObject *parent = 0);

    // Overrides QStringListModel's function. Setting this will
    // emit stringAdded(QString) for each string in the list. If
    // there was non empty string list set to this model before,
    // stringsReset() signal is emitted before 
    // stringAdded(QString) signals.
    void setStringList(const QStringList &strings);

signals:
    void stringAdded(const QString &newString);
    // Emitted when setStringList is called if already contained strings
    void stringsReset();
};

#endif // Q_STRING_LIST_MODEL_FOR_QT_COMPONENT_H

qstringlistmodelforqtcomponent.cpp:
#include "qstringlistmodelforqtcomponent.h"

QStringListModelForQtComponent::QStringListModelForQtComponent(const QByteArray &displayRoleName, QObject *parent)
    : QStringListModel(parent)
{
    QHash<int, QByteArray> roleNames;
    roleNames.insert(Qt::DisplayRole, displayRoleName);
    setRoleNames(roleNames);
}

void
QStringListModelForQtComponent::setStringList(const QStringList &strings)
{
    if (!stringList().isEmpty())
    {
        emit stringsReset();
    }
    QStringListModel::setStringList(strings);
    QStringList::const_iterator i = strings.constBegin();
    for (; i != strings.constEnd(); ++i) {
        QString s = *i;
        emit stringAdded(s);
    }
}
MainPage.qml:
import QtQuick 1.1
import com.meego 1.0

Page {
    id: mainPage
    tools: commonTools

    SelectionDialog {
        id: selectionDialog
        titleText: "Select item"
        selectedIndex: 0

        model: ListModel {
            // Populated from C++ backend
        }
    }

    Connections {
        target: selectionModel
        // Only onStringAdded signal is needed, since the C++ backend
        // needs only to initialize the strings once.
        onStringAdded: {
            selectionDialog.model.append({name: newString})
        }
    }
}

11 comments:

  1. Another option is to copy to your project "SelectionDialog.qml" (and "CommonDialog.qml" because it is not exported) as "MySelectionDialog.qml" and make this
    small change. After that any model with a "name" role can be used in a MySelectionDialog.

    Hopefully this change is ok, the merge request gets accepted and we get the fix in the next version!

    ReplyDelete
    Replies
    1. Just few remarks:

      1) I also had to import the "UIConstants.js" file some reason. My best bet is that that file is not exported either, just like CommonDialog.

      2) Import path altering inside the imported "MySelectionDialog.qml" and "CommonDialog.qml" files:
      import "." 1.0 -> import com.nokia.meego 1.0

      Although, it does not still work for me. I am now getting an empty list inside the selection dialog without any errors or warnings on the command line.

      1) Here is my derivative for the QStringListModel:
      https://projects.kde.org/projects/kde/kdeedu/kanagram/repository/revisions/master/entry/src/harmattan/selectionmodel.cpp

      It does not do anything more complex than just setting the "name" role for the display

      2) This is my main.cpp where I set the context property for this derivative instance:
      https://projects.kde.org/projects/kde/kdeedu/kanagram/repository/revisions/master/entry/src/harmattan/main.cpp#L43

      3) This is how I am now trying to invoke the "MySelectionDialog" and its model setting inside the MainPage.qml file of mine:
      https://projects.kde.org/projects/kde/kdeedu/kanagram/repository/revisions/master/entry/src/harmattan/MainPage.qml#L110

      If I try to make some debug session, like this:
      qDebug() << kanagramEngineHelper.anagramCategoryModel()->stringList();

      It shows the relevant string, but not in the selection dialog. :/

      Once, we get it work, it can be a good example for this workaround. :)

      Delete
    2. Laszlo,

      your problem is probably that you need to implement a count property to the model for it to be usable from SelectionDialog.

      You can see an example here:

      https://github.com/ajalkane/profilematic/blob/master/profilematic/src/qmlbackend/qmldaysmodel.h

      Delete
    3. This is the problem:

      https://qt.gitorious.org/qt-components/qt-components/merge_requests/1261

      The name and display role hackery, plus the QStringListModel derivative has not been needed since last September either after all:
      https://qt.gitorious.org/qt-components/qt-components/commit/cbeb20d68c9b792353e301363df9719f5a5e8f26

      Hope, it helps. :)

      Delete
  2. Thanks, that's a nice workaround!

    I hope to see this merged.

    ReplyDelete
  3. Great post. However there is a bug, if you comment the line where you set selectedIndex for selectionDialog then the first item won't appear in the list.

    ReplyDelete
  4. Yes, this problem with having to set selectedIndex is mentioned in the post. I have posted a bug report about it. I didn't find a nice work-around for that problem, so for now if using this one of the items must be pre-selected.

    Usually it's not a problem to design the dialog to have always some item selected, but if it is a problem you can use the method dz015 posted above. SelectionDialog should work correctly then even without selectedIndex.

    ReplyDelete
  5. Oh, indeed. I didn't notice, was too eager to find use of your solution :) But yeah, it's somewhat critical to me to be able to show the dialog with no selection, so I will try dz015's hack. Anyway, you saved me lot of time :)

    ReplyDelete
  6. Hi I can't get this working I keep getting missing argument begor roleNames, could you upload a project where you are already using this, please? Thanks!

    ReplyDelete
  7. This comment has been removed by the author.

    ReplyDelete
  8. Sorry, the HTML formatting has messed up the source. The line should be (I've fixed it now):

    QHash<int, QByteArray> roleNames;

    I don't have a ready source since I ended up needing a bit more complex with QAbstractListModel. This solution was just temporary for my project. For anything other than very simple things I'd recommend the approach given by dz015 in the first comment.

    ReplyDelete