How to create a round mouse area in QML - qt

I have a basic custom button using a Rectangle with radius: width/2. Now I add a MouseArea to my button. However the MouseArea has a squared shape. That means the click event is also triggered when I click slightly outside the round button, i.e. in the corners of the imaginary square around the round button. Can I somehow make also the MouseArea round?
import QtQuick 2.7
import QtQuick.Window 2.2
Window {
visible: true
width: 640
height: 480
title: qsTr("TestApp")
Rectangle {
id: background
anchors.fill: parent
color: Qt.rgba(0.25, 0.25, 0.25, 1);
Rectangle {
id: button
width: 64
height: 64
color: "transparent"
anchors.centerIn: parent
radius: 32
border.width: 4
border.color: "grey"
MouseArea {
anchors.fill: parent
onPressed: button.color = "red";
onReleased: button.color = "transparent";
}
}
}
}

Stealing code from PieMenu, here's RoundMouseArea.qml:
import QtQuick 2.0
Item {
id: roundMouseArea
property alias mouseX: mouseArea.mouseX
property alias mouseY: mouseArea.mouseY
property bool containsMouse: {
var x1 = width / 2;
var y1 = height / 2;
var x2 = mouseX;
var y2 = mouseY;
var distanceFromCenter = Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2);
var radiusSquared = Math.pow(Math.min(width, height) / 2, 2);
var isWithinOurRadius = distanceFromCenter < radiusSquared;
return isWithinOurRadius;
}
readonly property bool pressed: containsMouse && mouseArea.pressed
signal clicked
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: if (roundMouseArea.containsMouse) roundMouseArea.clicked()
}
}
You can use it like this:
import QtQuick 2.5
import QtQuick.Window 2.2
Window {
width: 640
height: 480
visible: true
RoundMouseArea {
id: roundMouseArea
width: 100
height: 100
anchors.centerIn: parent
onClicked: print("clicked")
// Show the boundary of the area and whether or not it's hovered.
Rectangle {
color: roundMouseArea.pressed ? "red" : (roundMouseArea.containsMouse ? "darkorange" : "transparent")
border.color: "darkorange"
radius: width / 2
anchors.fill: parent
}
}
}

Another option is a C++/QML way as decribed in this example. This example provides a way to use masks of any shapes. It can be customized to fit your needs.
Posting the code as it is:
maskedmousearea.cpp
MaskedMouseArea::MaskedMouseArea(QQuickItem *parent)
: QQuickItem(parent),
m_pressed(false),
m_alphaThreshold(0.0),
m_containsMouse(false)
{
setAcceptHoverEvents(true);
setAcceptedMouseButtons(Qt::LeftButton);
}
void MaskedMouseArea::setPressed(bool pressed)
{
if (m_pressed != pressed) {
m_pressed = pressed;
emit pressedChanged();
}
}
void MaskedMouseArea::setContainsMouse(bool containsMouse)
{
if (m_containsMouse != containsMouse) {
m_containsMouse = containsMouse;
emit containsMouseChanged();
}
}
void MaskedMouseArea::setMaskSource(const QUrl &source)
{
if (m_maskSource != source) {
m_maskSource = source;
m_maskImage = QImage(QQmlFile::urlToLocalFileOrQrc(source));
emit maskSourceChanged();
}
}
void MaskedMouseArea::setAlphaThreshold(qreal threshold)
{
if (m_alphaThreshold != threshold) {
m_alphaThreshold = threshold;
emit alphaThresholdChanged();
}
}
bool MaskedMouseArea::contains(const QPointF &point) const
{
if (!QQuickItem::contains(point) || m_maskImage.isNull())
return false;
QPoint p = point.toPoint();
if (p.x() < 0 || p.x() >= m_maskImage.width() ||
p.y() < 0 || p.y() >= m_maskImage.height())
return false;
qreal r = qBound<int>(0, m_alphaThreshold * 255, 255);
return qAlpha(m_maskImage.pixel(p)) > r;
}
void MaskedMouseArea::mousePressEvent(QMouseEvent *event)
{
setPressed(true);
m_pressPoint = event->pos();
emit pressed();
}
void MaskedMouseArea::mouseReleaseEvent(QMouseEvent *event)
{
setPressed(false);
emit released();
const int threshold = qApp->styleHints()->startDragDistance();
const bool isClick = (threshold >= qAbs(event->x() - m_pressPoint.x()) &&
threshold >= qAbs(event->y() - m_pressPoint.y()));
if (isClick)
emit clicked();
}
void MaskedMouseArea::mouseUngrabEvent()
{
setPressed(false);
emit canceled();
}
void MaskedMouseArea::hoverEnterEvent(QHoverEvent *event)
{
Q_UNUSED(event);
setContainsMouse(true);
}
void MaskedMouseArea::hoverLeaveEvent(QHoverEvent *event)
{
Q_UNUSED(event);
setContainsMouse(false);
}
Usage in QML:
import Example 1.0
MaskedMouseArea {
id: moonArea
anchors.fill: parent
alphaThreshold: 0.4
maskSource: moon.source
}
Register the custom item:
qmlRegisterType<MaskedMouseArea>("Example", 1, 0, "MaskedMouseArea");

Thanks to #Mitch. Sometimes such mousearea says it contains mouse after leaving it, so I've added "if(!mouseArea.containsMouse) return false;" to the beginning of the "containsMouse" property:
property bool containsMouse: {
if(!mouseArea.containsMouse)
return false;
var x1 = width / 2;
var y1 = height / 2;
var x2 = mouseX;
var y2 = mouseY;
var distanceFromCenter = Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2);
var radiusSquared = Math.pow(Math.min(width, height) / 2, 2);
var isWithinOurRadius = distanceFromCenter < radiusSquared;
return isWithinOurRadius;
}

Related

How can I use a MouseArea on a ShapePath in QML?

I'm currently learning how to use the Shapes in QML to draw more advanced components. I'm trying to create a button which looks like this :
When I try to apply a MouseArea over the Shape component, the MouseArea does not seem to be able to catch the events on the Shape. Here is my code :
import QtQuick 2.13
import QtQuick.Shapes 1.13
Item
{
Shape
{
id: myShape
ShapePath {
id: myButton
strokeWidth:3.114000082015991
strokeColor: "#000"
miterLimit:7
fillColor: "#ccc"
capStyle:ShapePath.RoundCap
PathSvg {
path: "M392.4,205.9a132.34,132.34,0,0,1,31.7,49.2H575.6a289.67,289.67,0,0,0-12.9-49.2Z"
}
}
}
MouseArea
{
id: myMouseArea
anchors.fill: myShape
enabled: true
hoverEnabled: true
onEntered: myButton.fillColor = "yellow"
onExited: myButton.fillColor = "green"
}
}
So my question is : is it possible to make a Shape/ShapePath clickable in the first place ? And if yes, how to do so ?
A similar question was asked here, but they just wanted a simple circle. Still, the non-accepted answer describes a masked mouse area that could be useful to you. It uses an image to define a masked area. It originally comes from a Qt example program.
maskedmousearea.cpp
MaskedMouseArea::MaskedMouseArea(QQuickItem *parent)
: QQuickItem(parent),
m_pressed(false),
m_alphaThreshold(0.0),
m_containsMouse(false)
{
setAcceptHoverEvents(true);
setAcceptedMouseButtons(Qt::LeftButton);
}
void MaskedMouseArea::setPressed(bool pressed)
{
if (m_pressed != pressed) {
m_pressed = pressed;
emit pressedChanged();
}
}
void MaskedMouseArea::setContainsMouse(bool containsMouse)
{
if (m_containsMouse != containsMouse) {
m_containsMouse = containsMouse;
emit containsMouseChanged();
}
}
void MaskedMouseArea::setMaskSource(const QUrl &source)
{
if (m_maskSource != source) {
m_maskSource = source;
m_maskImage = QImage(QQmlFile::urlToLocalFileOrQrc(source));
emit maskSourceChanged();
}
}
void MaskedMouseArea::setAlphaThreshold(qreal threshold)
{
if (m_alphaThreshold != threshold) {
m_alphaThreshold = threshold;
emit alphaThresholdChanged();
}
}
bool MaskedMouseArea::contains(const QPointF &point) const
{
if (!QQuickItem::contains(point) || m_maskImage.isNull())
return false;
QPoint p = point.toPoint();
if (p.x() < 0 || p.x() >= m_maskImage.width() ||
p.y() < 0 || p.y() >= m_maskImage.height())
return false;
qreal r = qBound<int>(0, m_alphaThreshold * 255, 255);
return qAlpha(m_maskImage.pixel(p)) > r;
}
void MaskedMouseArea::mousePressEvent(QMouseEvent *event)
{
setPressed(true);
m_pressPoint = event->pos();
emit pressed();
}
void MaskedMouseArea::mouseReleaseEvent(QMouseEvent *event)
{
setPressed(false);
emit released();
const int threshold = qApp->styleHints()->startDragDistance();
const bool isClick = (threshold >= qAbs(event->x() - m_pressPoint.x()) &&
threshold >= qAbs(event->y() - m_pressPoint.y()));
if (isClick)
emit clicked();
}
void MaskedMouseArea::mouseUngrabEvent()
{
setPressed(false);
emit canceled();
}
void MaskedMouseArea::hoverEnterEvent(QHoverEvent *event)
{
Q_UNUSED(event);
setContainsMouse(true);
}
void MaskedMouseArea::hoverLeaveEvent(QHoverEvent *event)
{
Q_UNUSED(event);
setContainsMouse(false);
}
Usage in QML:
import Example 1.0
MaskedMouseArea {
id: moonArea
anchors.fill: parent
alphaThreshold: 0.4
maskSource: moon.source
}
Register the custom item:
qmlRegisterType<MaskedMouseArea>("Example", 1, 0, "MaskedMouseArea");

Reallocating QSGGeometryNode Vertex Data

I am using Qt 5.8. I am attempting to draw circular points using QSGGeometry::DrawTriangles (using QSGGeometry::DrawPoints draws points as rectangles/squares--not desired). In order to do this I am drawing 8 triangles that gives the illusion of a circle. Each data point will have 8 triangles associated with it. The number of data points can vary at any given time. After a (user) specified amount of time as a data point is added, one data point is removed. There seems to be an error in the allocation of data when it's drawn. I used setVertexDataPattern(QSGGeometry::StreamPattern); in the construction of the QSGGeometryNode; in hopes of getting the desired output.
On each draw call, I call m_geometry.allocate(m_pts.size() * MAX_VERTICES), where MAX_VERTICES = 24 in case the number of points since the last draw call has changed. I have attempted to use GL_POLYGON (since it would require fewer vertices), but the same problem happens. There seems to be an attempt to draw a shape from one slice of the the first data point to another slice of the last data point. Is there something wrong with reallocating for every draw call? What is the proper way to handle drawing data with varying sizes?
Update I think it may deal with a size issue. I have sample code that only draws 1 triangle (instead of 8) and once you get to about 25000 (times 3 for each triangle) the odd line appears and seems to stop drawing additional triangles. In the following sample code (when using a smaller number of points) the last triangle is white.
main.qml
import QtQuick 2.8
import QtQuick.Window 2.2
import TestModule 1.0
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Rectangle {
color: "black"
width: parent.width
height: parent.height * .90
anchors.top: parent.top
MouseArea {
anchors.fill: parent
onClicked: {
Qt.quit();
}
}
TestItem {
id: testItem
anchors.fill: parent
ptCount: 25000
color: "green"
}
}
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
height: parent.height * .10
width: parent.width
border.color: "pink"
color: "lightgray"
TextInput {
anchors.fill: parent
anchors.centerIn: parent
id: textInput
text: "enter max number of points here"
horizontalAlignment: TextInput.AlignHCenter
verticalAlignment: TextInput.AlignVCenter
color: "steelblue"
onEditingFinished: testItem.ptCount = parseInt(textInput.text)
validator: IntValidator{bottom: 1}
}
}
}
TestItem.h
#include <QQuickItem>
class QSGGeometryNode;
class TestItem : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
Q_PROPERTY(qint32 ptCount READ ptCount WRITE setPtCount NOTIFY ptCountChanged)
public:
explicit TestItem(QQuickItem *parent = 0);
QColor color();
void setColor(const QColor &color);
void setPtCount(const qint32& newVal);
qint32 ptCount();
signals:
void colorChanged();
void ptCountChanged();
protected:
QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *);
void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry);
QColor m_color;
qint32 m_ptCount;
};
TestItem.cpp
#include "TestItem.h"
#include <QSGNode>
#include <QSGVertexColorMaterial>
TestItem::TestItem(QQuickItem *parent) : QQuickItem(parent), m_color(Qt::green), m_ptCount(25000)
{
setFlag(ItemHasContents, true);
}
QColor TestItem::color()
{
return m_color;
}
void TestItem::setColor(const QColor &color)
{
m_color = color;
update();
emit colorChanged();
}
void TestItem::setPtCount(const qint32 &newVal)
{
if (newVal < 0)
m_ptCount = 25000;
else
m_ptCount = newVal;
update();
emit ptCountChanged();
}
qint32 TestItem::ptCount()
{
return m_ptCount;
}
QSGNode *TestItem::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *)
{
QSGGeometryNode *node = nullptr;
QSGGeometry *geometry = nullptr;
if (!oldNode)
{
node = new QSGGeometryNode;
geometry = new QSGGeometry(QSGGeometry::defaultAttributes_ColoredPoint2D(), m_ptCount * 3);
geometry->setDrawingMode(GL_TRIANGLES);
node->setGeometry(geometry);
node->setFlag(QSGNode::OwnsGeometry);
QSGVertexColorMaterial *material = new QSGVertexColorMaterial;
node->setMaterial(material);
node->setFlag(QSGNode::OwnsMaterial);
}
else
{
node = static_cast<QSGGeometryNode *>(oldNode);
geometry = node->geometry();
geometry->allocate(m_ptCount * 3);
}
QSGGeometry::ColoredPoint2D *vertices = geometry->vertexDataAsColoredPoint2D();
qreal triWidth = 250/boundingRect().width() + 10;
for (int i = 0; i < m_ptCount; ++i)
{
QColor color;
if (i == m_ptCount - 1)
color = Qt::white;
else
color = m_color;
qreal x0 = (boundingRect().width() * .90/m_ptCount) * i ;
qreal y0 = 60 * sinf(x0* 3.14/180); // 60 just varies the height of the wave
qreal x1 = x0 + 0.05 * boundingRect().width(); // 0.05 so that we have 5% space on each side
qreal y1 = y0 + boundingRect().height()/2;
vertices[i * 3].set(x1, y1, color.red(), color.green(), color.blue(), color.alpha());
vertices[i * 3 + 1].set(x1 + triWidth, y1, color.red(), color.green(), color.blue(), color.alpha());
vertices[i * 3 + 2].set(x1 + triWidth, y1 + triWidth, color.red(), color.green(), color.blue(), color.alpha());
}
node->markDirty(QSGNode::DirtyGeometry);
return node;
}
void TestItem::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
{
update();
QQuickItem::geometryChanged(newGeometry, oldGeometry);
}
Any help with determining if this is a Qt bug or my error somewhere?
It turns out I needed to change the geometry's constructor to use UnsignedIntType; it defaults to UnsignedShortType.
geometry = new QSGGeometry(QSGGeometry::defaultAttributes_ColoredPoint2D(), m_ptCount * 3, 0, QSGGeometry::UnsignedIntType);

QML Clip Custom Item

I made my own QQuickItem, which I draw using the updatePaintNode method (using the scenegraph). In a qml File I add this item as a child to a Rectangle:
Rectangle {
anchors.centerIn: parent
width: parent.width/2.
height: parent.height/2.
border.color: "#000000"
clip: true
MyItem {
id: myitem
anchors.centerIn: parent
width: parent.width
height: parent.height
MouseArea {
anchors.fill: parent
onWheel: {
if (wheel.modifiers & Qt.ControlModifier) {
parent.scale = parent.scale+ 0.2 * wheel.angleDelta.y / 120;
if (parent.scale < 1)
parent.scale = 1;
}
}
}
}
}
As you can see, I added a zoom function and set
clip: True
for the rectangle. However, when I zoom, my item is not clipped at the Rectangle but extends beyond it. What would be the correct way to do clipping?
Edit: Here is the content of the cpp file for MyItem
MyItem::MyItem(QQuickItem *parent): QQuickItem(parent)
{
setFlag(QQuickItem::ItemHasContents,true);
// code to initialize xdata, ydata, xmax, ymax
}
QSGNode *MyItem::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) {
QSGGeometryNode *mypath = static_cast<QSGGeometryNode*>(oldNode);
if(!mypath) {
mypath = new QSGGeometryNode;
mypath->setFlag(QSGNode::OwnsMaterial,true);
mypath->setFlag(QSGNode::OwnsGeometry,true);
}
QSGGeometry *geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(),_xdata.size());
QSGGeometry::Point2D *points = geometry->vertexDataAsPoint2D();
for(int i=0;i<_xdata.size();i++) {
points[i].x = _xdata[i]/_xmax*width();
points[i].y = _ydata[i]/_ymax*height();
}
geometry->setLineWidth(2);
geometry->setDrawingMode(GL_LINE_STRIP);
mypath->setGeometry(geometry);
QSGFlatColorMaterial *material = new QSGFlatColorMaterial;
material->setColor(Qt::blue);
mypath->setMaterial(material);
return mypath;
}
and here is the header file:
#include <QQuickItem>
#include <vector>
class MyItem : public QQuickItem
{
Q_OBJECT
public:
MyItem(QQuickItem *parent = 0);
private:
std::vector<double> _xdata;
std::vector<double> _ydata;
double _xmax;
double _ymax;
signals:
public slots:
protected:
QSGNode * updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *data) override;
};

MenuBar like behaviour in QML

I want to have menubar like behaviour when clicked on a rectangle. Whenever a rectangle is clicked a model is updated and a ListView is shown. I want this ListView to disappear whenever another Rectangle is clicked and the listmodel should not be appended with each click. Here is my sample code.
Card.qml
Rectangle {
id: card
width: 50
height: 100
color: "pink"
Item {
id: rec
width: 50
anchors.bottom: parent.top
ListModel {
id: menuListModel
}
Component {
id: delegate
Rectangle {
width: 50
height: 20
color: "blue"
Text {
anchors.fill: parent
anchors.centerIn: parent
text: commandText
}
}
}
ListView {
anchors.fill: parent
model:menuListModel
delegate: delegate
interactive: false
}
}
MouseArea {
anchors.fill: parent
onClicked: {
rec.height += 40;
menuListModel.append({"commandText" : "Act"});
menuListModel.append({"commandText" : "Set"});
}
}
}
main.qml
Item {
width: 120
height: 200
Row {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
spacing: 10
Card {
id: card1
}
Card {
id: card2
}
}
}
Also I want to call certain function upon clicking menu buttons i.e. Act and Set.
Edit
The following function is called with appropriate flags when a card(here rectangle) is clicked.
property int command_activate: 0x0001
property int command_summon: 0x0002
property int command_spsummon: 0x0004
property int command_mset: 0x0008
property int command_sset: 0x0010
property int command_repos: 0x0020
property int command_attack: 0x0040
property int command_list: 0x0080
function showMenu(flag) {
if(flag & command_activate) {
rec.height += 15;
menuListModel.append({"commandText" : "Activate"});
}
if(flag & command_summon) {
rec.height += 15;
menuListModel.append({"commandText" : "Normal Summon"});
}
if(flag & command_spsummon) {
rec.height += 15;
menuListModel.append({"commandText" : "Special Summon"});
}
if(flag & command_mset) {
rec.height += 15;
menuListModel.append({"commandText" : "Set"});
}
if(flag & command_sset) {
rec.height += 15;
menuListModel.append({"commandText" : "Set"});
}
if(flag & command_repos) {
if(position & pos_facedown) {
rec.height += 15;
menuListModel.append({"commandText" : "Flip Summon"});
}
else if(position & pos_attack) {
rec.height += 15;
menuListModel.append({"commandText" : "To Defense"});
}
else {
rec.height += 15;
menuListModel.append({"commandText" : "To Attack"});
}
}
if(flag & command_attack) {
rec.height += 15;
menuListModel.append({"commandText" : "Attack"});
}
if(flag & command_list) {
rec.height += 15;
menuListModel.append({"commandText" : "View"});
}
}
So, in short when a card is clicked a menu has to be shown according to the flag on the top of the card.
There are several problems with your code.
You cannot name your delegate "delegate". When you do this, the ListView uses its own delegate property to set itself, leading to nothing happening.
Also, why don't you just statically fill your ListView then use the visible property to toggle whether you display it or not? If you want it to disappear whenever another Card is clicked, you may have to use the focus property.
Indeed, setting focus to true will reset the focus of all other Items within the focus scope. Something like this might work:
Rectangle {
id: card
...
property alias model: list.model
Component {
id: mydelegate
Rectangle {
width: 50
height: 20
...
}
}
ListView {
id: list
visible: card.activeFocus
anchors.bottom: parent.top
width: card.width
delegate: mydelegate
interactive: false
height: childrenRect.height
}
MouseArea {
anchors.fill: parent
onClicked: {
card.focus = !card.focus
}
}
}
As for calling a function, you could add the name of the function to call directly in the ListModel. Add a MouseArea in your delegate, and send a signal on clicked. Then, you just have to call the matching slot (Agreed, the this[slot]() syntax is a bit hacky).
In Card.qml
Rectangle {
id: card
...
property alias model: list.model
signal itemClicked(string slot)
Component {
id: mydelegate
Rectangle {
...
MouseArea
{
anchors.fill: parent
onClicked: {
if(model.slot)
itemClicked(slot)
}
}
}
}
...
}
In main.qml
...
Card
{
model: ListModel {
ListElement{commandText: "Act"; slot: "act"}
ListElement{commandText: "Set"; slot: "set"}
}
function act()
{
print("act triggered")
}
function set()
{
print("set trigggered")
}
onItemClicked: { this[slot]() }
}
...
As I can't comment, to ask for clarification, I will state my assumptions from what I understood:
You have a number of ListModel that contain the information of the menu that should pop up, when clicking on the corresponding rectangle item in the bar.
The ListView that pops up shall disappear, once you clicked any other rectangle in this bar. (Also: if something outside the bar or menu is clicked? Or an item in the bar is selected?)
I'd go with only one ListView, and update/change the model, as well as the position (e.g. margin) once a button is clicked, so you don't have multiple unused objects flying around.
To not show anything, I think it is sufficient, to set an empty model.
You can also have a list of functions.
Row {
id: row
property var f: [a, b, c]
function a() { console.log('a'); }
function b() { console.log('b'); }
function c() { console.log('c'); }
width: 300
height: 50
Repeater {
id: rep
anchors.fill: parent
delegate: button
model: 3
Rectangle {
id: button
width: 98
height: 50
border.color: 'black'
Text {
text: "a"
anchors.fill: parent
}
MouseArea {
anchors.fill: parent
onClicked: row.f[index]()
}
}
}
}
e.g. will call the function a, b, or c - depending on the index of the rectangle.
I hope, so far, I could help.

QML: 'Steal' events from dynamic MouseArea

I'm currently trying to implement a drag-to-create mechanism in QML, but I've hit upon a problem where I need the newly created MouseArea to become the target of mouse events even though the original MouseArea hasn't had a mouse button release event yet.
Window {
id: window
width: 300
height: 300
Rectangle {
id: base
width: 20
height: 20
color: "red"
MouseArea {
anchors.fill: parent
property var lastPoint
property var draggedObj: null
function vecLength( vec ) {
return Math.abs( Math.sqrt( Math.pow( vec.x, 2 ) +
Math.pow( vec.y, 2 ) ) );
}
onPressed: lastPoint = Qt.point( mouse.x, mouse.y )
onPositionChanged: {
if ( !draggedObj ) {
var diff = Qt.point( mouse.x - lastPoint.x,
mouse.y - lastPoint.y );
if ( vecLength( diff ) > 4 ) {
draggedObj = dragObj.createObject( window );
}
}
mouse.accepted = !draggedObj;
}
}
}
Component {
id: dragObj
Rectangle {
width: 20
height: 20
color: "blue"
Drag.active: dragArea.drag.active
Drag.hotSpot.x: 10
Drag.hotSpot.y: 10
MouseArea {
id: dragArea
anchors.fill: parent
drag.target: parent
}
}
}
}
If you run this code and try it, you will see that dragging in the red Rectangle causes the creation of the draggable blue Rectangle, but it won't follow the mouse because the red MouseArea is still receiving the mouse events despite the blue MouseArea being above it.
Is there any way of forcing the blue MouseArea to receive the mouse events?
I experienced with this before and had a beginning of solution in my attic.
The trick here is calling QQuickItem::grabMouse() and sending a mouse press event to the newly created object.
Unfortunately I believe this can only be done from c++.
I then created a helper class to expose this functionality to qml:
MouseGrabber.h
#ifndef MOUSEGRABBER
#define MOUSEGRABBER
#include <QObject>
#include <QQuickItem>
#include <QGuiApplication>
#include <QMouseEvent>
class MouseGrabber : public QObject
{
Q_OBJECT
Q_PROPERTY(QQuickItem* target READ target WRITE setTarget NOTIFY targetChanged)
Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged)
public:
explicit MouseGrabber(QObject *parent = 0) : QObject(parent), m_target(nullptr), m_active(true) { }
QQuickItem* target() const { return m_target; }
bool active() const { return m_active;}
signals:
void targetChanged();
void activeChanged();
public slots:
void setTarget(QQuickItem* target)
{
if (m_target == target)
return;
ungrabMouse(m_target);
if (m_active)
grabMouse(target);
m_target = target;
emit targetChanged();
}
void setActive(bool arg)
{
if (m_active == arg)
return;
m_active = arg;
if (m_active)
grabMouse(m_target);
else
ungrabMouse(m_target);
emit activeChanged();
}
private:
static void grabMouse(QQuickItem* target)
{
if (target)
{
target->grabMouse();
QMouseEvent event(QEvent::MouseButtonPress, QPointF(), Qt::LeftButton, QGuiApplication::mouseButtons(), QGuiApplication::keyboardModifiers());
QGuiApplication::sendEvent(target, &event);
}
}
static void ungrabMouse(QQuickItem* target)
{
if (target)
target->ungrabMouse();
}
QQuickItem* m_target;
bool m_active;
};
#endif // MOUSEGRABBER
This could have been made more convenient by directly calling slots instead of manipulating proprieties, but that's what I had in stock. For example a slot called grabMouseUntilRelease(QQuickItem* item), that grabs the mouse for this item, listen for a mouse release event with installEventFilter and ungrab it automatically.
Register the class so it can be instantiated in QML with qmlRegisterType somewhere in your code :
qmlRegisterType<MouseGrabber>("com.mycompany.qmlcomponents", 1, 0, "MouseGrabber");
After that you can instantiate a MouseGrabber in QML and use it by modifying its proprieties ( target and active ) :
QML
import com.mycompany.qmlcomponents 1.0
Window {
id: window
width: 300
height: 300
Rectangle {
id: base
width: 20
height: 20
color: "red"
MouseArea {
anchors.fill: parent
property var lastPoint
property var draggedObj: null
function vecLength( vec ) {
return Math.abs( Math.sqrt( Math.pow( vec.x, 2 ) +
Math.pow( vec.y, 2 ) ) );
}
onPressed: lastPoint = Qt.point( mouse.x, mouse.y )
onPositionChanged: {
if ( !draggedObj ) {
var diff = Qt.point( mouse.x - lastPoint.x,
mouse.y - lastPoint.y );
if ( vecLength( diff ) > 4 ) {
draggedObj = dragObj.createObject( window );
grabber.target = draggedObj.dragArea; // grab the mouse
}
}
mouse.accepted = !draggedObj;
}
}
}
MouseGrabber {
id: grabber
}
Component {
id: dragObj
Rectangle {
property alias dragArea: dragArea
width: 20
height: 20
color: "blue"
Drag.active: dragArea.drag.active
Drag.hotSpot.x: 10
Drag.hotSpot.y: 10
MouseArea {
id: dragArea
anchors.fill: parent
drag.target: parent
onReleased: {
if (grabber.target === this)
grabber.target = null; // ungrab the mouse
}
}
}
}
}
My other answer is way too much over engineered.
There is no need to steal the mouse events in your situation, you just want to update the position of the dragged blue rectangle in the onPositionChanged handler (or with a Binding or directly inside the Rectangle component).
Wrtiting this in your MouseArea is enough :
onPositionChanged: {
if ( !draggedObj ) {
var diff = Qt.point( mouse.x - lastPoint.x,
mouse.y - lastPoint.y );
if ( vecLength( diff ) > 4 ) {
draggedObj = dragObj.createObject( window );
}
} else { //update the position of the dragged rectangle
draggedObj.x = mouse.x - draggedObj.width/2;
draggedObj.y = mouse.y - draggedObj.height/2;
}
}
onReleased: draggedObj = null

Resources