PDA

View Full Version : Load a portion of UI from a plugin QML (dynamic GUI) and expose C++ models to it



gmanish
1st June 2014, 19:26
So, I'm trying to develop a plugin model for my app. Idea is simple:


Have an interface that all plugins implement. I load/discover plugins at runtime.
My host application would have dedicated areas in the UI, within which, these plugins would display their UI.


The excellent Plug & Paint tutorial (http://doc.qt.digia.com/4.6/tools-plugandpaint.html) covers step (1) above pretty well. I have created an interface, developed an example plugin and the host is able to discover, load and invoke methods in them at runtime.

For step (2), I have figured out how to have the plugin's UI (described via. QML) appear in the parents user interface (also described via. QML). I can use a QML Loader (http://qt-project.org/doc/qt-4.8/qml-loader.html) component in the main application QML, to load an external QML file into specific areas of the host application UI by setting the Loader.source at runtime. My main.qml looks like this:



Rectangle {
Item {
width: card_1_1.width
height: card_1_1.height
Loader {
id: loader
source: <will be set dynamically>
}
}


However, I do not know how to make the plugin's C++ model available to the plugin's QML.

In a normal non-plugin scenario, I would make my model available to QML using


QQuickView.rootContext()->setContextProperty(...)

I do not know how to do this when the model I wish to expose will be discovered by the main app at runtime. My plugin interface looks like this:



class IMediaSource : public QObject
{
Q_OBJECT

public:
virtual ~IMediaSource() {}
virtual QObject* getModel() = 0;
virtual QString getQMLPath() = 0;
};


I wish to expose the QObject* returned by IMediaSource::getModel() to the plugin's QML which is returned by IMediaSource::getQMLPath().

My main app QML is loaded like so:



QGuiApplication app(argc, argv);
QQuickView viewer;

viewer.setSource(QUrl("qrc:///..."));
viewer.showExpanded();
return app.exec();


What I have tried:
I've tried to use the following on the Loader elements' instance:



QQmlContext *context = mEngine->contextForObject(item); // item is an instance of Loader I'm interested in
if (context != NULL)
{
context->setContextProperty("cardPlugin", this);
context->setContextObject(this);
}


Both methods return:



QQmlContext: Cannot set property on internal context.
QQmlContext: Cannot set context object for internal context.


Workaround:
One solution would be to iterate over all plugins and use QQuickView.rootContext()->setContextProperty("xxx", IMediaSource->getModel()) to pass on the models to the application QML's root context so they're available to all QML's loaded via. Loader.
This apart from being clumsy also has the problem of name collisions. Two plugins could expose their models using the same name.

I'm looking for a more elegant solution. Ideas are welcome.

anda_skoa
2nd June 2014, 09:13
You could register a QML singleton type using a plugin specific namespace and then let the QML code "instantiate" the model.

Cheers,
_

gmanish
2nd June 2014, 13:30
Thanks for your time on this. Do you mean using qmlRegisterType<X>("com.mycompany.X", 1, 0, "X")?

gmanish
2nd June 2014, 18:40
You could register a QML singleton type using a plugin specific namespace and then let the QML code "instantiate" the model.

Cheers,
_
Thanks for your time on this. Do you mean using qmlRegisterType<X>("com.mycompany.X", 1, 0, "X")?

anda_skoa
2nd June 2014, 19:31
I meant qmlRegisterSingletonType()

However, there might be another option that is closer to what you tried.
I assume you are using QQmlComponent to create the UI. You can pass a context to its create method.
E.g. something like this (not tested)


QQmlContext *pluginContext = new QQmlContext(view.engine());
pluginContext->setContextProperty("model", plugin->getModel());

QQmlComponent component(view.engine(), plugin->getQMLPath());
component.create(pluginContext);


Cheers,
_

gmanish
3rd June 2014, 21:08
I meant qmlRegisterSingletonType()
However, there might be another option that is closer to what you tried.
I assume you are using QQmlComponent to create the UI. You can pass a context to its create method.
_

I'm using the following to create the main UI (i.e. loading main.qml). It is in this view that I want to load the plugins QML components:



QGuiApplication app(argc, argv);
QQuickView viewer;

viewer.setSource(QUrl("qrc:///..."));
viewer.showExpanded();
return app.exec();


Also, I found this thread (http://qt-project.org/forums/viewthread/36542) that seems pertinent to my problem. However, I cannot fully comprehend it. Specifically, what are window1, window2 & window3 in the given solution.

How do I create my main UI using QQmlComponent and later add the QQmlComponent created using the method you specify to the main UI's QQmlComponent? I found a possible implementation here (http://www.ics.com/blog/integrating-c-qml#.U44ftZSSwhP). But when I run it, I get a crash because:


QQuickWindow *window = qobject_cast<QQuickWindow *>(topLevel);

evaluates to window = NULL

anda_skoa
5th June 2014, 19:56
QQuickWindow *window = qobject_cast<QQuickWindow *>(topLevel);

evaluates to window = NULL

Is your top level item a Window element? If not, cast it to whatever you are using, e.g. QQuickItem

Cheers,
_

gmanish
16th June 2014, 06:26
Mostly for future visitors, I did manage to get this working. Following is a tiny code fragment that highlights the solution:



QQmlContext* pluginContext = new QQmlContext(mView->engine());
QmlComponent* component(new QQmlComponent(mView->engine(), "/PATH/TO/QML.qml"));
QObject* compRoot = NULL;

pluginContext->setContextProperty("model", myModelObject);
compRoot = component->create(pluginContext);
if (component->isReady()) {
QQuickItem* item = qobject_cast<QQuickItem*>(compRoot);
QPointF point(posHelper.getLeft(), posHelper.getTop());
QSizeF size(posHelper.getWidth(), posHelper.getHeight());

item->setParentItem(mRoot); // The item pointer obtained through QQuickView.rootObject
item->setPosition(point);
item->setSize(size);

qDebug() << "Card (" << posHelper.getCurRow() << ", " << posHelper.getCurCol() << "): " <<
"(" << point.x() << ", " << point.y() << ") - " <<
"(" << size.width() << ", " << size.height() << ")";
return component;
} else {
qWarning() << "Failed to create component from C++:" << component->errorString();
}
}

The thing that did the trick was:


item->setParentItem(mRoot)