Hello guys I need your help,
I'm creating a timeline like widget in Qt based on the QGraphics framework. My problem is to handle collisions of items (inherited from QGraphicsRectItem) in my Timeline tracks.
I use the itemChange() function to keep track of the collisions. To keep the items in the parent boundingRect I use the following code wich works like a charm
if (change == ItemPositionChange && scene())
if (thisRect.intersects(parentRect)) {
const QPointF offset(mapFromParent(thisRect.topLeft()));
QPointF newPos(value.toPointF());
if (snapToGrid) {
newPos.setX(floor(qMin(parentRect.right() - offset.x() - thisRect.width(),
qMax(newPos.x(), parentRect.left() / 2 - offset.x())) / (snapValue * pxPerSec(duration))) * snapValue * pxPerSec(duration));
}
else {
newPos.setX(qMin(parentRect.right() - offset.x() - thisRect.width(),
qMax(newPos.x(), parentRect.left() - offset.x())));
}
newPos.setY(parentItem()->boundingRect().height() * 0.1);
return newPos;
}
}
This stops the items immediately if they reach the left or right boundary of my timline tracks, even if I move the mouse outside my view/scene. It's like an invisible wall.
Now I want the same behaviour if one item in a track collides with another.
const QRectF parentRect(parentItem()->sceneBoundingRect());
const QRectF thisRect(sceneBoundingRect());
foreach (QGraphicsItem *qgitem, collidingItems()) {
TimelineItem *item = qgraphicsitem_cast<TimelineItem *>(qgitem);
QPointF newPos(value.toPointF());
if (item) {
const QRectF collideRect = item->sceneBoundingRect();
const QPointF offset(mapFromParent(thisRect.topLeft()));
if (thisRect.intersects(collideRect) && thisRect.x() < collideRect.x()) {
newPos.setX(collideRect.left() - offset.x() - thisRect.width());
}
if (thisRect.intersects(collideRect) && thisRect.x() > collideRect.x()) {
newPos.setX(collideRect.right() + offset.x());
}
}
newPos.setY(parentItem()->boundingRect().height() * 0.1);
return newPos;
}
The problem is that if I move an item via mouse against another item you see them intersecting/overlapping and then the item I moved snaps back to the minimum not intersecting distance. How do I manage to stop the moving item immediately if it hits another (no trembling forth and back movement intersecting thing). Just like the way the items are kept in parents boundingRect (first code block), the invisible wall like behaviour?
I think the problem here is with "thisRect". If you're calling this from an ItemPositionChange, then sceneBoundingRect is returning the bounding rectangle of the item in its previous position, not the new one. What happens is that the current position succeeds even though though there's a collision, but the next one fails because you're always checking the previous results, and then it snaps back to avoid the collision.
After getting the local item's scene rectangle, you'll need to translate it to the new future position of the item:
QPointF new_pos (value.toPointF ());
QRectF thisRect (sceneBoundingRect());
thisRect.translate (new_pos - pos());
I've moved the creation of "new_pos" outside the loop so that it's available for the rectangle translation. It's also faster.
Related
I wanted to have an online monitoring system that could tell where the shape is currently, but am getting very weird coordinates of the item, also the dimensions of it get higher by 1 each time I create new one and drag it.
Initial position (map size is 751 by 751, checked by outputting to qDebug(), scene bound to yellow space) :
Dragging it to the left top corner.
As you can see in the beginning it was on (200;200), but after dragging it is on (-201;-196). After deleting it and creating new shape on the same position with the same properties, new shape can't be seen because it is outside of the map, which suggests that edits don't show correct data.
Here is the code of updating the edits:
void CallableGraphicsRectItem::mouseReleaseEvent(QGraphicsSceneMouseEvent* event)
{
QGraphicsRectItem::mouseReleaseEvent(event);
ptr->updateEdits(this);
}
Here is what I managed to cut down into updateEdits():
void MainWindow::updateEdits(QAbstractGraphicsShapeItem* item)
{
//stuff not related to scene
auto posReal = item->scenePos();
auto pos = posReal.toPoint();
//create QString from coordinates
QString coordinate;
coordinate.setNum(pos.x());
ui->leftXEdit->setText(coordinate);
coordinate.setNum(pos.y());
ui->upperYEdit->setText(coordinate);
//get width and height for rect, radius for circle
auto boundingRectReal = item->sceneBoundingRect();
auto boundingRect = boundingRectReal.toRect();
ui->widthEdit->setText(QString::number(boundingRect.width()));
//disables height edit for circles, not really relevant
if (!items[currentShapeIndex].isRect)
{
ui->heightEdit->setDisabled(true);
}
else
{
ui->heightEdit->setDisabled(false);
ui->heightEdit->setText(QString::number(boundingRect.height()));
}
}
Here is how I anchor the QGraphicsScene to the left top corner of the yellow area:
scene->setSceneRect(0, 0, mapSize.width() - 20, mapSize.height() - 20);
ui->graphicsView->setScene(scene);
How can I report the right data to the edits?
You're better off overriding the itemChange method and using the ItemPositionHasChanged notification. You have to set the ItemSendsGeometryChanges flag on the item so that it receives these notifications.
I'm not sure that your item's final position has been set when you're still in the mouseReleaseEvent method. Tracking it in itemChange will ensure that the data is valid, and this kind of thing is what it's for.
Also, note that "pos" is in the item's parent coordinates, and "boundingRect" is in the item's coordinate space. You should use "scenePos" and "sceneBoundingRect" if you want to be sure you're using scene coordinates. If the item doesn't have a parent, then "pos" and "scenePos" will return the same values, but "boundingRect" and "sceneBoundingRect" will generally differ.
Does anyone have a better way to constrain a child of a QGraphicsItem to a scene?
I have successfully properly constrained a parent QGraphicsItem to its scene by overriding itemChange, but now I need to do the same for the child QGraphicsItem.
Example Use-case:
This code works... for the most part. The only problem is the QGraphicsItem's velocity when hitting either side will affect its endstop position:
QVariant SizeGripItem::HandleItem::itemChange(GraphicsItemChange change,
const QVariant &value)
{
QPointF newPos = value.toPointF();
if (change == ItemPositionChange)
{
if(scene())
{
newPos.setY(pos().y()); // Y-coordinate is constant.
if(scenePos().x() < 0 ) //If child item is off the left side of the scene,
{
if (newPos.x() < pos().x()) // and is trying to move left,
{
newPos.setX(pos().x()); // then hold its position
}
}
else if( scenePos().x() > scene()->sceneRect().right()) //If child item is off the right side of the scene,
{
if (newPos.x() > pos().x()) //and is trying to move right,
{
newPos.setX(pos().x()); // then hold its position
}
}
}
}
return newPos;
}
For the parent item, I used:
newPos.setX(qMin(scRect.right(), qMax(newPos.x(), scRect.left())));
which worked perfectly, but I'm stumped as to how or if I could use that here.
First, to be specific, scenes effectively have no boundaries. What you're trying to do is constrain the item to the scene rectangle that you've set elsewhere.
The problem I see is in your use of scenePos. This is an ItemPositionChange; the item's scenePos hasn't been updated with the new position yet, so when you check for scenePos being out of the scene rect, you're really checking the result of the last position change, not the current one. Because of that, your item ends up just off the edge of the scene rectangle and then sticks there. How far off the edge depends on how fast you were moving the mouse, which dictates how much distance there is between ItemPositionChange notifications.
Instead, you need to compare the new position to the scene rectangle and then restrict the value that gets returned to be within the scene rectangle. You need the new position in scene coordinates to do the comparison, so you need something like:
QPoint new_scene_pos = mapToScene (new_pos);
if (new_scene_pos.x() < scene()->sceneRect().left())
{
new_scene_pos.setX (scene()->sceneRect().left());
new_pos = mapFromScene (new_scene_pos);
}
This isn't complete code, obviously, but these are the conversions and checks you need to do to keep it in on the left side. The right side is very similar, so just use the new_scene_pos for the comparison there.
Note that I didn't assume that the left edge of sceneRecT is at 0. I'm sure that's what you coded where you set the sceneRect, but using the actual left value rather than assuming it's 0 eliminates any problems if you end up later changing the range of scene coordinates you're planning to work with.
I used "left" instead of "x" on the sceneRect call just because it parallels using "right" for the other side. They're exactly the same, but I think it reads slightly better in this case.
I created my own classes (view and scene) to display image and objects I added to it, even got zoom in/out function implemented to my view, but now I have to add new functionality and I don't even know how to start looking for it.
Whenever I press the scroll button of my mouse and hold it - I wish to move around the scene, to see different parts of it - just like I would with sliders. It is supposed to be similar to any other program allowing to zoom in/out to image and move around zoomed picture to see different parts of it.
Unfortunately - I don't even know how to look for some basic, because "moving" and similar refer to dragging objects around.
EDIT 1
void CustomGraphicView::mouseMoveEvent(QMouseEvent *event)
{
if(event->buttons() == Qt::MidButton)
{
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
translate(event->x(),event->y());
}
}
Tried this - but it is working in reverse.
I suppose you know how to handle events using Qt.
So, to translate (move) your view use the QGraphicsView::translate() method.
EDIT
How to use it:
void CustomGraphicsView::mousePressEvent(QMouseEvent* event)
{
if (e->button() == Qt::MiddleButton)
{
// Store original position.
m_originX = event->x();
m_originY = event->y();
}
}
void CustomGraphicsView::mouseMoveEvent(QMouseEvent* event)
{
if (e->buttons() & Qt::MidButton)
{
QPointF oldp = mapToScene(m_originX, m_originY);
QPointF newP = mapToScene(event->pos());
QPointF translation = newp - oldp;
translate(translation.x(), translation.y());
m_originX = event->x();
m_originY = event->y();
}
}
i create an graphic scene with the Graphics View Framework.
I have a couple(7 - 10) of ellipse (placed vertical) created with:
ellipse = scene->addEllipse(x1, y1, w, h, pen, brush);
Now i want to prepare the graphic for an animation. First all ellipse are black. After 5 sec the first should be colored red, 5 sec after the 1st = green and the 2nd = red and so on.
My Idea was to get the first item and color the ellipse. But how can i get the ellipse items? Is there any function that perform like that?
You can use the items() Method to get a sorted list off all Elements.
Then iterate the list and check if it is an ellipse item.
Items is also overloade for more special cases, see if one of them fits your needs.
Method:
QList<QGraphicsItem *> QGraphicsScene::items() const
You can find the documentation here: http://doc.qt.io/qt-4.8/qgraphicsscene.html#items
If you have performance concerns, here is an excerpt from the Qt Docs which I do 100% agree with:
One of QGraphicsScene's greatest strengths is its ability to
efficiently determine the location of items. Even with millions of
items on the scene, the items() functions can determine the location
of an item within few milliseconds. There are several overloads to
items(): one that finds items at a certain position, one that finds
items inside or intersecting with a polygon or a rectangle, and more.
The list of returned items is sorted by stacking order, with the
topmost item being the first item in the list. For convenience, there
is also an itemAt() function that returns the topmost item at a given
position.
To check the type of the item you can use:
int QGraphicsItem::type() const
Excerpt from the docs:
Returns the type of an item as an int. All standard graphicsitem
classes are associated with a unique value; see QGraphicsItem::Type.
This type information is used by qgraphicsitem_cast() to distinguish
between types.
Second approach is to use qgraphicsitem_cast() directly.
Here is an Example that uses a custom GraphicsItem Node:
// Sum up all forces pushing this item away
qreal xvel = 0;
qreal yvel = 0;
foreach (QGraphicsItem *item, scene()->items()) {
Node *node = qgraphicsitem_cast<Node *>(item);
if (!node)
continue;
QPointF vec = mapToItem(node, 0, 0);
qreal dx = vec.x();
qreal dy = vec.y();
double l = 2.0 * (dx * dx + dy * dy);
if (l > 0) {
xvel += (dx * 150.0) / l;
yvel += (dy * 150.0) / l;
}
}
You can store the pointers returned from calling scene->addEllipse and use those.
Alternatively, though probably not very efficient, you could iterate through all items in the scene and use a dynamic_cast to check the type.
I'd opt for the 1st method.
I have one question when infinite background scrolling is done, is the object remain fixed(like doodle in doodle jump, papy in papi jump) or these object really moves.Is only background move or both (background and object )move.plz someone help me.I am searching for this solution for 4/5 days,but can't get the solution.So plz someone help me. And if object does not move how to create such a illusion of object moving.
If you add the object to the same layer as the scrolling background, then it will scroll as the background scrolls.
If your looking for an effect like the hero in doodle jump, you may want to look at having two or more layers in a scene.
Layer 1: Scrolling Background Layer
Layer 2: Sprite layer
SomeScene.m
CCLayer *backgroundLayer = [[CCLayer alloc] init];
CCLayer *spriteLayer= [[CCLayer alloc] init];
[self addChild:backgroundLayer z:0];
[self addChild:spriteLayer z:1];
//Hero stays in one spot regardless of background scrolling.
CCSprite *squidHero = [[CCSprite alloc] initWithFile:#"squid.png"];
[spriteLayer addChild:squidHero];
If you want objects to scroll with the background add it to the background layer:
//Platform moves with background.
CCSprite *bouncePlatform= [[CCSprite alloc] initWithFile:#"bouncePlatform.png"];
[backgroundLayer addChild:bouncePlatform];
Another alternative is to use a CCFollow action. You would code as if the background is static (which it will be) and the player is moving (which it will be), but add a CCFollow action to the player. This essentially moves the camera so that it tracks your player.
You can also modify the classes so that you can get the CCFollow action to follow with an offset (i.e., so the player is not in the middle of the screen) as well as to have a smoothing effect to it, so that when the player moves, the follow action is not jerky. See the below code:
*NOTE I am using cocos2d-x, the c++ port. The methods are similar in cocos2d, and you should be able to modify these to fit the cocos2d syntax. Or search around -- I found these for cocos2d and then ported to c++.
//defines the action to constantly follow the player (in my case, "runner.p_sprite is the sprite pointing to the player)
FollowWithOffset* followAction = FollowWithOffset::create(runner.p_sprite, CCRectZero);
runAction(followAction);
And separately, I have copied the class definition for CCFollow to create my own class, CCFollowWithAction. This also has a smoothing effect (you can look this up more online) so that when the player moves, the actions are not jerky. I modified "initWithTarget," to take into account an offset, and "step," to add a smoothing action. You can see the modifications in the comments below.
bool FollowWithOffset::initWithTarget(CCNode *pFollowedNode, const CCRect& rect/* = CCRectZero*/)
{
CCAssert(pFollowedNode != NULL, "");
pFollowedNode->retain();
m_pobFollowedNode = pFollowedNode;
if (rect.equals(CCRectZero))
{
m_bBoundarySet = false;
}
else
{
m_bBoundarySet = true;
}
m_bBoundaryFullyCovered = false;
CCSize winSize = CCDirector::sharedDirector()->getWinSize();
m_obFullScreenSize = CCPointMake(winSize.width, winSize.height);
//m_obHalfScreenSize = ccpMult(m_obFullScreenSize, 0.5f);
m_obHalfScreenSize = CCPointMake(m_obFullScreenSize.x/2 + RUNNER_FOLLOW_OFFSET_X,
m_obFullScreenSize.y/2 + RUNNER_FOLLOW_OFFSET_Y);
if (m_bBoundarySet)
{
m_fLeftBoundary = -((rect.origin.x+rect.size.width) - m_obFullScreenSize.x);
m_fRightBoundary = -rect.origin.x ;
m_fTopBoundary = -rect.origin.y;
m_fBottomBoundary = -((rect.origin.y+rect.size.height) - m_obFullScreenSize.y);
if(m_fRightBoundary < m_fLeftBoundary)
{
// screen width is larger than world's boundary width
//set both in the middle of the world
m_fRightBoundary = m_fLeftBoundary = (m_fLeftBoundary + m_fRightBoundary) / 2;
}
if(m_fTopBoundary < m_fBottomBoundary)
{
// screen width is larger than world's boundary width
//set both in the middle of the world
m_fTopBoundary = m_fBottomBoundary = (m_fTopBoundary + m_fBottomBoundary) / 2;
}
if( (m_fTopBoundary == m_fBottomBoundary) && (m_fLeftBoundary == m_fRightBoundary) )
{
m_bBoundaryFullyCovered = true;
}
}
return true;
}
void FollowWithOffset::step(float dt)
{
CC_UNUSED_PARAM(dt);
if(m_bBoundarySet){
// whole map fits inside a single screen, no need to modify the position - unless map boundaries are increased
if(m_bBoundaryFullyCovered)
return;
CCPoint tempPos = ccpSub( m_obHalfScreenSize, m_pobFollowedNode->getPosition());
m_pTarget->setPosition(ccp(clampf(tempPos.x, m_fLeftBoundary, m_fRightBoundary),
clampf(tempPos.y, m_fBottomBoundary, m_fTopBoundary)));
}
else{
//custom written code to add in support for a smooth ccfollow action
CCPoint tempPos = ccpSub( m_obHalfScreenSize, m_pobFollowedNode->getPosition());
CCPoint moveVect = ccpMult(ccpSub(tempPos,m_pTarget->getPosition()),0.25); //0.25 is the smooth constant.
CCPoint newPos = ccpAdd(m_pTarget->getPosition(), moveVect);
m_pTarget->setPosition(newPos);
}
}