How to get widgets (QGraphicsProxyWidget) of QGraphicsScene? - c++

I am making use of a QGraphicsScene and I am adding regular widgets (QLineEdit, QComboBox, etc.) to it via implicitly created QGraphicsProxyWidget objects:
m_pLineEdit = new QLineEdit("", 0);
m_pProxy = m_pGraphicsScene->addWidget(m_pLineEdit);
I am currently searching for a way to later retrieve those widgets from the scene again for processing, but am not able to find one.
I tried the following approaches already:
Since I cannot pass the graphics scene as parent to the widget constructor, retrieving the widgets via m_pGraphicsScene->findChildren(QLineEdit*) does not work, since there is no direct relation.
The graphics scene does have a QGraphicsSceneBspTreeIndex child, but that is not part of the official Qt API and therefore relying on it cannot be the way to go.
Bottom-line: How can I get all the QGraphicsProxyWidget objects from a Qt graphics scene? Can this be done in the Qt standard or do I have to subclass QGraphicsScene and try to manage the widgets myself?

Bottom-line: How can I get all the QGraphicsProxyWidget objects from a Qt graphics scene?
Get the list of all the items in the scene via scene->items() , then check if they're of the right class:
// Horrible C++98 code which doesn't even feature algorithms
QList<QGraphicsItem *> items = scene->items();
foreach (QGraphicsItem *item, items) {
QGraphicsProxyWidget *w;
if (w = qgraphicsitem_cast<QGraphicsProxyWidget *>(item)) {
use(w);
}
}
However, I'd like to stress that you should really keep track of the items you put in the scene. (At least, the ones you're interested in using afterwards). Walking over the scene and retrieving the items like this seems very fragile, and it's a signal of poor code quality and poor design. Note that you have the proxy returned by the addWidget call, just save it somewhere.

Just after posting the question I by chance found a solution in the Qt source code. The widget proxies are treated as regular QGraphicsItem internally and can be casted via qgraphicsitem_cast:
QList<QGraphicsItem*> graphicsItemList = m_pGraphicsScene->items();
foreach(QGraphicsItem* pGraphicsItems, graphicsItemList)
{
QGraphicsProxyWidget* pProxy = qgraphicsitem_cast<QGraphicsProxyWidget*>(pGraphicsItems);
if(pProxy)
{
QLineEdit* pLineEdit = qobject_cast<QLineEdit*>(pProxy->widget());
if(pLineEdit)
// do stuff
}
}
If someone knows a simpler/faster method, I'd be happy to hear about it. Until then I will use the approach outlined above.

Related

How to display a scrollable list with a substantial amount of widgets as items in a Qt C++ app?

Goal: To have a scrollable list of custom widgets amounting hunderts of thousands (and possibly more) in a Qt5 C++ application under Windows 7, 10.
Problem: The program stops responding after minimizing the window to the task bar and restoring it again. It doesn't crash though. The CPU usage constants 25%. The GUI doesn't become responsive again even after several minutes of waiting. Furthermore, a large amount of memory is being consumed in general (more than 200M), which I think is too much even for 100k QLabels (aprox 2k per QLabel).
Here are some suggested solutions to a similar problem, which I don't find suitable for my case.
Example: The following example illustrates the problem. For the sake of the demonstration a list of QLabels is used, but it could be any class derived from QWidget.
MainWindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QScrollArea>
#include <QVBoxLayout>
#include <QLabel>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
};
#endif // MAINWINDOW_H
MainWindow.cpp
#include "MainWindow.h"
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
{
QScrollArea *scrollArea = new QScrollArea(this);
QFrame *frame = new QFrame();
QVBoxLayout *l = new QVBoxLayout(frame);
int N = 121004;
scrollArea->setWidget(frame);
scrollArea->setWidgetResizable(true);
for (int n = 0; n < N; n++) { l->addWidget(new QLabel(QString::number(n), this)); }
resize(640, 480);
setCentralWidget(scrollArea);
}
I have some bad news and some good news:
The bad news: You cannot do this with Qt Widgets directly.
The good news: There's a way to do it, that's independent of the number of items you have in the list (even billions), but you need to give yourself the time to learn how to do this.
So, first thing: QScrollArea is SO not the way to do this. The correct way to do this is using the Model/View/Controller programming paradigm. The data model that has the information to be displayed has to be completely separated from the view, so that Qt could only worry about displaying the items the user is trying to view. Think about it: If you have a billion elements to put in that list, does that mean the user has to see them all at once? Does that mean Qt has to render them all? In your code, that's what you're asking Qt to do. Are you surprised it's slow?
Advice Nr. 1: Read how Qt manages Model/View programming, then choose the correct viewing tool. I suggest QListView for what you described. QTableView will make things easier for you if you can put things in a table.
The control on the list is done through delegates. A delegate is the class responsible for drawing widgets in the view. The default will just do text vs icons.
Advice Nr. 2: Forget about creating Qt widgets for every element. I just finished answering another guy's question on why this won't work, even when using delegates. Take a look at Qt Torrent Example to see how controls are drawn there.
What you can do is draw controls, not widgets. This is because every widget you create has to go to the main event loop in Qt, which will make your program slow (and you experienced that already). If you loop from one to million just to add numbers it'll take a significant amount of time. Do you really want Qt's event loop to loop over all your widgets to process every one of them?
Advice Nr. 3: Start simple! You seem to have a lot to do. Start with model/view, then add a delegate that will paint a custom control, then expand it. Give yourself the time to learn all this.
Good luck!
I have implemented a list widget that can show billions of items with an arbitrary number of widgets per item without any performance issues. Unfortunately, I can not share the code.
It is implemented on top of QAbstractScrollArea, and does not use Qt's model/view framework. It only deals with keeping track of the range of items that are in view, calling the appropriate draw function on those items, and keeping track of the overall height of all items combined. That's it.
Every item may have one associated widget. This widget may be arbitrarily complex. Item widgets are made visible when the item is in view. In my implementation, items usually lazily create the widgets, which is one of the reasons why this is fast. In case you have a billion items for example, only a very small subset of those will ever be in view, so it would be a waste to spend any effort in constructing widgets for these items.
Since every item is responsible for the way it looks, this gives a lot of flexibility in terms of what is possible to display with such a very generic list widget.

How to synchronize movement between multiple QT graphics view widgets

I am trying to build a tool that can compare images side-by-side. To do this, I have used three graphics view widgets in QT. They all zoom at the same rate, but I cannot seem to find anything online about how to link the movement of the images (aka - the drag and drop with the mouse). Is this feature even possible to construct?
Here is what the GUI looks like:
Image compare GUI
Well, every time I used a QGraphicsView, I ended up subclassing it cause it didn't provide everything I needed "as public".
In your case though, I believe this is as simple as doing:
connect(graphicsView1->verticalScrollBar(), SIGNAL(valueChanged(int)),
graphicsView2->verticalScrollBar(), SLOT(setValue(int)));
connect(graphicsView2->verticalScrollBar(), SIGNAL(valueChanged(int)),
graphicsView1->verticalScrollBar(), SLOT(setValue(int)));
If you prefer, call some slots to perform more than a 1-to-1 action:
void MyClass::scrollGraphicsView1(int value)
{
graphicsView1->verticalScrollBar()->setValue(value);
}
void MyClass::scrollGraphicsView2(int value)
{
graphicsView2->verticalScrollBar()->setValue(value);
}

Prevent QGraphicsItem::itemAt() On a "Background Item"

I am trying to set up a GUI using plugins. In my current phase I need the user to click and specify locations of points.
The plugin architecture I have setup requires the plugin to return QGraphicsItem that the plugin wishes to use. In the main program (which future plugin writers won't have access to), I set a background image to guide clicks. The plugins can sneakily access the scene() using an itemChange() and installing an eventFilter() on the scene being set. This allows for the plugins to have access to scene clicks and process whatever information it needs. However, the background picture is being returned by itemAt() calls.
I wish to prevent this background Pixmap from returning with an itemAt() for all future plugins. Is this possible?
Tried:
setEnabled(false); //No success
This doesn't answer your question directly, but it should solve your problem: I suggest reimplementing QGraphicsScene::drawBackground() and drawing your pixmap there. That's what the method is for.
A similar approach would be to put the pixmap in a resources file, then use style sheets to set it as the background of either the QGraphicsView or its viewport widget (I think these have slightly different effects).
To make an item invisible to itemAt() (or any of the other collision detection methods) you can make the boundingRect() method return a QRectF with zero width and height.
This though has the side effect of making any of the partial viewport update modes of QGraphicsView malfunction, because QGV doesn't have any idea where the item is going to paint itself. This can be remedied by having QGV do full updates on every repaint (setViewportUpdateMode(QGraphicsView::FullViewportUpdate)). Depending on your scene this might be an acceptable compromise. If you're using the OpenGL graphics backend the viewport updates will always be full updates anyway.
After so many years, here is the answer.
Make your own subclass of QGraphicsItem and override the shape() method
QPainterPath shape() const override
{
return {};
}
Ref to docs: https://doc.qt.io/qt-6/qgraphicsitem.html#shape
The shape is used for many things, including collision detection, hit tests, and for the QGraphicsScene::items() functions.
So this will effectively make the item invisible for itemAt() and others.
And you still have the boundingRect() which is used to filter what items should be painted.

Ignore mouse events over transparent parts of an svg image in qgraphicsview?

I am working on a graphics view (using C++ and Qt) which contains quite a few svg images. I intercept clicks on them, but i'd like not to receive events (or to be able to ignore them) when mouse is over transparent parts of svg items.
Is it possible ?
Should svg files be specifically designed for such use ?
Is there some hidden Qt option i have not (yet) heard of ?
There's a CSS property which can be applied to SVG elements, pointer-events, though the default for this is visiblePainted:
The given element can be the target element for pointer events when the ‘visibility’ property is set to visible and when the pointer is over a "painted" area. The pointer is over a painted area if it is over the interior (i.e., fill) of the element and the ‘fill’ property has an actual value other than none or it is over the perimeter (i.e., stroke) of the element and the ‘stroke’ property is set to a value other than none.
Which would indicate that Qt graphics view doesn't support it.
Having no other choice but to find out the hard way the answer to my question, here is what i did :
looked for mousePressEvent definition in QGraphicsSvgItem.cpp. Found none.
looked for mousePressEvent definition in QGraphicsItem.cpp (ancestor of QGraphicsSvgItem). The method exists but no relevant action could be found there.
looked for mousePressEvent calls in QGraphicsItem.cpp. Found myself reading the code of QGraphicsItem::sceneEvent(), dispatcher of mouse events for a Qt graphics scene. There does not seem to be any sort of differentiating different zones of graphic items.
Hence the sad answer : Qt does not permit such a behaviour.
To complete other answers:
When re-implementing events, it is important to call the base-class event for default cases, if not, the event transparency over non-painted parts is lost.
E.g.
virtual void mouseReleaseEvent( QGraphicsSceneMouseEvent *e) override
{
if (/* any condition*/)
{
// Do some specific behaviour
}
else QGraphicsItem::mouseReleaseEvent(e);
}

Resizing a QGraphicsItem to take up all space in a QGraphicsView, problems when window is resized

I have a QGraphicsItem (actually, a QDeclarativeItem) and I want it to take up the entire visible space of the QGraphicsView (again, its actually the derived QDeclarativeView class) to which it was added. Normally, you can use QDeclarativeView::setResizeMode(QDeclarativeView::SizeRootObjectToView) and QDeclarativeView will automatically resize the root object to fit the view.
The problem I'm having is that I am manually creating the root widget using
QDeclarativeComponent component(declarativeView->engine(), QUrl(qml));
QDeclarativeItem* object = qobject_cast<QDeclarativeItem*>(component.create());
if (object)
{
declarativeView->scene()->addItem(object);
...
instead of letting QDeclarativeView do it automatically by calling setSource(). The reason I'm doing this is because I want to swap the QML scene when certain events occur, but I don't want to destroy the previous scene. Calling setSource() deletes all items that were added before setSource() is called. So, instead I'm creating the "root object" myself and adding it to the scene manually.
I am using the windows resizeEvent to resize my QDeclarativeItem, like this:
void AppWindow::resizeEvent (QResizeEvent* event)
{
QDeclarativeItem* object = sceneCache.value(sceneName, 0);
if (object)
{
object->setWidth(declarativeView->viewport()->width());
object->setHeight(declarativeView->viewport()->height());
}
}
This does work! But, its not very pretty. If you resize the window quickly, the QDeclarativeItem is not resized quickly enough and you briefly see a gray background before it catches up and resizes it. Its also not very smooth.
This only happens if I have a complex item being resized (in my case, its a QWebKit widget). It works fine for simpler items. The thing is, however, that if I let QDeclarativeView do it, I have neither of these problems: its resized correctly and smoothly.
I imagine this is not specific to the QtDeclarative stuff, but rather QGraphicsView, though maybe I'm wrong there.
Does anyone have any ideas?
I have solved my problems.
The problem seems to be with QML's WebView element. I switched it out for a QGraphicsProxyWidget registered from C++, with the widget set to a normal QWebView and all the problems disappear. It now behaves exactly how I expected the WebView to behave.
The downside is that I need to manually expose the QWebView signals/slots/properties to QML. (EDIT: I've solved this by having a read-only property called object, which returns the set widget. Since QObjects are automagically converted to javascript accessible objects, I can access the web view, for exmaple, like this: object.url = 'http://google.com'. For convenience, I also invoke an init javascript function ont he QML object, if it exists, so I simply set the web view up there - works great!)
Another change I made since I asked the question is to move the "root object" switching from C++ into QML itself, by creating a fullscreen Rectangle with a C++ -callable javascript function that uses Qt.createComponent() to load the "actual" QML file and anchor it with anchors.fill. This way the logic (and caching of objects) is done in QML, instead of C++, so I can simply setSource(), let Qt handle resizing and such and I work purely in QML (for this part of the app, anyway).
I am not sure if this will get rid of the flickering but you can use fitInView in your resizeEvent():
declarativeView->fitInView(object, Qt::IgnoreAspectRatio);