-
Notifications
You must be signed in to change notification settings - Fork 79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
C++ Integration #9
Comments
Hi. Thanks for the suggestion. I agree that we should have something for the non-visual types as well. Although I mostly agree with what you said, I don't agree with this part:
Maybe If you clarify what you mean by The only reason I see where a singleton is useful is when the data needs to persist throughout the application life time. Not just between views. A very simple example, I can have a on-boarding process where the user name has to be shown on each page, but that doesn't warrant a singleton use. In this case, the data should be passed to the next view as necessary. |
In cases like that i sometimes use an attached property that behave like the Like that i can extend my data object without touching all the views on the way. Simple example i use in my app.
I defined an attached property But this approach require lots of boiler plate code to inject property to children. With good macro View implementation can look like that: class View : public QObject
{
Q_OBJECT
public:
View(QObject* parent = nullptr)
: QObject(parent)
{
}
QML_ATTACHED_OBJECT(View, ScreenUid, PageUid);
QML_ATTACHED_PROPERTY(int, screenUid, ScreenUid, 1);
QML_ATTACHED_PROPERTY(int, pageUid, PageUid, 1);
}; Ugly macro looks like that : #define QML_ATTACHED_OBJECT_DECORATION(Class) \
private: \
QML_ATTACHED(Class); \
\
public: \
static Class* qmlAttachedProperties(QObject* object) \
{ \
return new Class(object); \
} \
\
private:
#define QML_ATTACHED_OBJECT_BITSET(Class, ...) \
private: \
enum class AttachedInheritedProperties \
{ \
__VA_ARGS__, \
COUNT \
}; \
std::bitset<static_cast<std::size_t>(AttachedInheritedProperties::COUNT)> _ownedProperties; \
std::bitset<static_cast<std::size_t>(AttachedInheritedProperties::COUNT)> _initializeProperties; \
\
private:
#define QML_ATTACHED_OBJECT(Class, ...) \
QML_ATTACHED_OBJECT_DECORATION(Class) \
QML_ATTACHED_OBJECT_BITSET(Class, __VA_ARGS__)
#define QML_ATTACHED_PROPERTY(__type, attribute, Attribute, __def) \
protected: \
Q_PROPERTY(__type attribute READ attribute WRITE set##Attribute RESET reset##Attribute NOTIFY attribute##Changed) \
private: \
static constexpr __type default##Attribute() \
{ \
return __def; \
} \
__type _##attribute = default##Attribute(); \
\
public: \
__type attribute() \
{ \
if(!attribute##Owned() && !attribute##Initialized()) \
{ \
set##Attribute##Initialized(); \
internalSet##Attribute(parent() ? find##Attribute##InParents(parent()->parent()) : default##Attribute()); \
} \
return _##attribute; \
} \
void set##Attribute(__type value) \
{ \
_ownedProperties.set(static_cast<std::size_t>(AttachedInheritedProperties::Attribute)); \
set##Attribute##Initialized(); \
internalSet##Attribute(value); \
propagate##Attribute(); \
} \
void reset##Attribute() \
{ \
if(!_ownedProperties.test(static_cast<std::size_t>(AttachedInheritedProperties::Attribute))) \
return; \
\
_ownedProperties.reset(static_cast<std::size_t>(AttachedInheritedProperties::Attribute)); \
set##Attribute##Initialized(); \
internalSet##Attribute(parent() ? find##Attribute##InParents(parent()->parent()) : default##Attribute()); \
propagate##Attribute(); \
} \
\
Q_SIGNALS: \
void attribute##Changed(); \
\
private: \
void propagate##Attribute() const \
{ \
propagate##Attribute(qobject_cast<QQuickItem*>(parent())); \
} \
void propagate##Attribute(QQuickItem* item) const \
{ \
if(!item) \
return; \
\
for(auto* const child: item->childItems()) \
{ \
auto* const attached = qobject_cast<View*>(qmlAttachedPropertiesObject<View>(child, false)); \
if(attached) \
{ \
if(!attached->inherit##Attribute(_##attribute)) \
continue; \
} \
propagate##Attribute(child); \
} \
} \
bool inherit##Attribute(__type value) \
{ \
if(attribute##Owned()) \
return false; \
set##Attribute##Initialized(); \
internalSet##Attribute(value); \
return true; \
} \
__type find##Attribute##InParents(QObject* obj) const \
{ \
if(!obj) \
return default##Attribute(); \
\
auto* const attached = qobject_cast<View*>(qmlAttachedPropertiesObject<View>(obj, false)); \
if(attached && attached->attribute##Initialized()) \
return attached->_##attribute; \
\
return find##Attribute##InParents(obj->parent()); \
} \
void internalSet##Attribute(__type value) \
{ \
if(value != _##attribute) \
{ \
_##attribute = value; \
Q_EMIT attribute##Changed(); \
} \
} \
bool attribute##Owned() const \
{ \
return _ownedProperties.test(static_cast<std::size_t>(AttachedInheritedProperties::Attribute)); \
} \
bool attribute##Initialized() const \
{ \
return _initializeProperties.test(static_cast<std::size_t>(AttachedInheritedProperties::Attribute)); \
} \
void set##Attribute##Initialized() \
{ \
_initializeProperties.set(static_cast<std::size_t>(AttachedInheritedProperties::Attribute)); \
} Hope this can help someone. |
I agree that that part could be worded better. But I mean if your data needs to exist before your view is created or after it has been destroyed.
Another common case for me is when trying to select between two view delegates based on some property value. In that case I would usually use a And if I am setting up live reloading (which I think all QML developers should set up, at least until the Qt company offers us a better solution), then I need my application view to persist between reloads. (instead of having to navigate back to the page I was on)
Wouldn't you just use a property in that case? I agree if the above do not apply then you would just register as a regular type to be created by the QML engine. So to summarize I think these are the cases where you wouldn't need a singleton instance created from C++:
In my experience, following the above decision chart, the only non-singleton instance models I am left with are the helper classes that offload pure computation (without any data) to my C++ backend. |
@OlivierLDff I definitely agree that attached properties is a useful tool for this purpose. And there's not much information about it and its use cases. I think the snippet would prove useful when included in the right context. Let me know if you'd be willing to add something about this to the guide, otherwise I'll try to use the feedback from this issue to write something.
I wouldn't choose a singleton to persist that information. In the project I'm working on, we also have very similar cases where a state of a control (e.g a group is expanded/collapsed) is important to remember. But this information is persisted somewhere in the core with appropriate data structures. The next time the same window is opened and we are showing the same group, we get that in the context (not the QML context) of the window. We already provide data to the view about the window (e.g which controls are shown, what data is displayed), and part of that data is that expanded/collapsed state.
My bad, it was meant to be a very simple example but its simplicity killed its effectiveness at trying to prove my point. You are right, in that case I would just use a property. And that is my point. No matter how complex the data gets, I would not choose to provide a singleton just because I need some data to persist between different views.
I would argue for the opposite of that where singletons are used almost always for helper functions, and non-singleton instances are used for data. The only exception that I see to this is when the data needs to persist throughout the application life time, and not just between the views. The more singletons that you have, the harder it would be to reason about data and ownership. I find that QML has the most flexibility and easiest to reason with (especially in very large projects) If they are individual isolated components that don't rely on any global state. This doesn't say anything about how you choose to store that data that you want available in multiple views. That data could be a singleton in the C++ side, but it doesn't necessarily mean that it has to be a singleton in the QML side. When data is injected using instance models, or by passing as a property, the ease of mocking would be much better than when a singleton is provided. For example, a designer can do their prototyping with QML scene with minimal access to the C++ data model if they just provide a mock data themselves. In your example with the database connection, this connection can be established in an instance-object as well. And it'd be easier to reason about the life of the connection. Or these instance objects can internally connect to an internal C++ singleton to share a connection and close it if no clients are using it. Although I'm not arguing against the use of singletons in the C++ side, I'm not that keen on advocating it in the QML side. As far as choosing an integration type goes, Qt's documentation provides a pretty good char. I think it'd be useful to have concrete examples of this in the guide with different use cases so that people can get a better understanding. It'd be great if you are willing to provide a good example of your use case instead of making a generalized advice for choosing singletons. Maybe it was not your intention but your examples and your chart made me think If I follow it, I'll end up with a lot of singleton in my project. |
@Furkanzmc For attached properties i think the context of screen id, or page id can be used. Or the context of theme like Material does. To propagate a theme. I want to add my use case about singleton, I feel that i'm using lots of singleton too.
And then in my qml folder i have something like:
Everything in Controls shouldn't use If a designer want to moc the Of course this is overly simplified example. My design & development goal using such an architecture is to make qml component independant, without wiring between them. |
This also addresses the discussions in issue #9.
This also addresses the discussions in issue #9. Next is to write a section on attached properties.
This also addresses the discussions in issue #9. Next is to write a section on attached properties.
Hello again @Furkanzmc and @OlivierLDff. Firstly apologies for my late reply. Edit: Sorry this comment turned out to be long. I have highlighted what I think are the main points of disagreement.
How would your model last then? The QML engine decides when to create and destroy the model when it is not registered as a singleton. I am assuming you use C++ singletons? (see my discussion below)
Ok I see what you mean. I should have emphasized that sharing data between views is not the main reason to use singleton. Though sometimes passing data through many elements between two components that are visually far apart but are logically close means all the components in between become less reusable since now they are polluted with this data being passed around. But I agree with you that it is not a very good reason since it adds global state. The main point I was trying to make in that paragraph was to have your data persists when you navigate away from a dynamically created view, and then navigate back to the same view. The view is destroyed and recreated. Whereas you might want your model's lifetime to be persistent.
Sorry I should have clarified. I am not saying QML singletons are not good for that, they are. I was just saying that with the other use cases that need model life-time be different than view life-time, I cannot avoid using a singleton. Whereas with the helper functions, since I don't have any persistent data, I can have the view create and destroy the model and drive model's lifetime.
I would argue against this for 2 reasons. One that it adds an extra level of indirection, your QML creates some C++ accessor object that uses another singleton backend. And the more important reason. It means you have to add a singleton on the C++ side, which I would argue is more evil than a QML singleton instance. I know a QML singleton instance's lifetime, and I know the QML engine's lifetime, so I can ensure the instance is created before the engine and is destroyed after the engine is destroyed. The issue with C++ singletons is that they since they are generally created using static variables they might be created before
This is something I have not considered as we do not yet unit test our Qt Quick types. But I am wondering, would this be an issue if the user also follow the guideline mentioned above to not share the singleton model between views, and use it only in the view that needs it and pass the appropriate data to other views that need it from that main view like how you describe for non-singleton types?
Would do you mean by "instance-object"? If you are talking about a type registered as non-singleton then I don't follow how that is possible. Let's say you need to read a port number from some configurations file. How would you then pass this port number to QML and how would QML pass this to the constructor of the model? You could use another model for the configurations, and then pass those as properties to the model and have it inherit from
I agree that is a good chart. But it does not address the cases mentioned in my post, namely when lifetime of your model is different than your view, or when your view cannot instantiate your model because you need some initialization (e.g. passing a config value to the constructor).
That would be great. Maybe I am missing something about how I should be doing the integrations.
My main use cases:
That is actually why I am very interested in this discussion and am wondering why the Qt documentation does not emphasize this. I have been wondering if I am missing something and have talked with different people but have received mixed feedback. I get the feeling that the difference between my approach and yours specifically is that you have C++ singletons and I prefer to have QML "singleton instances" (which are not singletons on C++ side) for the above mentioned reasons. I do not see any other option beyond these two approaches that would address the above object lifetime / construction limitations. |
It's my bad that I kept saying singletons. The data is owned by the C++ side, and can be persisted
I agree with this, and we are doing this in our project. I also understand this is the way to go
In our project, we use a modified version of the redux architecture. So, it's very easy for us to Another reason that I avoid singletons is because we share our code base with designers. In our
I also avoid singletons in C++ side, but since the conversation was revolving around it I kept
I think this might just be the only acceptable type of singleton usage in QML. There would be one
Our docking implementation works in a similar way. If you look into our main window code, you'd only // A bunch of other code
// ..
// MainDockController is a QQuickItem type in the C++ side.
MainDockController {
model: DockControllerModel {}
} When During initialization, we read a JSON file to get all the state data. This is when a routine reads
MainDockController {
model: DockControllerModel {
DummyData { }
DummyData { }
}
} This way, QML is not exposed any singletons, and we have the freedom to persist data however we want
I think these could be addressed using the strategies I mentioned above except the third one. This |
Very interesting article. I am currently adding dynamic function to QObject in QJSEngine, and it is interesting to see how you handled variadic functions. I want to share with you how i did myself. QJSEngine *engine = qjsEngine(this);
const auto jsValue = engine->newQObject(this);
const QString f = "myFunction";
const auto script = QString("(function (...args) { this.__callFunction('%1', args) })").arg(f);
const auto jsFunction = engine->evaluate(script);
jsValue.setProperty(f, jsFunction); And the To come back to the subject, what do you think of the approach taken by quickflux? Button {
text: `increment counter ${Store.counterName}`
onClicked: () => Actions.incrementCounter(Store.counterName)
} In your implementation how to you handle api call that modify your model?
Cons of this approach, is when doing hot reload, if you change a singleton, then you need to restart the app. |
Thanks for sharing.
I think the use of actions as a singleton is OK since it only exposes functions and not data. I think flux/redux architecture is powerful, and we are using something similar (but not strictly the same) design. I haven't used quickflux before. I just took a brief look at it and the examples. Looks to me that quickflux approach is similar to what you and daravi doing as well. I think a similar approach is fine as long as the data is passed down from the Window {
width: 480
height: 640
visible: true
ColumnLayout {
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
// ------------
TodoList {
model: VisualModel {
model: MainStore.todo.model
}
Layout.fillWidth: true
Layout.fillHeight: true
}
// ------------
} Good thing is, this and the approach used in the example are not actually significantly different. It's just a simple refactoring effort as long as your code base is small or well structured. With this version, there can now be another |
I have 2 question about your approach: I see your goal is to inject model/data from only one file right? In more complexe case, would you recommend using strong property type to pass property thru files? // ...
TodoList {
todoModel: MainStore.todo.model
}
// ... and then in import "../stores" as Stores
Item {
property Store.TodoStore todoModel
// ...
} or Item {
property var todoModel
// ...
} Second question, is for hot reloading, if I only want to open the // TodoListTest.qml
TodoList {
todoModel: MainStore.todo.model
} |
Exactly. I find this approach to work better and provide more flexibility.
I always prefer the static type approach, but in this particular case it might be weird because if you want to provide another model to this one, that also has to inherit from
No view is useful to view on its own, you always need to provide a model to it anyways. So, personally, I don't see that as a problem. Your approach with |
So what type would you use? |
I would choose |
To bring up the subject about the use of So I think I think it can also be a good fit in ApplicationWindow
{
// For mock purpose we can do it that way and overwrite the model with our own
MyAttachedData.paletteColors = PaletteColorsModel {
testData: "#123456"
}
// Or in production, let's say your data is coming from a cpp color model palette that gets loaded from a database or anything you need
MyAttachedData.paletteColors = MyCppPaletteColorsModel {
}
// It can also work if you c++ model is a singleton
MyAttachedData.paletteColors = MyCppSingletonPaletteColorsModel
// Or a context property, which you should avoid
MyAttachedData.paletteColors = myCppContextPaletteColorsModel
// Usage
Rectangle {
anchors.fill: parent
color: MyAttachedData.paletteColors.testData
}
} I didn't find much talk or discussion about attached properties, and yet I feel it's a really nice way to fix all the problems and it feel very qmlish. Have you used any attached properties in production yet? Btw really nice talk: https://www.youtube.com/watch?v=yYCYPcbgXSs Have a nice day. |
You are right. I think attached properties are a more convenient way of extending a type or providing common data. Although, I wouldn't want to use it for API access. I think attached properties are great for data access and customization. Calling functions on them doesn't feel right.
You are right. I'll put something together for this. Thank you for the feedback!
Thank you! |
Hello. One point that I see missing from both the official Qt documentation and this guide is distinguishing C++ model types and C++ visual types. I think most users use the visual types provided by QtQuick or Qt Design Studio instead of creating their own visual components. And they use C++ for model / data backend. So it is important to emphasize what is the most natural integration type for this use case.
Usually data needs to be persistent and not be created / destroyed when you navigate between different views. Furthermore you sometimes need to initialize the object in some specific way. For example pass a handle to a data base or to an IPC (inter-process communication) object to it. This means that the only integration method that works in this situation is using
qmlRegisterSingletonInstance
. (I don't think lifetime concerns is a problem since these days everyone is familiar withunique_ptr
, whereas if you need to initialize your object in some way or pass some specific arguments to it, having the QML engine create it is either impossible or very hard with no benefits). In summary, if you really want your model to be independent of your view, you want to also make the lifetime independent.It seems to me that the guideline should be presented in this way:
qmlRegisterSingletonInstance
and keep your instance as aunique_ptr
or parent it to the engine. If you want to duplicate your data for each view that uses the model, or if your model does not need to be persistent between views then useqmlRegisterType
qmlRegisterType
The reason I think this should be emphasized is that all Qt documentation emphasize the integration methods that create the object from QML. This is not practical for more than 90% of models in my experience due to the above limitations and leads to wasted time trying to fit a square peg in a round hole.
The text was updated successfully, but these errors were encountered: