Delegate creation and multithreaded property updating
-
Yes, i just checked. (it appears more often, due to unrelated changes)
The lock is needed to safely access the member on the C++ side, not to protect between C++ and QML.
Yes, that's a bit ugly, but i am aware of that and there is always one shared pointer left holding the reference.
And what alternatives do i have? I think this is a general problem with shared pointers and qml. -
@felsi The alternative is to provide a stable pointer (the Q_PROPERTY is then a CONSTANT and cannot change), and change the content of the object.
The object itself has Q_PROPERTY entries in turn for all the single properties that can change (and some of them could - again - be stable pointers) -
But that was a side note.
Is your problem with Track 3 still open, or does it work now? -
I didn't see whether your setter for Track 3 was ever called. Your logging does not contain that information.
-
It looks like this:
onPlaylistTrackChanged 1
updateTrack 1 : NULL
onCompleted 1
onPlaylistTrackChanged 2
updateTrack 2 : NULL
onCompleted 2
onPlaylistTrackChanged 3
updateTrack 3 : NULL
setTrack 1
onTrackChanged 1
updateTrack 1 : Track 1
setTrack 2
onTrackChanged 2
updateTrack 2 : Track 2
setTrack 3
setTrack 4
setTrack 5
onCompleted 3
onPlaylistTrackChanged 4
updateTrack 4 : Track 4
onCompleted 4
onPlaylistTrackChanged 5
updateTrack 5 : Track 5
onCompleted 5 -
I would really try this:
class Track : public QObject { Q_OBJECT Q_PROPERTY(QString labeling READ getLabeling NOTIFY notifyLabelingChanged) // I would prefer BINDABLE, it's much simpler to code, but not relevant to the issue at hand Q_PROPERTY(QString duration_string READ getDurationString NOTIFY notifyDurationStringChanged) public: // Add the obvious getters and setters private: QString m_Labeling{"loading..."}; QString m_DurationString{"00:00"}; }; class PlaylistTrack : public QObject { Q_OBJECT Q_PROPERTY(Track* track READ getTrackPtr CONSTANT) // I would add FINAL, but not really relevant public: Track* getTrack() {return &m_Track;} private: Track m_Track; // If you want PIMPL, I'd use std::unique_ptr<Track> and create it in the constrcutor };
The track will be created with PlaylistTrack , and will live as long as PlaylistTrack does.
Initially, it holds the default strings you previously specified in your QML.
Once you have data, you simply change the properties, and QML should update.
There are some variants to this pattern, but the important thing is that object points stay where they are for as long as the QML scene exists. This saves you from a lot of tricky edge cases and extra checks on the QML side. -
Tracks are widely spread across my whole program. My collection points to them, sorted trees of my collection point to them and different PlaylistTracks in different playlists can point to the same Track. They can even get replaced program-wide.
They are created by the collection and are only looked up in a hash table. All for low memory, fast loading, etc.
I can live with my little qml workaround, i encountered no other problems.
I posted more, because maybe there is a bug, when using connections in delegates. -
I admit I don't fully understand the issue you have myself. However, eliminating the state change from "null" to "something" in QML can't hurt. If you want to manage the tracks in a more centralized way, pass the shared_ptr as part of the constructor of PlaylistTrack and never change it during the lifetime of PlaylistTrack. Make the Q_PROPERTY CONSTANT. From the perspective of QML, that should be the same as my suggestion.
-
Thank you very much, i also prefer the safe way, if possible. :)
And your suggestion is clean and safe. But i really need to be able to set and change the track for a variety of reasons.
In fact, it also appears when i change the track.And for the missed signal problem, it should not matter whether it's an address or a value, it's not even a signal-parameter.
To me, it looks like, that the delegate gets created in a separate thread, otherwise "updateTrack 2" couldn't appear during creation of track 3. But i don't have a clue about the Qt source code, so i can't look that up easily...
And my guess would further be, that signals emitted after the creation has started and before the connection is established, are simply lost. -
To the best of my knowledge, all the scene setup and JavaScript runs in the main thread. Only parts of rendering may run in a different thread, but they would not change your properties.
Is the "updateTrack" in the Button's onCompleted still commented out? Because your logging shows that track 3 gets the onCompleted after some properties have already been set, so I would not expect any signals to arrive anymore. -
Fortunately, the problem appears every few runs...
By the way, there is of course no problem, when all are set before or after the delegate creations.
And also not everytime when in between.
After a few checks, i would say, the workarounds behave like expected.
For completeness:With connectTrack() hackmack:
onPlaylistTrackChanged 1
connectTrack 1
updateTrack 1 : NULL
onCompleted 1
onPlaylistTrackChanged 2
connectTrack 2
updateTrack 2 : NULL
onCompleted 2
onPlaylistTrackChanged 3
connectTrack 3
updateTrack 3 : NULL
setTrack 1
onTrackChanged 1
updateTrack 1 : Track 1
setTrack 2
onTrackChanged 2
updateTrack 2 : Track 2
setTrack 3
onTrackChanged 3
updateTrack 3 : Track 3
setTrack 4
setTrack 5
onCompleted 3
onPlaylistTrackChanged 4
connectTrack 4
updateTrack 4 : Track 4
onCompleted 4
onPlaylistTrackChanged 5
connectTrack 5
updateTrack 5 : Track 5
onCompleted 5With updateTrack() in onCompleted:
onPlaylistTrackChanged 1
updateTrack 1 : NULL
onCompleted 1
updateTrack 1 : NULL
onPlaylistTrackChanged 2
updateTrack 2 : NULL
onCompleted 2
updateTrack 2 : NULL
onPlaylistTrackChanged 3
updateTrack 3 : NULL
setTrack 1
onTrackChanged 1
updateTrack 1 : Track 1
setTrack 2
onTrackChanged 2
updateTrack 2 : Track 2
setTrack 3
setTrack 4
setTrack 5
onCompleted 3
updateTrack 3 : Track 3
onPlaylistTrackChanged 4
updateTrack 4 : Track 4
onCompleted 4
updateTrack 4 : Track 4
onPlaylistTrackChanged 5
updateTrack 5 : Track 5
onCompleted 5
updateTrack 5 : Track 5 -
I wouldn't call it a workaround. The order of creation and update is not deterministic, so you have to take into account that components are created before or after you have already set some properties. For example, are some items visible and some not? This could trigger a kind of "priority of initialization" heuristic.
-
Yeah, but i wouldn't call it a patch either... ;)
Yes, i know, that's the reason, why i force the connection to be established before i update.
As far as i can see, that ensures, that there is at least one correct update.
And i have no other problems, everything's updating fine, if it does.
Without an explanation of the root cause i have no better solution...I am very thankful for your time and effort to help me.
But honestly, if it's not a Qt bug, i will move on and live with the obscurity... -
QML scene are often loaded asynchronously. That's just necessary to prevent everything from blocking as you load a large scene.
When do you start calling the setters for Track? I don't believe I have seen your code that controls it.
Basically, if you don't wait for your QQuickView / QQuickWidget to signal it's status "Ready", you cannot depend that all components exist. Even after that, if you are using Loader or dynamic instantiation, you could be surprised. So the most robust code will not depend on the order of events. Component created first? Properties changed first? You should not (need to) care. -
Uh, it is actually part of a larger element, which i load asynchronously with a loader! And it appears exactly then, at start-up.
I don't know, how and what i could show you. I have a PlaylistReader, running in a thread, connected to the setter, running in main thread. It loads the current playlist at start-up.
I absolutely see your point, it's damn ugly imperative...If i disable asynchronously loading, setting and creating are not simultaneous anyway...
Uff, this opens up a lot of possible issues, damn.Thank you very, very much!!
I wish you an extra sunny day! -
You are very welcome.
If you like, I can share my approach to cope with these issues. -
My concept is this:
- A QML component and C++ property class (i.e. the class with the necessary Q_PROPERTYs) always come in pairs
- The property class instance ALWAYS exists before the QML component is created. It is passed as a "required property"
- Complex sub-components come with their own property class, so...
C++:
class CInnerProperties : public QObject { // Some properties }; class COuterProperties : public QObject { // Some properties Q_PROPERTY(CInnerProperties* innerProperties [...]) };
QML (Inner component)
MyInnerQmlComponent { required property CInnerProperties innerData //... }
QML (Outer component)
MyOuterQmlComponent { required property COuterProperties outerData //... MyInnerQmlComponent { innerData: outerData.innerProperties } }
- Instantiation is controlled from the C++ side, by creating a QQmlComponent, and then calling createWithInitialProperties (passing the necessary property class instance to satisfy the "required" property
- Pointer properties are almost always CONST. I change their content, not the pointer.
- Data is passed by value, by copy. To improve performance and reduce memory usage, I make extensive use of implicit sharing.
- Data is sent between threads via signals / slots
This approach has the following benefits:
- Since the property class is guaranteed to exist when the component is instantiated, I rarely have need to manually connect any signals. I can mostly rely on property bindings, which automatically work
- Since the property bindings are automatically there, it does not matter whether the properties are already initialized with their correct values when the QML component is created, or whether we start with dummy values, and update the properties
- By-value data management allows multiple threads to produce data, and the GUI thread to consume it. There is no locking necessary (besides the builtin atomic ref counts of implicit sharing). This also allows easy tracing of data changes.