I am using QT v5.12.6 and in my application i am planning to use Progress Bar during application bootup for around 15secs. In this 15secs operations like:
QML components creation.
connection with the server.
Other bootup operations will be running in the background.
Once, all the bootup operation is done i will hide my splash screen which is just a Rectangle which includes Loading text with a progress bar. For this 15 secs i will be incrementing the progress bar but the progress bar doesn't increment/move for some 10 secs. It looks like it is hanged, but if i use a busy indicator it starts to rotate. Unfortunately, i can't use busy indicator component as i need a look and feel like a progress bar.
My application runs on a embedded platform which has low end processor and very less RAM speed. I am assuming this problem is due to the Load on the UI as many components is getting created.
Is there any difference between busy indicator and Progress bar and also any suggestions on how to handle UI load on the bootup ?
Edit 1: Added an example. I have tried my level best to mimic the problem. In this example both the busyindicator and Progress bar is getting stuck for sometime. But in the embedded device Busy indicator works but no idea how. After running the application Please click on Click Me button.
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.3
Window {
property int pbValue:0
visible: true
width: 500
height: 400
title: qsTr("Hello World")
Rectangle{
id: mySplashScreen
anchors.fill: parent
ProgressBar{
id: pBar
height: 20
width: parent.width
anchors.bottom: parent.bottom
from:0
value : pbValue
to:30
}
BusyIndicator{
anchors.right: parent.right
running: (pbValue < pBar.to +1)
}
Button{
text: "click me"
onClicked: {
//Create component equivalent to my application which has "n"
//number of components like buttons, combobox, chart view etc.
//Here just creating the rectangle more number of times.
for(var i = 0 ; i < 15000 ;i++) //HINT : Increase/decrease the value if problem is not seen
{
var comp = mycomp.createObject(mySplashScreen)
}
}
}
}
Timer{
id:timer
interval: 250
running: (pbValue < pBar.to +1)
onTriggered: {
pbValue += 1;
}
}
Component{
id:mycomp
Rectangle{
width: 200
height: 200
color: "green"
}
}
}
Move your object creation code into a separate Timer with a small interval, and rather than creating all of the objects at once, create them maybe 50 at a time every 25 MS or something.
This will allow for the main event loop to process other things like the animations for busy indicator while its loading.
Here's one way to go about implementing this
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.3
Window {
property int pbValue: 0
visible: true
width: 500
height: 400
title: qsTr("Hello World")
Rectangle {
id: mySplashScreen
anchors.fill: parent
ProgressBar {
// changed handling of progress bar
id: pBar
height: 20
width: parent.width
anchors.bottom: parent.bottom
from: 0
value: compList.length // bind this value
to: 15000
}
BusyIndicator {
anchors.right: parent.right
running: (pbValue < pBar.to + 1)
}
Button {
text: "click me"
onClicked: {
timer.running = true
}
}
}
property var compList: [] // created property to store all created components to track what has been done
Timer {
id: timer
interval: 25
running: false
repeat: true
onTriggered: {
for (var i = 0; i < 50; i++) {
var comp = mycomp.createObject(mySplashScreen) // moved component into timer
compList.push(comp) // added component to huge list of components for tracking
}
pbValue = compList.length
if ((pbValue >= 15000)) {
timer.running = false
console.log("All components completed")
}
}
}
Component {
id: mycomp
Rectangle {
width: 200
height: 200
color: "green"
}
}
}
Related
I am attempting to create a qml button object that displays a screen for the duration of a function's runtime. I plan to use this loading screen when I need to parse through a larger dataset/run a slower function. Currently this is what I have come up with.
//LoadingButton.qml
import QtQuick 2.4
import QtQuick.Controls 1.2
Item
{
id: impl
function callbackFunction() { console.log("This is a dummy funciton and needs to be overwritten in the implementation") } //empty dummy function
property alias style: button.style
Button {
id: button
anchors.fill: parent
onClicked: {
loadingScreen.visible = true;
console.log("Loading should be visible")
impl.callbackFunction();
loadingScreen.visible = false;
console.log("Loading should be hidden")
}
}
Rectangle
{
width: 500
height: 500
x:0
y:0
z: 60
id: loadingScreen
color: "red"
visible: false
}
}
This example runs the callbackFunction once overwritten in the parent object correctly, but the visibility of the Rectangle does not change until the slower function is completed. Also the application freezes until it finishes.
Is there any way to force the Rectangle to show/hide mid-javascript function execution?
the best solution is of course to move your slow function to a background thread. That way the GUI stays responsive.
If you want to keep the callbackFunction in same thread as the GUI, you can use a Timer that will delay the start of the slow function until the loading screen is shown. Please note that the GUI will be blocked during the execution of the slow function.
import QtQuick 2.4
import QtQuick.Controls 1.2
Item
{
id: impl
function callbackFunction() {
console.log("This is a dummy funciton and needs to be overwritten in the implementation")
var cnt = 0
var largeNumber = 1
while (cnt < 99999999) {
largeNumber += largeNumber/3
cnt++
}
//put this at the end of your slow function
loadingScreen.visible = false;
console.log("Loading should be hidden")
}
property alias style: button.style
Button {
id: button
anchors.fill: parent
onClicked: {
loadingScreen.visible = true;
console.log("Loading should be visible")
timer.start()
}
}
Timer {
id: timer
interval: 500
repeat: false
onTriggered: impl.callbackFunction()
}
Rectangle
{
id: loadingScreen
width: 500
height: 500
x:0
y:0
z: 60
color: "red"
visible: false
BusyIndicator {
anchors.centerIn: parent
running: loadingScreen.visible
}
}
}
I have a qml application which performs a rather long action upon a users request. During the time, I want to display an overlay over the whole screen, so the user is aware that the application is working, basically a busy indicator.
My Problem is, that the application starts with the task, before updating the UI component. Here's a minimal example to demonstrate the problem:
import QtQuick 2.9
import QtQuick.Window 2.3
Window {
visible: true
width: 640
height: 480
title: qsTr("Ui Demo")
Rectangle {
id: rectangle
anchors.fill: parent
color: "green"
}
MouseArea {
id: action
anchors.fill: parent
onClicked: {
rectangle.color = "red"
for(var i = 0; i < 10000; i++)
console.log(i)
}
}
}
What I want is, that the Rectangles color turns red while the for loop is running, but the behavior I see is that the color changes only after the loop has finished.
I also tried the following with no difference:
Rectangle {
id: rectangle
anchors.fill: parent
color: "green"
onColorChanged: {
for(var i = 0; i < 10000; i++)
console.log(i)
}
}
I know, that the cleanest solution would be to perform the heavy work on a different thread to not block the UI thread. But I do not wish to do this, because in my actual application the blocking work is updating a ListModel, which (as noted here for example)
Qt views unfortunately don't know how to deal with [when they are] in foreign threads.
So I would need to implement a new, asynchronous Model class, which is effort and time my customer is currently not willing to pay for.
Therefor my question is: How can I make sure, that the UI is redrawn/updated as soon as I set the property?
A possible approach is to use transform the sequential logic of the "for" to an asynchronous logic through a Timer:
import QtQuick 2.9
import QtQuick.Window 2.3
Window {
visible: true
width: 640
height: 480
title: qsTr("Ui Demo")
Rectangle {
id: rectangle
anchors.fill: parent
color: "green"
}
MouseArea {
id: action
anchors.fill: parent
onClicked: {
rectangle.color = "red"
timer.start()
}
}
Timer{
id: timer
interval: 1
property int counter: 0
repeat: true
onTriggered: {
counter += 1
console.log(counter)
if(counter > 100000)
timer.stop()
}
}
}
Reliable Workaround
Thanks to eyllanesc's answer I figured out a possible solution.
I use a single shot timer, to start my work, because in the actual code I cannot call different steps with a repeating timer - but I do not need to anyway, as I don't want to display any animated UI elements. This code works for my purposes:
import QtQuick 2.9
import QtQuick.Window 2.3
Window {
visible: true
width: 640
height: 480
title: qsTr("Ui Demo")
Rectangle {
id: rectangle
anchors.fill: parent
color: "green"
}
MouseArea {
id: action
anchors.fill: parent
onClicked: {
rectangle.color = "red"
timer.start()
}
}
Timer {
id: timer
interval: 1
repeat: false
onTriggered: {
for(var i = 0; i < 10000; i++)
console.log(i)
rectangle.color = "green"
}
}
}
Adding a Timer - even with only a 1 msec interval - grants the logic with the processing time to change the color, before starting with the actual work. While this looks like a slightly hacky workaround it works just fine.
More elegant though less reliable approach
There is a cleaner, though less reliable solution:
Qts callLater() function seems to be sort of what I was looking for. Even though the official documentation seems incomplete, I found the function documentation in its source code:
Use this function to eliminate redundant calls to a function or signal.
The function passed as the first argument to Qt.callLater()
will be called later, once the QML engine returns to the event loop.
When this function is called multiple times in quick succession with the
same function as its first argument, that function will be called only once.
For example:
\snippet qml/qtLater.qml 0
Any additional arguments passed to Qt.callLater() will
be passed on to the function invoked. Note that if redundant calls
are eliminated, then only the last set of arguments will be passed to the
function.
Using the call later function delayed the call to the working code most of the time for long enough so that the UI would get updated. However about a third of the times, this would fail and show the same behavior as described in the question.
This approach can be implemented like the following:
import QtQuick 2.9
import QtQuick.Window 2.3
Window {
visible: true
width: 640
height: 480
title: qsTr("Ui Demo")
Rectangle {
id: rectangle
anchors.fill: parent
color: "green"
}
MouseArea {
id: action
anchors.fill: parent
onClicked: {
rectangle.color = "red"
Qt.callLater(action.doWork)
}
function doWork() {
for(var i = 0; i < 10000; i++)
console.log(i)
rectangle.color = "green"
}
}
}
This is the pull to refresh icon used to refresh views in Android.
I've been trying to bring that to qml but it is not so easy.
There are so many transitions that it quickly becomes very complex.
How difficult this should be to recreated in QML?
Is using canvas the better solution?
As i have first seen, the swipe brings down the arrow in a different pace of the swipe, while the arrow rotates. If this arrow comes from a canvas how can it relate to outside events, that is the swipe?
I used something like this:
//
// Slot called when the flick has started
//
onFlickStarted: {
refreshFlik = atYBeginning
}
//
// Slot called when the flick has finished
//
onFlickEnded: {
if ( atYBeginning && refreshFlik )
{
refresh()
}
}
It seems to work as expected and it is easy to implement
The problem is that Flickable and the derived ListView don't really provide any over-drag or over-shoot information in the cases where the visual behavior is disabled.
If dragging the visual over the beginning is not a problem for you, you can simply use the negated value of contentY which goes into the negative if the view is dragged before its beginning.
The only solution I can think of to not have any visual over-dragging but still get the over-drag information in order to drive your refresher is to set the view interactive property to false, and put another mouse area on top of that, and redirect drags and flicks manually to the now non-interactive view.
That last part might sound complex, but it isn't that complex, and I happen to know for a fact that it works well, because I have already used this approach and the source code is already here on SO.
So once you have access to the mouse area that controls the view, you can track how much you are in the negative, and use that information to drive the logic and animation of the refresher.
The notable difference between the implementation in the linked answer and what you need is that the linked answer has the mouse area in each delegate, due to the requirements of the specific problem I wanted to solve. You don't need that, you only need one single mouse area that covers the view.
I did like this recently.
Basically I use the position of a ScrollBar and if it goes negative I show a spinner and refresh. So I don't need to mess with the flick stuff.
import QtQuick.Controls 6.0
import QtQuick 6.0
ListView {
ScrollBar.vertical: ScrollBar {
id: scrollbar
}
property bool negativescroll: scrollbar.position < 0
onNegativescrollChanged: {
if (spinner.visible) {
refresh()
}
spinner.visible = !spinner.visible
}
BusyIndicator {
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
visible: false
running: visible
id: spinner
}
width: 180; height: 200
model: model
delegate: Text {
text: name + ": " + number
}
ListModel {
id: model
ListElement {
name: "Bill Smith"
number: "555 3264"
}
ListElement {
name: "John Brown"
number: "555 8426"
}
ListElement {
name: "Sam Wise"
number: "555 0473"
}
}
}
I came to a simpler solution based on dtech's experience involving multiple Flickable elements, which basically consists on filling the Flickable with a MouseArea, setting its boundsBehavior property to Flickable.StopAtBounds, and from there, if it's at the top, do things based on mouseY values.
The better approximation i could get is in the following code. A possible drawback is that diagonal swiping also counts as a refresh intention. It could be improved with GestureArea, but i'm too lazy to get my hands on this at the moment.
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Window 2.2
ApplicationWindow {
property real mm: Screen.pixelDensity
property real margins: 2 * mm
id: mainWindow
visible: true
width: 60 * mm
height: 120 * mm
title: qsTr("Hello World")
ListModel {
id: myModel
Component.onCompleted: {
for(var i = 0; i <= 100; ++i) {
myModel.append({num: i})
}
}
}
ListView {
id: view
boundsBehavior: Flickable.StopAtBounds
interactive: true
anchors.fill: parent
model: myModel
spacing: 4
delegate: Rectangle {
width: parent.width
height: 25 * mm
border.color: 'red'
Text {
id: name
text: num
anchors.centerIn: parent
}
}
Rectangle {
signal follow
id: swatch
width: 15 * mm
height: width
radius: width / 2
color: 'lightgray'
anchors.horizontalCenter: parent.horizontalCenter
y: - height
}
MouseArea {
property int mouseYSart
property int biggerMouseY
anchors.fill: view
onPressed: {
mouseYSart = mouseY
biggerMouseY = 0
}
onMouseYChanged: {
if(view.contentY == 0) {
var currentMouseY = mouseY
if(currentMouseY > biggerMouseY) {
biggerMouseY = currentMouseY
swatch.y += 1
}
if(currentMouseY < biggerMouseY) {
biggerMouseY = currentMouseY
swatch.y -= 1
}
}
}
onReleased: swatch.y = - swatch.height
}
}
}
I have some questions regarding QML. I have a TableView and , when i click on a column header, i need to open a new window which contains all elements under that column with a button on the left of each element.
Clicking on that button should send a message back to TableView to update.
My questions are:
How can I catch the mouse click for a column?
Which would be best solution for the 2nd window: a tableview with 2 columns(one for the button and one for the element)? In this case I am not sure how to set the value for the 1st column...
How can i pass messages between 2 qml windows?From the 1st window i send the model(elements under the column) and from the 2nd windows i send back one or multiple values(depending on how many buttons are checked)
Thank you
You can communicate inbetween multiple windows in QML, just the the same way, as you communicate between any other two Items by referencing them via ids or assiging them to properties that you later use to reference them.
An examople:
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.0
Item {
ApplicationWindow
{
id: appWindow
width: 500
height: 800
visible: true
ListModel {
id: lm
Component.onCompleted: {
for (var i = 0; i < 42; i++) append( { message: 'Hellow World ' + i })
}
}
ListView {
id: lv
width: 300
height: 800
model: lm
delegate: Button {
text: model.message
onClicked: secondWindow.text = text
}
}
}
ApplicationWindow
{
id: secondWindow
width: 500
height: 800
x: appWindow.x + 500
y: appWindow.y
visible: true
property alias text: label.text
Text {
id: label
anchors.centerIn: parent
}
}
}
I've been trying to run a BusyIndicator (http://doc.qt.io/qt-5/qml-qtquick-controls-busyindicator.html) while I am loading a qml file (http://doc.qt.io/qt-5/qml-qtquick-loader.html), but the BusyIndicator doesn't appear.
What I am trying to do is:
1- The user emits a "handlerLoader(name)", where "name" is the url of the next qml page.
2- In "onHandlerLoader" I run the busyIndicator.
3- Then, I change the Loader source.
The problem is that no matter the time I spent between steps 2 and 3, the BusyIndicator does not appear.
Moreover, when I comment step 3, the busyIndicator appears correctly.
What I am doing wrong?
Thanks!!
This is the code:
Rectangle {
visible: true
width: 800
height: 480
signal handlerLoader (string name)
Loader {
id: pageLoader;
source: "init.qml";
}
BusyIndicator {
id: busyIndicator_inicio
width: 100
height: 100
anchors.centerIn: parent
running: false
}
Connections {
target: pageLoader.item
onHandlerLoader: {
busyIndicator_inicio.running = true
pageLoader.source = name;
}
}
}
The reason is, that your heavy-loading Loader is blocking the thread.
Set it to asynchronous mode, to allow the rest of the program to run.
Further, I'd recommend to prefer declarative bindings to imperative assignments in handlers. See my example:
main.qml:
import QtQuick 2.4
import QtQuick.Window 2.2
import QtQuick.Controls 2.0
Window {
width: 1000
height: 800
visible: true
Button {
text: 'load'
onClicked: {
loader.source = "TestObj.qml"
}
}
Loader {
anchors.fill: parent
id: loader
active: true
asynchronous: true
visible: status == Loader.Ready
}
BusyIndicator {
id: ind
anchors.fill: parent
running: loader.status == Loader.Loading
}
}
TestObj.qml:
import QtQuick 2.0
Item {
Grid {
anchors.fill: parent
columns: width
rows: height
Repeater {
model: 100
Rectangle {
width: { for (var i = 0; i < 10000; i++) console.log(i); return 1 }
height: 1
color: 'green'
}
}
}
}
Since the asynchronous Loader might display incomplete files for some time, I set it to be visible only when its status changes to ready.