Solved Rectangle::contains() doesn't reflect radius
-
Hi,
I have several circular "Buttons" (here: Rectangle{}s with radius of half their width, named "key1", .., "key4") that are part of a MultiPointTouchArea. I need them being able to detect being pressed simultaneously (it's gonna be a fingering trainer for brass instruments). So I detect the TouchPoints and check them for matches with my Rectangles using their item::contains() method.
Unfortunately, contains() returns true within the whole Rectangle's area, where it should return false in the corners (where "radius: width / 2" has clipped the Rectangle).
Trying to set a containmentMask would bring me back to the lack of a genuinely circular item in qml.
Any ideas? Here's my code so far:
{ id: touchArea property bool pressed1: point1.pressed1 || point2.pressed1 || point3.pressed1 || point4.pressed1 property bool pressed2: point1.pressed2 || point2.pressed2 || point3.pressed2 || point4.pressed2 property bool pressed3: point1.pressed3 || point2.pressed3 || point3.pressed3 || point4.pressed3 property bool pressed4: point1.pressed4 || point2.pressed4 || point3.pressed4 || point4.pressed4 anchors.fill: parent touchPoints: [ TouchPoint { id: point1 property bool pressed1: point1.pressed && key1.contains(Qt.point(point1.x-key1.x, point1.y-key1.y)) property bool pressed2: point1.pressed && key2.contains(Qt.point(point1.x-key2.x, point1.y-key2.y)) property bool pressed3: point1.pressed && key3.contains(Qt.point(point1.x-key3.x, point1.y-key3.y)) property bool pressed4: point1.pressed && key4.contains(Qt.point(point1.x-key4.x, point1.y-key4.y)) }, TouchPoint { id: point2 property bool pressed1: point2.pressed && key1.contains(Qt.point(point2.x-key1.x, point2.y-key1.y)) property bool pressed2: point2.pressed && key2.contains(Qt.point(point2.x-key2.x, point2.y-key2.y)) property bool pressed3: point2.pressed && key3.contains(Qt.point(point2.x-key3.x, point2.y-key3.y)) property bool pressed4: point2.pressed && key4.contains(Qt.point(point2.x-key4.x, point2.y-key4.y)) }, TouchPoint { id: point3 property bool pressed1: point3.pressed && key1.contains(Qt.point(point3.x-key1.x, point3.y-key1.y)) property bool pressed2: point3.pressed && key2.contains(Qt.point(point3.x-key2.x, point3.y-key2.y)) property bool pressed3: point3.pressed && key3.contains(Qt.point(point3.x-key3.x, point3.y-key3.y)) property bool pressed4: point3.pressed && key4.contains(Qt.point(point3.x-key4.x, point3.y-key4.y)) }, TouchPoint { id: point4 property bool pressed1: point4.pressed && key1.contains(Qt.point(point4.x-key1.x, point4.y-key1.y)) property bool pressed2: point4.pressed && key2.contains(Qt.point(point4.x-key2.x, point4.y-key2.y)) property bool pressed3: point4.pressed && key3.contains(Qt.point(point4.x-key3.x, point4.y-key3.y)) property bool pressed4: point4.pressed && key4.contains(Qt.point(point4.x-key4.x, point4.y-key4.y)) } ] Row { anchors.fill: parent Rectangle { id: key1 width: Math.min(parent.width / appwin.noOfKeys, parent.height) height: width radius: width / 2 color: touchArea.pressed1 ? "red" : "yellow" } Rectangle { id: key2 width: Math.min(parent.width / appwin.noOfKeys, parent.height) height: width radius: width / 2 color: touchArea.pressed2 ? "red" : "yellow" } Rectangle { id: key3 width: Math.min(parent.width / appwin.noOfKeys, parent.height) height: width radius: width / 2 color: touchArea.pressed3 ? "red" : "yellow" containmentMask: Rectangle { anchors.fill: key3 radius: width/2 color: "green" } } Rectangle { visible: appwin.noOfKeys === 4 id: key4 width: Math.min(parent.width / appwin.noOfKeys, parent.height) height: width radius: width / 2 color: touchArea.pressed4 ? "red" : "yellow" } } }
Kind regards,
Sebastian! -
Query the parent item for the child key at each relevant point. If it exists, map the point to the key, and check if the resulting point is close enough to the key's center.
import QtQuick 2.15 import QtQuick.Window 2.15 Window { id: window width: 640 height: 480 visible: true MultiPointTouchArea { id: touchArea anchors.fill: parent onTouchUpdated: { var touched = new Set() for (var i in touchPoints) { var item = window.contentItem.childAt(touchPoints[i].x, touchPoints[i].y); if (!item) return; var mapped = touchArea.mapToItem(item, touchPoints[i].x, touchPoints[i].y); var distance = Math.sqrt((mapped.x - item.width/2)**2 + (mapped.y - item.height/2)**2); if (distance < item.width / 2) touched.add(item); } for (i in window.contentItem.children) { if (window.contentItem.children[i] === touchArea) continue; else if (touched.has(window.contentItem.children[i])) window.contentItem.children[i].touched = true; else window.contentItem.children[i].touched = false; } } } Rectangle { property bool touched: false width: Math.min(parent.width/2, parent.height) height: width radius: width / 2 border.width: 1 border.color: "black" color: touched ? "red" : "yellow" anchors.verticalCenter: parent.verticalCented anchors.right: parent.horizontalCenter } Rectangle { property bool touched: false width: Math.min(parent.width/2, parent.height) height: width radius: width / 2 border.width: 1 border.color: "black" color: touched ? "red" : "yellow" anchors.verticalCenter: parent.verticalCented anchors.left: parent.horizontalCenter } }
-
Query the parent item for the child key at each relevant point. If it exists, map the point to the key, and check if the resulting point is close enough to the key's center.
import QtQuick 2.15 import QtQuick.Window 2.15 Window { id: window width: 640 height: 480 visible: true MultiPointTouchArea { id: touchArea anchors.fill: parent onTouchUpdated: { var touched = new Set() for (var i in touchPoints) { var item = window.contentItem.childAt(touchPoints[i].x, touchPoints[i].y); if (!item) return; var mapped = touchArea.mapToItem(item, touchPoints[i].x, touchPoints[i].y); var distance = Math.sqrt((mapped.x - item.width/2)**2 + (mapped.y - item.height/2)**2); if (distance < item.width / 2) touched.add(item); } for (i in window.contentItem.children) { if (window.contentItem.children[i] === touchArea) continue; else if (touched.has(window.contentItem.children[i])) window.contentItem.children[i].touched = true; else window.contentItem.children[i].touched = false; } } } Rectangle { property bool touched: false width: Math.min(parent.width/2, parent.height) height: width radius: width / 2 border.width: 1 border.color: "black" color: touched ? "red" : "yellow" anchors.verticalCenter: parent.verticalCented anchors.right: parent.horizontalCenter } Rectangle { property bool touched: false width: Math.min(parent.width/2, parent.height) height: width radius: width / 2 border.width: 1 border.color: "black" color: touched ? "red" : "yellow" anchors.verticalCenter: parent.verticalCented anchors.left: parent.horizontalCenter } }
-
What a beautifully crafted solution. I've learned so many details from it (basics like the "in"-syntax in for-loops in JS or using a set()) ! Thanks a million!
Here's my (slightly adapted) working code:
MultiPointTouchArea { id: touchArea anchors.fill: parent onTouchUpdated: { var touched = new Set() for (var i in touchPoints) { var item = keyRow.childAt(touchPoints[i].x, touchPoints[i].y); if (!item) { continue; } var mapped = touchArea.mapToItem(item, touchPoints[i].x, touchPoints[i].y); var distance = Math.sqrt((mapped.x - item.width/2)**2 + (mapped.y - item.height/2)**2); if (distance < item.width / 2) { touched.add(item); } } for (i in keyRow.children) { if (keyRow.children[i].objectName.substr(0,3) === "key") { if (touched.has(keyRow.children[i])) { keyRow.children[i].touched = true; } else { keyRow.children[i].touched = false; } } } } Row { anchors.fill: parent id: keyRow Rectangle { id: key1 objectName: "key1" property bool touched: false width: Math.min(parent.width / appwin.noOfKeys, parent.height) height: width radius: width / 2 color: touched ? "red" : "yellow" } Rectangle { id: key2 objectName: "key2" property bool touched: false width: Math.min(parent.width / appwin.noOfKeys, parent.height) height: width radius: width / 2 color: touched ? "red" : "yellow" } Rectangle { id: key3 objectName: "key3" property bool touched: false width: Math.min(parent.width / appwin.noOfKeys, parent.height) height: width radius: width / 2 color: touched ? "red" : "yellow" } Rectangle { id: key4 objectName: "key4" visible: appwin.noOfKeys === 4 property bool touched: false width: Math.min(parent.width / appwin.noOfKeys, parent.height) height: width radius: width / 2 color: touched ? "red" : "yellow" } } }
Brilliant. Cheers!!
-
Happy to hear it.
If you expand from brass to something with more keys, a piano for example, it's probably more efficient to track the previously pressed set of keys rather than iterating through the entire set.