how to disconnect the method for a signal in qml? - qt

In QML, in some situations, I need to remove the old method to a signal and redefine a new method to handle the signal, a demo is as following:
import QtQuick 2.6
import QtQuick.Window 2.2
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
MouseArea {
id:mouse
anchors.fill: parent
onClicked: {
console.log("11111");
}
}
Text {
text: qsTr("Hello World")
anchors.centerIn: parent
}
Component.onCompleted: {
//if(false){
//}
// mouse.clicked = null;
//mouse.clicked.disconnect();
mouse.clicked.connect(
function(){
console.log("22222")
});
}
}
I connect a new function, however it will show both "11111" and "22222", seems it add a new function to handle the signal.
I tried set "mouse.clicked = null", still doesn't work.
I also tried disconnect the method,like as following:
MouseArea {
id:mouse
anchors.fill: parent
onClicked:say()
}
Component.onCompleted: {
mouse.clicked.disconnect(say);
mouse.clicked.connect(
function(){
console.log("22222")
});
}
function say(){
console.log("11111");
}
Still print both "11111" and "22222", how can I remove the old methond and redefine the method in Component.onCompleted()?
update on 2017-6-13:
Thanks, guys.I also use connections,still cannot disconnect as I use qt5.6.1, there is no enable property :( . if we use explicitly connect() function, it works. but in my case, I think I can't connect a method , I need to check some If situations to define whether I need to remove the signal handler in Component.onCompleted.
Also a quick question, why the window component completed faster than mouseArea? the log "first load" first loaded.
MouseArea {
id:mouse
anchors.fill: parent
Connections{
onClicked:say()
}
Component.onCompleted: {
//mouse.clicked.connect(say)
console.log("second load")
}
}
Component.onCompleted: {
console.log("first load")
mouse.clicked.connect(
function(){
mouse.clicked.disconnect(say);
//mouse.clicked.disconnect(say);
console.log("22222")
});
}

You can't disconnect the default signal handler onSignal: { do what ever }.
You can only dissconnect, when you have connected with onSignal.connect(say) beforehand. You could do this in Component.onCompleted.
If you use the syntax:
onSignal: say()
this equals
onSignal.connect(function() { say() })
and therefore you can't use onSignal.disconnect(say) as say was never connected. (I don't know whether it is connected to onSignal or to signal - both could be done manually, or whether it is not connected at all. You can't disconnect it, without having a reference to the implicitly created function.)
In QML, the nicer way would be to stay declarative. For this you can use the Connections-construct:
Connections {
target: mouse // set to null to disconnect (Qt<=5.6)
onClicked: { do what ever }
enabled: true // change this, when you want to disconnect it (Qt>=5.7)
}

You haven't used the explicit connect() function to begin with. To disconnect() a function, it has to have been connected explicitly using connect().
There is an example here that might help:
http://doc.qt.io/qt-5/qtqml-syntax-signals.html#connecting-signals-to-methods-and-signals

Related

Changes via GUI thread are not reflected in workerscript

In the code below, listmodel is shared between the threads and a timer is set to update the model's value, these changes should be reflected(desired behavior) in workerscript (docs). But in workerscript, it is always the initial value as it does not know data has changed.
// main file
Window {
id: window
visible: true
width: 150
height: 200
title: qsTr("Hello Testing")
DataModel {
id: model
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: {
model.setProperty(0, 'datum', new Date().toLocaleString())
}
}
WorkerScript {
id: bgScript
source: "script.js"
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: {
bgScript.sendMessage({'model': model})
}
}
Text {
id: datum
text: qsTr(model.get(0).datum)
}
}
// workerscript
WorkerScript.onMessage = function (arg) {
console.log(arg.model.get(0).datum) // value is "Date Time" on every call
}
// model
ListModel {
ListElement {
datum: "Date Time"
}
}
You copied your model into a JS object and passed that object into a JS function. You will never receive notification of updates to an object like that from within the JS function. Bindings only work on QML properties, not JS values.
But even if that did work in general, it still wouldn't work within a WorkerScript. According to the docs, a WorkerScript js file doesn't have access to properties from the calling QML.
Since the WorkerScript.onMessage() function is run in a separate
thread, the JavaScript file is evaluated in a context separate from
the main QML engine. This means that unlike an ordinary JavaScript
file that is imported into QML, the script.mjs in the above example
cannot access the properties, methods or other attributes of the QML
item, nor can it access any context properties set on the QML object
through QQmlContext.

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.

How can I trigger an explicit UI update in qml?

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"
}
}
}

BusyIndicator does not show up

I want to show a BusyIndicator while a long process is going on. The problem is it does not show up when I make it run and shows afterwards when the process is completed. According to the docs
The busy indicator should be used to indicate activity while content is being loaded or the UI is blocked waiting for a resource to become available.
I have created a minimal code that based upon the original code
Window {
id: win
width: 300
height: 300
property bool run : false
Rectangle {
anchors.fill: parent
BusyIndicator {
anchors.centerIn: parent
running: run
}
MouseArea {
anchors.fill: parent
onClicked: {
run = true
for(var a=0;a<1000000;a++) { console.log(a) }
run = false
}
}
}
}
So when the Rectangle is clicked I want to display the BusyIndicator for the time till the calculations gets completed.
For example purpose I have used the for loop here. In actual scenario I call a function (which inserts some 1000 rows into the Database) through the ContextProperty. But in that case too the BusyIndicator is not displayed.
Am I doing it the right way? Or what would be the best way to do it?
You cannot view your BusyIndicator just because long operation in onClicked handler blocks application GUI and indicator does not update. You should run such operation in a different thread to avoid freezing of GUI. Simple example:
QML
Window {
id: win
width: 300
height: 300
property bool run : false
Rectangle {
anchors.fill: parent
BusyIndicator {
id: busy
anchors.centerIn: parent
running: win.run
}
MouseArea {
anchors.fill: parent
onClicked: {
win.run = true
thread.sendMessage({run : true});
}
}
WorkerScript {
id: thread
source: "handler.js"
onMessage: {
win.run = messageObject.run;
}
}
}
}
handle.js
WorkerScript.onMessage = function(message) {
if(message.run === true) {
for(var a=0;a<1000000;a++) { console.log(a) }
}
WorkerScript.sendMessage({run : false});
}
There is a way to do this using QQuickWindow's afterSynchronizing signal:
import QtQuick 2.4
import QtQuick.Controls 1.3
ApplicationWindow {
width: 400
height: 400
visible: true
Component.onCompleted: print(Qt.formatDateTime(new Date(), "mm:ss:zzz"), "QML loaded")
onAfterSynchronizing: {
print(Qt.formatDateTime(new Date(), "mm:ss:zzz"), "Window content rendered")
if (!loader.item) {
loader.active = true
}
}
Item {
anchors.fill: parent
BusyIndicator {
running: !loader.item
anchors.centerIn: parent
}
Loader {
id: loader
active: false
anchors.fill: parent
sourceComponent: Text {
wrapMode: Text.Wrap
Component.onCompleted: {
for (var i = 0; i < 500; ++i) {
text += "Hello, ";
}
}
}
}
}
}
The idea is to use a Loader to have control over when the expensive operation happens. You could also use a dynamically loaded component via Qt.createQmlObject(), or Qt.createComponent() to dynamically load a component in a separate file.
If you run the example, you'll see that you get the following output:
qml: 58:12:356 QML loaded
qml: 58:12:608 Window content rendered
We use QQuickWindow's afterSynchronizing signal to know when the content of the window has been displayed, and only act on it the first time (via if (!loader.item)).
When the signal is initially emitted, we can be sure that the BusyIndicator has started its animation, so the user will actually see a spinning icon.
Once the Loader has finished loading the text, its item property will become non-null and the BusyIndicator will disappear.
Run into the same problem today! I will assume you are controlling your BusyIndicator from a C++ property called busy. And you are setting busy to true just before your calculations and to false just after. Doing this solved it for me. It's not a very elegant solution but it works:
QML
BusyIndicator {
running: CPPModule.busy
}
CPP
void CPPModule::setBusy(const bool &busy)
{
m_busy = busy;
emit busyChanged();
}
void CPPModule::InsertIntoDB()
{
setBusy(true);
QThread::msleep(50);
QCoreApplication::processEvents();
/*
very Long Operation
*/
setBusy(false);
}

How can I switch the focus for the pop-up window?

I encounter a problem which is that the pop-up window cannot get the focus when it is shown. I tried to use the activefocus function in main window, but it doesn't work. It is supposed that if I press the enter key, the pop-window will be closed. How can I get the focus for the pop-up window? Thanks.
...
GridView {
id:grid_main
anchors.fill: parent
focus: true
currentIndex: 0
model: FileModel{
id: myModel
folder: "c:\\folder"
nameFilters: ["*.mp4","*.jpg"]
}
highlight: Rectangle { width: 80; height: 80; color: "lightsteelblue" }
delegate: Item {
width: 100; height: 100
Text {
anchors { top: myIcon.bottom; horizontalCenter: parent.horizontalCenter }
text: fileName
}
MouseArea {
anchors.fill: parent
onClicked: {
parent.GridView.view.currentIndex = index
}
}
}
Keys.onPressed: { //pop up window
if (event.key == 16777220) {//enter
subWindow.show();
subWindow.forceActiveFocus();
event.accepted = true;
grid_main.focus = false;
}
}
}
Window {
id: subWindow
Keys.onPressed: {
if (event.key == 16777220) {//press enter
subWindow.close();
}
}
}
...
Let's start with some basics:
Keys.onPressed: { //pop up window
if (event.key == 16777220) {//enter
subWindow.show()
...
event.accepted = true
}
}
Not to mention how error-prone it is, just for the sake of readability, please don't hard-code enum values like 16777220. Qt provides Qt.Key_Return and Qt.Key_Enter (typically located on the keypad) and more conveniently, Keys.returnPressed and Keys.enterPressed signal handlers. These convenience handlers even automatically set event.accepted = true, so you can replace the signal handler with a lot simpler version:
Keys.onReturnPressed: {
subWindow.show()
...
}
Now, the next thing is to find the correct methods to call. First of all, the QML Window type does not have such method as forceActiveFocus(). If you pay some attention to the application output, you should see:
TypeError: Property 'forceActiveFocus' of object QQuickWindowQmlImpl(0x1a6253d9c50) is not a function
The documentation contains a list of available methods: Window QML type. You might want to try a combination of show() and requestActivate().
Keys.onReturnPressed: {
subWindow.show()
subWindow.requestActivate()
}
Then, you want to handle keys in the sub-window. Currently, you're trying to attach QML Keys to the Window. Again, if you pay attention to the application output, you should see:
Could not attach Keys property to: QQuickWindowQmlImpl(0x1ddb75d7fe0) is not an Item
Maybe it's just the simplified test-case, but you need to get these things right when you give a testcase, to avoid people focusing on wrong errors. Anyway, what you want to do is to create an item, request focus, and handle keys on it:
Window {
id: subWindow
Item {
focus: true
Keys.onReturnPressed: subWindow.close()
}
}
Finally, to put the pieces together, a working minimal testcase would look something like:
import QtQuick 2.9
import QtQuick.Window 2.2
Window {
id: window
width: 300
height: 300
visible: true
GridView {
focus: true
anchors.fill: parent
// ...
Keys.onReturnPressed: {
subWindow.show()
subWindow.requestActivate()
}
}
Window {
id: subWindow
Item {
focus: true
anchors.fill: parent
Keys.onReturnPressed: subWindow.close()
}
}
}
PS. Key events rely on focus being in where you expect it to be. This may not always be true, if the user tab-navigates focus elsewhere, for example. Consider using the Shortcut QML type for a more reliable way to close the popup.

Resources