model.rowCount() won't bind to Item's property - qt

I have a ListView which is initialized with a custom C++ model named rootModel inside Qml.
The model inherits QAbstractListModel and defines a QVector<customType> as a private member to populate the model.
In my ApplicationWindow I have created a Dialog in which I change the model and call the setList() function to update it. This works fine.
I also want to connect the model's size to a ScrollView's int property. This property will define the children of a RowLayout.
The problem is that when I try to bind this property to the model's size, the application crashes.
FYI, all the modifications of the model follow the Qt's rules. rowCount() is Q_INVOKABLE. I have also tried using the onModelChanged handler but this didn't work (I have checked in the documentation that this signal is emitted when modelReset() is emitted, which is taking place inside setList() through endResetModel()
I believe this is a straightforward procedure (have already performed property bindings many times in my project) but doesn't work as expected.
I quote some sample code of my project.
//main.qml
ConfiguredChannels{
id: configuredList
anchors{
left: parent.left
top: devices.bottom
right: tabs.left
bottom: parent.bottom
}
}
TabArea {
id: tabs
y: toolBar.height
x: parent.width / 8
anchors {
top: toolBar.bottom
}
width: 3 * parent.width / 4
height: 3 * parent.height / 4
countPWM: configuredList.model.rowCount() //This is where I want to bind.
}
//ConfiguredChannels.qml
id: confChanView
header: confChanHeader
model: ChannelModel{
id: rootModel
list: channelList
}
//TabArea.qml
Item{
id: tabAreaRoot
property alias channelsPWM: channelsPWM
property int countPWM
ScrollView{
id: scrollPWM
anchors.fill: parent
contentItem: channelsPWM.children
horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOn
RowLayout{
id: channelsPWM
spacing: 0
Layout.fillHeight: true
Layout.fillWidth: true
Component.onCompleted: {
var namesPWM = [];
for (var i=0; i<countPWM; i++){
namesPWM.push("Channel"+(i+1));
}
createChannels(countPWM, "PWM", channelsPWM, namesPWM);
}
}
}
[EDIT 1]
After looking closer I realized that with my current implementation, even if I bind properly to the model's size I would still not be able to create the desired amount of RowLayout's children on demand (after I change the model in the Dialog Configuration.qml).
That is because I have put their creation inside RowLayout's Component.onCompleted handler. The content of this handler will be executed once the Configuration.qml will be initialized inside main.qmlfor the first time. After that, any other change to the countPWM will not make a difference since the Component is already Completed! Please correct me if I am wrong at this point.
Based on that, I have followed another implementation. I have created a "wrapper" function of createChannels, named createStrips(countPWM). This way, to update properly the RowLayout's children I have to call this function.
\\Configuration.qml
\\more code
currentModel.setList(newList)
tabs.createStrips(tableModel.count) \\tableModel is used to populate the newList that will be set to the model
newList.clear()
\\more code
\\TabArea.qml
function createStrips(countPWM){
var namesPWM = [];
for (var i=0; i<countPWM; i++){
namesPWM.push("Channel"+(i+1));
}
createChannels(countPWM, "PWM", channelsPWM, namesPWM);
}
function createChannels(counter, channelType, channelParent, channelMapping){
if ( channelParent.children.length !== 0){
console.log("destroying");
for ( var j = channelParent.children.length; j > 0 ; j--){
channelParent.children[j-1].destroy();
}
}
for (var i=0;i<counter;i++){
var component = Qt.createComponent(channelType+".qml");
if( component.status !== Component.Ready )
{
if( component.status === Component.Error )
console.debug("Error:"+ component.errorString() );
return; // or maybe throw
}
var channels =component.createObject(channelParent, { "id": channelType+(i+1), "channelText.text": channelMapping[i]});
}
[EDIT 2]
Although the solution in EDIT 1 works and produces the right children for my ScrollView I don't think it is good enough and I believe the best implementation would be to bind the model's size change with the call to the createStrips(countPWM) function. Something like:
\\main.qml
ConfiguredChannels{
id: configuredList
anchors{
left: parent.left
top: devices.bottom
right: tabs.left
bottom: parent.bottom
}
onModelChanged: tabs.createStrips(model.rowCount) //or an appropriate signal handler defined on C++ side
}
And perhaps even better, make the creation of the children as a custom qml signal handler that will be emitted every time the model's size is changed. (I tried the onModelChanged as above but didn't work. Probably I am missing in which case is this signal emitted)
[SOLUTION]
I followed the instructions of the accepted answer as well as this link.
I added a Q_PROPERTY to my model's definition inside the header file named rowCount with the NOTIFY rowCountChanged as well as the signal void rowCountChanged(); . Also, inside the function setList(newList) which I use to update the model, I added at the end of its implementation the emit rowCountChanged(); . Last I connected this signal with my function createStrips(count) inside QML. Now every time the model's size is changed, my ScrollView will update automatically the strips shown as the RowLayout's children.
\\ChannelModel.h
...
Q_PROPERTY(int rowCount READ rowCount NOTIFY rowCountChanged)
...
signals:
void rowCountChanged();
\\ChannelModel.cpp
void ChannelModel::setList(ChannelList *list)
{
beginResetModel();
...
endRestModel();
emit rowCountChanged();
}
\\main.qml
Connections {
target: configuredList.model
onRowCountChanged: tabs.createStrips(configuredList.model.rowCount)
}

Only the q-property allow a binding, in your case the Q_INVOKABLE is not, so you will have to create it, for this we use the signal rowsInserted and rowsRemoved as shown below:
*.h
Q_PROPERTY(int rowCount READ rowCount NOTIFY rowCountChanged)
public:
...
signals:
void rowCountChanged();
*.cpp
//constructor
connect(this, &QAbstractListModel::rowsInserted, this, &YourModel::rowCountChanged);
connect(this, &QAbstractListModel::rowsRemoved, this, &YourModel::rowCountChanged);
*.qml
countPWM: configuredList.model.rowCount // without ()
Note:
I'm assuming that when you add an element or remove it, you're using:
beginInsertRows(QModelIndex(), rowCount(), rowCount());
//append data
endInsertRows();
Or:
beginRemoveRows(QModelIndex(), from, to)
// remove
endRemoveRows();

You can't bind to a Q_INVOKABLE since there is no association to a change-signal.
Create a Q_PROPERTY(count READ rowCount NOTIFY rowCountChanged) or something like that. Make sure that the signal rowCountChanged is emitted when rows are inserted or removed.
Of course it would be more canonical if you had a
Q_PROPERTY(count READ count NOTIFY countChanged)
int count() { return rowCount(); }
and make sure to emit countChanged.
Then you can bind to the property count.

Related

Repater: Using C++ property as model

I have a C++ property
Q_PROPERTY(QList<qreal> XTickPos MEMBER _xTickPos);
which I want to use in a Repeater. In the same QML file, the c++ class has been given the id
id: pw
The repeater looks like this
Item {
anchors.fill: parent
visible: true
Repeater {
model: pw.XTickPos.length
Rectangle{
height: 50
width: 2
x: pw.XTickPos[index]
y:10
visible: true
color: "black"
border.width: 2
}
}
}
However, nothing is drawn on the screen. If instead I make property in the QML file:
var xTickPos = []
and set it via a Q_Invokable function in c++
Q_INVOKABLE QList<qreal> getXTickPositions();
and in QML
root.xTickPos=pw.getXTickPositions();
and use the QML property xTickPos as model in the above repeater it is working. I checked that pw.XTickPos is correctly filled via a console.log
What am I missing here?
This one is kinda tricky.
The documentation states that you can use a JS array as a model, and it does state that QList<qreal> is automatically converted to a JS array when returned to QML.
But it seems that you can't use a QList<qreal> that is automatically converted to a JS array as a model. Go figure...
Naturally, it is preferable to have a proper model with its proper notifications for top efficiency. But in case you really want to go for the list property, it appears you will have to do the conversion manually in a getter:
QVariantList model() {
QVariantList vl;
for (auto const & v : yourList) vl.append(v);
return vl;
}
Amazingly, although Qt supposedly makes that conversion automatically, it doesn't seem to be able to make a QVariantList from a QList<qreal>.
That's Qt for you...

Add elements to a ListView inside another ListView

I need to insert elements in a ListView inside another ListView (via JS code inside my QML file) but when I try to access the inner ListView I get the error :
TypeError: Cannot call method 'insert' of undefined
Here is an example code to show my problem :
Item{
id:list
width: parent.width-210
height: parent.height
x:105
Component{
id:listDelegate
Item {
id:elem
height: 100
width: parent.width
Item{
id:titre_liste
height: 50
width: parent.width
Text{
anchors.left: parent.left
color:"white"
text:titre_txt
font.pixelSize: 25
font.bold: false
}
}
Item{
id:listInList
width: parent.width-100
height: parent.height
Component{
id:listInListDelegate
Item{
id:element_liste
height: parent.height
width: parent.width/5
Text{
anchors.left: parent.left
color:"white"
text:element_txt
font.pixelSize: 25
font.bold: true
}
}
}
ListView {
id: viewin
anchors.fill: parent
model: ListModel{
id:listModel_in
}
delegate: listInListDelegate
}
}
}
}
ListView {
id: viewglobal
anchors.fill: parent
model: ListModel{
id:listModel
}
delegate: listDelegate
}
}
And here is my JS code, at the end of the QML file :
function addItem(){
var i;
var numListe = -1;
var liste = "titre"
var item = "item"
for(i = 0;i<listModel.count;i++)
{
if(listModel.get(i).titre_txt === liste)
{
numListe = i;
}
}
if(numListe === -1)//if the list doesn't exist
{
listModel.append({titre_txt:liste});
numListe = listModel.count-1;
}
listModel.get(numListe).listModel_in.insert(0,{element_txt:item});
}
The error come from the last line of the JS code, when I try to insert a new element in the inner list. I verified that the value of "numListe" is 0 so it is not just a problem of wrong index.
How can I add elements to the inner list ?
There is a lot of stuff wrong with that code.
For starters - it is a mess, which is a very bad idea for someone who is obviously new at this stuff. Keep it clean - that's always a good idea regardless of your level of expertise.
listModel_in is an id and as such cannot be accessed outside of the delegate component.
That object however happens to be bound to the view's model property, so as long as the model doesn't change, you can access listModel_in via the model property. However, the view itself doesn't look like it is the delegate root object, so you have to interface it, for example by using an alias.
However, the inner model doesn't exist in the outer model, it only exists in the outer model's delegate item.
So you cannot possibly get it from listModel. You can get it from the viewglobal view, however ListView doesn't provide access by index. So you will have to set currentIndex for every index and use currentItem.
So it will look like this:
viewglobal.currentItem.modelAlias.insert(0,{element_txt:item});
But it should go without saying, you are putting data in the GUI layer, which is conceptually wrong. But it gets worse than conceptually wrong - you might not be aware of this, but ListView only creates items that it needs to show, meaning that it creates and destroys delegates as necessary. Meaning if your item falls out of view, it will be destroyed, and when it comes back into view, a new one will be created, and all the data you had in the model of the old delegate item will be lost. The view should never store data, just show it.
The inner model should be inside the outer model. However, last time I checked, QMLs ListModel didn't support model nesting, neither using declarative nor imperative syntax. If you want to nest models, I have provided a generic object model QML type you can use.

Component with code-controlled properties (without default value)

Summary
How can I create a component in QML that allows me to specify custom properties with default values, which can be overridden by command-line parameters, but which do NOT initially use the default value?
Background
I've created a QML component that can be used to parse command-line properties easily and bind to their values. Its usage looks like this:
Window {
visibility: appArgs.fullscreen ? "FullScreen" : "Windowed"
width: appArgs.width
height: width*9/16
CommandLineArguments {
id: appArgs
property real width: Screen.width
property bool fullscreen: false
Component.onCompleted: args(
['width', ['-w', '--width'], 'initial window width'],
['fullscreen', ['-f', '--fullscreen'], 'use full-screen mode']
)
}
}
$ ./myapp --help
qml: Command-line arguments supported:
-w, --width : initial window width (default:1920)
-f, --fullscreen : use full-screen mode (default:false)
It works quite well, except...
Problem
All bindings that are created to my component initially use the default value. For example, if I launch my app with -w 800 the window's width initially starts with a value of 1920, and then ~immediately resizes to 800 (when the Component.onCompleted code runs). This problem is unnoticeable 90% of the time, mildly annoying 8% of the time...and unusable in the final 2%.
Sometimes the properties that I want to control can only be set once. For example, a network port to connect to using fragile code that cannot disconnect and reconnect to a new port when it changes. Or a mapping library that loads an enormous set of resources for one visual style, and then throws errors if I attempt to change the style.
So, I need the properties to get the command-line value—if specified—the very first time they are created (and otherwise use the default value). How can I make this happen?
UPDATE: Actually, in that particular case it is actually very easy to avoid the resizing - just set visibility to false, then set the properties to the desired values, and set visibility to true:
Window {
id: main
visible: false
Component.onCompleted: {
main.width = ARG_Width // replace with
main.height = ARG_Width * 9/16 // your stuff
main.visibility = ARG_Fullscreen ? Window.FullScreen : Window.Windowed
main.visible = true
}
}
In this case it is convenient since you can simply hide the window until you set the desired property values. In case you actually need to create the component with the correct initial values, you can do something like this:
Item {
id: main
Component {
id: win
Window {
visible: true
width: ARG_Width
height: width*9/16
visibility: ARG_Fullscreen ? Window.FullScreen : Window.Windowed
}
}
Component.onCompleted: win.createObject(main)
}
In this case the application will start without any window, the desired values will be set on prototype level, so that its creation will be delayed and have the right values right from the start.
It is understandable that this happens, after all you don't read in the arguments until after your application has loaded. Thus it will load up the defaults and then switch to the supplied arguments.
If you want to avoid that, the most straightforward solution would be to read in the arguments and expose them as context properties before the main qml file is loaded, such as this (posting fullly working code since you mentioned you are not a C++ guy):
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
int w = 1920; // initial
bool f = false; // values
QStringList args = app.arguments();
if (args.size() > 1) { // we have arguments
QString a1 = args.at(1);
if (a1 == "-w") w = args.at(2).toInt(); // we have a -w, read in the value
else if (a1 == "-f") f = true; // we have a -f
}
engine.rootContext()->setContextProperty("ARG_Width", w); // expose as context
engine.rootContext()->setContextProperty("ARG_Fullscreen", f); // properties
engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); // load main qml
return app.exec();
}
And then in your main.qml file:
Window {
id: main
visible: true
width: ARG_Width
height: width*9/16
visibility: ARG_Fullscreen ? Window.FullScreen : Window.Windowed
}
As the component is created it picks up the correct values right away.
If I completely change the interface to my component, such that the default value is passed to a function that returns a value, then I can achieve my goals.
So, instead of:
property real width: Screen.width
...
Component.onCompleted: args(
['width', ['-w', '--width'], 'initial window width'],
)
I must use something like:
property real width: arg(Screen.width, ['-w', '--width'], 'real', 'initial window width')
This new interface has some disadvantages:
I can no longer specify the order I want arguments to appear in the help, as the properties may invoke arg() in any order.
I can no longer require positional arguments with no flags (e.g. app filename1 filename2) for the same reason.
I have to repeat the type of the property in the descriptor.
It has other benefits, however:
The names of properties do not have to be repeated.
It is fewer lines of code (one line per property instead of 2).
It actually solves my problem stated above.
Example usage:
CommandLineParameters {
id: appArgs
property string message: arg('hi mom', '--message', 'string', 'message to print')
property real width: arg(400, ['-w', '--width'], 'real', 'initial window width')
property bool fullscreen: arg(false, ['-f', '--fullscreen'], 'bool', 'use full screen?')
property var resolution: arg('100x200', '--resolution', getResolution)
function getResolution(str) {
return str.split('x').map(function(s){ return s*1 });
}
}
The Code:
// CommandLineParameters.qml
import QtQml 2.2
QtObject {
property var _argVals
property var _help: []
function arg(value, flags, type, help) {
if (!_argVals) { // Parse the command line once only
_argVals = {};
var key;
for (var i=1,a=Qt.application.arguments;i<a.length;++i){
if (/^--?\S/.test(a[i])) _argVals[key=a[i]] = true;
else if (key) _argVals[key]=a[i], key=0;
else console.log('Unexpected command-line parameter "'+a[i]+'');
}
}
_help.push([flags.join?flags.join(", "):flags, help||'', '(default:'+value+')']);
// Replace the default value with one from command line
if (flags.forEach) flags.forEach(lookForFlag);
else lookForFlag(flags);
// Convert types to appropriate values
if (typeof type==='function') value = type(value);
else if (type=='real' || type=='int') value *= 1;
return value;
function lookForFlag(f) { if (_argVals[f] !== undefined) value=_argVals[f] }
}
Component.onCompleted: {
// Give help, if requested
if (_argVals['-h'] || _argVals['--help']) {
var maxF=Math.max.apply(Math,_help.map(function(a){return a[0].length}));
var maxH=Math.max.apply(Math,_help.map(function(a){return a[1].length}));
var lines=_help.map(function(a){
return pad(a[0],maxF)+" : "+pad(a[1],maxH)+" "+a[2];
});
console.log("Command-line arguments supported:\n"+lines.join("\n"));
Qt.quit(); // Requires connecting the slot in the main application
}
function pad(s,n){ return s+Array(n-s.length+1).join(' ') }
}
}

How to display correctly Treeview with QML Qt 5.5

I'm trying to create a correct Treeview with Qml Qt 5.5.
I succeed to have a Treeview with a global root.
But impossible to find how to add child for row item.
For the moment I got something like that :
TreeView {
id:listTree
anchors.fill: parent
anchors.leftMargin: 1
headerVisible: false
backgroundVisible: false
selection: ItemSelectionModel {
model: myModel
}
TableViewColumn {
role: "name"
}
itemDelegate: Item {
Text {
anchors.verticalCenter: parent.verticalCenter
color: styleData.textColor
elide: styleData.elideMode
text: styleData.value
}
}
Component.onCompleted: {
model.append({"name":"Never"})
model.append({"name":"gonna"})
model.append({"name":"give"})
model.append({"name":"you"})
model.append({"name":"up"})
model.append({"name":"Never"})
model.append({"name":"gonna"})
model.append({"name":"let"})
model.append({"name":"you"})
model.append({"name":"dow"})
}
}
And I would like something like that :
How can I do it ?
You can also create a TreeModel class that extends QStandardItemModel and overrides roleNames(), like done here. To add children to nodes in your tree, just use appendRow().
TreeModel::TreeModel(QObject *parent) : QStandardItemModel(parent)
{
QStandardItem *root = new QStandardItem("root");
QStandardItem *child = new QStandardItem("child");
this->appendRow(root);
root->appendRow(child);
}
Your model doesn't have any parent child relationships which is why its displayed like a list.
You'll want your "TreeModel" to be a collection of TreeItems. Each TreeItem will have knowledge of their own children and their parent item.
You can follow a fully implemented Qt example found here http://doc.qt.io/qt-5/qtwidgets-itemviews-simpletreemodel-example.html. You'll want to (in C++) make a class for TreeItem, and a separate class for your TreeModel.
That example is working code, you can just copy and paste it and get a working model for your TreeView.
The part you'll be particularly interested in is the implementation of the method setupModelData(). That's where you'll want to parse through your wonderful dataset of 80's lyrics and assign each of them a TreeItem.
Each TreeItem (one for every row of data) should be given knowledge of its parent upon creation (in its constructor). Then as soon as its children are created, call parentTreeItem.appendChild(childTreeItem)
When your model is completed, you can assign it to your qml view in a few ways, registering it with qmlRegisterType is what I prefer (http://doc.qt.io/qt-5/qqmlengine.html#qmlRegisterType)
Once registered, it can be created in qml as though it were a ListView or any other qml object.
NOTE: You'll have this rootItem. This is something that isn't usable by the view, but all your "first indentation" parents are children of the rootItem.
Good luck!
Can you provide a code snippet of what line is causing your about failing to make a shortcut for QAbstractItemModel?

How to access dynamically/randomly loaded Repeater items in QML?

The answer provided by #TheBootroo here: link
provides a way to load and change between QML files/screens/views. But when doing it like this how can one use signal and slots?
One can access the items created by the repeater by using the Repeater::itemAt(index) method, but since I don't know in what order the items are loaded I don't know what index screen2, screen3, screen4 etc. is at.
Is there any way to solve this or do one have to instantiate all the screens in memory at start up?
My code below:
main.qml:
//List of screens
property variant screenList: [
"main",
"screen2",
"screen3",
...
]
//Set this to change screen
property string currentScreen: "main"
Repeater {
id: screens
model: screenList
delegate: Loader {
active: false;
asynchronous: true
anchors.fill: parent
source: "%1.qml".arg(modelData)
visible: (currentScreen === modelData)
onVisibleChanged: {
loadIfNotLoaded()
}
Component.onCompleted: {
loadIfNotLoaded()
}
function loadIfNotLoaded () {
//To load start screen
if(visible && !active) {
active = true;
}
}
}
}
Connections {
target: screens.itemAt(indexHere)
//screen is here the string passed with the signal from screen2.
onChangeScreen: currentScreen = screen
}
Button {
id: button1
text: "Go To Screen 2"
onClicked: {
currentScreen = "screen2"
}
}
And in screen2.qml:
signal changeScreen(string screen)
Button {
text: "Go To Main"
onClicked: {
changeScreen("main")
}
}
One can access the items created by the repeater by using the Repeater::itemAt(index) method, but since I don't know in what order the items are loaded I don't know what index screen2, screen3, screen4 etc. is at.
The order of the items the Repeater instantiates is actually defined - the items will be instantiated in the order of the model, so in your case "main" will be the first, then "screen2" and so on. Now, each item inside of the Repeater is a Loader - the Repeater will create 3 Loaders in a defined order. The Loaders themselves load their source item on demand.
I think the only thing missing in your code is that the Connections refers to the Loader, but you need it to refer to the item the Loader creates. So change the Connections to
Connections {
target: screens.itemAt(indexHere).item
onChangeScreen: currentScreen = screen
}
Notice the additional .item.
After fixing this, there is one additional problem though: The Repeater hasn't yet instantiated its items when the Connections element is created, so screens.itemAt(indexHere) will return null. One way to fix this would be to use a currentIndex property instead of a currentScreen property and use that in the binding for target, and set currentIndex after the Repeater has instantiated its items:
property int currentIndex: -1
...
Connections {
target: screens.itemAt(currentIndex).item
...
}
Component.onCompleted: currentIndex = 0
Even easier would probably be to put the Connections element inside the Repeater's delegate, i.e. have 3 Connections instead of one (assuming you have 3 screens).

Resources