How to add Items dynamically to a Row? (in javascript)
-
wrote on 21 Jul 2020, 17:49 last edited by mbruel
Hi,
I'm just learning QML and I'd like to factorize the creation of a Row using a js function.
So basically in a js function create an Item using Qt.createComponent, affect all the properties and then add it to my Row.
Is that possible?
I'd expect sth like thisRow { id: toolBar anchors.bottom: parent.bottom anchors.bottomMargin: 20; anchors.horizontalCenter: parent.horizontalCenter spacing: 15 height: 50 function addItem(idName, imagePath){ console.log("adding item: " + idName); var button = Qt.createComponent("ToolBarButton.qml"); button.id = idName; button.imagePath = imagePath; button.height = toolBar.height; button.width = 50; button.state = "Inactive"; button.barIndex = toolBar.children.length; toolBar.add(button); } Component.onCompleted: { addItem("home", "img/home.png"); } }
But I'm getting the error
qrc:/main.qml:131: TypeError: Property 'add' of object QQuickRow_QML_2(0x56297448e670) is not a function
-
Hi,
I'm just learning QML and I'd like to factorize the creation of a Row using a js function.
So basically in a js function create an Item using Qt.createComponent, affect all the properties and then add it to my Row.
Is that possible?
I'd expect sth like thisRow { id: toolBar anchors.bottom: parent.bottom anchors.bottomMargin: 20; anchors.horizontalCenter: parent.horizontalCenter spacing: 15 height: 50 function addItem(idName, imagePath){ console.log("adding item: " + idName); var button = Qt.createComponent("ToolBarButton.qml"); button.id = idName; button.imagePath = imagePath; button.height = toolBar.height; button.width = 50; button.state = "Inactive"; button.barIndex = toolBar.children.length; toolBar.add(button); } Component.onCompleted: { addItem("home", "img/home.png"); } }
But I'm getting the error
qrc:/main.qml:131: TypeError: Property 'add' of object QQuickRow_QML_2(0x56297448e670) is not a function
wrote on 21 Jul 2020, 18:15 last edited byhi
@mbruel said in How to add Items dynamically to a Row? (in javascript):
toolBar.add(button);
@mbruel said in How to add Items dynamically to a Row? (in javascript):
TypeError: Property 'add' of object QQuickRow_QML_2
QML Row type has no add mathod
see Dynamic QML Object Creation from JavaScript
example
Component.onCompleted: { createButton(); } Row{ id: row width: parent.width height: 50 } function createButton() { var btn = btnComponent.createObject(row,{ text : "myButton"}); } Component{ id: btnComponent Button{} }
-
wrote on 21 Jul 2020, 18:51 last edited by
hum I see, so I've updated my code to be:
function addItem(idName, imagePath){ console.log("adding item: " + idName); var c = Qt.createComponent("ToolBarButton.qml"); var button = c.createObject(toolBar, { id: idName, imagePath: imagePath, height: toolBar.height, width: 50, state: "Inactive", barIndex: toolBar.children.length }); } Component.onCompleted: { addItem("profile", "img/profile.png"); } Connections{ target: profile onSelected: changeMainMenu(idx); }
I'm getting now getting the issue:
qrc:/main.qml:186: ReferenceError: profile is not defined
So if I understand well I can't set the id of an object created dynamically? Is that right?
This wouldn't be such a big deal but then how can I connect a signal of that object created dynamically? -
hum I see, so I've updated my code to be:
function addItem(idName, imagePath){ console.log("adding item: " + idName); var c = Qt.createComponent("ToolBarButton.qml"); var button = c.createObject(toolBar, { id: idName, imagePath: imagePath, height: toolBar.height, width: 50, state: "Inactive", barIndex: toolBar.children.length }); } Component.onCompleted: { addItem("profile", "img/profile.png"); } Connections{ target: profile onSelected: changeMainMenu(idx); }
I'm getting now getting the issue:
qrc:/main.qml:186: ReferenceError: profile is not defined
So if I understand well I can't set the id of an object created dynamically? Is that right?
This wouldn't be such a big deal but then how can I connect a signal of that object created dynamically? -
wrote on 21 Jul 2020, 19:09 last edited by
great, I got what I wanted with that piece of code:
Row { id: toolBar anchors.bottom: parent.bottom anchors.bottomMargin: 20; anchors.horizontalCenter: parent.horizontalCenter spacing: 15 height: 50 function addItem(idName){ console.log("adding item: " + idName); var c = Qt.createComponent("ToolBarButton.qml"); var barIdx = toolBar.children.length; var button = c.createObject(toolBar, { id: idName, imagePath: "img/"+idName+".png", height: toolBar.height, width: 50, state: barIdx === 0 ? "Selected": "Inactive", barIndex: toolBar.children.length }); button.onSelected.connect(changeMainMenu); } Component.onCompleted: { addItem("home"); addItem("mail"); addItem("compas"); addItem("stack"); addItem("profile"); } }
my ToolBarButton Item is emitting the "selected" signal.
Last question that is a bit out of the scope of this thread. How can I create a List of string that I would iterate in my onCompleted method?
Like a property or simple variable of my root Window. -
great, I got what I wanted with that piece of code:
Row { id: toolBar anchors.bottom: parent.bottom anchors.bottomMargin: 20; anchors.horizontalCenter: parent.horizontalCenter spacing: 15 height: 50 function addItem(idName){ console.log("adding item: " + idName); var c = Qt.createComponent("ToolBarButton.qml"); var barIdx = toolBar.children.length; var button = c.createObject(toolBar, { id: idName, imagePath: "img/"+idName+".png", height: toolBar.height, width: 50, state: barIdx === 0 ? "Selected": "Inactive", barIndex: toolBar.children.length }); button.onSelected.connect(changeMainMenu); } Component.onCompleted: { addItem("home"); addItem("mail"); addItem("compas"); addItem("stack"); addItem("profile"); } }
my ToolBarButton Item is emitting the "selected" signal.
Last question that is a bit out of the scope of this thread. How can I create a List of string that I would iterate in my onCompleted method?
Like a property or simple variable of my root Window.wrote on 21 Jul 2020, 19:13 last edited by ODБOï@mbruel said in How to add Items dynamically to a Row? (in javascript):
Last question that is a bit out of the scope of this thread. How can I create a List of string that I would iterate in my onCompleted method?
Like a property or simple variable of my root Window.
https://doc.qt.io/qt-5/qtqml-syntax-objectattributes.html
property var someList: [ "three", "four"]
-
wrote on 21 Jul 2020, 19:20 last edited by
great, @LeLev you rock!
here is the updated code:
Row { property var menuList : ["home", "mail", "compas", "stack", "profile"] id: toolBar anchors.bottom: parent.bottom anchors.bottomMargin: 20; anchors.horizontalCenter: parent.horizontalCenter spacing: 15 height: 50 function addItem(idName){ console.log("adding item: " + idName); var c = Qt.createComponent("ToolBarButton.qml"); var barIdx = toolBar.children.length; var button = c.createObject(toolBar, { id: idName, imagePath: "img/"+idName+".png", height: toolBar.height, width: 50, state: barIdx === 0 ? "Selected": "Inactive", barIndex: toolBar.children.length }); button.onSelected.connect(changeMainMenu); } Component.onCompleted: { for (var i=0; i < menuList.length; ++i) addItem(menuList[i]); } }
-
wrote on 21 Jul 2020, 19:22 last edited by
Here is another way to add unique items using a repeater and a loader (see last post):
https://forum.qt.io/topic/117076/random-customs-controls-in-a-row/3 -
Here is another way to add unique items using a repeater and a loader (see last post):
https://forum.qt.io/topic/117076/random-customs-controls-in-a-row/3 -
You should refrain from using Qt.createComponent and manual dynamic instantiation in QML.
Stay declarative.
Your task can be achieved like so :
Row { id: toolBar property var menuList : ["home", "mail", "compas", "stack", "profile"] property int currentIndex: 0 anchors { bottom: parent.bottom bottomMargin: 20; horizontalCenter: parent.horizontalCenter } spacing: 15 height: 50 Repeater { model: toolBar.menuList ToolBarButton { id: button imagePath: "img/" + model.modelData + ".png" height: toolBar.height width: 50 barIndex: model.index state:barIndex === toolBar.currentIndex ? "Selected" : "Inactive" onSelected: toolBar.currentIndex = barIndex } } }
I also changed the selected button logic here to be self contained and because I didn't know what were the definition of your selected signal or changeMainMenu function.
-
You should refrain from using Qt.createComponent and manual dynamic instantiation in QML.
Stay declarative.
Your task can be achieved like so :
Row { id: toolBar property var menuList : ["home", "mail", "compas", "stack", "profile"] property int currentIndex: 0 anchors { bottom: parent.bottom bottomMargin: 20; horizontalCenter: parent.horizontalCenter } spacing: 15 height: 50 Repeater { model: toolBar.menuList ToolBarButton { id: button imagePath: "img/" + model.modelData + ".png" height: toolBar.height width: 50 barIndex: model.index state:barIndex === toolBar.currentIndex ? "Selected" : "Inactive" onSelected: toolBar.currentIndex = barIndex } } }
I also changed the selected button logic here to be self contained and because I didn't know what were the definition of your selected signal or changeMainMenu function.
wrote on 22 Jul 2020, 08:43 last edited by@GrecKo
Nice, thanks a lot!
Indeed it looks much better that way. I've checked the Repeater documentation and it's defo what I should use.
Here is the updated code which is nearly exactly what you gave me:Row { id: toolBar anchors { bottom: parent.bottom bottomMargin: toolBarMargin; horizontalCenter: parent.horizontalCenter } height: toolBarHeight spacing: toolBarSpacing Repeater { model: menuList ToolBarButton { id: button imagePath: "img/" + model.modelData + ".png" height: toolBar.height width: toolBarButtonWidth barIndex: model.index state: model.index === 0 ? "Selected" : "Inactive" onSelected: changeMainMenu(barIndex); } } Component.onCompleted: { toolBar.children[1].setBadge("3"); toolBar.children[3].setBadge("12"); } }
The result is something like this:
My ToolBarButton have their opacity changing whether their state is Inactive, Hovered or Selected. When they emit the selected signal, the changeMainMenu function would change the state of the other buttons and the content of the main area (not sure yet how I'll achieve that, probably using a StackedView)
function changeMainMenu(selectedMenu){ console.log("New idx: "+selectedMenu) for (var i = 0; i < toolBar.children.length; ++i) { if (i !== selectedMenu) toolBar.children[i].state = "Inactive"; } }
-
@GrecKo
Nice, thanks a lot!
Indeed it looks much better that way. I've checked the Repeater documentation and it's defo what I should use.
Here is the updated code which is nearly exactly what you gave me:Row { id: toolBar anchors { bottom: parent.bottom bottomMargin: toolBarMargin; horizontalCenter: parent.horizontalCenter } height: toolBarHeight spacing: toolBarSpacing Repeater { model: menuList ToolBarButton { id: button imagePath: "img/" + model.modelData + ".png" height: toolBar.height width: toolBarButtonWidth barIndex: model.index state: model.index === 0 ? "Selected" : "Inactive" onSelected: changeMainMenu(barIndex); } } Component.onCompleted: { toolBar.children[1].setBadge("3"); toolBar.children[3].setBadge("12"); } }
The result is something like this:
My ToolBarButton have their opacity changing whether their state is Inactive, Hovered or Selected. When they emit the selected signal, the changeMainMenu function would change the state of the other buttons and the content of the main area (not sure yet how I'll achieve that, probably using a StackedView)
function changeMainMenu(selectedMenu){ console.log("New idx: "+selectedMenu) for (var i = 0; i < toolBar.children.length; ++i) { if (i !== selectedMenu) toolBar.children[i].state = "Inactive"; } }
@mbruel said in How to add Items dynamically to a Row? (in javascript):
My ToolBarButton have their opacity changing whether their state is Inactive, Hovered or Selected. When they emit the selected signal, the changeMainMenu function would change the state of the other buttons and the content of the main area (not sure yet how I'll achieve that, probably using a StackedView)
That's still not very declarative :)
Act on your data, not your visual items.
Items should follow the data.I believe my proposed solution is better, maybe relocate the currentIndex property if you want.
Your solution:
// in your main or elsewhere: function changeMainMenu(selectedMenu) { for (var i = 0; i < toolBar.children.length; ++i) { if (i !== selectedMenu) toolBar.children[i].state = "Inactive"; } } // in your button state: model.index === 0 ? "Selected" : "Inactive" onSelected: changeMainMenu(barIndex);
You manipulate the properties of your items imperatively, you define in the state binding of the button (not really an actual binding since it will be overwritten by the changeMainMenu function anyway) that the first button is selected
My solution:
// in the toolbar or elsewhere: property int selectedIndex: 0 // 0 is the initial selectedIndex // in your buttons state: barIndex === toolBar.selectedIndex ? "Selected" : "Inactive" onClicked: toolBar.selectedIndex = barIndex // ^ note that I changed it to a clicked signal, the button just tells how the user interacted with it // it doesn't set itself as selected, the state binding does that. // both your parent code and the button code becomes simpler
@mbruel said in How to add Items dynamically to a Row? (in javascript):
Component.onCompleted: { toolBar.children[1].setBadge("3"); toolBar.children[3].setBadge("12"); }
Put that information in your model instead:
Repeater { model: ListModel { // an array of JS objects would work here too ListElement { image: "home"; badge: 0 } ListElement { image: "mail"; badge: 3 } ListElement { image: "compas"; badge: 0 } ListElement { image: "stack"; badge: 12 } ListElement { image: "profile"; badge: 0 } } ToolBarButton { ... imagePath: "img/" + model.image+ ".png" badge: model.badge } }
Sorry if I'm being a purist, I just want to share my vision of how one should write QML ;)
I should also mention that I'm not a fan of the
state
property, but that's more opinion based.
In your case I would add aproperty bool selected
property. Bind like so in the repeater:selected: model.index === selectedIndex
and in the button itself do:opacity: selected ? 1 : (hovered ? 0.7 : 0.4)
-
wrote on 22 Jul 2020, 11:13 last edited by mbruel
@GrecKo said in How to add Items dynamically to a Row? (in javascript):
state: barIndex === toolBar.selectedIndex ? "Selected" : "Inactive" onClicked: toolBar.selectedIndex = barIndex
Hum I don't catch something.... The "state binding" doesn't seem to work correctly.
When I click on a button, so the selectedIndex "main variable" is set to the index of my button but then the state of the other buttons is not updated due to the change of the value of selectedIndex.
What in your solution would trigger that? (What my changeMainMenu is actually doing)Put that information in your model instead
Cool will do, it sounds better but for now it is just a mock, I would get this info from C++. Hum, still for the initialization I could use that ListModel. I didn't read the QML model view framework yet...
Sorry if I'm being a purist, I just want to share my vision of how one should write QML ;)
don't be sorry, that's exactly what I'm looking for, get the best practices asap. Thanks for helping ;)
I should also mention that I'm not a fan of the state property, but that's more opinion based.
well I'm not a big fan either of states but I saw that in an example and it seems the easiest way to use Transitions.
// an array of JS objects would work here too
how you do that?
Otherwise for the mainArea I used a Loader that is reloaded in my changeMainMenu function. How would this work with your solution?
Here is my full main.qml:
import QtQuick 2.12 import QtQuick.Window 2.12 Window { id: root visible: true width: 400 height: 600 title: qsTr("my QML app!") property int toolBarIndex : 0 property int toolBarHeight : 50; property int toolBarSpacing : 15; property int toolBarMargin : 20; property int toolBarButtonWidth : 50; property int headerButtonSize : 30; property int mainMargin : 20; // property var menuList : ["home", "mail", "compas", "stack", "profile"] ListModel { id: toolBarModel ListElement { name : "home" ; badge: "0" ; color: "lightgreen" } ListElement { name : "mail" ; badge: "3" ; color: "lightblue" } ListElement { name : "compas" ; badge: "0" ; color: "lightgray" } ListElement { name : "stack" ; badge: "12" ; color: "lightyellow" } ListElement { name : "profile" ; badge: "0" ; color: "ivory" } } Rectangle { id: background anchors { fill: parent; margins: 0; } color: "green" Image { source: "img/bg.png"; fillMode: Image.Stretch; anchors.fill: parent; opacity: 1 } } Loader { id: mainArea anchors { top: parent.top left: parent.left topMargin: mainMargin + headerButtonSize / 2; bottomMargin: mainMargin leftMargin: mainMargin rightMargin: mainMargin } width: parent.width - 2*mainMargin height: parent.height - 2*mainMargin - toolBarHeight - toolBarMargin } ToolBarButton { id: search anchors { top: parent.top left: parent.left margins: mainMargin; } width : headerButtonSize height: headerButtonSize imagePath : "img/search.png" state : "Inactive" selectable: false } ToolBarButton { id: chat anchors { top: parent.top right: parent.right margins: mainMargin; } width : headerButtonSize height: headerButtonSize imagePath : "img/chat.png" state : "Inactive" selectable: false } Text { text: title anchors { top: parent.top horizontalCenter: parent.horizontalCenter topMargin: mainMargin; } color: "#ff0000" font.pointSize: 24 font.bold: true } function changeMainMenu(selectedMenu){ console.log("New idx: "+selectedMenu) for (var i = 0; i < toolBar.children.length; ++i) { if (i !== selectedMenu) toolBar.children[i].state = "Inactive"; } mainArea.setSource("SimpleMainArea.qml"); mainArea.item.lbl.text = toolBarModel.get(selectedMenu).name;//menuList[selectedMenu]; mainArea.item.color = toolBarModel.get(selectedMenu).color; } Row { id: toolBar anchors { bottom: parent.bottom bottomMargin: toolBarMargin; horizontalCenter: parent.horizontalCenter } height : toolBarHeight spacing: toolBarSpacing Repeater { model: toolBarModel; //menuList ToolBarButton { id: button height: toolBar.height width: toolBarButtonWidth // imagePath: "img/" + model.modelData + ".png" imagePath: "img/" + model.name + ".png" barIndex: model.index // state: root.toolBarIndex === barIndex ? "Selected" : "Inactive" // onSelected: root.toolBarIndex = barIndex state: barIndex === root.toolBarIndex ? "Selected" : "Inactive" onSelected: changeMainMenu(barIndex) } } Component.onCompleted: { for (var i = 0; i < toolBar.children.length; ++i) toolBar.children[i].setBadge(toolBarModel.get(i).badge); } } Component.onCompleted: { changeMainMenu(0); } }
The SimpleMainArea.qml
import QtQuick 2.0 import QtQuick.Controls 2.14 Rectangle { property alias lbl: lbl radius: 10 opacity: 0.8 Text { id: lbl anchors { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter; } text: "not set..."; } }
and my ToolBarButton.qml so you can tell me if you see bad practices ;)
import QtQuick 2.12 import QtQuick.Controls 2.14 Item { id: button property string imagePath; property int barIndex : 0; property bool selectable : true; property color color: "transparent" property double opacityInactive: 0.2 property double opacityHover : 0.4 property double opacitySelected: 1 property int borderWidth : 0 property int borderRadius: 0 property string previousState: "Inactive"; property color badgeColor : "#ec3e3a"; // redish color (exactly the one used in OS X 10.10) // Allow the programmer to define the text to display. // Note that this control can display both text and numbers. // property alias badgeText: badgeLbl.text signal selected(int idx); //RectangItemle to draw the button Rectangle { id: rect anchors.fill: parent radius: borderRadius color: button.enabled ? button.color : "grey" border.width: borderWidth border.color: "black" opacity: opacityInactive Image { id: img anchors.fill: parent; source: imagePath; fillMode: Image.PreserveAspectFit; } } Rectangle { id: badge visible: false smooth: true // Create an animation when the opacity changes Behavior on opacity {NumberAnimation{}} // Setup the anchors so that the badge appears on the bottom right // area of its parent anchors.right: rect.right anchors.top: rect.top // This margin allows us to be "a little outside" of the object in which // we add the badge // anchors.margins: -parent.width / 5 + device.ratio(1) color: badgeColor // Make the rectangle a circle radius: width / 2 // Setup height of the rectangle (the default is 18 pixels) height: 18 // Make the rectangle and ellipse if the length of the text is bigger than 2 characters width: badgeLbl.text.length > 2 ? badgeLbl.paintedWidth + height / 2 : height // Create a label that will display the number of connected users. Label { id: badgeLbl color: "#fdfdfdfd" font.pixelSize: 9 anchors.fill: parent verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter // We need to have the same margins as the badge so that the text // appears perfectly centered inside its parent. anchors.margins: parent.anchors.margins } } //Mouse area to react on click events MouseArea { hoverEnabled: true anchors.fill: button onEntered: { button.previousState = button.state; if (button.state != 'Selected') button.state='Hover'; } onExited : { button.state = previousState; } onClicked: { if (selectable) { button.previousState = 'Selected'; button.state = 'Selected'; } button.selected(button.barIndex); } } function setBadge(msg) { if (msg !== "" && msg !== "0") { badge.visible = true; badgeLbl.text = msg; badge.width = badgeLbl.text.length > 2 ? badgeLbl.paintedWidth + badgeLbl.height / 2 : badgeLbl.height; } else badge.visible = false; } function clearBadge() {badge.visible = false;} //change the color of the button in differen button states states: [ State { name: "Inactive" PropertyChanges { target : rect opacity: opacityInactive } }, State { name: "Hover" PropertyChanges { target : rect opacity: opacityHover } }, State { name: "Selected" PropertyChanges { target : rect opacity: opacitySelected } } ] state: previousState; //define transmission for the states transitions: [ Transition { from: ""; to: "Hover" OpacityAnimator { duration: 200 } }, Transition { from: "*"; to: "Selected" OpacityAnimator { duration: 10 } } ] }
Edit: here is a small video of the desired behaviour
Edit2: my commented
// state: root.toolBarIndex === barIndex ? "Selected" : "Inactive" // onSelected: root.toolBarIndex = barIndex
is not stable, it is working the first time but then it seems the change of root.toolBarIndex doesn't impact all the buttons.. Any idea why?
1/13