How to drag a qquickwidget? - qt

I have a QQuickWidget inside main window. This seems to work, however, it will shake violently when dragged.
//main.qml
Rectangle{
id: root
property point dragStart
signal moved(point offset)
MouseArea{
id: dragArea
anchors.fill: parent
onPressed: root.dragStart = Qt.point(dragArea.mouseX,dragArea.mouseY)
onMouseXChanged: move()
onMouseYChanged: move()
function move(){
var offset = Qt.point(dragArea.mouseX-root.dragStart.x, dragArea.mouseY-root.dragStart.y)
root.moved(offset)
console.log(offset)
}
}
}
//MainWindow.cpp
void MainWindow::moveQml(QPointF offset){
ui->quickWidget->move(ui->quickWidget->pos()+offset.toPoint());
}
Here is the debug output when I only drag to the bottom left, the point shouldn't have positive x:
qml: QPointF(6, 2) qml: QPointF(6, 2) qml: QPointF(-6, -1) qml:
QPointF(-6, -1) qml: QPointF(5, 1) qml: QPointF(5, 1) qml: QPointF(-5,
0) qml: QPointF(-5, 0) qml: QPointF(4, 0) qml: QPointF(4, 0) qml:
QPointF(-5, 1) qml: QPointF(-5, 1)

Oh, I see. After checking the output, I know what's wrong. When moving the mouse, the mouse usually changes its x and y. So the move() will get executed twice.
onMouseXChanged: {
var offset = Qt.point(dragArea.mouseX - root.dragStart.x, 0)
root.moved(offset)
}
onMouseYChanged: {
var offset = Qt.point(0, dragArea.mouseY - root.dragStart.y)
root.moved(offset)
}

Related

Qml - How to stop a rectangle dragging outside it's parent

I am using qml to create a resizable rectangle that can be dragged inside it's parent pane, but cannot be dragged outside (Fig. 1). When I resize it e.g. using the top left handle, it works great and can't be resized outside it's parent (Fig. 2). But when I drag the entire rectangle, it can be dragged outside its parent (Fig. 3). My code attempts to use the same logic for both, i.e. once the rectangle's left side reaches it's parent's left side, it cancels the drag. Works fine for resizing, but doesn't when I drag the rectangle. What am I doing wrong?
Pane {
id: compPane
implicitHeight: imageListModel.reqViewHeight
implicitWidth: imageListModel.reqViewWidth
leftPadding: 0
topPadding: 0
clip: true
background: Rectangle{
id: backgroundRect
anchors.fill: parent
color: "gray"
border.color: "black"
border.width: 6
}
// Resizable rectangle that can be dragged
Rectangle {
id: indRect
x:parent.width / 4
y: parent.height / 4
width: parent.width / 2
height: parent.width / 2
border {
width: 2
color: "steelblue"
}
color: "#354682B4"
MouseArea {
id: indImagesDragArea
anchors.fill: parent
anchors.margins: 8
drag.target: parent
onPositionChanged: {
if (drag.active){
if (indRect.x <= 0 + backgroundRect.border.width)
Drag.cancel()
if (indRect.x + indRect.width >= (compPane.width - backgroundRect.border.width) )
Drag.cancel()
}
}
}
// Top Left handle - works well
Rectangle {
width: 15
height: 15
color: "steelblue"
anchors.verticalCenter:parent.top
anchors.horizontalCenter: parent.left
MouseArea {
anchors.fill: parent
cursorShape: Qt.SizeFDiagCursor
drag{ target: parent; axis: Drag.XAndYAxis }
onPositionChanged: {
if(drag.active){
//var delta = Math.max(mouseX, mouseY)
var newWidth = indRect.width - mouseX
var newHeight = indRect.height - mouseY;
if (newWidth < width || newHeight < height)
return
if (indRect.x <= 0 + backgroundRect.border.width && mouseX < 0){
Drag.cancel()
indRect.x = 0 + backgroundRect.border.width
}
else {
indRect.width = newWidth
indRect.x = indRect.x + mouseX
}
if (indRect.y <= 0 + backgroundRect.border.width && mouseY < 0){
Drag.cancel()
indRect.y = 0 + backgroundRect.border.width
}
else{
indRect.height = newHeight
indRect.y = indRect.y + mouseY
}
}
}
}
}
}
}
Drag.cancel() is actually intended for use when implementing drag and drop types of interactions. In this case, the calls to it are doing nothing because you are never actually starting one of those sequences. Your upper left handle example works fine if you comment out the calls to Drag.cancel().
Why your upper left example is working is because you are limiting the x and y on position updates to clip them to the scene via explicit updates of indRect.x and indRect.y. You just need to implement the same technique for the main drag scenario.
For instance:
onPositionChanged: {
if (drag.active){
if (indRect.x <= 0 + backgroundRect.border.width)
indRect.x = 0 + backgroundRect.border.width;
if (indRect.x + indRect.width >= (compPane.width - backgroundRect.border.width) )
indRect.x = compPane.width - backgroundRect.border.width - indRect.width;
}
}
You could try to use the property of drag.minimum and drag.maximum.
See the documentation:
https://doc.qt.io/qt-5/qml-qtquick-mousearea.html#drag.threshold-prop

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

How to get rid of handler dragging delay?

There's a range slider implementation. The problem is that when I press on a handler and start dragging, it doesn't move for a moment, therefore it happens some delay and distance between a handler and a mouse cursor.
Image { // handler's implementation
id: handler1
// ...
MouseArea {
anchors.fill: parent
drag.target: parent
drag.axis: Drag.XAxis
drag.minimumX: -width
drag.maximumX: handler2.x - parent.width
onPositionChanged: {
slider1 = (max - min) * (handler1.fakeX) / root.width + min
}
}
}
Could you please tell me how to deal with it?
You have to set the drag.threshold property of your MouseArea to 0 so that dragging starts immediately.

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

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