QML components which have model / delegate reset when model is updated - qt

Repeater, ListView kind of components which have model / delegate structure are completely resetting when an element within array is modified.
At the example below, the delegate is "Button and it's own Popup".
First, hover your mouse on the button, you will see that button is having the hover effect, changing color etc. Then two seconds later, when Timer is triggered, that hover effect will reset.
Second, click the button, Popup will open, then when Timer is triggered, Popup will close.
I am not sure if this is a bug or not. If it isn't then please recommend me a way to have a list which I can modify the elements and it won't reset.
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
Window {
id: window
width: 300
height: 300
color: "black"
property var array: [0]
Timer {
running: true
repeat: true
interval: 2000
onTriggered: {
++array[0]
// Value won't change if it's not set to itself
array = array
}
}
// All "Item"s are being fully reset, at every update
// the hover effect on button disappears,
// and modal is being closed
Repeater {
model: array
delegate: Item {
Button {
text: "Open My Popup"
onClicked: modal.open()
}
Popup {
id: modal
anchors.centerIn: Overlay.overlay
modal: true
Text {
text: model.modelData
color: "white"
}
}
}
}
}

Related

MouseArea does not pass click to CheckBox

Take a look at this QML snipped:
import QtQuick 2.4
import QtQuick.Controls 2.4
Rectangle {
color: "blue"
width: 50
height: 50
CheckBox {
MouseArea {
anchors.fill: parent
propagateComposedEvents: true
}
}
}
I want to add MouseArea over CheckBox so I can handle doubleclick. However no matter how and what I do CheckBox stops working (clicking it won't show checked mark) as soon as there is MouseArea over it.
What's wrong here?
You can programmatically toggle Qt Quick 2 CheckBox with AbstractButton.toggle(). Also, MouseArea propagateComposedEvents property works only with other MouseAreas and not with Qt Quick Controls QML types.
I don't know your use case so I add few possibilities below.
Signal connect() method
Easiest way to achieve toggling through MouseArea is to create signal chain by connecting MouseArea clicked to CheckBox clicked.
Rectangle {
anchors.centerIn: parent
color: "blue"
width: 50
height: 50
CheckBox {
id: checkBox
onClicked: toggle()
MouseArea {
id: mouseArea
anchors.fill: parent
}
Component.onCompleted: mouseArea.clicked.connect(clicked)
}
}
Note that double click always starts with a single click. If you want to catch double clicks with MouseArea you can e.g. use a Timer for preventing propagating clicks to CheckBox.
Rectangle {
anchors.centerIn: parent
color: "blue"
width: 50
height: 50
CheckBox {
id: checkBox
MouseArea {
id: mouseArea
anchors.fill: parent
onClicked: {
if (timer.running) {
return
}
checkBox.toggle()
timer.start()
}
Timer {
id: timer
interval: 250
repeat: false
}
}
}
}
If you want to support CheckBox's pressed visualization and/or if you want to use bigger MouseArea than the size of the CheckBox you can take a look into this answer of the question Can't click button below a MouseArea.

How to propagateComposedEvents to Button from upper MouseArea?

Demo:
import QtQuick 2.15
import QtQuick.Controls 2.15
ApplicationWindow{
width: 500
height: 500
visible: true
Button {
text: "button"
onClicked: {
console.log("Button onClicked")
}
}
MouseArea {
anchors.fill: parent
propagateComposedEvents: true
onClicked: {
console.log("MouseArea onClicked")
mouse.accepted = false
}
}
}
I expected below when click the button
qml: MouseArea onClicked
qml: Button onClicked
but only got
qml: MouseArea onClicked
How to make the event pass to Button? and why the current demo cannot do that?
it seems a temp solution is:
import QtQuick 2.15
import QtQuick.Controls 2.15
ApplicationWindow{
width: 500
height: 500
visible: true
Button {
text: "button"
onClicked: {
console.log("Button onClicked")
}
}
MouseArea {
anchors.fill: parent
onClicked: {
console.log("MouseArea onClicked")
mouse.accepted = false
}
onPressed: {
console.log("MouseArea onPressed")
mouse.accepted = false
}
}
}
result:
qml: MouseArea onPressed
qml: Button onClicked
it is not what I want but can solve my current problem.
If this is a specific case which you are trying, one solution which you can do is to call the button clicked signal from onClicked in mouse area
import QtQuick 2.15
import QtQuick.Controls 2.15
ApplicationWindow{
width: 500
height: 500
visible: true
Button {
id:btn
text: "button"
onClicked: {
console.log("Button onClicked")
}
}
MouseArea {
anchors.fill: parent
propagateComposedEvents: true
onClicked: {
console.log("MouseArea onClicked")
btn.clicked()
}
}
}
Why the current demo cannot do that?
The point is that the order of the components in the file matters, and this order affects how they overlap each other. For example, in this case of manual positioning, we see how one component overlaps another, what is reflected in their display relative to each other. One component will overlap another component if it is located below in the code.
So in your case, it is not surprising that you get the following output:
qml: MouseArea onClicked
since the MouseArea is located after the Button, and mouse events do not reach the Button. If you arrange them in the reverse order (first the area, and then the button), then the output will be as follows:
qml: Button onClicked
How to make the event pass to Button?
If a click occurs, then it must refer to some one specific component. This means that it will not be possible to click on one component, but to process this click already in several. Therefore, in your case, you can first click on the MouseArea, and then provoke a click from the code on the Button, or vice versa.
Thus, if you want to get exactly the output that you indicated in the question, the already proposed answer will suit you. However, in this case, we lose the ability to use the standard functionality of the Button component, such as "change color when pressed", and more. So I can suggest this solution:
import QtQuick 2.15
import QtQuick.Controls 2.15
ApplicationWindow {
id: root
width: 500
height: 500
visible: true
MouseArea {
id: ma
anchors.fill: parent
propagateComposedEvents: true
onClicked: {
console.log("MouseArea onClicked")
}
Button {
text: "button"
onClicked: {
console.log("Button onClicked")
ma.clicked(ma.mouseX, ma.mouseY)
}
}
}
}
The output will look like this:
qml: Button onClicked
qml: MouseArea onPressed
However, in this case, pressing will work in two places, and at the same time the above-described functionality of the Button is preserved. You can also place the Button under the MouseArea - this will work too.

How to implement single connection between objects in QML?

In my Qt app I have many windows, and sometimes they need a "Back" button. This button is placed on ToolBar component in the header of the ApplicationWindow .
What I want to achieve, is that this Back button, would have only single connection to other objects , i.e. the connection to the last object that called connect method. Right now with every connect I am getting a new connection and when the signal is emitted, it is called multiple times. Unfortunately Qt doesn'thave disconnectAll method, if it would , that would have solve my problem , I would just call disconnectAll before and then connect and that would implement single connection.
So , how are you doing this functionality in Qt , with a simple method?
Here is a minimal reproducible example, click on the tabs many times, then press 'Back' button and you will see lots of console.log messages. And what I need is this message to correspond to the last object that is connected to the Back button.
import QtQuick 2.11
import QtQuick.Controls 2.4
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("Tabs")
signal back_btn_clicked()
SwipeView {
id: swipeView
anchors.fill: parent
currentIndex: tabBar.currentIndex
Page1Form {
id: page1
function page1_callback() {
console.log("page 1 back button triggered")
}
function install_button() {
enable_back_button(page1_callback)
}
}
Page2Form {
id: page2
function page2_callback() {
console.log("page 2 back button triggered")
}
function install_button() {
enable_back_button(page2_callback)
}
}
function install_back_button(idx) {
if (idx===0) {
page1.install_button()
}
if (idx===1) {
page2.install_button()
}
}
}
Button {
id: btn_back
visible: false
text: "Back Button"
onClicked: back_btn_clicked()
}
footer: TabBar {
id: tabBar
currentIndex: swipeView.currentIndex
TabButton {
text: qsTr("Page 1")
onClicked: swipeView.install_back_button(0)
}
TabButton {
text: qsTr("Page 2")
onClicked: swipeView.install_back_button(1)
}
}
function enable_back_button(func_name) {
btn_back.visible=true
back_btn_clicked.connect(func_name)
}
}
PageForm.ui is defined like this
import QtQuick 2.11
import QtQuick.Controls 2.4
Page {
width: 600
height: 400
header: Label {
text: qsTr("Page 1")
font.pixelSize: Qt.application.font.pixelSize * 2
padding: 10
}
Label {
text: qsTr("You are on Page 1.")
anchors.centerIn: parent
}
}
The simplest hack, I think, would be to store the callback in a property, then in enable_back_button(), reference that property in your disconnect() function, and update the property accordingly with the new callback passed as a function argument. (The rationale for this argument being that the disconnect() function must take in an argument: the slot to disconnect. So we'll need to keep track of it some way or another.)
ApplicationWindow {
visible: true
// ... omitted for brevity
property var prevCallback: null
// ... ofb
function enable_back_button(func_name) {
btn_back.visible=true
if (prevCallback)
back_btn_clicked.disconnect(prevCallback) // disconnect previous callback
back_btn_clicked.connect(func_name) // connect new callback
prevCallback = func_name // update property with new callback
}
}
And this could work on multiple connections as well, by simply changing the storage into an array, then iterating through that.

QML progress bar is NOT showing up on UI

I have this QML progress bar:
import QtQuick.Controls 2.0 as QQC20
Item {
QQC20.ProgressBar {
id: progressbar_id
visible: false // even if "true", the progress bar does NOT show up on UI
from: editorScene.progressbarMin
to: editorScene.progressbarMax
value: editorScene.progressbarVal
onValueChanged: {
console.log("Progressbar value changed: ", progressbar_id.value)
}
onVisibleChanged: {
console.log("Progressbar visibility chanaged: ", progressbar_id.visible)
}
}
}
I can confirm that the progress bar value and visibility are changed by the methods onValueChanged and onVisibleChanged.
However, the problem is that the progress bar does NOT show up on the UI! How can I actually show the progress bar on the UI? Can anybody give me a hint?
Right now, all you're doing is creating a QML type which you can use as part of your API. To actually see it, you need to create an instance of it under a ApplicationWindow or Window (or anything else equivalent, e.g. Canvas or Felgo's GameWindow).
There are two ways you can accomplish this. You can
Directly add your item as a child of a window.
Put your item in a separate file, and create an instance of that file under a window.
Lé Code
Method 1: Directly Adding as Child
Directly insert your codeblock as a child of an ApplicationWindow.
// Main.qml
import QtQuick 2.0 // for `Item`
import QtQuick.Window 2.0 // for `ApplicationWindow`
import QtQuick.Controls 2.0 // as QQC20 // no need to label a namespace unless disambiguation is necessary
ApplicationWindow {
width: 480 // set the dimensions of the application window
height: 320
// here's your item
Item {
anchors.centerIn: parent // place in centre of window
ProgressBar {
id: progressbar_id
anchors.horizontalCenter: parent.horizontalCenter // horizontally align the progress bar
from: 0 // don't know what editorScene is
to: 100 // so I'm using raw values
value: 5
onValueChanged: {
console.log("Progressbar value changed: ", progressbar_id.value)
}
onVisibleChanged: {
// side note: I'm not getting any output from this handler
console.log("Progressbar visibility chanaged: ", progressbar_id.visible)
}
}
}
// provide user-interaction for changing progress bar's value
MouseArea {
anchors.fill: parent // clicking anywhere on the background
onClicked: progressbar_id.value += 5; // increments the progress bar
// and triggers onValueChanged
}
}
Method 2: Using a Separate File
Save your item into a new qml file.
// MyProgressBar.qml
import QtQuick 2.0 // for `Item`
import QtQuick.Controls 2.0 // for `ProgressBar`
// here is your item, it has grown up to be in a file of its own 🚼
Item {
property alias value: progressbar_id.value // for user-interaction
ProgressBar {
id: progressbar_id
anchors.horizontalCenter: parent.horizontalCenter // centre horizontally
from: 0
to: 100
value: 5
onValueChanged: {
console.log("Progressbar value changed: ", progressbar_id.value)
}
onVisibleChanged: {
console.log("Progressbar visibility chanaged: ", progressbar_id.visible)
}
}
}
Note that you still need the import statements.
Then call it from a window in Main.qml. We'll use an ApplicationWindow here.
// Main.qml
import QtQuick 2.0
import QtQuick.Window 2.0 // for `ApplicationWindow`
// import "relative/path/to/progressbar" // use this if MyProgressBar.qml is not in the same folder as Main.qml
ApplicationWindow {
width: 480
height: 320
MyProgressBar {
id: progressbar_id
}
MouseArea {
anchors.fill: parent
onClicked: progressbar_id.value += 5;
}
}
If your qml files aren't in the same directory, make sure you add an import "relative/path" at the top of the Main.qml file among the other import statements.
For example, if
Your Qml project is in /Users/Lorem/Project,
The full path to your Main.qml is /Users/Lorem/Project/qml/Main.qml, and
The full path to your MyProgressBar.qml is /Users/Lorem/Project/qml/myControls/MyProgressBar.qml...
Then use import "myControls" in Main.qml to import the items from the myControls subdirectory. Remember, you only need to import the directory, not the file itself.
Result
This is what the result resembles when I run it from a macOS.
At startup.
After 3 clicks on the background.
There is also console/debug output after each click:
Progressbar value changed: 10
Progressbar value changed: 15
Progressbar value changed: 20

Can't close the window with button: QML

I've created an item, that contains a button. I'm trying to close parent window of item with this button, but I'm getting this message, when click the item:
TypeError: Property 'close' of object QQuickRootItem(0x1d8efed8) is not
a function
Can you help me with this?
Code of item:
import QtQuick 2.4
Item {
id: backButton
ItemForButton{
id: baseButton
text: "Back"
onClicked: {
backButton.parent.close()
}
}
}
Code for window:
Window {
id: window
visible: true
BackButton {
}
x: 30
y: 30
}
That seems a bit messy. If I were you, I'd add a clicked signal to the custom button type. For example:
Item:
import QtQuick 2.4
Item {
id: backButton
// Add a clicked signal here
signal clicked()
ItemForButton{
id: baseButton
text: "Back"
onClicked: {
// Emit the new clicked signal here:
backButton.clicked();
}
}
}
Window:
Window {
id: window
visible: true
BackButton {
// Respond to the signal here.
onClicked: window.close();
}
}
This provides the flexibility of using your custom BackButton type in other ways in the future.

Resources