Find screen position of a QGraphicsItem - c++

Use case: This should be a fairly common problem. In a normal QMainWindow with QMdiArea lives an mdiChild with a QGraphicsView. This view displays a QGraphicsScene with QGraphicsItems inside. A right-click at one of these items selects (focusses) the item and opens a context menu, which is conveniently placed at the screen coordinates QGraphicsSceneMouseEvent::screenPos(). This is working as expected.
Now I'd like to show the same context menu when the user presses a key (e.g. Qt::Key_Menu). I know the selected (focussed) item, I know the view which displays the scene.
So my question is:
What is the correct way to get the position (in global, screen coordinates) of the visible representation of a QGraphicsItem within a scene?
Here is what's not working:
QGraphicsItem *item = ...; // is the currently selected item next to which the context menu shall be opened
QGraphicsScene *scene = ...; // is the scene that hosts the item
QGraphicsView *graphicsView = ...; // is the view displaying the scene, this inherits from QWidget
// get the position relative to the scene
QPointF sp = item->scenePos();
// or use
QPointF sp = item->mapToScene(item->pos());
// find the global (screen) position of the item
QPoint global = graphicsView->mapToGlobal(graphicsView->mapFromScene(sp));
// now
myContextMenu.exec(global);
// should open the context menu at the top left corner of the QGraphicsItem item, but it goes anywhere
The doc says:
If you want to know where in the viewport an item is located, you can call QGraphicsItem::mapToScene() on the item, then QGraphicsView::mapFromScene() on the view.
Which is exactly what I'm doing, right?
Just stumbled upon a thread in a german forum that hints to:
QGraphicsView *view = item->scene()->views().last();
or even nicer:
QGraphicsView *view;
foreach (view, this->scene()->views())
{
if (view->underMouse() || view->hasFocus()) break;
}
// (use case in the forum thread:) // QMenu *menu = new QMenu(view);
Using that might allow a more generalized answer to my question...

I found a working solution.
The QGraphicsItem must be visible on the screen.
(Probably if it's not visible because the view shows some other point of the scene, one could restrain the point to the view's viewport's rect.)
// get the screen position of a QGraphicsItem
// assumption: the scene is displayed in only one view or the first view is the one to determine the screen position for
QPoint sendMenuEventPos; // in this case: find the screen position to display a context menu at
QGraphicsItem *pFocusItem = scene()->focusItem();
if(scene() != NULL // the focus item belongs to a scene
&& !scene()->views().isEmpty() // that scene is displayed in a view...
&& scene()->views().first() != NULL // ... which is not null...
&& scene()->views().first()->viewport() != NULL // ... and has a viewport
)
{
QGraphicsView *v = scene()->views().first();
QPointF sceneP = pFocusItem->mapToScene(pFocusItem->boundingRect().bottomRight());
QPoint viewP = v->mapFromScene(sceneP);
sendMenuEventPos = v->viewport()->mapToGlobal(viewP);
}
if(sendMenuEventPos != QPoint())
{
// display the menu:
QMenu m;
m.exec(sendMenuEventPos);
}
It is important to use the view's viewport for mapping the view coords to global coords.
The detection of the context menu key (Qt::Key_Menu) happens in the keyPressEvent() of the "main" QGraphicsItem (due to the structure of my program).

The code seems to be correct. But there might be some problem with the creation of the context menu.
Have you set the parent of the QContextMenu to MainWindow (or something of that sort in your application)??
I think that might be the problem in your case.
Good Luck!!

Just a stab in the dark but have a look at this http://www.qtcentre.org/threads/36992-Keyboard-shortcut-event-not-received.
On looking through the Qt documentation, it seems the use of QGraphicsView may cause some exceptional behaviour with regards to shortcuts.
It looks as if there might be a normative way of achieving the result you desire.
Depending how you are implementing your context menu, shortcuts and QGraphicsView, you might need to set the Qt::ContextMenuPolicy for the QGraphicsView appropriately and build and call the menu differently.
I'm quite interested in this question as I will need to do something quite similar shortly!

Related

Add some QWidget over a QOpenGLWidget

How do I add a QButton on the bottom right corner of a QOpenGLWidget?
The geometry property holds the geometry relative to it's parent excluding frame.
Calculate the position for your child widget relative to the parent widget geometry.
And then set the geometry using setGeometry.
A rough pseudo code is below, (Which is untested and just for Idea. The geometry calculation also may not be correct. But gives you an idea to achieve your goal).
Look into comments for details.
//YOUR OPENGL WIDGET
QOpenGLWidget *pOpenGL = new QOpenGLWidget(<<PARENT WINDOW>>, <<FLAGS>> );
//THE BUTTON YOU ARE TRYING TO ADD. ESTABLISH PARENT CHILD RELATION
QPushButton *pButton = new QPushButton(pOpenGL);
//THIS STEP IS IMPORTANT TO SET THE LOCATION
//CALCULATE THE GEOMETRY POSITION RELATIVE TO PARENT WIDGET
//JUST FOR YOUR IDEA. MAY NEED TO DO SOME PROPER CALCULATIONS
pButton->setGeometry(pOpenGL->x(),pOpenGL->y()-(pOpenGL->height()-20),10,20);
//THEN SET THE CENTRAL WIDGET
setCentralWidget(pOpenGL);
You can use the function move :
QOpenGLWidget *openglWdg = new QOpenGLWidget();
QPushButton * btn = new QPushButton(openglWdg);
btn->move(0 , 0);

Placing QWidgets at specified coordinates?

Context: I'm making Risk (the popular board game) in C++/Qt, and I've run into a problem. I decided to make the map interactive by placing buttons on every country, which could then be clicked on. After some experimenting, I've subclassed QGraphicsPixmapItem for the buttons, and stuck them inside a QGraphicsScene and a QGraphicsView. I've made the world map a background image via CSS, so that buttons could be overlaid without much hassle.
My problem: I want to place those buttons at specific coordinates. (If it matters, those coordinates would be absolute.) All of the interfaces that I've made so far, I've done in code - I'm not familiar with the Qt Designer, so I'm looking for a function or set of functions that'd let me place my buttons (more or less) where I want them.
What I've tried: I looked in the documentation, but couldn't find a function that let me control where items were placed, just ones that organized items in various ways - columns, horizontal boxes, vertical boxes, etc.
When I designed QWidgets before, I'd done so by placing buttons, other widgets, etc. in QLayouts, but there don't seem to be layouts that allow me the control I'd like. The only one I can see that'd do something similar, is QGridLayout, and experiments with that so far haven't worked the way I wanted them to - the buttons don't get anywhere near the edges of the map, no matter how many columns or rows I add.
The easiest solution would be giving up and placing the buttons beside the map, of course, but that's a last-ditch solution.
Thanks in advance.
EDIT: Added example source code, for clarity.
QHBoxLayout* layout = new QHBoxLayout;
TerritoryButton* test = new TerritoryButton(QPixmap("img.png"));
TerritoryButton* test2 = new TerritoryButton(QPixmap("img.png"));
QGraphicsScene* scene = new QGraphicsScene;
QGraphicsScene* scene2 = new QGraphicsScene;
scene->addItem(test);
scene2->addItem(test2);
QGraphicsView* view = new QGraphicsView(scene);
QGraphicsView* view2 = new QGraphicsView(scene2);
layout->addWidget(view);
layout->addWidget(view2);
setLayout(layout);
setFixedSize(1000, 512);
QGraphicsPixmapItem inherits QGraphicsItem, so you can call setPos(x, y) (after inserting the pixmap item into the scene with addItem).
void QGraphicsItem::setPos(const QPointF &pos)
Sets the position of the item to pos, which is in parent coordinates. For
items with no parent, pos is in scene coordinates.
The position of the item describes its origin (local coordinate (0, 0)) in parent coordinates.

How does one retrieve selected area from QGraphicsView?

I need my QGraphicsView to react on user selection - that is, change display when user selects area inside it. How can i do that?
As far as i can tell, selection in Qt Graphics framework normaly works through selection of items. I haven't found any methods/properties that touch on selected area, save for QGraphicsVliew::rubberBandSelectionMode, which does not help.
After some going through documentation, i found different solution.
In QGraphicsView there is a rubberbandChanged signal, that contained just the information i wanted to use. So, i handled it in a slot, resulting in the handler of following form:
void
MyImplementation::rubberBandChangedHandler(QRect rubberBandRect, QPointF fromScenePoint, QPointF toScenePoint)
{
// in default mode, ignore this
if(m_mode != MODE_RUBBERBANDZOOM)
return;
if(rubberBandRect.isNull())
{
// selection has ended
// zoom onto it!
auto sceneRect = mapToScene(m_prevRubberband).boundingRect();
float w = (float)width() / (float)sceneRect.width();
float h = (float)height() / (float)sceneRect.height();
setImageScale(qMin(w, h) * 100);
// ensure it is visible
ensureVisible(sceneRect, 0, 0);
positionText();
}
m_prevRubberband = rubberBandRect;
}
To clarify: my implementation zooms on selected area. To that effect class contains QRect called m_prevRubberband. When user stop selection with rubberband, parameter rubberBandRect is null, and saved value of rectangle can be used.
On related note, to process mouse events without interfering with rubber band handling, m_prevRubberband can be used as a flag (by checking it on being null). However, if mouseReleaseEvent is handled, check must be performed before calling default event handler, because it will set m_prevRubberband to null rectangle.
You can use qgraphicsscenemouseevent.
On MousePress save the current position and on MouseRelease you can compute a bounding rect using the current position and the MousePress position.
This gives you the selected area.
If you need custom shapes you could track the mouse movement (MouseMove) to get the shape.
An Example that uses qgraphicsscenemouseevent can be found here.

How to control the Widgets in QGridLayout that should be painted in GUI?

In my application I have a QGridLayout which covers the majority of the Window. In it I have added a sequence of QLineEdit & QLabel objects. Currently when the no of QLineEdit objects > 500 && QLabel objects > 500 the GUI is significantly slow & for greater values does not operate properly. Also most of these widgets are not visible in the window, they need to be scrolled to be viewd.
Since I am adding so many widgets in the grid layout (by looping & calling repaint after the loop) the painting takes lots of time.
So I have an idea for the solution that even though my widgets are added in the Grid Layout not everybody are painted. I want to have a rectangle within which all widgets are painted & the co-ordinates of the rectangle will be updated whenever the the window is scrolled. But I dont know how to do this. So I wanted to know is it possible to do that?
And if possible please add a small sample code so that I can understand how to implement that.
Thank You.
UPDATE : Adding an image to depict the sitatuion.
Black Rectangle = QGridLayout say myGid.
Red Rectangle = Bounding Rectangle which is approximately same size as Main Window of my Application.
Green Rectangle = Widgets in myGrid.
Green Rectangle filled with yellow = Widgets shown in Main Window (only these widgets should be considered for call to repaint), rest of the unfilled rectangles are widgets present in myGrid but not be considered for call to repaint.
Thus when I scroll in my main application, the co-ordinates of red rectangle are updated & all the widgets bounded by it are considered for repaint.
I hope I made the problem simple to understand.
I understand you don't want to discard your code. I'd try one of these, starting from the easiest:
Are you using a QScrollArea or are you simulating it using scroll bars? The QScrollArea probably already discards paint events to children widgets that are off the viewport. Assemble the grid off-screen. Otherwise, Qt will recalculate and repaint the layout every time you add a new widget. (Here is a complete example.)
QWidget* widget = new QWidget(); // This is an invisible widget.
for (int i = 0; i < 100; i++) {
for (int j = 0; j < 100; j++) {
QLineEdit* lineEdit = new QLineEdit();
lineEdit->setText(QString("I am the line edit at (%1, %2)").arg(i).arg(j));
layout->addWidget(lineEdit, i, j);
if (j % 10 == 0) {
// Do not block the UI thread while the UI is being assembled.
qApp->processEvents();
}
}
}
// The layout will be calculated only once, here:
scrollArea->setWidget(widget);
widget->show();
If that doesn't work, create an event filter that has a reference the visible rectangle. Event filtering is a useful technique in which you can intercept events targeted at one or more widgets and decide if they should be discarded before being handled.
In your case, when you intercept a QPaintEvent, check if the target widget intersects the visible rectangle. If it does, pass the event along to the target widget. If it doesn't, discard the event.
I don't know the specifics of how you scroll your UI, so I leave the calculating the visible rectangle up to you. The event filter code would be something like this.
bool MyClass::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::Paint) {
if (QWidget* widget = qobject_cast<QWidget*>(object)) {
QRect visibleRectangle = somehowGetVisibleRectangle();
if (visibleRectangle.intersects(widget->geometry())) {
return false;
} else {
// Returning true means "drop this event."
return true;
}
}
}
// Assuming MyClass extends QWidget. Adjust as necessary.
return QWidget::eventFilter(obj, event);
}
As a last resort, relayout your UI using QGraphicsScene, QGraphicsWidget, QGraphicsGridLayout, and QGraphicsView. The scene graph might be better at discarding unnecessary UI repaints.
First of all. Are you sure you are solving your problem in the right way? Perhaps you will be happier with QTableWidget? Its cells can be editable and then QTableWidget will take care of creating and maintaining QLineEdit for the cell that is being edited.

QGraphicsItem::SetCursor Won't Deselect

All,
I have a QGraphicsEllipseItem with setFlags(Qt::ItemIsSelectable | Qt::ItemIsMovable). This allows me drag and move ellipses quickly in a QGraphicsView.
I decided to be fancy, and setCursor(Qt::OpenHandCursor) to let the user know they can move it by clicking. However, now it won't let go when I let go of the left mouse button? What am I doing wrong?
Example code: Custom QGraphicItem and Repaint Issues
Note: I removed the update() calls, and added prepareGeometryChange() calls.
Now modify the MakeNewPoint function:
QGraphicsEllipseItem * InteractivePolygon::MakeNewPoint(QPointF & new_point)
{
QGraphicsEllipseItem * result = 0;
result = new QGraphicsEllipseItem();
result->setPos(new_point);
result->setRect(-4, -4, 8, 8);
result->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable)
result->setCursor(Qt::OpenHandCursor); //Setting this removes my ability to let go of an item. NOTE: result is parented by this.
return result;
}
later:
QGraphicsEllipseItem * new_item = MakeNewPoint(bla);
new_item->setParent(this);
//add it to my QList<QGraphicsEllipseItem *> m_points;
I would like to note that my QGraphicsEllipseItem is parented by a custom QGraphicsItem. I don't change the parents/Custom Item cursor, only the ellipse's. I do not experience this problem with non parented ellipses...
Interesting result: So my class custom QGraphicsItem class (the parent of the ellipses) is a QObject so I can filter incoming mouse events from the scene. I did a setCursor(Qt::ArrowCursor) in my custom class's constructor... and here's where it gets interesting:
The eventFilter now catches (event->type() == QEvent::GraphicsSceneMouseMove) even if a mouse button isn't pressed down. If I don't have the setCursor call, that event only fires while a mouse button is pressed... thoughts?
Okay got it, here's what's happening, it's intuitive once you realize it:
When you set that a QGraphicsItem to a unique cursor, the QGraphicsView has to setMouseTracking(true) otherwise the QGraphicsScene will never know when to change the cursor (ie when it's over the graphics item with the unique cursor.) The mouse move events were affecting my ellipse.
Normally, the QGraphicsScene only gets mouse move events when a button is held down.