I am attempting to properly constrain the movement of a QGraphicsItem (specifically QGraphicsRectItem) without changing the native behavior to function as a scrollbar on the X-axis.
I tried overriding the mouseMoveEvent function, but then I need to re-write the behavior for the rectangle in both the X and Y directions. At best, I can get the rectangle to snap to a single position with the mouse. (Here the rectangle will snap so the mouse holds it at the midpoint):
void SegmentItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
setY(0);
setX(event->scenePos().x() - boundingRect().width()/2);
}
I'm looking at itemChange right now, as described here, but it looks a little unwieldy and not exactly elegant.
EDIT: This should work, but I currently cannot coerce it to work.
Is there a way to just constrain the y-axis movement? (I will also need to create endstops for the scrollbar, but later.)
I tinkered with the code from the itemChange Class Reference Page, and enhanced it so all four corners of my QGraphicsRectItem would stay within the QGraphicsScene:
QVariant SegmentItem::itemChange(GraphicsItemChange change, const QVariant &value)
{
if (change == ItemPositionChange && scene()) {
// value is the new position.
QPointF newPos = value.toPointF();
QRectF rect = scene()->sceneRect();
rect.setWidth(rect.width() - boundingRect().width());
rect.setHeight(0);
if (!rect.contains(newPos)) {
// Keep the item inside the scene rect.
newPos.setX(qMin(rect.right(), qMax(newPos.x(), rect.left())));
newPos.setY(2);
return newPos;
}
}
return QGraphicsItem::itemChange(change, value);
}
To answer the other part of your question about constraining movement to just one direction...use the same itemChange structure as laid out in the above answer. The only additional thing you need to do is transfer your item's current X or Y coordinate to the new position before returning it. This line allows Y to track the mouse, but keeps X the same (i.e. movement is restricted to vertical):
newPos.setX (this->pos().x());
Similarly, to allow X to track the mouse, but keep Y the same (i.e. movement is restricted to horizontal):
newPos.setY (this->pos().y());
With the ItemPositionChange notification, the item's current position hasn't yet changed, so you can manipulate the new position any way you want before returning the new value.
Related
I'm trying to simulate 'scrolling' in an application in SDL2, however i dont think that moving each individual object on the screen every time the scroll event occurs is an efficient/elegant way of doing it. What i know of SDL2 is the top left begins at 0,0 in coordinates. For me to make this much easier to implement, is it possible to change the top left starting point of the GUI so that, when i scroll, it moves to say, 0,100 and next scroll, 0,200 etc. How could I do this? Thanks
Rather than changing the x,y position of the object itself, or changing the reference co-ordinate of SDL (which cannot be done), you can instead create offset variables.
For example, create an SDL_Point called ViewPointOffset:
SDL_Point ViewPointOffset;
The best practice is to put this in your window class (if you have one), or even better, a Camera class that is a member of the window class.
Then, when you're drawing, just subtract the offset from the x and y co-ordinates that you're drawing:
void draw(SDL_Renderer* renderer, const SDL_Point ViewPointOffset, SDL_Texture* tex, const SDL_Rect* srcrect, const SDL_Rect* dstrect){
SDL_Rect* drawrect;
drawrect->w = dstrect->w;
drawrect->h = dstrect->h;
drawrect->x = dstrect->x - ViewPortOffset.x;
drawrect->y = dstrect->y - ViewPortOffset.y;
SDL_RenderCopy(renderer, tex, srcrect, drawrect);
}
You can either create a second function, or attach a boolean to the input of that function, to allow you to ignore the offset; what if you have a GUI button that you don't want the offset to apply to, etc?
https://github.com/Helliaca/SDL2-Game is a small open source game using a similar method. You can find this code in base.cpp/.h
I made a function to move an borderless SDL window. I use SDL_MOUSEBUTTONDOWN to 'activate' the window movement and SDL_MOUSEBUTTONUP to 'deactivate' it. For some reason it does not just move like it should but instead moves way slower than my mouse and flickers if I moved it by a good distance.
I use SDL2, and I'm on windows 10.
My loop always updates my mouse position, and the function takes the mouse position reduces it by the last mouse position and then moves the window by that distance.
sPos moveClock(int event){
sPos temPos = setPos(0,0);
if(tempMoveVar==1){
temPos = setPos(gvMousePos.x-mPos.x,gvMousePos.y-mPos.y);
mPos = setPos(gvMousePos.x,gvMousePos.y);
}else if(event==-1){ //Mouse Down
mPos = setPos(gvMousePos.x,gvMousePos.y);
tempMoveVar=1;
}
if(event==-65){ //Mouse Up
tempMoveVar=0;
}
return temPos;
}
I just want the window to move 'with' the mouse while my mouse button is down, like you normally can move windows.
Rather than moving the window manually, I suggest using SDL_SetWindowHitTest:
int SDL_SetWindowHitTest(SDL_Window* window, SDL_HitTest callback, void* callback_data);
This function lets you specify what dragging specific pixels of a window does (possible actions are moving the window, resizing it, or doing nothing).
You should probably call this function once, after creating your window.
Parameters are:
SDL_Window* window speaks for itself.
SDL_HitTest callback receives a function that, when given coordinates of a pixel, determines what dragging this pixel does.
void* callback_data is described below.
You need to write a function to pass to callback. It has to have following return type and parameter types:
SDL_HitTestResult MyCallback(SDL_Window* win, const SDL_Point* area, void* data)
{
...
}
area->x and area->y are the coordinates of the pixel that's being checked. win is the window.
data will receive the same pointer you passed to callback_data when calling SDL_SetWindowHitTest. You can use this pointer to pass arbitrary data to your callback; or, if you don't need it, simply set it to 0.
Your callback should return one of the following:
SDL_HITTEST_NORMAL - no action.
SDL_HITTEST_DRAGGABLE - dragging this pixel moves the window.
SDL_HITTEST_RESIZE_* - dragging this pixel resizes a specific edge (or edges) of the window. (Here * is one of: TOPLEFT, TOP, TOPRIGHT, RIGHT, BOTTOMRIGHT, BOTTOM, BOTTOMLEFT, LEFT).
I got a library to display pictures, lets call it PictureGLWidget, with:
class PictureGLWidget: public QGLWidget {
so PictureGLWidget extends QGLWidget. In PictureGlWidget the
void PictureGlWidget::mouseReleaseEvent(QMouseEvent* releaseEvent);
is already implemented.
I started an own project, lets say class MyMainWindow, where I just use a PictureGlWidget as a Pointerobject:
PictureGlWidget * myPictureGLWidget = new PictureGlWidget(...);
//..
layout->addWidget(myPictureGLWidget , 0, 1);
Here at this point, I already can see the PictureGlWidget and the corresponding picture in my MainwindowWidget. When I click in that PictureGlWidget, hold the mouse, I can move the picture (like 2D-scrolling), since it is much bigge than my little MainWindow.
Further on PictureGlWidget provides a function
bool PictureGlWidget::getPictureLocation(double& xPos, double& yPos);
which just tells me the Pictures center position, where I released the current clipping of the picture. Remeber my picture is much bigger than my little MainWindowWidget and thus much much more bigger than my PictureGLWidget. Imagine the picture has 4000x4000px (0,0 upper left). The PictureGLWidget is only to display lets say 800x800px. So the getPictureLocation() sets the center cooridinates of the current displayed picture part and it would return something like (400, 400), which might be somewhere in the midldle upper left corner.
I would like to grab the current displayed pictureparts (just a little part of that big picture) center position, after scrolling in that Widget and I released the mouse. I thought I do that by overwriting the
MyMainWindow::mouseReleaseEvent(QMouseEvent *event){ qDebug() << "Mouse released!"; }
method, but did not connected it anywhere yet. Currently it is not reacting on my mouseReleases and that text is not displayed.
The virtual protected methods in QWidget that you can override to react on some events don't need to be "connected". These are not Qt slots but classical functions Qt automatically calls when necessary.
As explained in Qt Event system doc, if the implementation PictureGlWidget::mouseReleaseEvent(QMouseEvent*) accept the event, it is not propagated to the parent widget. But you can install an event filter to your PictureGLWidget and receive events before they are sent to it.
PictureGlWidget * myPictureGLWidget = new PictureGlWidget(...);
layout->addWidget(myPictureGLWidget , 0, 1);
myPictureGLWidget->installEventFilter(this);
Then implements the right method in your main window:
bool MyMainWindow::eventFilter(QObject *object, QEvent *event)
{
if (object == myPictureGLWidget && event->type() == QEvent::MouseButtonRelease) {
QMouseEvent * mouseEvent = static_cast<QMouseEvent *>(event);
// Do what you need here
}
// The event will be correctly sent to the widget
return false;
// If you want to stop the event propagation now:
// return true
}
You can even decide if, after doing what you have to do, you want to stop the event, or send it to the PictureQLWidget instace (the normal behavior).
Doc:
http://doc.qt.io/qt-4.8/qobject.html#installEventFilter
http://doc.qt.io/qt-4.8/qobject.html#eventFilter
Do not forget the Q_OBJECT keyword in your MyGLwidget custom class declaration
I have 2 custom qgraphicsitems on a qgraphicsScene, rendered by a qgraphicsview. Now I want to be able to drag and drop one of the 2 items to the other kind. But which events should I reimplement for this? The documentation is a bit confusing on this.
also I want the qgraphicsitem to jump back to its original position if the user drags it to another area than the qgraphicsitem it should be dropped on.
As far as i know this is not implemented in the QGraphicsScene itself.
You must derive your own class from QGraphicsView or QGraphicsScene and then overload:
class MyGraphicsView : public QGraphicsView
{
Q_OBJECT;
protected:
virtual void mousePressEvent(QMouseEvent* event);
virtual void mouseMoveEvent(QMouseEvent* event);
virtual void mouseReleaseEvent(QMouseEvent* event);
...
private:
QGraphicsItem *currentDraggedItem;
};
QGraphicsView gives works with view/window coordinates while QGraphicsScene works with Scene coordinates.
Add code like:
void MyGraphicsView::mousePressEvent(QMouseEvent* event)
{
currentDraggedItem = itemAt(event->pos());
QGraphicsView::mousePressEvent(event);
}
void MyGraphicsView::mouseReleaseEvent(QMouseEvent* event)
{
QGraphicsItem *foundItem = itemAt(event->pos());
if(foundItem && currentDraggedItem &&
foundItem != currentDraggedItem)
{
// Handle DragDrop Here
}
QGraphicsView::mouseReleaseEvent(event);
}
This does the job for one QGaphicsScene. If you have two of them - the both have to know each other and you must translate coordinates from the one QGraphicsView to the other QGraphicsView. using mapTo...().
The key to this is checking the QGraphicsItems rect and seeing if they intersect.
So, when the mouse down is pressed on an item, store its current position. You can now move it and wait for the mouse release. On the release of the mouse button, check if the bounding rects of the items intersect with QRect::contains(const QRectF). If they do, then you've dropped one onto the other. If not, then animate the graphics item back to the previously stored position.
Just make sure that when you're checking the bounding rects for intersection that you're doing this with both of them in scene space coordinates. Either convert them, or use QGraphicsItem::sceneBoundingRect().
I'm learning about QPainter, and I've created a simple widget where each time the user clicks on the widget, a new circle appears at that point.
But Qt doesn't allow painting outside paintEvent, so each time I want to draw a new circle, I need to invalidate the widget area and redraw all the previous circles, too. That doesn't seem very efficient - what if there are hundreds or even thousands of elements.
It would be best if the previous circles weren't erased, and I just drew the new one on top of the widget. But on Qt I can't draw without first invalidating (and thus erasing) the previous content.
What is the recommended way of handling this situation in Qt?
The recommended way to handle that situation is to use a QGraphicsScene and QGraphicsView, and then populate the scene with QGraphicsItems. According to the docs, that is exactly what the framework is designed for.
In short, you would override QGraphicsScene::mousePressEvent(), and in the new method you would create a new QGraphicsEllipseItem.
There is no need to invalidate the entire widget. update() and repaint() can take coordinates that you want to repaint thus only re-drawing the part that changed.
void update ( int x, int y, int w, int h )
void update ( const QRect & rect )
void update ( const QRegion & rgn )
void repaint ( int x, int y, int w, int h )
void repaint ( const QRect & rect )
void repaint ( const QRegion & rgn )