Culling items that are outside the visible area - qt

From the docs:
The default renderer does not do any CPU-side viewport clipping nor occlusion detection. If something is not supposed to be visible, it should not be shown. Use Item::visible: false for items that should not be drawn. The primary reason for not adding such logic is that it adds additional cost which would also hurt applications that took care in behaving well.
So is there a trick to do it easily, without implementing it myself?
Note that in my case the items that are outside the visible area are there because they are in a ScrollView and they are not scrolled-to.
The reason I want culling is to reduce CPU usage for full-scene redraws.

Here is a trivial example you can extend upon:
Window {
visible: true
width: 640
height: 480
Rectangle {
anchors.centerIn: parent
width: 200
height: 200
color: "yellow"
Flickable {
id: view
anchors.fill: parent
contentWidth: 200
contentHeight: col.height
property real span : contentY + height
Column {
id: col
x: 90
spacing: 2
Repeater {
model: 50
delegate: Rectangle {
width: 10
height: 10
color: inView ? "blue" : "red"
property bool inView: y > view.contentY && y < view.span
}
}
}
}
}
}
Obviously, a full-proof solution would also include the item's height in the calculation. You can also do the check in the x axis if necessary.

To add to dtech's answer, I just learned that there are QML components, such as GridView and ListView, that do culling automatically.

Related

Proper way of defining and composing custom elements in QML

Lets say I want to create a field element in QML which has its "field" part and could have icon on the right, left side or not show it at all. Basic qml code would look like this (without functionality to change sides):
// TestField.qml
Item
{
Image
{
anchors.right: field.left
anchors.top: field.top
source: "qrc:/res/settings/logOptions.svg"
height: 40
width: 40
}
Rectangle
{
id: field
anchors.fill: parent
color: "blue"
}
}
Then I will use it like this:
// Main.qml
TestField
{
x: 100
y: 100
height: 100
width: 100
}
Or use it in some grid layout, anchor it to another element, etc. I used hardcoded coordinates and sizes just to keep the question simpler.
The problem with this is that I specified sizes for the element (100x100) and my field rectangle would fill the whole space, leaving icon "hanging" out of bounds of this element. In this case QML will treat this element as of the size 100x100, but since the icon is out of bounds the actual size of the whole element with the icon is bigger (140x100) and could overlap with other such elements when placed into layout.
Worth noting that for such elements I want to be able to change position of the icon to be to the left or right of the rectangle (it will be some sort of property with enum value, that I will define in code, it won't change dynamically throughout application work) so some rectangles will have icon from different sides.
One solution I see is to rework TestField so its elements will be tied to parents width or height, for example:
Item
{
Image
{
x: 0
y: 0
width: parent.width / 2
height: parent.width / 2
}
Rectangle
{
id: field
x: parent.width / 2
y: 0
width: parent.width / 2
height: parent.height
}
}
The question is - is this a good solutions or there are proper ways to tackle such issue? My main concern is that element size should be actually the size that I could query with element.width or element.height and none of the internal part are out of bounds possibly overlapping with other elements. Maybe I'm missing some basic concept that will allow me to make elements that always keep their parts inside its bounds?
The problem with your sizing is that your Rectangle takes up the whole size of its parent. You want to shrink that to account for the size of the Image. I also added a way to switch the image between left and right side.
Item
{
property bool imageOnLeft: true
Image
{
id: img
anchors.left: imageOnLeft ? parent.left : field.right
source: "qrc:/res/settings/logOptions.svg"
height: 40
width: 40
}
Rectangle
{
id: field
anchors.left: imageOnLeft ? img.right : parent.left
width: parent.width - img.width
height: parent.height
color: "blue"
}
}

TestCase mouseDrag only clicks item inside Flickable but does not drag

In a QML TestCase, I'm trying to setup automatic scrolling of a ListView that is contained inside a Flickable (to add a custom footer that can be flicked into view, which wouldn't happen with just ListView { footer: Component {} })
However, the mouseDrag only seems to click the correct coordinate, but not drag it to any direction. Here is a simplified version that is as close to the real one as possible:
Implementation.qml
import QtQuick 2.5
FocusScope {
width: 1920
height: 1080
Flickable {
objectName: 'flickableList'
boundsBehavior: Flickable.StopAtBounds
clip: true
width: parent.width
height: 240
contentHeight: 500
ListView {
interactive: false
height: parent.height
width: parent.width
model: ['example1', 'example2', 'example3', 'example4', 'example5']
delegate: Item {
width: 300
height: 100
Text {
text: modelData
}
}
}
}
Item {
id: footer
height: 100
width: parent.width
}
}
TheTest.qml
// The relevant part
var theList = findChild(getView(), 'flickableList')
var startY = 220
var endY = 20
mouseDrag(theList, 100, startY, 100, endY, Qt.LeftButton, Qt.NoModifier, 100)
So, when I'm viewing the UI testrunner, I can see it clearly click on the correct delegate (it has a focus highlight in the actual implementation), ie. the third item "example3", which starts at Y 200 and ends at Y 300). But the drag event never happens. Nothing moves on the screen, and compare(theList.contentY, 200) says it is still at position 0. I would expect it to be at 200, since the mouse is supposed to be mouseDragging from position 220 to 20, ie. scrolling the list down by 200. And 220 is also within the visible height (240).
Just to be sure, I also reversed the Y values, but also no movement:
var theList = findChild(getView(), 'flickableList')
var startY = 20
var endY = 220
mouseDrag(theList, 100, startY, 100, endY, Qt.LeftButton, Qt.NoModifier, 100)
Also, as the 3rd item clearly is clicked on (it gets highlighted), the passed item theList (= the Flickable), should be valid.
Edit:
Oh, and this does scroll the list, but it goes all the way to the bottom of the list (388 px down in the actual implementation, even when the delta is just 30 pixels):
mousePress(theList, startX, startY)
mouseMove(theList, endX, endY)
mouseRelease(theList, endX, endY)
So the question is:
Does mouseDrag only work for specific types of components (ie. does not work on Flickable?), or is there something missing? How can I get it to scroll the list down? Thanks!
Your tag says you're using Qt 5.5 - I would recommend trying Qt 5.14 if possible, as there was a fix that might help:
mouseDrag(): ensure that intermediate moves are done for all drags
[...]
In practice, this means that mouseDrag() never did intermediate moves
(i.e. what happens during a drag in real life) for drags that go from
right to left or upwards.
https://codereview.qt-project.org/c/qt/qtdeclarative/+/281903
If that doesn't help, or upgrading is not an option, I would recommend looking at Qt's own tests (although they are written in C++):
https://code.qt.io/cgit/qt/qtdeclarative.git/tree/tests/auto/quick/qquickflickable/tst_qquickflickable.cpp#n1150
I think mouseDrag only works for mouse area. You could wrap every object with that.
But in the end, you need to use a mouse area inside you delegate and Drag and Drop it.
https://doc.qt.io/qt-5/qml-qtquick-drag.html
import QtQuick 2.5
FocusScope {
width: 1920
height: 1080
Flickable {
objectName: 'flickableList'
boundsBehavior: Flickable.StopAtBounds
clip: true
width: parent.width
height: 240
contentHeight: 500
ListView {
interactive: false
height: parent.height
width: parent.width
model: ['example1', 'example2', 'example3', 'example4', 'example5']
delegate: DelegateList{
textAreaText = modelData
}
}
}
Item {
id: footer
height: 100
width: parent.width
}
}
And the DelegateList.qml
Item {
id: root
property alias textAreaText: textArea.text
width: 300
height: 100
Text {
id: textArea
}
Drag.active: dragArea.drag.active
Drag.hotSpot.x: 10
Drag.hotSpot.y: 10
MouseArea {
id: dragArea
anchors.fill: parent
drag.target: parent
}
}

Qt/QML - Positioner property not attaching to model delegate

I'm using Qt Creator 4.6 on Linux. My problem can be reduced to the setup that is essentially the example provided in the documentation with some small changes:
https://doc.qt.io/qt-5/qml-qtquick-positioner.html
If I run the below code it draws some boxes, and clicking on each box should output its index. The code below works correctly for me as written. However, if I comment out TEXT 1 and uncomment TEXT 2, then clicking on the boxes outputs -1 for every box.
It seems like I have to use the Positioner in some way before the MouseArea or it won't work correctly (it can be used in the Text item or you can use it at the Rectangle level). I'm guessing it has something to with the MouseArea not being fully resolved until the actual click, and maybe if the compiler feels like nothing uses the Positioner it doesn't create it?
Is this behavior expected, and if so is it documented anywhere? In my real use case the workaround is to just use the Positioner to assign an index to an unused property or variable at the parent level so it's not too big a deal, but I'd like to understand it.
Window {
visible: true
width: 640
height: 480
Grid {
Repeater {
model: 16
Rectangle {
id: rect
width: 30; height: 30
border.width: 1
//color: Positioner.isFirstItem ? "yellow" : "lightsteelblue"
color: "green"
Text {
text: rect.Positioner.index //TEXT 1
//text: "test" //TEXT 2
}
MouseArea {
id: dragArea
anchors.fill: parent
onPressed: {console.log(rect.Positioner.index)}
}
}
}
}
}

How can I get the drop effect of a DragArea to animate towards the DropArea that received it?

Hope this makes some sense as a question. In my app, I have a DragArea defined which I use to start dragging things over top of various Rectangles that each contain a DropArea. Everything is working fine in my code except for a cosmetic effect that I would like to change.
In QML, when you start dragging from a DragArea and eventually drop, the animation effect is such that the thing you're dragging animates (while fading out) back to the spot from which you started dragging. This happens even when you drop over a DropArea that successfully captures the drop.
What I would like to do is have the drop effect animate towards the DropArea that received the drop - so that it appears I am dragging-and-dropping things into the Rectangle. Is there any way to do this?
I'm guessing that this in some way involves the .source and .target properties of these areas, but no luck so far in having any effect on where the drop animation goes.
By default, QML will give you no cosmetic behavior for drag and drop whatsoever. The drag target will begin at the drag start location, and will end wherever it is dropped, regardless of whether the drag is accepted or not.
Thus I assume the behavior you describe is implemented in your user code, which you have not disclosed. Regardless, what you want to do is quite easy, it involves tracking the position the drag originates at and it ends at, so you can use the two coordinates to animate the position.
In the following example the red rectangle can be dragged, and if dropped outside of a drop area it will animate from its current to its initial position, whereas if dropped in the yellow rectangle, it will animate from its initial to its drop position.
Window {
width: 600
height: 600
visible: true
Rectangle {
width: 200
height: 200
color: "yellow"
DropArea {
anchors.fill: parent
onEntered: drag.source.accepted = true
onExited: drag.source.accepted = false
}
}
Rectangle {
id: rect
width: 50
height: 50
color: "red"
x: parent.width * 0.5
y: parent.height * 0.5
Drag.active: mouseArea.drag.active
property point begin
property point end
property bool accepted : false
MouseArea {
id: mouseArea
anchors.fill: parent
drag.target: parent
onPressed: rect.begin = Qt.point(rect.x, rect.y)
onReleased: {
rect.end = Qt.point(rect.x, rect.y)
aX.from = rect.accepted ? rect.begin.x : rect.end.x
aX.to = rect.accepted ? rect.end.x : rect.begin.x
aY.from = rect.accepted ? rect.begin.y : rect.end.y
aY.to = rect.accepted ? rect.end.y : rect.begin.y
anim.start()
}
ParallelAnimation {
id: anim
NumberAnimation { id: aX; target: rect; property: "x"; duration: 200 }
NumberAnimation { id: aY; target: rect; property: "y"; duration: 200 }
}
}
}
}

Building TabBar in QML - Loader doesn't show all the Rectangles

import QtQuick 2.4
import QtQuick.Window 2.2
Window
{
visible: true
height: 500
width: 500
property VisualItemModel contentToBeShownOnTabClick : visualItemModelDemo
property variant tabLabels : ["Navigation", "Payload", "System Control"]
VisualItemModel
{
id: visualItemModelDemo
Rectangle
{
id: navigationTab
color: "green"
height: 200
width: 200
}
Rectangle
{
id: navigationTab1
color: "darkgreen"
height: 200
width: 200
}
Rectangle
{
id: navigationTab2
color: "lightgreen"
height: 200
width: 200
}
}
MainForm
{
Component
{
id: tabsOnBottomComponent
Repeater
{
model: tabLabels
// The Tabs
Rectangle
{
id: tabsOnBottom
// This anchoring places the tabs on the outer top of the parent rectangle.
anchors.top: parent.bottom
anchors.topMargin: 180
color: "lightsteelblue"
border.color: "steelblue"
border.width: 2
implicitWidth: Math.max ((labelTabsBottom.width + 4), 80)
implicitHeight: 20
radius: 2
// Tabs Text/Label
Text
{
id: labelTabsBottom
anchors.centerIn: parent
color: "white"
rotation: 0
// With reference to mode: tabLabels
text: modelData
font.pointSize: 11
}
MouseArea
{
anchors.fill: parent
onClicked: bottomTabClicked (index);
}
}
}
}
Rectangle
{
// The things which get displayed on clicking of a tab will be shown in this rectangle.
id: areaForTabContents
border.color: "black"
border.width: 10
height: parent.height
width : parent.width
color : "pink"
// These are the tabs displayed in one row - horizontally.
Row
{
id: horizontalTabs
Loader
{
anchors.fill: parent
sourceComponent: tabsOnBottomComponent
}
}
}
anchors.fill: parent
}
}
This gets shown as follows:
whereas I want it to see 3 rectangles there side by side.
Loader is not a transparent type w.r.t. the containing type, Row in this case. I think this is an issue related to creation context and the way Repeater works. From the documentation of the latter:
Items instantiated by the Repeater are inserted, in order, as children of the Repeater's parent. The insertion starts immediately after the Repeater's position in its parent stacking list. This allows a Repeater to be used inside a layout.
The Rectangles are indeed added to the parent which is the Loader, they stack up - Loader does not provide a positioning policy - then they are added to the Row resulting in just one Item (the last one) to be visible.
You can tackle the problem with few different approaches, depending on the properties you want to maintain or not. I would get rid of anchoring in the Component and move it to the containing Row. A too specific anchoring inside a Component could be a pain in the neck when it is instanced and used all over a (not so small) project.
As a first approach you can re-parent the Repeater to the Row, i.e. you can rewrite code as:
Row
{
id: horizontalTabs
Loader
{
sourceComponent: tabsOnBottomComponent
onLoaded: item.parent = horizontalTabs
}
}
However this would result in warnings due to the Component anchoring references not working as expected any more.
If you still want to maintain the anchoring, as defined in the Component, and off-load the creation, you can go for the dynamic way (if the semantics fits in your use case), i.e. you can use createObject. This way you totally avoid the Loader and the related issue. For instance, you can create the Repeater once the Row has completed its creation:
Row
{
id: horizontalTabs
Component.onCompleted: tabsOnBottomComponent.createObject(horizontalTabs)
}
Clearly, the creation code can be move anywhere else, depending on your needs.

Resources