Qt6 QML Treeview get the modelIndex of selected item - c++

I have a working treemodel, called myModel (derived from QAbstractItemModel) in C++ and I can show it in QML using the QML Treeview control and the TreeViewDelegate
When the mouse is pressed on one treeview item, I want to pass a QModelIndex (or parameters which I can use in C++ for constructing a QModelIndex) back to C++ for further processing.
I can use the model.index but this gives me the index in the view (which corresponds to the selected row), but not a useable QModelIndex. Also I experiemented with the depth of the treeview. But I am stuck.
Has anyone a solution?
BTW: I am using Qt6.4. I found something for Qt5, which is now archived (QML: How to get the QModelIndex in a delegate inside a TreeView).
This is the code where I try to investigate the behaviour of the treeview (the call to _controller(...) is the entry point back to C++:
TreeView {
id: garminDrives
Layout.fillHeight: true
Layout.fillWidth: true
Layout.preferredHeight: root.height
Layout.preferredWidth: root.width
delegate: TreeViewDelegate {
MouseArea {
anchors.fill: parent
onClicked: {
console.log("Tree item tapped. Model name = " + model.name
+ " Index to be passed back to C++ = " + model.index,
" Depth = " + garminDrives.depth(model.index) + " row = "
+ model.row + " col = " + model.column
+ " currentRow = " + garminDrives.currentRow
+ " currentCol = " + garminDrives.currentCol
)
_controller.getSelectedRow(model.index)
}
}
contentItem: Label {
id: delegatetext
text: model.name
}
}
model: myModel
}

Related

QML does not write to C++ property when bound to component and component value changes

I'm working on a QML project. In the UI I'm working on, I need to both update slider from C++ and read the current value into C++ from QML. Properties seems to be the right solution. So far I've read different questions on SO without success Two way binding C++ model in QML, Changed Properties do not trigger signal, etc... In my current code I declared a property in my C++ class
class MyClass : public QObject {
Q_OBJECT
public:
MyClass(QObject*);
Q_PROPERTY(double myValue READ getMyValue WRITE setMyValue NOTIFY myValueChanged)
void setMyValue(double n) {
std::cerr << "myValue being update: " << n << "\n";
myValue = n;
}
double myValue = 30;
...
}
And exposed it into Qt via a singleton
qmlRegisterSingletonInstance("com.me.test", 1, 0, "MyClass", &myClass);
Then bound the C++ property to a QML slider
import com.me.test
ApplicationWindow {
Slider {
id: slider
height: 30
width: 100
from: 0
to: 100
value: myClass.myValue
onValueChanged {
console.log("value = " + value)
console.log("myClass.myValue = " + myClass.myValue)
}
/* Doesn't help
Binding {
target: slider
property: "value"
value: myClass.myValue
}*/
}
}
The binding seems to work. I can modify the value of myValue then emit myValueChanged to make QML update it's slider. But given that myClass.myValue is bounded to slider.value. I'd assume both values gets updated at the same time. But dragging the slider shows that they have different values. The following it what is printed in the console when I drag my slider.
qml: value = 19.863013698630137
qml: myClass.myValue = 30
Furthermore setMyValue seems to not being called unless an explicit assignment is made like myClass.myValue = 0. I also tried the Binding component without success. Why is this the case and could I make the C++ property updated whenever i drag the slider?
Qt: 6.2.1
Compiler: clang/gcc
OS: Windows/Linux
Update: tested a reverse binding. Still printing the same result
import com.me.test
ApplicationWindow {
Slider {
id: slider
height: 30
width: 100
from: 0
to: 100
value: myClass.myValue
onValueChanged {
console.log("value = " + value)
console.log("myClass.myValue = " + myClass.myValue)
}
Binding {
target: myClass
property: "myValue"
value: slider.value
}
}
}
You're missing the signal when modifying the value:
Q_PROPERTY(double myValue READ getMyValue WRITE setMyValue NOTIFY myValueChanged)
This means that you promise you'll send the myValueChanged signal whenever the C++ code changes the value. So the following should work:
void setMyValue(double n) {
std::cerr << "myValue being update: " << n << "\n";
myValue = n;
emit(myValueChanged());
}
To update C++ property from QML, you can use Binding:
import com.me.test
ApplicationWindow {
Slider {
id: slider
height: 30
width: 100
from: 0
to: 100
value: myClass.myValue
onValueChanged {
console.log("value = " + value)
console.log("myClass.myValue = " + myClass.myValue)
}
// C++ property was bounded to QML above, now we should bind QML to C++
Binding {
target: myClass
property: "myValue"
value: slider.value
}
}
}
Here's a minimal working example of two way updating Slider:
main.cpp:
data dataObj;
engine.rootContext()->setContextProperty("dataCpp", (QObject*)&dataObj);
data.h:
class data : public QObject
{
Q_OBJECT
Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)
public:
explicit data(QObject *parent = nullptr);
int value(void);
void setValue(int new_value);
public slots:
void reset(void);
signals:
void valueChanged();
private:
int dataValue;
};
data.cpp:
data::data(QObject *parent) : QObject(parent)
{
dataValue = 250;
}
int data::value()
{
return dataValue;
}
void data::setValue(int new_value)
{
if(dataValue != new_value)
{
qDebug() << "setting" << new_value;
dataValue = new_value;
emit valueChanged();
}
}
void data::reset()
{
if(dataValue != 0)
{
qDebug() << "resetting to 0";
dataValue = 0;
emit valueChanged();
}
}
main.qml:
Slider {
id: slider
height: 50
width: 500
from: 0
to: 500
live: false
value: dataCpp.value
onValueChanged: dataCpp.value = value
}
Button{
anchors.top: slider.bottom
text: "Reset"
onPressed: dataCpp.reset()
}

Can not send keyPressEvent to qml

I am using a QQuickWindow, QQuickRenderControl, and a QOffscreenSurface to render a Qml scene into an OpenGL framebuffer object.For this reason, I am using qt example Qt/Examples/Qt-5.15.2/quick/rendercontrol as a base with some minor modifications.
Since no window is actually created, it becomes necessary to artificially transfer mouse and keyboard events to QQuickWindow.There was already mousePressEvent example for mouseEvents on renderControl example which works fine that means I can send mouse events to qml.
Similarly, I tried to implement keyPressEvent and keyReleaseEvent by using QCoreApplication:: sendEvent (m_quickWindow, e); function call but it does not send key events to qml even if I set force to true for a rectangle. When I debug the issue, I have observed that activeFocusItem is NULL on qquickwindow.cpp ==> deliverKeyEvent function.
After that, I put a mousearea inside rectangle and call forceActiveFocus() function for rectangle and then keyPressEvent are sent to qml successfully.Also, If I put timer and call forceActiveFocus for rectangle on some interval it also works but If I try to call same forceActiveFocus() function on Component.onCompleted it is not sending any key events to qml.
So Do I have to call forceActiveFocus() to manage key sending for offscreen surface? Also if I have to call forceActiveFocus() from qml, then is there any way to call it instead of timer or mousearea?
Following is my code snippets:
import QtQuick 2.5
import QtQuick.Window 2.2
Rectangle {
visible: true
width: 1280
height: 480
color: "#000"
Rectangle
{
id:keyPressRect
width:100
height: 100
//focus: true
color: activeFocus? "red": "blue"
onFocusChanged: console.log("focus changed to: " + focus)
onActiveFocusChanged: console.log("active focus changed to: " +activeFocus)
Component.onCompleted:
{
console.log("onCompleted(), focus: " + focus + " activeFocus: " + activeFocus)
forceActiveFocus()
console.log("onCompleted(), focus: " + focus + " activeFocus: " + activeFocus)
}
/*Component.onCompleted: mTimer.start()
Timer {
id:mTimer
interval: 5000
repeat: false
onTriggered: {
keyPressRect.forceActiveFocus()
console.log("testtt")
}
}*/
Keys.onPressed:
{
console.log("key pressed on qml")
}
Keys.onReleased:
{
console.log("key released on qml")
}
MouseArea
{
anchors.fill: parent
onClicked:
{
parent.forceActiveFocus()
console.log("mouse clicked")
}
}
}
}
And sending keyPressEvent from C++ as the following:
void WindowSingleThreaded::keyPressEvent(QKeyEvent *e)
{
qDebug() << "key press event from cpp";
qDebug() << e->key() << "key is";
QCoreApplication::sendEvent(m_quickWindow, e);
}

QML TableView with dynamic number of columns

I have been trying to use a QML TableView to display a QAbstractTableModel. The missing part of the equation seems to be that it is not possible to have a variable number of columns in the TableView, despite overriding QAbstractItemModel::roleNames which should tell Qt the number and name of my columns. I tried testing this out using only QML:
import QtQuick 2.0
import QtQuick.Controls 1.1
Rectangle {
anchors.fill: parent
property real showImage: 1.0
width: 500
TableView {
id: myTable
model: myModel
// TableViewColumn {
// role: "title"; title: "Name"; width: 200
// }
}
ListModel {
id: myModel
ListElement {
title: "item one"
}
ListElement {
title: "item two"
}
}
}
When run this doesn't show anything despite the TableView's mode containing ListElements with roles defined in them.
However if the above code is uncommented and a TableViewColumn is defined then the column will display data for that role as expected but the table will still not display any other roles. Obviously that will only work for a statically defined number of columns and not my case where the number of columns is not known until run time.
The example given is basically the same as my real life example except that my model is defined in C++.
It seems as if this may have already been asked here but it did not gain any response.
EDIT: I had tried calling a javascript function:
function addColumnToTable(roleName) {
var columnString = 'import QtQuick 2.3; import QtQuick.Controls 1.2; TableViewColumn {role: "'
+ roleName + '"; title: "' + roleName + '"; width: 40}';
var column = Qt.createQmlObject(
columnString
, myTable
, "dynamicSnippet1")
myTable.addColumn(column);
}
From C++:
QVariant roleName = "name";
QObject *root = view->rootObject();
QMetaObject::invokeMethod(root, "addColumnToTable", Q_ARG(QVariant, roleName));
This at least allowed me to dynamically add columns from C++ although not from within the model/view architecture. Yoann's solution is far and away better than this though.
You could create dynamically as many TableViewColumn as you need, using the resources property of your TableView.
You will have to add a method in your custom model class which will give you the roleNames you want to display.
QML:
Component
{
id: columnComponent
TableViewColumn{width: 100 }
}
TableView {
id: view
anchors.fill: parent
resources:
{
var roleList = myModel.customRoleNames
var temp = []
for(var i=0; i<roleList.length; i++)
{
var role = roleList[i]
temp.push(columnComponent.createObject(view, { "role": role, "title": role}))
}
return temp
}
model: myModel
MyModel.h:
class MyModel: public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(QStringList userRoleNames READ userRoleNames CONSTANT)
public:
explicit MyModel(QObject *parent = 0);
enum MyModelRoles {
UserRole1 = Qt::UserRole + 1,
UserRole2,
...
};
QStringList userRoleNames();
int rowCount(const QModelIndex & parent = QModelIndex()) const;
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const;
...
private:
QHash<int, QByteArray> roleNames() const;
...
};
MyModel.cpp:
...
...
QHash<int, QByteArray> MyModel::roleNames() const {
QHash<int, QByteArray> roles = QAbstractListModel::roleNames ();
roles[UserRole1] = "whatever";
roles[UserRole2] = "youwant";
return roles;
}
QStringList MyModel::userRoleNames() // Return ordered List of user-defined roles
{
QMap<int, QString> res;
QHashIterator<int, QByteArray> i(roleNames());
while (i.hasNext()) {
i.next();
if(i.key() > Qt::UserRole)
res[i.key()] = i.value();
}
return res.values();
}
...
...
The solution with resources does not work for me (Qt 5.6). The TableView is not updated.
This works for me:
Component{
id: columnComponent
TableViewColumn{width: 30 }
}
TableView {
id: tableView
model: listModel
property var titles: somethingDynamic
property var curTitles: {
var t=[]
for(var i=0;i<columnCount;i++){
t.push(getColumn(i).title)
}
return t
}
onTitlesChanged:{
for(var i=0;i<titles.length;i++){
if(curTitles.indexOf(titles[i])==-1){
var column = addColumn(columnComponent)
column.title=titles[i]
column.role=titles[i]
}
}
for(var i=curTitles.length-1;i>=0;i--){
if(titles.indexOf(curTitles[i])==-1){
removeColumn(i)
}
}
}
}
}
It also correctly updates drag-drop-reordered columns.
Another way to do it with an Instantiator:
TableView{
id: view
model: tableViewModel
Instantiator{
model: someStringList
onObjectAdded: view.addColumn(object)
onObjectRemoved: view.removeColumn(object)
delegate: TableViewColumn{
width: 100
title: modelData
role: modelData
}
}
}
This code (and the one from #palfi) cause some warning in the console:
For each column created there is:
qml: TableView::insertColumn(): you cannot add a column to multiple views
And then when column are removed it produce this warning:
file:///usr/lib/qt/qml/QtQuick/Controls/Private/BasicTableView.qml:297: Error: Invalid attempt to destroy() an indestructible object
If someone has a better solution, please post it!

Complex models and displaying data

I'm just beginning to learn C++ and Qt Framework in particular and I already have a problem right there. The question is how do I create and display data which is not just a string but rather an object, which properties I can access and display. E.g I have a list of employees and I want to display a list which looks like this:
---------------------
John Smith
Salary: 50,230
---------------------
Max Mustermann
Salary: 67,000
---------------------
The goal is that each item in the list is clickable and opens a new window with the details. Also, the important part is that I can be able to style the properties differently.
Qt provide us model and view frameworks, it is pretty flexible.
You could save your data by "model", show the data of your "model" by "view"
and determine how to play your data by "delegate"
The codes of c++ is a little bit verbose, so I use qml from the document to express the idea
import QtQuick 2.1
import QtQuick.Window 2.1
import QtQuick.Controls 1.0
Rectangle {
width: 640; height: 480
//the new window
Window{
id: newWindow
width: 480; height:240
property string name: ""
property string salaryOne: ""
property string salaryTwo: ""
Rectangle{
anchors.fill: parent
Text{
id: theText
width:width; height: contentHeight
text: newWindow.name + "\nSalaryOne : " + newWindow.salaryOne + "\nSalaryTwo : " + newWindow.salaryTwo
}
Button {
id: closeWindowButton
anchors.centerIn: parent
text:"Close"
width: 98
tooltip:"Press me, to close this window again"
onClicked: newWindow.visible = false
}
}
}
ListModel {
id: salaryModel
ListElement {
name: "John Smith"
SalaryOne: 50
SalaryTwo: 230
}
ListElement {
name: "Max Mustermann"
SalaryOne: 67
SalaryTwo: 0
}
}
//this is the delegate, determine the way you want to show the data
Component {
id: salaryDelegate
Item {
width: 180; height: 40
Column {
Text { text: name }
Text { text: "Salary : " + SalaryOne + ", " + SalaryTwo }
}
MouseArea{
anchors.fill: parent
//set the value of the window and make it visible
onClicked: {
newWindow.name = model.name
newWindow.salaryOne = model.SalaryOne
newWindow.salaryTwo = model.SalaryTwo
newWindow.visible = true
view.currentIndex = index
}
}
}
}
ListView {
id: view
anchors.fill: parent
model: salaryModel
delegate: salaryDelegate
}
}
You could separate the window or ListView into different qml files, combine the power of c++ ,qml and javascript. Declarative langauge like qml is pretty good on handling UI.
c++ version
#include <memory>
#include <QApplication>
#include <QListView>
#include <QSplitter>
#include <QStandardItemModel>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QStandardItemModel model(2, 1);
model.appendRow(new QStandardItem(QString("John Smith\nSalary: %1, %2\n").arg(50).arg(230)));
model.appendRow(new QStandardItem(QString("Max Mustermann\nSalary: %1, ").arg(67) + QString("000\n")));
QSplitter splitter;
QListView *list = new QListView(&splitter);
list->setModel(&model);
splitter.addWidget(list);
splitter.show();
return a.exec();
}
Enhance them by your need, c++ version also support delegate.
You could encapsulate the QListView and open a new window when the
user click on the index(you need QItemSelectionModel to detect which
item you selected).Before you can design higly customize UI,you have
to study a lot of the model and view frameworks of Qt. Since your case
are pretty simple, default QListView and QStandardItemModel is enough.
Supplement : How to detect which index you selected?
//the type of model_selected is QItemSelectionModel*
model_selected = list->selectionModel();
connect(model_selected, SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
this, SLOT(selection_changed(QItemSelection, QItemSelection)));
void imageWindow::selection_changed(QItemSelection, QItemSelection)
{
//do what you want
}

QML ListView multiselection

How can I select a few elements in the QML ListView and send its indices to C++ code?
Do something like that: if an element is clicked, set its property selected (or however you call it), and set in delegate that if selected is true, then it should be formatted differently. Plus add it to some list, to work with it.
I am pretty sure there is no way to make a QML ListView multi-selectable. Qt Declarative is focused on touch screen use and there is no meaningful way to multiselect in a pure touch UI.
i had the same issue and i found the best way to implement it, is to create a new role to the listview. Lets assume it is firstname and selected. you need to use both onCurrentIndexChanged and onClicked, because if you scroll, this will change the item but it is not a click. In both of them change the role selected into true, or adjust as it suits you, may be you don't need scroll to select and thus use only the onClicked. When clicked you can change the role selected into true
onCurrentIndexChanged:
{
mListModel.append({"firstName": newEntry,"selected":true})
}
and
onClicked:
{
mListModel.append({"firstName": newEntry,"selected":true})
}
then you may use a highlight in the deligate, this will change the color based on the state of the selected.
Here is a full code that is tested to work
//copyright: Dr. Sherif Omran
//licence: LPGL (not for commercial use)
import QtQuick 2.12
import QtQuick.Layouts 1.12
Item {
property string addnewitem:""
property int removeitemindex: -1
property string appenditemstring: ""
property int appenditemindx:-1
property int fontpoint: 20
property int radiuspoint: 14
property int spacingvalue: 0
property string delegate_color:"beige"
property string delegate_border_color:"yellowgreen"
property string highlight_color:"deeppink"
signal selectedvalueSignal (string iTemstring, string stateval)
property string sv: ""
property int indexcopy:0
id:lstmodelitem
width: parent.width
height: parent.height
ListModel {
id : mListModel
// ListElement {
// firstName : "John"
// }
}
ColumnLayout {
anchors.fill: parent
ListView{
id : mListViewId
model:mListModel
delegate :delegateId
Layout.fillWidth : true
Layout.fillHeight: true
clip: true
snapMode: ListView.SnapToItem //this stops the view at the boundary
spacing: spacingvalue
highlight: Rectangle
{
id: highlightid
width: parent.width
color: mListModel.selected==="true"?"blue":highlight_color
border.color: "beige"
z:3
opacity: 0.2
}
highlightRangeMode: ListView.StrictlyEnforceRange
highlightFollowsCurrentItem:true
onCurrentIndexChanged:
{
console.log("olistdynamic Indexchanged" + currentIndex)
mListViewId.currentIndex=currentIndex
lstmodelitem.selectedvalueSignal(currentIndex, mListModel.selected)
indexcopy=currentIndex
}
}
}
function getindex()
{
console.log("current index = " + indexcopy)
return mListViewId.currentIndex
}
function setindex(index)
{
//console.log("olistdynamic set index"+index)
mListViewId.currentIndex=index
}
function add2Item(newEntry,statev){
console.log("added item with value = " + newEntry + "state " + statev)
mListModel.append({"firstName": newEntry,"selected":statev})
}
function deleteItem(index){
mListModel.remove(index,1)
}
function appendIdem(index,valueEntry,newselectedsate)
{
console.log("append item")
mListModel.set(index,{"firstName": valueEntry,"selected":newselectedsate})
}
Component {
id : delegateId
Rectangle {
id : rectangleId
width : parent.width // Remember to specify these sizes or you'll have problems
height: textId.implicitHeight*1.2
color: selected==="true"?"blue":delegate_color
border.color: delegate_border_color
radius: radiuspoint
Text {
id : textId
anchors.centerIn: parent
text : firstName
font.pointSize: fontpoint
}
MouseArea {
anchors.fill: parent
onClicked: {
lstmodelitem.selectedvalueSignal(mListModel.firstName,mListModel.selected)
mListViewId.currentIndex=index
console.log("current index = " + index)
indexcopy=index
appendIdem(index,firstName,"true")
}
onClipChanged:
{
//console.log("a")
}
}
}
}
//if the item has been changed from null to text
onAddnewitemChanged: {
console.log("added item" + addnewitem)
add2Item(addnewitem)
}
//remove item with index
onRemoveitemindexChanged: {
console.log("remove item")
deleteItem(removeitemindex)
}
//to change the item, change the index first then the string
onAppenditemstringChanged: {
appendIdem(appenditemindx,appenditemstring)
}
}
You may try to get the ListItem's data and store it to an array on odd click and remove the ListItem's data from the array on even click. May be a simple workout, instead of creating a list of check box like items.