Adding custom properties to QML type - qt

I am very new to QML and fairly seasoned with C++. I have been trying to go through some QML examples to try and learn it.
I was playing around with the TumblerColumn control (from examples) and basically trying to set the model to set the year. it goes something like:
TumblerColumn {
id: yearColumn
width: characterMetrics.width * 4 + tumbler.delegateTextMargins
model: ListModel {
Component.onCompleted: {
for (var i = 2000; i < 2100; ++i) {
append({value: i.toString()});
}
}
}
onCurrentIndexChanged: tumblerDayColumn.updateModel()
}
Now, I made a change like:
TumblerColumn {
id: yearColumn
width: characterMetrics.width * 4 + tumbler.delegateTextMargins
property int startYear: 2000
property int endYear: 3000
model: ListModel {
Component.onCompleted: {
for (var i = startYear; i < endYear; ++i) {
append({value: i.toString()});
}
}
}
onCurrentIndexChanged: tumblerDayColumn.updateModel()
}
This returns an error:
ReferenceError: startYear is not defined
How can I define these readonly constant properties for such a QML element.

startYear and endYear aren't in that scope. Try this
TumblerColumn {
id: yearColumn
width: characterMetrics.width * 4 + tumbler.delegateTextMargins
property int startYear: 2000
property int endYear: 3000
model: ListModel {
Component.onCompleted: {
for (var i = yearColumn.startYear; i < yearColumn.endYear; ++i) {
append({value: i.toString()});
}
}
}
onCurrentIndexChanged: tumblerDayColumn.updateModel()
}

Try make new QtObject in QML and add getters and setters
Item {
QtObject {
id : readOnlyProperties
property int startYear : 2000
property int endYear : 3000
}
function getStartYear() {return readOnlyProperties.startYear ;}
function settartYear (_startYear ) { readOnlyProperties.startYear = _startYear; }
// the same getter and setter for endYear
}

Related

Qt3d Sceneloader: Replace the material component of an Entity with my custom material at runtime

I have the following code to load a collada scene using the SceneLoader:
SceneLoader{
id: sceneLoader
source: "file:///home/rui/projects/cad/bodyplacement_lm36_v2.dae"
MetalRoughMaterial {
id:metal_mat
objectName: "MetalRoughMaterial"
metalness: 0
roughness: 0.9
}
onStatusChanged: {
console.log("SceneLoader status: " + status);
if (status == SceneLoader.Ready) {
console.log("Scene is ready");
var entitynames=sceneLoader.entityNames();
for (var i = 0; i < entitynames.length; ++i) {
var entityname=entitynames[i];
var entityvar=sceneLoader.entity(entityname);
for (var j = 0; j< entityvar.components.length; ++j) {
var cmp=entityvar.components[j]
if(cmp){
var cmp_class=cmp.toString();
if(cmp_class.indexOf("QPhongMaterial")>=0){
entityvar.components[j]=metal_mat;
}
}
}
}
}
}
}
As stated in docs (https://doc.qt.io/qt-5/qt3drender-qsceneloader.html#details):
The loader will try to determine the best material to be used based on the properties of the model file. If you wish to use a custom material, you will have to traverse the tree and replace the default associated materials with yours.
After I iterate all the entities I try to replace the material component with the code:
entityvar.components[j]=metal_mat;
but it isn't working. After debugging I can see that the loaded material isn't replaced.
How can I replace the material component with my custom material at runtime?
I have been working to solve this issue. Your code has a couple of issues.
entityvar.components[j]=metal_mat;
this will not work because qml wants you to replace the whole array, you cannot modify it in place
var entitynames=sceneLoader.entityNames();
for (var i = 0; i < entitynames.length; ++i) {
var entityname=entitynames[i];
var entityvar=sceneLoader.entity(entityname);
This will not work for files that have entities or submodels with identical names, such as no name at all.
Here is how I solved these issues:
SceneLoader loads the scene from the fbx or obj file and places it as childNodes into its parent. So the only way to traverse the tree is to look at the parent entity's childNodes array.
import Qt3D.Render 2.12
import Qt3D.Extras 2.12
import Qt3D.Core 2.12
Entity {
id: rootEntity
property alias source: loader.source
property Transform transform: Transform{}
components:[ loader, transform ]
SceneLoader {
id: loader
Component {
id: pbrMatTemplate
MetalRoughMaterial { }
}
onStatusChanged: {
if(status == SceneLoader.Ready) {
instantiateMaterials();
}
}
}
function instantiateMaterials() {
// TODO create as many materials as you need here
var newmat = pbrMatTemplate.createObject(loader);
function traverseNodes(node) {
if(node.components) {
var comps = [];
for (var j = 0; j< node.components.length; ++j) {
var cmp = node.components[j];
var cmp_class = cmp.toString();
if(cmp_class.indexOf("QPhongMaterial")>=0) {
// TODO: look up material from list of materials you created instead of just using one
comps.push(newMat);
} else {
comps.push(cmp);
}
}
// replace whole list of components
node.components = comps;
}
if(node.childNodes) {
for(var i=0; i<node.childNodes.length; i++) {
if(node.childNodes[i] == null) continue;
traverseNodes(node.childNodes[i]);
}
}
} // traverseNodes()
traverseNodes(rootEntity);
}
}

Add property from string to dynamically created ListElement through ListModel.append()

What is the correct approach to insert dynamic properties to dynamically created ListElements in a ListModel using JS?
It's seems that i can't enter a variable reference between { } for supplying properties, it only works for values.model is a LisModel and this function should create the ListElements with both these properties.
function getReadings(model) {
var timeStr = "hour_0";
var temp = 11.9;
model.append({day: 1, timestr: 11.9}); //Error
model.append({day: 1, hour_0: 11.9}); //OK!
}
I'll appreciate any advice.
Keep in mind you're working with javascript. The { } is a JSON object, which is the parameter for the append( ) method.
This worked for me:
MouseArea {
anchors {top: parent.top; right: parent.right}
width: 100
height: 100
onClicked: {
var myKey = "day"
var myValue = "Saturday"
var json = { };
json[myKey] = myValue;
listModel.append(json)
}
}

Multiple items selection from ListView

I want to select multiple items from a ListView. In C++ I would have done something like this
if (clicked_card->is_selected) {
clicked_card->is_selected = false;
int i = 0;
while(selected_cards[i] != clicked_card) i++;
selected_cards.erase(selected_cards.begin() + i);
} else {
clicked_card->is_selected = true;
selected_cards.push_back(clicked_card);
}
The above code uses pointer for comparison. So how to so such selection in QML. The solution I've come up with is something like this
Card.qml
Image {
id: delegate
property bool isSelected: false
...
MouseArea {
onClicked: {
if(isSelected === true) {
isSelected = false;
gameScene.deselectCard(selectSeq);
}
else {
isSelected = true;
gameScene.selectCard({'opParam': opParam, 'selectSeq': selectSeq});
}
}
}
}
GameScene.qml
Item {
id: gameScene
property var selectedCards: []
signal selectCard(variant userData)
onSelectCard: {
gameScene.selectedCards.push(userData)
}
signal deselectCard(variant userData)
onDeselectCard: {
for (var i = 0; i < gameScene.selectedCards.length; i += 1) {
if (gameScene.selectedCards[i].selectSeq == userData) {
gameScene.selectedCards.splice(i, 1);
break;
}
}
}
}
The problem with the above code is that I'm storing property isSelected in a delegate which is created and destroyed by the system. So this is giving me false solution. Is there any better way of multiple selection or any improvements in the solution ? I'm using model from C++ by subclassing QAbstractListModel.
I found the answer in Qt documentation. I simply have to use [DelegateModel][1]. It has a group property, for every group defined in a DelegateModel two attached properties are added to each delegate item. The first of the form DelegateModel.in*GroupName* holds whether the item belongs to the group and the second DelegateModel.*groupName*Index holds the index of the item in that group.
import QtQuick 2.0
import QtQml.Models 2.2
Rectangle {
width: 200; height: 100
DelegateModel {
id: visualModel
model: ListModel {
ListElement { name: "Apple" }
ListElement { name: "Orange" }
}
groups: [
DelegateModelGroup { name: "selected" }
]
delegate: Rectangle {
id: item
height: 25
width: 200
Text {
text: {
var text = "Name: " + name
if (item.DelegateModel.inSelected)
text += " (" + item.DelegateModel.selectedIndex + ")"
return text;
}
}
MouseArea {
anchors.fill: parent
onClicked: item.DelegateModel.inSelected = !item.DelegateModel.inSelected
}
}
}
ListView {
anchors.fill: parent
model: visualModel
}
}
Other solution would have been to move the property isSelected to C++ data model and use a getter and setter function to update the changes.
An easy solution. Use QPair or QPair to store the state of all of your item.
typedef QPair<int, bool> ItemState;
Enable multiple selection in your list o table:
ui->tableView->setSelectionMode(QAbstractItemView::MultiSelection);
And when you want to select a collection, just try something like this:
QList<ItemState> collection;
foreach (ItemState& el , collection) {
const int row = el.first;
const bool state = el.second;
const QModelIndex& index = ui->tableView->model()->index(row, 0);
ui->tableView->selectionModel()->select(index, state ? QItemSelectionModel::Select : QItemSelectionModel::Deselect );
}
You should update your collection data, everytime you modify the data in the model (add, remove o move elements). When the user clicks in a card, just handle the clicked event and modify your collection item state, and recall the loop.

Incrementing property variable in a loop

Not sure how to increment "something" in a Repeater loop in QML: variables, properties.
I would prefer to not to have to involve a wrapper to a cpp function.
property int bs: 0
Repeater {
model: 12
bs: {
var vbs = bs + 1; //this works
vbs;
}//ERROR: Cannot assign to non-existent property bs
}
Repeater{
model: 7
Row {
spacing: table.rowSpacing
DiagValue{
value: {trans("sw " + (index + 1))}
width: 60
}
Repeater{
model: 12
CheckBox {
id:myCheckbox
width: 50
height: 50
backingVisible: false
checkable: true
onClicked:{
matrix[i_index? ][j_index? ] = myCheckbox.checked //how to do this assignement??
//pass the matrix to a cpp wrapper.
}
OR
onClicked:{
matrix[i] = myCheckbox.checked //how to do this assignement??
i++;//??
//pass the matrix to a cpp wrapper.
}
}
}
}
You are trying to use the index property of both repeater at the same time. I would do something like this:
Column {
Repeater{
model: 7
Row {
id: currentRow
// Store the index of the current row based on the
// index in the first Repeater
property int rowIndex: index
spacing: 10
Repeater{
id: repeaterColumns
model: 5
CheckBox {
id:myCheckbox
width: 50
height: 50
// currentRow.rowIndex is the line index
// index is the column index
text: "Line " + currentRow.rowIndex + " col " + index
}
}
}
}
}
I don't really get where matrix comes from and if it is a 2D array or 1D array element. One of these should work:
onClicked:{
matrix[currentRow.rowIndex][index] = myCheckbox.checked
}
or
onClicked:{
matrix[currentRow.rowIndex * repeaterColumns.count + index] = myCheckbox.checked
}
The idea here is not to try and increment anything yourself but rely on the proper index properties of your element.

QML : How to get current item in Pathview

I would like to know how to get the current item in the Pathview.
We do have have iscurrentItem property but how can we use it if its possible. With my present implementation I'm getting the current index but not the value of the item at that index
If you have the preferredHighlightBegin and preferredHighlightEnd properties defined you could use the childAt function to get the current item:
//example for vertical path
view.childAt(0, view.height * (preferredHighlightEnd + preferredHighlightBegin) / 2.0);
There is another approach where you can iterate all the children of the PathView and find which of the children is the current item:
for(var i = 0; i < view.children.length; ++i)
{
if(view.children[i].PathView.isCurrentItem){
console.log(i + " is current item")
//view.children[i] is your current item here
}
}
In your delegate Item you can set a Connection that updates a path custom property:
PathView {
id: myPathView
// ...
property Item currentItem
delegate: pathDelegate
}
Component {
id: pathDelegate
Item {
id: delegateItem
// ...
Connection {
target: myPathView
onCurrentIndexChanged: {
if (myPathView.currentIndex === delegateItem.index) {
myPathView.currentItem = delegateItem;
}
}
}
}
}
Not so efficient, but it works.

Resources