QGraphicsView freeze after scaling - c++

I am on Windows with Qt 4.7.
I have a bug in my qgraphicsview/qgraphicsscene whereby after scaling the view, something is causing a "hang" which appears to be a recursive/infinite loop in the event loop.
I apologize for not having a simple, complete code example; I will describe the symptoms and situation as best I can, and I'm hoping somebody will have an idea or two I can try:
I have subclassed QGraphicsView, QGraphicsScene, and QGraphicsItem. For example, wheelEvent in QGraphicsView has been subclassed to do the following:
scaleView(pow((double)2, -event->delta() / 240.0));
where
void MyGraphicsView::scaleView(qreal scaleFactor)
{
qreal factor = transform().scale(scaleFactor, scaleFactor).mapRect(QRectF(0, 0, 1, 1)).width();
if (factor < 0.07 || factor > 100)
return;
scale(scaleFactor, scaleFactor);
}
The constructor of my graphicsview subclass is as follows:
MyGraphicsView(QWidget *parent) :
QGraphicsView(parent)
{
setRenderHint(QPainter::Antialiasing);
setTransformationAnchor(AnchorUnderMouse);
setCacheMode(QGraphicsView::CacheNone);
setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
setDragMode(QGraphicsView::ScrollHandDrag);
}
My subclass of QGraphicsItem constructor is as follows:
MyGraphicsItem(QGraphicsObject *parent) :
QGraphicsObject(parent),
_colour(qrand() % 256, qrand() % 256, qrand() % 256),
_collapsed(false)
{
setFlag(ItemIsMovable);
setFlag(ItemSendsGeometryChanges);
setCacheMode(DeviceCoordinateCache);
setZValue(-1);
expandedPosition(this->pos());
}
I can isolate the problem to a call to the wheelEvent, or other event which calls QGraphicsView::scaleView.
If there is a MyGraphicsItem visible, then trying to paint (or something caused by painting) causes the whole system to hang. I think this is a loop in the event loop, but to get out of it (ctrl+alt+del to bring up "Start Task Manager", then esc to cancel) I lose the loop I was in.
The loop seems to be repeated calls to QGraphicsView::focusInEvent and focusOutEvent. I have overridden these and don't seem to have much luck in stopping the loop whatever I do with these functions.
Edit: this appears to be a red herring, a result of switching to breakpoints inside the debugger.
There is no issue if the view is not displaying a MyGraphicsItem.
Also, I can pan and repaint the view after doing the scale, but trying to resize the window or change focus causes the problem.
Similarly, if there has not been a call to scaleView, I have no problems.
Perhaps I am being too vague; but please fire away with any questions I can answer to help clarify. Any help appreciated!
Update:
So I seem to have got it down to something to do with a subclass of my subclass of QGraphicsItem.
I took a Node from the elastic nodes example, and put this in my scene. I can zoom in and out using my sceven and view, and no hang.
I can even make Node a subclass of MyGraphicsItem rather than QGraphicsItem, and again no hand.
As soon as I put my subclass of MyGraphicsItem in the scene, I see the issue.
Getting closer I hope!

Further Update:
I have found the cause of this. It seems to show that I missed an important memo somewhere.
I have a test graphics item that is NOT a MyGraphicsItem, but it is a QGraphicsLineItem, and this reproduces the problem.
I have overridden the paint method, and after a lot of elimination I have found that if I call setPen inside the paint method, my hang returns. e.g., my paint method is now:
void ConnectionItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
QWidget *widget)
{
QLineF myline = line();
QBrush brush(Qt::red);
QPen pen = this->pen();
pen.setBrush(brush);
**// this line results in the hang when the window is resized
setPen(pen);**
painter->setPen(pen);
painter->setBrush(brush);
painter->drawLine(myline);
}
So setPen (which will be QGraphicsLineItem::setPen) calls update() and then gets stuck in a loop calling QGraphicsLineItem::Paint().
Moral of the story: do not setPen or other paint related property inside the paint function!
I am not 100% clear why this is only an issue after scaling, as the paint routine is called quite happily prior to zoom.
Edit:
similarly
QPen pen = this->pen();
pen.setBrush(brush);
causes a problem
as does calling
prepareGeometryChange();
inside the paint routine

Related

Scaling the QGraphicsScene to fill whole QGraphicsView

I have found a few fixes for this issue, but none of seemed to be sufficient. I have a QGraphicsView displaying a QGraphicsScene. What I want to do is to scale the scene in order to fill the whole view. Also I want it to scale dynamically when the user will be resizing the window displaying the view. Is such a thing possible? If so I'd be glad if you could give me a short example on how should it be implemented. Thanks in advance.
You can make your custom class which inherits from QGraphicsView. You should reimplement resizeEvent( QResizeEvent *event ) in your custom QGraphicsView like:
void MyView::resizeEvent(QResizeEvent *event)
{
fitInView(0, 0, 500, 500,Qt::KeepAspectRatio);
QGraphicsView::resizeEvent(event);
}
This way the view will always display the whole scene. I.e. if the window size is changed and the graphicsView is resized, The scene gets scaled and you can see everything appropriately.

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.

Qt user resize event ends (stops)

I have a QWidget and i need to do some actions (refresh a picture in widget) when resize event ends. How can i catch this action?
I need to catch moment when user ENDs all his resize actions by releasing mouse button. It is not a good practice in my application to refresh image every pixel resized. It should calls only when mouse released and resize actions ends.
I am just tried to reimplement QMouseReleaseEvent to catch it, but it do not works when user presses on the border of widget to resize it. It means does not working in our situation.
Then i was tried to create my own QSizeGrip and insert it on the bottom of my widget, but reimplemented event QMouseReleaseEvent again did not work in it. Event did not generates any time user released mouse. I do not know why.
Anybody can help me with that problem?
Thanks in advance.
The timeout method is a decent idea, but if the user is resizing and then pauses for longer than the timer's interval then you end up not getting the true "user is done resizing the window" event. Setting the interval longer makes that situation less likely, but by doing that, you end up having a long delay between the time the user finished resizing and the time your function gets called. In my search for a solution, I found quite a few people solving it using the timer method, so apparently it's reliable enough for some use cases, but I think it's a bit hacky.
I like mhstnsc's idea, so after implementing it, I decided to add some code here that might be of use to someone trying to do something similar.
You could easily adapt it to catch the "user is done moving the window" event by making a m_bUserIsMoving flag and overriding "void MainWindow::moveEvent(QMoveEvent* pEvent)". I'm using it to save a config file whenever the user finishes resizing or moving the window, so that the last position will always be saved even if the app is killed in an unclean manner.
// constructor
MainWindow::MainWindow(QWidget* pParent, Qt::WindowFlags flags) : QMainWindow(pParent, flags)
{
m_bUserIsResizing = false;
qApp->installEventFilter(this);
}
// this will be called when any event in the application occurs
bool MainWindow::eventFilter(QObject* pObj, QEvent* pEvent)
{
// We need to check for both types of mouse release, because it can vary on which type happens when resizing.
if ((pEvent->type() == QEvent::MouseButtonRelease) || (pEvent->type() == QEvent::NonClientAreaMouseButtonRelease)) {
QMouseEvent* pMouseEvent = dynamic_cast<QMouseEvent*>(pEvent);
if ((pMouseEvent->button() == Qt::MouseButton::LeftButton) && m_bUserIsResizing) {
printf("Gotcha!\n");
m_bUserIsResizing = false; // reset user resizing flag
}
}
return QObject::eventFilter(pObj, pEvent); // pass it on without eating it
}
// override from QWidget that triggers whenever the user resizes the window
void MainWindow::resizeEvent(QResizeEvent* pEvent) { m_bUserIsResizing = true; }
It's slightly more complicated than the timer, but more robust.
I've do it in this way:
inherit my class from QWidget
define private variable int timerId = 0
overload QWidget::resizeEvent and QObject::timerEvent
void MapLoader::resizeEvent(QResizeEvent *){
if (timerId){
killTimer(timerId);
timerId = 0;
}
timerId = startTimer(5000/*delay beetween ends of resize and your action*/);
}
void MapLoader::timerEvent(QTimerEvent *te){
/*your actions here*/
killTimer(te->timerId());
timerId = 0;
}
Mouse events on windows decoration are managed by the underlying window system, this is why you can't catch them as you tried.
I had the same issue once, the solution I chose was to (re)start a singleshot QTimer on each resize event, and only process the update after the timer interval elapsed. Not very sexy but I did not find any other workaround..
Another way would be to install an event filter for the app and get all the events of the application, trap mouse press and mouse release and do not update the window in between .
"Installing an event filter on QCoreApplication::instance(). Such an event filter is able to process all events for all widgets, so it's just as powerful as reimplementing notify(); furthermore, it's possible to have more than one application-global event filter. Global event filters even see mouse events for disabled widgets. Note that application event filters are only called for objects that live in the main thread."
My Qt app uses image windows and does complex layered rebuilds, which can take some time even on a very fast machine. So not having the window redraw with every change in the window frame size was important to me so the response to the window frame resize would not be laggy.
So I solved it this way:
In my image window, I have enabled mouse tracking:
setMouseTracking(true);
Then, in the window class, I have a boolean, puntme; this is set when a resize event is caught:
bool puntme;
Then, in the mousemove event:
void imgWindow::mouseMoveEvent(QMouseEvent* event)
{
if (puntme)
{
puntme = false;
needRebuild = true;
update();
}
...
Basically, what this does is as soon as the user moves the mouse over the window -- which is a pretty natural thing for them to do if they were just resizing it -- then the window redraws with the new size. It doesn't happen during the resize, because Qt isn't forwarding move moves then.
Instead, during the resize, I just scale up an already existing bitmap, which gives a crude approximation of the change in scale with necessarily handling the actual newly more-or-less available resolution.
Worst case, user resizes, moves away from the window, and leaves the crudely scaled bitmap in place until the come back to it, at which point it will duly update to the actual new displayed bitmap == scale/size conditions.
There's no perfect way - what is really needed here is for Qt to provide (user has stopped resizing window" message, but in lieu of that, this has been working well for me.

Dragging object around a QWidget in Qt

I am trying to make a custom widget in Qt Creator that supports dragging objects around. At its simplest form, the widget has a QRect (or any other shape), on which I can click and then drag it around the widget. Once I release the mouse button, the QRect should stop being dragged.
In my QWidget class, I have this method
void ImageArea::mouseMoveEvent(QMouseEvent *event)
{
QPoint mousePos = event->pos();
qDebug() << mousePos.x();
qDebug() << mousePos.y();
qDebug() << "---------";
}
that can get the coordinates of the mouse as the pointer is moved around the screen. I have tried updating member variables for x and y, and then painting the QRect via the paintEvent method, but this doesn't work.
Does anyone have any suggestions?
To get mouse move events, you must set the QWidget::mouseTracking property to true:
ImageArea::ImageArea( QWidget* p ) : QWidget( parent ) {
...
setMouseTracking( true );
}
Implement paintEvent(QPaintEvent *) to draw the object(s) at the positions indicated by the current value(s) of their corresponding member variables.
After you've changed the values of one or more member variables (in mouseMoveEvent or wherever), call this->update(). That will tell Qt that it needs to call your paintEvent method again in the near future.
That should be all you need to do.
Be sure to use the moveTo method to move the rectangle.
Setting the x,y position directly may affect the size of the rectangle.
I don't see what you aren't doing, based on your question.
Are you sure that the rectangles are in new positions when you paint them?
Maybe you are missing the update step Jeremy Friesner told to implement.
It seems that you are missing mouse button tracking.
The easy way might be to get the mouse button states from QApplication::mouseButtons(). Although it might be slightly less efficient.

QWidget's paintEvent() lagging application

i'm studying and modifying the fridge magnets example, and the last thing I've tried to do was to draw a few labels and lines that are supposed to be on the background.
After looking around trying to figure out how to draw the labels and lines, I learned that I could override QWidget's paintEvent() to do it. After I did it though, the application got laggy, and I found out that it was because paintEvent() was being called in a seemingly infinite loop.
Trying to figure out how to fix that, I moved the code that drew the labels and lines to the constructor of the class. Only the labels were drawn on the application though. After that, I left the labels in the constructor, but moved the code that drew the lines back to paintEvent(). It worked, the lines were drawn as expected, and paintEvent() was called only when dragging stuff around.
Why were the lines not being drawn on the constructor, and why paintEvent() got into an infinite loop?
Here's the snippet that's supposed to draw the labels and lines:
QPen pen(Qt::lightGray, 0, Qt::SolidLine, Qt::SquareCap, Qt::RoundJoin);
QPainter paint(this);
paint.setPen(pen);
int scale = 20;
for(int x=0; x<25; x++){
QString timetext= "0"+QString::number(x)+":00";
QLabel *time= new QLabel(timetext,this);
time->move(x*scale,2);
time->show();
paint.drawLine(x*scale,12,x*scale,400);
}
You are adding Objects to the Widget Tree during paintEvent(). That is deemed to fail. The Qt scheduler for damage&drawing will see that a new child has to be drawn and try to manage that and likely the loop is the result. If you override paintEvent(), do all the painting in the same object! Golden rule: paintEvent() is only for painting! Not for creating objects or anything else.
Do it like this:
QFont font(painter.font());
font.setBold(true);
painter.setFont(font);
painter.fillRect(rect(), Qt::black);
painter.setPen(Qt::white);
painter.drawText(rect(), Qt::AlignCenter, tr("White text on dark background. Awesome."));
Why were the lines not being drawn on the constructor ?
I think they were, but they were "erased" by the next call to paintEvent() in which you didn't draw the lines anymore...
Why paintEvent() got into an infinite loop?
I think it could be related to your time->show(); which is called 25 times everytime paintEvent is called... I'm not sure about that but, since time as the widget as parent, when you call "show", maybe it calls "show" on its parent, therefore triggering paintEvent.... You know what I mean...
Since, Ypnos gave you a solution, I refer to him :)