QML: 'Steal' events from dynamic MouseArea - qt

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

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");

QML ListView populate animation not working

I have a C++ model derived from QAbstractListModel which I am using in a QML ListView
ListView {
id: listView
populate: Transition {
id: addTrans
SequentialAnimation {
PauseAnimation {
duration: (addTrans.ViewTransition.index -
addTrans.ViewTransition.targetIndexes[0]) * 100
}
NumberAnimation { property: "scale"; from: 0; to: 1 }
}
}
anchors.fill: parent
delegate: listDelegate
model: newsModel
}
I tried to add an animation for when the list is populated, but it doesn't work. So what I did was I tried to change populate to add and the animation is triggered, however the problem is that that each item is already in the list when the animation starts: in other words, instead of having an empty list to which items are added, I have a full list of elements that are then individually animated. I have tried also following this suggestion but it doesn't work.
EDIT: as requested I am providing a minimal working example to reproduce my problem.
The problem can be reproduced with the following:
#include <QObject>
#include <QQmlObjectListModel.h>
class Item : public QObject{
Q_OBJECT
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
public:
Item(QObject* parent = nullptr): QObject(parent)
{
}
QString name() const{
return mName;
}
void setName(QString const &name){
if(mName != name){
mName = name;
emit nameChanged();
}
}
signals:
void nameChanged();
private:
QString mName;
};
class DataManager : public QObject{
Q_OBJECT
Q_PROPERTY(QQmlObjectListModel<Item>* itemsModel READ itemsModel CONSTANT)
public:
DataManager(){
mItemsModel = new QQmlObjectListModel<Item>(this);
qRegisterMetaType<QQmlObjectListModel<Item>*>("QQmlObjectListModel<Item>*");
// I have also tried to add items from here, but the result is even worse
// for(int i = 0; i < 20; i++){
// addItem(QString::number(i));
// }
}
QQmlObjectListModel<Item>* itemsModel(){
return mItemsModel;
}
Q_INVOKABLE void addItem(QString const &name){
Item* item = new Item(mItemsModel);
item->setName(name);
mItemsModel->append(item);
}
private:
QQmlObjectListModel<Item>* mItemsModel;
};
Then in qml:
import QtQuick 2.12
import QtQuick.Controls 2.5
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("Scroll")
ScrollView {
anchors.fill: parent
ListView {
width: parent.width
// dataManager is a context property set via c++
model: dataManager.itemsModel
populate: Transition {
id: popTrans
SequentialAnimation {
PauseAnimation {
duration: (popTrans.ViewTransition.index -
popTrans.ViewTransition.targetIndexes[0]) * 100
}
NumberAnimation { property: "scale"; from: 0; to: 1 }
}
}
add: Transition {
id: addTrans
SequentialAnimation {
PauseAnimation {
duration: (addTrans.ViewTransition.index -
addTrans.ViewTransition.targetIndexes[0]) * 100
}
NumberAnimation { property: "scale"; from: 0; to: 1 }
}
}
delegate: ItemDelegate {
text: "Item" + name
width: parent.width
}
}
}
Component.onCompleted: {
var p;
for(p=0; p < 20; p++){
dataManager.addItem(p)
}
}
}
The QQmlObjectListModel is a helper class which I borrowed from here
I am not sure what you want exactly, but I hope my code help you.
populate: Transition {
id: addTrans
SequentialAnimation {
PropertyAction {
property: "visible"
value: false
}
PauseAnimation {
duration: (addTrans.ViewTransition.index -
addTrans.ViewTransition.targetIndexes[0]) * 100
}
PropertyAction {
property: "visible"
value: true
}
NumberAnimation { property: "scale"; from: 0; to: 1 }
}
}

How to create a round mouse area in QML

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;
}

Can't write to a C++ object from QML

I have a program that displays images and can put different filters on these images. In the QML GUI I have a series of buttons that let you set which highlights are enabled.
Column{
id: highlightButtons
anchors {left: frameViewer.right; top: parent.top; leftMargin: 10; topMargin: 10 }
spacing: 20
Rectangle{
id: class1
color: "red"
height: 45
width: 45
radius: 12
MouseArea{
anchors.fill: parent
onClicked: {hl.red = frames[frameList.currentIndex];
loadImages(); console.log(hl.red)}
}
}
Rectangle{
id: class2
color: "green"
height: 45
width: 45
radius: 12
MouseArea{
anchors.fill: parent
onClicked: {hl.green = frames[frameList.currentIndex];
loadImages(); console.log(hl.green)}
}
}
Rectangle{
id: class3
color: "blue"
height: 45
width: 45
radius: 12
MouseArea{
anchors.fill: parent
onClicked: {hl.blue = frames[frameList.currentIndex];
loadImages(); console.log(hl.blue)}
}
}
}
Where frames is a list of strings.
hl is a class that I use to store which highlights are enabled.
class HighlightModel : public QObject
{
Q_PROPERTY(QString red READ getRed WRITE setRed NOTIFY redChanged)
Q_PROPERTY(QString blue READ getGreen WRITE setBlue NOTIFY blueChanged)
Q_PROPERTY(QString green READ getBlue WRITE setGreen NOTIFY greenChanged)
public:
void setRed(QString &frame)
{
if(redHighlights.find(frame) != redHighlights.end())
{
redHighlights[frame] = !redHighlights[frame];
}
else
{
redHighlights.insert(std::pair<QString, bool>(frame, true));
}
emit redChanged();
}
void setGreen(QString &frame)
{
if(greenHighlights.find(frame) != greenHighlights.end())
{
greenHighlights[frame] = !greenHighlights[frame];
}
else
{
greenHighlights.insert(std::pair<QString, bool>(frame, true));
}
emit greenChanged();
}
void setBlue(QString &frame)
{
if(blueHighlights.find(frame) != blueHighlights.end())
{
blueHighlights[frame] = !blueHighlights[frame];
}
else
{
blueHighlights.insert(std::pair<QString, bool>(frame, true));
}
emit blueChanged();
}
bool getRed(QString frame)
{
return redHighlights[frame];
}
bool getGreen(QString frame)
{
return greenHighlights[frame];
}
bool getBlue(QString frame)
{
return blueHighlights[frame];
}
std::map<QString, bool> redHighlights;
std::map<QString, bool> greenHighlights;
std::map<QString, bool> blueHighlights;
signals:
void redChanged();
void greenChanged();
void blueChanged();
private:
};
My understand of Q_PROPERTY is that setting WRITE to setRed, setBlue etc. should mean that when I set hl.red to something, it should call the function I associate with WRITE. However, I have put breakpoints in the code there and that code is never run. When I use console.log to print hl.blue it just prints the string that I tried to pass before. Can anyone clear this up for me? Have I misinterpreted how this works?
EDIT
Here is the code where I associate the HighlightModel with the engine:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
qDebug() << VERSION_INFO;
FrameViewerWindow engine;
InfoHolder ver;
HighlightModel highlights;
engine.rootContext()->setContextProperty("ver", &ver);
engine.rootContext()->setContextProperty("hl", &highlights);
engine.addImageProvider("images", new ImageLoader(QQuickImageProvider::Image, &highlights));
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}

Mouse events not handled within QQuickItem inheritor

I want to write simple Qt Quick app with draggable QQuickItems. The items are well draggeble because of embedded MouseArea in the items. But a problem is that mouse events are not fired into C++ code in virtual overloaded functions. How to solve this problem or maybe there are some examples that I didn't find?
The QML file:
import QtQuick 2.0
import SimpleMaterial 1.0
Rectangle {
width: 320
height: 480
color: "black"
SimpleMaterialItem {
width: parent.width;
height: parent.height / 3;
color: "steelblue"
MouseArea {
anchors.fill: parent
width: 64
height: 64
drag.target: parent
drag.axis: Drag.XandYAxis
}
}
}
The C++ class:
class Item : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
public:
Item()
{
setFlag(ItemHasContents, true);
setFlag(ItemAcceptsDrops, true);
setFlag(ItemAcceptsInputMethod, true);
setAcceptedMouseButtons(Qt::AllButtons);
}
void mousePressEvent(QMouseEvent * event)
{
qDebug("Press"); // NOT CALLED!
}
public:
QSGNode *updatePaintNode(QSGNode *node, UpdatePaintNodeData *)
{
...
}
};
If MouseArea handles mouse event it doesn't pass event to its parent.
You need:
onPressed: {
mouse.accepted = false;
}
in mouse area to let the SimpleMaterialItem handle onPressed event.

Resources