Is there a simple way to modify a row height given its index in a QML TableView ?
My problem is that I dynamically load data is the TableView's model. I have a "warnings" column in which i would like to display a list of warnings using Qt RichText HTML tags (ul, li). However, this list is higher than the cell that contains it and there is a y-overflow.
The QTableView class have many methods to solve this problem such as setRowHeight(int row, int height), resizeRowToContents(int row) or resizeRowsToContents().
But it seems that the QML equivalent does not have such methods to easily resize a row...
The rowDelegate might solve my problem but I don't know how to use it to modify the rows' heights separately (I mean given its index).
Does anyone had the same problem and could give me a trick to solve it ?
My TableView :
ListModel {
id: filesList
ListElement {
name:""
dir:""
type:""
status""
}
}
TableView {
id: filesTable
TableViewColumn {
id:nameColumn
role: "name"
title: "Name"
width: dropFiles.width*.2
}
TableViewColumn {
id:dirColumn
role: "dir"
title: "Path"
width: dropFiles.width*.8
}
TableViewColumn {
id:typeColumn
visible: false;
role: "type"
title: "Type"
width: dropFiles.width*.2
}
TableViewColumn {
id:statusColumn
visible: false;
role: "status"
title: "Status"
width: dropFiles.width*.3
}
rowDelegate: Rectangle {
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
height: parent.height
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
if (mouse.button == Qt.RightButton)
{
if(styleData.selected){
rowContextMenu.popup();
}
else {
filesTable.selection.deselect(0, filesTable.rowCount-1);
filesTable.selection.select(styleData.row);
rowContextMenu.popup();
}
}
}
}
}
anchors.fill: parent
selectionMode: SelectionMode.ExtendedSelection
model: filesList
}
The JS function that update my TableView :
function displayParsingStatus(){
for(var i= 0; i<newModel.files.length; ++i){
switch(newModel.files[i].status){
case 0:
filesList.get(i).status="<font color=\"green\">Success</font>";
break;
case 1:
var status ="";
status += "<ul>";
for(var j=0; j < newModel.files[i].warnings.length;j++){
status += "<li><font color=\"orange\">Warning: " + newModel.files[i].warnings[j] + "</font></li>";
}
status += "</ul>";
filesList.get(i).status=status;
break;
case 2:
filesList.get(i).status="<font color=\"red\"><b>Error: " + newModel.files[i].error + "</b></font>";
break;
case 3:
filesList.get(i).status="Ignored";
break;
}
}
}
You're already setting the height on the QML delegate element:
height: parent.height
It's binding the height to the parent height.
If you set the height with an expression, it'll be triggered (and re-evaluated) every time there's a change on any of the elements of the expression.
That's why the QML properties have a NOTIFY signal.
So if you want to bind the height to some other element you just need to assign it to the height attribute.
I haven't tried but the childrenRect may be what you're looking for: http://doc.qt.io/qt-5/qml-qtquick-item.html#childrenRect.height-prop
You can also use ternary operator to assign values to the property, i.e:
height: (model.get(styleData.row)[styleData.role] === 0)?30:100
Related
I have an object that I want to move from it's previously set position every time that particular state is set. I've tried making a separate property called xPos to get around the binding loop error which is set by the object's new position of x after the state is set, then entering a default state just to be able to switch back to that specific state again since calling the same state does nothing but it doesn't seem to work.
Here is a snippet of my code:
property int xPos: 0
states: [
State {
name: "nextStep"
PropertyChanges {
target: progressBar_Id
x: -1*(progressBar_Id.step.width) + xPos
}
},
State {
name: "previousStep"
PropertyChanges {
target: progressBar_Id
x: progressBar_Id.step.width + xPos
}
},
State {
name: "default"
PropertyChanges {
target: progressBar_Id
x: xPos
}
}
]
transitions: [
Transition {
from: "*"
to: "nextStep"
NumberAnimation {properties: "x"; easing.type: Easing.Linear; duration: 1000}
onRunningChanged: {
if(!running) {
xPos = progressBar_Id.x;
console.info("xPos = " + xPos);
state = "default";
}
}
},
Transition {
from: "*"
to: "previousStep"
NumberAnimation {properties: "x"; easing.type: Easing.Linear; duration: 1000}
onRunningChanged: {
if(!running) {
xPos = progressBar_Id.x;
console.info("xPos = " + xPos);
state = "default";
}
}
}
]
xPos seems to get set the first time from the console output but never applies the new xCoord to the object.
Explicitly specify the id of the item on which you wish to set the state, e.g:
idOfComponent.state = "default"
All QML Items and derivatives have a property called state, so the code needs to be more specific.
Actually came up with a much better alternative using a ListView.
ListView {
id: listView_Id
width: contentWidth
height: bubbleSize
anchors.centerIn: parent
layoutDirection: Qt.RightToLeft
orientation: ListView.Horizontal
spacing: 0
delegate: Item {
width: bubbleSize
height: width
ProgressBubble {
stateText: stateNumber
currentState: state
}
}
model: ListModel { id: progressModel_Id }
}
Another qml file:
progressModel.insert(0, {stateNumber: number++, state: "current"});
But I ran into another problem, is there anyway to change a state within a delegate after it's been added to the ListView?
I've already tried progressModel.set and progressModel.setProperty but doesn't seem to work.
state is a property for qml types, so when you are assigning "state" to "currentState" for "ProgressBubble", its taking the value of state in which " ProgressBubble" is currently present.
Rename "state" to something else like "presentState" and then try.
Moreover id of ListView model(progressModel_Id) and the one used to insert model elements(progressModel) in different file are different, both of them must refer to same id.
After these changes, you can try set property of model. Sample code snippet:
import QtQuick 2.0
Rectangle {
id: root
width: 360
height: 360
property int number: 1
ListView {
id: listView_Id
width: 100 //contentWidth // this creates property binding issue
height: 100 // bubbleSize // I was not sure about this value
anchors.centerIn: parent
layoutDirection: Qt.RightToLeft
orientation: ListView.Horizontal
spacing: 0
delegate: Item {
width: 100 // bubbleSize // I was not sure about this value
height: width
ProgressBubble {
state: "default"
stateText: stateNumber
currentState: presentState
MouseArea {
anchors.fill: parent
onClicked: {
progressModel_Id.set(index,{stateNumber: stateNumber, presentState: "not current"})
}
}
}
}
model: ListModel { id: progressModel_Id }
}
Component.onCompleted:
{
progressModel_Id.insert(0, {stateNumber: number++, presentState: "current"});
}
}
Inside the delegate, I bind Image's source property to ListView's currentIndex which determines which image to load. This works great:
ListView {
id: answerListView
model: 5
currentIndex: -1
delegate: answerDelegate
}
Component {
id: answerDelegate
Item {
width: 100
height: 100
Image {
source: answerListView.currentIndex === index
? "selected.png" : "not_selected.png"
}
MouseArea {
anchors.fill: parent
onClicked: {
answerListView.currentIndex = index
}
}
Component.onCompleted: {
answerListView.currentIndex = 1; // doesn't work!!
}
}
}
Since currentIndex: -1, it will always show not_selected.png. To show selected.png, I change currentIndex in Component.onLoaded inside delegate.
I was expecting image to load selected.png since currentIndex was updated.
What is the correct way and what am I misunderstanding here?
Ok, new guess:
You want to have the posibility to select multiple Items. As currentIndex only stores one value, which is the value you assigned it last, you can use it to mark only one Item.
Therefore you need to find another way to store your selection. You might for example have a property in the delegate: property bool selected: false which you set to true upon selection.
The problem with this solution is, that it only works if all Items are instantiated at all times. As soon as one Item will be destroyed again, the information will be lost, and uppon the next creation, the selection/unselection is undone.
The better way would be to introduce a role in your model, that stores the selection outside of the non-persistant delegates for you:
ListView {
id: answerListView
model: lm
delegate: answerDelegate
width: 100
height: 220
}
ListModel {
id: lm
ListElement { selected: false }
ListElement { selected: false }
ListElement { selected: false }
ListElement { selected: false }
ListElement { selected: false }
}
Component {
id: answerDelegate
Item {
width: 100
height: 100
Image {
anchors.fill: parent
source: model.selected ? "selected.png" : "notselected.png"
}
Text {
text: (model.selected ? 'selected ' : 'notselected ')
}
Component.onCompleted: {
model.selected = true // doesn't work!!
}
MouseArea {
anchors.fill: parent
onClicked: {
model.selected = !model.selected
}
}
}
}
Another option would probably be a ItemSelectionModel, but I don't know atm, how it works.
Otherwise your example works as expected:
The Item with index 1 is shown, and displays the Image selected.png. All other Items are not shown (for the ListView is to small) but if the would be shown, they would show notselected.png for the answerListView.currentIndex is not equal to their index.
I have a Flow layout where I add items dynamically on user actions. In the same way I remove these items on user actions. The Flow QML component seems to work as expected until an item is removed. The item itself is removed, but the space it occupied is just blank. My intuition tells me the graphical item itself got deleted, but the view is not updating when items are removed.
Is the dynamical deletion of child items outside the scope of the Flow Component? Is there any other layout that behaves equally? GridLayout seems to be the closest, but it does not automatically wrap child items when the layout is resized.
Is there any non-hack way to enable Flow to rearrange when child item is disabled? If not, and if GridLayout is my best shot, how to make it wrap its child items like Flow does?
The code below demonstrates what I want to achieve:
Item {
id: root
Flow {
id: layout
anchors.fill: parent
Loader { id: loader }
}
MouseArea {
anchors.top: parent.top
height: parent.height / 2
width: parent.width
onClicked: loader.source = "SomeQmlComponent.qml"
}
MouseArea {
anchors.bottom: parent.bottom
height: parent.height / 2
width: parent.width
onClicked: loader.source = ""
}
}
Don't use Loader inside Flow. In your case items are parented to Loader and not to Flow so you lose all advantage of that. In normal way items are added and removed w/o problem:
import QtQuick 2.7
import QtQuick.Window 2.2
Window {
width: 600
height: 600
visible: true
Component {
id: element
Rectangle {
width: Math.round(Math.random() * 100) + 50
height: Math.round(Math.random() * 100) + 50
color: Qt.rgba(Math.random(),Math.random(),Math.random(),1)
}
}
Flow {
id: flow
spacing: 2
anchors.fill: parent
add: Transition {
NumberAnimation { properties: "x,y"; easing.type: Easing.OutBack }
}
move: add
}
Timer {
id: timer
property bool is_add: true
interval: 300
repeat: true
running: true
onTriggered: {
if(timer.is_add) {
element.createObject(flow);
if(flow.children.length > 20) {
timer.is_add = false;
}
} else {
var item = flow.children[0];
item.destroy();
if(flow.children.length <= 1) {
timer.is_add = true;
}
}
}
}
}
#folibis - thanks for the answer, it helped me with a problem I was trying to solve which was to dynamically add elements and have them resize to the screen. I took your example and made the rectangles fill the width and the height is filled by the rectangles with their height being even. So the shrink/expand with the number of rectangles. I reduced it to 4 rectangles for simplicity and made it remove a rectangle at random.
Component {
id: element
Rectangle {
width: flow.width
height: flow.height/flow.children.length
color: Qt.rgba(Math.random(),Math.random(),Math.random(),1)
}
}
Flow {
id: flow
spacing: 2
anchors.fill: parent
add: Transition {
NumberAnimation { properties: "x,y"; easing.type: Easing.OutBack }
}
move: add
}
Timer {
id: timer
property bool is_add: true
interval: 1000
repeat: true
running: true
onTriggered: {
if(timer.is_add) {
element.createObject(flow);
if(flow.children.length > 3) {
timer.is_add = false;
}
} else {
var i = Math.floor(Math.random() * Math.floor(flow.children.length ));
console.log(i)
var item = flow.children[i];
item.destroy();
if(flow.children.length <= 1) {
timer.is_add = true;
}
}
}
}
I need to draw bunch of Rectangles in my application. The user should be able to select each of them individually and move them freely and change their position. The user also should be able to select multiple Rectangles and move all the selected one simultaneously and release them somewhere else.
I could already implement something based on Gridview that can handle selection and movement of one Rectangle, but I cannot make it work for multiple selection/movement. Here is a snippet of code I have currently:
GridView {
id: mainGrid
cellWidth: 7;
cellHeight: 7;
ListModel {
id: myModel
function createModel() {
for(var i = 0; i < totalZoneList.length; i++)
{
for (var j = 0; j < moduleZoneList.length; j++)
{
myModel.append({"item1": ITEM1, "item2": ITEM2})
}
}
}
Component.onCompleted: {createModel()}
}
Component {
id: myblocks
Item {
id: item
width: mainGrid.cellWidth;
height: mainGrid.cellHeight;
Rectangle {
id: box
parent: mainGrid
x: //CALCULATED BASED ON MODEL
y: //CALCULATED BASED ON MODEL
width: //CALCULATED BASED ON MODEL
height: //CALCULATED BASED ON MODEL
MouseArea {
id: gridArea
anchors.fill: parent
hoverEnabled: true
drag.axis: Drag.XandYAxis
drag.minimumX: 0
drag.minimumY: 0
property int mX: (mouseX < 0) ? 0 : ((mouseX < mainGrid.width - mainGrid.cellWidth) ? mouseX : mainGrid.width - mainGrid.cellWidth)
property int mY: (mouseY < 0) ? 0 : ((mouseY < mainGrid.height - mainGrid.cellHeight) ? mouseY : mainGrid.height - mainGrid.cellHeight)
property int index: parseInt(mX/mainGrid.cellWidth) + 5*parseInt(mY/mainGrid.cellHeight) //item underneath cursor
property int activeIndex
property var xWhenPressed
property var yWhenPressed
propagateComposedEvents: true
onPressed: {
activeIndex = index
drag.target = box
xWhenPressed = box.x
yWhenPressed = box.y
gridArea.drag.maximumX = mainGrid.width - box.width
gridArea.drag.maximumY = mainGrid.height - box.height
}
onReleased: {
if(xWhenPressed !== box.x || yWhenPressed !== box.y)
{
//RECALCULATE THE POSITION
}
}
onPositionChanged: {
if (drag.active && index !== -1 && index !== activeIndex) {
mainGrid.model.move(activeIndex, activeIndex = index, 1)
}
}
} // Mousearea
} // Rectangle
} // Item
} // Component
} //mainGrid
I didn't manage to have your code working. First, I see mistakes on it:
Rectangle {
id: box
parent: mainGrid
...
}
you just need to remove the parent Item which is of no use, and set the Rectangle as root of the delegate.
Then, you forgot to mention that the target of drag is the Rectangle
drag.target: parent
Here is the code after correcting:
Component {
id: myblocks
Rectangle {
id: box
color: "red"
width: 20
height: 20
MouseArea {
id: gridArea
anchors.fill: parent
drag.target: parent
hoverEnabled: true
drag.axis: Drag.XandYAxis
} // Mousearea
} // Rectangle
} // Component
Then, you shouldn't use a GridView because you want elements to be moved. If you use a Repeater it works, and you just have to set x and y in the Rectangle to place the elements at the beginning.
Now this is a solution for your problem: you click on an element to select it, and you can move all selected items at once.
Window {
visible: true
width: 640
height: 480
property var totalZoneList: ["total1", "total2"]
property var moduleZoneList: ["module1", "module2"]
Repeater{
id: iRepeater
model: ListModel {
id: myModel
function createModel() {
for(var i = 0; i < totalZoneList.length; i++)
{
for (var j = 0; j < moduleZoneList.length; j++)
{
myModel.append({"item1": totalZoneList[i], "item2": moduleZoneList[j]})
}
}
}
Component.onCompleted: {createModel()}
}
delegate: myblocks
}
Component {
id: myblocks
Rectangle {
id: box
color: {
switch(index){
case 0: selected ? "red" : "#FF9999";break;
case 1: selected ? "blue" : "lightblue";break;
case 2: selected ? "green" : "lightgreen";break;
case 3: selected ? "grey" : "lightgrey";break;
}
}
x: (width + 5)*index
width: 20
height: 20
property int offsetX:0
property int offsetY:0
property bool selected: false
function setRelative(pressedRect){
disableDrag();
x = Qt.binding(function (){ return pressedRect.x + offsetX; })
y = Qt.binding(function (){ return pressedRect.y + offsetY; })
}
function enableDrag(){
gridArea.drag.target = box
}
function disableDrag(){
gridArea.drag.target = null
}
MouseArea {
id: gridArea
anchors.fill: parent
hoverEnabled: true
drag.axis: Drag.XandYAxis
onClicked: parent.selected=!parent.selected
onPressed: {
var pressedRect = iRepeater.itemAt(index);
if (pressedRect.selected == true){
for (var i=0; i<iRepeater.count; i++ ){
var rect = iRepeater.itemAt(i);
if (i != index){
//init for breaking existing binding
rect.x = rect.x
rect.y = rect.y
rect.disableDrag()
if (rect.selected == true){
rect.offsetX = rect.x - pressedRect.x
rect.offsetY = rect.y - pressedRect.y
rect.setRelative(pressedRect)
}
}
}
pressedRect.enableDrag()
}
}
} // Mousearea
} // Rectangle
} // Component
}
Recipe One:
Use a Repeater, so the positioning is not determined by a view but by yourself.
Use an invisible helper Item. This is your drag.target.
Implement your prefered way of selecting your objects - weather by clicking, or drawing boxes and performing a check which objects are included in this box. Make the positions of all selected objects relative to your invisible helper object.
Drag the helper object, and move all other objects accordingly.
When finished, unselect the objects again and make their positions absolute (within the parents coordinate system)
Recipe Two:
Reparent all selected Objects to a new Item, while mapping their coordinates accordingly
Move the new Item
Reparen all objects back to the original canvas, mapping their coordinates back.
I hope this is sufficient to solve your problem (as far as I understood it)
If it solves another problem, than you need to be more specific about the expected behavior of your draged objects.
I'm trying to make custom component for editable tables in QML, like this:
// BaseTableView.qml
import QtQuick 2.3
import QtQuick.Controls 1.2
Item {
signal addActionPerformed()
signal editActionPerformed(int id)
signal deleteActionPerformed(int id)
property var model
ToolBar {
id: toolBar
anchors.left: parent.left
anchors.right: parent.right
Row {
ToolButton {
id: addButton
iconSource: "qrc:/icons/actions/add.png"
onClicked: addActionPerformed()
}
ToolButton {
id: editButton
enabled: false
iconSource: "qrc:/icons/actions/edit.png"
}
ToolButton {
id: deleteButton
enabled: false
iconSource: "qrc:/icons/actions/delete.png"
}
}
}
TableView {
id: tableView
model: parent.model
anchors.left: parent.left
anchors.right: parent.right
anchors.top: toolBar.bottom
anchors.bottom: parent.bottom
onCurrentRowChanged: {
editButton.enabled = currentRow !== null
deleteButton.enabled = currentRow !== null
}
}
}
and use this component in another file like this:
// Another.qml file
import QtQuick 2.3
import QtQuick.Controls 1.2
import "../common" // Here is BaseTableView.qml
BaseTableView {
TableViewColumn {
role: "id"
title: qsTr("Id")
}
TableViewColumn {
role: "object_expression"
title: qsTr("Expression")
}
}
So, problem is how i can pass table view columns from usage to underlying TableView?
I've tried to make property list in BaseTableView and assign a list of objects to this property in Aother.qml? but unsuccessfully.
Use default properties:
An object definition can have a single default property. A default property is the property to which a value is assigned if an object is declared within another object's definition without declaring it as a value for a particular property.
More relevant for your scenario:
You will notice that child objects can be added to any Item-based type without explicitly adding them to the children property. This is because the default property of Item is its data property, and any items added to this list for an Item are automatically added to its list of children.
Default properties can be useful for reassigning the children of an item. See the TabWidget Example, which uses a default property to automatically reassign children of the TabWidget as children of an inner ListView.
If you take a look at the TabWidget example that the last paragraph refers to, you should have all you need:
import QtQuick 2.0
Item {
id: tabWidget
// Setting the default property to stack.children means any child items
// of the TabWidget are actually added to the 'stack' item's children.
// See the "Property Binding"
// documentation for details on default properties.
default property alias content: stack.children
property int current: 0
onCurrentChanged: setOpacities()
Component.onCompleted: setOpacities()
function setOpacities() {
for (var i = 0; i < stack.children.length; ++i) {
stack.children[i].opacity = (i == current ? 1 : 0)
}
}
Row {
id: header
Repeater {
model: stack.children.length
delegate: Rectangle {
width: tabWidget.width / stack.children.length; height: 36
Rectangle {
width: parent.width; height: 1
anchors { bottom: parent.bottom; bottomMargin: 1 }
color: "#acb2c2"
}
BorderImage {
anchors { fill: parent; leftMargin: 2; topMargin: 5; rightMargin: 1 }
border { left: 7; right: 7 }
source: "tab.png"
visible: tabWidget.current == index
}
Text {
horizontalAlignment: Qt.AlignHCenter; verticalAlignment: Qt.AlignVCenter
anchors.fill: parent
text: stack.children[index].title
elide: Text.ElideRight
font.bold: tabWidget.current == index
}
MouseArea {
anchors.fill: parent
onClicked: tabWidget.current = index
}
}
}
}
Item {
id: stack
width: tabWidget.width
anchors.top: header.bottom; anchors.bottom: tabWidget.bottom
}
}
In cases like this, where you want to replicate something that is done by an item offered by Qt, it can also be helpful to take a look at the source code of what you're trying to replicate. However, the documentation is a bit easier to read. :)