QML how force update / redraw - qt

In my application I have a process which is started by clicking on a button. The button already has a property to indicate when it is busy, this changes the colours to reflect the busy state. When the process is finished the busy property is set back to false.
The problem is that although the steps are:
button.busy = true
... Do something ...
button.busy = false
In reality the button does not change to reflect the busy state until the process is almost complete, it then changes back to non-busy.
Is there anyway I can insert something after setting the busy state to true and before doing something to get the GUI to update and reflect the state?
My button QML:
Button {
id: root
property bool busy: false
property bool showDropShadow: true
property color bottomColour: MerlinStyle.greenButtonBottom
property color iconColour: "white"
property color topColour: MerlinStyle.greenButtonTop
property string icon: ""
opacity: (pressed || !enabled) ? 0.5 : 1.0
onBusyChanged: {
//Set the colours according to busy state
if ( root.busy == true ) {
root.bottomColour = MerlinStyle.indicatorOrange;
root.topColour = MerlinStyle.indicatorOrange;
} else {
root.bottomColour = MerlinStyle.greenButtonBottom;
root.topColour = MerlinStyle.greenButtonTop;
}
}
background: Item {
RadiusRectangle {
id: rect
anchors.fill: parent
radius: MerlinStyle.rectRadius
topLeftPointed: true
gradient: Gradient {
GradientStop { position: 0.0; color: root.topColour }
GradientStop { position: 1.0; color: root.bottomColour }
}
}
DropShadow {
visible: showDropShadow && !pressed && enabled
anchors.fill: rect
horizontalOffset: 1
verticalOffset: 2
color: "#80000000"
source: rect
}
}
contentItem: Item {
ColoredImage {
anchors.centerIn: parent
height: parent.height * 0.85
width: parent.width * 0.85
source: icon
color: root.iconColour
}
}
}
I've tried to trigger an update using:
idOfButton.update
This always results in :
Button_QMLTYPE_28 : Update called for a item without content
The update function takes no parameters.

When you call that function, it simply blocks the GUI thread, and those events which have been put to the event queue will wait until the program returns to the event loop again. That's why you cannot see that the button is updated based on the property changes.
This happens because of bad design. As per Qt documentation:
use asynchronous, event-driven programming wherever possible
use worker threads to do significant processing
never manually spin the event loop
never spend more than a couple of milliseconds per frame within blocking functions
You should not call a blocking function from within the GUI thread. You need to run that function from another thread or if you have a purpose to do that, you can call your function with a Timer which is a dirty hack.
Timer{
id: dummyTimer
interval:1
repeat: false
running: false
onTriggered: {
control.someLazyBlockingFunction();
idOfButton.busy = false;
}
}
Button{
id: anotherButton
...
onClicked:{
idOfButton.busy = true;
dummyTimer.running= true;
}
}

Late answer, but might help someone.
In my case, my application is running on python (PyQt5 + QML), the event comes from QML code, and is handled in a slot (nothing new here).
The thing is, as stated in the qt documentation, you cannot block the main thread, so, the best way I found to handle it was spinning a new daemon thread to run the event code without hanging the frontend.
#pyqtslot(name="doMyButtonCallback")
def do_mybuttoncallback():
def mybuttonwork():
time.sleep(3)
print("i did not hang your code!"
t=threading.Thread(target=mybuttonwork)
t.daemon=True
t.start()

Related

Binding Type: Is it possible to listen to a temporarily overridden binding?

I'd like to create a widget that has a value that can be bound to a value outside of itself, but can also internally set this value. A scenario I'd like to be able to support:
Developer using the widget binds the widget's value to some external value
At run-time, widget value follows any external value changes via this binding
User interacts with widget, setting their own value
Some time later, external value is updated
Ideally, widget value would then return to being bound to the external value
Point 5 does not seem possible when using only bindings. Here is an example where 'textItem' is our imaginary widget:
import QtQuick 2.7
import QtQuick.Controls 2.0
ApplicationWindow {
visible: true
width: 640
height: 480
property real externalValue: (Math.random()* 50).toFixed(0)
Timer {
running: true
repeat: true
interval: 1000
onTriggered: {
// Simulate externalValue changes out of our control
externalValue = (Math.random()* 50).toFixed(0)
}
}
Component.onCompleted: {
// Unknown external binding set by developer using textItem widget
textItem.text = Qt.binding(function(){return externalValue})
}
Column {
Text {
id: textItem
text: ""
property real internalValue: (Math.random()* 50).toFixed(0)
Binding on text {
id: binding
value: textItem.internalValue
when: false
}
}
Button {
text: "Change Internal Value"
onClicked: {
textItem.internalValue = (Math.random()* 50).toFixed(0)
binding.when = true
}
}
}
}
So textItem.text listens to the externalValue binding until a user interacts with the button, which then binds textItem.text to the user-set internal value. Assuming textEdit is a black-box widget, and it has no concept of externalValue before its runtime binding, is there any way for textEdit to internally listen to the overridden externalValue binding and restore it (by setting binding.when = false) the next time that externalValue is updated by the timer?
One way to make the scenario work would be to use direct assignments in place of all of the bindings, but this seems like it would cause a confusing widget API since I can't stop users from trying to use bindings which would confusingly get broken by the widget's black-box internal assignments...
You can temporarily override bindings using States, like in the code below.
The real problem here is detecting when the external value has changed, in my solution I'm using a Timer for this, but your requirements ask for the external value to change again. Since you are externally binding to property text and also overriding the binding to text you temporarily loose the change signals from the external binding, hence cannot undo the temporary binding.
If you have control over the widget, I would implement a property which should be set externally and internally assign that value to where it should go. This gives you the ability to receive the changes and for example stop the tempBoundedTimer (Since I still think you should have a timer to not indefinitely override the binding in case the external value fails to update).
If you don't have control over the widget, I would settly down on a suitable interval for tempBoundedTimer
(In any case, I don't adding a Timer in each instance of the widget is very nice)
import QtQuick 2.7
import QtQuick.Controls 2.0
ApplicationWindow {
visible: true
width: 640
height: 480
property real externalValue: (Math.random()* 50).toFixed(0)
Timer {
running: true
repeat: true
interval: 1000
onTriggered: {
// Simulate externalValue changes out of our control
externalValue = (Math.random()* 50).toFixed(0)
}
}
Component.onCompleted: {
// Unknown external binding set by developer using textItem widget
textItem.text = Qt.binding(function(){return externalValue})
}
Column {
Text {
id: textItem
text: ""
property real internalValue: (Math.random()* 50).toFixed(0)
Timer {
id: tempBoundedTimer
repeat: false
interval: 2000
}
states: [
State {
when: tempBoundedTimer.running
PropertyChanges {
target: textItem
text: "internal:" + internalValue
}
}
]
}
Button {
text: "Change Internal Value"
onClicked: {
textItem.internalValue = (Math.random()* 50).toFixed(0)
tempBoundedTimer.start()
}
}
}
}
BTW, I think your Binding object should actually also work if the when is bound to tempBoundedTimer.running, but I couldn't get it to play nice. It seems the Qt.binding has priority

Get notified when QQuickItem will need updating

I have search high and low in the documentation but have not found anything regarding this. Is there anyway that an QML element I have created can get notified if one of it's children needs to be redrawn due to changes to it. Will the item send a signal or an event that the parent can connect/listen to. Preferably it would be emitted when the item is marked "dirty" and should be rendered again, but a signal like onPropertyChange would work also.
Example
MyQmlItem {
Rectangle {
width: 50; height: 60
color: "blue"
Text {
text: "hello world"
}
}
}
If some code e.g changes the color of the Rectangle I would like the MyQmlItem to be notified about this change.
FWIIW i managed to find an acceptable solution to above. I added a boolean property "isDirty" to the MyQmlItem class, this emits a signal when it is set to true. Then each child needs to set this if it makes changes that needs redrawing, the QML above then becomes
MyQmlItem {
id: "topItem"
Rectangle {
width: 50; height: 60
color: "blue"
Text {
text: "hello world"
onTextChange: {
topItem.isDirty = true;
}
}
}
not perfect, but good enough

QML ListView method positionViewAtEnd() does exactly the opposite

I'm going crazy. I have a ListView inside a ScrollView, hooked up to a model that inherits QAbstractListModel. When objects are added to the model, the ListView shows them using a delegate. So far, so good.
But I really want the view to stay scrolled to the bottom (like a chat window), and I'm having a very difficult time making that happen. Here is the relevant QML code:
Rectangle {
ScrollView {
[anchor stuff]
ListView {
id: messageList
model: textMessageFiltered
delegate: messageDelegate
}
}
TextField {
id: messageEditor
[anchor stuff]
onAccepted: {
controller.sendTextMessage(text)
text = ""
/* This works. */
//messageList.positionViewAtEnd();
}
}
Component {
id: messageDelegate
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
color: "white"
height: nameText.height + 4
Text {
id: nameText
wrapMode: Text.Wrap
text: "<b>" + authorName + " (" + authorId + ")</b> " + message
[anchor stuff]
}
ListView.onAdd: {
console.log("This prints just fine!")
messageList.positionViewAtEnd()
}
}
}
}
The really strange thing, is that messageList.positionViewAtEnd() (at the end of the file) actually jumps it to the beginning. Without the call, the view stays where it is, even as new entries appear in the list. And indeed, if you look at the Qt documentation for the ListView.positionViewAtEnd(), it says:
Positions the view at the beginning or end, taking into account ...
Is that a silly error in the documentation, or what? I've tried everything I can think of to make this work, particularly the positionViewAtIndex() method and using highlighters to force the scroll to happen. But nothing works. Note the /* This works. */ comment in the source code above. When that is enabled, it works totally fine! (except of course, it jumps to the ListView.count()-2 index, instead of the end of the list)
Does anyone have any idea what might be wrong here? Any examples I could try to prove that there's a terrible, terrible bug in QML?
I'm using Qt 5.3.1 with QtQuick 2.0 (or 2.1 or 2.2 fail too). I've tried many, many other configurations and code as well, so please ask if you need more info. I've completely exhausted my google-fu.
Thanks!
Edit 1
While the accepted answer does solve the above problem, it involves adding the Component.onCompleted to the delegate. This seems to cause problems when you scroll the list, because (I believe) the delegates are added to the view when you scroll up, causing the onCompleted trigger to be called even if the model item isn't new. This is highly undesirable. In fact, the application is freezing when I try to scroll up and then add new elements to the list.
It seems like I need a model.onAdd() signal instead of using the existence of a delegate instance to trigger the scroll. Any ideas?
Edit 2
And how does this NOT work?
ListView {
id: messageList
model: textMessageFiltered
delegate: messageDelegate
onCountChanged: {
console.log("This prints properly.")
messageList.positionViewAtEnd()
}
}
The text "This prints properly" prints, so why doesn't it position? In fact, it appears to reset the position to the top. So I tried positionViewAtBeginning(), but that did the same thing.
I'm totally stumped. It feels like a bug.
You need to set the currentIndex as well.
testme.qml
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Window 2.0
ApplicationWindow {
title: qsTr("Hello World")
width: 300
height: 240
ScrollView {
anchors.fill: parent
ListView {
anchors.fill: parent
id: messageList
model: messageModel
delegate: Text { text: mytextrole }
highlight: Rectangle { color: "red" }
highlightMoveDuration: 0
onCountChanged: {
var newIndex = count - 1 // last index
positionViewAtEnd()
currentIndex = newIndex
}
}
}
ListModel {
id: messageModel
ListElement { mytextrole: "Dog"; }
ListElement { mytextrole: "Cat"; }
}
Timer {
property int counter: 0
running: true
interval: 500
repeat: true
onTriggered: {
messageModel.append({"mytextrole": "Line" + (counter++)})
}
}
}
There is still some jumping to the first element and jumping back down for a fraction of a second.
There is a note in documentation:
Note: methods should only be called after the Component has completed. To position the view at startup, this method should be called by Component.onCompleted.
Change your ListView.onAdd: to
Component.onCompleted: {
console.log("This prints just fine!")
messageList.positionViewAtEnd()
}
And it works well.
In your case, the ListView emits add signal before the new delegate is created and completed. The ListView is still working on something behind the scene, so positionViewAtEnd cannot work as expected. And /* This works. */ because it is called after the new delegate is completed. However, don't assume this always works. Simply follow the note, call positionViewAtEnd in Component.onCompleted, in documentation.

New drag-and-drop mechanism does not work as expected in Qt-Quick (Qt 5.3)

I've tried to implement drag and drop in Qt 5.3 using the new QML types Drag, DragEvent and DropArea. This is the original example from the documentation of the QML Drag type with some small modifications:
import QtQuick 2.2
Item {
width: 800; height: 600
DropArea {
width: 100; height: 100; anchors.centerIn: parent
Rectangle {
anchors.fill: parent
color: parent.containsDrag ? "red" : "green"
}
onEntered: print("entered");
onExited: print("exited");
onDropped: print("dropped");
}
Rectangle {
x: 15; y: 15; width: 30; height: 30; color: "blue"
Drag.active: dragArea.drag.active
// Drag.dragType: Drag.Automatic
Drag.onDragStarted: print("drag started");
Drag.onDragFinished: print("drag finished");
MouseArea {
id: dragArea
anchors.fill: parent
drag.target: parent
}
}
}
Expected behaviour: The small blue rectangle (drag target) can be dragged around with the mouse. If dragged over the larger green rectangle in the center of the window, this rectangle turns red and back to green when leaving. In addition, the signals dragStarted, entered, exited, dropped and dragFinished are emitted in time and the corresponding signal handlers print out their messages.
Experienced behaviour:
Depends on Drag.dragType (see commented line above):
Drag.dragType is NOT set (default is Drag.Internal):
Drag and drop works as described, but only the signals entered and exited are emitted. The other signals (dragStarted, dragFinished and dropped) are suppressed. So there is no way to react to the drop in the DropArea.
Drag.dragType is set to Drag.Automatic:
All of the signals are emitted now, but the blue rectangle (drag target) does not move with the mouse. Instead, the mouse cursor changes its shape to visualize possible drop targets. After the mouse has been released, the blue rectangle jumps to the latest mouse position.
Neither of these two variants are pleasing. How can I get all signals and still be able to drag around the drag target? Unfortunately the documentation is everything but clear about drag-and-drop in QML, especially about the ominous Drag.dragType.
If you open the QQuickDrag source code and look at the differences between start(), which is used by Drag.Internal, and startDrag() which is used by Drag.Automatic, the difference is pretty obvious. start() sets up an event change listener, which it then uses to update the position of the attached object. startDrag() doesn't do this.
Why does it work this way? I have no idea! The QtQuick 2 drag and drop documentation certainly has room for improvement here.
There is a fairly simple workaround: take the best from both worlds. Use Drag.Automatic, but instead of setting Drag.active, call start() and drop() manually. It won't invoke Drag.onDragStarted() and Drag.onDragFinished() but you essentially get those for free anyway by listening for a change in the MouseArea's drag.active.
Here's the concept in action:
import QtQuick 2.0
Item {
width: 800; height: 600
DropArea {
width: 100; height: 100; anchors.centerIn: parent
Rectangle {
anchors.fill: parent
color: parent.containsDrag ? "red" : "green"
}
onEntered: print("entered");
onExited: print("exited");
onDropped: print("dropped");
}
Rectangle {
x: 15; y: 15; width: 30; height: 30; color: "blue"
// I've added this property for simplicity's sake.
property bool dragActive: dragArea.drag.active
// This can be used to get event info for drag starts and
// stops instead of onDragStarted/onDragFinished, since
// those will neer be called if we don't use Drag.active
onDragActiveChanged: {
if (dragActive) {
print("drag started")
Drag.start();
} else {
print("drag finished")
Drag.drop();
}
}
Drag.dragType: Drag.Automatic
// These are now handled above.
//Drag.onDragStarted: print("drag started");
//Drag.onDragFinished: print("drag finished");
MouseArea {
id: dragArea
anchors.fill: parent
drag.target: parent
}
}
}
I realize it's not a completely satisfying solution, but it does match your expected behavior.
This solution offers:
Notifications for all of the desired events: drag started, drag finished, enter drag area, exit drag area, and dropped in drag area.
The drag animation is automatically handled by QtQuick. The square doesn't freeze in place like it does when running the sample code with Drag.Automatic.
What it doesn't offer:
An explanation as to why QtQuick's drag and drop functionality works this way, or whether it's even the intended behavior by the developers. The current documentation seems ambiguous.
Just ran into this myself (using Qt 5.2, but the same problem exists there). I've got a 'slider box' on the X-axis and just wanted to know when the drag was finished... instead of responding to every position change along the way. My workaround involved hacking the states/transitions, with a ScriptAction to provide the logic. This is the simplified version for mimicking a response to the "onDragFinished" signal. So while it doesn't cover all your drag/drop signals, it might get you pointed in the right direction.
Rectangle {
id: sliderControl
height: coordinates.height
width: 80
color: "#F78181"
border.color: "#FE2E2E"
border.width: 1
opacity: 0.4
MouseArea {
id: mouseArea
anchors.fill: parent
drag.target: sliderControl
drag.axis: Drag.XAxis
drag.minimumX: 0
drag.maximumX: view.width - sliderControl.width
hoverEnabled: true
}
states: [
State {
name: "dragging"
when: mouseArea.drag.active
},
State {
name: "finished_dragging"
when: !mouseArea.drag.active
}
]
transitions: [
Transition {
from: "dragging"
to: "finished_dragging"
ScriptAction {
script: console.log("finished dragging script");
}
}
]
}
ps - I know that such a 'workaround' doesn't qualify for the bounty parameters, but I was pretty bummed to find only your question (no solutions) when I searched for help on the issue. Hopefully anyone else stumbling down this path will find this useful. Unfortunately, I've got no clue what's going on with QML's Drag.dragType either.

QML signal executed twice

I'm new in QML and QML signals and I'm having this silly problem that I'm not being able to resolve by myself. I'm triggering an onTouch signal and is executing twice, generating a double response that crashes my app.
Here's my QML code:
//LabelKey.qml
import bb.cascades 1.0
Container {
property string labelText: "#"
property real width: 153.3
property real height: 102.5
property int labelPosX: 60
property int labelPosY: 25
property int labelTextFontWidth: 45
property string imgSrc: "asset:///images/keyboard_button.png"
layout: AbsoluteLayout {
}
preferredWidth: width
preferredHeight: height
objectName: "contTecla"
id: contTecla
ImageView {
objectName: "imgTecla"
id: imgTecla1
imageSource: imgSrc
preferredWidth: width
preferredHeight: height
onTouch: {
textFieldKey.text = textFieldKey.text + labelTecla.text;
}
}
Label {
objectName: "labelTecla"
id: labelTecla
text: labelText
textStyle {
color: Color.DarkYellow
size: labelTextFontWidth
}
layoutProperties: AbsoluteLayoutProperties {
positionX: labelPosX
positionY: labelPosY
}
}
}
I have this TextField whose id is textFieldKey in another QML where I'm including the one I post above. The main idea is simple, is a keyboard where each key is a component of the code above and it has to print the value of the key pressed in this TextField.
The problem is, as I said, the signals is being called twice, filling the TextField with two chars of the value each time.
Please help me I don't know if maybe I'm missing something in the proper way of using signals or something like that.
Thanks!
I figure it out. The touch signals has 4 differents states:
Down: Occurs when the user touches the screen.
Move: Occurs when the user moves a finger on the screen.
Up: Occurs when the user releases a finger.
Cancel: Occurs when an interaction is canceled.
Each one identify with a number from 0 to 3.
And when a touch signal is triggered two states are involved, Down and Up. You just need to make sure with wich one you want to work with and catch it inside the onTouch signal:
if (event.touchType == numberOfTheTouchState){
}
You want to use
ImageView
{
objectName: "imgTecla"
id: imgTecla1
imageSource: imgSrc
preferredWidth: width
preferredHeight: height
onTouch:
{
if(event.isDown())
{
textFieldKey.text = textFieldKey.text + labelTecla.text;
}
}
}
As was noted, without this you get both the up and down events

Resources