PDA

View Full Version : How to structure a QML project



KeineAhnung
19th August 2014, 21:22
Hi,

currently I am trying to do my first QML project. I have some nice working code in several QML files that display different pages of my App. My next step was to get theses pages to talk to each other but I just do not understand how that is done. There are several code snippets on the net but I am always getting confused where to put what.
All I managed is to pass on some values using:

onClicked: pageStack.push(Qt.resolvedUrl("CountdownPage.qml"), {onTime: onSlider.value, reciverTime: recoverSlider.value, cycles: cycleSlider.value})
I am woking off the basic example provided in Sailfish SDK that looks like this. There is a main.qml

import QtQuick 2.0
import Sailfish.Silica 1.0
import "pages"

ApplicationWindow
{
id: main
initialPage: Component { FirstPage { } }
cover: Qt.resolvedUrl("cover/CoverPage.qml")
}
and within the folder pages are all the different pages of the project. If I want to use some variables that should be available in different pages I read that I could import a javascript file that holds these values. But a) I read that this is not a good idea and b) there is always a new instance of the file and if I change the values the other pages do net get the update. So how can I share values over my entire project?
Specifically I am interested in sharing a time value that is updated every second. Currently the timer is located in the CountdownPage.qml that is in the folder pages and looks like this:

Timer {
id: ticker
interval: 1000;
running: true;
repeat: true;
onTriggered: {
time--;

// Setting times
if(time < 0){
animateOpacity.stop()
timer.color = "white" // timer is the text field displaying the value
timer.opacity = 1.0
if(trainOn){
trainOn = false;
time = holdTime;
cycles--;
}else{
trainOn = true;
time = onTime
player.play() // player is a C++ class that I imported
}

if(cycles === 0){
ticker.running = false;
timer.text = qsTr("Finish!")
}
}

// Animations and Sounds
if(time < 6){
timer.color = "red"
animateOpacity.start()
}
if(!trainOn && time === 5)
soundCountdown.play()
if(trainOn && time === 0){
player.pause()
soundGong.play();
}
}
}
So it anteracts with other elements in that page. But I can change the page and go to cover while the ticker is still active and then I want to display the ticker time in my cover page. I guess it was not a good idea to have the ticker within the page but I do not know how to do it better. I managed to extend QML with a C++ class and use that in the CountdownPage but I also would like to be able to control the player instance(?) from all pages. So can some tell me how a QML project should be structured? What things should go in the QML file and what in c++ or js files and if it is in an external file in which qml file should I import it?

Thanks for reading through all of that =)

anda_skoa
20th August 2014, 08:32
There are several ways to achieve this, some having been added at later versions of Qt so not all might be applicable to you.

The most hackish but oldest option is to define such "global" objects with id in the main/first QML file. These ids are visible throughout the project.

Another option is to keep the state in JavaScript objects inside a JavaScript "library": http://qt-project.org/doc/qt-5/qtqml-javascript-resources.html#shared-javascript-resources-libraries

A better but newer option is to have a singleton type: http://qt-project.org/doc/qt-5/qtquick-demos-samegame-content-blackberry-settings-qml.html http://qt-project.org/doc/qt-5/qtquick-demos-samegame-content-qmldir.html

For applications with C++ core there is also the option of having the state in a C++ object and exporting that to QML using QQmlEngine::setContextProperty().
Or a combination, by using QQmlEngine::registerSingletonType().

My preference is on the last two, since it allows things such as reloading the UI without loosing any state.

Cheers,
_

KeineAhnung
20th August 2014, 21:52
Okay, thanks for the help. The reason why I got confused was that Qt Creator does not pick it up when I use the id.property. I have to type everything. Is this normal or should Creator autocomplete?

Moving my player to the main.qml enabled me to work with it from all pages. Doing the same with my ticker brought the entire project down. Is it possible to use the ticker as I programmed it through the entire project or do I need to set it differently?

Regarding the singelton approach for the global variables, is this here (http://qt-project.org/forums/viewthread/34680) correct? If so that is not really looking handy...

anda_skoa
21st August 2014, 09:31
Okay, thanks for the help. The reason why I got confused was that Qt Creator does not pick it up when I use the id.property. I have to type everything. Is this normal or should Creator autocomplete?

QtCreator can not assume a certain usage of a file, from its point of view it can only "see" what is in the current file scope.



Moving my player to the main.qml enabled me to work with it from all pages. Doing the same with my ticker brought the entire project down. Is it possible to use the ticker as I programmed it through the entire project or do I need to set it differently?

You'll have to debug what is actually wrong. The global id hack should work for the timer as well.



Regarding the singelton approach for the global variables, is this here (http://qt-project.org/forums/viewthread/34680) correct? If so that is not really looking handy...

Yes and no. That thread has several approaches, I guess you are referring to the one with registring in C++?
If you look at the links I've posted you can simply do that with a qmldir file.

Cheers,
_

wysota
21st August 2014, 10:35
I would suggest to reconsider having to have global data (be it a singleton or a "globally accessible object"), that's almost never a desired architecture. Maybe you can obtain the same effect by signal propagation or property aliasing.

KeineAhnung
21st August 2014, 20:12
Hi _,

actually I was referring to the thread where the link (http://qt-project.org/forums/viewthread/34680) pointed to. I just see that doing links is not so good, it is really hard to tell them apart from the rest of the text. Maybe the forum style sheets needs some improvement here?

@wysota,
Looking again at the property aliasing I think I figured that our now. In my main.qml it created a new Item called clock

Item {
id:clock

property int time: 6
property int trainingTime
property int holdTime
property int cycles
property bool active
property alias running: ticker.running

SoundEffect {
id: soundGong
source: "qrc:/sounds/gong.wav"
}
SoundEffect {
id: soundCountdown
source: "qrc:/sounds/countdown_male.wav"
}

NumberAnimation {
id: animateOpacity
target: countdownPage.display
property: "opacity";
from: 0.1
to: 1.0
duration: 1000
loops: Animation.Infinite
easing.type: Easing.OutSine
}

Timer {
id: ticker
interval: 1000
running: false
repeat: true

onTriggered: {
clock.time--
// console.log("Time: "+clock.time+" Cycle: "+clock.cycles)
if(clock.time < 0){
if(clock.active){
clock.active = false
clock.time = clock.holdTime
clock.cycles--
player.pause()
soundGong.play()
}else{
clock.active = true
clock.time = clock.trainingTime
player.play()
}
}
if(clock.time === 5)
animateOpacity.start()
if(clock.time === 5 && !clock.active)
soundCountdown.play()
if(clock.cycles === 0)
ticker.running = false
}


}
}

using clock.<property> anywhere in the project allows me to access properties of my clock. For example in my countdown page I can display the current clock time by using

Text {
id: display
anchors.centerIn: parent
text: clock.time
color: (clock.time < 6? "red" : "white")
}
Also the sounds work as planned. The only thing I did not get to work is the text animation. My plan was to create a signal within clock signal tick(clock.time) and "connect" it within the countdown page by using onTick: {} but this gives me the error : "Cannot assign to non-existent property "onTick"". My second thougt was to put it into the clock (see above) but that gives me the error: "ReferenceError: countdownPage is not defined".
So how can I send a signal from one file to another? All signal examples I found were only within one page. Or how else could I start and stop my text animation?

wysota
22nd August 2014, 08:13
The only thing I did not get to work is the text animation. My plan was to create a signal within clock signal tick(clock.time) and "connect" it within the countdown page by using onTick: {} but this gives me the error : "Cannot assign to non-existent property "onTick"".
Using timers for animations in QtQuick is not a proper approach. You have a bunch of types inheriting Animation to do animations.


So how can I send a signal from one file to another?
You don't send signals from somwhere to somewhere else. You just emit a signal and if anyone is interested in it, he can connect to that signal.


All signal examples I found were only within one page. Or how else could I start and stop my text animation?
The declarative approach would be to come up with a statement that states when the animation is running, e.g.:
NumberAnimation {
// ...
running: clock.cycles > 0
// ...
}

By the way, from what I see you can get rid of the whole ticker of yours and do everything you do there in a declarative manner instead of imperative statements. The hardest part in learning QML is to make yourself stop thinking imperatively and start thinking declaratively.

KeineAhnung
22nd August 2014, 09:21
Hi wysota,

first of all thanks a lot for your help. You put me on the right path!

By the way, from what I see you can get rid of the whole ticker of yours and do everything you do there in a declarative manner instead of imperative statements.
Is this also true if I want to us the property time on several pages? Say I have to labels in two qml files. Within the app the user shall be able to switch between the pages and always the the time.



You don't send signals from somewhere to somewhere else. You just emit a signal and if anyone is interested in it, he can connect to that signal.
This exactly what I did not manage. How do I connect to a signal? onSignalName in another qml file always gives me an error.

wysota
22nd August 2014, 09:48
Is this also true if I want to us the property time on several pages? Say I have to labels in two qml files. Within the app the user shall be able to switch between the pages and always the the time.
I'd start by explaining what are your intentions for having the "time" property in the first place. I think that the sole existance of this property is your main problem. Regardless if this property is physically global or not, you are treating it as one which in my opinion is a misdesign.


This exactly what I did not manage. How do I connect to a signal? onSignalName in another qml file always gives me an error.
There is a number of approaches, one of them being to use the Connections element.

Connections {
target: someOtherElement

onSignalHandler: { ... }
}

anda_skoa
22nd August 2014, 10:23
actually I was referring to the thread where the link (http://qt-project.org/forums/viewthread/34680) pointed to.

Yes, I got that. But there are several replies, the first one using an approach that is more complex than necessary. So I guess you were referring to that answer :)

Cheers,
_

KeineAhnung
22nd August 2014, 20:33
The app I am working on is a special stop watch and I want to display the elapsed time. This app shall be for my Jolla and a special feature on Sailfish is that your apps have an active cover when pushed to the home screen. I want to display the time on that "cover" as well.

Thanks for the help with the connections and the global variables!

wysota
24th August 2014, 07:45
The app I am working on is a special stop watch and I want to display the elapsed time. This app shall be for my Jolla and a special feature on Sailfish is that your apps have an active cover when pushed to the home screen. I want to display the time on that "cover" as well.

I don't see how this requires any global variables. If I really had to have such variable, especially one related to time, I'd probably implement it in C++ and expose it to the QML engine as a root object context property.

BTW. You don't need a Timer to show the current time (or elapsed time). And if you want to have a precise calculation of the elapsed time then using a Timer and increasing some time variable in its triggered slot is probably a good way to get invalid results. If you want precise results then simply store the start time and when you need to know how much time has elapsed, subtract current time from that start time. If you have pause functionality, do exactly the same -- note the pause start time and subtract it from pause end time. Such approach will make your app vulnerable to system clock changes but it will prevent situations when the timer doesn't fire exactly then you expect it to. And it wastes much less energy if you doesn't have to show the current elapsed value all the time.

KeineAhnung
25th August 2014, 22:12
I do not need any global variables thanks to your hints with binding the values. Indeed I already ran into the situation that the timer does not give me the correct time. When the system goes into sleep the timer slows down and one second become three or more.

The app shall show you how many seconds remain in for the current exercise or pause. So the time is displayed all the time. At least when the option prevent sleep is activated.

By the way, I do not get any notifications by mail anymore if a new post was posted even if I follow this thread (or any other). Is there a issue with the forum?